第 5 章 · 03 repair 自动修复与 REASONIX.md 项目记忆


文档摘要

第 5 章 · 03 repair 自动修复与 REASONIX.md 项目记忆 本节摘要:本节讲 agent 主循环的"自愈"能力与贯穿全章的项目记忆。先澄清一个常见误解:agent 包内的"工具失败自愈"(re-read 重读、schema 回显、循环保护)与 包(Reasonix 自身安装/升级的事务安全网)是两个完全不同的东西,本节分别讲清。然后精读 Reasonix 的项目记忆体系:REASONIX.md(注入每次会话系统提示的 cache-stable prefix,类 CLAUDE.md)、分层记忆(committed/shared REASONIX.md+AGENTS.md+CLAUDE.md / personal .local.

第 5 章 · 03 repair 自动修复与 REASONIX.md 项目记忆

本节摘要:本节讲 agent 主循环的"自愈"能力与贯穿全章的项目记忆。先澄清一个常见误解:agent 包内的"工具失败自愈"(re-read 重读、schema 回显、循环保护)与 internal/repair 包(Reasonix 自身安装/升级的事务安全网)是两个完全不同的东西,本节分别讲清。然后精读 Reasonix 的项目记忆体系:REASONIX.md(注入每次会话系统提示的 cache-stable prefix,类 CLAUDE.md)、分层记忆(committed/shared REASONIX.md+AGENTS.md+CLAUDE.md / personal *.local.md / ancestor 目录 / user-global)、@path 导入、#note 快速添加 always-on 指令、remember 工具持久记忆(frontmatter 文件 + MEMORY.md 索引,type 分类 scope 控制)。这套分层记忆是第 6 章 Cache-first 契约的"源头活水"。

内容来源:原项目源码 internal/agent/repeat_failure_guard.gointernal/agent/execute_one.gointernal/repair/(transaction.go/update.go/snapshot.go/apply_failure.go)、internal/memory/(remember.go/quickadd.go/store_v2.go)、REASONIX.mddocs/SESSION_MEMORY_RETRIEVAL.md,精读并套用体系化模板。

⚠️ 注意:internal/repair 包是 Reasonix 自身升级的事务安全网(snapshot/transaction/原子重命名/回滚),不是 agent 主循环里的"工具失败重试"。任务/教程中若把两者混为一谈,以本节澄清为准。

学习目标

阅读完本节,你应当能够:

  1. 区分 agent 主循环的"工具失败自愈"与 internal/repair 包的真实职责。
  2. 说出 execute_one 里"re-read / schema 回显 / 循环保护"三件自愈事各自解决什么。
  3. 解释 repeatFailureBreakThreshold = 2 的语义,以及为什么读操作不续这个预算。
  4. 说清 REASONIX.md 的定位(注入系统提示 cache-stable prefix)及其与 CLAUDE.md 的关系。
  5. 读懂分层记忆的优先级链:user-global → ancestor 目录 → 当前目录 normal → 当前目录 .local.md。
  6. 解释 @path 导入、#note 快速添加、remember 工具三者的区别与各自归属。
  7. 说出 remember 工具事实模型的 type(user/feedback/project/reference)与 scope(project/global)两个独立维度。

一、先澄清:两种"repair"不是一回事

很多人(包括不少教程)会把 Reasonix 里两个名字相近的东西混成一谈,本节一开始就把它们分清:

名称 位置 真实职责
工具失败自愈 internal/agent/(execute_one.go、repeat_failure_guard.go) agent 主循环里,工具调用失败后的就地恢复:重读文件、回显 schema、循环保护
repair 包 internal/repair/(transaction.go、update.go、snapshot.go、apply_failure.go) Reasonix 自身二进制/安装的升级事务安全网:下载更新 → 备份快照 → 原子替换 → 失败回滚

二者唯一的共同点是名字里都有"修"。本节先讲 agent 的工具失败自愈(因为它和前两节的 execute_one 主循环紧耦合),再讲 repair 包(它是 Reasonix 自己的"安装包管理器"内核),最后讲 REASONIX.md 项目记忆。

二、agent 主循环的工具失败自愈

第 5 章 01 节讲 execute_one 时提到过"总是 re-read""失败时回显 schema""循环保护"三件事,这里展开。

1. 总是 re-read

execute_one.go 在工具执行后(含工具返回错误的情况)有这段:

// Always re-read after post hooks — partial writes and hook side effects can // change the previewed path even when the concrete tool returned an error. a.observeAfterMutation(plan)

翻译:工具执行后(哪怕工具报错了),都要重新观察预览路径。原因:部分写(partial write)和钩子副作用可能在工具报错的情况下改了文件。比如 edit_file 改到一半失败了,文件可能已经是"改了一半"的状态;如果不重读,模型下一次还按"老内容"算 old_string,必然继续失败。re-read 让模型下次拿到的是磁盘真实状态,重试才能落地。

2. 失败时回显 schema

模型偶尔会把工具参数写成坏 JSON(比如把 options 写成 ["a":"b"])。agent 检测到 !json.Valid(call.Arguments) 时,会把工具的 schema 拼进错误信息回给模型:

if err != nil { detail := result // Malformed-args failures are a transient model JSON glitch ... echoing // the tool's schema makes the retry land valid instead of repeating the // same broken shape. if !json.Valid([]byte(call.Arguments)) { // ... 把 schema 拼进 detail } }

思路是"让重试有效,而不是无效重试":如果只告诉模型"参数错了",模型大概率再生成一遍同样的坏形状;把 schema 给它,它能在下一轮按合法形状重试。这是"修复重试"而非"无脑重试"。

3. 循环保护:repeat_failure_guard

repeat_failure_guard.go 是防"模型死磕一个改不对的 edit"的最后一道闸:

// repeatFailureBreakThreshold is how many identical write-like failures are // allowed before refusing the next attempt. Reads do not renew this budget // because they cannot make unchanged stale write arguments valid. const repeatFailureBreakThreshold = 2 func (a *Agent) repeatedFailureBlock(call provider.ToolCall, t tool.Tool) (string, bool) { sig, _, ok := a.repeatFailureSignature(call, t) // ... if record.count < repeatFailureBreakThreshold { return "", false } // ... return fmt.Sprintf( "blocked: [loop guard] %q has already failed %d times ... Rebuild the edit from the current file contents with a new old_string, use multi_edit for related changes, or explain the blocker in your final answer.", call.Name, record.count), true }

机制:对同一个写类工具、同样的写意图、同样的失败类别,连续失败 2 次后,第 3 次直接 block,返回一段明确的提示,要求模型"从当前文件内容重建 edit、改用 multi_edit、或在最终答复里解释这个阻塞"。

为什么阈值是 2 而不是 3 或 5?注释里 repeatFailurePreviewRechecksState 还有一个精巧的旁路:如果失败类别属于"可通过重读状态修复"的,loop guard 会再调一次 Preview 看状态是不是真的变了——如果文件已经被外部改成"能让这次写成功"的状态,budget 清零放行。这避免了"外部刚好改对了,但 loop guard 还在挡"的死板。

注释那句"Reads do not renew this budget because they cannot make unchanged stale write arguments valid"是设计的灵魂:读操作不续预算。如果读也能续预算,模型就会陷入"失败 → 读一下 → 再失败(因为读不能让过期的写参数变有效)→ 读一下 → 再失败"的无限循环。把读排除在续预算之外,逼模型要么真改写法,要么承认阻塞。

💡 契约要点:agent 的工具失败自愈是"三段递进":re-read(每次都重读,让模型拿真实状态)→ schema 回显(参数坏时给 schema,让重试有效)→ loop guard(同失败 2 次后硬挡,逼模型换思路)。这套机制让 agent 既能从偶发错误中恢复,又不会陷入死循环。注意它和 internal/repair 包毫无关系。

三、internal/repair:Reasonix 自身升级的事务安全网

internal/repair 是 Reasonix 自己的"安装包管理器"内核,和 agent 主循环完全无关。它的职责是:Reasonix 想升级自己时,保证"要么全成功,要么完全回滚,绝不留一个坏掉的半装版本"。源码关键文件:

  • transaction.go:RepairChange(单步变更)、RepairPlan(修复计划)、RepairPlanAction。把"升级"建模成一个有版本号(RepairPlanSchemaVersion = 1)的事务,每步变更可独立 undo。
  • update.go:updateTransactionVersion = 1pendingUpdateLockTimeout = 5 * time.Second。用 filelock 保证同一时刻只有一个更新事务在跑;5 秒拿不到锁就放弃。
  • snapshot.go:升级前给关键文件拍快照(PreviousStateID 绑定),出问题时按快照回滚。
  • apply_failure.go:UpdateApplyFailure 记录"更新安装器在 desktop 交接退出后失败"。注释解释:Windows 更新助手没法自己回滚(它跑在缓存目录里,在受验证的 Guard 安装之外),所以它写一个 marker,重新启动 Guard,Guard 下次启动时从安装目录内部做回滚。
  • update_handoff_test.go / update_legacy.go:跨平台原子重命名(rename_noreplace_*.go 按 darwin/linux/windows 分别实现)、遗留版本兼容。

为什么 Reasonix 要自己造这套?因为 Go 是单二进制,升级 = 替换正在运行的二进制 + 迁移配置/状态,这在三大操作系统上都是麻烦事(Windows 甚至不能直接覆盖正在运行的 exe)。internal/repair 把这件事做成"事务 + 快照 + 原子重命名 + 失败 marker + 回滚",保证用户不会因为一次失败升级得到一个启动不了的 Reasonix。

本节不展开 repair 包的每个文件(它和 agent 主循环解耦,属于"工具链/分发"范畴),但你只需要记住一句话:internal/repair 是 Reasonix 给自己装的"安全网",不是 agent 给工具调用兜的"重试"。 看到包名别被误导。

四、REASONIX.md:注入系统提示的 cache-stable prefix

讲完两个"repair",进入本节后半段:项目记忆。一切始于 REASONIX.md。这个文件开头的注释直接道破它的身份:

# Reasonix project memory This file is loaded into every session's system prompt (the cache-stable prefix), so keep it concise and durable — it is the project's standing instructions to the agent. It is the Reasonix analog of Claude Code's CLAUDE.md.

翻译:这个文件被加载进每次会话的系统提示(那个 cache-stable prefix),所以要写得简洁且持久——它是项目给 agent 的"长期指令"。它是 Reasonix 版的 Claude Code CLAUDE.md。

两个关键词必须记住。

第一,"every session's system prompt"。REASONIX.md 不是临时笔记,它对每次会话都生效——你写一次,以后每个会话的 agent 都读到。所以它必须放"跨会话稳定有效的指令"(比如"提交前跑 go test ./..."),不能放临时信息(比如"今天先别动 auth 模块")。

第二,"cache-stable prefix"。这是第 6 章的核心概念,这里先剧透:DeepSeek 的 prefix cache 靠"系统提示前缀字节稳定"来命中缓存。REASONIX.md 是前缀的一部分,所以它每多一段话,每 turn 都要为这段话付 token 成本(缓存命中也还是要传 token,只是不算计算费用)。这就是为什么注释强调"keep it concise and durable"——啰嗦的 REASONIX.md 会拖慢每个会话、每个 turn。

REASONIX.md 自己里面写的"Conventions"三条,是整个项目的宪法级约定:

  1. Go kernel under internal/;each package owns one concern(每个 internal 包只管一件事)。
  2. One transport-agnostic control.Controller sits behind every frontend(三个前端共用同一个 Controller)。
  3. Cache-first:system-prompt prefix 必须字节稳定,永不 mid-session 改,ride the turn tail(第 6 章主角)。

第 3 条就是下一章的入场券。

五、分层记忆:四级优先级链

REASONIX.md 只是分层记忆的一层。SESSION_MEMORY_RETRIEVAL.md 把 standing instructions(常驻指令)的解析顺序写得很清楚。Reasonix 认三种主文件名:REASONIX.mdAGENTS.mdCLAUDE.md(外加对应的 .local.md 变体)。解析顺序是:

规则口诀:"Deeper directories beat broader directories, and a local variant beats normal files in the same directory. Later entries therefore win when rules conflict." 更深的目录压过更宽的目录;同一目录里 local 变体压过 normal 文件;冲突时后加载的胜出。最终,当前用户请求是最高权威的用户指令——任何 standing instruction 都不能盖过用户当下明确说的话。

几个细节。

AGENTS.md 不是 CLAUDE.md 的 fallback,是平起平坐。 REASONIX.md 的 Memory 节特意强调:"All distinct supported files in a directory load; AGENTS.md is not merely a fallback." 一个目录里 REASONIX.md、AGENTS.md、CLAUDE.md 可以同时加载,都算数。这跟很多人"AGENTS.md 是 CLAUDE.md 没有时的备胎"的误解相反。

user-global 在分层最底。 解析从 user-global 开始(REASONIX_STATE_HOME,否则 REASONIX_HOME,否则 macOS/Linux ~/.reasonix、Windows %APPDATA%\reasonix),然后从工作区根走到目标路径。所以 user-global 是"最宽但最弱"的一层——项目级指令能覆盖它。

内容相同的文件去重。 "Files with identical expanded content are deduplicated, preferring the more specific source." 展开后内容完全一样的文件去重,保留更具体的来源。这避免了同一份指令被重复加载进系统提示(那会让前缀变长、浪费 token)。

@path 导入

指令文件可以用一行独立的相对路径导入另一个文件:

@docs/agent-testing.md

规则:导入是确定性展开、去重、最多 5 层、且被限制在源指令文件所属目录内。绝对路径、父目录逃逸、符号链接逃逸、不可读的导入、循环导入都会被拒绝并作为诊断暴露出来,而不是静默信任。你可以在 CLI 里用 /memory instructions 看实际展开结果(加载顺序、scope、目标目录、导入、诊断)。

@path 让你能把大段指令拆成多个文件维护,但展开后仍是 cache-stable prefix 的一部分,所以"导入越多,每 turn 成本越高"——和 REASONIX.md 本身一样的取舍。

六、#note 与 remember 工具:快速指令 vs 持久事实

standing instructions 之外,Reasonix 还有两种"添加记忆"的途径,容易混。

1. #note 与 /remember:追加 standing instruction

在聊天里用 #<note> 或 CLI 的 /remember <note>,会直接往项目的指令文档追加一条 always-on 指令internal/memory/quickadd.go 实现写入侧:

// quickAddHeading marks the section quick-added notes accumulate under, so // repeated "#" additions group together instead of scattering through a // hand-written file. const quickAddHeading = "## Notes" // AppendDoc appends a one-line note as a bullet under a "## Notes" section ...

机制:把 note 规范化成一行,追加到指令文件的 ## Notes 节下(没有就建),保证 note 不会散落在手写文件各处。它本质是一次普通文件编辑,用户以后还能手动重排。SESSION_MEMORY_RETRIEVAL.md 明确:"In the CLI, /remember <note> and # <note> directly append a note to the project instruction document. They are shortcuts for standing guidance, not the agent's background-fact remember tool."——它们是 standing guidance 的快捷方式,不是 remember 工具。

2. remember 工具:持久背景事实

remember 是个工具(tool.Tool),由模型主动调用。internal/memory/remember.go:

// rememberTool lets the model persist a durable fact to the auto-memory store. // It is stateful (bound to one project's Store), so boot constructs it and adds // it to the registry — the same pattern as the task tool — rather than // self-registering as a stateless built-in. type rememberTool struct{ store Store }

注意"stateful"和"boot constructs":remember 工具有状态(绑定一个项目的 Store),所以它由 boot 在装配时构造并加进 registry,不像无状态的 built-in 那样自注册。

remember 的事实模型每个事实是一个带 frontmatter 的 Markdown 文件,字段包括:不可变的 id、单调的 revisioncreated_at/updated_at、name/title/description、独立的 typescope、Markdown body。

type 分类内容(四个值):

type 含义 用途示例
user 用户身份或偏好 "用户偏好深色主题"
feedback 关于"怎么工作以及为什么"的指导 "提交前要跑 lint,因为 CI 会卡"
project 代码里看不出来的项目目标/约束 "当前在 release-2.3 分支"
reference 外部资源(URL、工单号) "JIRA ticket PROJ-123"

scope 控制范围(两个值):project(安全默认,只在本工作区)和 global(必须显式选择,在每个工作区都生效)。

type 不蕴含 scope。 文档强调:"Type does not imply scope. Project feedback remains project-local, and a global reference remains a reference." 一条 project feedback 仍是 project 局部;一条 global reference 仍只是 reference。两个维度正交,不会因为 type 是 reference 就自动 global。

remember 工具的 Description(给模型看的)里有段很实用的指导,大意是:别记仓库已经记录的东西(代码结构、git 历史),也别记只对当前对话有意义的事;如果用户让你记这类东西,记下它背后那个"非显然的点"。这对防止"模型把一串代码片段当事实存下来"很有用。

3. 三者对比

途径 谁触发 写到哪 性质 进入前缀时机
REASONIX.md 等指令文件 人手写 项目根/分层目录 standing instruction(常驻指令) 会话开始即加载
#note / /remember 用户在聊天里 追加到指令文件 ## Notes standing instruction 的快捷方式 下次会话进前缀(本轮只 tail note)
remember 工具 模型调用 frontmatter 事实文件 + MEMORY.md 索引 background fact(背景事实,可过期) 索引下次会话进前缀;体当轮 tail

最后一列是关键区别,也是和第 6 章的衔接点:任何"新加的指令/事实"都不会 mid-session 改系统提示前缀(那会打掉缓存),而是先以"turn tail note"的形式在本轮生效,下次会话才自然并进稳定前缀。这正是 REASONIX.md "Never mutate it mid-session — ride the turn tail instead" 的具体落地。

本节要点回顾

  1. 两种 repair 别混淆:agent 主循环的"工具失败自愈"(re-read/schema 回显/loop guard)是主循环内的事;internal/repair 包是 Reasonix 自身升级的事务安全网(快照/事务/原子重命名/回滚 marker),两者无关。
  2. 工具失败自愈三段递进:re-read(每次重读拿真实状态)→ schema 回显(参数坏时给 schema 让重试有效)→ loop guard(同失败 2 次硬挡,读不续预算)。
  3. internal/repair:Go 单二进制升级的事务化(版本号、filelock、快照、apply_failure marker、跨平台原子重命名),保证"要么全成功要么全回滚"。
  4. REASONIX.md:注入每次会话系统提示的 cache-stable prefix,类 CLAUDE.md;须简洁持久(每段话每 turn 都要付 token)。
  5. 分层记忆四级链:user-global → ancestor 目录 → 当前目录 normal → 当前目录 .local.md → 用户请求(最高权威);"deeper beats broader, local beats normal";AGENTS.md 与 CLAUDE.md 平起平坐。
  6. @path 导入:确定性展开、去重、最多 5 层、限源目录内,逃逸/循环拒绝并诊断;/memory instructions 看结果。
  7. #note vs remember:#note//remember 是追加 standing instruction(下次会话进前缀);remember 工具是存 background fact(frontmatter 文件 + MEMORY.md 索引,type 四值/scope 两值正交);两者都"本轮 tail、下次前缀",永不 mid-session 改前缀。

下一章是全书高潮(★):Prefix-cache 友好的上下文维护。我们会从 REASONIX.md 的 Cache-first 契约原文出发,讲清 DeepSeek prefix cache 与"字节稳定前缀"的关系、boot 如何装配系统提示、control.Compose 如何"ride the turn tail"、compact/snip/prune 如何在不得不改前缀时把代价降到最低。这是 Reasonix 把"LLM 推理优化"做成"系统工程"的核心。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U