本地LLM编排器

基于FastAPI构建的本地LLM编排器,支持Gemma 4 26B模型,提供工具调用、文档搜索和图像分析功能,适用于NASA SUITS TSS2026任务。

  • by Astrowebdeving
  • Python
  • 本地部署
  • 更新于
在 GitHub 查看 Astrowebdeving/AIA_Ollama_Basic_Orchestrator

使用说明

LLM编排师

本地LLM编排器使用FastAPI构建,由Gemma 4 26B(MoE,4B活动参数,Q4_K_M)通过Ollama提供支持。具有本地工具调用、内置推理和MCP工具服务器,用于通过UDP进行NASA SUITS TSS2026遥测、参考文档搜索(文本+PDF)、通过base64图像注入进行直接视觉分析,以及EVA助手的Magic Leap 2耳机语音/运行时上下文。

快速开始

# Install dependencies (requires Python 3.12+)
uv sync

# Pull required Ollama models
ollama pull gemma4:26b           # Chat model (~17GB)
ollama pull qwen3-embedding:0.6b # Embedding model (needed if RAG is re-enabled)

# Configure TSS server connection (set to the IP/port of the running TSS instance)
# Edit .env:
#   TSS_UDP_HOST=<TSS server IP>
#   TSS_UDP_PORT=14141

# Run the orchestrator
uv run python main.py

编排器开始 http://0.0.0.0:13853.

端点

方法 路径 描述
职位 /chat 与MCP工具调用和推理的代理聊天
得到 /context 上次令牌使用情况明细 /chat 呼叫
得到 /health LLM后端连接、模型可用性、MCP工具数量

POST/聊天

// Request
{
  "messages": [
    {"role": "user", "content": "What is the EVA1 oxygen status?"}
  ],
  "stream": false
}

// Response
{
  "role": "assistant",
  "content": "EVA1: O2 94%, CO2 0.5%, HR 72 bpm, suit pressure nominal."
}

该模型使用Gemma 4的原生思维来推理请求,然后自主调用MCP工具(例如,通过UDP获取实时TSS遥测数据,搜索参考文档)。思维跟踪记录在服务器端,但从未流式传输到客户端。

配置

所有设置都是从环境变量中读取的(默认情况下)。创建 .env 要覆盖的项目根目录中的文件:

# LLM provider: "ollama" (default), "afm", "llamacpp"
LLM_PROVIDER=ollama
LLM_MODEL=gemma4:26b
LLM_API_BASE=              # auto-set per provider if empty

# Ollama host -- always localhost. If OLLAMA_HOST is set system-wide to 0.0.0.0
# (Ollama's server bind address), the orchestrator automatically remaps it to localhost.
OLLAMA_HOST=http://localhost:11434
OLLAMA_KEEP_ALIVE=30m
OLLAMA_PREWARM=true

# Tokenizer -- auto-resolved to local tokenizer_cache/ dir if pre-downloaded.
# Startup expects local tokenizer files for offline field use.
# TOKENIZER_NAME=google/gemma-4-26B-A4B-it

# Context management
MAX_CONTEXT_TOKENS=128000
SUMMARIZE_THRESHOLD=80000

# TSS2026 server (UDP telemetry -- used by the get_tss_state MCP tool)
# Set TSS_UDP_HOST to the IP of the running TSS instance.
TSS_UDP_HOST=10.206.64.189
TSS_UDP_PORT=14141
TSS_UDP_TIMEOUT=2.0

OLLAMA_PREWARM=true 在启动时发送一个空的Ollama生成请求,因此 聊天模型在第一个Unity/耳机请求之前加载。 OLLAMA_KEEP_ALIVE 在聊天请求中传递,以使模型在回合之间保持驻留。

提供程序默认值

提供程序 默认API基础 默认模型 默认标记器 注释
ollama http://localhost:11434 gemma4:26b google/gemma-4-26B-A4B-it 完整的Ollama SDK,原生思维+工具
afm http://localhost:9999 mlx-community/Qwen3.5-35B-A3B-4bit Qwen/Qwen3.5-35B-A3B OpenAI兼容(AFM/MLX)
llamacpp http://localhost:8080 gemma4 google/gemma-4-26B-A4B-it OpenAI兼容(火焰服务器)

离线令牌化缓存

编排器使用HuggingFace标记器进行准确的标记计数和上下文预算。标记器文件是 预下载 进入 tokenizer_cache/ 因此,编排器在没有互联网接入的情况下启动,这是在本地网络上进行现场部署所必需的。

tokenizer_cache/
  google--gemma-4-26B-A4B-it/    # Gemma 4 tokenizer (used by ollama + llamacpp providers)
    tokenizer.json
    tokenizer_config.json
    chat_template.jinja

config.py 自动解析HF型号ID(例如。 google/gemma-4-26B-A4B-it)如果匹配的文件存在于中,则转到本地路径 tokenizer_cache/。目录名使用 -- 代替 / (例如。 google--gemma-4-26B-A4B-it). context_manager.py 使用以下命令加载标记器文件 local_files_only=True,因此field/online startup希望标记器已经在本地存在。

要为新的提供者/模型预下载标记器,请执行以下操作:

uv run python -c "
from transformers import AutoTokenizer
import os
name = 'google/gemma-4-26B-A4B-it'  # change to the HF model ID
save_dir = os.path.join('tokenizer_cache', name.replace('/', '--'))
os.makedirs(save_dir, exist_ok=True)
tok = AutoTokenizer.from_pretrained(name)
tok.save_pretrained(save_dir)
print(f'Saved to {save_dir}')
"

建筑

orchestrator/
  main.py                  FastAPI app, /chat agentic loop
  config.py                All configuration, env var loading, MCP server wiring
  llm_provider.py          Provider abstraction: Ollama, AFM/MLX, llama.cpp
  tss_udp_client.py        Async UDP client for TSS2026 telemetry
  mcp_client.py            MCP server lifecycle, tool discovery, tool execution
  context_manager.py       Token counting, budget calculation, truncation
  context_summarizer.py    Auto-summarizes when conversation exceeds 80k tokens
  tokenizer_cache/         Pre-downloaded HF tokenizer files (offline operation)
  docs/                    Mission/headset references searchable by the LLM (text, PDF, images)
  mcp_servers/
    tss_tools_server.py    MCP server: get_tss_state, search_docs, read_doc, inspect_image
  rag_service.py           Conversation history via LanceDB (disabled, preserved for later)
  test_tss_udp.py          Smoke test for UDP connectivity to TSS2026

主动MCP工具

工具 描述 最大输出
get_tss_state 通过UDP获取实时TSS2026遥测数据。范围: all, eva, rover, ltv, ltv_errors, vitals. 4K字符
search_docs 为文本模式选择docs/。搜索文本文件和PDF。返回file:line:match,没有周围的上下文。 50场比赛
read_doc 阅读文档或章节。使用after search_docs展开特定行周围的上下文。 2K默认值,8K最大值
inspect_image 通过base64注入将docs/中的图像直接加载到LLM的视觉上下文中。主LLM看到图像本身(不是二手描述)。地图、图表、设备照片。 每张图片约3-5K视觉标记

LLM根据用户的提示决定何时调用这些工具。没有自动注入,也没有后台轮询。

禁用工具(保留以备后用)

工具 说明 如何重新启用
search_knowledge 通过LanceDB嵌入对过去对话进行语义搜索 取消注释 tss_tools_server.py 和 main.py

TSS2026集成

这 get_tss_state MCP工具通过UDP(端口14141)直接与NASA SUITS TSS2026服务器通信。TSS服务器是由NASA管理的外部依赖项,编排器只需要其IP地址和端口即可连接。

TSS协议使用大端二进制数据包:客户端发送8字节的请求([uint32 timestamp][uint32 command])并接收JSON遥测作为响应。

命令 作用域 返回数据
0 rover 加压漫游车遥测(位置、转向、激光雷达、驾驶室、电池)
1 eva EVA1/EVA2套件遥测、DCU、UIA、IMU、错误
2 ltv LTV最后已知位置、信号强度
3 ltv_errors LTV错误程序
-- vitals 过滤后的EVA数据:心率、氧气、二氧化碳、温度、仅电池
-- all 命令0-3组合

集 TSS_UDP_HOST 和 TSS_UDP_PORT 在 .env 以匹配正在运行的TSS实例。在本地开发过程中,您可以运行TSS2026的本地副本进行测试;在JSC测试周上,指向美国宇航局官方托管的实例。

# Verify connectivity
uv run python test_tss_udp.py

文档搜索

将参考文件放在 docs/ (支持子目录)。LLM通过两步工作流程搜索它们:

  1. search_docs("egress procedure") --查找与file:行引用匹配的项
  2. read_doc("procedures/ev-team-procedure-timeline.pdf", around_line=42) --读取该匹配的上下文

支持的格式:Markdown、纯文本、PDF(通过PyMuPDF)。图像可以通过以下方式进行分析 inspect_image.

目前的任务/耳机参考包括:

路径 目的
headset_voice_commands.md 精确的Magic Leap MLVoice/Vosk命令别名、事件ID、场景限制和 send recording 行为学
headset_unity_runtime.md Unity场景门控、记录按钮行为、HUD/参考地图行为、出口/入口自动化、LTV验证、迷你地图警告和TSS运行时注释
mission_description/ 美国国家航空航天局SUITS任务概述和缩写列表
procedures/ EVA时间表、坐标和LTV维修程序
telemetry_ranges/ EVA遥测标称/非标称范围
maps/ 带注释/原始岩石堆场地图、禁区和灰尘地图
peripherals/ UIA/DCU照片和LTV任务委员会概述/特写照片

注: docs/ 在...里 .gitignore --用您的任务文档在本地填充它。它没有提交到存储库。

数据流

Magic Leap 2 headset / client -> POST /chat
  |
1. Build message list: [system_prompt] + [user messages]
   |-- System prompt tells Luna about Vosk/MLVoice artifacts, "send recording",
   |   Magic Leap Unity scenes, and the headset docs to consult
2. If total tokens > 80k -> summarize older conversation history
3. Send to Gemma 4 (with think=True)
4. Model reasons internally (thinking trace logged, not streamed)
5. Model decides: answer directly OR call tools
   |-- get_tss_state(scope=eva) -> UDP to TSS2026 -> live JSON
   |-- search_docs("oxygen") -> grep over docs/ -> file:line matches
   |-- search_docs("show reference map") -> exact headset voice/runtime docs
   |-- read_doc("procedures.pdf", around_line=42) -> context excerpt
   |-- inspect_image("maps/dust-map.png") -> base64 injected into LLM context
6. Tool results fed back -> model generates final answer
   |-- If images were injected, base64 is stripped after the LLM responds
   |   (images exist in context for exactly one inference call)
7. Return response (content only, no thinking trace)

上下文摘要

当对话的总令牌计数超过 SUMMARIZE_THRESHOLD (默认80k),较旧的消息会通过LLM自动压缩为单个摘要消息。最近的4条消息始终保持不变。

摘要过程中的图像处理:

  • 携带base64图像的消息会在摘要文本中注明(例如,“附加1张图像用于视觉分析”),以便摘要记录参考了视觉参考。
  • Base64图像数据为 从不 包含在摘要输入中——仅对文本标签进行摘要。
  • 摘要后,最近(未摘要)消息中幸存的任何图像都会被删除,因为LLM已经看到并回应了它们。

即时图像剥离(预摘要):

  • LLM处理完图像后,图像会立即从对话历史记录中删除 main.py这意味着base64数据存在于上下文中 一 然后用轻量级的文本注释替换推理调用。一张高分辨率地图(约3900×2900像素)在一个回合内花费约5000个视觉符号,而不是整个对话。

Gemma 4推理

Gemma 4通过Ollama支持本土思维 think=True 参数。SDK会自动分离 message.thinking (内部推理)从 message.content (最终答案)。编排者:

  • 通行证 think=True 在每次聊天通话中
  • 在服务器端记录思维轨迹长度
  • 从不将思维轨迹流式传输或返回给客户端

测试

# Test TSS2026 UDP connectivity (requires running TSS server)
uv run python test_tss_udp.py

# Quick model test
uv run python -c "
import asyncio
from llm_provider import OllamaProvider
async def test():
    p = OllamaProvider(host='http://localhost:11434')
    r = await p.chat(model='gemma4:26b', messages=[{'role':'user','content':'Hello'}])
    print(f'Content: {r.content}')
    print(f'Thinking: {len(r.thinking)} chars' if r.thinking else 'No thinking')
asyncio.run(test())
"

安全

  • MCP服务器可以公开文件系统访问权限或执行命令。不要将端口13853暴露给不受信任的网络。
  • 这 .env 文件在 .gitignore.