乐于分享
好东西不私藏

Claude Code 源码拆解第二篇:为什么你写的 prompt 总是跑偏

Claude Code 源码拆解第二篇:为什么你写的 prompt 总是跑偏

不是教你写出完美 prompt 句式,而是教你怎么把任务交接写完整。

前几天,一个朋友给我发了一段他和 agent 的对话。

他问:”帮我看看这个项目怎么优化一下。”

十分钟后,agent 给了他一堆正确但没用的空话:代码风格建议、性能优化原则、架构重构思路。

他皱着眉问我:”为什么我写得这么清楚,它还是给我一堆废话?”

我看了眼他的 prompt,只写了一句话。

我说:”你不是在写任务,你是在许愿。”

如果你最近也有这些感受,这篇大概率对你有用:

  • prompt 写了不少,但结果总是要自己大改。
  • 同样的任务,有时能对上,有时又跑偏。
  • 总觉得”是不是我 prompt 还不够花”。
prompt 不是愿望清单,而是任务交接单

一、prompt 的真正作用:不是咒语,而是交接单

很多人把 prompt 当成”对着 AI 许愿”,觉得句式越花哨、效果越好。

但 Claude Code 源码把这件事定义得很朴素:

Brief the agent like a smart colleague who just walked into the room.

这句话的关键词不是 smart,而是 “just walked into the room”。

这意味着:

  • agent 不会读心术,它只看你写下来的东西。
  • 你没写的,它只能猜;猜不准,就会给你一堆正确但没用的答案。
  • 你的 prompt,本质上是在补齐一个陌生同事接手任务所需的信息。

这不是咒语,而是任务交接单。

为什么源码要写这些约束

很多人会问:为什么 Claude Code 要在 prompt 里写这么多”不要做这个、不要做那个”?

答案藏在源码的细节里。

约束一:fork prompt 是 directive(指令),不是背景交代

在 src/tools/AgentTool/prompt.ts 里有一句很容易忽略的话:

Since the fork inherits your context, the prompt is a directive — what to do, not what the situation is.

意思是:fork 继承了你的上下文,所以不需要重复背景,只需要明确”做什么”。

这解释了为什么 fork 的 prompt 可以写得很短——不是因为短就够,而是因为背景已经在上层对话里了。

约束二:Don’t peek 和 Don’t race

源码里还有两条看起来很严格的约束:

Don’t peek. The tool result includes an output_file path — do not Read or tail it unless the user explicitly asks for a progress check.

Don’t race. After launching, you know nothing about what the fork found. Never fabricate or predict fork results.

为什么这么严?

  • “Don’t peek”:中途读 fork 的输出文件,会把 fork 的工具噪声拉进你的上下文,这违背了 fork 的初衷。
  • “Don’t race”:fork 结果没回来前,你什么都不知道,不能脑补结果。

这两条约束的本质是:信任通知机制,不要自己偷看或猜测。

约束三:placeholder result 和 prompt cache 共享

在 src/tools/AgentTool/forkSubagent.ts 里,所有 fork child 使用相同的 placeholder result:

Placeholder text used for all tool_result blocks in the fork prefix. Must be identical across all fork children for prompt cache sharing.

这意味着:所有 fork child 的 API 请求前缀是字节级一致的,这让 fork 在 cache 上非常便宜。

你看,源码里的每一条”不要”,背后都是工程成本的考量。

二、为什么简短指令一定会翻车

源码里有一句很重的话:

terse command-style prompts produce shallow, generic work.

翻译成一句话:简短指令,只能换来浅层结果。

这不是模型能力问题,而是任务信息不足导致的必然结果。

来看一个典型差 prompt:

你去看看这个项目怎么优化一下。

问题在哪?

  • 范围没说:哪个模块?什么粒度?
  • 目标没说:优化性能、可维护性、还是架构?
  • 输出没说:要代码?要方案?要对比?
  • 边界没说:哪些不要碰?

你只给了一句愿望,agent 只能给你一堆正确但没用的空话。

翻车的四种典型模式

源码里那句 “terse command-style prompts produce shallow work”,背后的根因是信息缺失。我们拆开看四种最常见的翻车模式。

模式一:范围过大

你去看看这个项目怎么优化一下。

翻车原因:

  • 没限定模块:是整个项目,还是某个目录?
  • 没限定粒度:是要代码级改动,还是架构级建议?
  • 没限定关注点:是性能、可维护性,还是安全性?

结果:agent 给你一堆正确但没用的空话,因为范围太大了。

模式二:输出格式没说

帮我分析一下这个迁移是否安全。

翻车原因:

  • 你要的是”安全/不安全”的结论,还是详细的风险清单?
  • 要的是一句话总结,还是逐条分析?

结果:agent 给了一个很长的分析,但你只想要一个结论。

模式三:边界模糊

看看这里有没有问题。

翻车原因:

  • 什么叫”问题”?性能问题?安全问题?代码风格问题?
  • 你没界定边界,agent 只能按自己的理解来。

结果:agent 检查了一堆你不在乎的东西,漏掉了你真正想看的。

模式四:判断外包

基于你的分析,给出优化建议。

翻车原因:

  • 你把”什么是好的优化”这个判断标准,外包给了 agent。
  • 它给的优化建议,可能完全不符合你的业务目标。

结果:你收到一堆技术上正确、业务上跑偏的建议。


源码里还有一句更直接的提醒:

Never delegate understanding. Don’t write “based on your findings, fix the bug”.

意思是:不要把理解和判断整包外包给 agent。

如果你连问题是什么都没说清楚,就不要指望它替你做判断。

差 prompt 换来浅层结果,好 prompt 来自完整交接

三、源码真正推荐的 prompt 写法

源码给出了一个很朴素的建议:

把 agent 当成刚进门的聪明同事,你需要回答 4 个问题:

  1. 范围:只看哪里。
  2. 目标:要回答什么问题。
  3. 输出:最后怎么回报。
  4. 边界:哪些事不要做。

一个够用的模板是这样的:

只处理 [范围]。
目标是回答 [问题/任务目标]。
不要做 [禁止事项]。
最后用 [输出格式] 返回,并附上 [文件路径/证据]。

差 prompt vs 好 prompt

你去看看这个项目里 agent 是怎么实现的,顺便看看有什么值得注意的地方。

只检查 src/tools/AgentTool/ 下与 subagent 调度相关的逻辑,回答 3 个问题:
1. fork 和 fresh subagent 的差异是什么;
2. 哪些约束说明 agent 更像任务执行单元;
3. 哪些提示词直接说"不要把理解外包给 agent"。
不要改代码,不要扩展到别的目录。
最后用短列表返回结论,并带上文件路径。

你会发现,好的 prompt 没有任何神秘感,只是把任务交接写完整了。

briefing 四要素:范围、目标、输出、边界

四、fresh agent 的背景补全:从零开始,必须重新 briefing

Claude Code 有两种 subagent:

  • fork
    :继承你当前的上下文,适合从正在进行的任务里拆出一个分支。
  • fresh subagent
    :从零开始,像刚走进会议室的同事。

如果你给 fresh agent 只写一句半截话,它真的只能给你半吊子结果。

源码里说得很直:

When spawning a fresh agent, it starts with zero context. Brief the agent like a smart colleague who just walked into the room.

给 fresh agent 的差 prompt

你去看看这个迁移是否安全。

给 fresh agent 的好 prompt

Review migration 0042_user_schema.sql for safety.
Context: 我们要给一个 5000 万行的表加 NOT NULL 列,现有行用默认值回填。
我已经检查过锁行为,但想再确认在并发写入下是否安全。
请回答:这个迁移是否安全?如果不安全,具体哪里会出问题?

差别在哪?

  • 补齐了背景:为什么要做这个迁移,你已经排查了什么。
  • 明确了问题:要回答”是否安全”和”具体哪里会出问题”。
  • 收束了范围:只看这个迁移,不要扩展到别的表。

很多人觉得 fresh agent “变笨了”,其实往往是背景给少了,结果自然发虚。

五、不要把理解外包:最容易被忽略的一条

源码里有一句约束,很多人会忽略:

Never delegate understanding. Don’t write “based on your findings, fix the bug”.

这句话的意思是:

  • 问题 framing 这件事,先由你来做。
  • 判断和决策,不要整包交给 agent。
理解不能外包,执行和判断要拆开

四种理解外包的错误模式

源码里那句 “Never delegate understanding”,背后是对四种常见错误的约束。

错误模式一:判断外包

基于你的分析,修复这个 bug。

问题:你把”判断问题根因”和”决定修复方案”都交给了 agent。如果它的分析偏了,你的修复也会跟着偏。

正确写法:

只分析这个 bug 的根因,返回文件路径和行号,不要改代码。
我来决定修复方案。

错误模式二:标准外包

看看有什么值得注意的地方。

问题:什么叫”值得注意”?这是把判断标准外包了。agent 可能给你一堆你不在乎的东西,漏掉你真正想看的。

正确写法:

只检查安全相关的代码路径,返回可能存在风险的文件和行号。

错误模式三:范围外包

顺便帮我优化一下。

问题:什么是”优化”?性能?可读性?架构?你没说清楚,agent 只能按自己的理解来。

正确写法:

只优化这个函数的性能,目标是减少 O(n²) 到 O(n),不要改接口签名。

错误模式四:优先级外包

给我一些建议。

问题:哪些建议是重要的?这个判断你也没给。agent 可能给你一堆低优先级的建议,漏掉真正重要的。

正确写法:

按风险等级排序,列出前 3 个最需要处理的问题。

把”执行”和”判断”拆开,让 agent 跑腿,但别让它替你拍板。

四种理解外包错误模式:判断、标准、范围、优先级

六、一个够用的 prompt 检查清单

每次写完 prompt,先过这 5 个问题:

  1. 我有没有把范围说清楚?
  2. 我有没有把目标说清楚?
  3. 我有没有把输出格式说清楚?
  4. 我有没有把边界说清楚?
  5. 我是不是正在把理解偷偷外包给 agent?

只要有两个答案是”没有”,先别开 agent。

七、进阶:prompt cache 和 schema 稳定性

如果你关心成本,还有一件事值得知道。

Claude Code 会缓存工具 schema,任何字节级变化都会导致约 11K token 的缓存失效。

源码里有一句注释:

Tool schemas render at server position 2, any byte-level change busts the entire ~11K-token tool block.

这意味着:

  • 动态工具描述会让系统变贵。
  • 频繁改 prompt 或工具定义,会打爆缓存。

这也是为什么 Claude Code 会把 agent 列表放到消息附件里,而不是嵌入工具描述:

为了保持 prompt cache 稳定。

这个话题我们下一篇继续拆:工具越多不一定越强,上下文越脏 agent 越笨。

八、进阶:长工具输出的上下文污染

还有一类踩坑,很多人不会意识到,但源码里做了防御。

问题:你让 agent “看看整个日志”,这个日志会一直留在上下文里。

在 src/utils/toolResultStorage.ts 里,系统会对超长的工具输出做持久化:

Output too large. Full output saved to: <filepath>

系统会用 preview 替换完整输出,避免上下文爆炸。

为什么长输出会污染上下文

  • 每次工具调用,输出都会进入消息历史。
  • 如果你让 agent “看看整个日志”,这个日志会一直跟着你。
  • 系统会尽力帮你止血(持久化 + preview),但根本解决方案是:写 prompt 时就限定范围。

给读者的实操建议

  • 不要让 agent “看看整个日志”,而是指定行号范围。
  • 不要让 agent “列出所有文件”,而是先用 glob 收窄。
  • 如果你发现上下文越来越重,检查一下是否有长工具输出。

这个话题我们会在第三篇继续展开。


最后总结

写 prompt 不是许愿,而是写清楚一份能让陌生同事接手的任务单。

如果你只记住一句话,我建议记这个:

先别想句式,先问自己:任务交接是否完整?


本文提到的源码位置

  • src/tools/AgentTool/prompt.ts:95-113, 115-154
    (briefing、directive、Never delegate understanding)
  • src/tools/AgentTool/forkSubagent.ts:92-93
    (placeholder result 和 prompt cache 共享)
  • src/utils/toolSchemaCache.ts:3-8
    (schema 稳定性和 11K token 缓存)
  • src/utils/toolResultStorage.ts:130-184
    (长工具输出的持久化)

如果你觉得这篇有用,下一篇我继续拆 tool 和 context:为什么工具越多不一定越强,上下文越脏 agent 越笨。Claude Code 源码里藏着很多容易被忽略的工程约束,我们一篇一篇把它拆清楚。