端到端主链路:用户输入如何变成一次工具调用
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 外环与上表分层即本页所述调度骨架;治理门禁叠在工具执行边界上,不另起一条主链路(见治理专栏)。
一次请求的时序
Section titled “一次请求的时序”主路径是同步 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步骤对应关系:
- 客户端调 SDK(
prompt/command/shell等)。 - HTTP 路由进入会话写路径(例如
POST /session/:id/message)。 createUserMessage:规范化 parts 后写入用户消息。runLoop:读取经压缩过滤后的历史,按待办分支处理 subtask / compaction,或进入模型轮。- Processor 创建本轮助手骨架并
process。 - LLM 发起
streamText。 - 流事件(text / tool / reasoning / step)经 Processor 增量落库,并经 Bus 进入 SSE。
process返回continue|stop|compact,外环决定是否再问模型。- HTTP 返回本轮助手结果;增量通常已通过 SSE 到达前端。
助手骨架:先占位,再填充
Section titled “助手骨架:先占位,再填充”第 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:何时继续、何时结束
Section titled “runLoop:何时继续、何时结束”runLoop 是 ReAct 外环:内环(本轮调用哪些工具)在模型流与工具执行里完成;外环只根据结束态与未决工具决定是否再向云端请求一轮。
内环与外环各管什么
Section titled “内环与外环各管什么”一轮 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 与上下文压缩。
工具如何进入本轮
Section titled “工具如何进入本轮”工具不会因已注册就自动暴露给模型。进入本轮大致经过:内置与自定义全集 → 按模型 / 提供方 / 开关过滤 → 插件可改写定义 → 会话侧再按权限收口 → 执行时做参数校验与输出截断。
flowchart LR All[builtin + custom] --> Reg[按 model / provider / 开关过滤] Reg --> Plug[plugin 改写 definition] Plug --> Perm[resolveTools + permission] Perm --> LLM[本轮工具表] LLM --> Exec[execute:参数校验 + 截断]按模型 / 提供方 / 开关过滤
Section titled “按模型 / 提供方 / 开关过滤”这一层回答的是:在当前运行环境里,哪些工具对当前模型有意义。与具体某次对话无关:
- 提供方能力:联网检索类工具默认只对内置提供方开放,其它提供方需要显式打开检索开关才会进入候选。
- 模型习惯的改动接口:较新的 GPT 系列优先暴露
apply_patch,其它模型暴露edit与write,避免同一次改动出现两种写法。 - 运行形态与实验开关:面向交互式客户端的提问工具,以及 LSP、计划模式等实验能力,按客户端类型或开关决定是否加入内置集。
会话侧按权限收口
Section titled “会话侧按权限收口”这一层回答的是本轮对话允不允许模型看见。输入是角色默认权限、会话级权限,以及单次 prompt 传入的工具布尔开关(加载时折算成 allow / deny):
- 命中
pattern: "*"且action: "deny"的工具从本轮工具表整体移除。模型看不到它,也不会为它生成调用。 - 单次
tools里被置为false的工具同样移除;ask的工具保留可见,真正执行时再询问。 - 合并顺序是角色默认在前、会话与单次覆盖在后,后者只收窄前者。
例:角色默认允许 bash,本次请求传 tools: { bash: false },这一轮模型只看到读写类工具;下一条请求去掉该开关,bash 又回到工具表。
执行时参数校验与输出截断
Section titled “执行时参数校验与输出截断”模型发出的 tool-call 参数先按工具的 JSON Schema 校验,通过后才执行;执行结果在回填前做截断:
- 统一上限按行数与字节双重判断,超限时保留头部预览,完整内容落盘并在结果里给出路径与续读提示。
- 部分工具有更小的自有上限:读取类工具单行超过 2000 字符会截断该行,目录列举与检索类工具默认只回前 100 条。
- 截断只影响回填给模型的正文,工具实际执行过的事实不变;模型可按提示分段重读或改用更精确的检索。
mutating 路径还可挂人工门禁与审计落链;未命中规则时按上图路径直接执行。工具可见性与截断细则见工具系统。
排障:现象落在哪一段
Section titled “排障:现象落在哪一段”| 现象 | 优先核对 |
|---|---|
| 提供方 / 模型报错 | LLM 适配与提供方配置 |
| 模型看不到某工具 | 注册表过滤、会话 resolveTools、权限规则 |
| 工具状态异常或卡住 | Processor 对 tool-call / result 的落库与重试 |
| 上下文过长或莫名截断 | compaction、filterCompacted、工具输出截断 |
| 路径越界 | 实例目录 / worktree 边界 |
| UI 无增量、只有最终整包 | 是否在 POST 前建立 SSE;事件订阅是否断开 |
本地默认可用 SQLite 复现主链路;多租户或生产验收切 Postgres 时,消息模型不变,隔离在存储与请求身份上生效。可靠性侧的部署与验收口径见部署验证。
这条链路为什么拆成多层
Section titled “这条链路为什么拆成多层”把调度、落库、模型适配和工具策略拆开,是为了让多轮可追踪与单点可替换同时成立:
- 流事件最终落成 Message / Part,排障与跨轮记忆共用同一真相源。
- 换提供方或改工具策略时,不必重写外环调度。
- 上下文用压缩 + 截断两道闸,避免只靠调大窗口。
- 内核按平台根目录隔离运行上下文(配置发现、工具执行边界与状态缓存);用户与租户的隔离在请求上下文、存储与文件沙箱上生效。
- 插件钩子(system、参数、头、工具定义、消息变换)挂在边界上,而不是散落在业务分支里。
代价是:排障必须沿分层定位,不能假设一次 HTTP 响应体里带有全部增量;客户端必须维护 SSE 订阅生命周期。存储方言、租户头与治理门禁需按部署显式配置,默认本地形态与多租户生产形态能力面不同。
相关阅读: