AGENTS.md 不是普通文档:一次真实项目里的 Agent Context 治理

0 阅读15分钟

当 Agent Instructions 从几 KB 长到几十 KB,它就不再只是 Markdown 文档,而是一份有运行时预算、有作用域、有到场时机和生命周期的 Context 资产。


本文实验项目

这篇文章不是为了说明问题而构造的 Demo。

整个治理过程来自我正在维护的一个 DeepSeek Harness 插件:

dsh-knit

它解决的是另一个 Context 问题:根据当前任务,从项目工作区找到更相关的文档、代码和其他上下文,并将这些上下文组织给人和 Agent 使用。

它本身采用本地、确定性的方式工作,不依赖模型调用,也不依赖网络。

而这次 AGENTS.md 治理,恰恰就是在维护 Knit 的过程中发生的。

本文中的 L0 / L1 / L2 / L3、预算闸门、任务触发词路由、Claims 与文件指纹,全部来自这个真实项目。

项目:PolinniZhong/dsh-knit


一、我原来以为 AGENTS.md 是文档,直到发现它其实有 Runtime Budget

我的项目里有一份 AGENTS.md,是给 AI 干活的执行规则:

怎么改代码、哪些不能动、发布怎么做、界面数值是多少。

它一度是:

65,092 B
≈ 63.6 KiB

而我使用的 DeepSeek Harness 宿主,对 Agent Instructions 的 maxBytes 是:

65,536 B
= 64 KiB

也就是说,这一份文件已经占掉了:

65,092 / 65,536
≈ 99.3%

这里先说明一个单位问题:

本文统一使用 KiB,即 1024 进位的二进制单位,不把 KB 和 KiB 混用。

宿主到底怎么处理超预算?

我直接读了 dsh-agent-instructions 的实现。

它不是智能摘要。

也不是把 Markdown 理解以后重新压缩。

它的核心流程更接近:

flowchart TD
    A[Instruction Files] --> B[按层级收集]
    B --> C{是否超过 maxBytes}
    C -->|否| D[正常注入]
    C -->|是| E[先丢弃较宽泛的文件]
    E --> F{最具体文件仍超预算?}
    F -->|否| G[剩余文件正常注入]
    F -->|是| H[保留前面的字节]
    H --> I[截掉后面的内容]
    I --> J[加入截断提示]

所以本质上不是:

“模型帮我总结规则。”

而是:

预算不够,就裁。

而且它保留的是前面的内容,后面的字节被截掉。


64 KiB 也不是全部留给正文

这里还有一个很容易被忽略的细节。

实际注入的文本除了正文,还会有:

  • system reminder 框架
  • 固定 intro
  • Instructions from: <relative path> 文件头
  • 空行等渲染开销

文件路径长度还会影响文件头大小。

在我这次具体的单文件场景里,文件就在会话工作目录下,显示路径就是:

AGENTS.md

因此扣掉渲染开销以后,对应的正文空间大约是:

65,232 B

这个数字不是所有项目、所有路径的通用常数。

路径越长,可用于正文的空间还会进一步减少。


但这里发生了一个很重要的事实纠正

我原来以为:

65,092 B 的 AGENTS.md 已经发生过静默截断。

于是我去查证据。

结果发现:

65,092 B < 65,232 B

所以在当时的单文件场景下:

65,092 B
+ 渲染开销
= 65,396 B

65,396 B < 65,536 B

它并没有触发截断。

我甚至把自己项目里原来的 65,100 B 警戒线也重新查了一遍。

最终只能确认:

65,100 B 是项目自己设定的一条保守线,但没有找到可靠的历史留档来证明它为什么是这个数字。

所以我没有继续把它写成:

“我抓到了一个 Context 截断事故。”

因为没有证据。

真正被证实的是另外一件事情:

我曾经让 Agent Instructions 长期贴着宿主运行时预算上限运行,而且没有一个可靠的预算治理机制。

这已经足够构成一个工程问题。


二、真正的压缩,不是删句子,而是控制 Context 的到场时机

我第一反应当然是删。

实际上,我确实手工压过一轮:

65,092 B
↓
60,387 B

但这只能解决一个问题:

这一版更短了。

它没有解决:

哪些内容根本不应该每一轮都进入 Context?

只要继续维护几版,这个文件还会重新膨胀。

所以我把问题换成:

这段内容,什么时候必须进入 Agent 当前 Context?

这成为整个治理过程中最重要的一刀。

我最后把项目里的规则人工分成四层:

flowchart TB
    S[Agent Instructions Source]

    S --> L0[L0 常驻规则<br/>Every Turn]
    S --> L1[L1 作用域规则<br/>Relevant Scope]
    S --> L2[L2 任务资料<br/>Relevant Task]
    S --> L3[L3 历史 / 取证<br/>Historical / Evidence]

    L0 --> A[跨任务硬约束<br/>当前状态 / 禁止事项]
    L1 --> B[代码纪律<br/>测试写法 / 几何 / 颜色]
    L2 --> C[发布剧本<br/>真机验收 / 故障恢复]
    L3 --> D[版本历史<br/>决策记录 / 被否方案]

L0:每轮都要到场

包括:

  • 跨任务硬规则
  • 当前状态
  • 禁止事项
  • 遇到什么事情应该去读什么资料

这是最贵的一层。

因此,它不应该承担“整个项目百科全书”的职责。


L1:进入作用域后再到场

我的实现代码都在 knit/ 下面。

例如:

  • 构建约束
  • 测试写法
  • UI 几何参数
  • 特定代码纪律

这些内容并不需要在处理项目中其他事情时一直存在。

宿主本身已经提供了嵌套 instruction 的发现机制。

我用探针实测后确认:

read / write / edit
        ↓
成功访问相关文件
        ↓
发现对应目录的 AGENTS.md
        ↓
后续 Context 获得相关作用域规则

而 shell 并不属于同样的触发路径。

这个细节让我意识到:

Context 什么时候出现,本身就是工程设计的一部分。


L2:做某件事情才读取

例如:

发布 / 打 tag / 对校验和
→ 发布剧本

改完界面 / 真机验收
→ 验收清单

插件在界面里全不见了
→ 恢复手册

这些内容不是没用,而是:

没必要每一轮都存在。


L3:历史、决策与取证

例如:

  • 每个版本发生过什么
  • 为什么没有采用方案 B
  • 完整的故障取证
  • 被否方案
  • 历史踩坑

它们的价值在于:

需要时可追溯。

而不是:

每一轮都必须进入模型。


这里必须说清楚:

L0 / L1 / L2 / L3 是我在 Knit 项目里人工定义的治理层,不是 DeepSeek Harness 官方的四种 Context Mode。

宿主真正提供的是:

层级发现
+
作用域机制
+
预算限制
+
去重
+
增量更新

我只是把这些能力组织成了一套更明确的 Context 治理方式。


三、Context 治理不是压缩率,而是“放多少、什么时候放、去哪里找”

拆完以后,我才发现一个更有意思的问题:

Context Governance 并不是一个压缩率问题。

至少有三个问题:

flowchart LR
    A[Agent Context Governance]

    A --> B[放多少?]
    A --> C[什么时候放?]
    A --> D[去哪里找?]

    B --> B1[Budget]
    C --> C1[Layer / Scope]
    D --> D1[Task Routing]

    B1 --> E[可控 Context]
    C1 --> E
    D1 --> E

预算解决:

放多少?

分层解决:

什么时候到场?

路由解决:

做什么事情应该去哪里找?

然后还有两个更后面的问题:

这份规则是不是最新的?

我现在依据的是哪一版?

这对应:

Integrity / Claims
+
Provenance / Fingerprint

于是,一个原本看起来很简单的 Markdown 文件,开始出现了完整的工程治理维度:

Budget
Scope
Timing
Routing
Integrity
Provenance

这才是我后来开始用:

Agent Context Governance

来描述这件事情的原因。


四、从“权威分工”到 Context Routing

这是这次改动里,我最喜欢的一条。

原来的规则是一张:

“哪些问题去哪些文件看”的权威分工表。

看起来很清楚。

但它隐藏了一个问题:

Agent 仍然需要记住这张表。

于是我把它改成任务触发词路由。

发布 / 打 tag / 对校验和
→ 发布剧本

改完界面 / 真机验收
→ 验收清单

插件在界面里全不见了
→ 恢复手册

也就是从:

Knowledge
   ↓
File

变成:

Task
   ↓
Trigger
   ↓
Reference

这个区别很小,但我认为更符合 Agent 工作方式。

因为 Agent 面对的通常不是:

“我现在需要知道某类知识。”

而是:

“我现在正在做一件事情。”


但它不是自动检索

这里一定要把边界说清楚。

这次实现的是:

当前任务
   ↓
触发词
   ↓
告诉 Agent 去哪里找

不是:

当前任务
   ↓
系统自动读取文档
   ↓
直接注入 Context

所以它降低的是:

Context Retrieval Cost

不是:

Context Retrieval Success

它提高的是找到正确上下文的概率和效率,而不是保证 Agent 一定会执行读取。


有意思的是,这一步改完以后,常驻层反而少了:

253 B

我增加了三条路由,同时把一整段版本史移到了历史层。

所以:

真正省下来的不是压缩算法,而是放错了地方的内容。


五、让 Context 治理进入工程流水线

如果只做一次拆分,几个月以后它还是会长回来。

所以我给项目增加了几个治理闸门。


5.1 Budget Gate

我增加了一套:

knit/tools/agents-budget.mjs

项目预算暂时定义成:

L0 ≤ 16 KiB
L1 ≤ 40 KiB
L0 + L1 ≤ 56 KiB

为什么不直接把预算放到宿主的 64 KiB?

因为我要给未来的 Context 注入留出空间。

当前数据是:

层正文项目预算余量
L015,936 B16,384 B448 B
L140,186 B40,960 B774 B
合计56,122 B57,344 B—

而实际注入时还有渲染外壳。

本次测量:

正文
56,122 B

实际载荷
56,463 B

宿主上限
65,536 B

因此:

65,536
-
56,463
=
9,073 B
≈ 8.86 KiB

这才是这次场景下真正意义上的剩余空间。

注意:

这 9,073 B 不是“压缩节省出来的空间”,而是当前实际 payload 距宿主上限的剩余预算。


5.2 Integrity Gate

规则文件还有一个问题:

它会过期。

例如:

版本:0.20.0
测试:641 项
测试文件:26 个

这些数字写在 Markdown 里以后,代码继续变化,它们就可能变成假话。

所以我给预算工具加入了:

node knit/tools/agents-budget.mjs --claims

检查:

版本号
测试文件数量
测试计数
N/N 自洽

然后发生了一件很有意思的事情。

第一次运行时,校验器自己先出 Bug 了。

它看到:

641 / 1

认为测试计数出现了多个值。

结果一查,是:

aspect-ratio: 1/1;

它把 CSS 的比例值识别成了测试数。

于是我收紧了正则,然后又故意传一个错误的测试数做反向验证:

真实值 641
→ 通过

故意传入 999
→ 必须失败

最后才算通过。

这个过程让我更加确认一件事情:

治理工具自己也必须被治理。


5.3 Provenance

我还给两层指令文件增加了:

sha256
mtime

这里不做完整的 Context Snapshot。

它解决的是一个更加实际的问题:

以后回头看发布记录,我能知道当时依据的是哪一版源规则文件。

所以我把它称作:

Context Provenance

可以简单理解成:

classDiagram
    class Context {
        +sourcePath
        +loadMode
        +reason
    }

    class SourceFile {
        +path
        +sha256
        +mtime
    }

    Context --> SourceFile : derived from

它并不能还原最终模型收到的完整 payload。

但是它已经比:

“我记得当时应该是那个版本。”

可靠很多。


5.4 Release Gate

版本一致性、发布前检查、打包校验这一层本来就有。

现在只是和:

Budget Gate
+
Integrity Gate
+
Release Gate

一起进入发布前的治理流程。

当前项目测试:

641 项
26 个测试文件

全部通过。


六、为什么其他 Coding Agent 也在往类似方向走?

这次治理不是一个孤立的项目技巧。

不同 Agent 的实现完全不同,但它们都已经在暴露同一个事实:

Project Instructions 本身就是 Runtime Context。

Codex:Instruction 本身存在明确 Budget

OpenAI 当前 Codex agent loop 文档明确描述了 AGENTS.md 的聚合方式,并设置默认 32 KiB 的项目指令限制,同时根据目录层级合并更具体的 instructions。

这意味着一个非常重要的事情:

AGENTS.md 从产品语义上已经不是“普通 Markdown”,而是 Agent Runtime 的输入。


Gemini CLI:Hierarchy + JIT

Gemini CLI 现在支持:

Global
↓
Workspace / Project
↓
Subdirectory
↓
JIT Context

当工具访问某个目录或文件时,它还可以进一步发现对应目录中的 GEMINI.md。

这个思路与我在项目里的 L1 作用域治理非常接近:

相关 Context 在真正进入相关工作范围后再出现。


Claude Code:把项目规则和其他知识拆开

Claude Code 也采用项目级规则文件、用户级记忆和引用机制,把真正需要长期保留的规则和更大范围的知识组织开。它代表的也是同一个方向:

不是所有知识都应该进入每一轮的主 Context。


所以我现在越来越倾向于把这件事情叫:

Context Supply Chain

规则不是孤立的一份 Markdown。

它有:

Source
↓
Scope
↓
Routing
↓
Injection
↓
Validation
↓
Provenance

整个链路都需要治理。


七、我特意没有做“智能 Context Compiler”

到这里,可能有人会问:

既然都开始治理 Context 了,为什么不直接让 LLM 把 AGENTS.md 总结成更短的版本?

因为我认为:

工程规则和普通知识的容错率不是一个级别。

如果做:

AGENTS.md
   ↓
LLM Summary
   ↓
Compiled Context

确实可能得到更短的内容。

但同时会带来:

  • 规则遗漏
  • 语义变化
  • 优先级误判
  • 每次编译结果不一致
  • Source 与 Context Payload 不一致

然后你还要再回答一个问题:

Agent 到底收到了什么?

所以这次我刻意没有做四件事。

1. 不做 Runtime Section Compiler

不在运行时重新把人工规则编译成另一份模型专用规则。

2. 不用关键词给规则自动打优先级

规则已经人工分层。

再让正则自动打分,反而可能产生误判。

3. 不让同一个文件动态切出不同 Context 片段

否则每一轮进入 Agent 的内容都可能发生变化,Context 会变得很难解释。

4. 不在项目里再造第二套 Context Inspector

最终:

Agent 到底收到了什么 Context?

应该由宿主提供透明度。

项目只负责把自己的规则治理好。


八、把这次实践抽成七条可以搬走的原则

走到这里,我觉得这件事已经不只属于 dsh-knit。

如果换成其他 Coding Agent,我仍然会建议:

1. 先量预算,再谈压缩

不知道距离宿主上限还有多少,“太大了”只是感觉。

2. 按“何时必须到场”分层

不要只是按主题拆文件。

主题决定归档,时机决定注入。

3. 给延迟内容配任务触发词路由

不要让 Agent 记一张“什么知识在哪个文件”的表。

4. 把容易漂移的说明变成机器可检查的断言

能计算的东西,不要只写在 Markdown 里。

5. 给 Context Source 建立 Provenance

至少能够知道:

Source Path
sha256
mtime

6. 不要为了压缩率牺牲可解释性

对于工程规则来说:

可解释性本身就是一种工程能力。

7. 区分“机制变好了”和“Agent 行为变好了”

这次治理能证明的是:

预算可测
规则可分层
任务可路由
声明可检查
来源可追溯
宿主可去重

但不能证明:

Agent 因此已经更听话了。

要证明行为收益,还需要后续几十个真实 Coding Session 的前后对比,例如:

规则违规率
返工次数
错误资料读取率
人工介入次数
任务完成率

写在最后

我以前把 AGENTS.md 当成项目说明书。

现在更愿意把它看成:

Agent 的 Runtime Context 输入。

一旦这么看,很多以前觉得不重要的问题就会出现:

它有多大?
什么时候进入?
为什么进入?
应该去哪里找?
是不是最新?
有没有重复?
还能再长多少?

这也是我这次治理最后留下来的一个判断:

Agent 规则不是写下来的那一刻就对 Agent 产生作用,而是进入当前 Context 后,才真正获得被看到和使用的机会。

但这里故意不加一句:

“进入 Context 就一定会生效。”

因为这不是同一个问题。

Context Governance 解决的是:正确的规则能不能以可控、可解释、可验证的方式到场。

至于 Agent 最终有没有按照这些规则行动,那属于下一层——Context Effectiveness。

而这可能才是 AI Coding 从“模型会写代码”进入“Agent 能持续做工程”之后,真正值得继续研究的问题。