让 Code Agent 读懂业务:把仓库知识做成可维护的 Skills

代码检索只能告诉 Agent“这里写了什么”。要让它处理复杂业务,还需要把术语、规则、代码入口、变更方法和验证手段组织成会随代码演进的知识系统。

Code Agent 改一个局部函数时通常表现不错。任务扩大到跨模块需求、遗留系统重构或业务规则调整,效果往往会突然下降:它能找到许多相关代码,也能解释每个函数,却不知道哪些分支仍在生效、某个字段为什么不能改、一次修改还要同步检查哪些地方。

这类失败经常被归结为模型能力不足,实际还有另一层原因。仓库给人的信息本来就不完整。代码保存了最终实现,却没有完整保存当时的业务背景、被放弃的方案和必须保持的约束。熟悉项目的开发者会用历史经验补齐这些空白,Agent 只能根据文件名、调用关系和局部注释猜测。

给它更大的上下文窗口能缓解一部分问题,不能替代准确的领域知识。把整个仓库和几十篇文档塞进 Context,得到的往往是一堆互相冲突的新旧信息。更值得建设的是一套仓库知识系统:它能完成路由和按需展开,也能映射回代码,并随着代码变化持续更新。

一、代码为什么不足以表达业务

先看一个常见的判断:

if (request.isNewCustomer() && experimentService.hit(userId)) {
    applyIntroductoryPrice(order);
} else {
    applyStandardPrice(order);
}

代码能告诉我们条件和动作,却回答不了下面的问题:

  • “新用户”按注册时间、首次下单还是首次支付定义?
  • 实验关闭后,这条分支是保留兜底还是可以删除?
  • 优惠价格能否和其他券叠加?
  • 价格结果是否还会被下游结算服务二次修正?
  • 调整逻辑后需要回归哪些渠道和历史订单?

这些信息可能散落在产品文档、评审记录、数据库字段注释和某位开发者的记忆里。代码搜索能找到 applyIntroductoryPrice,找不到团队当初为什么决定“首单券不能叠加”。调用图可以追踪程序怎么走,也不会自动区分活跃链路、迁移中的旧链路和仅用于数据回放的旁路。

大型遗留项目还会放大两个问题。

第一,代码量和有效信息量并不成正比。一个仓库可能有大量历史适配层、已经停流的业务分支和自动生成代码,日常修改却集中在少数路径上。Agent 如果平均对待所有文件,检索结果很容易被旧实现淹没。

第二,业务边界和代码目录很少完全重合。一个“修改优惠资格”的需求,可能同时经过 API、规则引擎、缓存、订单快照和结算校验。只按目录写文档,会把同一条业务链切碎;只按业务写文档,又容易失去与代码的对应关系。

所以,领域知识不能只是业务说明书。它至少要回答三个问题:这条规则是什么意思,它落在哪些代码节点,修改后怎样证明没有破坏其他约束。

二、从“多给一点上下文”转向知识路由

最直观的做法是让 Agent 接入团队文档库,根据需求自行搜索。这个方案适合查找明确事实,用在持续开发上会遇到几种麻烦:

  1. 文档新旧混杂,搜索相关性不等于事实有效性。
  2. 长文档通常按人类阅读习惯编排,Agent 为了找到一个规则需要读取大量背景。
  3. 文档描述业务,代码表达实现,两者之间缺少明确锚点。
  4. 每次任务重新检索,结果不稳定,也很难复盘 Agent 到底依据了哪一版知识。

Skills 更像仓库内的专家手册。系统先暴露很短的名称和触发描述,任务命中后再读取主文件,遇到具体问题才继续打开引用资料。信息按使用时机分层,Agent 不需要在任务开始时吞下整个知识库。

一个简单的加载路径可以写成:

任务输入
  ↓
全局索引:判断属于哪个业务模块
  ↓
模块 Skill:读取规则、流程和代码地图
  ↓
引用资料:按当前问题加载接口、案例或详细流程
  ↓
实际代码:确认当前实现并执行修改
  ↓
测试与检查:验证结果,同时反查知识是否过期

变化发生在知识获取方式上。过去先搜索一堆可能相关的资料,现在先路由到可信模块,再按问题展开细节。

三、仓库知识可以分成四层

我更倾向于把这套系统拆成四层,让 Skill 与仓库入口、引用资料和自动检查互相连接。

1. 仓库入口:告诉 Agent 去哪里找

入口文件可以是 AGENTS.md,也可以是当前工具约定的项目指令文件。它只保留全局信息:

  • 项目负责什么,主要模块如何划分;
  • 构建、测试和本地运行命令;
  • 全局术语以及业务名和代码名之间的对应关系;
  • 不能违反的仓库约束;
  • 每类需求应该读取哪个 Skill。

入口文件不适合塞入完整业务流程。它在许多任务中都会被读取,过长会持续占用 Context。它更像目录和交通规则,负责把 Agent 送到正确的知识模块。

全局工程规则与仓库知识也应分开。比如“修改行为必须补测试”可以跨项目复用;“退款单必须保留原计价版本”只属于当前业务。前者适合公共 Rule,后者应该进入仓库内的领域 Skill。

2. 模块 Skill:支持 Agent 制定修改方案

一个模块的主文件应该让 Agent 在读完后具备制定方案所需的最小知识。推荐包含六部分:

  1. 触发范围:哪些任务需要读取它,哪些相似任务属于其他模块。
  2. 领域词汇:业务术语、代码命名和数据字段如何对应。
  3. 业务不变量:无论需求怎样变化都不能破坏的约束。
  4. 主流程:从入口到落库或出参的关键节点。
  5. 代码地图:节点对应的目录、类、函数、配置或表。
  6. 变更手册:常见修改方式、影响面和验证命令。

这里要控制一个尺度。主文件太短,Agent 仍需要盲目搜索;主文件太长,又会退化成另一份大文档。判断标准不是固定字数,而是读完后能否画出主流程,并知道下一步应该打开哪些文件。

3. 引用资料:只在需要时展开

API 定义、状态机全表、历史迁移说明、复杂案例和故障记录可以放进 references/。主文件只保留链接与读取条件,例如:

## 退款重算

修改退款金额、计价版本或优惠回收逻辑时,先阅读:

- `references/refund-recalculation.md`
- `references/pricing-snapshot-schema.md`

只改退款通知文案时不需要加载上述文件。

“什么时候读”比“这里有一篇资料”更有用。没有读取条件,Agent 容易走向两个极端:漏掉关键文件,或者把所有引用全部打开。

4. 可执行验证:把知识变成检查

纯文本规则只能提醒 Agent。能够编码的约束最好继续下沉到测试、静态检查和脚本里。

例如 Skill 写着“每个价格快照必须包含规则版本”,仓库可以同时提供:

./gradlew test --tests '*PricingSnapshot*'
./scripts/check-pricing-version-field.sh

Agent 修改完成后执行命令,比让它重新阅读规则并自我确认更可靠。随着规则逐渐稳定,知识系统还应把其中一部分转成机器可以判定的证据。

四、一份可以直接使用的 Skill 模板

假设仓库里有一个计价模块,可以采用下面的目录:

.agents/
└── skills/
    └── pricing/
        ├── SKILL.md
        └── references/
            ├── calculation-flow.md
            ├── terminology.md
            ├── pricing-snapshot.md
            └── change-cases.md

SKILL.md 可以从这份骨架开始:

---
name: pricing-domain
description: 处理计价、优惠叠加、价格快照、退款重算和计价规则版本变更时使用。
---

# 计价领域

## 何时使用

- 修改价格计算、优惠资格或叠加顺序
- 新增计价因子、计价版本或价格快照字段
- 排查下单价、支付价与退款金额不一致

纯展示文案、报表口径和活动素材不属于本 Skill。

## 业务不变量

- 已支付订单必须能按创建时的规则版本重放
- 退款以订单价格快照为准,不读取当前活动配置
- 所有金额使用最小货币单位的整数表示

## 主流程与代码锚点

1. 请求归一化
   - `PricingRequestAssembler#assemble`
2. 资格判断
   - `EligibilityService#evaluate`
3. 规则计算
   - `PricingEngine#calculate`
4. 快照持久化
   - `PricingSnapshotRepository#save`

## 常见变更

### 新增优惠类型

1. 增加类型定义与反序列化映射
2. 明确和现有优惠的互斥或叠加顺序
3. 将计算明细写入价格快照
4. 补充下单、重放和退款测试

## 修改后验证

运行:

`./gradlew test --tests '*Pricing*'`

检查:

- 历史规则版本能否反序列化
- 价格快照字段是否完整
- 退款重放结果是否与原订单一致

这份模板没有追求把业务讲完。它先把入口、不变量、代码位置和验收方式钉在一起,让 Agent 可以据此展开调查。真实项目再把复杂流程移入引用文件,避免主文件不断膨胀。

五、知识与代码之间必须有显式映射

“首单优惠不能和渠道券叠加”是一条领域知识。如果 Skill 只写到这里,Agent 还需要自己搜索“首单”“渠道券”“叠加”等词,再猜测哪段实现真正生效。命名稍有差异,映射就可能失败。

更可用的写法会继续指出:

  • 规则入口是 DiscountStackingPolicy#resolve;
  • 首单优惠在代码中使用枚举 INTRO_OFFER;
  • 渠道券来自 ChannelBenefitAdapter;
  • 冲突时保留用户收益更高的一项;
  • 结果同时写入订单快照和计价明细;
  • 回归测试位于 DiscountStackingPolicyTest。

这样做看起来像增加维护成本,实际上减少了每次任务的重复推理。人类开发者也从中受益。接手模块的人不必沿着十几层调用逐一猜测,先读代码地图便能找到需要验证的节点。

代码锚点不应写得过细。行号最容易失效,私有函数也可能在重构中频繁改名。优先选择稳定的模块、公开类型、接口、路由、事件名和测试套件。细节放在 Agent 本次读取的代码中确认,Skill 保存相对稳定的导航信息。

我会把知识分成三种稳定度:

类型 示例 推荐维护方式
业务不变量 已支付订单按原规则退款 写入 Skill,并尽量转成测试
流程与边界 计价经过资格、计算、快照三个阶段 写入主文件和代码地图
实现细节 某个私有函数和具体行号 让 Agent 读取当前代码,不长期抄入 Skill

稳定度越低的内容,越不适合手工复制进知识文件。否则文档会很详细,也会很快失真。

六、知识腐化要当作工程故障处理

知识系统上线后,最大的风险是它看起来仍然完整,内容却已经过时。没有文档时 Agent 会谨慎搜索;错误文档会给出一种假的确定性,让它沿着错误入口快速修改更多文件。

防腐不能依靠“大家记得顺手更新”。更新动作要嵌入开发流程。

1. 使用时反查

Agent 读取 Skill 后仍要查看实际代码。两者出现冲突时,任务不能悄悄选择其中一边继续,而应记录:

  • 冲突的是业务规则、代码位置还是接口定义;
  • 当前代码的证据是什么;
  • 代码实现错误,还是知识已经落后;
  • 建议更新哪个文件。

人工确认后再修改知识。这个环节能逐步清理存量偏差,因为真实需求会不断经过最常使用的模块。

2. 从讨论中提取新增知识

复杂需求中,开发者往往会补充一句关键背景:“这个字段为空不是异常,它表示沿用订单创建时的版本。”如果这句话只留在对话里,下一个任务还要再讲一次。

任务结束时可以让 Agent 列出本轮新增的领域事实,并逐条判断去向:

  • 长期有效的规则进入 Skill;
  • 详细案例进入引用资料;
  • 可以自动判断的规则进入测试或 Linter;
  • 只对当前任务有效的信息留在任务记录中。

不要让 Agent 不经确认直接把整段聊天追加到知识库。聊天里包含假设、临时方案和已经被推翻的讨论,原样沉淀只会制造新噪声。

3. 在提交前做知识影响检查

提交前根据 diff 判断是否触发知识更新。例如:

变更命中领域入口、公共接口、路由或数据模型
  ↓
查找引用这些锚点的 Skill
  ↓
比较代码变化与知识描述
  ↓
输出“无需更新”或具体修改建议
  ↓
测试与人工 Review

一开始不必做复杂平台。脚本可以先用 git diff --name-only 找到模块,再让 Agent 检查关联 Skill。检查应该发生在代码合并前,拖到几周后集中补文档通常只会留下更多遗漏。

4. 定期做全量巡检

增量检查会漏掉规则没覆盖的变化。定期巡检可以处理四类问题:

  • Skill 中引用的类型、文件或命令已经不存在;
  • 接口、事件和数据结构与引用资料不一致;
  • 新模块长期没有知识覆盖;
  • 多个 Skill 对同一术语给出了不同定义。

全量巡检成本高,频率可以低一些。它适合作为版本发布或固定周期任务,而不是每次提交都扫描整个仓库。

七、不要只统计写了多少个 Skill

Skill 数量和文档字数很容易增长,却不能说明 Agent 是否真的变得更可靠。更有意义的指标包括:

路由命中率

给定一批真实需求,Agent 是否能选择正确的模块 Skill。路由错误意味着后面的内容再准确也没有用。可以保留一组去敏后的需求标题作为固定测试集。

知识有效率

Agent 读取的知识里,有多少内容对本次方案和修改产生了作用。如果每次都打开十篇引用资料,最后只使用一条规则,说明主文件的读取条件仍然太宽。

锚点存活率

定期检查代码地图中的文件、类型、路由和测试命令是否存在。它不能判断语义完全正确,但能用较低成本发现明显腐化。

返工与人工补充次数

记录一次任务中人工补充了多少次背景、Agent 因误解业务返工多少次。这个指标比“生成了多少行代码”更接近知识系统的目标。

验证闭环率

Skill 里声明的关键规则有多少对应自动化测试或检查。只能靠自然语言提醒的规则仍然脆弱,闭环率提高意味着知识逐渐转成可执行约束。

评估时最好使用真实任务的脱敏版本,而不是专门为 Skill 编写的演示题。后者容易把触发词和答案写在同一个句子里,测到的是模板匹配,不是复杂仓库中的实际帮助。

八、Skills 与 SDD 不需要二选一

Spec-Driven Development 为单次变更描述目标、方案、接口和验收标准。领域 Skill 保存跨需求复用的术语、规则、代码地图与变更方法。两者解决的时间尺度不同。

对一个持续维护多年的系统,如果每个小需求都生成比代码还长的 Spec,维护成本确实会失控。反过来,只靠长期知识也无法表达本次需求独有的取舍。例如“这次迁移分两期,第一期只覆盖新订单”就不应该写成永久规则。

更实用的组合是:

  • 小改动读取领域 Skill,直接形成简短计划并实施;
  • 涉及多模块、数据迁移或外部协议的变更,额外编写本次 Spec;
  • Spec 完成后,把其中长期有效的结论提炼进 Skill;
  • 临时范围、排期和一次性兼容方案留在 Spec,不污染领域知识。

新项目也更适合从 Spec 起步。业务边界尚未稳定时,过早把大量假设写进长期 Skill,几轮迭代后就会迅速过期。等核心概念和主流程稳定,再把反复出现的部分沉淀下来。

九、从一个真实模块开始

一次性为整个仓库补齐知识通常会失败。编写者花费数周整理,等第一版完成时,前面的模块已经变化,而且大量内容没有经过真实需求验证。

更稳妥的推进方式是:

  1. 选择近期会持续迭代、边界相对清楚的模块。
  2. 收集最近几次需求中反复解释的背景和常见返工点。
  3. 写出最小 Skill,只覆盖术语、不变量、主流程、代码锚点和验证。
  4. 用下一次真实需求检验路由和内容,不够再补。
  5. 建立提交前检查后,再扩展到第二个模块。

第一版不需要追求百科全书。一个能把 Agent 稳定带到正确入口、避免破坏两三个关键约束的 Skill,已经比一篇没人维护的完整业务文档更有用。

团队还要明确所有权。业务规则由谁确认,代码地图由谁更新,冲突由谁裁决,都应有默认负责人。Agent 可以发现差异、生成建议和完成机械修改,无法替团队决定哪一条业务定义才是最终事实。

十、我的几个判断

做完这套设计后,我觉得有几个边界尤其值得保留。

第一,Skills 不应成为新的文档仓库。它的价值是把完成任务所需的最小知识送到恰当的位置。原始会议记录、完整产品方案和大量历史背景仍可以留在原来的资料系统中。

第二,检索与知识工程并不冲突。Skill 提供可信骨架和读取路径,搜索负责处理没有固化的新信息。只依赖搜索,结果容易漂移;完全拒绝搜索,知识体系又会封闭和滞后。

第三,能够长期依赖的知识最终会进入程序。规则先以文字出现,稳定后转成类型、约束、测试、静态检查或运行时监控。Skill 负责解释它们为什么存在、在哪执行以及变更时要看什么。

最后,模型升级不会消除仓库知识问题。更强的模型可以更好地推断陌生代码,但推断仍然无法恢复没有写下来的业务决策。领域知识的准确度、代码与知识之间的映射、修改后的验证证据,决定了 Code Agent 在复杂项目里能走多远。

参考阅读