在每次调用模型 API 时,Claude Code 发送给 API 的请求负载包含三个核心组成部分:
- System Prompt(系统提示)——用于定义 Agent 的身份、行为准则以及会话上下文;
- Tools(工具定义)——工具 schema 列表,向模型声明可调用的能力;
- Messages(消息序列)——包含用户指令、CLAUDE.md 配置以及工具执行结果的对话消息列表。
这三部分在 Agent Loop 调用模型时都会被传入,但来源各有不同。System Prompt 和 Tools 通常在进入循环前已经准备就绪,并在整个循环过程中保持相对稳定;而 Messages 则是随着每轮工具的执行不断追加和更新的部分。Agent Loop 的核心运作机制是:模型返回工具调用请求,系统执行该工具并将结果追加到 Messages 中,然后进入下一轮模型调用,如此反复直至任务完成。
// src/query.ts — Agent Loop 中调用模型(简化示意)
for await (const message of deps.callModel({
systemPrompt: fullSystemPrompt, // System Prompt
messages: ..., // 对话消息
tools: ..., // 工具 schema 列表
}))
整个设计贯穿一个关键约束:System Prompt 和 Tools 属于对缓存敏感的前缀层,而 Messages 则是持续增长的动态层。模型 API 会尽可能复用稳定的请求前缀;如果 System Prompt 或工具 schema 在运行中途发生改变,前缀缓存便会失效。
这一约束直接决定了 Claude Code 的上下文组装策略:保持前缀稳定,将动态内容后置。换言之,适合缓存的内容应尽量前置并保持稳定,运行时变动则尽可能地转移到 Messages、attachment 附加内容或延迟工具加载机制中。
Agent 运行前的上下文组装
每当会话启动时,Claude Code 会优先构建基础的 System Prompt 和工具池;之后会尽量维持前缀的稳定性,将变化部分安排在 Messages、attachment 附加上下文或延迟工具加载中处理。
System Prompt 的工程化组装
System Prompt 并非一个庞大的单一字符串,而是一个由字符串组成的数组。每个元素代表一个独立的段落,仅在发送给 API 前才拼接成最终形式。这种设计带来了以下好处:
- 每个段落职责分明,便于独立维护和测试;
- 静态段落与动态段落可以实现分离,静态部分能够直接命中模型的前缀缓存;
- 动态段落可根据条件裁剪,灵活控制注入的内容。
阅读这段代码时,只需关注三个要点:返回值类型为数组;静态 section 放置在前面;动态 section 放置在缓存边界之后。
export async function getSystemPrompt(tools, model, ...): Promise<string[]> {
const dynamicSections = [
systemPromptSection('session_guidance', () => getSessionSpecificGuidanceSection(...)),
systemPromptSection('memory', () => loadMemoryPrompt()),
systemPromptSection('env_info_simple', () => computeSimpleEnvInfo(model, ...)),
// ...
];
return [
// --- 静态段落 ---
getSimpleIntroSection(), // 身份声明
getSimpleSystemSection(), // 系统规则
getSimpleDoingTasksSection(), // 任务执行准则
getActionsSection(), // 操作安全
getUsingYourToolsSection(), // 工具使用偏好
getSimpleToneAndStyleSection(), // 沟通风格
getOutputEfficiencySection(), // 输出效率
// === 缓存边界标记 ===
...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),
// --- 动态段落 ---
...dynamicSections,
].filter(s => s !== null);
}
静态段落用于定义 Agent 的“行为规范”。它们的共同特征在于:通常不依赖于当前用户、项目目录、MCP 连接状态或本轮输入,可以跨用户进行缓存,因此更适合放置在最前面作为稳定前缀:
- 身份声明与安全指令:例如“You are an interactive agent that helps users with software engineering tasks”,以及网络安全和 URL 生成限制
- 系统规则:工具权限、system-reminder 标签说明、外部数据的 prompt injection 警告
- 任务准则:先读取文件再修改、不添加多余功能、不写多余注释、安全意识(遵循 OWASP Top 10)
- 操作安全:关注操作的可逆性和影响范围,破坏性操作需要用户确认
- 工具偏好:优先使用专用工具(Read/Edit/Write/Grep/Glob)而非 Bash
- 沟通风格:简洁、不使用 emoji、代码引用需附带文件路径和行号
动态段落包含当前会话所涉及的相关上下文。它们会因用户环境、配置、记忆、语言偏好等不同而发生变化。但在单个会话内部,大多数动态段落仍会使用 memoized 机制(即仅计算一次并存入内存,后续直接复用结果):会话开始时计算一次,后续请求直接复用。那些真正需要在运行时变化的内容,通常会被后移到 Messages、增量 attachment 附加上下文(仅发送变化部分)或延迟工具加载中,避免直接改动可缓存前缀。典型动态段落包括:
- session_guidance——当前可用的工具和技能列表,包括 Agent 工具(允许模型启动子 Agent 以并行处理子任务)和 Skill 工具(将用户定义的斜杠命令如
/review封装为模型可调用的工具)的使用指导 - memory——自动记忆系统的行为指令,指导模型如何保存和检索记忆
- env_info_simple——当前工作目录、操作系统、Shell 类型、模型名称
- language / output_style——用户配置的语言偏好和输出风格
- mcp_instructions——MCP 服务器的连接状态和使用说明;它并非普通的 memoized 段落,MCP 连接变化更多通过 uncached / delta 机制、Tool Search /
defer_loading或下一次顶层上下文构建方式体现,而不是在同一个 Agent Loop 的每次工具 follow-up 中重新计算 System Prompt
两者的对比:
| 静态段落 | 动态段落 | |
|---|---|---|
| 跨会话 | 被设计为尽量稳定,适合作为高效缓存前缀 | 因用户环境和配置而异 |
| 会话内 | 基本保持不变 | 大部分使用 memoized;需要变更时通常后移到 Messages、delta 或 deferred tools |
| 内容占比 | 约占 60% 以上 | 剩余部分 |
静态段落与动态段落之间使用了 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记进行分隔。这个标记的工程价值在于:它为 Prompt Cache 提供了一条明确的切分锚点。可以将其理解为:边界之前的段落应尽可能保持字节级别的稳定,边界之后的段落允许根据会话而变化,从而使缓存策略能够精准地确定哪些部分可以复用。
Tools
Tools 部分并非“会话开始后一成不变”。Claude Code 会维护当前可用的候选工具池,然后决定哪些工具直接纳入本轮模型请求,哪些通过 Tool Search 延迟加载。
可以先用三层模型理解:
- 候选工具池——当前会话可能使用的工具全集,来源包括内置工具、MCP、Skill 等。
- 本轮直接传入的工具——直接放入模型 payload 的工具 schema,属于对缓存敏感的前缀部分,通常是高频、基础、需要即时可用的工具。
- deferred tools(延迟加载工具)——不直接进入前缀的长尾或动态工具,通过 Tool Search 或
defer_loading在需要时暴露,避免工具 schema 撑大稳定前缀或频繁打破缓存。
延迟加载的触发机制是:当模型表达了需要某类工具的意图时,系统会通过 Tool Search 从候选池中提取对应的 schema 并补充到上下文里,而不是在每次请求时都将全部工具 schema 塞入前缀中。
Claude Code 的工具来源包括:
- 内置工具——包括 Read、Write、Bash、Grep、Glob 等文件操作与搜索工具,数量约 40 多个
- MCP 工具——通过 MCP(Model Context Protocol)服务器动态注册的外部工具
- Skill 工具——将用户定义的斜杠命令转换为可调用的工具
工具池的核心组装路径之一为 assembleToolPool()(它接收当前会话的工具来源配置,返回过滤后的候选工具池):该函数负责将内置工具和 MCP 工具按权限进行过滤、排序、去重;但工具来源和后续合并并不全都发生在这个函数中。是否直接进入本轮请求,还取决于后续的工具选择和延迟加载策略。
Messages 的初始组装
与 System Prompt 和 Tools 不同,Messages 在会话过程中是持续增长的。首次对话时,messages 数组的内容由以下三部分组成:
- CLAUDE.md——通过
prependUserContext()方法(将 CLAUDE.md 内容包装为 user 消息并插入到 messages 数组最前面)作为首条 user 消息注入。需注意,该操作在每轮调用模型前都会执行,因此 CLAUDE.md 在每一轮对话中都位于 messages 的最前列。 - 用户输入——用户实际输入的消息(通过
createUserMessage生成) - attachment 附加上下文(
AttachmentMessage)——包括 @ 提及的文件内容、IDE 选中的代码片段、hook 注入的额外上下文等
组装过程如下:
CLAUDE.md 注入
Messages 部分最核心的注入内容就是 CLAUDE.md——用户可以通过 Markdown 文件来定义 Agent 的行为规范。该类文件按照优先级从低到高的顺序加载:
- Managed(托管)——位于
/etc/claude-code/CLAUDE.md,由管理员定义的全局策略 - User(用户)——位于
~/.claude/CLAUDE.md,用户的私有全局偏好 - Project(项目)——位于项目根目录或上级目录中的
CLAUDE.md、.claude/CLAUDE.md或.claude/rules/*.md,纳入版本控制管理 - Local(本地)——位于项目根目录的
CLAUDE.local.md,仅对本地生效的私有覆盖
Claude Code 会从当前目录向上遍历至根目录,在每个层级查找上述文件。优先级的具体处理方式是:高优先级文件的内容排列在低优先级之后。由于 Claude 模型按从上到下的顺序阅读 messages,后出现的指令通常会被优先遵循——因此,如果 User 级指定“用中文回复”,而 Project 级指定“用英文回复”,模型会倾向于遵循 Project 级的指令。这一排序仅描述同为 CLAUDE.md 上下文时的工程策略,并不代表 Project 级内容可以覆盖 System Prompt 或安全边界。
简而言之,System Prompt 负责定义“模型该如何被约束”,而 CLAUDE.md 则负责定义“该项目希望模型了解哪些信息”。
CLAUDE.md 通过 标签作为首条 user 消息注入。需注意,此处的 是 user message 内容中类似于 XML 的标签,并不等同于 API 中的 system role:
<system-reminder>
As you answer the user's questions, you can use the following context:
# claudeMdCodebase and user instructions are shown below. Be sure to adhere to these
instructions. IMPORTANT: These instructions OVERRIDE any default beha vior
and you MUST follow them exactly as written.
Contents of ~/.claude/CLAUDE.md (user's private global instructions for all projects):
# 全局偏好
- 默认使用中文回复
- commit message 使用英文
- 代码风格偏好:优先函数式写法,避免 class
Contents of CLAUDE.md (project instructions, checked into the codebase):
# 项目规范
- 所有接口必须返回统一的 `{ code, data, message }` 结构
- 错误处理使用 AppError 类,不要直接 throw Error
- 参数校验使用 zod schema
# currentDate
Today's date is 2026-05-17.
IMPORTANT: this context may or may not be relevant to your tasks.
system-reminder>
用户输入
用户输入会先被拆分为两部分:原始输入本身被包装为 UserMessage,而同轮需要补充的上下文则被包装为 AttachmentMessage。
- 纯文本输入:直接作为
content传递 - 粘贴图片:文本和图片组合进
UserMessage.content,图片会经过必要的处理以满足 API 限制 - @文件 / @目录 / @图片文件 / MCP 资源 / @agent:不会修改用户的原文,而是解析为独立的
AttachmentMessage(attachment 附加上下文),并放置在UserMessage之后
attachment 附加上下文
此处的 attachment 并不只来自 @ 语法。在用户输入预处理阶段,系统会先调用统一的 attachment 附加上下文收集逻辑,结果与 UserMessage 一同进入 messages。简化来说,模型看到的消息序列类似如下:
[CLAUDE.md user message, 用户原始输入, attachment:file, attachment:diagnostics, ...]
不应将 attachment 理解为一种固定、全量、每轮必带的接口表。源码中可见的 attachment 类型很多,其中一部分依赖于特定的功能、模式或 feature gate。为了把握主线,可以先按用途进行分组:
| 分组 | 典型类型 | 作用 |
|---|---|---|
| 用户显式输入 | file、directory、pdf_reference、mcp_resource | 将 @ 引用的文件、目录、PDF 或 MCP 资源补充到用户消息旁边 |
| 已读与文件变更 | already_read_file、edited_text_file、edited_image_file | 避免重复注入,或仅补充文件读入后的增量变化 |
| IDE 与诊断 | selected_lines_in_ide、opened_file_in_ide、diagnostics | 将用户当前查看的代码、选中区域、LSP 诊断信息传递给模型 |
| Skill / Agent / Tool 发现 | skill_discovery、dynamic_skill、skill_listing、agent_mention、agent_listing_delta、deferred_tools_delta | 向模型告知可用技能、Agent 类型,以及延迟工具的变化情况 |
| Hook 与异步事件 | hook_additional_context、hook_success、async_hook_response、queued_command、task_status | 将 hook 输出、后台任务、异步通知补充到下一轮上下文中 |
| 运行模式与提醒 | plan_mode、plan_mode_exit、auto_mode、auto_mode_exit、todo_reminder、task_reminder、verify_plan_reminder、critical_system_reminder、context_efficiency、date_change | 通过轻量提醒同步当前运行状态与约束条件 |
| 预算与输出控制 | token_usage、budget_usd、output_token_usage | 帮助模型了解上下文占用、预算和输出长度 |
| 特定功能路径 | nested_memory、mcp_instructions_delta、teammate_mailbox、team_context、ultrathink_effort、companion_intro | 仅在对立功能、团队模式或实验路径下出现 |
Agent 运行过程中的动态上下文
前三节讨论的是 Agent Loop 启动前的基础组装。进入循环后,Claude Code 会保持 System Prompt 和工具的稳定性;主要变化集中在 Messages 数组,而 MCP 这类动态工具则优先通过 Tool Search 或 defer_loading 处理。
Agent Loop
主循环在 query() 中,核心逻辑如下:
// query.ts — Agent Loop 核心结构(已简化,保留关键调用)
while (true) {
// 1. 准备本轮要发送给模型的 messages(提取本轮需要发送的消息,可能包含历史截断逻辑)
messagesForQuery = getMessagesForCurrentTurn(state.messages);
// 2. 调用模型
for await (const message of deps.callModel({
messages: prependUserContext(messagesForQuery, userContext), // 每轮调用前把 CLAUDE.md / userContext 放回 messages 前部
systemPrompt: fullSystemPrompt,
tools: toolUseContext.options.tools,
})) {
/* 收集 assistant 消息和 tool_use 块 */
}
// 3. 没有工具调用 → 结束
if (!needsFollowUp) {
return { reason: 'completed' };
}
// 4. 执行工具(异步执行工具调用,返回 tool result 消息流)
const toolUpdates = runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext);
for await (const update of toolUpdates) {
yield update.message;
toolResults.push(update.message);
}
// 5. 注入运行时附加上下文
// ...(见下文)
// 6. 更新 messages,进入下一轮
state = {
messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
transition: { reason: 'next_turn' },
};
}
整个循环的流程可以概括为:准备消息、调用模型、检查工具调用、执行工具、注入增量上下文、更新状态并继续循环。
每轮循环刷新了什么
前面列出的 attachment 附加上下文的收集逻辑并不仅仅在第一轮运行。在工具执行完成后,Claude Code 会在下一次模型调用前重新注入 attachment 附加上下文,但这并不意味着会将已经注入过的内容全量重注。大多数 attachment 都有各自的触发条件或去重状态:如果没有新事件、新变化或到期提醒,就不会产生新的 attachment。去重依据也分散在各类型自身的状态中,比如已发送过的 skill name、已读文件记录、队列消费状态、文件 diff 基线等。
后续轮次中最常补充的是以下“增量变化”:
- 排队消息:后台任务完成、外部通知、子 Agent 消息等异步事件,在消费后将从队列中移除。
- 文件变更:已读入上下文的文件如果被工具修改,仅注入新的文本差异或图片内容。
- 预取记忆:记忆检索在模型返回工具调用时异步启动;结果仅消费一次,并会过滤掉模型已读、已写或已编辑的记忆文件。
- 技能发现:基于本轮消息和工具写入信号进行预取;技能列表本身也会记录已发送过的 skill name,仅补充新增项。
- 诊断信息:编辑文件后,IDE/LSP 产生的新错误或警告,会以诊断类 attachment 附加上下文的形式补充给模型。
更准确地说,循环中的 attachment 机制是在每轮工具执行后进行一次增量检查:只有检测到新的队列消息、文件差异、检索结果或技能变化时,才将对应的信息补充到下一轮 Messages 中。下面这张图仅展示了四类最典型的增量变化。
总结
Claude Code 的上下文并非一次性拼接成一个静态的大 prompt,而是采用分层组装、分阶段更新的方式:
- System Prompt 承载稳定规则和动态段落边界,尽量使可缓存的前缀保持稳定。
- Tools 根据内置工具、MCP、Agent、Skill 等来源进行组装,并在必要时通过延迟加载机制降低上下文负担。
- Messages 是 Agent Loop 中持续变化的主体:用户输入、模型回复、工具调用结果和 attachment 附加上下文都会按照顺序进入消息流。
- attachment 附加上下文是在运行时补充上下文的关键机制:第一轮侧重于用户输入和初始环境,后续轮次则重点关注工具执行后的增量变化。
因此,要理解 Claude Code 的上下文组装,核心不在于记住某个固定的 prompt 样式,而在于认清三件事:哪些内容保持稳定、哪些内容按需装配、哪些内容会随着工具执行不断增量地补充到下一轮 Messages 中。
