Helix 智能体架构
Helix 核心接口、消息流转与后端架构
本文按当前代码(services/app + apps/web)描述:HTTP 接口、发送消息后的处理链路、SSE 的完整用法,以及后端每个类的职责。实现以源码为准,路径相对于仓库根目录。
1. 核心接口
所有 HTTP 入口集中在 HelixController,前缀 /api/v1。前端 apps/web/src/api.ts 的 REST 走 Vite 代理 /api → localhost:8080;SSE 在开发环境直连 http://localhost:8080,避免代理把流攒批。
1.1 接口一览
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /health |
存活探测 |
| GET | /models |
已启用模型卡片 + 默认模型 + fake |
| GET/POST/PUT/DELETE | /model-configs |
模型配置表 CRUD |
| GET/POST | /conversations |
会话列表 / 新建 |
| DELETE | /conversations/{id} |
删会话(含 run、消息、审批) |
| GET | /conversations/{id}/messages |
历史消息 |
| POST | /conversations/{id}/messages |
发送用户消息,创建 run 并启动 Agent |
| GET | /conversations/{id}/runs/latest |
该会话最近一次 run |
| GET | /runs/{id} |
run 状态、计划、轨迹、审批 |
| GET | /runs/{id}/steps |
run_steps 轨迹 |
| GET | /runs/{id}/events |
SSE 实时事件 |
| POST | /runs/{id}/cancel |
取消 |
| POST | /runs/{id}/resume |
计划审批后继续 |
| POST | /runs/{id}/approvals/{aid} |
高风险工具审批 allow|deny|allow_session |
| GET/POST | /knowledge-bases |
知识库 |
| GET/POST/DELETE | /documents |
上传 / 列表 / 删除 |
| POST | /documents/{id}/reindex |
重切片 |
| GET/PUT | /workspaces/current/settings |
默认模型、步数、日预算、Key |
| GET | /usage |
当日用量 |
可选鉴权:配置了 HELIX_API_TOKEN 时,ApiTokenFilter 要求 X-API-Token 或 Authorization: Bearer。/health 与 /actuator/health 放行。
1.2 三种推理模式
POST .../messages 的 mode:
| mode | 行为 |
|---|---|
react(默认) |
ReAct:把 ToolHub 的工具 schema 交给模型,可多轮 tool_call |
direct |
不传 tools,一轮生成后结束 |
plan |
先让模型产出 JSON 计划 → AWAIT_APPROVAL → 用户 resume 后再 loop |
2. 发送消息后的处理与流转
这是系统主路径。前端 send() → REST 落库 → 后台线程跑 Agent → SSE 推增量 → 结束后前端拉全量消息。
2.1 总览
sequenceDiagram
participant UI as apps/web
participant API as HelixController
participant CS as ConversationService
participant Loop as AgentLoop
participant Hub as EventHub
participant GW as ModelGateway
participant NIM as NVIDIA /v1/chat/completions
UI->>API: POST /conversations/{id}/messages
API->>CS: postMessage(text, modelOverride, mode)
CS->>CS: 写 user Message + Run(CREATED)
CS->>Loop: start(runId) // helixExecutor 异步
CS-->>UI: { message, run: { id, state: CREATED } }
UI->>API: GET /runs/{id}/events (fetch SSE)
API->>Hub: subscribe(runId, after)
Hub-->>UI: heartbeat + 历史非 token 事件
Loop->>Loop: CREATED → EXECUTING
Loop->>Hub: status thinking
Loop->>GW: chat(window, tools, onToken)
GW->>NIM: stream=true
NIM-->>GW: delta.reasoning / content
GW-->>Loop: onToken(piece)
Loop->>Hub: token
Hub-->>UI: event: token
alt 模型返回 tool_calls
Loop->>Hub: tool.start
Loop->>Loop: 执行工具或 AWAIT_APPROVAL
Loop->>Loop: continue 下一轮
else 纯文本
Loop->>Loop: 写 assistant Message
Loop->>Hub: done SUCCEEDED
Hub-->>UI: event: done(然后 complete emitter)
UI->>API: GET messages + GET run
end
2.2 前端发送(App.tsx → api.postMessage)
- 可选先
POST /conversations(无会话时)。 - 乐观插入一条本地 user 气泡。
POST /api/v1/conversations/{id}/messages,body:
{ "text": "...", "mode": "react", "modelOverride": { "provider": "nvidia", "modelId": "openai/gpt-oss-120b" } }
modelOverride 仅在用户改过下拉框时带上。
- 用返回的
run.id调用subscribeRun,同时 4s 轮询GET /runs/{id}作兜底(SSE 丢了也能在终态拉消息)。
2.3 ConversationService.postMessage
实现:services/app/.../conversation/ConversationService.java。
- 校验 会话存在、
text非空。 - 选模型
ModelRouter.resolve(override, conversation.model, workspace.settings)
优先级:请求覆盖 > 会话已存模型 > 工作空间默认 >HelixProperties(默认nvidia/minimaxai/minimax-m3)。 - 写 user 消息
role=user,id形如msg_...。 - 写 Run:
state=CREATED,goal=text,mode,modelRefJson,maxSteps/ 日预算来自 workspace 或环境。 - 把
message.runId指到该 run。 - 启动:默认
helix.sync-runs=false,调用AgentLoop.start(线程池helix-run-*)。测试 profile 可同步execute。 - 立即返回
{ message, run: { id, state } }。此时模型往往还没开始,state仍是CREATED。真正的助手内容不走这个响应,走 SSE + 事后 GET messages。
2.4 AgentLoop.execute:状态机入口
合法迁移见 RunFsm:
CREATED → PLANNING | EXECUTING | FAILED | CANCELLED | BUDGET_EXCEEDED
PLANNING → AWAIT_APPROVAL | EXECUTING | ...
AWAIT_APPROVAL → EXECUTING | CANCELLED | FAILED
EXECUTING → AWAIT_APPROVAL | SUCCEEDED | FAILED | CANCELLED | BUDGET_EXCEEDED | AWAIT_USER
CREATED 时:
mode=plan且还没有 plan →PLANNING,planOnce(不带 tools),解析 JSON 计划,发plan事件,转入AWAIT_APPROVAL,本轮返回,等POST /runs/{id}/resume。- 否则 →
EXECUTING,进入loop。
异常:describeFailure 把 429 / 超时 / 404 Function / 错误 model id / 鉴权映射成中文,写入 assistant 消息 + run.error,发 error 再 done。
2.5 loop:一轮或多轮模型调用
每一轮:
- 取消 / 日预算 /
maxSteps检查。 bumpStep+ checkpoint。- 拼上下文
MemoryService.window:- system =
SystemPrompt(工具列表 + 引用规范) - 超过 24 条会先让模型做摘要(
SUMMARY_PLEASE) - 再带最近 16 条(含 tool 角色)
- system =
direct则tools=[],否则ToolHub.listSpecs()。gateway.chat(..., onToken):- NVIDIA:
CompletionsStreamHTTPstream=true,把delta.content以及 gpt-oss 的reasoning/reasoning_content回调出来。 - 每段
onToken→EventHub.emit("token", {text})。 - 流失败且非 404/429/超时 → 回退非流式
OpenAiChatModel;若此时streamed仍空,整段再发一次token。
- NVIDIA:
- 有 tool_calls:
- 落一条 assistant 消息(
parts.toolCalls) - 每个 call:
tool.start→ 高风险且未批准则approval+AWAIT_APPROVAL并 return - 否则
ToolHub.invoke,观察写入role=tool消息,tool.end,continue下一轮(模型能看见 tool 结果)
- 落一条 assistant 消息(
- 无 tool_calls:落 assistant 文本,
SUCCEEDED,done。
高风险工具(当前 code_exec)审批通过后:decide → EXECUTING → finishPendingTool 真正执行,再继续 loop。
2.6 消息在库里的形态
表 messages:
| role | 何时写入 | 要点 |
|---|---|---|
| user | postMessage |
用户原文 |
| assistant | 最终回答 / 计划 / 失败文案 / 带 toolCalls 的中间步 |
partsJson 可含 toolCalls、plan、error |
| tool | 工具执行后 | parts.toolCallId、toolName;content 包在 <tool_result> 里 |
| summary | 对话过长时 | 不进 UI 列表,只进 LLM window |
前端在 done 后 GET /messages 用库里的完整消息替换实时气泡(liveText 清空)。因此 持久化真相是消息表,SSE 只负责过程。
2.7 模型调用原理(NVIDIA)
AgentLoop
→ ModelGateway.clientFor(ModelRef)
无 Key / HELIX_FAKE_MODEL → FakeModel
nvidia → OpenAiCompatProvider(base=https://integrate.api.nvidia.com/v1)
→ CompletionsStream.execute
POST {base}/chat/completions stream=true
解析 SSE data: 行
content 优先作为最终 ChatResult.content
content 为空时用 reasoning 填可见流
模型 ID:ModelCatalogService.canonicalModelId 把 gpt-oss-120b 映射成 openai/gpt-oss-120b。配置表 model_configs 提供 temperature / top_p / max_tokens / tools 开关。
3. SSE 在本项目中的用法
Helix 不用 把 LLM 的 HTTP 流直接转给浏览器。而是:
- 后端自己连 NVIDIA(流式)。
- 把增量翻译成 按 run 隔离的应用层事件。
- 浏览器订
GET /runs/{runId}/events。
NVIDIA 的 text/event-stream 与浏览器的 SSE 是两段独立连接。
3.1 为什么要自己做一层
- 一次 run 里不只有 token:还有 status、工具、审批、计划、引用、结束。
- 刷新 / 切会话后要 续订,不能只靠 TCP 不断。
- Vite 代理会缓冲 chunked 响应;开发态 SSE 必须直连 8080。
- gpt-oss 的 token 在
reasoning_content里,必须在服务端拆包后再转成统一的token事件。
3.2 服务端:EventHub + SseEmitter
文件:runtime/EventHub.java。按 runId 维护三张并发表:
| 结构 | 内容 |
|---|---|
seq |
该 run 的单调事件 id(字符串数字) |
history |
可回放事件(不含 token) |
subs |
当前挂着的 SseEmitter 列表 |
订阅 subscribe(runId, lastEventId):
new SseEmitter(10 分钟)。- 注册 onCompletion / onTimeout / onError → 从
subs摘掉。 - 立刻发
event: heartbeat(让响应提交、代理别缓冲)。 - 回放
history里 id > cursor 的事件;碰到done则complete()。 - 返回 emitter,Spring MVC 保持异步请求。
Cursor:?after= 优先,否则请求头 Last-Event-ID。
发送 emit(runId, event, data):
id = seq++。event != "token"才写入 history(避免重连时把几百个 token 一次性倒回)。- 向该 run 所有 emitter
send(id + name + JSON data)。 - 发送失败且不是客户端断开 →
complete();客户端断开则只 drop。 done后 complete 并 drop。
心跳:@Scheduled(fixedRate = 8000) 对所有订阅发 heartbeat。失败的连接被清掉。这不是给 UI 用的,是为了保活和探测死连接。
控制器在订阅前设置:
Content-Type: text/event-stream;charset=UTF-8Cache-Control: no-cache, no-transformX-Accel-Buffering: noConnection: keep-alive- 若有
Origin,回写Access-Control-Allow-Origin(开发态 5173 → 8080)
线上 Tomcat 异步请求超时:spring.mvc.async.request-timeout: 600000。客户端中止连接由 ApiExceptionHandler 吞掉,避免堆栈刷屏。
3.3 事件目录
浏览器只处理 命名事件(event: 字段)。heartbeat 丢弃。
| event | data 要点 | 是否进 history | 前端 |
|---|---|---|---|
heartbeat |
{"ok":true} |
否 | 忽略 |
status |
{phase, text} 如 thinking / planning |
是 | livePhase |
token |
{text} 增量字 |
否 | 拼到 liveText |
tool.start |
{name, args} |
是 | 右侧工具 / 进行中卡片 |
tool.end |
{name, ok, preview} |
是 | 更新工具卡片 |
plan |
{steps:[]} |
是 | 计划 Tab |
approval |
{id, risk, tool, args} |
是 | HITL 弹层 |
cite |
{docId, ordinal} |
是 | 引用 Tab |
error |
{code, message} |
是 | banner;结束泵 |
done |
{runId, state} |
是 | 拉 messages/run,关流 |
SSE 线上形态示例:
id: 0
event: heartbeat
data: {"ok":true}
id: 1
event: status
data: {"phase":"thinking","text":"正在思考"}
id: 2
event: token
data: {"text":"The"}
id: 15
event: done
data: {"runId":"run_abc","state":"SUCCEEDED"}
3.4 客户端:subscribeRun(api.ts)
不用 EventSource。原生 EventSource 在 CONNECTING 时也会打 error,旧逻辑会关连接再重连,把积压 token 一次画完。
现行方案:fetch + ReadableStream + AbortController。
sseOrigin() =
VITE_API_BASE 或(DEV)http://localhost:8080
GET {origin}/api/v1/runs/{runId}/events?after={lastId}&token=
Accept: text/event-stream
可选 X-API-Token
读循环:
TextDecoder追加到 buffer,按\n\n切 SSE 块。dispatchSseBlock解析id/event/data:多行。token→onToken;done/error→finished=true,reader.cancel()。- 网络失败且未 abort:指数退避重连,带上已记录的
lastId(after)。
token 不在 history 里,重连不会倒字;最多丢未持久化的实时字,终态靠 GET messages 补齐。 close():stopped=true+abort()。
App.tsx 的 attachStream:同一 runId 已有流则 resume 直接返回,避免切会话 effect 把活连接掐掉。done 后拉全量消息,用库数据替换 liveText。
3.5 与 NVIDIA 流的对照
| NVIDIA → Helix | Helix → 浏览器 | |
|---|---|---|
| 端点 | POST .../v1/chat/completions |
GET /api/v1/runs/{id}/events |
| 协议 | SSE,data: {chat.completion.chunk} |
SSE,自有 event 名 |
| 解析 | CompletionsStream |
dispatchSseBlock |
| 结束 | data: [DONE] |
event: done 后 emitter.complete |
| 鉴权 | Authorization: Bearer nvapi-... |
可选 API Token |
两段流失败互不影响:NVIDIA 断了由 AgentLoop 记 FAILED;浏览器断了只丢实时通道,run 继续。
3.6 续订与刷新
用户刷新或从侧栏点回正在跑的会话:
GET /messages画已落库部分。GET .../runs/latest,若 state 仍在 LIVE(CREATED/PLANNING/EXECUTING/AWAIT_*),attachStream(runId, { resume: true })。- SSE 回放 history 中的 status/plan/tool/approval(token 不回放)。
- 新 token 继续追加;若 run 已
done,回放里带done,前端再拉一次 messages。
4. 后端代码架构与类职责
HelixApplication 启动、线程池、种子 workspace/KB/模型表
HelixController 唯一 REST + SSE 适配器
ApiExceptionHandler HTTP 错误码、客户端中止静默
│
├─ conversation/ 会话与消息持久化
├─ runtime/ Run 状态机、Agent 循环、SSE 总线、审批
├─ model/ 模型目录、路由、NVIDIA/OpenAI/Fake
├─ memory/ 滑动窗口与摘要
├─ tool/ 工具注册与执行
├─ rag/ 知识库切片检索
├─ observe/ 轨迹与日预算
└─ security/ CORS、API Token
包结构对应 com.helix.*。运行时一个 JVM 进程,H2(local)或 Postgres;模型 按请求构建,没有进程级 ChatModel 单例。
4.1 启动与配置
| 类 | 作用 |
|---|---|
HelixApplication |
@SpringBootApplication;helixExecutor(core 2 / max 16);启动时种子 ws_default、默认 KB、空表则 seed 模型,并 repairKnownIds |
HelixProperties |
helix.*:NVIDIA/OpenAI Key 与 Base URL、默认模型、maxSteps、日预算、dataDir、fake、apiToken。useFakeModel() 认配置或环境变量 HELIX_FAKE_MODEL |
Jsons |
Jackson 封装、生成 msg_/run_ 等 id |
H2CheckConstraintFix |
本地 H2 放宽 runs.state 长度,使 PLANNING 等能写入 |
4.2 API 层
| 类 | 作用 |
|---|---|
api.HelixController |
全部 /api/v1;把实体转成 Map 视图;SSE 头 |
api.ApiExceptionHandler |
MODEL_UNAVAILABLE / RATE_LIMITED / 404;SSE 客户端断开不报 500 |
4.3 会话域 conversation
| 类 | 作用 |
|---|---|
Conversation / ConversationRepository |
会话:标题、workspace、modelRef、归档 |
Message / MessageRepository |
消息:role、content、partsJson、runId |
Workspace / WorkspaceRepository |
工作空间设置 JSON(默认模型、Key、预算) |
ConversationService |
创建/列表/删除会话;postMessage 编排;View DTO |
4.4 运行时 runtime
| 类 | 作用 |
|---|---|
Run / RunRepository |
一次 Agent 执行:goal、mode、state、plan、checkpoint、modelRef、step |
RunState |
CREATED … SUCCEEDED/FAILED/CANCELLED/BUDGET_EXCEEDED |
RunFsm |
合法状态迁移;终态集合 |
AgentLoop |
核心编排:start/execute/loop/plan/cancel/resume/decide;调模型、工具、SSE、落库 |
EventHub |
进程内 SSE 总线:订阅、回放、心跳、按 run 扇出 |
SystemPrompt |
加载 prompts/system.md,填工具列表 |
Approval / ApprovalRepository / ApprovalService |
HITL:pending/approved/denied;session allow 列表 |
4.5 模型 model
| 类 | 作用 |
|---|---|
ModelRef |
(provider, modelId);解析 openai: 前缀或 org/name |
ModelRouter |
覆盖 > 会话 > 工作空间 > env |
ModelGateway |
按请求选 Fake / NVIDIA / OpenAI 客户端;对外 catalog |
ModelProvider |
chat(messages, tools, ref, onToken) 契约 |
OpenAiCompatProvider |
LangChain4j 非流式 + 自研流式;限流重试;错误分类 |
CompletionsStream |
Java HttpClient 读 NVIDIA SSE;reasoning/content/tool_calls |
FakeModel |
无 Key 或测试:按用户话术脚本化 calculator/datetime/kb/code |
ModelConfig / ModelConfigRepository / ModelCatalogService |
DB 模型表、seed、别名、CRUD、卡片参数 |
4.6 记忆 memory
| 类 | 作用 |
|---|---|
MemoryService |
window:system + 摘要 + 最近 16 条;超 24 条触发摘要模型调用;toLlm 把 toolCalls 还原成 OpenAI 消息 |
4.7 工具 tool
| 类 | 作用 |
|---|---|
ToolSpec |
名称、描述、JSON Schema、risk、超时、handler |
ToolHub |
注册 calculator / datetime / weather / kb_search / code_exec;校验参数、超时线程、审计 |
BuiltinTools |
计算器、时间、假天气 |
CodeExec |
本机 python/js 子进程(高风险) |
ToolCallEntity / ToolCallRepository |
工具调用审计行 |
ToolError |
参数/执行失败 |
4.8 RAG rag
| 类 | 作用 |
|---|---|
KnowledgeBase 及 Repository |
知识库 |
DocumentEntity 及 Repository |
上传文件元数据 |
Chunk 及 Repository |
切片 + 简易向量 |
RagService |
上传、PDF 抽字、切块、hash 向量检索;kb_search 的数据源 |
4.9 可观测 observe
| 类 | 作用 |
|---|---|
TraceService |
MDC trace_id=runId;run_steps 追加 state/model/tool/checkpoint |
RunStep / RunStepRepository |
轨迹行 |
BudgetService |
按 workspace 估 token、日限额、/usage |
UsageDaily / UsageDailyRepository |
每日累计 |
4.10 安全 security
| 类 | 作用 |
|---|---|
CorsConfig |
/api/** CORS |
ApiTokenFilter |
可选共享 Token |
5. 一张图看完主链路上的对象
用户输入
→ ConversationService.postMessage
Message(user) + Run(CREATED)
→ AgentLoop (helix-run 线程)
MemoryService.window → 消息列表
ModelGateway → CompletionsStream → NVIDIA
EventHub.emit(token/status/tool/...)
ToolHub.invoke → Message(tool)
Message(assistant) + Run(SUCCEEDED|FAILED|AWAIT_APPROVAL|...)
→ GET /runs/{id}/events (EventHub / SseEmitter)
→ 浏览器 fetch 流 → liveText
→ done → GET /messages 替换为库中记录
相关源码入口:
- 接口:
services/app/src/main/java/com/helix/api/HelixController.java - 发消息:
.../conversation/ConversationService.java - 循环:
.../runtime/AgentLoop.java - SSE:
.../runtime/EventHub.java+apps/web/src/api.ts(subscribeRun) - 模型流:
.../model/CompletionsStream.java