第三部分 · Continual Harness
第 9 章:/refine——能自我改进的 harness
第二个压缩器:不总结对话,而是给可复用状态动小手术
让 agent 写自己的记忆,然后祈祷?
「agent 自己修改自己的系统提示词」听起来像一个事故报告标题。记忆污染、自我强化偏见、一次调试产物变成永久规则——每一条都是真实的风险。Claude Code 的答案是干脆不让 agent 写:CLAUDE.md 归人管。
Prime Agent 的答案不是禁止,而是工程化。它的 /refine 子系统给「自我改进」套了一整套约束:只写特定类型的条目、必须有证据陈述、基础提示词物理不可改、每次变更带前后快照、全局与本地隔离、默认保守、可回滚。这一章逐层拆这台机器——它大概是全书里「约束密度」最高的子系统。
心智模型:第二个压缩器
refine 的系统提示词开篇就给出了自己的定位:
"Your job is to improve the editable continual harness state from the current trajectory. This is similar in spirit to context compaction, but instead of summarizing the conversation you emit precise Create, Update, or Delete edits to reusable state."
压缩管理 token 内的世界,refine 管理 token 外的世界。压缩把旧对话变成摘要留在上下文里;refine 把可复用的经验变成结构化条目放进持久层,让它们在所有未来的上下文里生效。两者共用同一个执行模型(回合边界执行、完成后自动恢复 agent),甚至互为触发器——压缩事件本身就是自动 refine 的触发源之一。
四种条目,一份 schema
可编辑的持久状态只有四类(RefinementKind),分工写在提示词里:
"Use memory for declarative facts and preferences, skill for repeatable procedures exposed as Python calls, prompt for narrow behavioral policy addendums, and subagent for reusable delegation roles."
| kind | 内容 | 特殊约束 |
|---|---|---|
prompt | 补充提示词笔记(默认分组 policy) | 基础系统提示词不可改(双重守卫,见下) |
memory | 事实、决定、失败、偏好、结果 | — |
skill | 已安装 Python REPL 技能的描述符 | 必须带 reference(type/import/callable)与 arguments 契约,TS 与 Python 双重校验 |
subagent | 可复用委派规格 | 必须附 RLM 原生调用形式(handle = await rlm("sub-task"));明令不得发明 run_subagent(...) 包装器 |
统一条目结构 HarnessEntry 带 id / kind / title / content / path / scope / reference / arguments / metadata / source / version——每次 update 版本号 +1。source 区分写入者:"refine"(宿主侧)或 "agent"(kernel 侧 rlm.harness.*)。
存储分两级:local 在会话 artifact 目录的 harness/harness_state.json,随会话走;global 在 ~/.prime/agent/harness/,跨会话。作用域策略被写得很硬:local refine 时 global 条目只读——提示词原文:"never propose update or delete edits for them; create a local entry instead"。影响半径是一等公民。
端到端:规划与应用两段式
规划段:让模型审阅自己
planRefinement() 把五块材料交给一次独立的非推理补全调用:
const conversationText = serializeConversation(convertToLlm(messages)).slice(-80_000);
...
const userPrompt = [
`<current_harness_state>\n${overviewForPrompt(state)}\n</current_harness_state>`,
`<refinement_history>\n${historyForPrompt(history)}\n</refinement_history>`,
`<conversation>\n${conversationText}\n</conversation>`,
`<scope_policy>\n${scopeInstruction}\n</scope_policy>`,
options.instructions ? `<user_refine_instructions>\n${options.instructions}\n</user_refine_instructions>` : "",
"Return only JSON edits. If no useful edit is justified, return an empty edits array with a rationale.",
].filter(Boolean).join("\n\n");
注意材料清单本身就是一套证据机制:<conversation> 是证据来源(截尾 80k 字符),<refinement_history> 让模型看到每次历史改动的 expected outcome——上次改的东西兑现了吗——从而能纠正或回滚失败的改进,<scope_policy> 重申影响半径纪律。
输出必须是固定形状的 JSON:{summary, rationale, expectedOutcome, edits[]}。三个字段的命名即纪律——rationale 的定义就是 "why these edits are justified by trajectory evidence",expectedOutcome 要求写明「应该改善什么、如何验证」。没有强制引用语法,证据要求靠字段契约与提示词反复强化。
两个工程细节值得放大。其一,强制非推理:
// /refine requires a parseable JSON object in the final text. Some reasoning-capable // OpenAI-compatible models can spend the response on visible thinking and return no // final text, which makes otherwise successful daemon /refine calls fail parsing. // Keep the refinement request non-reasoning regardless of the interactive session // thinking level so the model uses its output budget for the JSON object. void thinkingLevel;
会话里开着多深的思考,refine 调用都不开——输出预算必须留给 JSON 本体。其二,截断与畸形的病理分流:isIncompleteJson 逐字符扫描未闭合的字符串与括号,区分「预算耗尽被截断」(报 TRUNCATED_JSON_ERROR,建议缩小请求重试)与「格式错误」。输出预算本身也是弹性的:min(model.maxTokens, 32_000)——固定小上限会把最有价值的多编辑提案截断。
应用段:一个短暂的临界区
规划可能与用户的活跃工作重叠数秒乃至更久——这段窗口里 kernel 的 rlm.harness.* 可能写入同一个文件。应用阶段因此做了三件防御性的事:
- 重读磁盘。不信任规划开始时读的状态,注释直说:LLM 调用期间 kernel 或另一个 session 可能已写入。
- 乐观并发校验。规划时捕获的 baseline 状态逐条比对:若某条目在规划期间被改过、而本提案并未改它,该 edit 失败——"entry changed during refinement planning"。单条失败不连坐其余 edit。
- 原子落盘。临时文件 + rename,保留原文件权限位(缺省 0o600)。
落盘动作分两路记账:global 追加一行完整 RefinementResult 到 refinements.jsonl(含每条 edit 的 before/after 深拷贝);local 作为 customType: "prime-agent.refinement" 的 custom entry 写进会话 JSONL。然后才是那个关键动作——
this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames()); this.agent.state.systemPrompt = this._baseSystemPrompt;
——新记忆当场生效于系统提示词。refine 的结果不作为消息进入上下文;用户看到的只是一行 "Refined continual harness state: N edits applied.",而模型下一轮的系统提示词已经不一样了。
不可变基座的双守卫
「基础系统提示词不可改」不是一句声明,是两道闸门。提示词层,REFINEMENT_SYSTEM_PROMPT 原文:
"prompt: supplemental prompt notes only. The base system prompt is immutable and MUST NOT be rewritten."
代码层,validateEdit 里有一个硬编码的 id 守卫:
if (edit.kind === "prompt" && (edit.id === "base_system_prompt" || computedId === "base_system_prompt")) {
return "base system prompt is not editable";
}
模型被说服是一回事,模型没被说服时也拦得住是另一回事。注入给模型的 overview 顶部还会再复述一遍 "The base system prompt is immutable; prompt entries below are supplemental notes only."——三层防御,同一个不变量。
回滚:重放历史,而不是找快照
/refine rollback <id> 不调 LLM。rollbackProposal() 从历史记录里机械构造反向提案:把目标 refine 的 appliedEdits 逆序遍历,有 before 的恢复成 update/create,只有 after 的(即当年是 create)变成 delete。回滚结果带着 rollbackOf: target.id 再入历史,形成链条。
没有文件系统快照,只有 before/after 记录的重放——这要求每次应用都忠实记录两侧状态,而 Prime 确实这么做了。global 历史放在 refinements.jsonl 的另一层含义在此显现:全局 refine 可以从任意 session 回滚,因为历史不属于任何会话。
四个入口,一个执行体
| 入口 | 语义 | 时机 |
|---|---|---|
/refine [instructions] [--global] | 交互式 session 命令,入队后在回合边界执行(skipAbort: true——refine 从不中止 agent) | 随时 |
RPC refine | daemon/SDK 客户端调用,超时放宽到 10 分钟(要走一次 LLM) | 随时 |
kernel await refine.run(...) | 只登记,不执行:返回 {scheduled: true},回合结束时消费。无活跃回合时返回 scheduled: false | 仅 turn 内 |
| 自动 refine | 触发器 + LLM gate + 冷却,批准后走与手动相同的执行体 | agent_end / 压缩后 |
kernel 入口的「登记不执行」值得一个解释:如果 refine() 在工具调用内部 await agent 空闲,就会死锁——agent 正忙着执行这个工具调用,永远不会空闲。这已经是本书第三次见到同一类死锁推理(宿主桥的 control 通道、消息投递的不等待)。Prime 的并发设计里,「不在持锁路径上等待持锁者」是一条贯穿性纪律。
自动 refine:默认开启,但生性保守
AutoRefineSettings 默认 enabled: true、turnInterval: 25、冷却 20 分钟、压缩后也触发。但它要过三道闸:
- 资格:仅 depth-0 且有 local harness 目录的会话(子代理不做自动改进)。
- LLM gate:
reviewAutoRefine()用一段独立系统提示词审该不该 refine——"Reject one-off noise, unsupported hypotheses, and transient tool outputs."(输入截尾 40k,输出预算 4096。) - 保守指令:批准后使用的 instructions 额外强调 "Prefer an empty edits array over speculative or one-off memories. Do not promote anything global unless explicitly requested."
失败也会打冷却戳——防止每次 agent_end 都重试一次昂贵的失败。默认开启的自我改进,被调成了一台大部分时间沉默的机器。
双写者:没有锁,只有互相兜底
最后看一眼最微妙的共存关系:宿主(/refine)与 kernel(rlm.harness.*)都能写同一个 harness_state.json。Prime 没有加锁,而是让两侧各带一个防御:宿主在应用前重读磁盘 + baseline 乐观校验(§应用段);Python 侧的 HarnessState 每次读写前比对文件 mtime,发现宿主进程外重写就先 reload。两侧的损坏读取都降级为空状态——"must not crash the kernel"。
这是一个务实的判断:写冲突的实际概率(模型在 cell 里写 harness 的同时用户恰好手动 /refine)远低于加锁机制的复杂度成本;而低频冲突的后果被乐观校验兜住,最多一条 edit 失败。
实践应用
- 自我改进的可行性取决于约束的形状。类型化条目、证据字段、不可变基座、scope 隔离、乐观并发、历史重放回滚——每一项都在削减「agent 写记忆」的风险面。没有这些约束的同类尝试,通常会退化成噪声收集器。
- 规划与应用分离,让昂贵的部分可重叠。LLM 规划在后台与用户工作并行,只有毫秒级的应用段需要独占。这个模式适用于一切「AI 生成 + 确定性落地」的两段式操作。
- 把「上次改进的结果」回显给下次改进。refinement history 带 expectedOutcome 且回显进规划输入——改进系统由此获得纠错能力,而不是单调累积。
- 为 JSON 输出做病理分流。截断与畸形是两种病,给两种诊断。输出预算随模型弹性伸缩,固定小预算会系统性地杀死复杂提案。
- 影响半径要写进策略,不只是写进代码。local 默认、global 显式、local refine 中 global 只读——blast radius 成了提示词层的纪律。
总结
/refine 是一台「第二个压缩器」:从轨迹中提取可复用状态,以 JSON 编辑提案的形式,经证据纪律、并发防护、快照记录后落入持久层,并当场重建系统提示词。基座不可变是三层防御的硬底线,回滚靠 before/after 重放,自动模式默认开启却刻意保守。它回答了序言里的那个问题——「系统如何记住」——以一种几乎过度谨慎的方式。这正是它敢默认开启的原因。
记忆有了,改进有了。下一章看第三种沉淀:当重复的工作流重到需要代码承载时,技能系统如何把它做成可安装的 Python 包。