如果你自己动手跑过 Coding Agent,八成有过这种体会:单独让它写个函数、补个测试,速度确实快;可一旦把“把这个项目做完”这种大目标扔给它,它很快就原形毕露——写了一半忘掉原始需求、跑挂了测试不修还要往下写、改数据库结构都不提前吱一声。更别说连续跑几十轮,基本上十轮以内就得人工介入一次。最近上海 AI Lab 公开分享的“Harness 套 Harness”方案,核心思路就是给 Coding Agent 套上两层工作流框架,用严格的编排和验收规则约束它,让它连续自主执行 70 轮任务:环境搭建、代码生成、测试修复、文档汇总,一条流水线从头跑到尾。这篇博文把这一思路的原理解析、复现路径和踩坑记录完整写出来,适合正在做 Agent 开发、AI 辅助编程,或者想给团队搭一套“自动开发流水线”的工程师参考。
1. 核心思路拆解:为什么要“Harness 套 Harness”
1.1 先想清楚一个基础问题:Harness 到底是干什么的
很多第一次接触“Harness”的人都会把它当成某种新模型或者新语言,实际上它跟模型完全是两码事。Harness 这个词本身是“安全带、工具架”的意思,在 AI Agent 开发里,它指的是独立于模型之外的工作流编排与行为约束层。
我习惯用一个类比来讲这件事:如果你把 Coding Agent 当成一个业务能力很强但完全不懂公司规矩的实习生,那模型是“聪明的大脑”,Harness 就是“工牌、作业指导书和打卡机”。Agent 负责想、负责做,Harness 负责规定它应该做什么、按什么顺序做、做到什么程度算完、出问题谁说了算。
两者的职责对比如下:
| 角色 | 对应物 | 核心职责 |
|---|---|---|
| Agent | 干活的实习生 | 基于模型做决策,调用工具,产出代码和文本 |
| Harness | 工牌 + 作业指导书 + 审批流 | 管理任务状态,约束行为边界,控制轮次,执行验收 |
这个区分非常重要。很多人用 Agent 翻车,不是模型不行,而是没有 Harness 管住它。模型给了一段“看起来合理”的回答,Agent 就真的去执行了,哪怕这段回答跟实际项目状态完全脱节。Harness 存在的意义,就是把这个“看起来合理”变成“经过校验的合理”。
1.2 “套两层”的真正含义:项目经理 + 实施小组
单层 Harness 已经能约束单个 Agent,为什么还要套两层?这是这套方案里我觉得最关键的设计。在处理复杂开发任务时,单层 Harness 会碰到两个现实问题。
第一个问题是上下文长度。一个 Agent 的上下文窗口是有限的,你要它同时记住原始需求、技术方案、当前文件内容、最近的测试输出,还要它处理几十个子任务,跑到后半程上下文必然溢出,模型开始“选择性失忆”。
第二个问题是任务间的耦合。一个开发任务往往包含多个阶段:环境搭建完之后才能写代码,代码写完之后才能跑测试,测试挂了又得回头改代码。如果所有阶段都挤在一个 Harness 里,任何一步出问题,整条流水线就得回滚重来,状态管理会非常混乱。
“Harness 套 Harness”就是把这些问题分层处理。外层 Harness 充当项目经理,负责把大需求拆成子任务、维护任务队列、判断子任务验收是否通过、决定什么时候进入下一轮;内层 Harness 充当实施小组,每个子任务内部有一个独立的 Coding Agent 专注当前这一件事,它不需要关心全局,只需要在给定的小范围内完成编码、编译、测试这个小循环。
外层的状态空间很小,只关心“任务做到哪了、有没有达标”;内层的上下文很干净,只关心“当前这个模块怎么实现”。两者解耦之后,70 轮连跑才不会演变成上下文灾难。
1.3 连续 70 轮到底意味着什么
70 轮这个数字,第一次看到会觉得夸张,但拆开看其实就是一整个开发任务的生命周期。我复现的时候大致按这种比例分配过:
- 需求解读与任务拆解:约 5 轮
- 环境准备与依赖管理:约 8 轮
- 项目骨架与接口设计:约 10 轮
- 核心逻辑实现与模块联调:约 22 轮
- 测试编写与缺陷修复:约 15 轮
- 文档生成与收尾检查:约 10 轮
合计正好 70 轮附近。所以说“让 Agent 连续干 70 轮”并不是刻意刷数字,而是把原本需要人类开发者在 IDE 里来回切换、举棋不定、反复 debug 的整个过程,用一套可编排的工作流交给 Agent 自治完成。
这种长时自治任务还有一个隐性价值:每一轮的结果都是结构化的、可回溯的。人工开发时,“我当时是怎么想到这个方案的”往往说不清;Agent 跑完 70 轮,每一轮的任务、动作、产物、验收结论全部有日志,这本身就是一个极好的项目资产。
2. 支撑 70 轮连跑的关键设计细节
2.1 任务拆解与验收标准:每个子任务都必须带退出条件
我见过很多失败的 Agent 工作流,问题根源不是模型智商不够,而是任务定义不清晰。开发者只告诉 Agent“写一个用户登录模块”,Agent 就按照自己对“登录模块”的理解开工了,写完说一声“完成”就退出。至于登录需不需要 token 刷新、需不需要验证码、数据库表结构怎么设计,通通没有约束。
外层 Harness 的首要职责就是把这些模糊需求转成一张带退出条件(Definition of Done)的任务卡片。每个子任务至少要包含四件事:
- 目标描述:这个子任务要交付什么
- 输入清单:依赖哪些文件、哪些上游任务产物
- 动作约束:允许改哪些文件、不允许动哪些范围
- 验收标准:用什么命令、什么断言来判断“真的做完了”
我自己的经验是,验收标准必须写成可执行的形式,比如“运行pytest tests/test_login.py全部通过”,而不是“代码质量良好”。因为 Agent 对“良好”的理解跟人的理解永远有差距,但命令行输出的 0 和 1 是没有歧义的。哪怕多花几轮在任务拆解上,也比后期花十几轮返工划算。
2.2 状态管理与跨轮记忆:最容易被低估的工程点
70 轮任务里,Agent 要跨过几十次“遗忘点”。如果每轮都把所有对话历史塞给模型,token 成本爆炸不说,模型还会被早期过时的信息污染。我在实操中把记忆拆成了三层,这算是这套方案里最实用的部分:
- 短期记忆:内层 Agent 在当前子任务内的完整对话上下文,子任务结束就清空
- 中期记忆:每一轮产出的文件清单、测试结果、关键报错摘要
- 长期记忆:项目原始需求、技术选型决策、全局接口约定,这些只在必要时注入
简单说就是“该忘的让它彻底忘,该记的用结构化方式留住”。内层 Agent 每完成一个子任务,外层 Harness 做的不是把完整日志装入下一个任务,而是提炼一张“产物清单+风险摘要”传给下一轮。这种做法的直接效果是,跑到第 60 轮时,Agent 对需求的理解依然跟第 1 轮一致,而不是被中间二三十轮的错误信息带偏。
2.3 Coding Agent 的动作闭环:写码、执行、看错、再改
内层 Coding Agent 的能力核心,是构建一个紧凑的“开发-执行-反馈”循环。这个循环在代码里本质上是多轮工具调用,每轮包含四个动作:生成本轮要写入文件的代码片段、调用文件写入工具落盘、调用 shell 工具运行程序或测试、读取输出结果作为下一轮的决策依据。
工具列表我建议至少包含四类:
tools = [ FileReadTool(max_content=20000), # 读文件,限制最大长度防止溢出 FileWriteTool(), # 写文件 ShellTool(timeout=30, max_output=8000), # 执行命令,超时截断 TestRunnerTool(timeout=60), # 跑测试,单独做超时控制 ]这里有个细节值得单独说:所有工具都要做输出截断。Agent 连续跑几十轮时,经常会出现一条命令打印几千行日志,如果不截断,下一轮模型的输入会瞬间膨胀,把有效的决策上下文挤掉。截断之后,Agent 看到的是“前 2000 字 + 后 2000 字”,中间被省略的信息对它做决策基本没有影响。
内层 Harness 的 System Prompt 里,我还会固定一段约束文字:
你的每一次回应都必须按以下格式输出:决策依据、本轮动作、预期结果。动作类型只能是 read_file、write_file、run_command、run_test 四种之一。如果测试失败,你必须先用 read_file 查看相关代码再修改,禁止盲目重试。
这段约束的价值在于,它把 Agent 的思考过程从“自由发挥”改成了“结构化推进”,既方便外层 Harness 解析中间状态,也变相逼着 Agent 每次修改前先看一下真实代码,而不是凭“记忆”乱猜。实测下来,盲目重试的比例至少下降一半。
2.4 安全阀与人机协作边界:自治不等于失控
连续 70 轮的自治执行,听起来很爽,但工程上必须面对一个灵魂拷问:万一它第 30 轮把整个项目的公共接口全改了呢?
所以这套工作流里必须有安全阀。我不建议让 Agent 拥有完全自由的文件操作权限,而是按动作风险分三级:
- 低风险:读写指定目录下的普通源码文件、运行测试,直接放行
- 中风险:修改依赖清单、修改配置项,需要记录并通知人工
- 高风险:删除文件、变更数据库结构、发布构建产物,默认挂起等待人工确认
另外一个非常实用的机制是单子任务失败次数熔断。我发现大多数任务在 3 到 5 次迭代内都能解决,如果内层 Agent 连续失败超过 5 次,基本可以判定当前任务描述有歧义或者存在前置依赖问题,继续重试只是烧钱。这时候外层 Harness 应该把任务标记为 blocked,暂停整条流水线,把上下文摘要提交给人类开发者,而不是让 Agent 继续硬闯。
安全阀不会拖慢多少速度,因为 70 轮里真正需要人工确认的高风险动作通常只有三四个,但这三四个确认能避免大量灾难性的返工。
3. 实操:从零搭建一套“70 轮连跑”的 Harness 工作流
3.1 选型:LangGraph 做外层编排,DeepSeek 系模型打底
搭建这类工作流,选型是第一步。外层 Harness 我选了 LangGraph。原因很简单:它原生支持有状态图(StateGraph),节点之间可以显式定义状态转换和分支条件,正好对应外层 Harness 的“任务排队-执行-验收-下一步”调度逻辑。对比之下,LangChain 虽然生态成熟,但在多轮状态管理上表达能力弱一些;完全自研调度器也行,但需要处理图形状态流转、持久化、日志回放这些底层问题,开发量明显变大。
模型部分,在我复现的实践中,DeepSeek 系模型(包括其 API 或本地部署版本)是性价比很稳的选择。70 轮任务跑下来 token 消耗很大,模型价格直接决定这个方案能不能量产;同时 Coding Agent 需要较强的代码理解与长上下文保持能力,DeepSeek 这两块的表现在同价位里属于能打的那一档。当然,你完全可以用其他长上下文模型替换,核心不依赖具体厂商。
3.2 环境准备:沙箱隔离 + 基础依赖
第一步是准备一个干净的沙箱目录。强烈建议给 Agent 单独开一个工作目录,里面放一个requirements.txt和一个空的项目骨架,不要让 Agent 直接在你的真实项目里乱跑。这不是不信任模型,而是工程上必须保证“Agent 的破坏半径有限”。
安装依赖很简单:
python -m venv .venv source .venv/bin/activate pip install langgraph langchain openai如果你用的是 DeepSeek 的 API,可以按 OpenAI SDK 兼容的方式配置 base_url 和 api_key。模型参数我建议 temperature 调到 0.2 左右,max_tokens 给足,但别用默认值让 Agent 无限生成长文——它很可能在写日志上比写代码热情高得多。
3.3 内层 Coding Agent:最小可用的实现
我先构建内层 Agent 的状态节点,让它接收“任务编号、任务描述、工作目录、验收命令”这几个输入,然后进入工具调用循环。核心逻辑类似下面这样:
from langgraph.graph import StateGraph, END from typing import TypedDict class CodingState(TypedDict): task_id: str task_desc: str work_dir: str accept_cmd: str attempts: int def run_inner_agent(state: CodingState): # 在这里调用 LLM + tools,执行写码、跑测试的循环 # 每次工具调用后检查 attempts,超过 max_attempts 直接返回失败 return {"attempts": state["attempts"] + 1} graph = StateGraph(CodingState) graph.add_node("coding", run_inner_agent) graph.add_edge("coding", "coding") # 循环直到满足退出条件关键在于退出条件的判断:测试命令返回 0才算任务完成,其他任何“我觉得完成了”都不算数。内层 Agent 不需要知道全局任务有多宏大,它只需要对这个子任务负责。
3.4 外层 Harness:调度循环与轮次控制
外层 Harness 的逻辑反而比内层更简单,但它决定了 70 轮怎么组织。核心就是一个调度循环,每次从任务队列里取出一个当前依赖都满足的任务,交给内层 Agent 去执行,然后根据验收结果更新队列状态,同时把轮次数加一。
task_queue = build_tasks_from_requirement(raw_requirement) harness_round = 0 while task_queue and harness_round < 70: task = select_next_task(task_queue) # 按依赖拓扑选一个可执行任务 result = run_inner_agent(task) if verify_acceptance(task, result): mark_done(task, result) else: mark_blocked(task) task_queue = update_dependencies(task_queue, task) harness_round += 1这个循环里还藏着一个容易被忽略的细节:每轮结束时把该轮的决策摘要写入审计日志。审计日志不需要很长,只要记录轮次、任务编号、Agent 执行的动作类型、关键输出文件、验收结果这几项就够了。我后面排查问题,靠的全是这份日志。
3.5 一轮执行日志的真实样貌
跑完整套流程后,外层 Harness 会生成类似下面这样的轮次记录。我抽三行典型日志贴出来:
| 轮次 | 任务编号 | Agent 动作 | 关键输出 | 验收结果 |
|---|---|---|---|---|
| 18 | TASK-014 | write_file + run_test | src/auth/token.py实现 refresh_token 逻辑 | 2 个测试通过 |
| 45 | TASK-031 | read_file + run_test | 修复tests/test_checkout.py中 timezone 相关断言 | 1 个之前失败的测试通过 |
| 69 | TASK-048 | write_file | 生成README.md和API.md | 文档校验通过 |
第 45 轮是我印象很深的一轮,Agent 第一次跑测试失败后,没有盲目重试,而是先 read_file 打开了测试文件,发现时间固定写法有问题,然后针对性修改。这说明前期的系统提示词约束和内层小上下文设计真的起了作用。
4. 常见问题与排查技巧实录
4.1 跑着跑着就“幻觉”:上下文污染是头号杀手
症状是跑到第 30 轮以后,Agent 突然开始引用一个十几轮之前的文件路径,或者把上一个子任务的测试报错当成当前任务的障碍。原因基本是上下文里混入了过期的状态信息。
排查方法是先看审计日志里 Action 的输入输出,确认它引用的文件路径、时间戳是否和当前任务对得上。解决办法有两个,一个是在子任务之间强制清空内层上下文,只把结构化摘要传给下一轮;另一个是让 Agent 每次修改代码之前必须 read_file 当前实际内容,禁止直接根据记忆生成修改后的完整文件。这两招一起上,幻觉问题能压到很低。
4.2 工具卡死:没有超时的工具会拖垮整条流水线
连续跑 70 轮的时候,某些命令会意外地一直挂住,比如一个没写退出条件的脚本、一个启动后不结束的本地服务。如果工具的 timeout 参数设置得太大或者没设,外层 Harness 就会卡死在那一轮,后面的任务全部排队等候。
我的做法是:所有 shell 类工具强制超时 30 秒,测试类工具 60 秒,超时直接杀掉进程并把 stdout 尾部内容作为结果回调给 Agent。如果同一个任务连续三次超时,不再重试,直接把任务标记 blocked 转人工。实测下来,这一条规则保证了整条 70 轮流水线能从头跑到尾,而不是在某个死循环里干瞪眼。
4.3 嵌套状态同步丢失:内层改了外层不知道
“Harness 套 Harness”最隐蔽的坑是状态同步。外层 Harness 维护全局任务状态,内层 Coding Agent 执行时会修改文件,但内层如果需要把某些信息回写(比如“我新建了一个配置文件,后续任务要用”),通过返回值省略掉的话,外层就完全不知情,后续任务就会因为找不到文件而失败。
解决办法很简单:内层 Agent 每轮结束必须把“本轮新增或修改的文件路径”显式写入摘要,外层 Harness 再把这个摘要合并进任务队列的输入清单。注意传递状态时用拷贝而不是引用,防止内层不小心直接改掉外层状态的原始对象。这些小问题单独看都不起眼,但在 70 轮的长跑里会被放大成致命故障。
4.4 token 成本失控:长任务钱包告急
70 轮不是免费的。我按一轮平均输入 2 万 token、输出 3000 token 来算,整条流水线大概要消耗 150 万到 200 万 token。这个量级下,模型单价差的就很明显了,这也是我推荐长上下文 + 低价模型作为默认的原因。
省成本的技巧有三个:第一,内层子任务之间的历史不传递,只传摘要;第二,工具输出强制截断,长日志只保留头尾;第三,对低风险的收尾任务(比如生成 README)可以用更小的模型。做完这三件事,整体 token 消耗能比朴素做法省下差不多一半,而且 Agent 的表现几乎不受影响。
我自己跑这种超长 Harness 工作流,最大的体会是“先把节奏慢下来”。前 10 轮宁可在任务拆解和验收标准上多花些时间,后面六十几轮反而会非常顺;反过来,前面图快让 Agent 直接开写,后面至少有二十轮在给前面的模糊需求擦屁股。最后再分享一个小技巧:让外层 Harness 每轮都把 Agent 产生的 diff 文件名写进审计日志,这比任何监控面板都实在,项目一旦出了幺蛾子,你能直接定位到是第几轮、哪个文件被哪次修改改崩的,回溯成本几乎为零。