wth2jhl.online

Back

Harness² 改的是什么 — 8 个可编辑组件 c1..c8 的洋葱圈结构,中心 model 标记 frozenHarness² 改的是什么 — 8 个可编辑组件 c1..c8 的洋葱圈结构,中心 model 标记 frozen

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 还可继续涨

Harness² 改的是什么 — 8 个可编辑组件 c1..c8 的洋葱圈结构,中心 model 标记 frozen

图 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² 之前学界有几条思路,每条都有明显问题:

  1. 改模型权重(fine-tune / RLHF):贵、慢、不能跨任务复用
  2. 改单条 prompt(手工 prompt engineering):靠人、不可扩展、每个新任务得重新写
  3. 改 system prompt 加 few-shot(RAG 类):短任务好用,长任务 / 多 step 任务根本装不下
  4. 改”模型能调的工具集”(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组件作用改它的实际后果
c1system prompt每次 call 都发,定义 agent 身份改职业身份 / 任务理解顺序 / 输出风格
c2guardrails拒绝什么 / 何时停改安全 / 风险偏好 / 何时 escalate
c3memoryLongTermMEMORY(跨 call 持久)改 agent 长期记什么 / 怎么检索
c4skills按需 trigger 的 SKILL.md改 agent 会不会调用某个工具
c5tool guidance工具怎么调(参数 / 顺序)改工具使用效率
c6subagents拆 task 委派给子代理改任务分解粒度
c7scripts本地 Python 脚本改能跑什么计算
c8plugins中间件 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 预期不符                        → err
text

注意:这些错不 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 是我自己画的”预算表”):

一次 Propose–Probe–Compose 的预算账本 — R=4k rollouts + P=3k sessions 两条色条 + 流向

图 1 三条核心规则:

  1. R = 4k:k 个 base + 2k 个 candidates(其中 k 个 invariant + k 个 domain-specific)+ k 个 finals。base 是所有候选共用的初始 execution 证据。
  2. P = 3k:k 个 INV session + k 个 DOM session + k 个 composition session。每个 session 是一次”proposer 读 evidence 改 harness”的过程(纯改不动手)。
  3. budget 不对齐就 raise:ledger.py:assert_identity 显式 assert R == 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 个文件(原文):

context/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
plaintext

关键是最后一句:“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:

一次 Propose–Probe–Compose 看到什么 — 9 个 evidence 文件(分 3 类:task / evidence / harness)

图 2 想说一件事:9 个文件分 3 类——task(1 个,蓝)、evidence(4 个,紫)、harness(4 个,青)。每条线连到中心”Propose→Probe→Compose”的圆上,意思是 proposer 一次 step 看到的就是这 9 个目录。

这种”evidence 是文件不是 prompt 片段”的设计,在两件事上赢了:

  1. 不截断:long-running 任务的 rollouts 可能有 50K tokens,正常塞 prompt 会爆,但让模型自己 read 工具去翻就没问题
  2. 审计可复现:这 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 顶上(原文):

<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
plaintext

注意几个细节:

  • 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 BenchSpec value plus an adapter). ChannelSpec describes 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 用 codex CLI + 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:

  1. 预算 identity 显式 assert:ledger.assert_identity 显式检查 R == 4k 跟 P == 3k,重复 label 在 count 之前检查。这种”让预算跟实际跑对得上”的习惯在 benchmark / hyperparameter sweep 上通用
  2. evidence 是文件不是 prompt:context.py 顶上”prompt 是短路径,evidence 自己 read”的范式,跟 Anthropic 的 long context / computer use 思路一致——别让 prompt 预算限制 evidence
  3. “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² 自己的实验:

  1. uv venv --python 3.12 .venv && uv pip install -e '.[render]' 起环境(Linux only,Python 3.12+,Node.js 20+)
  2. harness2-demo --output runs/demo --k 2 跑一个无模型的本地 demo,先熟悉 PPC 循环
  3. cp .env.example .env + gcloud auth application-default login 配 Vertex AI 凭证(GLM-5 走 Vertex,Gemini 3.5 Flash 也走 Vertex)
  4. harness2-setup jb 装 benchmark 依赖——这步会 git clone job-bench-eval 等 3 个 upstream 仓库到 dependencies/,有网络风险(README 明说 “Public benchmark rubrics may be accessible online”)
  5. harness2 --bench jb --k 1 --mode parallel --max-tasks 1 跑一个任务看流程(1 个 task 是最便宜的 sanity check)
  6. 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。

深入解析 Harness²:让 agent 自己改自己的 prompt 和工具,不动模型权重
https://wth2jhl.online/blog/harness2-recursive-agent-harnessing/
Author 小吴同学
Published at October 19, 2026