跳转到内容

端到端主链路:用户输入如何变成一次工具调用

Agent 运行时要稳定多轮,依赖的是:用户输入进入后,调度、落库、工具与上下文治理走同一条可回放链路。Agent Forge 将会话调度、流事件落库、工具可见性与上下文压缩串在同一条主链路上;持久化支持 SQLite 或 Postgres(含租户隔离),请求可带身份头,mutating 路径可进入审批与审计。

总览:谁调度、谁落库、谁调模型

Section titled “总览:谁调度、谁落库、谁调模型”

客户端(控制台、CLI、SDK)不直连模型。一次用户输入先变成会话上的 HTTP 请求,再进入会话调度;调度决定本轮是跑子任务、压缩历史,还是创建助手骨架并拉模型流。模型与工具产生的增量写入消息部件(Message / Part);UI 的流式观感依赖另一条只读通道订阅这些变更。

flowchart LR
UI[Client] --> SDK[SDK]
SDK --> HTTP[HTTP /session]
HTTP --> Prompt[SessionPrompt]
Prompt --> Loop[runLoop]
Loop --> Proc[Processor]
Proc --> LLM[LLM.stream]
LLM --> Tools[ToolRegistry]
Proc --> Store[(Message / Part)]
Store -.-> SSE[SSE /event]
SSE --> UI
角色职责
SessionPrompt入口:落用户消息、驱动 runLoop、解析工具集与权限
Processor消费模型流事件,增量写入 Part;处理重试、中断与 compact 信号
LLM 适配组装 system / messages / tools,调用 streamText
ToolRegistry + 权限决定模型可见哪些工具、执行前是否再过滤
Message / Part 存储跨轮记忆与排障时间线;默认 SQLite,配置 Postgres 时走同一消息模型并带租户隔离
Bus + SSE把 Part 增量与会话状态推给已订阅的客户端

runLoop 外环与上表分层即本页所述调度骨架;治理门禁叠在工具执行边界上,不另起一条主链路(见治理专栏)。

主路径是同步 prompt:HTTP 受理后跑完本轮调度再返回助手消息。流式观感不靠该 POST 的响应体推 token,而靠客户端事先(或同时)订阅事件流。

sequenceDiagram
participant C as Client
participant H as HTTP
participant P as SessionPrompt
participant L as runLoop
participant R as Processor
participant M as LLM
participant S as Store
participant E as SSE
C->>E: GET /event(长连接)
C->>H: POST /session/:id/message
H->>P: prompt
P->>S: createUserMessage
P->>L: loop
L->>S: filterCompacted 历史
L->>R: process
R->>M: stream
M-->>R: text / tool / reasoning
R->>S: 增量写 Part
R-->>E: Bus 事件
E-->>C: delta / status
L-->>P: continue / stop / compact
P-->>C: 本轮 assistant

步骤对应关系:

  1. 客户端调 SDK(prompt / command / shell 等)。
  2. HTTP 路由进入会话写路径(例如 POST /session/:id/message)。
  3. createUserMessage:规范化 parts 后写入用户消息。
  4. runLoop:读取经压缩过滤后的历史,按待办分支处理 subtask / compaction,或进入模型轮。
  5. Processor 创建本轮助手骨架并 process。
  6. LLM 发起 streamText。
  7. 流事件(text / tool / reasoning / step)经 Processor 增量落库,并经 Bus 进入 SSE。
  8. process 返回 continue | stop | compact,外环决定是否再问模型。
  9. HTTP 返回本轮助手结果;增量通常已通过 SSE 到达前端。

第 5 步所说的骨架指一条空壳助手消息:模型流开始前先落一行 assistant 消息,带 parentID(本轮用户消息)、角色、所走模型与工作目录,cost 与 tokens 全为 0,finish 尚未写入。此时它没有任何 Part,客户端看到的是助手消息已经出现、正文还没到。

Processor 接手后,把模型流事件投到这条消息上,骨架逐步被填成完整回复:

flowchart LR
L[runLoop] --> Sk[创建 assistant 骨架]
Sk --> P[processor.create]
P --> S[llm.stream]
S --> E[流事件]
E --> T[text / reasoning Part]
E --> K[tool Part]
E --> B[step 边界 Part]
T --> M[(同一条 assistant 消息)]
K --> M
B --> M
M --> F[收尾:finish + tokens + cost]

落库路径是事件驱动的:Processor 发出统一写入口调用,同步事件触发投影层写存储,Processor 自身不出现 SQL。它产生的写入意图分两类:

  • 整行更新(新建 Part、工具状态迁移、step 边界、最终 finish 与用量):经同步事件由投影层落库。
  • 只广播的增量(正文与推理的逐段 delta):只进事件总线,供 SSE 订阅,不每段重写整行。

这样安排有两个结果:客户端不必等整轮结束才看到消息;排障与跨轮记忆读到的始终是同一份持久化消息,而不是某轮内存里的半成品。

异步受理可用 prompt_async:HTTP 先返回受理结果,后台仍跑同一套 SessionPrompt;进度同样只走 SSE。细节见HTTP 流式与 SSE 双通道。

runLoop 是 ReAct 外环:内环(本轮调用哪些工具)在模型流与工具执行里完成;外环只根据结束态与未决工具决定是否再向云端请求一轮。

一轮 ReAct 里既有模型思考也有工具执行,两者的推进位置不同:

环推进位置管的事结束信号
内环单次 streamText 及其工具执行模型本轮发几个 tool-call、按什么顺序执行、结果如何回填finish-step(本步结束)
外环runLoop 每一圈subtask / compaction 分支、要不要再问一轮、何时收工助手已 finish 且无未决 tool-call

SDK 在内环里走完发 tool-call、执行、把 tool-result 交回模型的往返,不需要回到 runLoop。外环只在一次模型轮结束后重读历史,再决定继续、压缩还是退出:

sequenceDiagram
participant O as runLoop(外环)
participant I as streamText(内环)
participant X as Tool execute
O->>I: 本轮请求(历史 + 工具表)
I->>X: tool-call(可并行多个)
X-->>I: tool-result
I-->>O: finish-step
O->>O: 重读历史:还有未决 tool-call / 待办 task?
O->>I: 需要时再问一轮

例:模型一轮里并行发起 3 个检索类 tool-call。内环把 3 个调用执行完并写回工具 Part;finish-step 落下 step 边界与用量后,外环发现模型还需要基于结果作答,于是带着更新后的历史进入下一圈。

flowchart TD
Start[读 filterCompacted 历史] --> Scan[回溯 lastUser / lastAssistant / tasks]
Scan --> Done{助手已 finish<br/>且无未决 tool-call?}
Done -->|是| End[prune 后返回]
Done -->|否| Task{待办 task?}
Task -->|subtask| Sub[handleSubtask] --> Start
Task -->|compaction| Comp[执行压缩] --> Start
Task -->|overflow| Make[创建 compaction] --> Start
Task -->|无| Remind[提醒与助手骨架]
Remind --> Proc[processor.process]
Proc --> Out{outcome}
Out -->|continue| Start
Out -->|stop| End

要点:

  • 退出条件只有一个:助手消息已 finish,且没有未决 tool-call。任一不满足就继续循环。
  • 待办分支优先于模型轮:先跑 subtask、再处理 compaction 或上下文溢出,最后才建骨架去问模型。
  • 历史视图使用 filterCompacted:压缩点之前的早期轮次不再整段送入模型,避免上下文无限膨胀。
  • 部分提供方会标 finish 但仍留下未执行的 tool-call;外环对此有兜底,避免在仍有未决工具时提前退出。
  • 同会话共用调度 Runner,避免并发两个 runLoop 写乱同一会话。

压缩与摘要策略的展开见runLoop 与上下文压缩。

工具不会因已注册就自动暴露给模型。进入本轮大致经过:内置与自定义全集 → 按模型 / 提供方 / 开关过滤 → 插件可改写定义 → 会话侧再按权限收口 → 执行时做参数校验与输出截断。

flowchart LR
All[builtin + custom] --> Reg[按 model / provider / 开关过滤]
Reg --> Plug[plugin 改写 definition]
Plug --> Perm[resolveTools + permission]
Perm --> LLM[本轮工具表]
LLM --> Exec[execute:参数校验 + 截断]

这一层回答的是:在当前运行环境里,哪些工具对当前模型有意义。与具体某次对话无关:

  • 提供方能力:联网检索类工具默认只对内置提供方开放,其它提供方需要显式打开检索开关才会进入候选。
  • 模型习惯的改动接口:较新的 GPT 系列优先暴露 apply_patch,其它模型暴露 edit 与 write,避免同一次改动出现两种写法。
  • 运行形态与实验开关:面向交互式客户端的提问工具,以及 LSP、计划模式等实验能力,按客户端类型或开关决定是否加入内置集。

这一层回答的是本轮对话允不允许模型看见。输入是角色默认权限、会话级权限,以及单次 prompt 传入的工具布尔开关(加载时折算成 allow / deny):

  • 命中 pattern: "*" 且 action: "deny" 的工具从本轮工具表整体移除。模型看不到它,也不会为它生成调用。
  • 单次 tools 里被置为 false 的工具同样移除;ask 的工具保留可见,真正执行时再询问。
  • 合并顺序是角色默认在前、会话与单次覆盖在后,后者只收窄前者。

例:角色默认允许 bash,本次请求传 tools: { bash: false },这一轮模型只看到读写类工具;下一条请求去掉该开关,bash 又回到工具表。

模型发出的 tool-call 参数先按工具的 JSON Schema 校验,通过后才执行;执行结果在回填前做截断:

  • 统一上限按行数与字节双重判断,超限时保留头部预览,完整内容落盘并在结果里给出路径与续读提示。
  • 部分工具有更小的自有上限:读取类工具单行超过 2000 字符会截断该行,目录列举与检索类工具默认只回前 100 条。
  • 截断只影响回填给模型的正文,工具实际执行过的事实不变;模型可按提示分段重读或改用更精确的检索。

mutating 路径还可挂人工门禁与审计落链;未命中规则时按上图路径直接执行。工具可见性与截断细则见工具系统。

现象优先核对
提供方 / 模型报错LLM 适配与提供方配置
模型看不到某工具注册表过滤、会话 resolveTools、权限规则
工具状态异常或卡住Processor 对 tool-call / result 的落库与重试
上下文过长或莫名截断compaction、filterCompacted、工具输出截断
路径越界实例目录 / worktree 边界
UI 无增量、只有最终整包是否在 POST 前建立 SSE;事件订阅是否断开

本地默认可用 SQLite 复现主链路;多租户或生产验收切 Postgres 时,消息模型不变,隔离在存储与请求身份上生效。可靠性侧的部署与验收口径见部署验证。

把调度、落库、模型适配和工具策略拆开,是为了让多轮可追踪与单点可替换同时成立:

  • 流事件最终落成 Message / Part,排障与跨轮记忆共用同一真相源。
  • 换提供方或改工具策略时,不必重写外环调度。
  • 上下文用压缩 + 截断两道闸,避免只靠调大窗口。
  • 内核按平台根目录隔离运行上下文(配置发现、工具执行边界与状态缓存);用户与租户的隔离在请求上下文、存储与文件沙箱上生效。
  • 插件钩子(system、参数、头、工具定义、消息变换)挂在边界上,而不是散落在业务分支里。

代价是:排障必须沿分层定位,不能假设一次 HTTP 响应体里带有全部增量;客户端必须维护 SSE 订阅生命周期。存储方言、租户头与治理门禁需按部署显式配置,默认本地形态与多租户生产形态能力面不同。


相关阅读: