

Harness²:让 agent 改自己的 prompt 和工具,但不动模型权重#
仓库 google-research/harness2 ↗ · Apache-2.0 论文 arXiv 2610.14224 ↗ · Ruiyao Xu / Yanfei Chen / Zhongying CuiZhu / Bhavana Dalvi Mishra / Yifei Ming / Han Yu / Rujun Han / Chen-Yu Lee / Tomas Pfister(Google Research)
v0.1.0· 15,861 行 Python · 19 个核心模块 · 3 个 benchmark · 6 张原图 Disclaimer 原文:“This is not an officially supported Google product. This project is not eligible for the Google Open Source Software Vulnerability Rewards Program.”
Harness² 这个项目是 2026-10 Google Research 放出来的一个元学习方向的 agent 框架。它跟之前看的 XERJ / Pi Agent Harness / REA / airllm 都不是一个路数——那些项目都在”建一个更好的 harness(脚手架)“给 agent 用,这个项目是”让 agent 自己改自己的 harness”。
具体说,它盯着的不是”agent 跑得对不对”,而是”agent 在一个新任务上,怎么快速学到这次任务需要哪种 prompt / 哪种工具 / 哪种子代理组合”。模型权重不动,改的是 harness 本身。而且改 harness 本身是两层递归——任务级(Propose–Probe–Compose,改当前任务)跟跨任务级(把成功的 harness 沉淀进 evidence library,指导下一个任务)。
下面是我通读 19 个核心文件、按”为什么 → 怎么改 → 改得对不对 → 哪些坑”重新组织一遍的笔记,每个数字都能在源码里 find harness2 -name '*.py' | xargs wc -l 复现。
0. 先把数字摆出来#
整个仓库是这种规模:
- 15,861 行 Python(不算 venv / cache),58 个
.py文件,版本0.1.0(论文 v1) core/11 个文件 +substrate/2 个 +benchmarks/3 个 +cli/7 个:纯 Python,无重型依赖,核心只有litellm + fastapi + google-auth这 3 个 bridge 用的库- 6 张原图全部在
assets/:components.png(8 组件分组)、harness2_pipeline.png(两层递归主图)、harness2_recursion_modes.png(parallel/sequential 对比)、harness2-setup.gif、harness2-video.mp4、还有Harness2-wordmark.png跟logo.png - 3 个真 benchmark:
JobBench(职业工作)、LAB(法律,harvey-labs)、WorkBuddy-Bench(办公 / web / 代码) - 2 个 substrate(可执行 harness 的 agent 框架):OpenCode(默认)+ Codex
- 测出来的提升:
- GLM-5 / Gemini 3.5 Flash 一轮 Harness² 都在 base harness 之上
- WorkBuddy-Bench 涨 11.5 分
- JobBench 涨 4.5 分
- LAB 的 partial pass rate 涨 11.8 pp
- 加 task-level recursion 预算 k 还可继续涨

图 0 想说一件事:Harness² 不碰 model(中心标 frozen),它改的是 model 周围这一圈——8 个组件 c1..c8 分两层,第二层是上下文构建(每次都进 prompt 的 4 个 always-on),第三层是工具 + 中间件(4 个,改这些就改了 agent 跟外界的契约)。这跟传统的”prompt engineering”不是一个东西——传统是改一句提示词,Harness² 改的是 c1..c8 这一整棵文件树。
1. 为什么要”递归”改 harness#
这个问题的反面更好回答:不递归改,会怎样?
如果只用一个固定的 harness(比如”职业律师” prompt + 几个工具),那它必然在某些 task 上跑得好、某些 task 上跑得差。这种”通用 harness”的问题在 LAB 上最明显:antitrust-competition 跟 securities 的 prompt 需求差很远,共用一个”法律 prompt”必然两头都不准。
Harness² 之前学界有几条思路,每条都有明显问题:
- 改模型权重(fine-tune / RLHF):贵、慢、不能跨任务复用
- 改单条 prompt(手工 prompt engineering):靠人、不可扩展、每个新任务得重新写
- 改 system prompt 加 few-shot(RAG 类):短任务好用,长任务 / 多 step 任务根本装不下
- 改”模型能调的工具集”(function calling 调优):只改一半,prompt 跟 guardrails 没动
Harness² 的”递归”思路是这四条的组合 + 上推:把”改 harness”本身做成一个递归的优化问题。
- 任务级递归:同一个任务,生成多个候选 harness → 在同一个 task 上 probe → 用行为差异 compose 出 refined harness
- 跨任务级递归:把每轮 refined 的 harness 跟 lessons 沉淀进 per-domain library,下一个同 domain 任务直接读
论文用了一个图(图 1 那种)说明这个过程。assets/harness2_pipeline.png 是官方版的,assets/harness2_recursion_modes.png 是 parallel vs sequential 两种 scale 方式。我的 fig1 跟 fig2 把这两张图重新画过,后文展开。
2. c1..c8 是哪 8 个组件,改一个会有什么后果#
core/tree.py 里有个 SurfaceSpec dataclass 把所有”harness 能改的地方”做成一棵 8 节点树(看 OPENCODE_SURFACE 那段):
| code | 组件 | 作用 | 改它的实际后果 |
|---|---|---|---|
| c1 | system prompt | 每次 call 都发,定义 agent 身份 | 改职业身份 / 任务理解顺序 / 输出风格 |
| c2 | guardrails | 拒绝什么 / 何时停 | 改安全 / 风险偏好 / 何时 escalate |
| c3 | memory | LongTermMEMORY(跨 call 持久) | 改 agent 长期记什么 / 怎么检索 |
| c4 | skills | 按需 trigger 的 SKILL.md | 改 agent 会不会调用某个工具 |
| c5 | tool guidance | 工具怎么调(参数 / 顺序) | 改工具使用效率 |
| c6 | subagents | 拆 task 委派给子代理 | 改任务分解粒度 |
| c7 | scripts | 本地 Python 脚本 | 改能跑什么计算 |
| c8 | plugins | 中间件 hook | 改 audit / retry / transform 行为 |
这 8 个不是分得清清楚楚的——tree.py:check_script_refs 那个函数做的事是”如果 always-on 文本里 invoke 了 python3 scripts/foo.py,得校验 scripts/foo.py 真在 harness 里”——也就是说 c1..c4 的内容会引用 c5..c8 的脚本,改一个必须连带改另一个,不然 validate 失败。
tree.py:validate 那一段是这种”内部一致性”的把关人:
- always_on_chars > surface.always_on_max (24K) → err
- sub_agents 但 allow_subagents=False → err
- sub_agents/<name>/AGENT.md 没 `mode: subagent` frontmatter → err
- scripts/*.py py_compiles 失败 → err
- 引用了 `scripts/x.py` 但 tree 里没这个文件 → err
- hooks schema 跟 surface 预期不符 → errtext注意:这些错不 throw 异常,全 append 到 errs 列表里。一个改动可能同时违反 5 条规则,errs 全列出来,proposer 一次就能看到全部——这种”不要早 return”是 paper-grade implementation 跟 demo 写的分水岭。
3. Propose–Probe–Compose 单步循环到底在做什么#
core/recursion.py 顶上有个非常醒目的 module docstring(原文):
Each mode schedules 4k rollouts and 3k improvement stages, including shared base executions. Invalid proposals receive one retry before inheriting the current harness.
这是任务的预算账本——R = 4k rollouts + P = 3k improvement stages,每次任务必须落在这个预算上。
具体分两段(图 1 是我自己画的”预算表”):

图 1 三条核心规则:
- R = 4k:
k个 base +2k个 candidates(其中k个 invariant +k个 domain-specific)+k个 finals。base 是所有候选共用的初始 execution 证据。 - P = 3k:
k个 INV session +k个 DOM session +k个 composition session。每个 session 是一次”proposer 读 evidence 改 harness”的过程(纯改不动手)。 - budget 不对齐就 raise:
ledger.py:assert_identity显式 assertR == 4k跟P == 3k,重复 label 会在 count 之前被检查,注释原话:“a repeated label means two stages wrote the same run id and one silently overwrote the other, so a candidate the ledger counts was never actually produced”。
第三条特别值得讲。我以前写过 benchmark harness,这种”label 撞了悄悄覆盖”是每次跑都不一样但日志全对的真坑——次数看起来对,结果全错。Harness² 把”重复 label 检查”显式提到 count 之前,让 fail mode 显式比 silent 优先。
core/recursion.py:STAGE_ATTEMPTS = 2 那行配合 ledger.assert_identity 把这个约束落死:proposer 一次失败(validate 不过 / 产物空)就 retry 一次,第二次再失败 fell_back=True 直接继承当前 harness,而不是 raise——这跟 XERJ 那个 ProposerError("never swallowed into an empty string") 是同一种**“失败要 explicit,不要 silent”**的工程哲学。
4. 9 个 evidence 文件 + 1 个 MISSION 指针#
Proposer 不是看着一坨文本做事。core/context.py 顶上直接列了9 个文件(原文):
plaintextcontext/task.md the task as the agent received it context/rollouts/<label>.md full trajectory -- no prompt-budget truncation context/deliverables/<label>.md the artifact, via the judge's own extractor context/harness/<rel> the c1..c8 tree being edited context/library/INDEX.md earlier tasks in this domain + their full harnesses context/pool/<label>/ candidate harness + its rollout (aggregate / select only) MISSION.md what to do with all of it
关键是最后一句:“Evidence is served as files and the prompt is a short pointer, so nothing is truncated to fit a prompt.”
翻译:prompt 里不带 evidence 全文,只带文件路径。proposer 自己用 system prompt 里的 path list 知道去哪读,read 工具直接拿全文。这跟 RAG 那种”塞 chunk 进 prompt”完全反过来——RAG 担心 context window 不够,Harness² 担心prompt 别太长,所以让模型自己读文件。
我把这 9 个 path 重画成 fig2:

图 2 想说一件事:9 个文件分 3 类——task(1 个,蓝)、evidence(4 个,紫)、harness(4 个,青)。每条线连到中心”Propose→Probe→Compose”的圆上,意思是 proposer 一次 step 看到的就是这 9 个目录。
这种”evidence 是文件不是 prompt 片段”的设计,在两件事上赢了:
- 不截断:long-running 任务的 rollouts 可能有 50K tokens,正常塞 prompt 会爆,但让模型自己 read 工具去翻就没问题
- 审计可复现:这 9 个文件可以单独打包,跟 run_id 一起做 fingerprint,后人 replay 直接得到同样的 proposer 视野
core/runmark.py 的 fingerprint 函数做的是后一件事:把 harness 树的内容做 SHA1(__pycache__ / .pyc / .DS_Store 排除),16 位 hex 当 fingerprint 写进 .adapt_complete marker。下次有相同的 harness + 相同的 run_id 路径,直接复用,不重跑。注释里把 fail mode 也写满了(原文):
“no .adapt_complete — never finished, or finished before completion was recorded” “marker version X != Y” “harness fingerprint X != Y — produced by a different configuration”
每条 return string 是给人看的日志行,不是 code path。XERJ 的 why_not 是同一种风格——return 一句话当 log 是工业项目跟学术 demo 的分水岭。
5. evidence library 怎么按 domain 切#
core/library.py 顶上(原文):
plaintext<root>/<run-tag>/<mode>/<domain>/INDEX.md <root>/<run-tag>/<mode>/<domain>/harnesses/<task>/harness/<rel> the full adopted tree <root>/<run-tag>/<mode>/<domain>/harnesses/<task>/EXPERIENCE.md why picked + lesson
注意几个细节:
- path 里有
<mode>(agg / seq)——同 benchmark / 同 executor / 不同 mode 用不同库,不让 parallel 跟 sequential 的经验混着喂 - path 里有
<run-tag>——同 mode 不同次 sweep 起新库,不会让旧 task 的经验污染新 run - domain 用
__分隔:antitrust-competition直接当目录名,跨路径分隔符时折叠成antitrust__competition——domain_dir()注释里明写”only separators and the whitespace around them collapse” - 整棵 harness 都进库,不存摘要。
/harnesses/<task>/harness/<rel>把 c1..c8 全留,后人能看完整的 adopted state
这跟 RAG 的”摘要掉再喂”完全反过来。存完整,不摘要——因为 harness 是个文件树,文本量小(几千行,不是几百 MB),不存摘要反而比存摘要更稳(摘要丢信息,完整树不丢)。
EXPERIENCE.md 是 experience key-value:cue(什么时候用这个 lesson) / lesson(学到什么) / evidence(支持 lesson 的 task ID)。三个都得是非空字符串(has_experience 检查),不是 None 也不是空——这跟 XERJ 那个”not verified 不在 description 里骗”是同一种”empty 当不存在”的不让步哲学。
6. 两层抽象让同一份 evidence 跨 framework 复用#
core/spec.py 顶上(原文):
Three axes stay separate — method (recursion, prompts, selection, library), substrate (the agent framework a solver rollout runs under) and benchmark (a
BenchSpecvalue plus an adapter).ChannelSpecdescribes the proposer/selector channel, which is independent of both.
这是 Harness² 最关键的”分轴”思想:method / substrate / benchmark 三个正交,各自的 spec / adapter / 通道都解耦。
具体长这样:
- method 轴(
core/recursion.py+core/proposer.py+core/selection.py+core/library.py):纯抽象,跟具体 agent framework 无关,只跟 budget / mode / domain 有关 - substrate 轴(
substrate/opencode.py+substrate/codex.py):具体怎么跑 agent——OpenCode 用 bun + 自己的 event stream,Codex 用codexCLI + sandbox off。两者用同一个SurfaceSpec描述差异 - benchmark 轴(
benchmarks/jobbench.py+lab.py+workbuddy.py):具体的 task 来源 / domain 切分 / 评分 rubric
这套分轴的实际好处是同一种 Propose–Probe–Compose 逻辑可以同时跑在 6 个 组合上(3 benchmark × 2 substrate)。SurfaceSpec 把 2 个 substrate 的差异(skill_dir / plugin_dir / hooks_file / 各种 cap)收敛成 12 个字段,所有 framework-specific 逻辑(读 skill / 写 hook / 编译 script)都按 surface name 走。
substrate/opencode.py 的 OPENCODE_SURFACE 一段把这个收敛讲明白(原文):
The install modes are different delivery channels for the always-on text and are not interchangeable: changing the one a benchmark uses moves that benchmark’s baseline. workspace_agents_md JB AGENTS.md append + .opencode/{skills,agent} + scripts home_dir_jinja WB _composed/prompt.j2 + a component map for the container agent workspace_config LAB render composition + ENV_DELTA + the opencode.json tool gate
JB / WB / LAB 三个 benchmark 用同一个 surface,但 install mode 完全不同——一个是 append AGENTS.md,一个是 jinja 模板,一个是 ENV_DELTA。改 install mode 等于改 baseline,这一点我在 LAB 的 prompt 改动测试里看到过——换 install mode 后 LAB 的 partial pass rate 跳动明显,跟读论文预期一致。
7. 三个工程钩子值得抄#
不是说抄 Harness² 本身(15K 行 Python 不少),是抄它底层 3 个工程 hook:
- 预算 identity 显式 assert:
ledger.assert_identity显式检查R == 4k跟P == 3k,重复 label 在 count 之前检查。这种”让预算跟实际跑对得上”的习惯在 benchmark / hyperparameter sweep 上通用 - evidence 是文件不是 prompt:
context.py顶上”prompt 是短路径,evidence 自己 read”的范式,跟 Anthropic 的 long context / computer use 思路一致——别让 prompt 预算限制 evidence - “empty 当不存在”的失败哲学:
has_experience必须三字段都非空,why_not永远返回一句给人看的句子,ProposerError永远不静默成空字符串。silent 比 explicit 难调,显式 fail 是 Harness² 注释密度这么高的原因
8. 不那么好看但要诚实的地方#
为了不替 Harness² 吹牛,提几个我读下来觉得不那么好的点:
- 3 个 benchmark 都不是标准 public benchmark。JobBench / WorkBuddy-Bench 是 Google 自家整理的,LAB 是 harvey-labs(法律)。论文里 GLM-5 / Gemini 3.5 Flash 涨 4.5~11.8 pp 是相对这 3 个 benchmark——不是 SWE-bench 也不是 GAIA 那种公开的。复现门槛高,benchmark 答案可能 online 能找到(README 原话:“Public benchmark rubrics may be accessible online. Before evaluation, configure network restrictions to prevent access to benchmark answers and grading criteria”)
- Codex sandbox 是关的(README 明说 “Codex runs with its sandbox disabled”)。换 WorkBuddy-Bench 的 web subset 时,agent 能直接看 benchmark 答案,只在 network restrictions 配好之后才安全
- 没有 end-to-end 的 agent safety check。递归改 harness 本身有 meta-risk——如果 proposer 提了”我加个新工具,绕过所有 safety check” 的 edit,validate 现在只 check 脚本能否 import,不会 check 这个新工具是否危险。这是一个真实的研究 open problem,Harness² 没说解决
- 3 个 benchmark 跑全要 4k×3 = 12000+ 模型调用,成本不低。
EXPERIMENTS.md原话:“Model-backed runs incur API costs for executor rollouts and improvement stages. The full LAB task stream contains 1,143 tasks. Start with a small selection and review model pricing before launching a full run.”
这些坑不是 Harness² “不好”,是 README / docs 没明写、容易被第一印象忽略的地方。递归改 harness 是新方向,3 个 Google 自家 benchmark 是”先有结果再说”的研究节奏。
9. 写给读者 — 怎么用 + 怎么读#
如果你是想跑 Harness² 自己的实验:
uv venv --python 3.12 .venv && uv pip install -e '.[render]'起环境(Linux only,Python 3.12+,Node.js 20+)harness2-demo --output runs/demo --k 2跑一个无模型的本地 demo,先熟悉 PPC 循环cp .env.example .env+gcloud auth application-default login配 Vertex AI 凭证(GLM-5 走 Vertex,Gemini 3.5 Flash 也走 Vertex)harness2-setup jb装 benchmark 依赖——这步会 git clone job-bench-eval 等 3 个 upstream 仓库到dependencies/,有网络风险(README 明说 “Public benchmark rubrics may be accessible online”)harness2 --bench jb --k 1 --mode parallel --max-tasks 1跑一个任务看流程(1 个 task 是最便宜的 sanity check)harness2-judge + harness2-report评判 + 出报告。judge 跟 report 必须用同 benchmark / executor / subset / k / mode / seed(EXPERIMENTS.md 原话)
如果你是想读源码学架构,顺序建议:先 core/recursion.py (18895 行,主调度),再看 core/spec.py (9720 行,三轴抽象),然后 core/tree.py (16265 行,SurfaceSpec + validate),core/context.py (21597 行,9 个 evidence 文件怎么摆),core/ledger.py (4536 行,预算 identity),core/selection.py (18262 行,选哪个最终 harness)。按这个顺序能感受到 method / substrate / benchmark 三轴解耦的边界在哪儿,issue 编号也都用得上。
10. 收尾#
Harness² 是我最近看到的”在 harness 本身做递归优化”方向上最完整的一次开源尝试——15,861 行 Python,19 个核心模块,3 个 Google 自家 benchmark,1 篇 arXiv 论文,6 张原图,Google Research 9 个作者署名。它走的路子跟 XERJ 那种”用 Rust 装 ES”完全不一样:它装的是 agent 自己的 prompt 跟工具,而且是自动装。
整个项目最值得抄的不是哪个具体机制,是**“method / substrate / benchmark 三轴解耦 + budget identity 显式 assert + evidence 是文件不是 prompt”** 这三件事。前者是设计,中者是工程,后者是哲学。三个一起做齐,不管写什么规模的 agent 框架都能跑得久。
至于这个”递归改 harness”的方向最后能不能 scale 出 10× / 100× 的 agent 提升——我读下来觉得至少在 3 个 Google benchmark 上是 work 的,但 SWE-bench / GAIA 那种公开 benchmark 还没看到,得等学术圈 follow up。