工作区的基本法:.agents.md 规范与指令对齐
What(本文讲什么)
.agents.md(或同类的 AGENTS.md / CLAUDE.md / Copilot instructions 等)不是“写给人的 README”,而是写给 agent runtime 的一份“可注入规则集”:它告诉系统在这个工作区里哪些行为是允许的,哪些是禁止的,哪些必须走门禁(超时、重试、幂等、隔离、权限、审计、观测)。
这篇文章把规则系统讲成一个可工程化落地的模块:
- 规则文件的角色边界:它解决什么,不解决什么。
- 规则的发现与层叠:目录级叠加与覆盖语义。
- 规则如何进入执行链:注入点、装配策略与审计证据。
- 为什么“写规则”必须配合门禁与日志,否则仍然会出事故。
Problem(要解决的工程问题)
agent 在仓库里工作时,失败经常不是“不会写代码”,而是“不知道你的边界”:
- 风格漂移: 缩进、命名、注释语言不一致,改动扩散,review 成本飙升。
- 越界读写: agent 修改了不该动的目录,或把临时产物写进源代码树,引发回滚灾难。
- 架构腐蚀: 业务逻辑被平铺到
components//lib/,跨模块深层导入,违反依赖倒置。 - 安全事故: 把密钥写进仓库、把敏感文件打包外传、或在不该联网的时候联网。
- 不可复盘: 出事后你只能看到“它改了什么”,但不知道“当时它遵循了哪些规则、为什么这么做”。
所以规则系统的目标不是“让 agent 变聪明”,而是把风险从随机变成可控:让错误可预测、可阻断、可回滚、可审计。
Principle(规则文件的定位:持久指令,而不是 runtime)
很多工具支持把“规则文件”放进仓库并在会话开始时加载。JetBrains 的文档就明确区分了“随仓库携带的 instruction files(例如 AGENTS.md / CLAUDE.md)”与 IDE 内部的 project rules。你的结论应该是:
- 规则文件是“持久指令载体”:版本化、可 review、可回滚。
- 注入引擎(runtime)是“执行者”:决定什么时候注入、注入多少、如何裁剪、如何记录审计证据。
- 门禁与治理才是“硬约束”:超时、重试、幂等、权限、隔离、审计、观测必须在执行层落实。
把规则写得再漂亮,如果 runtime 不执行,它就只是文学作品。
Usage(怎么用:把规则系统接进你的控制回路)
1) 规则文件要写成“可执行约束”
好的规则文件不是“倡议书”,而是“可判断的约束”。你要尽量避免:
- 模糊词: “尽量”“最好”“推荐”。
- 无门禁: 只说“不要越权”,但没说越权如何被阻断。
- 无范围: 没说清楚哪些目录/文件是禁区,哪些工具可用。
把规则写成下面这种结构,runtime 才能把它变成确定性行为:
# 工作区规则(供 agent runtime 注入)
## 1. 绝对边界(CRITICAL)
- 禁止写入:content/、src/ 以外的任何目录
- 禁止读取:.env、secrets/、~/.ssh 等敏感路径
- 禁止联网:除非任务明确允许(并记录 URL 与检索日期)
## 2. 工程门禁(必须执行)
- 每次工具调用必须有:超时、重试上限、幂等 key、审计字段
- 任何写操作必须走 patch 提交:禁止直接在工作区自由写文件
## 3. 架构约束(可被检查)
- feature 必须在 src/features/<name> 下
- 跨模块只能导入对方 index.ts(禁止深层导入)
## 4. 约定与风格(可机械化)
- 注释必须中文
- 单文件单类;类 < 500 行;方法 < 50 行
注意这里的关键词:超时、重试、幂等、权限、隔离、审计、观测。规则文件必须把这些“事故防线”写进去,否则你只能在事故发生后补锅。
2) 目录级层叠(Cascading Rules):规则需要可组合
大型仓库往往是多语言、多模块共存。你需要明确“规则的有效范围”,常见做法是:
- 根目录规则:全局底线(安全、边界、审计、最小权限)。
- 子目录规则:局部覆盖(语言栈、测试命令、模块边界)。
- 叠加语义:越靠近目标文件的规则优先级越高,但不能突破根目录的绝对边界。
把它做成一个确定性的算法,而不是“写几段文字希望模型理解”。
3) 规则发现器:把 effective rules 变成审计证据
下面的伪代码强调两个点:
- 规则发现是确定性的(可复现)。
- effective rules 必须被记录(可审计、可回放)。
import os
from dataclasses import dataclass
@dataclass(frozen=True)
class EffectiveRules:
# 规则内容(合并后的最终文本)
text: str
# 规则来源路径(用于审计)
sources: list[str]
class RuleResolver:
"""
规则解析器:
从目标文件向上寻找规则文件,形成“有效规则集合”,并记录来源路径作为审计证据。
"""
def __init__(self, *, rule_filenames: list[str]):
self._rule_filenames = rule_filenames
def resolve(self, *, target_file_path: str, workspace_root: str) -> EffectiveRules:
cur = os.path.dirname(os.path.abspath(target_file_path))
root = os.path.abspath(workspace_root)
pieces: list[str] = []
sources: list[str] = []
while True:
for name in self._rule_filenames:
p = os.path.join(cur, name)
if os.path.exists(p):
with open(p, "r", encoding="utf-8") as f:
pieces.append(f.read())
sources.append(p)
if cur == root:
break
parent = os.path.dirname(cur)
if parent == cur:
break
cur = parent
# 约定:越深的目录越具体,因此合并时放在更靠前的位置
pieces = list(reversed(pieces))
sources = list(reversed(sources))
return EffectiveRules(text="\n\n".join(pieces), sources=sources)
4) 注入点:规则不应“每轮全量塞入”
规则注入要配合 token budget:
- 稳定前缀:核心规则与工具 schema 尽量固定,利于缓存与稳定行为。
- 按需裁剪:只注入与当前任务相关的部分规则(例如只注入“跨模块导入约束”)。
- 记录装配计划:每轮把注入了哪些规则块写进 trace/span,否则无法复盘。
Design(设计取舍:为什么必须有“门禁层”)
规则文件主要管“意图”,门禁层管“行为”。这两者必须分离:
- 规则告诉你“应该怎么做”。
- 门禁保证你“只能这么做”。
例如,规则写了“禁止写入敏感目录”,但真正阻断写入的应该是:
- 路径白名单(权限)。
- 只读挂载(隔离)。
- patch 提交流(审计)。
- 超时/重试上限(防止死循环)。
这就是为什么同一套规则在不同 runtime 下表现差异很大:关键不是文字,而是执行层。
Pitfall(常见坑与防错)
- 规则太多太散: 一堆碎规则会造成注入成本高、注意力噪声大。
- 规则互相矛盾: 没有“绝对边界”与“覆盖优先级”,会导致行为漂移。
- 只写风格不写门禁: 最容易出事故的不是缩进,而是越权与副作用重复提交(幂等)。
- 不记录 effective rules: 出事后无法回答“当时到底遵循了哪套规则”。
- 规则不版本化: 临时改动规则会让线上行为不可追踪,难以回滚(审计)。
Debug(如何调规则系统)
把规则系统当作一个“可测模块”:
- 对每个目录准备一个小测试用例,断言 effective rules 的 sources 顺序与内容合并顺序。
- 在 trace/span 中记录 rule sources 与 hash(内容摘要),确保线上可回放。
- 当出现越界行为时,先查“effective rules 是否缺失”,再查“门禁是否执行”。
Source(资料来源)
- JetBrains instruction files: https://www.jetbrains.com/help/ai-assistant/configure-agent-behavior.html
- AGENTS.md/规则载体实践讨论: https://particula.tech/blog/agents-md-ai-coding-agent-configuration
- 规则载体对比: https://www.chaitanyaprabuddha.com/blog/agents-md-vs-claude-md-vs-cursor-rules
- 工程化规则生成案例: https://github.com/naravid19/ai-project-rules-generator
规则清单模板(把“可执行约束”写完整)
下面给一个更贴近生产的清单模板。它的目标不是变长,而是把“事故相关的规则”补齐,避免只写风格约束:
- 工具约束:
- 工具白名单(允许哪些 tool)
- 每个 tool 的超时与重试上限(超时、重试)
- 是否需要幂等 key(幂等)
- 读写边界:
- 允许读的路径白名单(权限)
- 允许写的路径白名单(权限)
- 强制只读挂载与输出目录(隔离)
- 网络边界:
- 允许访问的域名/端口白名单(权限)
- 默认拒绝策略(隔离)
- 审计与观测:
- 每次 tool call 必须记录:tool 名、参数摘要、trace_id、超时、重试次数(审计、观测)
- 每次写操作必须产出:patch_id / commit_id / WAL 记录(审计)
- 失败与降级:
- 触发降级的条件(降级)
- 触发回滚/补偿的条件(回滚)
- 触发人工介入的条件(审计)
把这个清单写进规则文件后,注入引擎才有机会“把规则落到执行链上”。
一个反例(说明为什么只写风格是无效的)
下面是一个常见但几乎没用的规则文件(反例):
- “请写清晰的代码”
- “请加注释”
- “请遵循架构”
它的问题是:没有范围、没有门禁、没有可检查对象。发生越权写入、重试副作用、超时卡死时,这些规则提供不了任何阻断能力。
因此你在写规则时要不断问自己:这条规则能否被 runtime 机械执行?不能,就要补上门禁与范围。