在日常使用 AI 写代码时,有几个问题几乎避无可避:
- AI 写着写着跑偏了,怎么办?
- 遇到自己不懂的生疏领域,AI 给出方案,是先花时间恶补知识还是闭眼听它的?
- 调 Bug 时 AI 在同一个报错上反复碰壁"鬼打墙",怎么解决?
- 几个小时的长任务,怎么保证 AI 不把最初的目标忘得一干二净?
- AI 深度参与写出来的项目,其他人怎么才能快速看懂?
近段时间,我高强度地在使用 TypeScript 社区知名开发者 Matt Pocock 开源的 mattpocock/skills。这个工具库的核心理念,就是把随性、碰运气的"氛围编程(Vibe Coding)",改造成有严格工程门禁的确定性工作流。
结合这段时间的使用体验,我们来看看 Matt 如何用这些skills 来回答这 5 个问题的。
1. 如何保证 AI 没有跑偏?
把一个稍微复杂的任务交给 AI 时,几轮对话下来,代码常常会脱离原有的架构规划,甚至把原本写好的逻辑改乱。
AI 跑偏的主要原因有三个:上下文太长导致注意力被稀释、一开始没有把设计收敛清楚,以及缺乏客观的机器反馈。
Matt 给出的解法不是寄希望于模型"自觉清醒",而是在流程上立下几道硬性关卡:
动手前先做设计质询
在让 AI 动手写代码之前,强制启动一道质询流程(见 skills/grilling)。
让 AI 先把需求整理成一棵决策树,按层级逐一盘问边界条件、异常分支和前置假设。在所有关键分支确认之前,严禁直接跳到写代码阶段。先把需求在纸面上推演清楚,往往就能过滤掉大部分偏航的风险。
用 TDD 提供客观的机器反馈
进入编码阶段后,强制走测试驱动开发流程(见 skills/tdd)。
AI 必须先写出一个能稳定复现预期行为的失败测试(Red),再去编写最简实现使其通过(Green)。测试能不能跑通,由机器直接判决,这是最客观的红线,能有效防止 AI 靠猜想去凑代码。
垂直切片与单任务清空上下文
很多人习惯按架构横向拆分任务(比如"先写完所有的数据库表,再写完所有的接口"),这种方式在长会话里极易漂移。
更好的做法是使用垂直切片(Tracer Bullets)机制(见 skills/to-tickets):把任务拆成穿透数据层、接口层、前端与测试的单条细粒度 Ticket。每个 Ticket 严格在一个全新的会话窗口内完成,一旦测试通过并提交 Git Commit,立即清空会话,切断上下文膨胀带来的干扰。
拦截不可逆的 Git 破坏性指令
在终端环境层面配置硬规则(见 skills/git-guardrails-claude-code),直接拦截 git push --force、git reset --hard、git clean -f 等危险命令,守住底层的物理安全。
2. 面对不熟悉的领域,AI 提出的问题自己也不懂,是先补全知识还是按 AI 的推荐走?
在做技术选型或方案推导时,常常会遇到这种情况:
业务需要接入一个自己从没碰过的协议或技术栈,AI 给出了几个复杂的方案选项,里面的专有名词和原理自己根本看不懂。停下来从头学可能要花好几天,但直接照搬推荐又怕踩坑。
Matt Pocock 提出的原则很清楚:以 AI 的推荐作为假设基准,但必须通过客观约束检验其权衡,不可盲从;事实归工具,决策归人类。
在具体操作上,可以把握这几点:
区分事实与决策
客观事实(比如某个 Node.js 版本是否支持特定 API、某个第三方库的打包体积与运行表现)是 AI 和自动化工具的强项,完全可以放手让它们写测试脚本、查阅官方文档去调研,不需要人类花大量时间死记硬背。
但涉及方案的取舍与代价,决策权必须留在人类手里,不能让 AI 代替自己做主。
检验推荐的三个具体问题
当 AI 给出明确推荐时,即便自己对细节不熟,也可以追问它三个问题:
- 采纳这个方案牺牲了什么?前置假设是什么?
- 如果半年后发现不合适需要替换,重构的代价有多大?它的对外接口是否足够独立?
- 为什么不采用社区里更普遍的传统做法?
借助极简原型(Prototype)建立直观认知
如果在口头上讨论不清,可以让 AI 在沙盒里写一个几十行代码的极简原型(见 skills/prototype)。跑一跑真实代码,看看实际输入输出,比在脑子里凭空猜测要实在得多。
记录决策理由(ADR)
即便决定采纳 AI 的建议,也要通过架构决策记录(见 skills/grill-with-docs)在 docs/adr/ 里记下"为什么选 A、放弃 B 的原因"。日后对这个领域的理解加深了,也能随时根据记录推翻旧方案。
3. AI 如果在一个问题上"鬼打墙"应该怎么解决?
很多人用 AI 调 Bug 时都有过这种痛苦经历:报错出现后,AI 给出修改建议;改完之后报了另一个错,AI 又把代码改回去;来回折腾十几轮,代码改得乱七八糟,Bug 依然在那里。
AI 在原地打转,根本原因在于缺少精准的负反馈,导致它退化成了"看着代码瞎猜"。而随着报错日志越积越多,上下文被严重污染,模型就更容易被带偏。
我们来看一个 API 客户端里的常见例子,直观对比一下两种排查方式。
实战对比:API 客户端偶发解析异常
假设我们写了一个数据请求模块,偶尔会报 SyntaxError: Unexpected end of JSON input。
❌ 容易陷入死循环的闲聊式调试
普通会话里经常出现这样的拉扯:
开发者 :"调用
fetchData()时偶尔报 JSON 解析错误,帮我修一下。"AI(瞎猜) :"可能是接口返回的内容不完整,我给解析部分加了
try...catch,并在出错时默认返回{}。"开发者 :"改完之后下游拿不到字段,全报
TypeError: Cannot read properties of undefined了!"AI(继续瞎猜) :"不好意思,那我改回抛出异常,在外面加个重试逻辑,如果失败就
setTimeout重试 3 次......"开发者:"现在的报错变成了超时,请求直接卡死了!"
AI 从头到尾没有确认过真正的原因,只是在生产代码上反复试错,对话窗口堆积了大量无效信息,最后整个会话只能作废。
✅ diagnosing-bugs 的排查规则
如果按照 skills/diagnosing-bugs 的规则,流程完全不同:
第一步:无秒级复现,严禁动业务代码。
AI 必须先写出一个能在 1 秒内稳定触发、精准断言该报错的最小测试用例:
javascript
// tests/repro-fetch-stream.test.js
import { test } from 'node:test';
import assert from 'node:assert';
import { createMockServer, fetchData } from '../src/client.js';
test('reproduce truncated stream response', async () => {
const server = await createMockServer({ chunkDelay: 50, truncate: true });
// 必须断言预期的业务表现,若出现断流应精准抛出 HttpTruncatedError 而非吞掉异常
await assert.rejects(
() => fetchData(server.url),
{ name: 'HttpTruncatedError' }
);
server.close();
});
运行该命令时,必须看到红色的失败结果:
bash
$ node --test tests/repro-fetch-stream.test.js
✖ reproduce truncated stream response (12ms)
AssertionError [ERR_ASSERTION]: Expected error to be thrown, but got SyntaxError: Unexpected end of JSON input
只要没写出能稳定亮红的测试,AI 就必须停手,禁止修改任何一行生产代码。
第二步:提出可证伪的假设。
AI 提出的每个猜测,都必须符合固定格式:
"假设:底层 Stream 在传输未完成时提前触发了关闭事件。
预测 :如果该假设成立,在数据流完全接收前监听
on('end')并校验 Content-Length,上述测试应该稳定转绿。"
第三步:反思架构接缝。
如果发现代码紧密耦合了原生的 fetch,导致根本写不出上面那个只耗时 12ms 的 Mock 测试,这本身就说明当前模块缺乏接缝(Seam)。
此时应参考 skills/improve-codebase-architecture,先把网络层与解析层解耦,留出可测试的接缝,再去验证修复。
一旦会话已经充斥了无效尝试,不要继续在该会话里挣扎。把写好的 repro-fetch-stream.test.js 单独提出来,扔进一个干净的新会话,AI 往往能一次改对。
4. 长任务怎么保证 AI 不忘记原本的目标?
在长任务中,如果只是在同一个窗口里不断聊天,AI 几乎一定会遗忘最初的顶层约束。
依靠大模型的上下文记忆来维持长任务是靠不住的。正确的做法是把状态放在外面,把会话本身当作可抛弃的耗材。
以写一个"CSV 转嵌套 JSON 命令行工具"为例:
实战对比:CSV 转换工具开发
❌ 依赖记忆的长对话
开发者 :"帮我写一个 CLI 工具,把 CSV 转成嵌套 JSON,必须支持 Node.js 流式读取、类型推断、点分隔路径(如
user.address.city),以及 gzip 输出。"(讨论 15 轮参数解析和类型推断后......)
开发者:"点分隔的深层嵌套对象怎么处理?"
AI :"我改写了解析逻辑,先把整个 CSV 文件通过
fs.readFileSync读进内存数组,然后循环组装嵌套对象......"
此时 AI 早已把第一轮强调的"流式读取"和内存限制忘得一干二净,为了做新特性,直接破坏了前面的设计。
✅ wayfinder + to-tickets 的外置状态流
在规范工程里,长任务的状态记录在文件和 Issue 里:
1. 全局决策地图(见 skills/wayfinder)
在 Issue Tracker 或项目根目录维护一份固定格式的地图:
markdown
# wayfinder:map
## Destination(终点)
纯流式驱动(Stream-first)的 CSV 转嵌套 JSON CLI 工具,内存占用峰值严格低于 50MB。
## Decisions so far(已定决策)
- #1 使用 Node 原生 stream/transform,不引入高开销外部解析库
- #2 类型推断采用严格模式(整型、浮点型、布尔型)
## Not yet specified(战争迷雾)
- 遇到非法 CSV 格式行时的容错策略(跳过并报警 vs 立即中断)
## Out of scope(范围外)
- 暂不支持 XML 或 YAML 格式输出
- 暂不接入外部网络数据源
任何代码改动如果超出了 Destination 或踏入了 Out of scope,都会被直接否决。
2. 垂直切片与会话销毁(见 skills/to-tickets)
把长任务拆成独立切片:
- Ticket-1:点分隔键名解析核心函数与 TDD 单测;
- Ticket-2:流式 Transform 转换流接入;
- Ticket-3:CLI 命令入口参数绑定。
此时,会话变成了一次性工具:
text
开启全新 Agent 会话
└─ 读取 Ticket-1
└─ 编写失败单测(Red)
└─ 编写最简实现通过测试(Green)
└─ 提交 Git Commit
└─ 彻底关闭会话,清空上下文!
下一个 Ticket 由新会话启动,读取 Git 提交与 Issue 继续推进。状态保存在 Git 和地图里,AI 每次都在最干净的上下文中工作。
5. 如何让其他人也能很快地理解项目?
很多人会有顾虑:通过大量 Ticket 切片、由 AI 深度参与写出来的项目,其他人甚至以后的自己接手时,会不会变成一堆拼凑出来、无法理解的碎片代码?
Matt Pocock 的实践揭示了一个很有意思的事实:真正适合 AI 高效协作的工程约束,恰恰也是人类团队顺畅协作的最佳实践。
项目难懂,原因无非是概念混乱、模块过度细碎,或者设计意图丢失。要让新成员快速上手,有几个基础建设值得做:
维护统一的术语表(GLOSSARY.md)
在根目录下维护词汇表(见 skills/domain-modeling),把业务概念与代码中的类名、函数名严格对应起来,避免同义词混用。新成员看一眼词汇表,就能建立一致的心智模型。
记录架构决策(docs/adr/)
关键的技术选型和权衡取舍,都通过轻量的 ADR 文档留存(见 skills/grill-with-docs)。新人接手时,不用到处打听当初为什么这么写,翻翻 ADR 就一清二楚。
尽量写深模块(Deep Modules)
参考 John Ousterhout 在《软件设计哲学》中的原则(见 skills/improve-codebase-architecture):对外提供简单、稳定的接口,对内隐藏复杂的实现细节。
不要为了迎合测试把代码拆成几十个互相调用的浅函数,导致看一个流程得在 5 个文件之间来回跳。让模块变深,代码的自解释性自然就上去了。
渐进式披露(Progressive Disclosure)
在项目的指引文件(如 AGENTS.md)里,顶层只保留最重要的目录索引与核心约定,具体细节按需查阅。无论是人还是工具,都不用一上来就背诵几千行规则。