← 返回
AI技术

ADR 太多让 AI 遵循过时决策,怎么解:全保留,用索引标出当前有效项

✍️ zhirenhun 📅 2026/10/29 👁 7 阅读 ⏱ 22 分钟
ADR 太多让 AI 遵循过时决策,怎么解:全保留,用索引标出当前有效项

“我的 AI Agent 总是依赖已被取代的架构决策”,这个问题最直接的修法是写个小解析器:读取每个 ADR 的 Supersedes 字段,把旧记录标记为已替换。我们确实做了这么一个工具。然后拿它跑了另外两个仓库,分别有 270 和 93 条 ADR,结果它把仍在生效的决策报成了已废弃。在较大的那个仓库里,它判定为“已取代”的 5 条记录全是误报。

只抓取 ADR 编号的解析器不只是会漏东西。它还会自己制造误读,而 AI Agent 会信以为真。本文记录我们如何在 ELN workflow(我们内部给 Claude Code 用的插件套件)里重建 ADR 索引,让“哪些决策仍然有效?”由一套确定性的规则来回答,而不是靠模型去读 ADR 正文再猜。本文是上一篇文章的续篇:AI 忽视已有决策的真正原因:设计一个 ADR 上下文自动注入 hook。那篇文章结尾留了两个已知缺口:注入列表到 30 条就截断,另有 29 条用其他格式写的记录完全没被读取。

这些 hook 是 Claude Code 专用的。但问题本身不是:任何 coding agent,不管是 Claude Code、Codex 还是别的,只要在一个有几百条 ADR 的仓库里干活,都得搞清楚哪些决策仍然成立。

简要版

  • 四条交付路径。会话开始时提供一份最新优先的简报,附一张已被取代的记录表;Agent 即将读取旧记录时给出警告(无论通过 Read 工具还是 cat,且从不阻断);对拉取请求所改动的头部行做 CI 检查;以及一条命令,同时更新旧 ADR、新 ADR 和索引。
  • 结果。启动简报从 3,511 字节降到 1,140 字节,索引可见的现行决策从 126 条增至 155 条,展示的已取代行从 0 条增至 5 条。在 179 条记录时,简报为 1,186 字节,预算为 1,200 字节。
  • 只认数字的 ADR 解析器会误读的四种情况

    第一版会抓取 Supersedes 和 Superseded by 字段中的每一个 ADR 编号。对照真实文件检查,并在另外两个仓库上运行后,发现它把四类头部信息读错了。

    由此得出的规则很短,主要讲的是解析器拒绝推断哪些内容:

    Agent 实际收到的信息

    有人建议把阅读规则写进 README。但一条 Agent 可能读、也可能不读的说明,没有机制上的保障,我不放心,所以先做了测量。当时仓库里有 167 份 ADR,总计 2,171,690 字节,最大的一份 68,777 字节。

    我担心的那种行为——Agent 用 cat 一次性读取所有 ADR——在这 38 个会话里一次都没发生。它实际做的是列出目录(36 次),按文件名挑选,再逐个读取,这正是索引让它做的事。

    保留文件,每次重建索引

    ADR 存在的意义,是让今天设计背后的推理可以追溯;删掉旧记录就失去了这一点。所以文件保持原样:不删除、不移动、不拆分。插件的技能、钩子和脚本按编号和路径引用 ADR,167 条记录中只有一条 ADR 没有被任何地方引用。移动文件会破坏这些引用。

    "这条决策是否现行?"由索引来回答,每次都由一个解析器从头部字段(状态、日期、取代、被取代)重新构建。仓库里没有提交 index.json,也没有缓存,因为存储的索引会像手写表一样逐渐偏离文件。

    docs/adr/README.md 里那份供人阅读的表格还在,但它是自动生成的:eln-adr 会重写一个带标记的区块,如果该区块与重新运行生成的结果不一致,CI 就会失败。简报和读取警告从不使用那张表,它们用的是对文件的解析结果。

    生效状态 含义 是否计为当前
    live 已接受或已修订,未被取代 是
    partial 部分被更新的记录取代 是,并显示取代它的记录
    superseded 已被更新的记录取代 否
    proposed 尚未接受 否
    rejected 我们未采纳的选项 否
    unknown 状态不在固定词表内 否,并计入计数

    索引如何送达 Agent

    没人读的索引改变不了任何事。送达路径有四条。

    会话开始时的简报

    Claude Code 启动时,一个钩子(Claude Code 在固定时机自动运行的脚本)会把以下内容放进 Agent 的上下文。

    顺序是固定的:计数、CLI 用法、被取代记录表、按修订日期排序的最新 10 条记录、proposed 计数。预算为 1,200 字节,裁剪顺序同样固定(proposed 降为计数、缩短标题、最新记录减到 8 条、再减到 6 条、被取代记录表降为计数、标题再缩短,最后去掉最新记录列表)。计数行、CLI 行和被取代记录的数量始终保留。简报从不引导 Agent 去执行 ls docs/adr/。在 compact 或 resume 之后,只注入不超过 300 字节的指针:上下文刚被压缩就推回一大块内容,等于让压缩白做,而索引随时可以用 CLI 重新生成。

    Agent 即将读取 ADR 时的警告

    第二个钩子在 Agent 读取 ADR 之前运行,查出该记录的生效状态,只在确有影响时才出言提醒。

    每条警告都带 CLI 路径。Claude Code 内部启动的子 Agent 收不到会话启动简报,对它们来说这条警告就是唯一的指引。

    eln-adr show:先看要点

    show 用不超过 2.5 KB 返回一条记录的核心信息。

    这里一条 25,216 字节的记录压缩成了 878 字节。有了状态、替代记录、修订、标题所在行号和决策开头,就足以判断要不要继续读,以及读哪几行。

    show 还会在部分被取代的记录上标 ⚠️,并把替代记录标注为“(partial)”,提示 Agent 那部分要去读替代记录。记录本身仍算现行有效,状态行写的是“effective: partial”。

    CI 只查改动行,每个生命周期步骤一条命令

    索引做得再对,下一个写错的头部就能把它毁掉。CI 检查 PR 新增或修改的 ADR,而且只查改动过的头部行:状态词、- Date: YYYY-MM-DD 行、取代字段的格式、只在单边声明的取代关系、新增的重复编号。每类失败都有对应的原因码。未改动的旧记录不会被拦,所以现有 167 个文件不必一次性重写。

    接受、修订、取代各是一条命令。eln-adr supersede <old> --by <new> 会一次性写入旧记录的“Superseded by”、新记录的“Supersedes”以及索引表。默认是试运行,只有加上 --write 才真正写入。写入前会重新解析结果,只要以下任一项不通过就完全不写:目标字段之外的行保持不变、CI 检查零阻断问题、再次运行同一命令不产生任何变化、字段中已有的每条声明仍然存在。

    数据

    用同样的命令测量,之前 167 条记录,之后 168 条(这次变更本身的 ADR 也加了进去):

    之前 之后
    Agent 启动时收到的文本 3,511 B(最旧的 30 条) 1,140 B(计数、被取代表、最新 10 条)
    索引中可见的有效决策 126 155
    被取代表 0 行 5 行(另有 2 条待定)
    “用 ls docs/adr/ 查看其余” 有 无

    后来又测了一次,179 条记录:

    收尾

    保留全部ADR是正确决定。问题出在让模型自己从正文推断“这条是否仍然有效”。只要模型还在做这个推断,随着记录数量增长,启动列表就会越来越偏向旧决策,Agent不得不逐条打开正文核对,还要为此消耗token。我们把答案移入一个索引,每次依据同一套规则构建,在启动时以及即将读取旧记录时提供,并通过CI和一步命令让新记录的头部保持严格。最关键的一条规则是事先确定解析器不得推断哪些内容;没有这条规则,解析器自己就会把仍然有效的决策报告为已被取代。

    注释:术语与截图


    关于ELN工作流——我们自研的、用于保障AI开发质量的插件套件——的文章将继续以系列形式发布。感兴趣的话,欢迎点赞关注。

    我们的产品:https://www.eln.ne.jp/products


    EarthLink Network 自研产品,让整个公司跑在 AI 之上——每一款产品都源于一路走来的实际需求。以下是我们的产品全览:

    → EarthLink Network 自研的 18 款产品

    想了解公司及各产品的更多信息,请访问 www.eln.ne.jp。

    ——

    广告 广告
    🧑‍💻

    zhirenhun

    一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

    claudecode ai architecture devtools