这半年我一直在跟长程 Coding Agent 任务死磕,跑过一轮又一轮改造后,最直观的感受是:光有聪明的模型远远不够,想让一个 Agent 在千万 Token 级别的代码库里稳扎稳打跑完一个几小时的长任务,必须给它套上足够结实的“缰绳”。这个“缰绳”,业内现在有个挺形象的叫法——Harness Engineering。说白了,就是围绕 Agent 设计一套可控的执行框架:怎么给任务分段、怎么控制上下文流动、怎么用外部工具校验每一步结果、怎么在跑偏时及时拉回来。
如果你也在搞 Coding Agent 相关的自动化、批处理、跨模块改造,或者纯粹被长上下文任务的“智障时刻”折磨过,这篇文章应该能给你一些能直接抄走的经验。我会从翻车现场讲起,拆解 Harness 的设计思路,再给出一套我实际验证过的最小实现框架,最后整理一堆调试实录和反直觉的避坑点。
1. 为什么需要缰绳:先是翻车,然后才是工程化
1.1 我最早遇到的那些“野生 Agent”事故
先说说我不加任何控制结构、直接把大模型塞进代码库时的惨状。那是一个大约两百万行代码的旧系统,我给 Agent 的任务是“把某个内部 API 调用从旧接口迁移到新接口”,涉及约 40 个文件、横跨 5 个模块。听起来不算复杂,但跑起来之后问题接二连三。
最典型的是上下文漂移。Agent 在调完前两个文件后,第三个文件就忘了最初的迁移约束,开始“自由发挥”。它甚至自己发明了一个新接口名字,然后在代码里引用了那个不存在的函数,还试图给我生成一个兼容性的 shim 层。我检查的时候哭笑不得——这个 shim 层写得还挺工整,但整个方案完全偏离了任务边界,因为它把早期分析阶段的中间结论当成了无可争议的事实,越走越远。
第二个问题是没有校验闭环。Agent 改完代码自信满满地汇报“完成”,但编译根本过不了。它的解释是“接口签名有微调”,甚至建议我改掉调用方的传参顺序去迎合它的改动。这种结果本质上是因为它只看到了任务描述,没有在每次修改后真正跑编译、跑测试,缺少外部环境的硬反馈。
第三类问题是效率灾难。任务在早期阶段就把上下文窗口吃满了。它每读取一个文件就把全文塞进对话,很快就触达窗口上限。后面的阶段它在压缩和遗忘中挣扎,有时候你看到它输出的 token 里夹杂着早期某个文件的末尾几行,明显是从上下文里“误搬”过来的。我后来统计了一下,这类没有 Harness 的任务,成功率大概只有三成,而且失败模式千奇百怪。
1.2 为什么会跑偏:模型是聪明的,但它没有“约束下的行动”概念
去责怪模型没有意义,它本质上就是一个靠概率预测下一个 token 的系统。问题出在“任务形态”和“模型特性”不匹配。编码任务恰恰是对一致性、边界性、可验证性要求最高的场景之一,而裸奔的 Agent 天生有几个毛病。
第一是中间结论的遗忘误差。早期的决策一旦被新内容挤占,Agent 就会用自己“补脑”出来的新结论替换旧结论,而且它不会主动提醒你“我开始猜了”。第二个毛病是行动和推理互相污染。它一边读代码一边做修改,推理链里混着环境信息、任务细节、模型自身联想,上下文越乱,后续输出的置信度越差。第三个是“完成任务”的幻觉。只要看起来像完成了,模型就会说完成了,尤其在没有外部校验的时候,它往往会给自己编一个合理的解释。
其实这跟人做事的道理是一样的。一个新手程序员拿到一个大需求,如果不把任务拆成可验证的步骤、不做阶段检查、不控制输入端的信息流,他也同样会跑偏。只不过人好歹有常识和记忆,Agent 的常识更弱,记忆更不可靠,所以更需要外部系统替它管理状态。
Harness Engineering 解决的问题,就是把“自由发挥”的模型变成一个“受控执行力”的执行器。它不改变模型本身,而是改变模型被使用的方式:控制输入、控制步骤、控制校验、控制恢复路径。这也是我认为这个方向最有价值的地方——你不需要等待更聪明的模型,只需要把现有模型放到正确的框架里,效果就能翻几倍。
提示:我后来跟不少团队聊过,大家遇到的高频失败模式高度一致。如果你也感觉 Agent “时灵时不灵”,先别急着换模型,大概率是缺一个 Harness。
2. 缰绳的设计思路:把执行变成一条受控的生产线
2.1 从“一个超大 Prompt”到“规划-执行-校验”三段式
早期我做自动化任务,习惯把需求、代码库结构、约束、示例全写进一个巨大的 Prompt,然后期待模型一口气输出完整方案。这在短任务、小范围内可能可行,但在千万 Token 级长任务里几乎必死。原因很简单:上下文窗口是有限的,而长任务的分析路径是无限的。
后来我调整思路,把整个流程拆成三段:规划、执行、校验。规划阶段只负责从仓库中抽取最高层的结构信息和任务约束,生成一个分阶段的执行计划;执行阶段按照计划逐个访问文件、生成修改;校验阶段对每次修改运行编译、lint、单元测试等外部检查,把结果反馈给下一个步骤。关键变化是:Agent 不再“一次性完成整个任务”,而是成为一条受控生产线上的工人,每一步都有自己的输入、输出和验收标准。
这种设计有一个额外好处:可观测性。每一个阶段都有独立的日志、独立的输入输出记录,你随时能知道 Agent 在干什么,哪里顺利,哪里开始拖延。这对调试和优化至关重要。以前一个大 Prompt 拍下去,出问题了根本不知道是哪段推理逻辑在胡扯。
2.2 缰绳的三条主线:路径、步进、反馈
我后来把 Harness 的职责浓缩成三条主线。
第一条是控制路径。你决定 Agent 能看什么文件、按什么顺序看、修改什么文件、不能修改什么。这一步在 Harness 里通常表现为定义“任务作用域”和“读取策略”。不要觉得这样限制自由度会降低效果,恰恰相反,路径控制能减少无效探索,也能防止 Agent 改到不该改的地方导致连锁爆炸。比如迁移 API 的任务,我会把调用方文件列表预先算好,Agent 只能在这个白名单里活动。
第二条是步进机制。任务被拆成若干紧密耦合的原子步骤,每一步的目标必须单一、明确、可判定。比如“修改文件 A 中第 30 行的函数调用”就是一个好步骤;“彻底优化这个模块的性能”就不是。每一步执行后都强制停顿,进入校验状态,只有校验通过才能进入下一步。
第三条是反馈闭环。单纯跑完代码是不够的,必须有外部信号的注入。我常用的方式包括:编译错误流、测试失败输出、静态检查结果、甚至自定义断言——比如“新增代码里不允许再出现旧接口名的引用”。这些反馈会被格式化成标准结构喂回给模型,作为下一步行动的决策依据。
用生活化的比喻来说,这三个机制就像骑马时的缰绳、鞍具和方向指令。缰绳控制方向不让马乱跑,鞍具让你稳稳待在马上不掉下来,方向指令告诉马每一段该往哪走。Harness Engineering 就是同时做好这三件事。
3. 上下文工程:千万 Token 长任务的关键战场
3.1 记忆失控的两个典型现场
我见过两种最容易让长任务崩溃的上下文问题,基本可以覆盖九成的失败案例。
第一种叫“维基化遗忘”。任务前期分析了很多仓库结构信息,比如模块依赖关系、接口定义、历史变动。这些信息在任务后半段其实只有少数几条是真正关键的,但 Agent 分不清哪条关键,于是后期它要在一堆过时信息里找方向。更麻烦的是,模型对“遗忘”没有感知,它不会主动回去翻原始资料,而是用开头几行被反复提及的信息强行支撑结论。结果就是前期的某个非关键配置被当成了“既定事实”,后面所有决策都在这个沙地上建房子。
第二种是“词法污染”。长任务里难免有一些风格相似的函数名、变量名,或者多个新老接口并存。Agent 写着写着把新接口的参数顺序和旧接口搞混,或者无意间引用了被标记为废弃的类型。这种错误的典型特征是在 diff 里怎么看怎么像“手滑”,但它会反复出现,因为上下文里新旧信息互相纠缠,模型没有足够的约束去区分。
3.2 解决思路:把上下文做成“外部数据库”而不是“全文背诵”
面对这些问题,我最后采纳的方案是:尽量少把原始大块内容塞进上下文,而是在 Harness 层维护一个“可检索的外部记忆”。
具体实现上,仓库信息经过预处理后按需注入。比如任务需要了解某个模块,先让 Harness 去读该模块的函数列表和公共接口,再通过一次工具调用来提取与当前任务相关的局部实现。Agent 一次只需要看到小块的、与眼前步骤相关的代码,而不是整个仓库的地图。这就像你让人去维修一栋大楼,不需要让他背完整张图纸,只需要引导他到目标楼层,给他看那个房间的电路图。
同时,我维护一个“决策日志”。每一步的关键推理过程、结论、依据文件、时间戳,都会被结构化成日志存下来。Agent 在当前步骤需要回看早期的某个决策时,不是从窗口里翻,而是调用 Harness 提供的搜索工具,按关键词拉取对应的决策记录。这样作决策的原始上下文留在外部,模型只在需要时“查档”,大大减少了窗口内的信息污染。
还有一点很反直觉:主动给上下文“瘦身”。在完成一个大阶段后,我会故意清空当前的对话上下文,把阶段结论总结成几百字的摘要,再作为新阶段的开场信息。很多同学担心这样会丢失细节,其实对比下来,好处远大于坏处。因为阶段内部的中间过程很少需要跨阶段引用,而真正跨阶段的关键结论在摘要里已经提炼出来了。那些原始细节即使还在上下文里,也会在长序列中被忽略,还不如清掉。
注意:上下文瘦身的时机很讲究。不能在一个步骤的执行中途清理,必须在步骤边界、下一个步骤还没开始之前。否则模型会在执行中“失去记忆”,产生前言不搭后语的修改。
4. 最小可用的 Harness 实现:从框架到代码
4.1 主干循环:一个可运行的 Agent 驱动骨架
下面这版实现是我在实际项目中跑过很多次的简化骨架。它没有依赖任何重量级框架,核心是一个循环加三个组件:规划器、执行器、校验器。你可以直接把它当模板改。
# harness_minimal.py import json from typing import Any, Dict, List, Optional class HarnessContext: """保存任务级状态与外部记忆入口""" def __init__(self, task_spec: Dict[str, Any]): self.task_spec = task_spec self.step_index = 0 self.completed_steps: List[Dict] = [] self.known_facts: Dict[str, Any] = {} self.current_context: List[Dict] = [] def push_context(self, item: Dict): self.current_context.append(item) def build_prompt(self, instruction: str) -> str: # 把上下文压缩为当前步骤所需的片段 ctx = json.dumps(self.current_context[-20:], ensure_ascii=False) return f"任务背景:{self.task_spec['goal']}\n已知事实:{json.dumps(self.known_facts)}\n当前上下文:{ctx}\n当前指令:{instruction}" def run_step_with_llm(prompt: str) -> str: # 这里替换成你的模型调用封装 raise NotImplementedError def execute_harness(task_spec, planner, validator) -> Dict: ctx = HarnessContext(task_spec) plan = planner.generate_plan(task_spec) for step in plan["steps"]: # 1. 生成当前步骤的定向指令 instruction = step["instruction"] prompt = ctx.build_prompt(instruction) # 2. 模型执行动作,返回结构化动作(修改、查询、读取) action = json.loads(run_step_with_llm(prompt)) # 3. 执行动作(这里是调用外部工具) result = execute_action(action, ctx) # 4. 校验器判断该步骤是否通过 ok, feedback = validator.check(step, result, ctx) # 5. 无论通过与否,记录决策日志,便于后续追溯 ctx.completed_steps.append({ "step": step["id"], "action": action, "result": result, "ok": ok, "feedback": feedback, }) if not ok: # 失败后允许有限次数重试,重试时应注入错误反馈 for attempt in range(2): retry_prompt = ctx.build_prompt( f"上一步校验失败,原因:{feedback}\n请重新给出动作。" ) action = json.loads(run_step_with_llm(retry_prompt)) result = execute_action(action, ctx) ok, feedback = validator.check(step, result, ctx) if ok: break if not ok: # 超过重试次数则整体中止,保留现场用于人工介入 return {"status": "failed", "fail_step": step["id"], "trace": ctx.completed_steps} # 6. 通过后更新全局已知事实,供后续步骤检索 if step.get("facts_to_update"): ctx.known_facts.update(step["facts_to_update"]) # 步骤边界:清空会话中早期琐碎上下文,保留摘要 if step.get("summarize"): summary = summarize_steps(ctx.completed_steps[-3:]) ctx.current_context.clear() ctx.push_context({"type": "summary", "text": summary}) ctx.step_index += 1 return {"status": "success", "trace": ctx.completed_steps}这段代码的精髓在于几个小地方。首先是build_prompt会限制上下文的注入来源,模型每步看到的不是整个对话历史,而是结构化的事实表加最近二十条上下文。其次是失败重试不是简单地把同一 Prompt 再发一遍,而是把校验器的“具体失败原因”注入进去,让模型基于错误信息修正动作,而不是凭感觉重试。第三是步骤边界的summarize机制,它把前面几个步骤的结果浓缩成摘要,清空中间细节,避免后续步骤被陈旧的中间输出干扰。
4.2 校验器设计:让外部信号替模型“把关”
校验器是 Harness 里最值得下功夫的部分,因为它决定了整个执行链的质量天花板。一个设计得好的校验器,能把模型从“概率输出”牢牢约束在“真实可运行”的范围内。
我的校验器分三层。第一层是语法编译层,直接跑编译器和 linter,错误信息原样提取。这层能挡住八成低级的错误。第二层是语义层,跑单测和轻量级断言,比如检查某个被废弃的 API 引用是否存在、某个新增的类型是否被正确导入。这层能挡住逻辑错位的问题。第三层是任务约束层,这并不是系统自动能算出来的,需要你在任务开始前写下几条可验证的约束规则。比如“迁移后全仓库不得再出现旧接口名”,校验器就可以用正则扫描全仓库来检查。
如果你没有时间做复杂的规则引擎,也有一个讨巧的办法:让校验器调用另一个独立的模型做“交叉检查”。比如执行模型修改完代码后,校验模型以“挑刺”模式审查 diff,专门找逻辑不一致、未处理边界、引用错误。两个模型相互独立,可以有效避免同一个模型对自己的输出过于自信。这个方法实现成本很低,效果却出乎意料地好。
我个人的经验:校验器宁严勿松。宁可多误报几次、多花几个重试 Token,也不要放过一个关键错误放它进入后续步骤。因为一个阶段性错误进入后面的步骤后,修复成本会指数级上涨——Agent 后期的调整经常会基于这个错误方案做小修小补,产生大量的连带错误。
5. 常见问题与排查实录:你也会踩的坑
5.1 问题速查表
| 现象 | 常见原因 | 处理方案 |
|---|---|---|
| 步骤校验失败但模型反复给出相同答案 | 失败反馈被淹没在长上下文中 | 缩短上下文、把失败原因放在 Prompt 最显眼处,甚至单独作为一个 system 块 |
| 任务跑到一半开始重复分析同一个文件 | 缺少“已访问”记录,模型不知道它做过这件事 | 在 Harness 中维护动作历史表,禁止重复读取 |
| 生成的代码风格前后不一致 | 跨步骤摘要丢失了风格示例 | 在全局 facts 中固定写入风格约定样本,每次注入 |
| Agent 自己修改了任务边界之外的文件 | 路径控制失效,模型误以为可以自由探索 | 把文件路径改动权限收敛到 Harness 的允许名单,拦截非法路径 |
| 上下文窗口仍然超限 | 单次读取文件太大,或者重试次数太多 | 对超大文件按函数/类分段读取;减少最高重试次数 |
| 重试后反而引入了新错误 | 失败信息不具体,模型靠猜测重新输出 | 将编译错误中的文件、行号、符号直接注入,禁止模型在没有新信息的情况下重试 |
5.2 一个真实的排查过程
有一次长任务运行到第 23 步时,Agent 突然在某个模块的日志组件上反复失败。它的任务是给一个旧日志系统加指标统计前缀,校验器连续三次报出“引用了未定义的常量 LOG_PREFIX”。我本来以为是小问题,但连续重试 5 次都没有解决,而且它每次生成的代码都包含不同的新错误。
我打开日志看,发现一个关键细节:Agent 在开始这一阶段时,上下文里其实已经没有这一段任务最初使用的常量定义文件内容了。它只能凭早期摘要中提到的模糊描述来猜,而摘要只写了“使用日志前缀常量”,没有写明定义在哪个文件、具体值是什么。模型在盲区里尝试了 5 次,越试越错。
解决办法也算简单。我在全局 facts 里增加了“关键符号定义索引”,提前把仓库里所有被任务引用的符号以及它们的定义位置整理成一个表。当 Agent 需要某个符号时,Harness 通过检索工具把定义文件的具体内容注入上下文,而不是靠摘要猜。改了之后,这一个阶段只花了一次执行就通过。
这个案例给我的教训很深刻:上下文中“有过”和“永远可用”是两回事。长任务发展到后期,早期读取的细节几乎必然被挤出窗口,所以关键符号、关键约束必须显式地写入持久化的外部记忆,随用随取。
5.3 反常识经验:控制重试次数反而提升质量
大部分人认为重试越多成功率越高,我在 Harness 里的实测结论恰恰相反。当重试超过两次之后,模型的每次输出常常会带有“赌徒心态”,偏向于大幅修改而不是微小修正,反而破坏原本接近正确的方案。后来我把单步重试上限设成 2 次,超过就中止并人工介入。整体成功率没有下降,反而因为减少了“二次破坏”而提高了。
另外还有一个细节:重试时的 Prompt 要尽量短。错误信息已经足够引导修正,没必要把整段历史都堆上去。我通常会清空除错误信息之外的中间内容,在重试时只保留“错误的文件、行号、符号、错误类型”和“上一步动作的一句话摘要”,效果很好。
6. 从最小实现走向规模化:给多仓库系统套缰绳
6.1 成本与性能的取舍
当你把 Harness 逻辑跑通后,下一个问题就是能否撑住大规模、多任务并行的场景。我面对的典型场景是同时跑 10 到 20 个不同模块的长任务,每个任务都会有独立的 Harness 实例、独立的日志、独立的决策记录。
编译型校验在这里会有显著的耗时开销。一个大型仓库每次全量编译接近 10 分钟,这放在单任务里可以接受,并行多任务时就受不了。我的做法是给校验器加“增量编译”模式,只在本次修改涉及的文件范围内做编译,配合全局符号索引做引用检查。这样校验耗时从 10 分钟降到 20 秒左右,而错误检出率只下降了几个百分点。
另一点是 Token 成本。长任务追踪起来,模型调用消耗的大头往往不在核心执行,而在重试和校验过程中产生的多余推理。控制重试次数、限制上下文注入量、及时清理历史,这三件事对 Token 成本的影响比想象中大得多。我在一次对比实验里,把上下文注入策略从“保留最近 50 条”改成“保留最近 10 条 + 全局事实表”,在成功率不变的前提下,单任务 Token 消耗下降了约 40%。
6.2 给 Harness 写“组合式扩展”而不是“全盘重写”
很多开发者容易犯一个毛病:任务 A 的 Harness 写得很好,到了任务 B 就推倒重来。实际上,Harness 的核心骨架大同小异,差异主要在三块:规划器怎么拆步骤、校验器用什么规则、上下文注入策略怎么定制。把这三块做成可配置的组件,就能快速适配各种任务类型。
比如任务 A 是 API 迁移,它的校验器重点做“旧接口引用扫描”;任务 B 是性能优化,校验器重点做“热点路径 benchmark 对比”;任务 C 是跨模块重构,规划器就需要特别关注拓扑排序,确保按依赖顺序修改文件。它们的区别都只在插件层面,外层循环、状态管理、重试策略完全可以复用。
这也是 Harness Engineering 最有魅力的点:它把“让 Agent 做一件事”从一次性的魔法变成了工程化的流水线。上层系统的复杂度被收敛到了有限的、可测试的、可观测的几个组件里,出现问题你能定位、能回滚、能调整策略,而不是对着一个大黑盒干瞪眼。
我在实际跑完几十个长任务后的体会是,Harness Engineering 的投入产出比非常高。你不需要拥有很强的模型基础设施,只需要把一个统一的执行骨架做好,再把每个任务的领域约束塞进校验器。哪怕模型版本不升级,整体成功率也能从三成左右提升到八成以上,而且可预测性强得多——跑挂了你知道它挂在哪一步,挂的原因是什么,这比“玄学成功”重要太多了。
如果你现在手里的 Coding Agent 还处于裸奔状态,我建议你从这套最小骨架开始改。先加校验闭环,再加路径控制,然后逐步补上外部记忆和步骤摘要。每一步的改造成本都不高,但对成功率的提升非常直观。等这套框架跑顺了,你再回头看那些曾经让人崩溃的长任务,会发现它们真的只是缺了一层好缰绳而已。