如何让 AI Agent 按需读取项目知识:Flow2Spec 的路由设计实践

当 AI 编程工具真正进入生产项目后,常会出现一种看似矛盾的现象:模型明明能完成复杂代码实现,却仍会反复追问项目中的基础事实。
比如,某个接口是否支持重试、批处理任务应该使用什么幂等键、某个状态字段究竟由哪个模块维护。这些信息通常早已存在于代码或文档中,但新会话并不知道历史设计决策,也无法理解上一次为何这样实现,于是只能重新检索仓库。
最直接的做法,是把所有规则集中写进 AGENTS.md 或 CLAUDE.md。在项目规模较小时,这种方式简单且高效;但随着项目持续演进,单文件会越来越臃肿。Agent 每次都要加载大量无关信息,不仅浪费上下文窗口,还可能遗漏真正关键的约束条件。
我们在设计 Flow2Spec 时,把问题重新定义为:
本文不会展开介绍全部能力,而是聚焦这套项目知识库路由为何这样设计,以及在多人协作修改知识时,如何处理业务语义冲突。
先明确三个设计目标
在正式实现之前,我们先为知识层定义了三个核心目标。
第一,Agent 不应该为了处理一个支付需求,就读取整份项目文档。它需要一个低成本的知识入口,优先缩小候选范围。
第二,命中某一条业务规则,并不意味着上下文已经完整。支付主题很可能还依赖账户风控、订单边界和统一错误码,因此这些依赖关系必须被显式表达出来。
第三,知识会随着代码演进不断变化。知识层必须能够进入 Git diff 和 Code Review 流程,而不能沦为一个无法追踪来源的外部黑盒。
基于这些约束,我们最终采用了仓库内的分层结构:
.Knowledge/├── manifest-routing.json # L0:路由索引├── matchers/ # L1:关键词分片├── topics/ # L2:主题摘要与硬约束├── stock-docs/ # L3:稳定的架构和能力文档└── req-docs/ # L3:具体需求与技术方案
这里真正重要的并不是目录名称,而是每一层承担的职责明确不同。
| 层级 | 保存内容 | Agent 的读取方式 |
|---|---|---|
| L0 路由索引 | task、topic 路径、依赖和元数据 | 会话优先读取机读入口 |
| L1 匹配分片 | 一组与任务相关的触发词 | 只打开可能命中的分片 |
| L2 主题摘要 | 边界、硬约束和下钻入口 | 命中后读取并展开依赖 |
| L3 长文档 | 架构终稿、需求方案和完整背景 | 信息不足时按需读取 |
这样设计的目的,是让大多数常见问题尽量在 L0 到 L2 就能解决,只有在信息存在缺口时,才继续读取长文档或源码。
路由入口为什么要做成 manifest
manifest-routing.json 是知识层的机读入口。下面是一段经过简化的结构:
{"topicPaths": {"flow2spec-collaboration": ".Knowledge/topics/flow2spec-collaboration.md","f2s-task": ".Knowledge/topics/f2s-task.md"},"topicDependencies": {"flow2spec-collaboration": ["f2s-task"]},"taskToTopicRules": [{"task": "flow2spec-collaboration","matcherId": "m-flow2spec-collaboration","matcherPath": ".Knowledge/matchers/m-flow2spec-collaboration.json","topics": ["flow2spec-collaboration"]}]}
这段配置本质上梳理了三类关键关系:
taskToTopicRules负责从任务定位 matcher 和候选 topic;topicPaths负责从 topic id 映射到实际文件;topicDependencies负责声明命中主题前需要补齐哪些前置主题。
路由索引本身只保存关系,而不会把所有关键词直接塞进一个巨大的 JSON 文件。关键词被拆分到独立 matcher 中:
{"id": "m-flow2spec-collaboration","schema": "flow2spec.matcher.v1","includeAny": ["团队协作","多人共用知识库","developerId 隔离","kb-delta","topic revision","revision 冲突","optimistic lock"]}
分片的好处在于,局部规则调整时,其他路由文件不会产生无意义的 diff。与此同时,Agent 也无需一次性读取所有关键词,只要通过 matcherPath 打开相关分片即可。
这并不是在替代语义检索,而是提供一种更偏确定性的仓库内协议。它牺牲了一部分模糊召回能力,换来了可读性、可审查性和可预测的知识路由结果。对于权限控制、接口幂等、数据边界这类不能依赖“相似度大致命中”的项目约束,这种取舍更合理。
Match 之后不能直接 Act
仅仅命中一个 topic,往往很容易制造出虚假的确定性。
假设用户提出这样的问题:“两个人同时更新同一个业务主题时,知识库会不会把内容直接覆盖掉?”matcher 确实可以命中协作主题,但在真正回答之前,还必须进一步确认:
- 当前 topic 是否覆盖并发写入场景;
- 是否需要先读取任务状态的所有权规则;
- 用户问的是 Git 文本冲突,还是业务语义冲突;
- topic 中的描述是否仍与当前实现保持一致。
因此,完整的知识检索流水线被拆成四步:
match → expand → verify → act
Match:缩小候选范围
根据用户任务读取对应 matcher,得到主候选 topic,而不是遍历全部知识文件。
Expand:展开依赖
读取主 topic 声明的依赖。例如协作主题依赖任务主题,因为在知识合并前,必须先知道当前开发者的 TASK_ROOT 位于哪里。
Verify:检查缺口
判断现有项目知识是否真的足以覆盖当前问题。如果覆盖不足,就继续读取 stock-docs/、req-docs/ 或源码;如果需求本身不清楚,则先向用户确认。
Act:执行任务
只有在依赖完整、关键事实已确认之后,才进入回答、修改代码或提交变更的阶段。
verify 是这条链路里最容易被跳过、同时也是最关键的一步。检索解决的是“哪些内容可能相关”,而缺口检查真正解决的是“当前信息是否足以执行”。
Topic 为什么只保存精简事实
topic 的目标并不是复制完整文档,而是为 Agent 提供最常用的硬约束、业务边界和继续下钻的入口。一个真实 topic 的 frontmatter 类似这样:
id: flow2spec-collaborationrevision: 0summary: 任务状态本地隔离、共享知识 delta 合入与 revision 冲突处理dependsOn: [f2s-task]primary: featureconfidence: manualtags: [policy]
正文部分只保留协作边界、知识合入规则、团队观察面以及长文档路径。这样一来,topic 足够简短,可以在同一次任务中灵活组合多个主题;而完整解释仍保留在长文档中。
这里还有一个很容易踩坑的点:摘要不能写成宣传文案。类似“强大、智能、高效”这类表述,对知识路由几乎没有帮助。真正有效的摘要,应该直接描述事实、适用范围与限制条件。
开发完成后,知识怎样写回来
只读项目知识还远远不够。Agent 在实现过程中,经常会从源码里确认新的业务限制,比如退款必须原路返回,或者某个锁的 TTL 为 10 分钟。如果这些事实只停留在当前会话里,下次遇到类似问题时,仍然需要重新搜索。
直接让 Agent 修改 topic 虽然看起来简单,但在多人协作开发中会立刻暴露两个问题:
- Git 只能看到文本变化,却无法理解这次修改的真实意图;
- 两段文本即使可以自动合并,也不代表两条业务规则在语义上彼此兼容。
因此,知识变更会先写成结构化 kb-delta.json:
{"taskId": "add-payment-rule","developerId": "alice","baseRevisions": {"payment-rules": 3},"changes": [{"type": "appendBody","targetTopic": "payment-rules","summary": "补充退款时限","content": "## 退款时限nn审核通过后 3 个工作日内原路退回。"}]}
delta 目前只允许四种动作:
| 类型 | 含义 |
|---|---|
appendBody | 在已有 topic 末尾追加内容 |
replaceBody | 替换 topic 正文 |
updateFrontmatter | 更新 topic 元数据 |
createTopic | 创建 topic,并按需建立 matcher 和路由 |
动作白名单让 CLI 能在落盘前校验目标、字段和版本,同时也让 Code Review 更容易看懂这次知识变更到底想表达什么。
用 topic revision 阻止过期写入
baseRevisions 记录的是生成 delta 时所看到的 topic 版本。执行 plan 时,CLI 会将它与磁盘上的 revision 进行比较:
baseRevision == diskRevision→ 才允许 apply,写入完成后 revision +1baseRevision != diskRevision→ 直接停止,要求先重新读取最新内容
举一个非常直观的例子,Alice 和 Bob 都基于 payment-rules revision: 3 开始修改。结果 Alice 先一步合入,磁盘中的版本随即变成 4。此时,Bob 的 delta 在继续执行 plan 时,就会收到 revision mismatch,而不会被继续放行写入。
这时 Bob 必须先阅读 revision 4 的正文,再判断两份规则应该并列保留、重新改写,还是放弃其中一份。这里故意不做自动文本拼接,因为“两个段落都能插入进去”和“两个业务结论彼此不冲突”完全不是一回事。
revision 是磁盘层面的乐观锁,而不是远程分布式锁。它的边界非常明确:
- 它只能保护通过 delta 通道提交的知识变更;
- 它无法感知队友尚未拉取的远端提交;
- 直接手工修改 topic 会绕过 revision 预检;
- 正常的 Git pull、分支同步和 Code Review 依然不可省略。
这个机制并没有消灭冲突,而是尽量把冲突提前到 plan 阶段,并把语义裁决保留给真正理解业务上下文的人。
任务状态和项目知识为什么要分开
当多人同时使用 Agent 时,仓库里通常会并存两类状态:
- “这轮会话进行到哪一步”属于个人过程状态;
- “系统当前有哪些业务约束”属于团队共享事实。
如果把两类信息都提交到 Git,个人 checklist、临时判断和待办事项就会频繁产生冲突。反过来,如果两类信息都只放在本地,那么已经验证过的项目知识又无法被团队共享复用。
我们采用的边界是:
.task/
.task/ 保存 checklist、会话上下文和本轮 delta,用于跨会话续作;.Knowledge/ 只接收已经确认的事实。团队进度仍通过 PR、commit、issue 和里程碑来观察,而不会把个人 Agent 会话同步成另一套项目管理系统。
一次最小初始化实测
为了验证这套结构不是只停留在文档层面,我在一个空 Git 仓库中执行了 Codex 初始化:
npx @double-coding/flow2spec@latest init codex --locale zh-CN --yesnpx @double-coding/flow2spec@latest doctor
初始化后,系统生成了 .Knowledge/、.codex/、根目录下的 AGENTS.md 和 flow2spec.config.json,同时还将 .task/ 自动加入 .gitignore。
doctor 的检查结果为 8 项通过、0 个警告、0 个错误,覆盖以下内容:
- Node.js 版本;
- 项目配置;
- Agent 入口;
- 知识库入口;
- Codex 配置完整性;
- developerId 和
TASK_ROOT; .task/忽略规则;- topic 校验与 routing 漂移。
不过,这只能说明初始化链路和基础结构本身工作正常,并不能直接证明知识路由质量足够好。路由是否真正有效,依然取决于团队是否把 topic 写成明确事实、matcher 是否覆盖真实搜索表达,以及代码变化后是否及时同步项目知识。
实际使用中的几个限制
这套方案并不是没有维护成本。
首先,matcher 使用的是显式触发词,优点是结果容易解释,但对于团队从未预料过的表达方式,召回能力会相对有限。必要时,仍然需要依赖普通代码搜索或其他检索手段兜底。
其次,topic 越大,并发修改时 revision 冲突面就越大;但如果 topic 拆得过细,又会导致依赖关系变得复杂。相对可行的标准,是让一个 topic 围绕一组稳定、可独立判断的业务约束,而不是按文件数量机械拆分。
再次,知识正确性最终仍然来自代码、测试和人工确认。confidence、revision 和校验命令只能帮助管理知识内容,不能把未经验证的推断直接变成事实。
最后,小型项目未必需要引入这套结构。对于一次性脚本或只有少量文件的个人项目,一份简洁的规则文件通常已经足够直接。只有当重复搜索、上下文漂移和多人知识冲突开始显著增加成本时,分层知识路由才值得长期维护。
总结
让 AI 真正理解项目,并不等于把更多文本不断塞进上下文窗口。更关键的问题在于:如何快速定位相关事实、如何补齐依赖关系、如何判断当前信息是否足够,以及当事实变化后如何安全地写回知识库。
Flow2Spec 当前给出的答案,可以概括为三点:
- 用 manifest、matcher、topic 和长文档组成渐进式知识层;
- 用
match → expand → verify → act把缺口检查放在真正执行之前; - 用结构化 delta 和 topic revision 管理多人协作下的知识变更。
这套设计仍需要在更多真实项目中持续验证,尤其是 matcher 的长期维护成本、topic 拆分粒度以及跨分支语义冲突处理。但至少有一点已经越来越清晰:项目上下文不能只被当作提示词,它更应该成为一种可版本控制、可审查、可持续维护的工程资产。
文中的完整实现和 schema 可以在 Flow2Spec 仓库 中查看。
