随笔
Claude Code 深度源码解读 02:Context 构建是一条输入装配线
拆解 submitMessage、system prompt、processUserInput、attachments、memory 与 transcript 写入顺序。
简化版入口:Context 构建。
我的设问与回应
设问 1:Context 是不是把资料尽量多地塞给模型?
回应答案: 不是。Context 构建更像输入装配线:按来源、作用域、生命周期把系统规则、用户输入、附件、memory、项目快照和 Hook 输出组装成模型可消费的消息。
证据与证明路径: 证据来自 QueryEngine、fetchSystemPromptParts、processUserInput、getAttachmentMessages、recordTranscript 的顺序。git status 被明确标注为会话开始时快照,说明源码并不把它当成运行时动态反馈。
还值得继续学习或反思: 要继续学习如何给每类上下文标 source、scope、freshness,避免上下文完整性变成噪声堆积。
设问 2:为什么模型请求前要先写 transcript?
回应答案: 因为 transcript 是恢复与审计边界,不只是日志。用户消息先落盘,后续 API 中断、fallback 或 resume 才有可恢复事实来源。
证据与证明路径: 证据是原报告把 recordTranscript 放在模型请求前,并在贡献部分强调支持恢复与审计。
还值得继续学习或反思: 如果产品只记录 UI progress,不记录进入模型的规范化消息,就无法可靠 resume。
源码调研主体
本组解读基于冻结源码快照进行教育、防御和架构研究。
本期主题:Harness 如何把用户输入转化为稳定、可执行、上下文充分的模型认知窗口。
1. 研究问题
本期围绕一个问题展开:
Claude Code Harness 如何在真正执行工具之前,让模型先“看对问题”?
这比工具执行更靠前。一个 coding agent 稳定解决问题,第一步不是执行命令,而是把用户意图、项目状态、系统约束、文件上下文、记忆、任务状态和权限上下文组织成模型可用的输入窗口。
2. 对应总览节点
对应第 1 期总览图中的节点:
User InputContext PreparationMemoryLifecycle HooksTranscript / Session StateQuery Loop
这部分机制参考前文「Context 构建 机制图」。
3. 核心流程图
这部分机制参考前文「Context 构建 机制图」。
4. 关键源码入口
| 职责 | 文件 |
|---|---|
| 会话入口与上下文总装 | src/QueryEngine.ts |
| system/user/system context 获取 | src/utils/queryContext.ts |
| 系统与用户上下文来源 | src/context.ts |
| 用户输入处理主入口 | src/utils/processUserInput/processUserInput.ts |
| 普通文本 prompt 转消息 | src/utils/processUserInput/processTextPrompt.ts |
| 附件体系 | src/utils/attachments.ts |
| memory prompt | src/memdir/memdir.ts |
| message 创建与规范化 | src/utils/messages.ts |
| transcript 写入 | src/utils/sessionStorage.ts |
4.1 主线与场景特殊处理分类
| 机制/模块 | 分类 | 为什么这样归类 |
|---|---|---|
QueryEngine.submitMessage() | 主线机制 | 会话 turn 的入口,负责把输入、上下文、工具环境和模型请求串起来 |
fetchSystemPromptParts() | 主线机制 | 构造 API 前缀上下文,是模型请求前的基础步骤 |
processUserInput() | 主线机制 | 将用户输入归一化为 Message,是进入模型前的必经处理 |
processTextPrompt() | 主线机制 | 普通 prompt 的最终消息构造路径 |
ToolUseContext / ProcessUserInputContext | 主线机制 | 后续工具、权限、Hook、附件与状态访问共享的运行时上下文 |
getAttachmentMessages() | 主线机制 + 场景增强聚合点 | 附件聚合是主线,但内部大量附件按场景条件启用 |
getUserContext() 中的 CLAUDE.md | 场景增强 | 依赖项目中是否存在相关指令文件,也可被环境变量或 bare mode 影响 |
getSystemContext() 中的 git status | 场景增强 | 只在 git 仓库、非 remote、git instructions 未关闭时注入;不是运行时依赖 |
| IDE selection / opened file | 场景增强 | 只在 IDE 连接并提供选区或打开文件时注入 |
| LSP diagnostics / diagnostics | 场景增强 | 依赖诊断系统或 LSP 状态,不是常规文本任务必经路径 |
| UserPromptSubmit Hooks | 安全/治理横切 | 可在模型请求前补充上下文、阻断继续或改变输入效果 |
| transcript 写入 | 可观测/恢复支撑 | 在模型请求前持久化用户消息,支撑 resume 和审计 |
| image resize / image metadata | 场景增强 | 只在 content blocks 或 pasted images 存在时触发 |
| queued commands / task notifications | 场景增强 + 可观测/恢复支撑 | 只在队列存在时注入,支撑异步任务和通知进入当前 turn |
5. 机制拆解
5.1 QueryEngine 是 Context 构建的总装层
QueryEngine.submitMessage() 做的第一件事不是调用模型,而是冻结本轮所需的运行参数:
- 当前工作目录。
- 命令集合。
- 工具集合。
- MCP 连接。
- agent 定义。
- 权限检查函数。
- AppState 读写函数。
- read file cache。
- thinking config。
- 模型选择。
随后它构造 ProcessUserInputContext。这个上下文继承自 ToolUseContext,但在输入处理阶段多了 slash command、本地 UI、附件处理所需的能力。
5.2 fetchSystemPromptParts 构造 API 前缀上下文
fetchSystemPromptParts() 返回三类上下文:
| 上下文 | 来源 | 作用 |
|---|---|---|
defaultSystemPrompt | getSystemPrompt() | 规定模型行为、工具说明、运行策略 |
userContext | getUserContext() | 注入 CLAUDE.md、当前日期等用户/项目上下文 |
systemContext | getSystemContext() | 注入可选系统状态快照,例如 git status、cache breaker |
这三类上下文构成模型请求的稳定前缀。它们被单独构造,是为了让 prompt cache 和上下文边界更可控。
5.3 context.ts 提供可选项目状态快照
getUserContext() 和 getSystemContext() 是项目环境进入模型窗口的重要入口。
getUserContext() 主要负责:
- 发现并读取 CLAUDE.md 类记忆/指令文件。
- 写入当前日期。
- 缓存 CLAUDE.md 内容,供 auto-mode classifier 等机制复用。
getSystemContext() 主要负责注入会话级系统状态快照。git 不是运行时依赖,也不是 Context 构建主线,只是 coding 场景下的可选项目状态信号:
- 在非 remote、且 git instructions 未关闭时,读取一次 git status。
- 读取当前分支、main branch、最近提交、git user,并明确标注这是 conversation start 的快照。
- 注入 cache breaker 等系统级调试信息。
这些信息不是用户显式输入,但在存在 git 仓库时会影响模型对项目初始状态的判断。源码中的提示文本明确说明该 git status 是 “at the start of the conversation” 的快照,并且 “will not update during the conversation”。因此它不是运行中动态反馈机制,也不是非 git 项目必须具备的条件;运行中的 git 变化仍需要通过工具调用、附件、diff/status 命令或其他上下文刷新路径进入模型。
5.4 processUserInput 是“输入归一化器”
processUserInput() 把不同来源的输入统一成消息:
这部分机制参考前文「Context 构建 机制图」。
它处理的输入形态包括:
- 普通字符串 prompt。
- SDK / IDE 传入的 content blocks。
- 粘贴图片。
- slash command。
- bash mode 输入。
- remote bridge 输入。
- agent mention。
- ultraplan keyword。
- IDE selection。
这说明 Context 构建不是简单地把用户文本塞进 API,而是一个多入口归一化过程。
5.5 getAttachmentMessages 是上下文扩展中枢
getAttachmentMessages() 是本期最关键的函数之一。它负责把“用户没有直接写在 prompt 里,但模型应该看到”的信息注入上下文。
附件分三类:
这部分机制参考前文「Context 构建 机制图」。
这种分层有明确目的:
- 用户输入附件先执行,因为它可能触发 nested memory。
- thread-safe 附件可以在主线程和 subagent 中使用。
- main-thread 附件只给主会话使用,避免 subagent 拿到不该拿的 UI/IDE 状态。
5.6 UserPromptSubmit Hooks 在模型调用前介入
processUserInput() 在确认需要查询模型后,会执行 executeUserPromptSubmitHooks()。
Hook 可以做三类关键事情:
- 返回 blocking error,直接阻止本轮模型调用。
- 返回 prevent continuation,让原始 prompt 留在上下文中但停止继续。
- 返回 additional context,作为
hook_additional_context附件加入消息。
这意味着 Hooks 在工具执行前就能影响模型看到的问题定义。
5.7 processTextPrompt 形成最终用户消息
普通 prompt 最后进入 processTextPrompt()。它负责:
- 生成 prompt id。
- 记录 tracing / OTel 事件。
- 识别 negative / keep-going 类用户意图。
- 合并文本和图片 content blocks。
- 创建
UserMessage。 - 将附件追加在 user message 之后。
输出结构通常是:
UserMessage
AttachmentMessage[]这组消息随后被 QueryEngine 推入 mutableMessages,并在模型请求前写入 transcript。
5.8 模型请求前先写 transcript
QueryEngine.submitMessage() 在进入 query() 之前,会把用户消息写入 transcript。
这个设计对稳定性很关键:如果进程在模型响应前被杀掉,用户输入仍然已经落盘,后续 resume 不会丢失任务入口。
6. 对稳定解决问题能力的贡献
6.1 提升问题理解完整性
模型看到的不只是用户 prompt,还包括:
- 项目指令。
- 当前日期。
- git 状态。
- IDE 选区。
- 打开的文件。
- diagnostics。
- changed files。
- queued commands。
- plan / todo / task 状态。
- memory 与 nested memory。
这让模型在行动前拥有更完整的问题背景。
6.2 降低上下文遗漏风险
附件系统把隐含上下文显式化。例如 IDE 选区、LSP diagnostics、todo reminders 都不是用户自然语言的一部分,但对 coding task 很关键。
6.3 保持主线程与 subagent 上下文边界
附件分为 thread-safe 和 main-thread,避免 subagent 误拿主线程专属状态。这是多 agent 稳定性的基础。
6.4 支持恢复与审计
模型请求前先写 transcript,使任务入口可恢复。即使模型尚未返回,系统也知道用户提交了什么。
6.5 支持前置纠偏
UserPromptSubmit Hook 可以在模型调用前阻止、补充或改写上下文。这比工具执行后再纠偏更早。
7. 风险、边界与待验证问题
7.1 Context 构建复杂度高
getAttachmentMessages() 聚合了大量上下文来源。优点是完整,风险是隐藏耦合强:某个附件异常、过大或时序不当,可能影响模型输入质量。
7.2 上下文完整性与噪声之间存在张力
越多附件不一定越好。计划提醒、todo、diagnostics、memory、changed files 都可能提高完整性,也可能挤占注意力窗口。
7.3 prompt cache 与动态上下文之间存在张力
fetchSystemPromptParts() 把 system prompt、user context、system context 作为稳定前缀,但 git status、CLAUDE.md、动态附件等都会影响缓存命中和上下文一致性。
7.4 Hook 过早介入可能改变用户意图
UserPromptSubmit Hook 可以阻断或补充上下文。它是稳定性机制,也是潜在的行为变更入口。
8. 本期结论
本期结论是:
Claude Code Harness 的稳定性首先来自输入侧治理。它在模型调用前完成了用户输入归一化、项目上下文收集、附件扩展、记忆提示、Hook 前置纠偏、运行时上下文构造和 transcript 落盘。
这说明它不是把 prompt 直接交给模型,而是先构造一个可执行、可恢复、带边界的认知窗口。
9. 下一期衔接
第 3 期进入 Tools / Permissions 控制链路。
建议选择 BashTool 作为代表样本,但定位必须明确:
- 它不是 Harness 全貌的代表。
- 它是行动能力与安全控制链路的代表。
- 研究重点是模型如何从“看对问题”进入“受控行动”。
第 3 期应追踪:
tool_use -> StreamingToolExecutor / runTools -> runToolUse -> canUseTool -> checkPermissions -> Hooks -> BashTool.call -> shell execution -> tool_result10. 拓展阅读:Context 构建澄清问答
本节记录第 2 期报告形成后的澄清问题。重点是区分主线机制与场景增强,避免把可选能力误写成 Harness 必经路径。
10.1 最终给模型 API 的完整 Prompt 如何构成?
问题:最终拼装出来给到模型 API 的完整 Prompt 的构成方式是什么?它们的前后关系如何影响模型 attention 和 cache 命中率?
澄清:最终请求不是一个单字符串 prompt,而是 Anthropic messages API 请求中的三大部分:
API request
├─ system: system prompt blocks
├─ messages: user / assistant / tool_result message array
└─ tools: tool schemas主线关系:
这部分机制参考前文「Context 构建 机制图」。
具体顺序:
QueryEngine.submitMessage()获取defaultSystemPrompt、userContext、systemContext。- 组装
systemPrompt = default/custom + memoryMechanicsPrompt + appendSystemPrompt。 query()中通过appendSystemContext(systemPrompt, systemContext)得到fullSystemPrompt。query()中通过prependUserContext(messagesForQuery, userContext)把 user context 包成一个 synthetic user message 前置到 messages。- API 层调用
normalizeMessagesForAPI()规范化 messages。 - API 层调用
buildSystemPromptBlocks()把 system prompt 拆成可缓存的 system blocks。 - 最终请求同时包含
system、messages、tools。
对 attention 的影响:
system承载最高层行为约束和工具行为说明。userContext被包装成<system-reminder>synthetic user message,并提示 “may or may not be relevant”。- attachments 跟随用户消息进入 messages,对当前任务局部上下文影响更直接。
- tool results 在后续轮次回灌,影响模型下一步行动。
对 cache 命中率的影响:
- system prompt 会按静态/动态边界拆 block。
- 静态 system prompt 可使用 global/org cache scope。
- 动态内容如 system context、append prompt、Chrome tool instructions、部分 tool schema 变化会影响缓存。
- deferred tools / tool search 用来减少大工具池对上下文和缓存的冲击。
分类:
| 机制 | 分类 | 说明 |
|---|---|---|
system blocks | 主线机制 | 每次模型请求的核心指令区 |
messages array | 主线机制 | 会话、附件、工具结果的主要承载体 |
tools schemas | 主线机制 | agentic coding 能力的行动接口 |
| prompt cache block 拆分 | 可观测/恢复支撑 + 性能支撑 | 不改变语义主线,但影响效率和稳定性 |
| deferred tools / tool search | 场景增强 + 性能支撑 | 工具池大或动态工具多时降低上下文和 cache 压力 |
10.2 Image 处理是否真实存在?具体方案是什么?
问题:模型是多模态模型,user input 中提前处理 image 等信息是否真实存在?处理方案有哪些?
澄清:真实存在。图片处理不是伪处理,最终会生成 Anthropic image content block 进入 UserMessage。
两类输入路径:
- SDK/IDE 直接传入的
ContentBlockParam[]中包含 image block。 - 用户粘贴图片,进入
pastedContents。
处理流程:
这部分机制参考前文「Context 构建 机制图」。
具体处理:
- 只对
source.type === 'base64'的 image block 做 resize/downsample。 - 从
media_type推断扩展名。 - base64 decode 成 buffer 后压缩或缩放。
- 返回新的 image content block:
{
type: "image",
source: {
type: "base64",
media_type: "image/...",
data: "..."
}
}- 粘贴图片还会被
storeImages()保存到本地,以便模型后续通过工具引用图片路径。 - 如果能获取尺寸,则生成 image metadata;如果只有 source path,也会生成 source path metadata。
最终进入模型的内容:
| 内容 | 是否进入模型 | 形式 |
|---|---|---|
| image content block | 是 | UserMessage.content 中的多模态 block |
| image dimensions/source metadata | 是 | isMeta user message |
| 本地保存的图片文件 | 不直接作为图片进入 API | 作为路径信息供后续工具引用 |
分类:
| 机制 | 分类 | 说明 |
|---|---|---|
| 普通文本 prompt | 主线机制 | 常规输入路径 |
| image content block | 场景增强 | 只在多模态输入或粘贴图片时触发 |
| image resize/downsample | 场景增强 + 安全/治理横切 | 受 API 图片大小/尺寸限制驱动,防止请求失败 |
| image metadata isMeta message | 场景增强 | 辅助模型理解图片尺寸和来源 |
10.3 loadMemoryPrompt when enabled 的 when 由什么决定?
问题:流程中出现 loadMemoryPrompt when enabled,这里的 “when” 具体由哪些因素决定?
澄清:主要由 isAutoMemoryEnabled() 和若干产品/feature 场景决定。
isAutoMemoryEnabled() 的优先级:
1. CLAUDE_CODE_DISABLE_AUTO_MEMORY=true/1 -> 关闭
2. CLAUDE_CODE_DISABLE_AUTO_MEMORY=false/0 -> 开启
3. CLAUDE_CODE_SIMPLE / --bare -> 关闭
4. CLAUDE_CODE_REMOTE 且没有 CLAUDE_CODE_REMOTE_MEMORY_DIR -> 关闭
5. settings.autoMemoryEnabled 显式设置 -> 使用设置值
6. 默认开启loadMemoryPrompt() 内部再分场景:
| 条件 | 结果 |
|---|---|
feature('KAIROS') && autoEnabled && getKairosActive() | 使用 daily log prompt |
feature('TEAMMEM') && team memory enabled | 使用 auto + team combined memory prompt |
autoEnabled | 创建/确认 auto memory dir,返回普通 auto memory prompt |
| 否则 | 返回 null |
还有一个 SDK/custom prompt 特殊路径:
customSystemPrompt 存在
AND CLAUDE_COWORK_MEMORY_PATH_OVERRIDE 有效满足时,QueryEngine.submitMessage() 会额外注入 memory mechanics prompt。原因是 custom system prompt 会替换默认 system prompt,必须显式补回 memory 机制说明。
分类:
| 机制 | 分类 | 说明 |
|---|---|---|
isAutoMemoryEnabled() | 场景增强开关 | memory 不是所有运行模式必需 |
| 普通 auto memory prompt | 场景增强 | 默认可能开启,但仍可被 env/settings/simple/remote 条件关闭 |
| KAIROS daily log prompt | 场景增强 | 产品/feature 模式 |
| TEAMMEM combined prompt | 场景增强 | team memory 模式 |
| Cowork override + custom prompt | 场景增强 | SDK/custom system prompt 场景补偿 |
10.4 特殊输入路径分别是什么?目的和特殊处理是什么?
问题:slash command、bash mode 输入、remote bridge 输入、agent mention、ultraplan keyword、IDE selection 分别对应哪些方面?目的是什么?处理过程中做了哪些特殊针对处理?
澄清:这些都不是同一层级的主线机制。它们是围绕主线输入处理的场景增强或安全治理路径。
| 项 | 分类 | 目的 | 特殊处理 |
|---|---|---|---|
| slash command | 场景增强 / 命令入口 | 让用户用 /compact、/clear、/hooks 等触发本地命令或 prompt command | input 以 / 开头且未 skip 时走 processSlashCommand();slash command 的附件提取在 command 内部处理 |
| bash mode 输入 | 场景增强 / 直接 shell 输入 | 用户手动执行 shell,不是模型发起工具调用 | 走 processBashCommand();包装 <bash-input>;调用 BashTool.call() 或 PowerShellTool.call();dangerouslyDisableSandbox: true;结果包装为 <bash-stdout> / <bash-stderr>;shouldQuery: false |
| remote bridge 输入 | 场景增强 / 远程控制 + 安全治理 | mobile/web/remote control 发来的输入,避免远端触发本地 UI 型命令 | 默认 skipSlashCommands;若 bridgeOrigin 且命令通过 isBridgeSafeCommand() 才允许执行;local-jsx 禁止,prompt command 安全,少数 local command allowlist |
| agent mention | 场景增强 / 子代理路由提示 | 用户用 @agent-xxx 表达要某个 agent 参与 | processAgentMentions() 提取 mention,匹配 active agent,生成 agent_mention attachment;找不到 agent 会记录失败 |
| ultraplan keyword | 场景增强 / 计划模式快捷入口 | 用户自然语言触发 /ultraplan | 只在 interactive prompt、非 slash、非 headless、未已有 ultraplan session 时触发;用 pre-expansion input 检测,避免 pasted content 误触发 |
| IDE selection | 场景增强 / IDE 上下文注入 | 把编辑器选区或当前打开文件给模型 | 只在 IDE 连接、存在 filePath/text/lineStart 时注入;会检查 isFileReadDenied(),被权限 deny 的文件不会注入 |
主线与场景关系:
主线:
User input -> processUserInput -> UserMessage/AttachmentMessage -> query
场景增强:
slash command / bash mode / bridge / agent mention / ultraplan / IDE selection / images
安全横切:
bridge-safe command allowlist
file read deny check for IDE context
UserPromptSubmit hooks
可观测/恢复:
prompt id
OTel user_prompt
transcript before model call重要边界:
- slash command 是用户显式控制 CLI 功能,不等同于模型工具调用。
- bash mode 是用户自己执行 shell,不是模型
tool_use。 - remote bridge 是跨设备输入路径,需要额外限制本地 UI 和命令执行。
- agent mention 是上下文提示/路由信号,不等于一定 spawn agent。
- ultraplan keyword 是自然语言快捷入口。
- IDE selection 是上下文增强,不是必经路径。
机制主线收束
站在架构师视角,我会把这组源码机制收束为三个问题:为什么需要它,什么时候必须引入它,以及真正要设计的是什么。
- why:Context 解决的是模型开始前是否看对问题,而不是让模型看更多材料。
- when:当输入来源超过纯文本,例如图片、IDE、memory、项目规则、Hook,必须引入装配线。
- what:按来源分层、按作用域过滤、按生命周期进入消息窗口,并把用户消息先落 transcript。
Context 构建 这组源码最值得带走的,不是某个函数或某个配置项,而是它如何把模型能力放进一组可验证、可拒绝、可恢复、可审计的工程边界里。读源码时,如果只记住名词,会很快散;如果抓住 why、when、what,就能把这组机制迁移到自己的 agent 架构判断中。