1. 从"套娃"说起:Harness 套 Harness 到底在解决什么问题
第一次看到"Harness 套 Harness"这个说法,我脑子里蹦出来的画面是俄罗斯套娃——一层包一层,每层都在干自己的活。但真正跑过几轮之后才明白,这个比喻只对了一半。套娃是静态的嵌套,而 Harness 套 Harness 是动态的、有反馈回路的嵌套:外层 Harness 负责调度和验收,内层 Harness 负责执行和自检,两者之间通过结构化的信号传递状态,形成一个可以连续运转几十轮的闭环。
先说清楚 Harness 是什么。在 Coding Agent 的语境里,Harness 不是模型本身,而是包裹在模型外面的那层"脚手架"——它负责给 Agent 提供工具调用能力、上下文管理、错误恢复、任务分解、结果验证这些基础设施。你可以把模型想象成一个很聪明但记性有限、容易跑偏的实习生,Harness 就是他的工位、工具箱、任务看板和验收标准。没有 Harness 的 Agent 就像让实习生空手去修车,工具没有、流程没有、验收没有,干两下就卡住了。
那为什么需要"套"两层?单层 Harness 不够用吗?我实测下来的感受是:单层 Harness 在短任务上没问题,一旦任务链条拉长到十几轮以上,就会出现几个典型症状。第一是上下文膨胀,每一轮的对话历史、工具返回结果、中间产物全部堆在同一个上下文里,到后面模型开始"忘事",前面定好的约束后面就不遵守了。第二是错误累积,某一轮的一个小错误没有被及时发现和纠正,后面几轮都在错误的基础上继续推进,越走越偏。第三是验收缺失,Agent 自己说自己干完了,但到底干没干完、干得对不对,没有一个独立的检查环节。
Harness 套 Harness 的核心思路就是把这些职责拆开。外层 Harness 管"做什么"和"做完了没有",内层 Harness 管"怎么做"和"这一步做对了没有"。外层不关心具体实现细节,只关心任务分解、进度追踪和最终验收;内层不关心全局目标,只关心当前这一轮的执行质量和自检结果。两层之间通过一个结构化的状态通道通信,外层往下发任务指令和约束条件,内层往上回报执行结果和自检结论。
这个架构最关键的设计决策是:外层 Harness 不直接调用模型,而是调用内层 Harness。这意味着外层可以完全不知道内层用的是什么模型、什么工具、什么策略,它只认内层返回的结构化结果。这种解耦带来的好处是,你可以替换内层的模型或工具链而不影响外层的调度逻辑,也可以替换外层的调度策略而不影响内层的执行逻辑。我在实际项目中就遇到过需要把内层从某个模型换成另一个模型的场景,因为外层完全解耦,替换过程只改了一个配置项,调度逻辑一行没动。
还有一个容易被忽略的点:两层 Harness 之间的通信协议必须是结构化的,不能是自然语言。我一开始图省事,让内层用自然语言向外层汇报,结果外层解析起来经常出错,有时候内层说"基本完成了",外层不知道这算完成还是没完成。后来改成 JSON 格式的结构化回报,每个字段都有明确的语义和取值范围,外层的判断逻辑就变得非常可靠。这个教训很深刻——Agent 之间的通信,能用结构化数据就别用自然语言。
2. 连续 70 轮不崩:外层 Harness 的调度与验收机制
连续跑 70 轮是什么概念?假设每一轮平均需要 3 到 5 分钟(包括模型推理、工具调用、结果验证),70 轮就是 3.5 到 6 个小时的连续运转。在这段时间里,没有人类介入,Agent 要自己决定下一步做什么、自己判断上一步做得对不对、自己处理各种意外情况。这对调度机制的要求非常高,任何一个环节的脆弱性都会在几十轮的放大下变成致命问题。
2.1 任务分解的粒度控制
外层 Harness 的第一个核心职责是把一个模糊的大目标分解成一系列可执行的小任务。这里的关键是粒度控制——太粗了内层执行不了,太细了轮次爆炸。我的经验是,每个子任务应该是一个内层 Harness 能在 1 到 3 轮内完成的工作量。比如"实现用户登录功能"这个目标,分解成"设计登录接口的数据结构""实现密码哈希和验证逻辑""编写登录接口的路由处理""编写登录接口的单元测试"这样的粒度就比较合适。
粒度控制还有一个隐藏的坑:子任务之间的依赖关系必须显式声明。我早期版本没有做依赖管理,结果外层调度器有时候会并行下发两个有依赖关系的任务,内层执行的时候发现前置条件不满足,只能报错返回,浪费了一轮。后来我在任务定义里加了depends_on字段,调度器在下发任务前先检查依赖是否满足,不满足就跳过等下一轮。这个改动让无效轮次减少了大概 30%。
2.2 进度追踪与状态机
70 轮连续运转,如果没有一个可靠的状态机来追踪进度,很快就会乱套。我的做法是给每个子任务定义一个状态枚举:pending、in_progress、completed、failed、blocked。外层 Harness 每一轮开始的时候,先扫描所有任务的状态,决定这一轮要推进哪个任务。
状态转换的规则需要仔细设计。比如一个任务从in_progress变成completed,必须满足两个条件:内层 Harness 报告执行成功,并且外层的验收检查通过。只有内层报告成功但外层验收没通过,状态应该变成failed而不是completed。这个区分很重要,因为failed的任务需要重新分解或调整策略,而completed的任务可以直接进入下游。
还有一个实战中总结出来的规则:连续失败两次的任务必须升级处理。如果一个任务连续两轮都返回failed,外层 Harness 不应该继续原样重试,而应该把这个任务标记为blocked,然后尝试分解成更小的子任务,或者调整任务的约束条件。我见过太多 Agent 在同一个任务上反复失败十几轮的情况,就是因为调度器只会无脑重试,不会升级策略。
2.3 验收检查的独立性与严格性
外层 Harness 最重要的职责是验收。这里的原则是:验收必须独立于执行,而且必须比执行更严格。内层 Harness 说"我做完了",外层不能直接信,必须自己跑一遍验收检查。
验收检查的内容取决于任务类型。对于代码生成任务,验收通常包括:代码能否通过语法检查、能否通过单元测试、是否符合代码规范、是否满足接口约定。对于文档撰写任务,验收包括:是否覆盖了所有要求的知识点、是否有事实性错误、格式是否符合要求。对于数据分析任务,验收包括:数据源是否正确、计算逻辑是否合理、结果是否可复现。
我踩过的一个坑是:验收标准太模糊导致外层无法自动判断。早期我写的验收标准是"代码质量良好",结果外层 Harness 根本没法自动检查这个标准,只能靠模型自己判断,而模型自己判断自己写的东西,基本都会说"良好"。后来我把验收标准改成可自动检查的硬性条件,比如"所有函数都有类型注解""单元测试覆盖率不低于 80%""没有超过 50 行的函数",验收就变得可靠了。
提示:验收标准一定要写成机器可检查的形式。凡是需要"主观判断"的标准,要么拆解成可量化的子标准,要么交给独立的评审 Agent 去判断,不要让执行者自己验收自己。
2.4 上下文窗口的滚动管理
70 轮下来,如果每一轮的完整上下文都保留,上下文窗口早就爆了。外层 Harness 需要做上下文滚动管理,我的策略是三层结构:永久层保存全局目标和核心约束,滚动层保存最近 N 轮的摘要信息,临时层保存当前轮的完整上下文。每一轮结束后,临时层的内容被压缩成摘要进入滚动层,滚动层超过 N 轮后最旧的摘要被丢弃。
摘要的生成质量直接影响后续轮次的决策质量。我试过几种摘要策略,最后发现最有效的是结构化摘要:不是让模型写一段话总结,而是让模型填写一个固定格式的摘要模板,包括"本轮完成了什么""遇到了什么问题""下一轮需要注意什么""有哪些未解决的遗留项"。这种结构化摘要在外层调度器做决策的时候更容易解析和利用。
3. 内层 Harness 的执行闭环:从工具调用到自检回报
内层 Harness 是真正干活的那一层。它接收外层下发的子任务和约束条件,调用模型和工具完成任务,然后向外层回报结构化的执行结果。内层 Harness 的设计目标只有一个:在单轮内尽可能高质量地完成任务,并且诚实地报告执行结果。
3.1 工具集的精简与组合
内层 Harness 需要给模型提供工具。工具集的设计有一个反直觉的经验:工具不是越多越好。我早期给内层配了二十多个工具,结果模型经常选错工具,或者在一个简单任务上反复尝试不同的工具。后来我把工具集精简到八个核心工具,每个工具的功能边界非常清晰,模型的选择准确率明显提升。
这八个核心工具大致是:文件读写、代码执行、网络请求、文本搜索、结构化数据解析、格式转换、校验检查、日志记录。每个工具都有明确的输入输出格式和错误码定义。工具的错误码特别重要——模型需要根据错误码来判断是重试、换策略还是上报失败。比如RETRYABLE错误码表示可以重试,FATAL表示必须上报,INVALID_INPUT表示需要修改输入参数。
工具的组合使用是内层 Harness 的一个高级技巧。有些任务单靠一个工具完不成,需要多个工具串联。比如"从网页抓取数据并保存为 CSV"这个任务,需要网络请求工具抓取页面、结构化数据解析工具提取表格、格式转换工具转成 CSV、文件读写工具保存。内层 Harness 需要能够自动编排这些工具的调用顺序,并且在中间步骤失败时知道回退到哪一步。
3.2 自检机制的设计
内层 Harness 在向外层回报之前,必须先做自检。自检的内容包括:任务要求的所有输出是否都产生了、输出是否符合格式要求、是否有明显的错误或遗漏、是否违反了外层下发的约束条件。
自检机制最容易犯的错误是自检标准太宽松。模型天然倾向于认为自己做的东西是对的,如果自检标准不够严格,自检就形同虚设。我的做法是给自检设计一个独立的检查清单,每一项都是二值判断(通过/不通过),而不是让模型自由发挥。比如对于代码生成任务,自检清单包括:语法检查是否通过、所有函数是否有文档字符串、是否有未处理的异常分支、是否使用了禁用的 API。每一项都必须是明确的"是"或"否",不允许"基本是"这种模糊回答。
自检发现问题的处理策略也很关键。如果自检不通过,内层 Harness 不应该直接向外层报告失败,而应该先尝试自我修复。修复的策略包括:重新生成有问题的部分、调整参数重试、换一种实现方式。只有自我修复也失败的情况下,才向外层报告失败,并且要在回报中说明已经尝试过哪些修复策略。
3.3 结构化回报的字段设计
内层向外层的回报必须是结构化的。我用的回报格式大致包含这些字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| task_id | string | 对应的子任务 ID |
| status | enum | success / partial / failed |
| outputs | array | 产生的输出文件或数据 |
| self_check | object | 自检结果,每项为布尔值 |
| errors | array | 遇到的错误及处理情况 |
| suggestions | array | 对后续轮次的建议 |
| context_summary | string | 本轮的结构化摘要 |
这个格式是经过多次迭代才稳定下来的。早期版本没有suggestions字段,结果外层调度器在做决策的时候缺少内层的反馈信息,经常做出不合理的调度。加上这个字段之后,内层可以把"这个任务其实还需要一个前置任务""当前方案有性能隐患"这类信息传递给外层,外层的决策质量明显提升。
context_summary字段的格式也需要统一。我要求内层按照固定模板填写:完成了什么、没完成什么、为什么没完成、下一步建议。这个模板强制内层做结构化的反思,而不是随便写一段话。
3.4 错误恢复的分级策略
内层 Harness 在执行过程中会遇到各种错误。错误恢复的策略需要分级:一级错误是瞬时的、可重试的,比如网络超时、临时文件锁,直接重试即可;二级错误是需要调整参数的,比如输入格式不对、依赖版本不匹配,需要修改后重试;三级错误是需要改变策略的,比如当前方案根本走不通,需要换一种实现思路;四级错误是需要上报的,比如缺少必要的权限、依赖的外部服务不可用。
分级的关键是让模型自己判断错误级别,而不是由 Harness 硬编码。因为同一个错误码在不同场景下可能属于不同级别。比如"文件不存在"这个错误,如果是在读取一个应该存在的文件时发生,那是三级错误(方案有问题);如果是在检查一个可选文件时发生,那可能只是一级错误(忽略即可)。Harness 提供错误分类的指导原则,具体判断交给模型。
我实测下来,分级错误恢复能把内层的任务完成率提升 20% 到 30%。因为很多错误其实是可以自动恢复的,只是早期版本没有做分级,所有错误都直接上报,导致外层收到大量本可以内部消化的失败报告。
4. 两层之间的通信协议:为什么结构化数据比自然语言可靠
两层 Harness 之间的通信是整个架构的血管。通信协议设计得好,两层配合流畅;设计得不好,两层互相拖累。我在这上面踩过的坑最多,也最有心得。
4.1 自然语言通信的陷阱
最开始我让两层之间用自然语言通信,外层发一段话描述任务,内层回一段话汇报结果。看起来很简单,实际跑起来问题一大堆。
第一个问题是歧义。外层说"尽快完成这个任务",内层理解成"用最快的方式完成",结果跳过了必要的验证步骤。外层说"确保代码质量",内层理解成"加注释",结果代码逻辑本身有问题但注释写得很漂亮。自然语言的歧义在人类之间可以通过追问来消除,但两层 Harness 之间没有追问机制,歧义就直接变成了错误。
第二个问题是解析困难。外层需要从内层的自然语言汇报中提取结构化信息,比如任务是否完成、产生了哪些输出、遇到了什么错误。这些信息在自然语言里没有固定位置,外层只能靠模型去解析,而模型解析自然语言经常出错。我遇到过内层说"基本完成了,但有一个小问题",外层解析成"完成了",然后继续推进下游任务,结果下游任务因为那个"小问题"全部失败。
第三个问题是不可审计。自然语言通信没有固定的格式,出了问题很难定位是哪一层的责任。结构化数据通信的话,每个字段都有明确的来源和含义,出问题可以直接定位到具体的字段和值。
4.2 通信协议的具体设计
我最终的通信协议设计是这样的:外层向内层下发任务时,使用一个结构化的任务描述对象,包含任务 ID、任务类型、输入数据、约束条件、验收标准、超时限制这些字段。内层向外层回报时,使用前面提到的结构化回报格式。
任务描述对象的关键字段设计:
task_id:全局唯一,用于追踪和关联task_type:任务类型枚举,决定内层用哪套执行策略inputs:输入数据,可以是文件路径、URL、结构化数据constraints:约束条件,比如"不能使用外部库""必须在 30 秒内完成"acceptance_criteria:验收标准,外层验收时逐项检查timeout:超时限制,超过后内层必须中止并回报priority:优先级,用于外层调度排序
约束条件和验收标准是最容易设计不好的部分。约束条件要具体、可检查,比如"不能使用外部库"比"尽量少依赖"好得多。验收标准要可自动化检查,比如"所有函数都有类型注解"比"代码规范"好得多。
4.3 版本兼容与协议演进
通信协议不是一成不变的。随着项目推进,你会发现需要增加新的字段、修改字段的语义、废弃旧的字段。这时候版本兼容就很重要。
我的做法是给协议加版本号,外层和内层各自声明自己支持的协议版本。如果版本不匹配,外层在启动时就会报错,而不是跑到一半才发现字段对不上。协议演进时,新字段一律设计为可选,旧字段废弃时先标记为 deprecated 但保留一段时间,等所有内层都升级后再移除。
还有一个实战经验:协议字段的命名要极其明确,不要用缩写或模糊词。我早期用ok表示任务是否成功,后来发现ok的语义太模糊——是执行成功还是验收成功?是部分成功还是完全成功?后来改成execution_status和acceptance_status两个字段,语义就清晰了。
5. 70 轮连续运转的实测数据与失效模式分析
前面讲的都是设计层面的东西,这一节讲实测。我在一个中等规模的代码重构项目上跑了完整的 70 轮连续运转,记录了一些关键数据和失效模式。
5.1 轮次分布与时间开销
70 轮里,真正在"干活"的轮次大概占 60%,也就是 42 轮左右。剩下的 28 轮里,大约 15 轮是验收和调度开销,8 轮是错误恢复,5 轮是上下文整理和摘要生成。这个分布说明调度和验收的开销不小,如果任务分解得不够好,调度开销占比会更高。
单轮平均耗时大约 4 分钟,其中模型推理占 60%,工具调用占 25%,验收检查占 15%。70 轮总耗时大约 4.7 小时。这个时间开销对于需要连续处理的长任务来说是可以接受的,但如果任务本身可以并行,可以考虑多个内层 Harness 并行执行,外层做并行调度。
5.2 典型失效模式
70 轮里遇到的失效模式大致可以归为几类:
上下文漂移是最常见的失效模式。跑了二三十轮之后,内层开始"忘记"早期的约束条件。比如早期约定"所有 API 调用必须加超时处理",跑到后面内层生成的代码就不加超时了。原因是滚动层的摘要没有把约束条件完整保留下来。修复方法是在永久层里显式保存所有约束条件,每一轮都重新注入。
验收标准漂移是第二常见的。外层的验收标准在早期定义得很严格,但随着轮次推进,为了"让任务能推进下去",验收标准被不知不觉地放松了。这个问题的根源是验收逻辑里有一些"容错"分支,在特定条件下会跳过某些检查。修复方法是把验收标准写成不可变的配置,每一轮都从配置里读取,不允许运行时修改。
死循环是第三常见的。某个任务反复失败,外层反复重试,内层反复用同样的方式执行,形成死循环。修复方法就是前面提到的"连续失败两次必须升级处理"规则。
资源泄漏是第四常见的。内层在执行过程中创建了临时文件、打开了网络连接、申请了内存,但任务失败时没有正确释放。跑了几十轮之后,资源耗尽导致后续轮次全部失败。修复方法是在内层的错误处理里强制加入资源清理逻辑,不管任务成功还是失败,都要释放资源。
5.3 失效模式的排查链路
遇到失效模式时,排查链路很重要。我的排查顺序是:先看外层的调度日志,确认失效发生在哪一轮、哪个任务;再看内层的执行日志,确认内层在这一轮做了什么、遇到了什么;然后看两层之间的通信记录,确认信息传递是否准确;最后看上下文摘要,确认是否有信息丢失。
这个排查链路的关键是日志的完整性和可关联性。每一轮的日志都要有统一的 trace_id,外层的调度日志、内层的执行日志、通信记录都要带上这个 trace_id,这样才能把一轮的完整信息串起来。我早期没有做 trace_id,排查问题的时候要在几个日志文件里来回翻,效率极低。
注意:日志的存储要有轮转策略。70 轮的完整日志可能有好几百 MB,如果不做轮转,磁盘很快就满了。我的做法是保留最近 20 轮的完整日志,更早的日志只保留摘要和错误信息。
6. 从单层到双层:迁移过程中的实操建议
如果你已经在用单层 Harness,想迁移到双层架构,这一节是我踩过坑之后总结的实操建议。
6.1 先做职责拆分,再做代码拆分
迁移的第一步不是写代码,而是想清楚哪些职责归外层、哪些归内层。我的建议是拿一张纸,把当前单层 Harness 的所有职责列出来,然后逐个判断:这个职责是"决定做什么"还是"决定怎么做"?前者归外层,后者归内层。判断不清楚的,先归内层,因为内层更接近执行,职责归内层至少不会导致执行出问题。
职责拆分完成后,先不要动代码,而是用文档把两层的接口定义清楚。接口定义包括:外层向内层下发什么、内层向外层回报什么、两层的启动和停止顺序、异常情况的处理约定。接口定义清楚了,代码拆分就是体力活。
6.2 通信协议先跑通最小闭环
代码拆分的时候,不要一上来就实现完整的通信协议。先实现一个最小闭环:外层下发一个最简单的任务,内层执行后回报一个最简单的结构化结果,外层验收后标记完成。这个最小闭环跑通之后,再逐步增加字段、增加任务类型、增加错误处理。
我见过有人一上来就实现完整协议,结果字段设计不合理,跑起来各种对不上,改起来牵一发动全身。最小闭环的好处是快速验证协议的基本设计是否合理,不合理的话改起来成本低。
6.3 灰度迁移而不是一次性切换
如果单层 Harness 已经在生产环境跑着,不要一次性切换到双层。我的做法是灰度迁移:先让双层架构处理一小部分任务,单层继续处理大部分任务,对比两边的成功率和耗时。跑一段时间确认双层确实更好之后,再逐步扩大双层的任务比例,直到完全切换。
灰度迁移期间,两套架构的日志要分开记录,方便对比。对比的指标包括:任务成功率、平均轮次、平均耗时、人工介入次数。我实测下来,双层架构在长任务上的成功率比单层高 25% 左右,但在短任务上反而略低,因为双层的调度开销在短任务上不划算。所以迁移策略可以是:长任务用双层,短任务继续用单层。
6.4 监控与告警的配套建设
双层架构的监控比单层复杂,因为要监控两层各自的健康状态和两层之间的通信质量。我的监控指标包括:外层的调度成功率、内层的执行成功率、通信协议的错误率、平均轮次耗时、上下文窗口使用率、资源占用情况。
告警规则要设置得合理。比如"内层执行成功率低于 80%"应该告警,"单轮耗时超过 10 分钟"应该告警,"连续 3 轮没有任务状态变化"应该告警。告警太多会麻木,告警太少会漏掉问题。我的经验是先从最关键的几个指标开始,跑一段时间后根据实际情况调整。
7. 这套架构适合什么场景,不适合什么场景
最后说说适用边界。Harness 套 Harness 不是银弹,它有明确的适用场景和不适用场景。
适合的场景:任务链条长、需要多轮迭代、对执行质量要求高、需要独立验收的复杂任务。比如大型代码重构、多步骤数据分析、需要反复调试的算法实现、跨多个文件的文档撰写。这些场景的共同特点是单轮完不成、需要持续追踪进度、错误代价高。
不适合的场景:短平快的单轮任务、对延迟极度敏感的任务、任务分解本身就很困难的任务。短任务用双层架构是杀鸡用牛刀,调度开销比执行开销还大。对延迟敏感的任务,双层的通信开销会明显增加延迟。任务分解困难的任务,外层根本不知道该下发什么,双层架构反而增加了复杂度。
还有一个边界条件:内层 Harness 的能力上限决定了整个系统的能力上限。外层再会调度,内层执行不了就是执行不了。所以如果发现内层经常在某些类型的任务上失败,优先考虑增强内层的能力(换模型、加工具、改策略),而不是在外层加更多的调度逻辑。
我在实际使用中的一个体会是:双层架构的价值不在于"更聪明",而在于"更可靠"。单层 Harness 在理想情况下也能完成任务,但双层架构通过职责分离和独立验收,把"理想情况"变成了"常态"。70 轮连续运转不崩,靠的不是某一层的强大,而是两层之间的相互制衡和纠错。这个思路其实和很多工程领域的实践是一致的——冗余和校验是可靠性的基石,Agent 系统也不例外。