Claude code 源码学习(一)
打算通过 Claude code 源码学习 Prompt Engineering、Context Engineering 和 Harness Engineering 的相关知识,这篇是本系列的第一部分。
一、动态拼接提示词
Claude Code将系统提示词拆分成了多个独立段落,在运行时进行动态拼接。
这样做主要有两个好处:
-
按需注入,减少噪音。 每一段内容都可以独立控制是否生效。例如,只有在当前会话真正连接了 MCP 服务器时,才会注入 MCP 工具说明;如果没有连接,这段内容就不存在。
⚠️ 系统提示词不该塞入用不到的信息,否则既浪费 Token,又会干扰模型。用户自定义的提示词、当前仓库的
CLAUDE.md内容,也都是按条件动态插入的。
-
动静分离,优化缓存。 稳定的内容和变化的内容分开处理。Claude Code 会在会话开始时,把角色说明、工具偏好、行为规范等静态部分组装好,并打上 cache_control: ephemeral标记。 这样 Anthropic 的 Prompt Cache 就会在服务端缓存这些内容的 KV 状态。后续每轮对话只需拼接少量动态内容(如剩余 Token 预算),直接命中缓存。在长会话中,这能大幅节省 API 费用并降低延迟。
二、提示词中需要写哪些内容
1. 身份与客观环境
告诉模型它是谁、在什么场景下工作。这里的重点不是虚设头衔,而是注入环境事实。
像当前工作目录、日期、操作系统、是否为 Git 仓库、终端是否支持 Markdown 渲染等,这些信息会直接影响模型的决策,但它无法自己推断,必须直接告诉它。
2. 行为引导与元认知引导
这两者都在做“引导”,但解决的问题不同。
-
行为引导:是为了弥补模型的知识缺口,注入人类的领域经验。比如团队的工作流是什么、某类任务分几个阶段、优先使用哪个工具等。 -
元认知引导:则是为了修正模型推理时的固有偏差,教模型“怎么想”。
例如,Claude Code 明确规定:不要因为在某个方向上花了太多时间就死磕,发现更好的方法要立刻切换(对抗沉没成本偏差);如果任务有风险或不确定,必须明确告知用户,不能自己悄悄把任务“缩水”(对抗责任转移)。
3. 明确的边界与限制
每个工具和技能都要写清楚“何时该用”与“何时禁用”。这里有个很实用的经验:
-
行为类边界不需要一次性全部预设。 源码里有大量 @[MODEL LAUNCH]注释,记录了某项限制是因为发现了什么具体问题才在特定版本加入的。这说明边界规则是迭代出来的,过度设计反而不好。 -
安全类边界必须主动预设,不能等模型犯错了再补。 -
高优先级的规则要在使用点重复强调。 上下文变长后,模型对开头规则的注意力会下降。Claude Code 会在特定工具的说明里单独重申关键限制,而不是全堆在总规则里。
4. 明确的终止条件
对于多步任务,为每个阶段显式设定终止条件(Success criteria),避免模型只凭感觉推进,提高可靠性。
三、工具使用说明
系统提示词和工具说明里都可以写工具相关的内容,但侧重点完全不同:
-
系统提示词负责“使用时机”。 比如这个工具在什么场景该用、什么场景不该用、如何与其他工具做选择。这些信息需要在模型决定用哪个工具之前就读到,用于拦截错误的工具选择。 -
工具说明负责“使用技巧”。 比如参数怎么填、有哪些注意事项和常见报错。这些细节只有在模型决定使用该工具后才需要,放在系统提示词里太早,放在使用点(工具描述)里才最合适。
四、为推理过程增加 Few-shot 示例
过去的认知里 Few-shot 主要用在输出风格示例,但是 Claude Code 内置的 Skill 提示词中使用了 <reasoning> 和 <commentary> 标签,用有序列表详细模拟了“在当前情境下,为什么选这个行动而不是另一个”的思考过程,通过这种方式把决策过程透明化。
这能让模型学会判断逻辑,而不是单纯模仿行为。
五、比想象中更丰富的 Skill 系统
官方文档提到 Skill 的 Front matter 时,通常只包含 name 和 description 字段, 读了源码才知道它还支持指定模型(model)、自动授权工具列表(allowed-tools)、上下文模式(context)以及任务量级(effort)等更多参数。后面在官方文档中也找到了相关描述。
-
allowed-tools的真实作用:它不是用来限制工具权限的,而是用来扩展自动授权范围。配置了该字段后,模型调用这些工具时会自动放行,无需用户手动确认,且不影响原有的工具权限。 -
多个 Skill 的执行逻辑:之前误以为同时使用多个 Skill 也就是会同时向提示词中注入多段文档。看了源码才知道,实际上只有第一个会被当作 Skill 执行,第二个会被解析为第一个 Skill 的参数。 -
记忆机制:Skill 的内容会以普通用户消息的形式常驻对话历史。这意味着同一个 Skill 在单次会话中只需注入一次,模型就会一直记住它。这个机制倒跟之前的理解差不多,在使用 Skill 的时候不用每次都手动触发,还能节省一些上下文窗口。
这一节好像跟主题无关,但是学了就分享出来吧,哈哈。
六、结构化输出的两种落地方式
如果需要让模型输出 JSON,源码展示了两条可靠的路径:
-
工具调用(Tool Calling):相比于在提示词里强行规定输出格式,利用模型微调过的工具调用能力输出 JSON 会稳定得多。Claude Code 内部就有一个名为 StructuredOutput的工具专门负责处理这类需求。 -
API 的 output_format参数:直接在调用 LLM API 的时候传入 JSON Schema, API 会在采样阶段强制保证输出格式。这种方式更彻底,非常适合内部分配的小型任务(如分类、选择、打分),连定义工具的步骤都省了。
以前就听说过通过 Function Call 生成 JSON 会比在提示词中规定输出格式更稳定,因为前者利用的是微调过的能力,后者利用的是模型的自然语言能力,这也是第一次见到这种方法的实际使用。
七、总结
阅读 Claude Code 的源码,本质上是在学习一套工业级的提示词工程架构。
💡 它告诉我们:优秀的提示词不是靠文笔写出来的,而是靠工程化手段构建出来的。
通过动态拼接实现精准注入,通过动静分离优化缓存,通过区分“行为引导”与“元认知引导”来提升模型的思考质量,并把规则拆解到系统提示词和工具说明的最优位置。
这些从实践中踩坑总结出的经验,比任何枯燥的理论教程都更有价值。
夜雨聆风