做一个 Coding Agent,接上模型,再给它几个工具,看起来就可以开始干活了。
那为什么同一个模型,放在不同的 Agent 里,效果会有差别?为什么一个简单的循环就能完成一些编程任务,做成产品以后,却又需要上下文压缩、权限、会话恢复和多 Agent?这些能力到底要做到什么程度?
我觉得,讨论 Agent 工程,绕不开这几个问题。模型决定了很多事情,但开发者交付的是一套能够运行的系统。模型发起工具调用以后,谁来执行,失败了怎么办,下一轮给模型看什么,都要由这套系统处理。
这部分通常叫 Harness,可以理解为 Agent 的运行框架。
今天聊一篇专门拆解 Harness 的研究。它检查了 Claude Code、Codex CLI、Gemini CLI、Mistral Vibe、OpenHands、Aider、Mini-SWE-Agent、Hermes、Pi、OpenCode 和 OpenClaw 的源码。论文篇幅很长,我挑几个做 Agent 时会遇到的问题展开,看看这些系统具体怎么处理,也说说我的判断。
目录
- 一、一个 while 循环,能做多少事情?
- 二、都是调用模型,为什么还要单独做一层?
- 三、工具多了,是更强,还是更难选?
- 四、上下文压缩,到底要留下什么?
- 五、找代码,需要先建向量索引吗?
- 六、同意执行命令以后,还需要沙箱吗?
- 七、多 Agent 能省多少时间?先看任务怎么分
- 八、从头做一个 Harness,我会先补哪些能力?
一、一个 while 循环,能做多少事情?
先看一个任务:用户要求给现有项目加一个 CSV 导出功能。导出时,要沿用当前页面的筛选条件,不能影响原有分页。
Agent 需要找到页面和接口,读代码,决定改哪里,完成修改,再运行验证。中间可能发现接口返回的数据不够,也可能遇到类型错误。下一步怎么走,要根据前一步的结果决定。
基础循环大概就是这样:
接收目标和上下文 → 调用模型 → 执行模型请求的工具 → 把结果补充到上下文 → 再次调用模型 → 继续执行,或结束任务图 2|行动与观察循环、加入检查反馈的循环,以及协调者与工作者结构。
图中间加入了检查步骤。比如完成导出功能的修改以后,运行测试,把失败信息返回给模型继续修正。Aider 的编辑与检查流程就属于这一类。右边则增加协调者,让工作者返回结果以后再决定下一步。几种结构的差异在于怎么组织反馈和分工,底层仍然需要执行工具、处理结果。
这个结构本身并不复杂。Mini-SWE-Agent 就是一个比较直观的例子:围绕简单循环,通过 shell 与环境交互。它的设计便于观察执行轨迹,也便于研究模型在各轮做了什么。
但把这个循环变成开发者每天使用的产品,还会遇到其他问题。
比如模型请求执行测试,测试进程一直没有退出;用户点击停止,后台命令还在跑;工具返回几万行日志,下一次模型请求直接超出上下文限制;模型连续执行同一条命令,结果没有变化,却迟迟不结束。
这些问题要在模型调用之外处理。运行系统要处理最大轮数、超时、取消、输出长度和预算限制。
图 1|Harness 的组成:模型接入、执行循环、工具、上下文、安全、编排和扩展。界面层可以是终端、IDE、SDK 或服务。
我会按图里的七个部分检查一个 Agent。
部分 | 要回答的问题 |
|---|---|
循环 | 下一步怎么推进,什么时候停止 |
模型接入 | 模型协议、工具调用、流式输出和重试怎么处理 |
工具 | 能读什么、改什么、执行什么,结果怎么返回 |
上下文 | 本轮需要哪些信息,历史太长怎么办 |
安全 | 操作是否允许,执行环境限制在哪里 |
编排 | 子任务怎么分,结果怎么汇总 |
扩展 | 技能、外部工具和其他系统怎么接入 |
简单循环的好处是容易理解。每轮发生什么,顺着代码就能看清楚。功能多了以后,也容易把各种判断都塞进循环:执行前检查权限,调用前检查预算,返回后检查上下文,结束前检查状态。
Mistral Vibe 提供了另一种做法,把多种运行策略组织成中间件。上下文压缩之类的逻辑,可以从主循环中拆出来,在每轮执行时单独处理。
这个取舍,我觉得要看功能是否已经互相影响。如果只有几项检查,直接写进循环未必有什么问题。预算、压缩、只读模式、审计等规则需要各自调整时,拆成独立模块会更容易维护。
循环复杂程度不能直接代表 Agent 能力。简单循环也可以完成复杂任务;复杂运行系统通常还要满足交互、恢复、安全和扩展要求。比较的时候,先问它解决的是哪一类问题。
再看几个系统,循环里的实现差异会更清楚。
OpenHands 更重视执行记录和会话结构。它把事件写入持久化日志,模型看到的历史,则由当前会话分支上的事件整理而来。会话可以形成树,支持回放和分叉。一次模型响应里如果包含多个工具调用,系统可以将它们作为一批动作执行。
并行时,它还按工具声明的资源加锁。比如两个调用都操作同一个文件,就需要串行;读取不同文件,则有机会同时执行。这比简单地判断“读工具能并行,写工具不能并行”更细,但工具需要准确声明使用的资源,运行系统也需要维护锁。
放到 CSV 导出任务里,前端和后端位于不同文件,可以分别操作;两个工作者同时修改同一份接口定义,就要协调。并行调度需要知道它们实际碰到了什么资源。
Claude Code 按工具是否适合并发来分批执行。工具默认不允许并行,确认安全的读取、搜索等操作才进入并行批次,写入和命令执行采用更保守的处理。
和 OpenHands 的资源锁相比,这种方式依赖工具类别判断,实现更直接。并行读取可以减少等待,但两项互不相关的写入操作,也可能因为类别限制而无法同时执行。并发粒度不同,速度和实现成本的取舍也不同。
Codex 用异步状态机组织会话。Codex 的 Rust 实现围绕 Session 处理轮次、模型流式事件和工具执行,再向外提供事件读取接口。工具调用也有专门的运行层。
这种结构适合把“任务已经接受”“模型正在输出”“工具正在执行”“轮次结束”分别传给界面和上层调用方。Agent 作为服务或 SDK 使用时,持续反馈这些状态,比等任务结束后只返回一段文字更有用。
Pi 的核心循环比较精简,也专门处理了用户中途发来的消息。它区分 steering 和 follow-up 两种队列:前者在当前轮工具执行完后交给模型处理,后者在 Agent 原本准备停止时再处理。
比如导出功能做到一半,用户补充“只导出选中的记录”,这是对当前任务的调整;用户又说“做完以后帮我检查另一个页面”,则可以等待当前任务结束。运行系统区分这两类输入,系统就能区分哪些消息需要立即调整当前任务,哪些可以稍后处理。
Pi 还有一个具体保护:如果模型响应因为长度限制被截断,本轮工具调用不执行。参数即使勉强能解析,也可能不完整。这个判断发生在执行之前,避免在修改内容只输出了一部分时就操作文件。
OpenCode 把部分待处理工作写进消息日志。启动子 Agent、压缩上下文等待办动作,会作为消息的一部分写入日志,再由循环逐项取出处理。进程重启后,可以继续识别日志里的待处理工作。
不过,能恢复队列,不代表所有有副作用的工具操作都可以安全重试。文件已经改了但结果没有记录的情况,仍然需要结合工具的实际结果和执行记录判断。
这几种实现各有侧重:OpenHands 围绕事件组织会话,Codex 用异步状态机调度,Pi 细分用户输入,OpenCode 则把待办动作写进日志。选择的时候,我觉得可以先明确产品需要哪一种行为,再决定循环里要增加什么。
二、都是调用模型,为什么还要单独做一层?
模型接入容易被低估。第一版通常只要调用一个接口,把消息和工具定义传过去,再解析返回值。
支持第二个模型以后,事情就开始变多。工具调用格式可能不同,推理内容的返回方式可能不同,模型支持的参数也不同。切换模型时,历史里的消息能不能继续用,同样需要处理。
拿前面的 CSV 导出任务来说。模型输出一个工具调用,系统接收的是流式数据。如果参数还没传完,就开始解析和执行,容易把不完整的内容当作正式输入。模型接入层需要知道什么时候一次工具调用的信息已经接收完整,工具执行层再做参数校验。
请求失败也需要分类。限流可以等待后重试;上下文过长,需要减少输入;参数不支持,需要调整请求。所有错误都原样重试,容易造成无效等待。
论文里有几种不同安排。厂商自己的 Agent 通常会更多利用自家模型能力;支持多家模型的系统,则需要维护兼容信息和转换逻辑。Pi、Hermes、OpenCode 都有各自的兼容处理,提示词缓存也是其中一部分。
我觉得这层抽象至少要让上面的循环知道:这次模型请求是否成功,拿到了哪些工具调用,用了多少 token,失败属于哪一类。至于具体厂商的格式差异,尽量集中处理。
缓存也有类似问题。提示词内容差不多,并不意味着服务一定能复用。稳定前缀、消息排列和厂商支持的缓存标记,都可能影响结果。
例如每轮都在系统提示词开头插入最新时间或变化的项目状态,前缀就会变化。把固定规则和基础工具放在相对稳定的位置,把动态信息放到后面,更便于利用缓存。最终有没有减少成本,还要看实际返回的用量。
把这些逻辑集中到模型接入层,后续接入新模型,就有明确的地方处理兼容问题。如果每个 Agent 都自己写一份,同类问题就要在多处修复。
模型兼容也会直接影响工具接口。OpenCode 会根据模型切换编辑方式:GPT 系列模型使用移植自 Codex 的 apply_patch 补丁格式,其他模型使用另一套编辑接口。
这个做法说明,同一个运行系统可以给不同模型提供不同的工具接口。需要付出的成本是维护这些分支,并分别验证编辑成功情况。统一成一套接口更容易维护,按模型适配则可能更容易利用模型已经熟悉的形式。
所以我会把“支持某个模型”拆开看。请求能否成功是一层,能够稳定生成工具参数是另一层,缓存、上下文和编辑接口能否适配,还要继续验证。
三、工具多了,是更强,还是更难选?
同样是修改代码,Agent 可以只用 shell,也可以使用专门的读取、搜索、编辑和执行工具。
图 3|各系统的工具数量与编辑形式。
工具少,接口简单,模型需要通过组合命令完成更多事情。工具多,可以直接表达常见操作,但每个工具都有参数和说明,模型要先理解,再选择。
所以我更关心工具的输入输出是不是清楚。
举例来说,Agent 要把导出按钮的处理函数改掉。一个编辑工具要求传文件路径、旧文本和新文本,旧文本必须在文件中只匹配一处。匹配不到就返回错误,匹配多处也返回错误。
这种方式有一个好处:模型如果记错了文件内容,工具会明确拒绝。它需要重新读文件,找到正确位置,再修改。
如果改成模糊匹配,就要处理另一类问题:看起来相似的两段代码,工具选了哪一段?空格或缩进变化可以容忍到什么程度?修改完以后,怎样确认没有改错位置?
几个系统在这里有不同选择。Claude Code 等采用较严格的编辑约定;Aider 有从精确到模糊的匹配处理;Gemini CLI 的实现中,还出现了通过额外模型调用修复编辑匹配的方式。
我觉得这个差异很具体:严格匹配更容易发现文件状态不一致,但可能需要模型多读一次;宽松匹配能容忍部分输出偏差,但要承担错误定位和验证成本。没有一个匹配规则适合所有模型和任务。
再展开几个编辑实现。
Codex 使用自己的补丁格式。apply_patch 通过补丁块描述文件的新增、修改或删除。这类接口适合一次表达多处变化,工具需要负责验证补丁能否应用,不能只确认格式能解析。
Pi 会统一处理 Unicode 和空白差异,但不依赖相似度阈值来放宽匹配。它处理 Unicode 和空白差异,检查是否匹配到多个位置,以及多次编辑的范围是否重叠,并在执行前生成 diff 预览。目标是容忍格式差异,同时保留精确修改的约束。
Hermes 和 OpenCode 更强调匹配容错。它们有多阶段匹配处理,依次尝试精确匹配,以及处理空白、缩进和上下文差异。Hermes 还区分验证和应用,OpenCode 会把 LSP 诊断补充进编辑结果。
拿两个都叫 handleExport 的函数来说,宽松匹配不能只回答“找到相似内容了”,还要处理是否存在多个候选。修改后补充诊断,能够让模型及时看到类型或语法问题,但诊断没有报错,也不代表业务行为正确。
Mistral Vibe 的变化也值得看。论文保留了前后两个时间点的源码。在这段时间里,它移除了原先的模糊 SEARCH/REPLACE 方式,转向精确且唯一的旧文本匹配。对有歧义的编辑,返回工具错误。
这个变化说明,编辑接口的设计也会调整。随着模型和任务环境变化,工具承担多少修复工作,也可能调整。自建 Harness 时,编辑成功率、错误位置和额外调用成本,需要一起观察。
工具返回也要说清楚。搜索没有结果,与搜索输出被截断,是两回事。命令退出失败,与命令还在运行,也是两回事。模型下一轮能不能处理好,取决于它有没有拿到这些状态。
再说工具规模。接入多个 MCP 服务以后,工具定义本身就会占用上下文。即使本轮只是改一个按钮,模型也可能收到一大批完全无关的工具说明。
按需发现和加载,可以减少这部分开销。Claude Code 提供延迟加载和工具搜索;Codex 有基于 BM25 的工具搜索;Hermes 会在工具目录占用过多上下文时,通过搜索、描述和调用三个入口访问目录。
以一个包含代码、文档、工单和发布能力的平台为例,当前任务只涉及代码,可以先提供基础开发工具。如果用户要求关联工单,再检索相关工具定义。这样模型不必每轮阅读完整目录。
但这里也有代价。模型要多做一次工具搜索,目录描述也要足够准确。工具已经安装,却始终搜不到,延迟加载就没有解决任务问题。因此我会同时检查上下文成本和能否找到所需工具。
外部接口也不必一对一全部暴露。为了回答“这个需求现在卡在哪里”,模型可能要查询需求、任务、负责人和状态。固定的数据查询和关联可以由程序完成,工具返回整理好的结果。模型再决定是否需要继续查细节。
四、上下文压缩,到底要留下什么?
继续看 CSV 导出任务。用户一开始明确要求“沿用当前筛选条件,不影响分页”。Agent 读了代码,查了依赖,运行测试,又处理了一次编译失败。
如果历史太长,系统把它压缩成“正在实现 CSV 导出”,信息就不够了。接下来模型可能生成一个能下载的文件,却把筛选条件忘了。
这个例子说明,压缩需要保留任务状态。只概括讨论过哪些主题,很难支持后续执行。
图 4|四种上下文管理方式:完整历史、递归摘要、上下文压缩器和阈值压缩。
Aider 的做法比较容易理解:历史消息超出 token 预算以后,把较早的一部分交给模型总结,保留最近的原始消息。如果摘要加上近期消息后仍然过长,就继续处理。
这里保留近期消息有实际用途。正在修复编译错误时,最近的错误堆栈和代码,比一句“编译失败,正在处理”更有用。
OpenHands 把这部分做成可插拔的 Condenser。早期提供的压缩器种类较多,后来的 SDK 精简了实现,同时把压缩请求和结果也放进事件记录。
这有助于排查一个具体问题:Agent 到底是在压缩之前忘了筛选条件,还是压缩结果漏掉了它?如果压缩本身留下记录,就能比较前后状态。上下文超限或历史格式出错时,也可以转入压缩处理,而不一定直接结束会话。
其他系统会按 token 阈值触发压缩。Gemini CLI 会生成状态快照并保留近期历史;Mistral Vibe 会把之前的用户消息重新放进压缩后的上下文;Pi 和 OpenCode 等实现会利用已有摘要继续合并信息。
这几种做法分别解决不同问题:保留当前调试现场,让原始目标继续存在,避免多次压缩后丢掉早期决策。
我会让摘要至少回答这些问题:任务是什么,约束有哪些,已经改了哪些文件,做过什么验证,目前卡在哪里,接下来准备做什么。
对于导出功能,摘要里应该明确保留“筛选条件要进入导出请求,分页行为保持不变”。如果已经发现后端导出接口不支持某个筛选字段,也要写进去。否则模型后面可能重复采用已经排除的方案。
触发压缩时,也要为后续消息预留空间。当前请求没有超限,但工具马上返回一份大文件,下一轮就可能超限。历史预算和工具输出预算需要一起安排。
较长的内容可以存到外部文件或存储中,只把存储位置和必要片段返回给模型。Agent 需要时再读取。完整记录仍然保存,模型本轮不必全部看到。这样既能控制上下文,也能在排障时回到原始数据。
这几种实现里,还有一些与会话恢复直接相关的做法。
Hermes 在压缩时保留会话之间的关联。压缩时结束当前 SQLite 会话,再创建通过 parent_session_id 关联的子会话。新的上下文变短,之前的记录仍然保留,可以沿着父子关系查询。
如果一个导出任务经历过三次压缩,就不会只剩下最后一份摘要。排查时可以回到每段会话,检查早期要求和修改过程。这需要维护会话关系,查询历史时也要处理跨会话的重复信息。
Pi 的会话底层是追加式 JSONL 树。每条记录有自己的标识和父标识,用一个指针标记当前会话所在的分支。分叉、回看和探索另一条路径,可以建立在同一份结构上。读过和改过的文件清单,也能跨压缩保留。
这里有个边界:切换会话分支,并不自动恢复文件系统。模型的对话历史回到了之前的状态,磁盘上的代码可能已经发生变化。产品如果提供回退,就要说清楚回退的是对话,还是对话和文件一起恢复。
OpenCode 的摘要按固定栏目记录任务状态。它把之前的摘要再交给压缩 Agent,按目标、重要细节、工作状态、下一步和相关文件合并。仍然成立的信息保留,过期的信息移除。
对长开发任务,这类结构化摘要比一段泛泛的对话概括更容易核对。导出接口已经确认,测试还没通过,就可以分别写进“工作状态”和“下一步”。
上下文压缩和持久化记录要分开。摘要用于下一轮推理,完整事件记录用于恢复、回放和审计。不能因为模型暂时不需要一段日志,就把唯一的执行证据删掉。
长期记忆同样需要区分。项目的测试命令可以长期保存;一次测试失败和临时推测,应该留在当前任务里。否则下次任务可能继承已经失效的信息。
五、找代码,需要先建向量索引吗?
对 Coding Agent,我会先用好代码本身的结构。
用户要求增加 CSV 导出,入口可能是当前页面的组件名、筛选字段或请求路径。找到页面后,再沿着事件函数、请求封装和后端接口继续读。每一步的结果都能在文件里确认。
ripgrep、glob、语法树和语言服务,可以覆盖很多这样的定位工作。它们返回路径、行号或符号,方便 Agent 继续操作,也能反映当前工作目录里的变化。
这篇研究的一个观察是,样本没有采用向量嵌入检索代码。我觉得可以据此调整起步顺序:先验证文本和结构检索能做到什么,再判断是否需要额外索引。
代码定位、对话记忆和跨文档查询的需求不同。OpenClaw 等系统在记忆检索中仍然使用向量检索,需要根据具体任务选择。
如果要增加语义检索,我会拿一组现有搜索难以处理的任务来比较。例如用户只给出业务描述,没有明显关键词,语义检索是否更容易找到相关代码?找到以后是否减少了后续读取?索引维护和同步增加了多少成本?这些都可以具体测。
项目规则文件也属于上下文来源。AGENTS.md、CLAUDE.md 等文件,负责提供代码里不容易直接看出的要求。
例如根目录说明通用的测试和格式规范,某个子目录要求兼容旧接口。Agent 修改这个目录之前,需要加载对应规则。把全部目录说明一次性加载进来,会增加输入;按需加载,则必须保证规则在修改之前生效。
研究中的多个系统都在采用这种分层 Markdown 上下文。它的优势是规则放在项目里,可以检查和维护。实际实现还要说清楚加载范围、覆盖关系和更新时间,避免模型拿着旧规则工作。
Aider 的 RepoMap 也可以单独看。它通过 tree-sitter 提取代码符号,根据当前对话对相关信息排序,在有限的 token 预算内生成仓库地图。
比如当前讨论导出接口,仓库地图可以帮助模型先看到相关符号和结构,再读取具体文件。它与直接全文搜索提供的是不同信息:一个帮助了解代码关系,一个帮助定位具体内容。最终修改仍要回到实际文件。
另外,代码上下文和长期记忆也别混在一起。几个系统在“谁负责把信息写成记忆”上,选择很不一样。
Codex 采用后台提取和整理。研究中的记忆流水线先从会话记录提取信息,再由受限制的内部 Agent 整理成文件。整理过程没有网络访问,只允许指定范围内的本地写入,并对变化进行检查。
Gemini CLI 增加了人的审核。它会从已结束的会话中提取信息,生成补丁,放到项目的 inbox,用户审阅以后再采用。这样记忆形成得慢一些,但哪些内容会成为后续任务的依据,需要经过用户确认。
Hermes 使用有大小限制的 Markdown 记忆文件。会话开始时加载一份记忆,在本次会话中保持不变;后续更新写入磁盘,不立即改动固定的提示词前缀。查询历史记录则依赖 SQLite 全文搜索,也可以通过插件接入向量检索。
OpenClaw 的比较重点不同。它更接近多渠道助理网关,论文的文件编辑表也没有把它列为专门的代码编辑器。把它纳入样本,是为了比较通用 Agent 的会话、记忆和扩展结构。
它的 Active Memory 插件可以在主 Agent 回复之前运行专门负责记忆检索的子 Agent,补充相关信息。这里要解决的是“这次回答需要哪些历史记忆”,与 Coding Agent 查函数定义并不相同。多渠道和长期使用,还会带来会话路由、记忆范围与工具可见性的问题。
我觉得长期记忆的设计需要先回答一个问题:什么信息可以变成未来任务的默认依据?“这个仓库的导出接口不支持某个字段”也许值得保存,但接口升级以后就需要更新;一次临时猜测,不能因为写入了记忆就变成项目事实。
六、同意执行命令以后,还需要沙箱吗?
需要。操作授权和执行隔离解决的是不同问题。
用户同意 Agent 运行测试,允许的是这次测试操作。测试进程实际能读取哪些文件、访问哪些地址、启动什么子进程,还要由执行环境限制。
拿导出功能来说,Agent 修改完代码,要运行项目测试。测试脚本可能调用其他程序。权限层判断允许执行测试,并不自动保证这些程序只能访问项目目录。
图 5|论文中的 Claude Code 权限设计:静态 Hook 规则、模型权限分类、用户确认对话。
分层判断的思路是,让不同规则处理不同情况。可写路径、工具限制等明确要求,可以由程序检查;项目自己的校验可以接到 Hook;需要理解意图的操作,可以交给模型辅助判断或请求用户确认。
但模型判断允许执行之后,执行环境仍然需要自己的边界。尤其是共享、托管或无人值守任务,文件系统、网络和进程的访问范围都要单独配置。
授权范围也要具体。允许修改当前项目,不等于允许修改上级目录。用户同意了一条命令,后面参数发生变化,也要判断原授权是否适用。
取消是另一个需要检查的地方。用户点停止以后,模型请求停了,工具是否停了?工具停了,子进程是否还在?多 Agent 情况下,工作者是否继续执行?界面显示“已停止”时,实际执行也应该已经结束。
还有会话恢复。某次写入已经完成,但执行结果还没保存,运行实例就退出了。恢复以后,如果系统直接重试,可能产生重复副作用。
因此会话里需要记录执行状态,区分调用已发起、正在执行、已返回结果。对有副作用的工具,再根据具体场景处理幂等、状态查询和人工介入。不能只保存一份聊天历史,就认为恢复完成了。
我觉得权限、隔离、执行记录和恢复需要一起设计。它们最后都要回答:这次动作到底能不能做,已经做了没有,出问题以后怎么接着处理。
七、多 Agent 能省多少时间?先看任务怎么分
任务长,不一定适合多 Agent。
例如 CSV 导出功能,前端和后端都要改。如果两边还没有确认接口参数,各自启动一个 Agent 同时写,最后可能对不上。先确定参数和返回格式,再让双方在独立目录里实现,才有并行空间。
如果只有一处小改动,启动多个 Agent 还需要传递任务、等待结果和合并修改,可能比一个 Agent 直接处理更慢。
图 6|单 Agent、顺序委派、并行子会话、层级线程树、递归组合,以及注册表与协议。实线表示启动或调用,虚线表示返回。
我觉得多 Agent 有两类比较清楚的用途。
第一类是独立任务并行推进。接口约定已经明确,前端、后端和测试可以分别处理。但要安排工作目录和合并,避免同时覆盖文件。各自通过测试以后,合在一起仍然需要集成验证。
第二类是分开处理信息。一个工作者调查导出接口,另一个检查筛选逻辑,主 Agent 拿到结果后决定怎么改。主会话不用装入每一次搜索和所有文件内容,可以继续保留总体目标。
这两类收益也对应两类成本:并行需要协调修改,分开处理信息,需要保证汇总结果准确、完整。
子任务应该写到什么程度?我会明确目标、范围、工具和返回内容。例如“检查后端导出接口支持的筛选参数,返回文件位置、参数定义和不支持的条件,暂不修改代码”。主 Agent 拿到这份结果,就能继续决定接口方案。
如果只说“帮我看看后端”,工作者不知道查到哪里算结束,主 Agent 也很难判断报告是否够用。
图 7|协调者模式:工作者按工具白名单执行任务,再向协调者返回结果。
这个实现里,我更关注工具白名单。调查任务可以限制在读取和搜索,实施任务再提供编辑与执行能力。主 Agent 可以按阶段委派,避免所有工作者从一开始就修改环境。
研究中的协调流程包括调查、汇总、实施和验证。对应到导出任务,就是先查前后端现状,确认接口,再分派修改,最后验证筛选、分页和下载结果。这样每个阶段的输入和结束条件都比较明确。
工作者的结果也要能核对。哪些结论来自代码,哪些是推测,有没有运行验证,都应该写清楚。几个工作者意见不一致,主 Agent 需要回到文件或环境里确认,不能只是把报告拼在一起。
再看几个系统具体怎么委派,差异就更明显了。
Codex 给工作者独立的线程标识和历史。父子线程通过类型化消息交互,上下文继承可以选择完整历史或最近若干轮。这样既能让工作者接续已有调查,也能限制它看到的信息。
Mistral Vibe 的委派是顺序执行。父 Agent 通过 task 工具启动新的 AgentLoop,等待子任务完成,再接收事件和结果。它限制可以委派的角色,避免随意递归到更宽权限的配置。
所以看到“支持子 Agent”,不能马上理解成“能够并行加速”。顺序委派的价值更多是分开任务和上下文,父任务仍然要等待。
Gemini CLI 使用 Agent 注册表。定义可以来自用户、项目和扩展;本地与远程执行通过统一的会话接口组织,并有实验性的 A2A 服务。在企业里,如果某个检查能力已经是远程 Agent,可以沿这个边界接入,不一定把所有执行代码放进同一进程。
Hermes 对子任务权限采用交集。工作者的工具集合不能超过父任务已有的能力,并额外限制递归、用户交互、记忆写入等操作。默认委派比较保守,也提供通过配置启用的编排和共享数据库协作方式。
Pi 把子 Agent 放在扩展里。核心没有 spawn 工具,示例扩展通过启动独立 Pi 进程执行子任务,解析 JSONL 输出,汇总进展和成本。上下文因此天然分开,父子通信、资源管理则要由扩展处理。
OpenHands 用独立会话组织工作者,OpenCode 用子会话。前者为工作者建立独立事件记录,把摘要和指标带回父任务;后者沿用已有会话引擎,再根据权限配置收窄子会话能力。
它们共同需要解决目标、上下文、权限和结果返回,但分别通过线程、进程、注册表或会话实现。对于自建平台,我会先明确需要哪一种隔离和恢复,再挑合适的委派方式。
主任务的预算和取消,也要传到子任务。父任务已经停止,子任务不能继续消耗资源或改代码。
几个系统还在“怎么发现任务没有进展”上做了不同安排。
OpenHands 检查重复动作与观察、反复报错、交替模式等多类情况。Gemini CLI 先通过哈希比对发现重复内容和工具调用,执行轮数较多时,再让模型自检。两者都在尝试识别无效的重复执行,但维护成本和误判情况需要分别验证。
OpenCode 连续遇到相同调用时,会进入权限确认,让用户决定是否继续。Hermes 的默认设置更偏向把重复失败警告写回工具结果;它另有 verify-on-stop 检查,可以在修改代码后缺少新的验证证据时,要求继续执行。
这个停止检查比较具体:Agent 不需要每一轮都做一遍完整反思,而是在准备结束时检查是否缺少验证。对于导出任务,代码已经改了,但没有新的测试或检查结果,就还有事情没做完。
这类机制仍然不能替代验收。执行过测试,只能证明有验证动作;筛选条件是否生效、导出数据是否正确,需要相应检查覆盖。
八、从头做一个 Harness,我会先补哪些能力?
看完这些实现,我觉得可以先把一个任务的完整过程做扎实。
1. 能查清执行过程的循环。模型输入、工具参数、执行结果和结束原因,都能按会话查到。先给出轮数、时间和预算限制,避免失败后无限重试。
2. 一组能重复运行的任务。既有正常修改,也有文件找不到、编辑歧义、测试失败、工具超时和用户中断。每项能力加进去以后,都用这些任务重新检查。
3. 长任务的上下文处理。大输出按需读取,摘要保留目标和进度,近期调试信息保留原文。再验证压缩和恢复后,Agent 是否继续遵守原来的要求。
4. 实际产物验证。导出任务不能只看测试命令退出成功,还要检查筛选有没有生效、分页有没有变化、CSV 内容是否正确。运行状态和用户验收要分别判断。
5. 权限和执行环境。可写范围、网络限制、授权与取消,都需要落到代码和配置中。有副作用的操作,考虑失败后怎样确认执行状态。
这些基础能力做好以后,再按需要接入技能、MCP 或多 Agent。技能适合保存常用流程与领域知识,MCP 解决外部工具连接,ACP 可以用于编辑器交互或 Harness 互操作。具体协议要看系统边界,内部委派未必需要跨系统协议。
这篇研究还提供了一个约 90 行的最小 Harness 示例。它适合看清循环、工具和上下文如何连在一起。用于真实项目,还要补齐模型兼容、输入校验、预算、权限、取消和恢复,不能把示例行数当成完整产品的实现成本。
为了方便对照,我把这 11 个系统在本文涉及的特点放在一起。
系统 | 本文重点展开的实现 |
|---|---|
Mini-SWE-Agent | 简单线性循环、shell 工具、执行时间与连续错误次数限制 |
Aider | 编辑后检查与修正、多种编辑格式、RepoMap |
Claude Code | 工具并发分类、延迟加载、协调者与工作者 |
Codex CLI | 异步会话状态机、补丁工具、线程委派、后台记忆整理 |
Gemini CLI | 调度与循环检查、模型编辑修复、上下文处理、注册表与远程 Agent |
Mistral Vibe | 每轮执行前的中间件检查、精确编辑、压缩后保留用户目标、顺序委派 |
OpenHands | 持久事件日志、资源锁、可插拔压缩、独立工作者会话 |
Hermes | 停止前验证、编辑容错、父子会话关联、受限委派 |
Pi | 用户输入队列、响应截断保护、会话树、扩展式子 Agent |
OpenCode | 日志兼作队列、按模型选择编辑接口、增量摘要、子会话 |
OpenClaw | 多渠道助理网关、回复前检索记忆、通用 Agent 扩展 |
这个对照还有一个用途:避免把所有功能都列成自建系统的第一版需求。一个单人使用的代码工具,未必需要复杂会话路由;企业托管平台,却要优先考虑运行状态、权限和恢复。目标不同,先补的能力也不同。
最后说一下我会怎么衡量改动。一个新能力加进去,任务是否更容易完成,失败位置有没有变化,耗时和成本增加多少,人需要介入几次,都要记录。
比如加入子 Agent,前后端修改同时推进了,但合并冲突增加,整体耗时未必下降。摘要减少了 token,如果同时丢掉筛选约束,产物就不符合要求。工具增加了,如果模型选择错误,还是会走弯路。
这些都需要回到执行记录里看。对于前面的 CSV 导出,我会检查 Agent 有没有读到必要代码、确认接口、保留约束、正确修改并验证结果。哪一步出了问题,就改对应的运行环节。