Ryuu 的个人博客

一个计算机初学者

前面几篇文章里,我一直在谈同一个问题的不同侧面。

代码、工具与人:AI 时代的程序员 里,我谈的是 AI 并没有让程序员的责任消失;在 GPT 有智能,但不是责任主体 里,我谈的是 GPT 可以参与理解和判断,但不能成为责任主体;在 把 AI 放进工作流 里,我进一步谈到,AI 不是靠一次漂亮提示词用好的,而是要被放进一个有边界、有验证、有沉淀的工作流里;在 TDD 让 AI 可靠,AI 让 TDD 可行 里,我又专门写了测试如何约束 AI 的生成能力。

这篇想反过来看:如果工作流没有设计好,AI 编程会怎样失控?

这里说的“反模式”,不是指 AI 不该参与编程,也不是指某个模型能力不够。相反,很多问题正是因为 AI 太能生成、太会解释、太容易把一个还没被确认的想法变成完整代码。它不是慢吞吞地失败,而是很流畅地失败。

所以本文讨论的不是 prompt 技巧,也不是模型排行榜,而是 AI 进入软件开发流程以后,在任务定义、上下文管理、变更控制、诊断、验证和责任边界上常见的工作流问题。

把局部提速误认为整体提速

AI 确实能降低很多局部任务的成本。查资料、解释代码、生成样板、起草测试、补充边界、整理日志,这些事情都可以变快。问题在于,局部实现变快,不等于整体交付变稳。

软件开发不是只由“写出代码”组成的。一个变更能不能进入系统,还取决于需求是否收敛、上下文是否正确、测试是否覆盖真实风险、review 是否能承受变更规模、发布后是否可回滚。AI 加快的是其中一部分环节。如果其他环节没有同步升级,生成速度反而会把压力传导到 review、测试、CI 和发布流程里。

DORA 关于生成式 AI 的研究里也提示过类似现象:AI adoption 可以和个人生产率、flow、满意度提升相关,但也可能伴随交付吞吐和交付稳定性的下降。[1][2] 这并不说明 AI 对工程有害,而是说明“更快生成”必须被接入更强的验证和交付系统。

第一个反模式就是:只看见 AI 带来的局部提速,没有看见它对整体工作流提出的新要求。

任务没有收敛就进入实现

最常见的反模式,是需求还没收敛,就让 AI 直接实现。

表现通常很熟悉:

  • 只有一句模糊需求,就让 AI “做一下”。
  • 没有定义非目标。
  • 没有说明兼容性、权限、性能、异常路径和边界条件。
  • 没有说清楚哪些行为不能被破坏。
  • 把 AI 给出的方案解释,当成需求澄清结果。

AI 很擅长补全缺失信息。问题是,它补出来的东西不一定是业务真实意图。任务越模糊,AI 越容易用“合理默认值”填空。它会根据常见模式生成一个看起来完整的实现,但这个实现可能从第一步就偏离了真实目标。

这类问题在业务规则里尤其危险。比如一个订单系统里,奖励是在订单创建时确定,还是在支付成功时确定,这不是代码风格问题,而是业务事实。如果这个事实没有先被确认,AI 仍然可以写出字段、状态流转、测试和说明文档。越完整,越容易让人误以为它是对的。

更稳的做法,是先把任务压缩成可讨论、可验证的形式:

  • 目标是什么?
  • 非目标是什么?
  • 输入、输出和状态变化是什么?
  • 哪些旧行为不能变?
  • 哪些异常和边界必须覆盖?
  • 什么证据可以说明完成?

复杂任务可以先让 AI 调查和整理,而不是直接改代码。AI 可以帮我们建立问题地图,但不应该替我们决定问题边界。

上下文管理失控

第二个反模式,是把上下文当成资料仓库。

有些人使用 AI 时,会把大量文件、历史讨论、日志、旧计划、失败尝试一次性塞进上下文里。背后的想法很自然:给得越多,AI 知道得越全。可在真实工作里,上下文不是越多越好。

上下文是一种工作资源。它有容量限制,也有注意力分布问题。即使模型还没有撞上 token 上限,也可能已经开始忽略中间区域的关键约束,或者被无关信息带偏。长任务里常见的漂移、重复、遗忘和自相矛盾,很多都和上下文污染有关。

在 AI 编程里,上下文管理至少要区分阶段:

Research 阶段可以宽一点。这个阶段的目标是找相关代码、相关测试、历史约束和可能风险,所以需要广泛浏览。

Plan 阶段要窄一些。这个阶段的目标是做设计取舍,所以应该聚焦关键模块、关键约束和验证方式。

Implement 阶段应该更窄。真正改代码时,最好只保留当前变更所需的文件、接口、测试和约束。

如果把这三个阶段混在同一个不断膨胀的对话里,AI 很容易进入一种“看起来还在工作,但已经不再稳定利用关键信息”的状态。

所以稳定规则不应该长期留在聊天记录里。构建命令、测试命令、命名约定、禁止事项、目录边界、review 习惯,这些应该进入 AGENTS.md、项目文档、测试、lint、CI 或脚本。OpenAI 的 Codex 实践也强调把项目上下文和重复指导沉淀到仓库里,而不是每次都靠临时 prompt 重新说明。[3]

好的上下文管理,不是把所有东西都给 AI,而是让 AI 在正确阶段看到正确材料。

变更范围自然膨胀

第三个反模式,是让变更范围自然膨胀。

AI 很容易给出完整方案。它会顺手整理命名,顺手改接口,顺手抽象一层,顺手重构附近代码。很多时候,这些改动单独看并不糟糕,甚至局部上更漂亮。但工程变更不只看局部漂亮,还要看任务边界、review 成本、回归风险和长期一致性。

典型表现包括:

  • 修一个 bug,顺手重构多个模块。
  • 为了适配新实现,改变既有接口。
  • 把功能修改、重构、测试迁移、文档调整混在一个 diff 里。
  • 生成大批量代码,但没有明确分层。
  • 代码看起来更现代,但破坏了项目既有风格。

这类问题的根源,是 AI 的生成倾向和工程变更的审查需求并不天然一致。AI 倾向于补齐、扩展、圆满化;工程上却经常需要局部、保守、可回滚。

所以在 AI 编程里,review 不能只问“这段代码写得好不好”,还要先问“这些代码是否应该出现在本次变更里”。

更稳的做法是:

  • 明确允许修改哪些文件。
  • 明确哪些模块只读。
  • 把重构和功能修改拆开。
  • 优先小批次提交。
  • 大改动先 review plan,再进入实现。

AI 生成速度越快,越需要人为压住变更批次。

诊断过程被跳过

第四个反模式,是 debug 时直接让 AI 猜根因。

这件事很诱人。把错误日志、代码片段丢给 AI,它很快就能给出一个解释和修复方案。很多时候这个方案还挺像回事。但诊断的核心不是“给出一个合理叙述”,而是“用证据收敛假设”。

没有复现,就不知道问题是否真的存在于当前观察里。没有假设列表,就不知道自己排除了什么。没有能区分假设的证据,就不知道改动是不是只碰巧修到了附近。

AI 在这里的风险,是它会很快生成一个故事:因为 A,所以 B,应该改 C。这个故事可能正确,也可能只是流畅。OpenAI 关于 hallucination 的讨论提醒过我们,模型的流畅输出和事实正确性不是同一件事。[4]

更稳的诊断流程应该是:

  1. 先复现问题。
  2. 明确当前已知事实。
  3. 列出可能假设。
  4. 找能区分假设的证据。
  5. 根据证据更新判断。
  6. 最后再修改代码。
  7. 修复后留下回归测试或最小验证命令。

AI 可以参与这个过程。它可以帮我们读日志、找相关代码、提出假设、设计插桩点、解释测试失败。但它不应该跳过证据,直接进入修复。

debug 中最坏的情况,不是 AI 没有给出答案,而是它给了一个足够像答案的答案,让人停止收集证据。

同一个 Agent 自写自验

第五个反模式,是让同一个 AI 自己写实现、自己写测试、自己修改测试、自己宣布通过。

这看起来像闭环,实际上可能是闭门造车。

如果测试来自同一个模型对需求的即兴理解,它测到的可能不是外部规格,而是模型自己的解释。更糟的是,实现失败以后,AI 可能会为了绿灯去修改断言、放宽条件、删除不方便的用例,或者把当前错误行为写进测试里。

这就是我在 TDD 让 AI 可靠,AI 让 TDD 可行 里反复强调的点:AI-TDD 的关键不是“让 AI 先写测试”,而是先有外部行为规格,再让测试约束实现。

测试在 AI 编程里有双重身份。对 CI 来说,它是验证;对 AI 来说,它也是上下文。好的测试可以把需求稳定地暴露给模型;坏的测试则会同时污染上下文和反馈回路。

更稳的分工应该是:

  • 人定义行为规格和验收边界。
  • AI 起草测试和实现。
  • 自动化工具给出反馈。
  • 人或独立 review 审查测试是否表达了正确规格。

新测试最好先失败,而且失败原因应该是目标行为缺失,而不是测试本身写错、环境缺失或依赖问题。通过以后,也要防止 AI 为了绿灯修改测试语义。

实现写错了,可以重写。测试写错了,会把错误规格固化下来。

完成声明没有验证证据

第六个反模式,是把 AI 的完成声明当成工程事实。

“已完成”“已修复”“测试通过”“应该可以了”,这些都是文本。工程上的完成必须绑定证据。

证据可以是:

  • 实际运行过的测试命令。
  • lint、typecheck、build 的输出。
  • 失败用例从红到绿的记录。
  • diff review 的结果。
  • UI 截图或手工验证步骤。
  • CI 状态。
  • 明确说明未验证部分和剩余风险。

如果没有这些证据,AI 的完成声明只是它对自己输出的总结。这个总结可能有帮助,但不能替代验证。

这里最容易出现的问题,是 AI 用语气弥补证据。它会写得很确定,甚至列出“已完成事项”。但如果没有实际命令和结果,这些文字并不能说明系统状态已经改变。

所以我现在更倾向于把完成判断写成固定要求:完成之前必须说明运行了什么检查,结果如何,哪些检查没有运行,为什么没有运行,剩余风险是什么。Codex 的最佳实践也把测试、检查、行为确认和 review 放在可靠性收口里。[5]

AI 可以宣布候选结论,但工程完成需要证据。

人工介入退化成橡皮图章

第七个反模式,是所谓人在环只剩下“点继续”。

很多 AI 工具会在关键时刻请求用户确认。这个机制本身是必要的,但如果只给一个确认按钮,而不给足够上下文,人工介入就会退化成橡皮图章。

有效的人工介入,至少要让人看到:

  • 当前目标是什么。
  • AI 准备做什么。
  • 影响范围是什么。
  • 风险在哪里。
  • 是否可回滚。
  • 有哪些替代选项。
  • 当前证据支持到什么程度。

人在环不是让人频繁打断 AI,也不是让人最后兜底。它应该出现在高杠杆位置:计划确认、风险动作、测试规格、发布决策、证据不足时的暂停。

Microsoft 的 Human-AI Interaction Guidelines 强调 AI 系统应该让用户能够理解、纠正和恢复;NIST 的 AI Risk Management Framework 也把风险放进治理、度量和管理结构里看。[6][7] 这对应到日常 AI 编程,就是不要把责任模糊地推给“用户已确认”。确认必须基于上下文,反馈也要能沉淀。

如果用户纠正了 AI 的行为,这个纠正不应该只停留在一次对话里。它应该尽量进入规则、测试、文档、模板或脚本。否则同样的问题下次还会发生。

一个更稳的工作流

把这些反模式放在一起看,它们其实都指向同一个问题:AI 编程不能只围绕“生成”设计,而要围绕“可验证交付”设计。

一个更稳的流程可以很简单:

  1. Research:先理解现有代码、测试、约束和风险。
  2. Plan:定义改动范围、非目标、验证方式和人工介入点。
  3. Implement:小步修改,避免混入无关重构。
  4. Verify:运行测试、lint、build,必要时做人工检查。
  5. Document:把稳定结论沉淀到项目规则、文档或知识库。

这不是为了把流程变重。简单任务当然不需要完整仪式。改一个拼写、补一个小配置、调整一个局部样式,没有必要强行走完整流程。

关键在于,任务一旦涉及模糊需求、存量系统、跨模块变更、生产风险、测试缺口或长期维护,就不能只依赖 AI 的生成能力。越是复杂任务,越需要阶段化、上下文压缩、小批次变更和验证证据。

AI 编程成熟度的标志,不是让模型一次生成更多代码,而是让生成结果更容易被约束、审查、验证和回滚。

真正需要升级的,不只是 prompt,而是工作流。

参考与延伸阅读


  1. DORA, Accelerate State of DevOps Report 2024。这里引用它来支撑“AI 对个人体验和团队交付系统可能产生不同方向影响”的边界判断。 ↩︎

  2. DORA, Impact of Generative AI in Software Development。这里引用它来支撑“生成速度可能通过更大 batch size 压力传导到 review、测试和交付系统”的解释。 ↩︎

  3. OpenAI Developers, Codex best practices。这里引用它来支撑“项目上下文、重复指导和验证命令应该沉淀到仓库和工作流中”的实践判断。 ↩︎

  4. OpenAI, Why language models hallucinate。这里引用它来提醒:模型输出的流畅性不能直接等同于事实可靠性。 ↩︎

  5. OpenAI Developers, Codex best practices。这里引用它来支撑“完成断言需要测试、检查、行为确认和 review 收口”。 ↩︎

  6. Microsoft Research, Guidelines for Human-AI Interaction。这里引用它来支撑“人需要能够理解、纠正和恢复 AI 输出”的交互原则。 ↩︎

  7. NIST, AI Risk Management Framework。这里引用它来支撑“AI 风险需要进入治理、度量和管理结构,而不是由模型或单次确认承担”的边界。 ↩︎

把 AI 放进工作流 里,我写过一个判断:AI 不是靠一次漂亮的提示词用好的,而是要被放进一个有边界、有验证、有沉淀的工作流里。

这篇继续往下走,专门谈这个工作流里越来越重要的一层东西:Agent Skill。

如果一个同事还不知道什么是 Agent Skill,我不太想一上来就告诉他:它是一个包含 SKILL.md 的目录,里面有 YAML front matter、说明正文、references/scripts/assets/

这当然是事实,但这不是最重要的事实。

更重要的是:为什么我们已经会写 Prompt 了,还需要 Skill?

Prompt 适合表达这一次,Skill 适合沉淀下一次

我们平时和 AI 协作,最自然的方式是写 Prompt。

“帮我看一下这段代码。”

“按 TDD 的方式实现这个功能。”

“先不要动手,先读项目结构。”

“写完以后跑测试,不要只说完成。”

这些话都很有用。Prompt 的优势就在这里:它直接、灵活、贴近当前任务。你脑子里刚形成一个意图,就可以把它说出来,让 AI 立刻参与。

但问题也在这里。

很多要求并不是只对这一次有用。比如:

  • 修 bug 前要先复现。
  • 不熟悉代码时要先 zoom out,而不是盯着当前文件解释。
  • 写文章前要先确定读者、目的、论点和材料。
  • 改完代码后要用新鲜证据验证,不能靠语气宣布完成。
  • 重要结论要写回项目文档或知识库,而不是散落在聊天记录里。

如果这些要求每次都靠临时 Prompt 提醒,它们就很容易失效。

人会忘记说。对话会变长。上下文会变脏。AI 会在后半段逐渐回到它熟悉的生成模式里。更麻烦的是,这些做法一旦只存在于聊天记录里,就很难被复用、修改、审查和传给团队里的其他人。

所以 Prompt 的问题不是“不够强”,而是它天然偏向一次性沟通。

Skill 要解决的是另一类问题:把反复出现的工作方式,沉淀成可复用、可触发、可维护的上下文。

Skill 不是更长的 Prompt

很容易把 Agent Skill 理解成“高级提示词”。

这个理解只对了一半。

Skill 里面确实有自然语言指令,Agent 也确实会读这些指令。但一个好的 Skill 不只是把一大段 Prompt 存起来。它至少多解决了三个问题。

第一,它解决“什么时候用”的问题。

Skill 的 description 不只是给人看的简介。对 Agent 来说,它是路由线索。一个好的 description 要说清楚:这个 skill 适用于什么任务,不适用于什么任务,和相邻 skill 的边界在哪里。

第二,它解决“触发后先判断什么”的问题。

SKILL.md 不应该像百科全书,也不应该复制工具教程。它更应该告诉 Agent:当前任务先确认什么,哪些边界不能越过,什么时候需要继续读 references/,什么结果才算完成。

第三,它解决“细节如何按需加载”的问题。

不常用的背景资料、长检查清单、示例、脚本、模板,不一定要全部塞进主文件。它们可以放进 references/scripts/assets/。Agent 需要时再读,需要执行时再调用。

所以 Skill 和 Prompt 的差别,不只是“短”和“长”的差别。

Prompt 更像一次对话里的意图表达。Skill 更像某类任务的工作协议。

Skill、Prompt 和 Tool 各自解决不同问题

这三个概念很容易混在一起。

可以先粗略地区分一下:

概念 解决的问题 例子
Prompt 这一次要做什么 “帮我写一篇解释 Agent Skill 的文章”
Tool Agent 能执行什么动作 读写文件、运行测试、搜索网页、调用浏览器
Skill 某类任务应该怎么做 写文章前先确认读者、目的、论点、材料和声音

比如我说:“帮我写测试。”

这是一句 Prompt。

Agent 能调用测试命令、编辑器、文件系统,这些是 Tool。

如果我希望它按 TDD 的节奏推进,先写失败测试,再写最小实现,再重构,并且不要为了让测试通过反过来放松断言,那就是 Skill 要表达的工作方式。

Skill 不替代 Prompt,也不替代 Tool。

Prompt 提供当前目标。Tool 提供行动能力。Skill 提供可复用的判断和流程。

Skill 的核心是渐进式披露

Agent Skill 背后的关键设计,不是“把更多说明交给 AI”,而是“只在需要时加载正确说明”。

这叫渐进式披露。

Agent 不应该在每次任务里都读完所有规则。那样上下文会很快变成垃圾桶。真正可持续的方式是分层:

第一层,启动时只让 Agent 看到 skill 的名字、描述和位置。它知道“有这么一个能力”,但还没有把全文塞进上下文。

第二层,当任务匹配时,Agent 才读取对应的 SKILL.md

第三层,如果 SKILL.md 指向更细的参考资料、脚本或模板,Agent 再按需要继续读取或执行。

OpenAI 的 Codex 文档把 skill 描述为可复用的工作流格式,并明确提到 Codex 先看到 skill 的名称、描述和路径,只有决定使用时才读取完整 SKILL.md。Anthropic 在介绍 Agent Skills 时也用了同样的思路:先加载 namedescription,任务相关时再读正文,更细的文件继续按需展开。[1][2]

这件事很重要。

因为上下文窗口不是越满越好。把所有规范、示例、异常情况、脚本说明都塞进去,短期看像是“资料齐全”,长期看却会稀释真正关键的约束。Agent 可能读了很多字,却更难抓住当前任务最应该遵守的那几条规则。

好 Skill 不是让 Agent 多读字,而是让 Agent 少想错。

一个 Skill 通常长什么样

从格式上看,一个 Skill 通常是一个目录。

1
2
3
4
5
my-skill/
├── SKILL.md
├── references/
├── scripts/
└── assets/

SKILL.md 是入口。它通常包含 namedescription,然后是一段给 Agent 的任务说明。

references/ 放更细的材料,比如写作检查清单、项目规范、领域背景、案例、边界说明。

scripts/ 放可以执行的脚本。能机械化的东西,尽量不要长期停留在自然语言里。脚本比口头规则更稳定,也更容易验证。

assets/ 放模板、图片、样例文件或其他资源。

但格式只是外壳。真正决定 Skill 质量的,是它有没有把边界和判断写清楚。

一个坏的 description 可能是:

1
description: Help with project work.

这句话几乎没用。它太宽了,什么都像能触发。

一个更好的写法会像这样:

1
description: Use when drafting or revising public-facing technical articles. Identify reader, purpose, thesis, evidence, structure, and voice before drafting. Prefer project-docs-writing for durable internal project documentation.

它说清楚了三件事:什么时候用,做什么,和相邻 skill 的边界在哪里。

这就是 Skill 和普通说明文档的差别。Skill 首先要能被正确触发,其次才是触发后写得多完整。

好 Skill 先教判断,再教步骤

很多人第一次写 Skill,容易把它写成操作手册。

第一步做什么,第二步做什么,第三步做什么。再加一大堆命令示例、注意事项、边界情况。最后 SKILL.md 越写越长,像一份很努力但很臃肿的说明书。

这不一定有效。

Agent 很多基础操作本来就会。它会读文件,会看 --help,会根据项目里的 package scripts 推断测试命令,会模仿现有代码风格。Skill 不应该把这些 baseline 能力重新讲一遍。

Skill 最值得写的是 Agent 默认不知道、但对这类任务很关键的东西:

  • 这个团队的特殊约定。
  • 这个任务的判断边界。
  • 常见错误路径。
  • 完成定义。
  • 失败时应该回退到哪里。
  • 哪些信息必须来自外部证据,而不是猜测。

比如一个写作 skill,不需要教 Agent “Markdown 用 # 表示标题”。它真正应该写的是:先确认读者是谁,文章要解决什么问题,中心论点是什么,材料哪些来自事实,哪些只是作者经验,哪些地方不能编造。

比如一个诊断 skill,不需要教 Agent “可以打印日志”。它真正应该写的是:先建立反馈循环,先复现,再提出可证伪假设,用观察结果更新判断,不要直接跳到最像答案的修复。

比如一个验证 skill,不需要教 Agent “测试命令怎么运行”。它真正应该写的是:不要在没有新鲜证据的情况下宣布完成;验证失败时要报告失败,而不是把语气改得更肯定。

好的 Skill 不是把 Agent 当新手,而是把 Agent 拉回正确的判断结构里。

坏 Skill 会把 Agent 推进噪声里

Skill 写坏了,会比没有 Skill 更糟。

最常见的坏法,是把 Skill 写成“万能入口”。

比如一个叫 engineering 的 Skill,里面同时包含代码审查、TDD、架构设计、文档同步、Git 提交、发布流程和故障诊断。它看起来很完整,但每次触发都把一大堆当前任务用不到的内容塞进上下文。最后 Agent 反而更容易抓错重点。

第二种坏法,是复制工具文档。

比如在 Skill 里长篇解释 git statusnpm testpytestcurl 怎么用。这些东西大多可以由 Agent 自己发现,或者应该交给脚本、README、package scripts、CI 配置处理。自然语言 Skill 应该保留给不能机械化的判断。

第三种坏法,是只有反模式,没有替代方案。

“不要乱改文件。”

“不要跳过测试。”

“不要写太长。”

这些话单独看都对,但只写“不要”很容易让 Agent 锚定在错误动作上,又不知道正确路径是什么。更好的写法是同时给出替代方案:

不要直接改实现。先复现问题,记录可观察现象,再提出假设。

不要把所有细节塞进 SKILL.md。主文件只保留路由和关键判断,长案例放到 references/

不要只说完成。先运行和任务相关的验证命令,再用验证结果说明完成状态。

第四种坏法,是边界不清。

两个 Skill 如果都像能处理同一个任务,Agent 就会犹豫,或者选错。Skill 的边界应该写在 description 里,而不是藏在正文深处。

所以评估一个 Skill,不只要问它“能不能用”,还要问它有没有制造新的上下文噪声。

什么时候该把 Prompt 升级成 Skill

不是所有 Prompt 都值得变成 Skill。

一次性需求没有必要 Skill 化。还在探索中的偏好也不急着沉淀。只有当一段提示开始反复出现,并且忘记它会导致稳定的质量问题时,才值得考虑升级。

我会用这几个问题判断:

第一,它是不是高频出现?

如果每周都在重复同一类要求,比如写文章、诊断 bug、做代码审查、同步项目文档,那它有可能适合 Skill。

第二,它是不是代表稳定的团队约定?

比如“这个项目所有文档结论要区分事实、计划和猜测”,“修改支付逻辑必须补对账视角”,“发布前必须跑某几个验证命令”。这些不只是个人偏好,而是团队工作方式。

第三,它有没有明确触发边界?

如果你说不清什么时候该用、什么时候不该用,那它还不适合写成 Skill。边界模糊的 Skill 很容易误触发。

第四,它是不是包含判断规则,而不只是命令清单?

命令清单更适合脚本、Makefile、package scripts 或 CI。Skill 适合承载“什么时候该走哪条路”的判断。

第五,它是不是会从版本化中获益?

如果这个工作方式需要被审查、迭代、共享、回滚,那它就更像工程资产,而不是一次聊天里的提醒。

当这些问题的答案大多是“是”,Prompt 就可以升级成 Skill。

从最小 Skill 开始

写 Skill 不应该从设计一个完整体系开始。

更好的方式是从一个真实任务开始。

先选一个你最常重复的工作流,比如“写技术文章”“修 bug”“做 TDD”“同步项目文档”。然后只写最小入口:

1
2
3
4
---
name: article-writing
description: Use when drafting, outlining, rewriting, editing, or reviewing public-facing articles. Identify reader, purpose, thesis, material, voice, and venue before drafting.
---

正文也先保持短:

1
2
3
4
5
6
7
8
9
# Article Writing

Before drafting, identify reader, purpose, thesis, material, voice, and venue.

Choose a structure before writing.

Separate sourced facts from author experience and invention.

Draft with a visible through-line: every section should advance the thesis.

然后用它做真实任务。

如果 Agent 误触发,就改 description

如果 Agent 触发后仍然忘记关键边界,就把边界前置。

如果正文越来越长,就把细节拆进 references/

如果某些步骤可以机械化,就写脚本。

如果某条规则只是你临时想到的,不要急着加进去。等它在真实任务里反复证明有用,再沉淀。

这和写代码有点像。不要一开始就抽象一个庞大的框架。先从一个代表性任务开始,让真实失败告诉你这个 Skill 缺什么。

Skill 让 AI 协作更像工程资产

Prompt 仍然重要。

我们不可能把所有意图都提前写成 Skill。每一次任务仍然需要人说明目标、提供上下文、判断结果。Skill 不是为了取代 Prompt,而是为了让那些稳定、重复、可复用的部分不再每次从零开始。

这也是我觉得“从 Prompt 到 Skill”重要的原因。

它不是提示词技巧的升级,而是工作流抽象的升级。

当我们只写 Prompt 时,AI 协作更像一场场临时对话。对话当然有价值,但它很难沉淀。

当我们开始写 Skill 时,一部分经验会变成可版本化的资产:团队怎么写文章,怎么修 bug,怎么做测试,怎么验证完成,怎么把结论写回文档。

这些东西过去可能藏在资深同事的脑子里,藏在 code review 的评论里,藏在一次次会议和聊天记录里。Skill 把它们转成 Agent 能按需读取的工作上下文。

所以,Agent Skill 不是为了让 AI 替我们判断。

恰恰相反,它是把人的判断边界写得更清楚,让 AI 在这些边界里更稳定地工作。

Prompt 表达这一次我要什么。

Skill 沉淀下一次仍然要遵守什么。

这就是从 Prompt 到 Skill 的那一层抽象。

参考与延伸阅读


  1. OpenAI Developers, Build skills。这里引用的是 Codex Skills 对可复用工作流、渐进式披露、SKILL.mdscripts/references/ 的说明。 ↩︎

  2. Anthropic, Equipping agents for the real world with Agent Skills。这里引用的是 Agent Skills 的目录结构、name / description 路由、渐进式披露和按需加载参考文件的设计。 ↩︎

前面几篇文章里,我一直在绕着同一个问题打转:AI 到底应该放在软件开发的什么位置?

代码、工具与人:AI 时代的程序员 里,我谈的是程序员的职责并没有因为 AI 消失;在 GPT 有智能,但不是责任主体 里,我谈的是 GPT 可以有智能,但不能替我们承担判断后果;在 把 AI 放进工作流 里,我又进一步谈到,AI 不是靠一次漂亮提示词用好的,而是要被放进一个有边界、有验证、有沉淀的工作流里。

这篇想继续往下走,谈一个更具体的实践:TDD。

我越来越觉得,AI 和 TDD 不是谁取代谁的关系。恰恰相反,它们有一种相互成就的关系:

TDD 让 AI 可靠,AI 让 TDD 可行。

这句话的前半句比较容易理解。AI 写代码很快,但越快越需要验证;TDD 提供的失败测试、最小实现和重构反馈,能把 AI 的生成能力拉回到具体行为上。

后半句也同样重要。过去很多人不是不知道 TDD 有价值,而是很难坚持。写测试、补边界、维护反馈回路,这些都是真实成本。AI 出现以后,这些成本被压低了。它可以帮我们起草测试、补充边界、迁移既有风格、解释失败原因、推进小步重构。也就是说,AI 不只是需要 TDD,AI 也让 TDD 更容易被执行。

AI 让错误也变快了

AI 时代写代码,最明显的变化当然是快。

过去实现一个需求,我们要自己读代码、想接口、写逻辑、补测试、跑验证。这个过程不一定优雅,但它有一种天然的摩擦。人写得慢,所以每一步多少都会停一下,想一想这段逻辑是不是对的,边界有没有漏,和现有系统是否合得上。

现在不一样了。你可以把需求丢给 AI,它很快就能给出一整套实现。更厉害的是,它不只会写实现,还会写解释,甚至还能顺手补几条测试。

这当然是好事。很多重复劳动、样板代码、边界枚举、失败排查,都可以被大幅加速。

但风险也在这里。AI 的问题很多时候不是“写不出来”,而是“写得太像真的”。它可以把一个还没有被确认的理解,快速变成一段结构完整、命名像样、测试齐全的代码。它的输出越流畅,人越容易误以为事情已经成立。

过去错误可能只是一个想法。现在错误可以很快变成代码,变成测试,变成提交,甚至变成一段看起来很合理的说明。

比如一个订单系统里,奖励到底是在“订单创建时确定”,还是在“支付成功时确定”,这是业务事实,不是代码风格。AI 如果一开始理解错了,它照样可以写出完整的字段、状态流转、单元测试和说明文档。问题不在于这些代码不像工程代码,而在于它们可能从第一步就把错误业务事实固定下来了。

所以 AI 改变的不只是开发效率,也改变了错误扩散的速度。

TDD 难,不是因为它没价值

说到 TDD,很多讨论容易走向两个极端。

一种是把 TDD 当成某种纯粹正确的工程道德:不先写测试就是不专业。另一种是把 TDD 当成过时的仪式:现实项目哪有时间红绿重构,先把需求做出来再说。

我不太喜欢这两种说法。

TDD 的价值一直在那里。先写测试,意味着先把期望行为说清楚;看到测试失败,意味着确认当前系统确实缺少这个行为;写最小实现让测试通过,意味着先解决具体问题,而不是顺手扩大战场;在测试保护下重构,意味着结构调整有反馈网兜。Kent Beck 在《Test-Driven Development: By Example》里反复强调的也是这个节奏:用很小的步长,在测试、实现和重构之间建立反馈。[1]

这些东西并不过时。

但 TDD 的成本也一直在那里。写测试要花时间,想边界要花时间,理解测试框架要花时间,维护 fixture 和 mock 要花时间。尤其在存量项目里,最难的往往不是“写一个测试”,而是写一个符合当前项目习惯、能稳定运行、以后别人愿意维护的测试。

这也是很多 TDD 书籍真正花篇幅讨论的部分。《xUnit Test Patterns》不是只讲“要写测试”,而是系统整理 fixture、stub、mock、test smell 和可维护测试结构;《Growing Object-Oriented Software, Guided by Tests》也不是把测试当成附属物,而是把测试放进对象设计和系统演进里看。换句话说,TDD 的难点从来不只是“先写测试”这个动作,而是如何长期维护一套有表达力、可定位、可演进的反馈系统。[2][3]

很多人不做 TDD,不一定是反对它的价值,而是抗不过它的成本。

至少在我现在的使用里,AI 在这里带来了一个很有意思的变化:它降低了 TDD 的执行成本。

它可以帮我们从需求里拆出输入、输出、状态变化和错误路径;可以根据已有测试补出相似用例;可以把重复的断言整理成 helper;可以在测试失败时快速定位失败原因;可以在红绿循环中承担大量机械推进。

这个判断不只是体感,也有一些旁证。Microsoft Research 和 GitHub 关于 Copilot 的受控实验里,使用 Copilot 的开发者在一个 JavaScript HTTP server 任务中完成速度快了 55.8%。[4] GitHub 2024 年针对软件开发团队的 AI 调查也显示,超过 98% 的受访者表示所在组织已经尝试用 AI coding tools 生成测试用例。[5] Stack Overflow 2025 年开发者调查里,约 70% 的 AI agent 用户同意 agent 减少了特定开发任务耗时,69% 同意它提高了个人生产率。[6]

这些数据不能直接证明“AI 让 TDD 变简单了”。它们支持的是一个更窄的结论:在编码、测试起草、重复修改这些局部任务上,AI 确实在降低摩擦。TDD 过去最难坚持的部分,恰好有不少落在这些局部任务上。

过去 TDD 像是一件“知道应该做,但经常做不动”的事。AI 出现以后,它变得更容易启动,也更容易坚持小步反馈。

这就是“AI 让 TDD 可行”的部分。

AI 更需要 TDD

但另一面更关键:AI 也更需要 TDD。

因为 AI 很容易给出一个看起来完整的答案。它会解释需求,会写实现,会补测试,会总结自己做了什么。如果没有外部约束,这个过程很容易变成一个封闭叙事:它先生成一个理解,再根据这个理解写测试,然后根据这个测试写实现,最后宣布一切通过。

表面上看,这像是一个完整闭环。实际上,闭环里最重要的问题可能没有被回答:

这个测试测的是我们真正想要的行为吗?

它有没有把 bug 固化成期望?

它有没有只覆盖 happy path?

它有没有 mock 掉真正有风险的边界?

它是不是为了让实现通过,顺手放宽了断言?

TDD 在 AI 时代的价值,不只是“先写测试”这个动作,而是把 AI 的生成能力接入工程反馈回路。失败测试不是装饰,它要证明目标行为当前确实不存在;通过测试也不是表演,它要证明实现满足了我们事先定义的行为。

AI 可以生成代码,但测试把它拉回到行为。

AI 可以解释自己,但验证结果比解释更重要。

AI 可以快速尝试方案,但红绿反馈能告诉我们哪些尝试真的成立。

所以 TDD 在 AI 时代不是变轻了,而是变得更关键了。生成能力越强,越需要验证结构。

这里也有反方向的证据提醒我们别太乐观。2024 年一项关于 GitHub Copilot 生成 Python 测试的实证研究评估了 290 个生成测试:在已有测试套件上下文中,只有 45.28% 是可直接通过的非空测试;如果没有已有测试套件上下文,92.45% 的生成结果是失败、损坏或空测试。[7] 也就是说,AI 确实能帮我们写测试,但“有上下文”和“需要人工修正”不是细节,而是成败条件。

另一项关于生成式 AI 和 TDD 的早期实验也给出了类似提醒:GenAI 可以参与 TDD 流程,但代码质量监督不能省掉,模型有时会为了给出答案而误导非专家。[8] 越是想把 TDD 自动化,越要明确哪些地方必须由人或外部反馈接管。

先定义行为,而不是先写测试

不过,这里有一个容易误解的地方。

AI 时代的 TDD,不能简单理解成“让 AI 先写测试”。如果需求本身还没说清楚,AI 先写测试也只是先写一份猜测。

真正应该先出现的,不是测试代码,而是行为规格。

也就是说,在让 AI 动手之前,最好先把这些东西说清楚:

  • 输入是什么?
  • 输出是什么?
  • 哪些状态会变化?
  • 哪些业务规则不能被破坏?
  • 哪些边界情况必须覆盖?
  • 权限、幂等、并发、兼容性有没有要求?
  • 哪些情况不属于本次范围?
  • 最后应该运行哪些验证命令?

这一步不一定要写成长篇文档。很多时候,几条清楚的 bullet 就够了。关键是要把“对不对”从聊天感觉变成可执行的标准。

测试不是让 AI 猜我们想要什么,而是把人的判断翻译成可以运行的东西。

从 AI 的角度看,测试还不只是验收条件,它也是一种上下文。WebApp1K 这类 TDD benchmark 直接把测试用例同时当作 prompt 和 verification,评估模型能不能从测试中读出需求、实现功能并通过验证。它的结论很有启发:TDD 场景里,模型能力不只取决于一般代码能力,还取决于 instruction following、in-context learning,以及长上下文下是否丢失约束。[9]

这也是为什么我觉得 AI-TDD 里,人的位置并没有后退。人少写了一些样板代码,但更需要负责定义行为、判断边界和审查测试是否表达了正确规格。

如果行为定义错了,AI 写得越快,只会错得越完整。

最危险的是自写自验

AI-TDD 里最危险的反模式,是让同一个 AI 自写自验。

它自己理解需求,自己设计测试,自己写实现,自己修改断言,自己宣布通过。整个过程看起来很顺,但问题是:没有外部规格,也没有独立审查。

这会产生一些很隐蔽的问题。

比如实现失败了,它可能不是停下来问规格是否理解错了,而是把断言改松一点。
比如原来有一个 bug,它可能把当前错误行为写进测试里,让 bug 变成“被保护的期望”。
比如它为了让单元测试容易写,把数据库、时间、权限、队列这些真正有风险的边界全部 mock 掉。
比如它写了很多测试,但测试复刻了生产逻辑,所以实现错了,测试也跟着错。
比如它新建一套横向切片测试,看起来覆盖很完整,却完全不符合项目已有 fixture、helper 和目录习惯。

这些问题比“实现代码写错”更麻烦。

代码写错了,测试可能会抓出来。测试写错了,就会把错误规格长期固化下来。它不只是没发现问题,还会给人一种已经验证过的错觉。

所以在 AI-TDD 里,测试 review 甚至比实现 review 更重要。

实现可以重写,错误测试会污染反馈回路。

更好的分工

我现在更倾向于把 AI-TDD 看成一种分工,而不是一种仪式。这一节只回答一个问题:一次需求从行为规格到完成断言,应该由谁负责哪一段。

人负责定义行为和验收边界。
AI 负责起草测试、生成实现、修复失败、整理重复。
自动化工具负责持续给出反馈。
人再回来判断这些反馈是否足以说明任务完成。

放到实际流程里,可以是这样:

  1. 先写清楚行为规格和不覆盖范围。
  2. 让 AI 先阅读相关代码和现有测试。
  3. 让 AI 总结测试框架、fixture、mock 边界和运行命令。
  4. 新增测试。
  5. 先运行测试,确认它因为目标行为缺失而失败。
  6. 先 review 测试,再写实现。
  7. 写最小实现,让测试通过。
  8. 运行相关测试、typecheck、lint 和构建。
  9. 在测试保护下重构。
  10. 用验证结果,而不是 AI 的语气,判断是否完成。

这里最重要的不是步骤数量,而是反馈方向。

测试要先约束实现,而不是实现失败以后反过来改测试。
验证命令要约束完成判断,而不是 AI 说完成就完成。
现有测试生态要约束新增测试,而不是每次需求都新建一套自己的小世界。

这个流程看起来慢一点,但它实际上是在把 AI 的速度导入一个更可靠的轨道。

没有轨道,速度越快越危险。

存量项目里,先研究测试生态

如果是在一个新项目里,TDD 的启动成本相对低一点。测试框架、目录结构、fixture 习惯都还可以一起塑造。

但大多数真实工作不是这样。真实项目里已经有历史包袱,有旧测试,有命名习惯,有各种 helper 和 mock 边界。AI 如果一上来就新建测试文件,很容易写出一种“看起来没问题,但不像这个项目”的测试。

这种测试短期可能能跑,长期很难维护。

如果上一节讲的是“流程分工”,这一节讲的就是“进入项目之前先认路”。对存量项目,我觉得 AI-TDD 的第一步不是写测试,而是研究测试生态。

可以先让 AI 回答这些问题:

  • 相关模块已有测试在哪里?
  • 用的是什么测试框架?
  • fixture、factory、mock helper 怎么组织?
  • 单元测试、集成测试、端到端测试各自负责什么?
  • 哪些外部依赖通常 mock,哪些边界通常走真实集成?
  • 本地快速验证命令是什么?
  • CI 必跑命令是什么?
  • 有没有最接近本次需求的既有测试?

这一步很像我前面说的“先把 AI 放进工作流”。AI 不是凭空开始写,而是先进入项目已有的规则系统。

越是存量项目,越不能只追求“有测试”。真正有价值的是“这个测试属于这个项目”。

测试策略应该写进仓库

研究一次测试生态还不够。如果我们希望 AI 稳定地做对这些事,就不能每次都靠临时提醒。

对 AI 来说,不在上下文里的规则就等于不存在。你心里知道这个项目不喜欢过度 mock,知道端到端测试只覆盖关键路径,知道某些数据库行为必须走集成测试,但如果这些规则没有写出来,AI 不会天然知道。

所以测试策略应该进入仓库。

它可以在 AGENTS.md,可以在 docs/testing.md,可以在测试目录的 README,也可以在团队约定的其他位置。关键是让 AI 和人都能看到:

  • 常用测试命令是什么?
  • 什么情况写单元测试?
  • 什么情况写集成测试?
  • 什么情况才需要端到端测试?
  • fixture、factory 和 helper 应该怎么复用?
  • 哪些边界不要随便 mock?
  • 测试命名有什么约定?
  • flaky test 怎么处理?
  • CI 上哪些检查必须通过?

这些内容看起来像文档,其实也是 harness 的一部分。它们把人的偏好和经验变成环境约束,让 AI 不需要每次都从零猜测。

这也是 AI 时代软件工程很重要的一个变化:项目不只是给人读的,项目也要能被 AI 读懂。

不是更多测试,而是更好的反馈

AI 可以很快生成很多测试,但更多测试不一定意味着更可靠。

如果测试很慢、很脆、定位困难,AI 的迭代效率反而会下降。它可能开始绕过测试,或者为了让测试通过去修改断言。端到端测试尤其如此。它们有价值,但不应该因为 AI 写得快就无限膨胀。

健康的测试组合仍然需要层次。

快速的单元测试和组件测试,适合覆盖纯逻辑、边界计算、解析校验和状态转换。
集成测试适合覆盖数据库、队列、权限、时间、事务这些真实风险。
少量端到端测试适合保护关键用户路径和系统级冒烟。

AI 降低了起草测试和重复修改的成本,但没有取消测试设计的判断。

我们仍然要问:这个风险应该在哪一层验证?这个测试失败后能不能快速定位?这个 mock 是隔离噪音,还是遮住了真正风险?这个用例是在测行为,还是在测实现细节?

TDD 的目标不是堆测试数量,而是建立有效反馈。

生成越便宜,验证越重要

AI 让软件开发里的生成变得便宜了。

代码可以更快生成,测试可以更快生成,文档可以更快生成,方案也可以更快生成。生成变便宜以后,真正稀缺的东西就变了。

DORA 关于生成式 AI 的研究也提示了同一个方向:AI 会提升个人生产率、flow 和满意度,但如果团队的 review、测试和交付机制没有跟上,交付稳定性和吞吐反而可能下降。DORA 的 GenAI 报告进一步指出,AI adoption 增加和 delivery throughput、delivery stability 下降存在关联,其中一个解释是 AI 让代码生成更快,容易带来更大的 batch size,而大 batch 更慢 review,也更容易造成系统不稳定。[10][11]

这说明 AI 把瓶颈从“能不能写出来”推到了“能不能及时 review、测试和交付”。TDD 在这里就不只是个人习惯,而是让生成速度进入可控反馈回路的一部分。

稀缺的不再只是“谁能把代码写出来”,而是:

  • 谁能定义正确的问题?
  • 谁能说清楚行为边界?
  • 谁能判断测试是否表达了规格?
  • 谁能确认反馈是否可信?
  • 谁能为最终结果负责?

TDD 正好站在这个位置上。

它不是为了抵抗 AI,也不是为了保留某种旧时代的开发仪式。它是在 AI 时代重新变得重要的一种工程结构:用测试把意图固定下来,用失败证明问题存在,用通过证明行为成立,用重构保护长期演进。

所以我不觉得 AI 会让 TDD 过时。

更准确地说,AI 和 TDD 在补足彼此。

AI 让我们更容易抵达绿灯,但 TDD 提醒我们先确认红灯是真的。AI 让生成变快,TDD 让验证跟上。AI 降低了 TDD 的执行成本,TDD 降低了 AI 的工程风险。

这就是我现在更愿意相信的开发方式:

不是把 AI 当成一个可以直接交付答案的人,也不是把 TDD 当成一个必须机械执行的仪式,而是让 AI 的生成能力和 TDD 的验证结构形成闭环。

生成能力越强,越需要验证结构。验证结构越清楚,AI 的生成能力才越值得放心使用。

参考与延伸阅读


  1. Kent Beck, Test-Driven Development: By Example。本文借用的是其中“用小步反馈推动设计”的 TDD 基本结构,而不是把书中的全部实践等同于 AI-TDD。 ↩︎

  2. Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code。这里引用它来支撑“测试可维护性本身是一门工程实践”,尤其是 fixture、test double 和 test smell。 ↩︎

  3. Steve Freeman, Nat Pryce, Growing Object-Oriented Software, Guided by Tests。这里引用它来支撑“测试可以参与系统设计,而不只是事后验收”。 ↩︎

  4. Eirini Kalliamvakou, The Impact of AI on Developer Productivity: Evidence from GitHub Copilot,Microsoft Research。55.8% 是特定实验任务中的结果,不应外推为所有开发任务的通用提升。 ↩︎

  5. GitHub Blog, Survey: The AI wave continues to grow on software development teams。这里用来说明团队已经在尝试用 AI 生成测试用例,而不是证明生成测试天然可靠。 ↩︎

  6. Stack Overflow, 2025 Developer Survey。这里引用的是开发者对 AI agent 节省局部任务时间和提高个人生产率的自报告数据。 ↩︎

  7. Khalid El Haji, Carolin Brandt, Andy Zaidman, Using GitHub Copilot for Test Generation in Python: An Empirical Study。这里引用它来支撑“AI 生成测试高度依赖已有测试上下文,且需要人工修正”。 ↩︎

  8. Davide Falessi et al., Generative AI for Test Driven Development: Preliminary Results。这是一项早期实验,适合作为“GenAI 可以参与 TDD,但质量监督不能省”的边界证据。 ↩︎

  9. Yi Cui, Tests as Prompt: A Test-Driven-Development Benchmark for LLM Code Generation。这里引用它来支撑“测试同时可以作为 prompt 和 verification”的视角。 ↩︎

  10. DORA, Accelerate State of DevOps Report 2024。这里引用它来支撑“AI 提升个人层面体验和生产率,但对交付稳定性和吞吐有意外 tradeoff”的系统视角。 ↩︎

  11. DORA, Impact of Generative AI in Software Development。这里引用它来支撑“生成速度可能通过更大 batch size 压力传导到 review、测试和交付系统”的解释。 ↩︎

有些 Bug 很反直觉。

比如一个任务进度,明明已经在数据库里变成了完成状态,退出游戏再重新登录以后,又变回未完成。

更诡异的是,更新时间是新的,进度却是旧的。

这类问题最麻烦的地方在于:单看某一条链路,它都是对的;把几条链路放在一起,它就错了。

现象

这次问题的表现大概是这样:

  1. 玩家在游戏内做了一次兑换。
  2. 数据库里的任务记录一度变成了完成状态:progress=1status=CAN_CLAIM
  3. 玩家退出游戏后重新登录。
  4. 同一条任务又变回了未完成:progress=0status=INIT

如果只看兑换流程,会觉得它没有问题。兑换完成后,平台服务通知任务服务更新进度,数据库也确实写成功了。

真正的问题发生在退出游戏的时候。

三个模块各自都很合理

这个任务系统被拆在三个模块里:

模块 职责
平台服务 平台事件入口,比如支付、兑换这类不直接发生在游戏循环里的操作
任务服务 任务系统核心,负责 DB 读写、进度计算、任务刷新
游戏服务 游戏内实时任务进度,负责内存缓存、客户端推送、退出时 flush

它们的职责单独看都很自然:

  • 平台事件发生在平台服务,所以兑换后由平台服务通知任务服务。
  • 任务数据落在任务服务,所以最终由任务服务写数据库。
  • 游戏中需要实时显示任务进度,所以游戏服务会在玩家进游戏时加载任务进度到内存。

问题就出在最后一点:游戏服务里有一份任务进度缓存。

两条写路径

这个系统里,任务进度至少有两条更新路径。

第一条是游戏内事件。

例如射击、开宝箱、赚金币这种行为,本来就发生在游戏服里:

1
2
3
4
5
游戏内事件
-> 更新游戏服务内存缓存
-> 推送客户端任务进度变化
-> 退出或阶段性 flush
-> 任务服务写 DB

这条路径是自洽的。内存先更新,DB 后更新,客户端也能收到推送。

第二条是平台事件。

兑换不是游戏服自己完成的,而是平台服务处理的:

1
2
3
4
平台服务完成兑换
-> 通知任务服务更新进度
-> 任务服务按条件更新任务
-> 写 DB

这条路径也自洽。兑换成功后,任务服务把 DB 里的进度加上去。

但它少了一件事:没有通知游戏服务里的任务缓存。

脏缓存是怎么覆盖正确数据的

把时间线连起来,就能看到问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
玩家进入游戏
-> 游戏服务从任务服务加载任务进度
-> 缓存在游戏服务内存中
-> 此时 finishNum=0, status=INIT

玩家在游戏内兑换
-> 平台服务完成兑换
-> 任务服务写 DB
-> DB 变为 progress=1, status=CAN_CLAIM
-> 游戏服务缓存仍然是 finishNum=0, status=INIT

玩家退出游戏
-> 游戏服务 flush 玩家任务缓存
-> 把内存里的旧进度 flush 到任务服务
-> 任务服务用旧 progress/status 覆盖 DB

所以,数据库不是没有更新成功。

它是先被正确更新了,然后又被旧缓存覆盖了。

这就是这个问题最容易误判的地方:如果只查兑换那一刻的数据库,会看到正确结果;如果查退出后的数据库,会看到错误结果。中间缺的不是一次写入,而是一次来自旧状态的覆盖写入。这个形态和并发控制里常说的 lost update 很接近:不是没有写入,而是后来的旧快照把已经成立的新事实覆盖掉了。[1]

为什么更新时间是对的

这个现象也很迷惑:upd_time 看起来是对的,但 progressstatus 是错的。

原因在 SQL 写法里。

任务更新时,progressupd_timestatus 是一起 SET 的:

1
2
3
4
5
6
update task_progress
set progress = ?,
upd_time = ?,
status = ?
where role_id = ?
and task_id = ?

退出 flush 那条路径在写之前会先查一次 DB。此时 DB 里已经有新的 upd_time,但内存缓存里还是旧的进度和状态。

于是它写回去的是:

  • progress: 来自旧缓存,变回 0
  • status: 来自旧缓存,变回 INIT
  • upd_time: 来自刚查出来的 DB 记录,看起来还是新的

这就造成了一个很有迷惑性的现场:时间像是更新过,数据却像是回滚了。

它不是事务回滚,而是旧状态覆盖。

根因不是缓存,而是缓存的所有权不清楚

看到这里,很容易得出一个简单结论:缓存有问题。

但更准确地说,是这份缓存的所有权和写入边界不够清楚。

游戏服务里的任务缓存适合处理游戏内事件,因为这些事件天然发生在游戏服务,并且需要实时推送客户端。

但兑换任务不是游戏内事件。它的事实来源在平台服务,最终状态在任务服务,游戏服务只是曾经加载过一份旧快照。

如果一份缓存满足下面两个条件,它就很危险:

  1. 它不是事实来源。
  2. 它可以在退出、定时任务、补偿流程里反向覆盖事实来源。

这时候缓存就不只是缓存了,它变成了另一个写入者。Microsoft 的 Cache-Aside pattern 文档也明确提醒:cache-aside 本身并不保证缓存和数据源一致,外部进程更新数据源时,缓存要等重新加载或失效后才会反映变化。[2]

而多个写入者最怕的,就是每个人都以为自己手上的状态是最新的。

修复为什么不是加一次同步

直觉上的修复是:兑换后通知游戏服务,把任务缓存也更新掉。

这当然是一种方案,但它会引入新的问题:

  • 要给游戏服务增加更新任务进度的 RPC 能力。
  • 要考虑玩家是否在线、在哪个游戏服、通知失败怎么办。
  • 要保证 DB 更新和游戏服务缓存更新之间的失败补偿。
  • 以后所有平台任务都要想清楚要不要同步游戏服务缓存。

这不是不能做,而是成本更高。AWS 对 cache-aside 和 write-through 的区分可以解释这里的取舍:如果选择 write-through,应用就要在更新主数据库后同步维护缓存;如果选择 cache-aside,就要承认缓存是按需加载的副本,并明确它的失效和回源机制。[3]

这次采用的是更小的修复:不要把兑换任务加载到游戏服务缓存里。

原因很简单:兑换任务不需要游戏内实时追踪。

玩家做兑换时,事实发生在平台服务;任务完成状态也由任务服务写 DB。它不需要由游戏服内存来参与生命周期。

所以把兑换任务加入一个“非游戏内追踪任务”的排除列表:

1
2
3
4
5
6
7
private static final int EXCHANGE_TASK_ID = 12;

public static final Set<Integer> NON_GAMEPLAY_TASK_IDS = set(
BIND_PHONE_TASK_ID,
DAILY_RECHARGE_TASK_ID,
EXCHANGE_TASK_ID
);

这样玩家进入游戏时,这类任务不会进入游戏服务的内存缓存:

1
2
3
4
不加载到游戏服务
-> 不产生游戏服务缓存
-> 退出时不 flush
-> 不会用旧缓存覆盖 DB

这个修复的关键不是“少写一段同步代码”,而是重新划清了任务的归属。

游戏内任务交给游戏服务缓存追踪。

平台任务不要进入游戏服务缓存。

什么时候应该让缓存参与写入

这次问题给我的提醒是:不要只问“要不要缓存”,还要问“缓存有没有资格写回”。

如果缓存只是读优化,它失效或过期最多影响性能。

如果缓存会写回数据库,它就进入了 缓存一致性 的核心路径。此时至少要回答几个问题:

  1. 缓存里的数据是谁更新的?
  2. 还有没有其他路径会更新同一份 DB 数据?
  3. 缓存写回时,是覆盖写、增量写,还是带版本判断的写?
  4. 如果 DB 已经被其他路径更新,缓存写回应该跳过、合并,还是报错?
  5. 哪些业务根本不应该进入这份缓存?

这几个问题里,最后一个经常被忽略。乐观并发控制给出的思路是:保存前比较“我当初读到的版本”和“数据库当前版本”,如果已经变化,就不要静默覆盖,而是进入冲突处理。EF Core 的 concurrency token 和 Fowler 的 Optimistic Offline Lock 都是在解决这类“旧副本写回”问题。[4]

很多时候,最稳的修复不是让所有状态同步到所有地方,而是减少不该拥有状态的地方。

小结

这个 Bug 表面上是“任务进度倒退”,本质上是两条写路径竞争同一行数据:

1
2
3
平台服务路径:写入正确进度
游戏服务路径:退出时 flush 旧缓存
最终结果:旧缓存覆盖新 DB

它提醒我一件事:

缓存不是只要命中率高就算设计完成。只要缓存能写回,它就是一个数据作者。

一个系统里可以有多个读者,但最好不要有太多自以为拥有事实的作者。

参考与延伸阅读


  1. Martin Fowler, Optimistic Offline Lock。这里借用的是“检测并阻止并发业务事务互相覆盖”的思路,不是说这个事故一定发生在 ORM 或长事务场景。 ↩︎

  2. Microsoft Azure Architecture Center, Cache-Aside pattern。这里引用它来支撑“cache-aside 不自动保证缓存和数据源一致,外部写路径会让缓存变旧”的判断。 ↩︎

  3. AWS, Caching patterns - Database Caching Strategies Using Redis。这里引用它来区分 cache-aside / lazy loading 和 write-through:前者按需填充,后者要求应用在主库更新后主动维护缓存。 ↩︎

  4. Microsoft Learn, Handling Concurrency Conflicts - EF Core;Martin Fowler, Optimistic Offline Lock。这里引用它们来支撑“旧快照写回应带版本判断或冲突处理,而不是无条件覆盖”的设计原则。 ↩︎

前面两篇文章分别谈了程序员的职责和 GPT 的能力边界:代码、工具与人:AI 时代的程序员,以及 GPT 有智能,但不是责任主体

这一篇继续往下走,谈一个更具体的问题:日常工作里,我到底应该怎样使用 AI?

我现在越来越觉得,AI 不是靠一次提示词用好的。

提示词当然重要。但真正决定效果的,往往不是某一句话写得多漂亮,而是 AI 被放进了什么样的工作方式里:什么时候先澄清,什么时候先读上下文,什么时候可以动手,什么时候必须验证,什么时候应该把结论写回文档。OpenAI 在谈 Codex 的 agent-first 实践时,把工程师的角色描述为设计环境、指定意图和建立反馈循环;Anthropic 在讨论 agentic systems 时,也区分了预定义路径的 workflow 和由模型动态决定步骤的 agent。[1][2]

也就是说,问题不是“怎么让 AI 回答得更像一个聪明人”,而是“怎么让 AI 参与一个可靠的工作流”。

不要只和 AI 聊天

很多时候,我们会反复对 AI 说同样的东西:先理解上下文,不要急着改;修 bug 前先复现;写完以后要验证;不要随便宣布完成;重要结论要沉淀到文档里。

如果每次都靠临时提醒,这件事很容易失效。因为对话会变长,上下文会变脏,人的注意力也会松。AI 可能这次听进去了,下次又回到它更熟悉的生成模式里:先给一个看起来合理的答案。

所以我把这些重复要求整理成了 skill:https://cnb.cool/ryuu64/skills。

skill 对我来说,不是“更长的提示词”,而是一种协作协议。它把那些反复出现的工程判断写成可复用流程,让 AI 知道在什么场景下该采用什么工作方式。这个判断也和 Codex、Claude Code 的官方建议相近:稳定的项目偏好应该进入 AGENTS.md、skill 或项目文档,而不是每次都靠临时 prompt 重复。[3][4]

先决定当前阶段

AI 很擅长生成内容,但它不一定天然知道什么时候该停下来理解上下文,什么时候该先验证假设,什么时候该把结论写回文档。

所以我更倾向于先判断当前需求处在哪个阶段。对复杂、模糊或多步骤任务,先 plan 再动手,比直接进入实现更稳;对简单任务,则没有必要把流程做重。[5]

当我还没有想清楚方案时,grill-then-docs 对我很重要。它先用 grill 的方式把一个决策点问透:有哪些选项,各自的代价是什么,推荐哪一个,为什么。等结论稳定以后,再把这些结论写回项目文档。这样 AI 不只是陪我聊了一轮方案,而是把这轮讨论变成项目之后还能复用的上下文。

比如看不懂一段代码时,我不希望 AI 直接解释当前文件,而是先使用 zoom-out 拉高一层,弄清楚它在系统里的位置:上游是谁、下游是谁、核心概念是什么、下一步应该读哪里。Claude Code 的常见工作流里也建议先从 broad questions 开始,再逐步深入具体组件和执行路径。[6] 这样后面的修改才不会只盯着局部。

修 bug 时,我会更希望它进入 diagnose 的流程:先建立反馈循环,再复现问题,然后提出可证伪的假设,用日志、测试或调试器去验证。很多 bug 真正难的地方不在改动本身,而在确认自己没有修错方向。Anthropic 的 coding agent 讨论也指出,代码任务之所以适合 agent,很大一部分原因在于代码可以通过自动化测试验证,agent 可以用测试结果迭代。[7]

如果是在实现一个明确的新行为,我会倾向于用 tdd。先列业务规则、边界条件和验收范围,再写失败测试,再写最小实现。AI 可以很快生成代码,但测试会把它拉回到具体行为上,不让它只写出一段“看起来像那么回事”的实现。

任务做完以后,我还会要求 verification-before-completion。AI 不能只说“已经完成”,而要先运行对应的验证命令,读输出,再根据结果判断能不能做完成断言。这个 skill 对我很重要,因为 AI 很容易在语气上显得很确定,但工程上真正需要的是证据。Codex 的最佳实践也把测试、lint、type check、行为确认和 diff review 放在可靠性收口里。[8]

如果过程中形成了稳定结论,比如某个模块的约束、某种排障方法、某个架构决策,我会再用 project-docs-syncproject-docs-writing 把它同步回项目文档。否则这次对话结束以后,经验又散掉了,下次还要重新解释。OpenAI 在 Harness Engineering 里把 repository knowledge 作为 system of record,并把短入口文件、结构化文档和渐进式披露放在一起使用;这其实是在解决同一个问题:让 AI 能从项目内部重新找到稳定上下文。[9]



flowchart TD
    A[遇到需求] --> B{当前最需要澄清什么}
    B -->|方案还不清楚| K[grill-then-docs<br/>先问透决策,再写回文档]
    B -->|不熟悉代码| C[zoom-out<br/>先建立上下文地图]
    B -->|排查 bug| D[diagnose<br/>复现、假设、验证、收敛根因]
    B -->|实现新行为| E[tdd<br/>测试先行,按行为推进]
    K --> F
    C --> F[修改或继续分析]
    D --> F
    E --> F
    F --> G[verification-before-completion<br/>用新鲜证据确认结果]
    G --> H{形成稳定结论?}
    H -->|是| I[project-docs-sync / project-docs-writing<br/>写回项目文档]
    H -->|否| J[结束任务]
    I --> J

这些流程看起来琐碎,但它们决定了 AI 是在帮你推进工程,还是只是在生成一堆看起来合理的文本。

处理 AI 的失控

把 AI 放进工作流,并不是因为 AI 不会做事。恰恰相反,是因为它太容易流畅地做事,甚至在方向不对的时候也显得很有把握。

上下文污染

我使用 AI 的过程中遇到过不少问题。最常见的一类,是上下文变得太长、太杂以后,AI 开始不理解业务。它会抓住对话里某个局部信息,然后沿着一个不对的方向继续推理,甚至越解释越偏。

这种时候,继续在原来的对话里纠正,不一定是最好的办法。因为问题有时不在某一句提示词,而在整个上下文已经被污染了。OpenAI 对“大 AGENTS.md”的反思很有代表性:上下文是稀缺资源,太多指导反而会挤掉任务、代码和真正相关的文档。[10] 我会选择新开一个对话,或者先清理上下文,再把真正重要的背景、约束和当前目标重新说清楚。如果项目里已经有文档,就让 AI 先看文档,而不是继续依赖一段越来越长的聊天记录。

调试时跳过证据

另一类问题出现在 debug 时。AI 很容易想直接根据代码猜原因,然后给出一个修复方案。这个过程看起来很快,但风险很高,因为它可能只是在修一个“看起来像问题”的地方。我遇到过 AI 死活不肯先插桩、不肯根据日志复现,而是反复尝试直接改代码。

后来我用 diagnose 去约束这个过程,并且反复调整它的规则:先建立反馈循环,先复现,再提出假设;需要日志就插有明确目的的日志;每次观察结果以后更新假设,而不是直接跳到修复。这样做会慢一点,但它能把 AI 从“猜一个答案”拉回到“拿证据收敛问题”。这也是人在环(Human-in-the-Loop)真正有价值的地方:人不只是最后点确认,而是在高不确定、高风险或缺证据的位置把流程停住。[11]

混淆计划和事实

还有一类更隐蔽的问题:AI 看起来像是知道自己在干什么,但实际上它可能只是选择了一个“看起来合理”的位置。

比如我的 grill-then-docs,它的目标是先把方案问清楚,再把稳定结论沉淀下来。可在一开始,AI 经常会把 grill 之后“接下来要做什么”写进 ./docs/features./docs/architecture。从表面看,这很合理:方案讨论和功能、架构都有关。但实际上这是错误的,因为 grill 后续要做的事情只是计划,不是已经成立的功能事实,也不是架构事实。

它应该先进入 ./docs/plan,等实现完成、结论稳定以后,才有资格进入 features 或 architecture。

这个错误很有代表性。AI 能根据文字相似性把内容放到看起来相关的地方,但“语义相关”不等于“事实身份正确”。一个计划即使和架构有关,也仍然只是计划;一个候选方案即使描述得很完整,也还不是系统事实。Microsoft 的 Human-AI Interaction Guidelines 也提醒我们,AI 系统需要让用户能理解、纠正和恢复,而不是把一个看似合理的输出直接当作事实。[12]

这背后更完整的能力边界,我在 GPT 有智能,但不是责任主体 里单独写过。放到工作流里看,解决办法不是指望 AI 自己突然理解项目知识治理,而是把规则明确写进去:grill 之后形成的后续计划,应该写到 /docs/plan,不能直接写进 /docs/features/docs/architecture

这不是一个简单的目录偏好,而是在告诉 AI:什么东西还只是计划,什么东西才可以被当作项目事实。

把偏好变成约束

使用 AI 写代码时,很多问题不是 AI 不会,而是它不知道你在意什么。

你在意命名是否贴合领域,注释是否解释意图而不是复述代码,提交信息是否清楚,文档是否跟着代码一起更新,这些偏好如果每次都临时说明,就很容易遗漏。

skill 的价值在于把这些偏好变成稳定约束。它不是让 AI 替我做判断,而是让 AI 在我设定的边界里工作。更准确地说,skill 管的是“在某类任务里如何行动”,而不是“把人的责任外包出去”。

这也是为什么我不太愿意把这件事叫作“写提示词”。提示词更像一次性的表达,工作流则会问另一组问题:

  • 这个需求现在处在哪个阶段?
  • AI 应该先读什么上下文?
  • 什么情况下可以动手?
  • 什么结果可以算完成?
  • 需要什么证据才能宣布完成?
  • 哪些结论应该写回文档?

这些问题比一句漂亮提示词更重要。

让 AI 更像协作者

skill 是一种协作协议。它让 AI 知道在什么场景下该采用什么工作方式,也让我不用每次都从零开始解释自己的工程习惯。

这也对应了前面两篇文章的结论:AI 可以加速表达、生成和试探,但程序员仍然要负责判断、边界和验证。把 AI 放进工作流,就是把这些责任拆成更清晰的流程,让 AI 更稳定地参与其中。

好的 AI 使用方式,不是把人从流程里拿掉,而是让人更清楚地站在该负责的位置上。

参考与延伸阅读

  • OpenAI, Harness engineering:适合理解为什么 AI coding 的关键不只是提示词,而是环境、工具、反馈和知识系统。
  • OpenAI Developers, Codex best practices:适合理解 AGENTS.md、plan-first、测试验证和 review 如何进入日常工作流。
  • Anthropic, Building Effective AI Agents:适合理解 workflow、agent、自主性、测试反馈和 guardrails 的边界。
  • Anthropic Docs, Claude Code common workflows:适合把“看代码、修 bug、写测试、写文档”拆成可执行工作流。
  • Anthropic Docs, Extend Claude with skills:适合理解 skill 为什么应该聚焦入口和按需加载,而不是变成大而全说明书。
  • Microsoft Research, Guidelines for Human-AI Interaction:适合理解人在 AI 系统中的控制、纠错和恢复位置。

  1. OpenAI, Harness engineering: leveraging Codex in an agent-first world。这里引用它来支撑“AI 使用效果取决于环境、意图、工具和反馈循环,而不只是 prompt”的判断。 ↩︎

  2. Anthropic, Building Effective AI Agents。Anthropic 区分 workflow 与 agent,并提醒只在复杂度确实需要时提升自主性。 ↩︎

  3. OpenAI Developers, Codex best practices。这里引用的是“把重复指导沉淀到 AGENTS.md、skill、配置和自动化工作流”的实践建议。 ↩︎

  4. Anthropic Docs, Extend Claude with skills。Claude Code 的 skill 文档强调 SKILL.md 保持聚焦,细节放入按需加载的 supporting files。 ↩︎

  5. OpenAI Developers, Codex best practices: Plan first for difficult tasks。这里引用它来支撑复杂任务先规划、再执行的工作流。 ↩︎

  6. Anthropic Docs, Claude Code common workflows。其中“理解代码库”和“查找相关代码”的建议都强调从广到窄地建立上下文。 ↩︎

  7. Anthropic, Building Effective AI Agents。这里引用的是 coding agents 适合用测试反馈迭代的判断。 ↩︎

  8. OpenAI Developers, Codex best practices: Improve reliability with testing and review。这里引用它来支撑“完成断言必须绑定测试、检查、行为确认和 review”。 ↩︎

  9. OpenAI, Harness engineering。这里引用的是“repository knowledge as the system of record”和渐进式披露的实践。 ↩︎

  10. OpenAI, Harness engineering。这里引用的是“大型指令文件会挤占任务、代码和相关文档”的上下文管理经验。 ↩︎

  11. 参见 Microsoft Research, Guidelines for Human-AI Interaction;以及 NIST, AI Risk Management Framework。这里引用它们来支撑“人工介入点需要上下文、控制、恢复和责任边界”的说法。 ↩︎

  12. Microsoft Research, Guidelines for Human-AI Interaction。这份 CHI 2019 论文提出并验证了 18 条 Human-AI Interaction 设计指南,适合支撑“AI 输出需要可理解、可纠正、可恢复”的边界。 ↩︎

讨论 GPT 这样的 AI 时,我们很容易陷入两个极端。

一种说法是:它只是概率接龙,根本不理解,也没有智能。另一种说法是:它已经表现得像一个人,所以它当然理解,也当然能判断。

我觉得这两种说法都太快了。GPT 确实不是传统意义上的“查表机器”,它能在语言、代码、概念和任务之间建立复杂关系,也能解决许多过去需要人类经验才能处理的问题。但它的理解不是人的理解,它的判断也不是责任主体的判断。

这个判断不只是使用体验。关于大语言模型是否“理解”,学界本来就有持续争论。Mitchell 和 Krakauer 在关于 LLM 理解能力的综述中,把问题放在“不同理解模式”的层面讨论;Bender 和 Koller 则提醒我们区分语言形式和语言意义,不能因为模型能处理形式,就直接推断它拥有人的意义理解。[1] 另一边,GPT-3、GPT-4 相关研究又确实显示,规模化语言模型能在许多任务上表现出强大的迁移和问题求解能力。[2]

所以,真正值得问的不是“GPT 到底有没有理解”,而是:我们说一个东西理解了某件事时,到底在说什么?

什么叫理解

理解不是一个单层概念。

如果把理解定义得很窄,等同于“能不能在文本中建立结构关系并给出有效解释”,那 GPT 显然有某种理解能力。如果把理解定义得更接近人,要求它把语言和现实经验、行动后果、社会责任连接起来,那它又明显不具备人的理解。这也是为什么我更愿意把问题拆开,而不是直接回答“有”或“没有”。

最低限度的理解,是能建立关系。比如看到一个错误日志,知道它可能和某个配置、某个 API 用法、某个版本变化有关;看到一段代码,知道变量、函数、调用链之间大致是什么关系。GPT 在这个层面上很强,它能从大量文本和代码中学到模式,并在新的上下文里组合这些模式。

更进一步的理解,是能放进上下文里解释。不是只知道“这段代码是什么”,而是知道它为什么在这里,受什么约束,和系统里其他部分有什么关系。如果给 GPT 足够上下文,它也能在这个层面表现得不错。它可以解释一段业务逻辑,可以比较几个方案,可以指出某个实现可能不符合当前架构。

但人的理解还不止这些。

人理解一件事,往往还意味着知道它在现实中的位置。这个需求为什么存在,谁会使用它,错了会造成什么后果,哪些事实已经成立,哪些还只是计划,哪些判断需要承担责任。这些东西不只是文本关系,也不只是概念关系,它们和现实经验、组织规则、项目状态、责任分配有关。

GPT 可以在文本里模拟这些关系,也可以根据你给出的规则做出候选判断。但它并不生活在这个项目里,不承担这个系统上线后的后果,也不会因为一次错误分类让团队文档长期混乱。它可以说“这看起来像架构决策”,但它不能替你确认这是否已经成为项目事实。

这就是我说的:GPT 有理解能力,但它的理解不是人的理解。

什么叫判断

判断也要分层看。

有些判断是规则型的。比如这段代码有没有语法错误,某个测试是否通过,一个接口返回值是否符合约定。只要规则足够明确,GPT 可以很好地辅助这些判断,甚至可以帮你生成验证方式。

但“能给出判断”不等于“能稳定知道自己判断是否正确”。TruthfulQA 这类研究说明,模型可能会复述训练语料中的常见错误;关于模型校准的研究也显示,模型在特定格式下能估计自己答案的可信度,但这种能力会随任务和格式变化而漂移。OpenAI 关于幻觉的研究也指出,很多训练和评测方式会奖励猜测,而不是奖励承认不确定。[3]

有些判断是基于事实状态的。比如一段文档应该放到 docs/featuresdocs/architecture 还是 docs/plan。这表面上像是分类问题,实际上是在判断这段内容到底是什么:它是已经实现的功能事实,还是尚未执行的计划?是已经被系统采纳的架构约束,还是一次讨论中的候选方案?

GPT 容易在这里出错。它可能会根据语义相关性做选择:这段话和功能有关,就放到 features;这段话和架构有关,就放到 architecture。但对项目来说,真正重要的不是“相关”,而是“身份正确”。一个计划即使和架构有关,也仍然只是计划;一个候选方案即使描述得很完整,也还不是系统事实。

这类判断需要事实依据,也需要责任主体。GPT 可以提出候选结论,但不能替程序员确认事实状态。

还有一类判断是价值取舍。比如为了可维护性牺牲一点性能是否值得,为了交付速度临时接受一个技术债是否合适,一个设计是否符合团队长期方向。这些判断并不只是事实题,也不只是偏好题,而是事实、约束、目标和责任放在一起之后的选择。

这类判断在 AI 研究里也很微妙。关于 ChatGPT 道德判断的实验显示,它的回答可能和人类判断不一致,也会受到提示方式、选项呈现和模型差异影响。也就是说,AI 可以生成看起来像价值判断的文本,但这不等于它成为了一个能稳定承担判断后果的价值主体。[4]

GPT 可以参与讨论,可以列选项,可以指出代价。但最后做判断的人,仍然必须是人。

什么叫智能

如果把智能理解为解决问题的能力,那么 GPT 显然表现出了某种真实的机器智能。

它能写代码、解释代码、翻译文本、总结资料、生成测试、提出方案、辅助排障。它不是简单地复制训练材料,而是在新上下文里组合已有模式,形成有用输出。把这些能力说成“完全没有智能”,并不符合我们的实际体验。

这也能从研究脉络里看到。GPT-3 论文展示了大模型在少样本设置下的跨任务能力;“Sparks of AGI” 一文则记录了早期 GPT-4 在数学、代码、视觉、医学、法律等多种任务上的能力跃迁。无论是否接受作者关于 AGI 的判断,至少可以说明:把 GPT 只说成“完全没有智能的文字拼接器”,已经很难解释它在许多任务上的表现。[2:1]

但如果把智能理解为人的完整智能,那 GPT 又明显不够。

人类智能包含目标、经验、行动、反馈、责任、价值感和现实处境。人不是只在文本里做下一步预测,而是在世界里行动,并承受行动带来的后果。程序员写错代码会面对故障、用户、团队、维护成本和长期演进;GPT 不会。

这也是许多治理和出版规则强调人的原因。比如 Springer Nature 的 AI 政策明确不把 LLM 列为作者,理由不是它不能生成文字,而是作者身份意味着对作品负责,而这种责任不能有效适用于 LLM。NIST 的 AI 风险管理框架也把 AI 风险放在组织治理、度量、管理和责任结构中,而不是把责任交给模型本身。[5]

所以,更准确的说法是:GPT 有机器智能,但它不是完整的人类智能。它能在很多任务上表现得聪明,却不是一个能替你承担责任的主体。

为什么这对程序员重要

这个区分不只是哲学问题,它会直接影响我们怎么使用 AI。

如果你觉得 GPT 完全不理解,你可能会低估它,把它只当成补全工具,错过它在信息整理、方案推演、代码理解和排障辅助上的价值。

如果你觉得 GPT 像人一样理解,你又可能会高估它,把事实确认、架构判断、业务取舍和完成断言交给它。这样做的风险更大,因为 GPT 的输出常常很流畅,很像知道自己在干什么。

更稳妥的位置是:承认它有能力,但不把责任交给它。

GPT 可以帮我们更快建立问题地图,更快生成草稿,更快提出候选方案,更快组织信息。但程序员仍然要负责确认事实、判断边界、验证结果,并承担系统后果。

AI 时代的程序员,不应该把自己退化成“审稿的人”,也不应该假装 AI 只是一个高级自动补全。更好的关系是:让 AI 参与思考过程,但由人保留判断权;让 AI 加速表达和试探,但由人确认什么可以成为事实。

所以我更愿意这样总结:

GPT 有智能,但不是责任主体。它可以帮助我们理解问题,却不能替我们成为那个必须负责的人。

这也是为什么我更倾向于 把 AI 放进工作流,而不是把判断权交给它。

参考与延伸阅读


  1. Melanie Mitchell and David C. Krakauer, The debate over understanding in AI’s large language models;Emily M. Bender and Alexander Koller, Climbing towards NLU: On Meaning, Form, and Understanding in the Age of Data。这里引用它们来支撑“LLM 是否理解不是单层问题,语言形式和人的意义理解需要区分”。 ↩︎

  2. Tom B. Brown et al., Language Models are Few-Shot Learners;Sébastien Bubeck et al., Sparks of Artificial General Intelligence: Early experiments with GPT-4。这里引用它们来支撑“规模化语言模型确实展现出跨任务能力”,但不把这些能力直接等同于人的完整智能。 ↩︎ ↩︎

  3. Stephanie Lin, Jacob Hilton, Owain Evans, TruthfulQA: Measuring How Models Mimic Human Falsehoods;Saurav Kadavath et al., Language Models (Mostly) Know What They Know;OpenAI, Why language models hallucinate。这里引用它们来支撑“会回答、回答真实、知道自己是否可靠,是不同层次的能力”。 ↩︎

  4. ChatGPT does not replicate human moral judgments。这里引用它来支撑“AI 可以生成道德/价值判断文本,但不等于稳定复制人类判断或成为价值责任主体”。 ↩︎

  5. Springer Nature, AI policy;NIST, AI Risk Management Framework。这里引用它们来支撑“AI 输出能力和责任主体资格需要区分,责任应落在可治理的人或组织结构上”。 ↩︎

引子

很多人喜欢问 AI 会不会取代程序员?然而这个问题问得太急了。它默认了一件事:程序员的核心工作就是写代码。

这个默认前提,值得先停下来看看。

编码只是职责之一

有没有 AI,编码都只是程序员职责的一部分。

这就像会计并不是“制表的人”。制表当然是会计工作中很重要的一环,但会计真正负责的是账务是否准确、规则是否被遵守、风险是否被识别,以及经营事实是否被可靠地表达出来。表格只是承载这些判断与结果的工具。

程序员也是如此。代码很重要,但代码不是工作的全部。程序员真正负责的是理解问题、建立模型、拆分边界、识别约束、设计系统行为,并让这个系统在现实环境中可运行、可维护、可验证。

AI 让编码这件事变得更快,也让“写出代码”这件事显得没有过去那么稀缺。但这并不意味着程序员的责任消失了。相反,它会把程序员的价值更清楚地推回到那些更难外包给工具的部分:判断该不该做,决定怎么做,验证做得对不对,以及承担系统长期演进的后果。

我们应该使用 AI 写代码吗

如果一个工具能够帮助我们更快地表达想法、查找资料、生成样例、整理思路、补全代码,那么程序员没有理由拒绝它。拒绝 AI 本身,并不会让一个人变得更专业。就像拒绝 IDE、调试器、搜索引擎,也不会让人更懂软件工程。

但使用 AI 写代码,并不意味着把程序员的责任交给 AI。AI 可以生成代码,但它不会替你理解业务目标;它可以给出方案,但它不会替你承担系统后果;它可以补全实现,但它不会自动知道这个实现是否适合当前上下文。

AI 更像是一个能力放大器。你想得清楚,它可以放大你的效率;你想得模糊,它也会放大你的混乱。它能让正确的方向更快落地,也能让错误的方向更快堆积成代码。

所以问题不在于“要不要用 AI 写代码”,而在于“用 AI 写代码时,程序员是否仍然掌握判断权”。

AI 时代应该如何写代码

AI 时代写代码,最重要的变化不是少写几行代码,而是写代码之前和之后的工作变得更重要了。

过去我们常常把注意力放在“怎么实现”上:接口怎么写,循环怎么写,数据结构怎么组织,异常怎么处理。这些问题当然仍然重要,但 AI 已经能在很多局部实现上提供不错的帮助。真正容易出问题的地方,反而是那些 AI 不一定知道、也不一定能替你判断的部分。

首先,要先把问题说清楚。不要一上来就让 AI 写代码,而是先描述目标、约束、输入输出、边界条件和不能破坏的现有行为。问题描述得越清楚,AI 给出的代码越可能接近你真正需要的东西。程序员在这里做的不是“下命令”,而是在整理自己的判断。

其次,要把 AI 的输出当作草稿,而不是答案。AI 生成的代码可以作为一个起点,但它需要被阅读、质疑和验证。变量名是否表达了业务含义,抽象是否过度,边界条件是否遗漏,错误处理是否符合系统习惯,这些都需要程序员重新接管。

再次,要用测试和运行结果约束 AI。只靠“看起来对”是不够的。越是借助 AI 快速生成代码,越应该补上可执行的验证:单元测试、集成测试、日志、断言,或者最小可复现的运行结果。AI 可以帮你写测试,但测试要覆盖什么,仍然取决于你对问题的理解。

最后,要保持对上下文的敏感。很多代码不是孤立存在的,它会受到项目风格、历史包袱、性能要求、部署环境、团队习惯和业务规则的影响。AI 很容易给出一个局部上漂亮、整体上不合适的方案。程序员需要判断这段代码能不能放进当前系统,而不仅仅是能不能单独运行。

所以,AI 时代写代码,不是把“思考”交给 AI,然后自己负责复制粘贴。更好的方式是:人负责定义问题、拆分边界、判断取舍和验证结果,AI 负责加速搜索、表达、生成和试探。这样使用 AI,代码会写得更快,但责任仍然留在应该承担责任的人那里。

我使用 AI 的经验

很多时候,我们会反复对 AI 说同样的东西:先理解上下文,不要急着改;修 bug 前先复现;写完以后要验证;不要随便宣布完成;重要结论要沉淀到文档里。我把这些重复要求整理成了 skill:https://cnb.cool/ryuu64/skills。

形成标准流程

AI 很擅长生成内容,但它不一定天然知道什么时候该停下来理解上下文,什么时候该先验证假设,什么时候该把结论写回文档。skill 的作用,就是把这些流程显式写出来。

当我还没有想清楚方案时,grill-then-docs 对我很重要。它先用 grill 的方式把一个决策点问透:有哪些选项,各自的代价是什么,推荐哪一个,为什么。等结论稳定以后,再把这些结论写回项目文档。这样 AI 不只是陪我聊了一轮方案,而是把这轮讨论变成项目之后还能复用的上下文。

比如看不懂一段代码时,我不希望 AI 直接解释当前文件,而是先使用 zoom-out 拉高一层,弄清楚它在系统里的位置:上游是谁、下游是谁、核心概念是什么、下一步应该读哪里。这样后面的修改才不会只盯着局部。

修 bug 时,我会更希望它进入 diagnose 的流程:先建立反馈循环,再复现问题,然后提出可证伪的假设,用日志、测试或调试器去验证。很多 bug 真正难的地方不在改动本身,而在确认自己没有修错方向。

如果是在实现一个明确的新行为,我会倾向于用 tdd。先列业务规则、边界条件和验收范围,再写失败测试,再写最小实现。AI 可以很快生成代码,但测试会把它拉回到具体行为上,不让它只写出一段“看起来像那么回事”的实现。

任务做完以后,我还会要求 verification-before-completion。AI 不能只说“已经完成”,而要先运行对应的验证命令,读输出,再根据结果判断能不能做完成断言。这个 skill 对我很重要,因为 AI 很容易在语气上显得很确定,但工程上真正需要的是证据。

如果过程中形成了稳定结论,比如某个模块的约束、某种排障方法、某个架构决策,我会再用 project-docs-syncproject-docs-writing 把它同步回项目文档。否则这次对话结束以后,经验又散掉了,下次还要重新解释。



flowchart TD
    A[遇到需求] --> B{当前最需要澄清什么}
    B -->|方案还不清楚| K[grill-then-docs<br/>先问透决策,再写回文档]
    B -->|不熟悉代码| C[zoom-out<br/>先建立上下文地图]
    B -->|排查 bug| D[diagnose<br/>复现、假设、验证、收敛根因]
    B -->|实现新行为| E[tdd<br/>测试先行,按行为推进]
    K --> F
    C --> F[修改或继续分析]
    D --> F
    E --> F
    F --> G[verification-before-completion<br/>用新鲜证据确认结果]
    G --> H{形成稳定结论?}
    H -->|是| I[project-docs-sync / project-docs-writing<br/>写回项目文档]
    H -->|否| J[结束任务]
    I --> J

这些流程看起来琐碎,但它们决定了 AI 是在帮你推进工程,还是只是在生成一堆看起来合理的文本。

处理 AI 的失控

上下文污染

我使用 AI 的过程中也遇到过不少问题。最常见的一类,是上下文变得太长、太杂以后,AI 开始不理解业务。它会抓住对话里某个局部信息,然后沿着一个不对的方向继续推理,甚至越解释越偏。

这种时候,继续在原来的对话里纠正,不一定是最好的办法。因为问题有时不在某一句提示词,而在整个上下文已经被污染了。我会选择新开一个对话,或者先清理上下文,再把真正重要的背景、约束和当前目标重新说清楚。如果项目里已经有文档,就让 AI 先看文档,而不是继续依赖一段越来越长的聊天记录。

调试时跳过证据

另一类问题出现在 debug 时。AI 很容易想直接根据代码猜原因,然后给出一个修复方案。这个过程看起来很快,但风险很高,因为它可能只是在修一个“看起来像问题”的地方。我遇到过 AI 死活不肯先插桩、不肯根据日志复现,而是反复尝试直接改代码。

后来我用 diagnose 去约束这个过程,并且反复调整它的规则:先建立反馈循环,先复现,再提出假设;需要日志就插有明确目的的日志;每次观察结果以后更新假设,而不是直接跳到修复。这样做会慢一点,但它能把 AI 从“猜一个答案”拉回到“拿证据收敛问题”。

混淆计划和事实

还有一类更隐蔽的问题:AI 看起来像是知道自己在干什么,但实际上它可能只是选择了一个“看起来合理”的位置。

比如我的 grill-then-docs,它的目标是先把方案问清楚,再把稳定结论沉淀下来。可在一开始,AI 经常会把 grill 之后“接下来要做什么”写进 ./docs/features./docs/architecture。从表面看,这很合理:方案讨论和功能、架构都有关。但实际上这是错误的,因为 grill 后续要做的事情只是计划,不是已经成立的功能事实,也不是架构事实。它应该先进入 ./docs/plan,等实现完成、结论稳定以后,才有资格进入 features 或 architecture。

这个错误很有代表性。AI 并不一定知道“计划”和“事实”之间的价值差异。它能根据文本相似性判断某段内容和哪个目录更相关,却未必能判断这段内容在项目知识体系里的身份:它是待办、假设、决策、事实,还是长期约束。对程序员来说,这些边界很重要;对 AI 来说,如果没有明确规则,它可能只会选择概率上最像的地方。

这里说的价值判断,不是“我更喜欢哪种写法”这种偏好判断,而是基于事实状态做定性:这段内容现在到底是什么。它是已经实现的功能事实,还是尚未执行的计划?是已经被系统采纳的架构约束,还是一次讨论中的候选方案?这些判断依赖项目当前状态,也依赖团队对文档的定义。

GPT 可以根据文字相似性把内容放到看起来相关的地方,也可以在规则足够明确时照着规则执行。但如果规则没有把这些事实身份说清楚,它就很容易把“语义相关”误当成“事实身份正确”。./docs/features./docs/architecture./docs/plan 的区别,本质上不是路径选择,而是项目知识治理中的事实判断:什么已经成立,什么还只是计划,什么可以作为长期约束沉淀下来。

所以问题不在于 AI 能不能给出一个看似合理的分类,而在于它有没有足够事实依据做这个分类,以及分类标准是否被明确表达。对程序员来说,“这个内容应该写到哪里”背后其实是在判断它在项目中的状态;对 AI 来说,如果没有明确规则,它可能只是选择概率上最像的目录。

所以我后来在 skill 里明确约束:grill 之后形成的后续计划,应该写到 /docs/plan,不能直接写进 /docs/features/docs/architecture。这不是一个简单的目录偏好,而是在告诉 AI:什么东西还只是计划,什么东西才可以被当作项目事实。

这些问题也让我更确定一件事:AI 不是一直稳定的。上下文、提示方式、任务阶段都会影响它的表现。程序员需要识别什么时候该继续推进,什么时候该停下来重置上下文,什么时候该强制它回到验证流程。

把偏好变成约束

使用 AI 写代码时,很多问题不是 AI 不会,而是它不知道你在意什么。你在意命名是否贴合领域,注释是否解释意图而不是复述代码,提交信息是否清楚,文档是否跟着代码一起更新,这些偏好如果每次都临时说明,就很容易遗漏。

skill 的价值在于把这些偏好变成稳定约束。它不是让 AI 替我做判断,而是让 AI 在我设定的边界里工作。

让 AI 更像协作者

skill 是一种协作协议。它让 AI 知道在什么场景下该采用什么工作方式,也让我不用每次都从零开始解释自己的工程习惯。

这也对应了前面说的:AI 可以加速表达、生成和试探,但程序员仍然要负责判断、边界和验证。skill 只是把这些责任拆成更清晰的流程,让 AI 更稳定地参与其中。

影响奖励结果的所有输入已经确定、且不再变化的那一刻。

这是确定订单奖励内容的唯一标准。

引子

在一次业务设计讨论中,关于奖励的确定时机出现了不同意见。(我的真实经历)

一种观点认为,奖励应当在最终流程中再计算;
另一种观点认为,只要奖励结果已经可以确定,就应该立即计算并固化。

这看起来只是计算时机的差异,
但背后实际讨论的是:奖励结果在系统中什么时候才应当被视为事实。

什么时候确定奖励内容是合理的?

奖励内容本质上是一次规则计算的结果,它通常依赖以下输入:

  • 订单金额(含优惠、折扣)
  • 商品 / 档位 / 套餐
  • 活动规则(倍率、阈值、档位)
  • 玩家状态(VIP、首充、次数等)

当这些输入被完整收集,且后续流程中不会再发生变化时,奖励内容就可以被确定。

换句话说:

只要奖励的计算结果在逻辑上已经不可再变,就可以确认。

常见的可确认时间点

以下场景通常已经满足“输入已经确定、且不再变化”的条件:

  • 支付成功后,订单金额与商品信息已确定
  • 商品或服务的交付参数被锁定
  • 活动规则以快照形式固化到订单中

这些时间点的共同特点是:
奖励计算所依赖的数据已经稳定。

为什么要尽早确定奖励内容?

原因一:规则是可变的

如果奖励内容不在当时确认,后续再计算就会受到规则变动的影响,例如:

  • 活动配置被调整
  • 奖励倍率发生变化
  • 原有档位被下架

这将直接导致一种严重问题:

用户下单时看到的是 A 奖励,
系统最终确认的是 B 奖励。

这是比技术错误更严重的事故——用户认知不一致。

原因二:奖励需要可解释(对账 & 追责)

提前确定奖励内容,可以让系统具备清晰的“历史解释能力”:

为每一笔订单保留奖励结果快照

支持事后审计与对账

支持客服明确解释“这笔奖励是如何算出来的”

奖励内容一旦确认,就应当成为订单事实的一部分,而不是一个需要反复推导的结果。

小结

奖励可以晚发放,但不应该晚确定。

确定奖励的本质,是把规则计算结果从“可推导结果”转为“订单事实”。

系统应保存当时的输入、规则版本和计算结果。后续如果发生退款、取消、补偿等变化,应通过冲正或补偿流程处理,而不是重新解释历史订单。

本文讨论的是 check 命名在业界的默认语义约定,而不是个人风格偏好。

在很多项目中,checkXxx 被滥用于 API、Service 甚至业务逻辑中,导致调用语义混乱。

check 的定位

check 是可以用的,但是它只能在一个非常窄的语义里使用。
在大多数项目里, checkXxx() 是一个前置条件检查,不满足时抛出异常且没有业务副作用。

  1. 返回类型 void不是 boolean
  2. 失败时抛出异常。
  3. 成功时什么都不发生。

例如:

1
2
3
private void checkStatus();
private void checkPermission();
private void checkConfig();

check 的滥用

对 check 的最严重最常见的滥用出现在命名 API 时。

命名 API 时,我们的目标是消除惊讶,而 check 在 API 语境下是语义不稳定的命名。 对外的 API 一旦叫 checkXxxx,使用者就会在多种猜测中不知所措:

  1. 成功时返回的是一个 boolean还是一个结果?
  2. 失败时是一个 false 异常 还是结果?
  3. 我调用这个 API 会不会修改一些状态?

举一个更具体的例子,当一个方法叫 checkOrder() 时:

如果想知道订单是否有效,我们会期待它叫 isValid()

如果想触发校验流程并拿到错误列表,我们会期待它叫 validateOrder()

如果想在订单无效时阻止程序运行,我们会期待它叫 ensureOrderValid()

check 就像一个黑色盲盒,不看实现永远不知道它会静默失败还是原地爆炸。 因此,它只适合作为类内部的辅助方法(Helper methods)。

正确命名 API 的方式是这样的:

1
2
3
validateXxx()    // 有结果
verifyXxx() // 鉴权 / 第三方
ensureXxx() // 业务保障(失败抛异常)

总结

团队中可以将 check 约定为仅用于内部前置校验的方法命名,对外 API 必须使用结果或行为导向的动词。

场景 用 check 吗
返回 boolean
返回 Result / Error
Controller / API
有业务副作用
失败直接抛异常
内部前置校验
private / protected
Domain 规则校验

很多人会说 redis 执行 lua 是原子性的,但这样说不太准确。

执行隔离性(Isolation Guarantee)

redis 执行命令是单线程的,执行 lua 脚本时会阻塞其他指令和脚本的执行。

这确保了脚本在执行的过程中不会被并发操作干扰(例如同时修改一个数据)。

请看如下无执行隔离的时序图示例:

时序 客户端A 客户端B
t1 客户端A 执行 GET token:42→ 返回存在
t2 客户端B 执行 GET token:42→ 返回存在
t3 客户端A 执行 DEL token:42(成功)
t4 客户端B 执行 DEL token:42(因 token 已被A删除,此操作实际无效,但B仍会继续下单)

可见A和B都成功执行了下单操作,出现重复下单错误。

再看如下有执行隔离的时序图示例:

时序 客户端A 客户端B Lua
t1 发送 EVAL "if redis.call('GET', KEYS[1]) then return redis.call('DEL', KEYS[1]) else return 0 end" 1 token:42
t2 发送 EVAL "if redis.call('GET', KEYS[1]) then return redis.call('DEL', KEYS[1]) else return 0 end" 1 token:42
t3 执行客户端A的脚本: 1. GET token:42(存在) 2. DEL token:42(成功删除)
t4 客户端B 的请求此时才被处理。执行同样的脚本: 1. GET token:42(已不存在) 脚本返回 0,拒绝操作。

可见只有A成功了,B的操作失败了,未出现重复下单错误。

不满足全有或全无原则(all-or-nothing)

all-or-nothing
指事务要么完全发生,要么根本不发生。例如A向B转账100元,B已收到100元,但A并非扣除100元,这是不可接受的。

原子提交/回滚机制(Atomic Commit/Rollback)

在数据库的实现中一般会使用原子提交/回滚实现全有或全无原则。

原子提交

当事务中的所有操作都成功执行,并且验证没有违反约束时,系统会 一次性把事务的所有修改写入数据库(这一批操作完成了)。

提交之后,所有事务产生的更改对其他事务可见。

原子回滚

如果事务中某个环节失败,或者用户/系统主动要求撤销,就必须 撤回事务中已做的所有修改,让数据库回到事务开始之前的状态(这一批已做的操作都不算数)。

一般会使用 undo log 来实现,保证把执行的操作撤销掉。

请保持脚本的轻量

由于脚本执行期间会阻塞整个 Redis 的执行,避免在脚本中编写复杂循环或耗时操作是关键。否则会严重影响 Redis 的响应性能。

0%