AI写得快 ≠ 真正提效:一文讲清Harness“记忆”和“验证闭环”
开发者公众号专属群聊
扫码加入获取更多一手教程、科技前沿报告
用 AI 写代码,"写得快"和"真正提效"是两件事。我踩的坑集中在三处:一是 AI 没有记忆,每次新会话都从零开始,同一个模块的链路、隐藏的命名规则、上次踩过的坑,它都要重新摸索一遍;二是 AI 改完就停,它把代码改对就认为任务结束,而真正的交付还要验证、提交、等上线、确认线上生效;三是 AI 会犯错,还会自作主张,它会挑"看起来对"的接口调用,会在长链路里忘记上下文,也会为了让结论好看而缩小验证范围。所以我做的不是"让 AI 更聪明",而是给它套上一套驾驭系统(Harness):有记忆、能动手、跑完闭环,并且在关键路口有人守着。
01
1.1 整体是怎么运转的:写入、读取、全自动
后面各小节都是这一节的展开,先给一张全景。
写入:知识怎么进知识库
知识只有一个来源:会话复盘——任务结束后,AI 自己回顾全过程,把"代码里没有、要反复探索才能拼出来"的知识写成文档,落到两级索引结构里。
写入时必须守三条硬规则:① 只写三类内容(阴性知识、代码位置索引、隐含关联关系),代码里已写明的一律不写;② 新主题落到对应模块目录下,并在该模块 agents2.md 追加一行索引,保证索引与目录一一对应;③ 一律"定点修改",禁止整篇覆盖——覆盖会抹掉 frontmatter 里的成熟度与引用统计。
读取:知识怎么被用上
固定顺序,只读两三篇:
读一级索引
agents.md(只列模块)→ 定位到所属模块;读该模块二级索引
<模块>/agents2.md(列出文档 + 一句话描述)→ 判断哪篇相关;读那篇实际文档;
都没覆盖到,才允许自己去探索代码。
这个顺序由一条 hook 强制:本会话没读过一级索引之前,读文件、搜索、执行命令、MCP 调用都会被拦下。也就是说,"先查知识库再动手"不是靠自觉,而是绕不过去。
全自动:不需要人工介入
写入、整理、聚合、索引同步、日志轮转,全部由 AI 与脚本在任务结束时自动完成。人不需要为知识库做任何额外动作——知识库自己长、自己淘汰。
1.2 为什么要建知识库
代码和文档能告诉 AI "这里有什么",告诉不了它"这些东西怎么串起来"——比如某个字段的真实语义和字面意思不一样、某个配置改了之后要连带改哪三处、系统 A 的动作实际会触发系统 B 的什么行为。这类知识没有任何静态引用能直接看出来,只能靠人反复探索拼出来;而拼出来之后如果只留在某一次会话里,下一个会话、下一个人又要重来。
所以知识库只记这一类连接性知识,具体三类:
阴性知识:代码里没写清、容易误判的事实;
代码位置索引:关键逻辑/配置/入口的具体文件路径;
隐含关联关系:不通过静态引用能看出来的跨系统连接。
代码里已经写明的业务逻辑、参数列表、目录结构,一律不记——那些 AI 自己读代码就知道。
1.3 知识库长什么样:两级索引
知识库是一个 Obsidian vault,通过 MCP 访问。结构是两级索引:
agents.md(只列模块)└─ <模块>/agents2.md(列出本模块所有文档 + 一句话功能描述)└─ <模块>/xxx.md(正文 + 头部 frontmatter)
AI 的固定动作是:读一级索引定位模块 → 读二级索引判断哪篇相关 → 读具体文档。即使知识库长到几百篇,也只读两三篇就能命中,不会一上来就全文检索。
每篇文档头部还有一段 frontmatter(元数据区),记这篇知识的成熟度、被引用次数、用过的场景、依赖的文件路径等。它不参与阅读——AI 读正文时看不到这段元数据,它是给治理机制(见 1.8)和 Obsidian 视图用的。
这带来两个工程上的约束:读整篇用"定向读"只取正文,避免把元数据一起拉进上下文;写入一律"定点修改"、禁止整篇覆盖——覆盖会把看不见的 frontmatter 一起抹掉,成熟度和引用统计就静默丢了。
1.4 让"先读知识库"变成硬约束
只写在提示词里,AI 遵守是概率事件。所以这里用了三种机制,先说明它们分别是什么:
hook(钩子):在工具调用前后自动触发的脚本,能拦下这次调用。它不是提示词,AI 绕不过去。(使用内部 CodeBuddy 实现);
rule(规则):写在项目里的一段约束文本,每次会话自动注入给 AI。表达意图方便,但属于"软约束",AI 可能不遵守;
command(斜杠指令):用
/xxx拉起的一段固定流程,本质是一份写死的任务说明。
三者配合起来:
hook 层强制:一个 PreToolUse hook 挂在所有探索类工具上(读文件、搜索、执行命令、MCP 调用)。当前会话没读过一级索引时,这些调用一律被拦下,并在阻断消息里告诉 AI 该先去读
agents.md。rule 层引导:同时保留一份规则文本,两层并存——hook 是硬拦截,规则是软引导。
工具链也从知识库同步:command、rule、hook 脚本的源都放在 vault 的
toolchain/,每次会话开始时由 SessionStart 脚本自动同步到工作区。也就是说,知识库不只是"被 AI 读的资料",它同时是工具链的发布源——改一条规则,下次会话就生效。
一个细节:hook 是同步阻塞的(客户端要等它返回),所以重量级同步放 SessionStart(每会话一次),PreToolUse 上只留轻量判定,否则每次工具调用都要付一遍启动开销。
hook 示例(kb-first-read.py 精简):hook 从 stdin 拿到工具名与入参,未读一级索引时对探索类工具返回 exit 2 阻断。
GUARDED_TOOLS = {"Read", "Grep", "Glob", "Bash", "mcp_call_tool", "mcp_get_tool_description"}BLOCK_MESSAGE = ("动手前必须先读知识库一级索引:调用 obsidian MCP 的 vault_read,"'参数 path="agents.md",读完再执行其他操作。\n')>def main():......
注册在 settings.local.json:PreToolUse + matcher: "*",命令为 /usr/bin/python3 -S .codebuddy/hooks/kb-first-read.py。
rule 示例(read-agents-md-first.mdc,节选):
---description: 动手前先读知识库一级索引:两级索引导航入口、Obsidian MCP 读取方式与工具选择、MCP 不可用时的降级要求alwaysApply: true---># 规则:动手前先读知识库一级索引 `agents.md`(导航索引是唯一正确入口)>## 二、正确入口(固定顺序,两级索引)1. 先读**一级索引** `agents.md`(只列模块)→ 定位到所属模块2. 再读该模块的**二级索引** `<模块>/agents2.md`(列出本模块目录下所有文档及功能描述)→ 命中主题3. 读对应的实际文档 `<模块>/xxx.md`4. 文档没覆盖的,才允许自己动手探索 / 跑脚本验证
1.5 写入规范:记什么、不记什么
一条知识值不值得写,用一句话判断:脱离"这次任务"的上下文单独拿出来看,是否依然成立、依然有用。
按这条标准,以下内容不写:过程性元信息(日期、执行者、变更记录)、范围声明式表述("本次改动范围内")、给不出验证方法的猜测(只能进文档末尾的"待验证"小节)、过度取证细节(具体日志条数、耗时数字)。
这套规范不是我事后总结的,而是直接写进了驱动复盘的规则里,AI 每次按它筛:
规范原文(retro-knowledge-on-session-end.mdc,节选;同一份规则的“执行动作”部分见 1.7):
只筛出三类、且现有文档尚未记录的内容:
1、阴性知识——代码和现有文档里没有写清楚、需要反复探索才能拼出来的信息
2、代码位置索引——关键逻辑/配置/入口的具体文件路径
3、隐含关联关系——代码之间(跨代码库或同代码库内)不通过静态引用能直接看出来的连接关系
1.6 自动化知识整理
知识库如果不能自动生长,就会变成又一个需要人工维护的文档站。整理由两条指令驱动,一轻一重:
maturity-lint(高频轻任务):会话结束时自动跑一次。它只读新增的引用数据,不读全库正文、不做内容整理、不做衰减,做四件事——聚合弱/强信号 → 提升等级、累加计数 → 同步索引列 → 日志轮转。只提升,从不降级。 它已沉淀为脚本(.codebuddy/scripts/maturity-lint.py),指令本身只负责"前置探测 + 调脚本 + 转述报告"。cleanup-knowledge-base(低频重任务):周期性跑一次。它读全库,做三件事——整理内容(去重、消除歧义)、规范两级索引结构(索引与目录一一对应、补齐缺失元数据)、执行全库衰减扫描。它只整理已有内容,不探索新知识、不做需要代码验证的新增,因此不能拿它代替日常的maturity-lint。
两者是刻意的频率分工:便宜的机械动作高频跑,昂贵的判断性动作低频跑——把全库巡检塞进每天执行,成本会高到没人愿意跑。
1.7 自动复盘
复盘分两层,目的不同:
会话复盘(沉淀知识):任务闭环结束后,AI 回顾从排查到线上验证的全过程,把新产生的知识补进知识库。它解决的是"经验不沉淀"。
AI 执行复盘(反哺 skill):用另一个 AI 分析 AI 的会话记录,检查它是否遵守了排查流程、是否调用了正确的接口、结论是否合理、有没有漏步骤,输出"做对了什么 做错了什么 skill 哪里写得不清楚导致 AI 犯错"。它解决的是"AI 犯同样的错"——改的是 skill,不是骂 AI。
会话复盘由一条规则驱动:它声明"任务结束时必须做这件事",并写清记什么、落到哪、怎么报账。规则原文(节选):
规则示例(retro-knowledge-on-session-end.mdc,"执行动作";节选):
二、执行动作
读知识库:一级索引 agents.md → 模块二级索引 <模块>/agents2.md → 相关实际文档,理清现有主题范围、避免重复记录。
回顾本次会话从探索到实现的完整过程,只筛出三类、且现有文档尚未记录的内容……
落地(两级索引):模块已有主题 → 定点补充;无 → 新建文档,并在该模块 agents2.md 追加一行索引。
报告本次引用:列出本次实际采用的知识文档,并把清单追加到 <工作区>/.codebuddy/kb-refs.log。
随后执行增量聚合:按 maturity-lint 把本次引用并入知识库统计。
1.8 知识成熟度与引用追踪体系(新手可跳过)
这是让知识库能"自动淘汰"的关键——每篇文档的元数据里带着它的成熟度和引用记录:
四级成熟度:
draft<verified(被真实任务采用过)<proven(跨会话、跨场景被复用)<archived。提升只看强信号:被采用过 1 次 →
verified;被采用过 2 次以上、且场景数 ≥2 →proven。衰减按类型定周期(navigation 6 个月 decision 9 pitfall·process 12 / model 18),算法是幂等的——多跑几次不会加速衰减。
信号有两条通道:弱信号自动采集(hook 在放行"读知识文档"时记一行日志,说明谁读了哪篇;它只作诊断,不参与提升和衰减);强信号靠声明(任务结束时 AI 明确报出"本次实际采用了哪几篇、用在什么场景、结论是否被验证",只有这一路参与成熟度计算)。
这套体系怎么自动跑起来:
采信号:弱信号由 hook 自动写
kb-usage.log;强信号由会话结束时的复盘声明写kb-refs.log。聚合:
maturity-lint读这两个日志的新增段,按(session, path)去重后累加ref_count、对scenes取并集,据此重算等级。回写:脚本经 Obsidian 的本地 REST API 直接改 vault(与 MCP 是同一个服务),frontmatter 用定点 patch,不做整篇覆盖。
防重复计数:每个日志配一个检查点文件,记"已处理到哪个字节"并带该段前缀的哈希,下次只读新增部分;取舍是"宁可重复计数,不可丢数据",所以检查点写失败会在报告里明确指出。
同步索引列:等级变化后把新值写回对应模块
agents2.md的"成熟度"列,AI 下次读索引即可看到。轮转:日志超过 1 MB 自动归档(保留最近 3 份),检查点归零,避免校验成本无界增长。
也就是说,一篇知识从"被读过"到"被采用"再到"升级/降级",全程自动,不需要人工介入。
这套机制的价值是:知识库自己长、自己淘汰,不需要人工定期清理。 AI 的入口是索引表里的"成熟度"列——成熟度越高,越优先采信。
02
2.1 为什么要做闭环
AI 把代码改对了,任务走了不到一半。后面还有:本地能不能跑起来、测试环境验证是否通过、提交和 MR 门禁是否放行、改动什么时候真正上线、线上问题是否真的不再复现、单子该流转到哪个状态。任何一步断了,前面都白做。
所以闭环的目标很直接:让 AI 从"改完代码"一路走到"线上验证通过",中间不允许静默停下。
同时有一条原则:把运动员和裁判员分开。写代码的 AI 和审查、验证的 AI 是不同角色,不能既当运动员又当裁判员——这是抵抗惰性(AI 的,也是人的)最有效的办法。
2.2 能力底座:四个 skill 的组合
AI 要闭环,前提是"能动手"——它得知道代码跑在什么系统上,能读到系统里的信息,能触发系统里的动作。这靠的不是一个大而全的程序,而是一组 skill:每个 skill 是一份给 AI 的操作手册(SKILL.md + scripts + references),告诉它这类事情该怎么做、该调什么脚本。
四个 skill 各管一段,串起来才是完整闭环:
> 前提:所有验证一律在测试环境做,禁止在正式环境做任何验证动作。
平台访问 skill:让 AI 能操作你的系统
AI 要排查问题、要验证改动,前提是它能拿到系统里的信息、能触发系统里的动作。做法是把平台 API 封装成脚本,再用 skill 告诉 AI"这类问题该调哪个脚本":
脚本层:按系统分目录封装 API——查任务、拉日志、取产物、启流水线……人在多个平台之间来回切换登录的操作,脚本把它串成一条命令;
skill 层:每个 skill 只覆盖一类场景,写明触发条件、操作步骤、常见坑;
对外集成:同一批能力可以再封装成 MCP Server,供外部 AI 平台调用。
这部分可以整体替换:换成自己的 CI/CD、日志、工单系统,做法一样——先有脚本,再有 skill。
代码提交 skill:把提交规范固化成流程
提交这件事的难点不在 git 命令,而在规范:单号关联、门禁、分支口径、MR 流程,任何一步靠 AI 自由发挥都会出问题。所以把它固化成 skill,让 AI 按流程走:
关联需求单:commit 消息必须带单号(
--story=<短ID>/--bug=<短ID>);skill 里把"没拿到单号就不进入提交环节"写成硬前置,而不是建议;门禁自动解决:MR 起来后 QTA 等检查会给出结论。skill 把"等检查 → 读到失败项 → 定位原因 → 修复 → 重跑"这条链固化下来,AI 不需要人盯着就能把门禁过掉;
自动发 MR 与合入:按仓库约定走完整流程(建分支 同步目标分支 发 MR 等门禁 合入 / 切回),而不是停在 push;
自动留痕:每次发 MR 写一条审计记录(发起时间、合入时间、链接、仓库、关联单号),事后可查。
一条经验:对 AI 来说"提交代码"不等于 git commit,而是"提交 → 发 MR → 合入"的完整语义。这个语义必须在 skill 里写死,否则 AI 会在 commit 之后就停下来等你确认。
等待 skill:让 AI 能等到系统跑完(核心)
这是整个闭环里最关键的一块。
AI 的默认行为是"发起了动作就算完成"——它把流水线触发了、把任务启动了,就认为事情办完了,然后拿着旧产物去验证。没有等待能力,闭环就是假的。
等待 skill 要解决三件事:
等到终态:轮询目标对象(流水线 任务 门禁)直到成功、失败或超时,而不是查一次就下结论;
按正确的维度等待:等的是"这个分支的这次构建",不是"最近一次构建"——等错对象,拿到的产物根本不是你的改动;
超时如实上报:等不到就说等不到,不能假设"应该已经好了"。
有了它,AI 才能做到"改动真的生效了,我再去验证"——这是闭环成立的前提。
本地验证 skill:让 AI 先在本地验证
提交之前先在本地跑通,是最便宜的一道防线。但"本地验证"对 AI 并不直观——不同形态的程序验证方式完全不同,而且坑很多:
服务型程序:本地配置往往不在仓库里(构建时从配置中心拉取),直接读仓库里的空配置文件会误判"没有配置";
脚本型程序:依赖运行环境注入的环境变量,本地裸跑会缺参数,需要先从真实任务里把环境变量提出来;
客户端 / 服务端型程序:缺环境变量时部分逻辑会静默返回 null——编译照常成功、结果完全没产生,最容易误判"已生效";
前端:构建命令通常同时包含类型检查和打包,不能只跑打包绕过类型检查。
本地验证 skill 的价值就在这:把这些"看着成功、其实没生效"的坑提前写清楚,让 AI 知道该怎么验、以及什么情况下不能算通过。
2.3 一体化 command:一条指令跑完整个流程
上面四个 skill 是"零件",command 是把它们串成流水线的"总装"。
/close-loop:给一个需求单 bug 单 + 要做的事,AI 从排查 → 编码 → 本地验证 → 提交 发 MR / 合入 → 等生效 → 线上真实闭环验证 → 流转单子 → 汇报 → 知识复盘,一路走到底,中间不静默停下。简单任务到这一步就够了。/close-loop-create:连单子都还没有时用——给一段需求描述和所属迭代,它先建单,再自动接上close-loop。
一般的使用方法是:
/review-req-chat (先聊清楚需求:模糊、缺失、矛盾的地方当场问清)↓/review-req-doc (把结论落进需求文档,保持简洁)↓/close-loop-create 或 /close-loop (再进闭环)
close-loop:把八步写死在指令里
一条指令能跑完整个流程,靠的不是"更聪明的 AI",而是把步骤顺序写死——每一步做什么、什么条件下不许停,都固化在指令里。原文(节选):
close-loop 原文(节选):
执行一个“闭环任务”:用户提供需求单/bug 单号 + 想做的事情描述,你负责完成从背景分析/问题排查 → 编码 → 验证 → 推送/发MR/合入 → 等待生效 → 线上真实闭环验证的完整链路,直到任务真正生效完成,不在中间任何一步静默停下。
执行步骤(完整闭环,逐步推进)
背景分析 / 问题排查:…理清:问题现象/需求背景、涉及的代码库与分支、验证口径。
编码:定位并修改代码,只做用户要求的功能,不生成无关文件。
验证(本地/测试):改完先验证通过才进入提交;验证口径有歧义先与用户确认。
推送 发 MR 合入:按约定执行 commit → push → create MR → merge MR。
等待生效:合入后等待改动上线……轮询到终态。
线上真实闭环验证:用客观依据确认线上已实际生效、问题不再复现……“代码已合入/已发布”本身不算验证通过。
汇报:总结排查结论、改动内容、验证依据、MR 链接、发布/生效状态、单子流转结果。
知识复盘更新:任务闭环执行完毕后……复盘本次执行产生的新知识并更新到知识库。
整条指令里最关键的是开头那句"不在中间任何一步静默停下"——AI 的默认倾向是"做完一步就汇报、等你指示",不明确禁止,闭环就会断在第 4 步或第 6 步。
前置 review-req-chat:只提问,不编码
需求不清就直接开跑,AI 跑得越快、错得越远,所以闭环前先加一道澄清。这条指令很短,全文如下:
review-req-chat 原文:
帮我对当前的需求进行澄清,找出其中模糊、缺失或矛盾的地方。
1、若需求中提到了具体文件,先读取并理解其内容,再基于此提出问题。
2、只对疑惑的点、或对编码有重大影响的决策提问;无关紧要的细节可自行合理决策,不必询问。
基于我的回答,将澄清后的完整需求写入一份新的 markdown 文件,保存到 docs/design_md/ 目录下。要求保持文档简洁,不要加入设计相关的细节性内容。
在我确认可以开始编码前,不要编码。
它最重要的设计是最后一句:"在我确认可以开始编码前,不要编码"——把最容易越界的动作("我觉得需求清楚了,直接开写")明确锁住。/review-req-doc 是同一套澄清的文档版:只针对疑惑点和影响编码的决策提问,把澄清结果补进需求文档并保持简洁。
2.4 复杂任务:close-loop-team
单点小改,close-loop 一条指令就够;改动面大、需要多轮审查与验证往复时,用 close-loop-team。
与 close-loop 的区别
优化点:清空上下文,让目标聚焦
团队版真正解决的,不是"人多干得快",而是长链路里上下文会被压缩、AI 会忘。
每个成员被调用时都是全新上下文,只带着"这一轮该干的事"——目标天然聚焦,不会被前面几十轮的排查过程带偏;
阶段之间靠制品文件传递信息(排查结论、改动说明、审查意见、验证报告),而不是靠对话历史;
由此有一条硬要求:制品必须精确。审查和验证给出的问题要写到"文件:行";回退时把上一轮的问题清单原文注入给编码成员,不能只说"上次有问题请修复"。制品写得越准,重载后的成本越接近"直接改代码"的下限。
另外两条与效率直接相关的设计:
审查和验证成员在工具层面被禁止改代码(直接去掉写文件的工具),而不是靠提示词约束;
循环有上限:编码⇄审查、验证⇄编码、线上验证⇄编码各最多 3 轮;回退到编码后,验证链必须重新完整走一遍(不允许跳过审查直接验证);达到上限一律停下报告,
blockedfailednot_verified都如实上报,不得静默继续。
流程重量要匹配任务规模:单点小改用顺序版,改动面大才上团队版。
03
3.1 可复制性:最小落地路径
不必一次做全,按这个顺序最省力:
先建记忆:一份两级索引的知识库,只记"反复探索才能拼出来的知识";再给"先读知识库"加一条硬约束(hook 层拦截)——不加约束,它不会成为习惯。
再给手脚:把最高频的操作能力脚本化、封装成 skill。优先做"等待 skill"——没有等待能力,AI 会在系统还没跑完时就去验证,闭环是假的。
最后串成一条线:把"排查 → 编码 → 本地验证 → 提交 / 合入 → 等生效 → 线上验证 → 汇报"固化成一条 command,并把"达上限就停、如实上报"的循环控制写进去。
改动面大时再上团队版:单点小改不需要,别一上来就套重流程。
第 1、2 步与具体业务系统无关,任何团队都能直接抄;第 3 步里的平台 skill 换成你自己的系统即可。
3.2 适用边界
交付型任务用严格约束,探索型任务要放开。 目标明确、有验收标准的(修 bug、做需求)适合上面这套流程;还不知道要做成什么样的能力建设,套重流程会直接掐死探索。
流程重量匹配任务规模。 单点小改用
close-loop,改动面大才上close-loop-team。重手段只对增量跑。 比如"注入已知缺陷、看规则能不能抓到"这类验证手段,全量跑代价太高,只在增量或本次改动上做。