六套 Harness 怎样完成模型—工具闭环¶
上一篇:运行时、配置与模型输入 · 返回课程总目录 · 下一篇:权限、状态与恢复
模型吐出 Tool Call 时,只是用结构化数据表达「我想调这个工具」。Harness 还得把调用解析出来,找到对应实现,检查策略后执行,记下结果,然后把新观察送回下一轮。这篇继续用运费修复任务,看六套实现在哪里接上这个闭环,也看清并发、失败和停止条件为什么塞不进一个 while。
最小闭环不是「模型调用一次工具」¶
模型流产生 Tool Call
→ Harness 验证名称与参数
→ 路由到具体工具
→ 权限与环境决定是否执行
→ 工具产生成功、拒绝或错误结果
→ 结果按 Call ID 写入消息与 Session
→ Harness 决定再次采样、压缩、取消或结束
运费任务至少要读文件、改代码、跑测试,其中任何一步失败,Harness 都应把错误变成下一轮能读懂的观察,不能只往终端扔一行错误。模型就算最后说了「完成」,也代替不了目标测试的实际结果。这两件事不能混。
同一问题在六套实现中的落点¶
| 课程 | 谁主要控制下一轮 | 工具路由与结果回填 | 代表性细节 | 继续阅读 |
|---|---|---|---|---|
| DeepSeek Harness | Agent Loop | Core Loop 与 Tool/Code Mode 包 | 模型消息、工具调用和 Session 事件跨包组合 | Agent Loop、模型与工具结果 |
| Codex | Thread/Turn 内的 Core 任务 | Tool Router 与各 Handler | 工具可以并发执行,但历史提交保持调用顺序 | 模型响应与工具循环 |
| Gemini CLI | Turn 与 Scheduler | Scheduler 管理 Tool Call 生命周期 | Policy、Confirmation、取消和结果事件围绕调度器展开 | Turn、Scheduler 与路由 |
| Claude | 闭源 CLI 控制产品内部循环;SDK 处理公开消息与控制请求 | SDK 暴露权限回调、Hook、MCP 与消息流 | 只能证明公开 SDK 边界,不能画出产品内部 Router | 消息流与生命周期 |
| pi | agent-loop.ts 的双层循环 |
Agent Core 执行工具批次并追加 Tool Results | Steering 与 Follow-up 在不同时间点插入;截断 Tool Call 不执行 | Agent Loop、双队列与工具批次 |
| OpenCode | Session Prompt Loop | Processor 归约流事件与 Tool Parts | continue/compact/stop 控制 Session,不是任务评分 |
Session Prompt、LLM 与 Processor |
表里问「谁控制下一轮」,实际上是要你去公开源码里找:哪个控制点会创建下一次模型请求。它不要求某个类必须叫 Agent。Claude 这一行特意画清了证据边界,因为 SDK 虽然暴露了异步流,我们却仍然看不到 CLI 内部的完整 Loop。
一次工具调用至少有五种结果¶
如果 Tool Result 只有成功和失败两种值,恢复时需要的信息就会被丢掉,因此更实用的做法是把结果细分为:
| 结果 | 副作用是否发生 | 下一步通常是什么 |
|---|---|---|
| 成功 | 已发生 | 把结果送回模型 |
| 策略拒绝 | 未发生 | 解释限制,换方案或请求授权 |
| 用户取消 | 未发生 | 停止或等待用户,不应自动重试 |
| 执行错误 | 可能未发生,也可能部分发生 | 依据工具语义检查环境,再决定重试 |
| 状态未知 | 无法确认 | 先观察环境,禁止盲目重放 |
如果工具名不存在,或者参数没通过校验,副作用通常还没发生。可 Shell 超时时,子进程可能已经启动,网络请求断开时,服务端也可能早已处理完请求。工具必须能把这些状态分别报给 Harness,Session 才能安全恢复。
并发比较要同时记录两种顺序¶
并发工具有「完成顺序」和「提交顺序」:
Codex 用测试核对了两件事:工具可以按并发时的先后完成,历史却仍然要确定地提交。pi 会看完整个 Tool Batch 再计算终止条件,Gemini CLI 则让 Scheduler 同时管理多个调用。其他实现就算串行执行,也得把每个 Tool Result 对回原来的 Call ID。
如果谁先从网络返回,历史就先写谁,模型可能会把 B 的结果配给 A,以后重放时 Context 也会忽前忽后。工具不必因此全部串行,只要提交历史时保住确定的顺序,让人能重建因果关系就够了。
Stop Reason 为什么不能直接结束任务¶
Provider 给出的 stop、toolUse、length 或 error,只在解释这一次模型响应为什么停下。pi 遇到 length 截断时,不会执行可能没传完的 Tool Call。OpenCode 就算已经收到 Finish Reason,只要 Parts 里还有 Tool Call 就会继续。DeepSeek Harness、Codex 和 Gemini CLI 也都要同时查看工具状态和队列,才知道要不要发起下一轮。
任务级结束还需要判断:
- 是否仍有未结算工具;
- 是否有 Steering、Follow-up 或用户新输入;
- 是否需要 Compaction 后继续;
- 是否被取消、预算耗尽或协议错误阻断;
- 是否已经产生足以回答用户的环境证据。
最后一项通常还要交给 Eval 或应用规则判断,单看模型的 Stop Reason 并不足以判定任务结果。别在这里提前收尾。
Hook、Extension 和 Processor 可以改变什么¶
不同项目会让扩展在不同位置插手,有的能在调用前拦住动作,有的能在调用后改写结果,还有的会变换 Context、订阅事件或委托子任务。比较这些入口时,你一定要问清它们在副作用发生前还是发生后介入。
- 调用前 Hook 可以阻止动作发生;
- 调用后 Hook 只能改变模型看见的结果,不能假装撤销副作用;
- Context Transform 可以隐藏或摘要历史,但不改变环境;
- Event Subscriber 观察事实,不应反向改写核心状态;
- 子 Agent 拥有另一条 Loop,需要保留父子身份。
如果把所有扩展都叫作 Middleware,这些安全边界就会被一个名字盖住。别混在一起。
回到运费任务:六套实现共享的事实链¶
具体函数名可以不同,但一次能够复核的修复仍然应该保留下面这条事实链:
对 DeepSeek Harness、Codex、Gemini CLI、pi 和 OpenCode,我们可以顺着公开的核心源码往里追,只是各自能追到的深度不同。Claude 这条线则只能根据公开 SDK 消息和控制契约,核对产品显露在边界上的事实。某一层找不到证据时,就明写「当前来源不可核对」,不能拿另一套 Harness 的常见做法补上空白。
一次可复核的本地实验¶
不调用真实模型,你也能验证 Loop 怎样运转。先写一个按脚本返回结果的 Model Stub,让它依次给出 read、edit、bash test 和最终文本,再让另一组输入在编辑后把测试跑失败。然后检查下面几件事:
- Tool Result 与原 Call ID 对齐;
- 失败结果进入下一轮,而不是让 Loop 崩溃;
- 截断或非法参数不会产生副作用;
- 并发完成顺序变化时,历史仍可重放;
- 模型自述「完成」不会覆盖测试失败。
这个实验只能核对通用机制,无法证明任何上游项目在所有配置下都这样做。证据到此为止。
练习:沿两套源码解释同一个失败¶
假设测试工具返回退出码 1,请选择两条课程,指出结果在哪里被转换成 Tool Result,又会写入何种 Session/Message 对象,并找到决定再次请求模型的控制点。如果用户在这时取消,还要说明取消信号怎样与普通工具失败区分。