1. 从 HarnessDev 论文说起:LLM 自建 Agent Harness 到底难在哪
HarnessDev 这篇论文做的事情,用一句话概括就是:把问题从“模型在固定 Agent 系统里能不能完成任务”,换成了“模型能不能把这个 Agent 系统本身造出来,并且持续改好”。它面向的是 Agent 工程化落地场景,适合已经在写 Agent、调工具链、做评测回归的开发者。论文给出的答案不算乐观也不算悲观:前沿模型已经能从几乎不具备策略的种子程序出发,做出可运行的 Harness,在写作和机器学习实验任务上甚至能接近或超过选定的人类工程系统参考,但在代码与检索研究任务上仍有明显差距;更关键的是,基于评测反馈的自我演化虽然能在可见任务上提分,却很不稳定,且常常不能迁移到未见任务或另一个执行模型。
这篇论文最有价值的不是某个新架构,而是一套评估 Agent 工程能力的方法论。它把 Harness 拆成六类职责:Execution(循环、规划、调度、停止条件)、Tools(工具注册、选择、输入输出约束、异常处理)、Context(任务、代码、历史、日志如何进入模型上下文)、State(目标、假设、进度、失败和工件状态的保存与恢复)、Lifecycle(动作前后钩子、超时、失败恢复、收尾)、Verification(测试、检查、结果验证、轨迹记录)。然后区分四个角色:创建模型 L_C 在开发环境 D 里构建出冻结 Harness H,冻结后的 H 加上执行模型 L_E 跑下游任务 x,最后由固定评测器 J 打分。这个分离很关键,因为一个 Harness 在“作者模型”身上表现好,可能只是模型与 Harness 协同适配,而不是 Harness 本身成了可迁移的软件资产。
论文的 RQ1 用 4 个领域、5 个 benchmark、2207 个不重复实例来测创建能力,RQ2 则让 creator 反复看到 100 个 SWE-Pro 加 89 个 Terminal-Bench 任务的反馈,最多提交 10 对正式版本,最后在互斥的 630 个 SWE-Pro 实例上测留出表现。结果里有两个数字特别值得记住:五个 Self 谱系的留出集全都比 H0 高,但增益只有 +1.43 到 +4.44,均值 +3.11;换成固定 Gemini 执行后,只有 Opus 的留出集仍变好,Qwen、DeepSeek、GPT-5.5 都回退。也就是说,可见反馈集上的正增益,不能直接当成跨 executor 的通用改进。
如果你正在做 Agent 工程化,这篇论文的实践含义很直接:Harness 质量与 executor 兼容性强相关,模块声明不等于机制生效,可见反馈会诱导过拟合,小 probe 容易误导,诊断比补丁数量更重要。下面我会把论文里的评测契约翻译成一套可复制的 Harness 配置模板与演化验证步骤,并说明怎么通过 TaoToken 统一 Key/API 通道接入 LLM 完成构建与迭代测试。
2. 前置准备:用 TaoToken 统一 Key 与 API 通道接入 LLM
在复现 HarnessDev 式的“构建—冻结—评测—演化”闭环之前,先把模型接入层收拢。论文里 creator 和 executor 可能是不同模型,开发环境也可能是 Claude Code 或 Codex,如果每个模型、每个工具都单独配一套 Key 和 Base URL,后面做跨模型回归会非常痛苦。我试过把接入层统一到一个兼容 OpenAI 协议的中转入口,改模型只改 Model ID,Base URL 和 Key 不动,回归脚本几乎不用改。
TaoToken 的 API 入口是https://taotoken.net/api,兼容 OpenAI 的/v1/chat/completions与/v1/models路径。你需要在控制台创建一个 API Key,然后把它写进环境变量,不要硬编码进仓库。下面这套环境变量命名是我在 Harness 项目里常用的,creator 和 executor 分开,方便后面做 Self-Eval 与 Unified-Eval 对照。
export TAOTOKEN_API_KEY="sk-你的TaoTokenKey" export TAOTOKEN_BASE_URL="https://taotoken.net/api/v1" # creator 模型(负责写/改 Harness) export CREATOR_MODEL="claude-sonnet-4-5" # executor 模型(Harness 冻结后真正跑下游任务) export EXECUTOR_MODEL="gemini-3.1-pro"如果你用 Claude Code 作为 creator 的开发工作台,可以在项目根目录放一个.claude/settings.json,把 Base URL 和 Key 指到统一通道。注意 Claude Code 走的是 Anthropic 兼容路径,TaoToken 的 Anthropic 入口在文档里有说明,配置时 Base URL 不要带/v1后缀,具体以接入文档为准。
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的TaoTokenKey", "ANTHROPIC_MODEL": "claude-sonnet-4-5" } }如果你用 Codex 作为 creator,配置写在~/.codex/auth.json和~/.codex/config.toml里。auth.json 只放 Key,config.toml 放 Base URL 和 Model ID,三件套缺一不可。下面是我实际用过的片段,路径和字段名按你本地 Codex 版本对齐。
{ "OPENAI_API_KEY": "sk-你的TaoTokenKey" }model = "gpt-5.5" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api/v1" env_key = "OPENAI_API_KEY" wire_api = "chat"配好之后先做一次最小连通性验证,确认 Key、Base URL、Model ID 三件套都对。这一步不要跳过,后面 401 和 model not found 大多是因为这里没对齐。
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "'"$CREATOR_MODEL"'", "messages": [{"role": "user", "content": "只回复 ok"}], "max_tokens": 8 }'返回里能看到choices[0].message.content就说明通道通了。如果你更想先在网页里对比不同模型对同一段 Harness 代码的理解差异,可以直接用模型对话入口试几轮,确认模型行为符合预期再写进脚本。长期跑编码和 Agent 演化的话,Coding Plan 的额度模型更适合反复冻结、反复评测这种高频调用场景。
3. 可复制配置:Harness 六模块模板与评测契约
论文把 Harness 拆成 E/T/C/S/L/V 六类职责,但真正落地时不能只写六个类名,得让每个模块在运行时有可观测的触发点。下面这份harness.yaml是我按论文评测契约整理的模板,字段名和结构可以直接抄进你的项目,路径按你本地仓库调整。核心思路是:每个模块都要声明entrypoint(入口函数)、events(运行时事件名)、assert(覆盖率断言),这样后面才能检查“模块是否真的走到执行路径”。
harness: name: harnessdev-repro version: 0.1.0 seed: ./seed/weak_seed.py execution: entrypoint: harness.execution:run_loop events: [loop.start, loop.step, loop.stop] stop_conditions: - max_steps: 40 - no_tool_call_for: 3 - verifier_passed: true assert: min_loop_steps: 1 max_loop_steps: 40 tools: registry: harness.tools:REGISTRY events: [tool.select, tool.call, tool.error] constraints: timeout_seconds: 30 max_output_chars: 8000 assert: tool_call_rate: ">0.3" error_rate: "<0.2" context: builder: harness.context:build events: [context.build, context.truncate] budget_tokens: 120000 strategy: sliding_window_with_summary assert: truncate_events: ">=0" state: store: harness.state:FileStore events: [state.save, state.load, state.checkpoint] checkpoint_every_steps: 5 assert: checkpoint_events: ">=1" lifecycle: hooks: harness.lifecycle:HOOKS events: [before_action, after_action, on_timeout, on_failure] assert: hook_invocations: ">=1" verification: verifier: harness.verify:run_checks events: [verify.start, verify.fail, verify.pass] checks: - name: diff_non_empty - name: artifact_exists - name: self_report_matches_artifact assert: verify_invocations: ">=1"这份模板里最容易被忽略的是assert段。论文发现 18 个代码 Harness 里 11 个定义了状态类,但只有 1 个暴露保存接口、1 个实现定期 checkpoint,26679 条任务轨迹里没有出现任何 checkpoint 事件。也就是说,代码里写了class Memory,不等于运行时真的存了状态。所以state.assert.checkpoint_events >= 1这种断言必须写进评测契约,否则你会在静态代码审查里被骗过去。
评测契约还要把“能力指标”和“效率指标”分开记录。论文里每个冻结 Harness 同时报告下游 benchmark 原生指标和执行模型消耗的 token 总数/每任务均值,且不把 creator 开发 Harness 的 token 算入执行成本。下面是一个结果账本的 JSON 结构,每次冻结版本提交时追加一条,方便后面算 final gap。
{ "harness_version": "v3", "frozen_at": "2026-09-10T12:00:00Z", "creator_model": "claude-sonnet-4-5", "executor_model": "gemini-3.1-pro", "feedback_set": { "swe_pro_100": 0.687, "terminal_89": 0.741 }, "holdout_set": { "swe_pro_630": 0.5159 }, "executor_tokens": { "total": 29300000, "per_task_avg": 13200 }, "module_coverage": { "execution": true, "tools": true, "context": true, "state": false, "lifecycle": true, "verification": true }, "notes": "state.checkpoint 未触发,降级为探索性版本" }注意module_coverage这一项。论文用轨迹核验机制是否真被触发,18 个 Harness 里工具、生命周期、验证完整的分别为 13/18、13/18、15/18,未触发的组件全部与状态/记忆相关。所以每次冻结版本都要跑一遍覆盖率检查,把没触发的模块标出来,不要让它悄悄混进“正式版本”。
4. 验证请求:构建、冻结、评测与演化闭环
配置写好后,按“构建—冻结—评测—演化”四步跑闭环。第一步是让 creator 从弱种子生成 H0。弱种子不是空仓库,也不是半成品 Agent,它只提供稳定 CLI、模型配置入口、审计文件写入和路径/文件/搜索/进程等被动原语,没有循环、规划、工具策略、上下文管理、状态、重试、恢复、验证或停止规则。你可以把种子程序理解成一个“只会被动响应”的壳,原样跑下游 benchmark 应该全是 0 分。
# 1. 用 creator 模型从弱种子生成 H0 python -m harnessdev.build \ --seed ./seed/weak_seed.py \ --creator-model "$CREATOR_MODEL" \ --base-url "$TAOTOKEN_BASE_URL" \ --api-key "$TAOTOKEN_API_KEY" \ --out ./harness/H0 \ --max-turns 60 # 2. 冻结 H0,记录 commit 与配置哈希 python -m harnessdev.freeze \ --harness ./harness/H0 \ --out ./frozen/H0.json # 3. 用 executor 模型跑反馈集 python -m harnessdev.eval \ --harness ./frozen/H0.json \ --executor-model "$EXECUTOR_MODEL" \ --base-url "$TAOTOKEN_BASE_URL" \ --api-key "$TAOTOKEN_API_KEY" \ --tasks ./tasks/swe_pro_100.jsonl \ --out ./results/H0_feedback.json # 4. 跑留出集(creator 永远看不到结果) python -m harnessdev.eval \ --harness ./frozen/H0.json \ --executor-model "$EXECUTOR_MODEL" \ --tasks ./tasks/swe_pro_630.jsonl \ --out ./results/H0_holdout.json跑完 H0 后,把反馈集结果喂给 creator 做演化。这里要严格按论文的隔离方式:creator 只能看到反馈集结果,留出集结果从不返回。每次正式版本提交必须同时完成两套完整评测,最多 10 对。下面这段脚本把“读结果—改代码—重评测—选版本”串起来,但注意不要让它自动选版本,版本选择要人工确认,因为论文发现只有 2/9 个 creator 声明的最终版本恰好是该谱系的留出最优版本。
import json, subprocess, pathlib FEEDBACK = pathlib.Path("./results") FROZEN = pathlib.Path("./frozen") MAX_PAIRS = 10 def run_eval(harness, tasks, out): subprocess.run([ "python", "-m", "harnessdev.eval", "--harness", str(harness), "--executor-model", "gemini-3.1-pro", "--tasks", tasks, "--out", str(out), ], check=True) for i in range(MAX_PAIRS): prev = FROZEN / f"v{i}.json" feedback = FEEDBACK / f"v{i}_feedback.json" if not feedback.exists(): break # creator 读反馈,产出候选改动 subprocess.run([ "python", "-m", "harnessdev.evolve", "--harness", str(prev), "--feedback", str(feedback), "--creator-model", "claude-sonnet-4-5", "--out", f"./harness/v{i+1}", ], check=True) # 冻结候选版本 subprocess.run([ "python", "-m", "harnessdev.freeze", "--harness", f"./harness/v{i+1}", "--out", str(FROZEN / f"v{i+1}.json"), ], check=True) # 同时跑反馈集和留出集 run_eval(FROZEN / f"v{i+1}.json", "./tasks/swe_pro_100.jsonl", FEEDBACK / f"v{i+1}_feedback.json") run_eval(FROZEN / f"v{i+1}.json", "./tasks/swe_pro_630.jsonl", FEEDBACK / f"v{i+1}_holdout.json")演化过程中要记录每次切换的“失败证据—假设—影响路径—预期观测变化—回归结果”。论文统计 64 次正式切换里,8 次同时伤害两个 benchmark,16 次只伤害其中一个,3 次形成跨 benchmark 取舍,7 次没有可测变化,27 次虽涨分但落在重复运行噪声带内,只有 2 次有超出噪声带的清晰正证据。同一提交的 pair score 可波动约 +/-4.75 点,所以任何小于这个幅度的“涨分”都要先当成噪声处理。
验证成功的结果长什么样?以 Opus 的正例为参照:它先检查 189 个反馈案例,发现 99/100 次运行自报成功但只有 48 次真正通过,归因于过早完成;然后加入 pre-finish completion/verification gate,并对代码 diff 或终端工件做一次自检;最终 Self 反馈 pair 从 71.1 到 74.1,留出集从 63.02 到 67.46。这是少数同时有失败模式、机制改动、端到端验证三者闭环的案例。你的演化日志里如果能复现出“可复现失败机制 + 小而可解释改动 + 独立验证改变下游行为”这三段,才算一次有效演化。
5. 常见错排查:401、local proxy failed、reading choices、OAuth
接入和评测过程中最容易撞的几类报错,我按真实日志对照整理了一遍。先看 401,这通常不是 Key 错了,而是 Base URL 和 Key 的配对错了。TaoToken 的 OpenAI 兼容路径是https://taotoken.net/api/v1,Anthropic 兼容路径是https://taotoken.net/api,两者不要混用。如果你在 Claude Code 的 settings.json 里写了带/v1的 Base URL,或者在 Codex 的 config.toml 里写了不带/v1的 Base URL,都会出现 401 或 404。
HTTP 401: {"error":{"message":"invalid api key","type":"authentication_error"}}排查顺序:先echo $TAOTOKEN_API_KEY确认环境变量非空,再curl直接打/v1/models看能否列出模型,最后检查配置文件里的 Base URL 是否和工具协议匹配。Claude Code 用 Anthropic 协议,Codex 用 OpenAI 协议,Cline MCP 也走 OpenAI 协议,三者的 Base URL 写法不一样。
第二类是local proxy failed。这个报错通常出现在你本地起了代理进程、但代理进程没有正确转发到 TaoToken 入口的时候。先确认代理进程监听端口和配置文件里的端口一致,再确认代理进程的环境变量里有TAOTOKEN_API_KEY。如果你用的是 Cline MCP,MCP server 的启动命令里要显式传入 Base URL 和 Key,不能只依赖全局环境变量。
{ "mcpServers": { "harnessdev": { "command": "python", "args": ["-m", "harnessdev.mcp_server"], "env": { "OPENAI_BASE_URL": "https://taotoken.net/api/v1", "OPENAI_API_KEY": "sk-你的TaoTokenKey", "OPENAI_MODEL": "claude-sonnet-4-5" } } } }第三类是reading choices相关报错,典型日志是KeyError: 'choices'或reading 'choices'。这通常意味着返回体不是标准的 chat completion 结构,可能是模型名写错导致返回了错误对象,也可能是流式和非流式混用。先确认model字段是 TaoToken 支持的 Model ID,再确认请求里stream字段和你的解析代码一致。如果你在 Harness 的 context builder 里做了自定义解析,记得对choices缺失做兜底。
resp = client.chat.completions.create( model=os.environ["EXECUTOR_MODEL"], messages=messages, stream=False, ) if not getattr(resp, "choices", None): raise RuntimeError(f"unexpected response: {resp}") content = resp.choices[0].message.content第四类是 OAuth 相关报错,多见于 Claude Code 或 Codex 的登录态和 API Key 混用。如果你在 Claude Code 里既登录了账号又配了ANTHROPIC_API_KEY,工具可能优先走 OAuth 而不是你的 Key,导致请求打到错误端点。解决办法是在 settings.json 里显式声明ANTHROPIC_API_KEY并确认没有残留的 OAuth token 文件。Codex 同理,auth.json里只放 API Key,不要放 OAuth 凭据。
还有一类不算报错但很坑的情况:Harness 自报 success 但评测器给 0 分。论文明确说代码任务只从真实仓库 diff、终端任务只从最终环境状态取分,Harness 自报 success 不会带来分数。所以你的 verification 模块必须做self_report_matches_artifact检查,把自报结果和真实工件对齐,否则会出现“看起来跑通了、实际没交付”的假阳性。
6. 把闭环跑起来:从失败轨迹到留出集确认
论文给的最小闭环是:失败轨迹 -> 归类出可验证的 Harness 缺陷 -> 小而可解释的改动 -> 本地契约/回归测试 -> 多任务评测 -> 留出集确认 -> 记录或回滚。这套流程里最容易被跳过的是“归类出可验证的 Harness 缺陷”和“留出集确认”。很多团队一看到分数下降就让另一个模型重写提示词,论文显示这种局部搜索很容易追到噪声峰值,真正可积累的改进需要把失败诊断、工件可审计性、跨模型回归和留出泛化纳入同一个工程流程。
具体操作上,我建议每次演化前先跑一遍失败轨迹归类脚本,把失败样本按 E/T/C/S/L/V 六类打标,统计每类缺陷的出现频次。然后只针对频次最高且可复现的那一类做改动,改动幅度控制在中位最终版本的水平:8 个文件、增加 476 行、删除 38 行左右。改动太大容易引入不可归因的变量,改动太小又可能落在噪声带内。
跨模型回归是另一个必须做的动作。论文发现 Opus 的 SWE-Pro 从 69.3 降到 33.0,EQ-Bench3 从 84.6 降到 74.2,原因是其检索 Harness 的重复查询率从 10.1% 升到 88.2%,说明去重、复核和停止策略适配了原模型的行为。所以你的 Harness 至少要在两个不同 executor 上跑一遍,如果只在原模型上涨分、换模型就回退,那这个改动只能算“模型适配”而不是“Harness 改进”。
最后是留出集确认。留出集必须和反馈集互斥,且 creator 永远看不到结果。论文用 630 个 SWE-Pro 实例做留出,你的项目可以按 80/20 切分,但要注意留出集的任务类型要覆盖反馈集的主要类型,否则留出结果没有参考价值。每次正式版本提交后,把反馈集和留出集结果都写进结果账本,算一下 final gap(历史最佳留出分减去 creator 自己选择的最终版本分),如果 final gap 大于 0,说明版本选择不是留出最优,需要回看选择逻辑。
如果你想把 creator 和 executor 的调用都收拢到统一通道,方便做跨模型回归,可以在项目里固定TAOTOKEN_BASE_URL和TAOTOKEN_API_KEY,只切换 Model ID。这样每次换模型只需要改一个环境变量,回归脚本不用动。需要新建 Key 或查看额度,去 API Keys 页面操作;接入细节和 Anthropic 兼容路径的写法,接入文档里有完整说明。验证模型行为是否符合预期,可以用模型对话先试几轮;长期跑编码和 Agent 演化,Coding Plan 的额度模型更适合高频冻结和评测。把接入层收拢之后,你就能把精力放在 Harness 本身的失败诊断和留出泛化上,而不是反复调 Key 和 Base URL。