CC

ClaudeCode Source Analysis

交互深潜

交互深潜

ClaudeCode 的上下文工程不是单点功能,而是“输入预处理 + 工具执行协议 + 多层记忆 + 后台 consolidation”组成的运行时系统

这一页不再做宽泛概览,而是顺着真实代码路径解释:一次输入如何在进入模型前被规范化和压缩;工具如何被注册、筛选、调度和回写;memory 为什么会拆成 memdir、session memory、transcript、agent memory 多层;Dream 为什么本质上是一个后台 memory gardener。

本页回答什么问题

  • 用户输入在进入模型前到底会经历哪些重写、扩展和压缩?
  • 工具不是函数表,而是怎样一种执行协议?
  • ClaudeCode 如何在流式输出过程中并发执行工具,又保持 transcript 正确?
  • Memory 为什么被拆成 memdir、session memory、agent memory 和 transcript 多层?
  • Dream 在系统里到底扮演什么角色,哪些结论是已确认事实,哪些仍是推断?

总览图:一次 turn 里有哪些上下文工程层

这张图展示的是“主模型交互前后”的真实工程链路。输入先被编译成 turn,再被 query runtime 压到窗口预算内,之后才进入 tool loop,而 memory 与 Dream 又在主循环之外持续维护长期连续性。

文件映射:src/utils/handlePromptSubmit.tssrc/utils/processUserInput/processUserInput.tssrc/utils/queryContext.tssrc/query.tssrc/services/tools/*src/utils/sessionStorage.tssrc/memdir/*

关键观察:ClaudeCode 的 context engineering 分成“输入级预处理”和“query 级预处理”两层,后者会在每一轮 query 前持续重写上下文窗口,而不是一次性选完上下文后交给模型。

1. 模型交互前预处理机制

这一层真正做的事,是把原始终端输入编译成主循环可消费的结构化 turn。它同时处理控制流、内容归一化、prompt 路由和上下文前置,不是单纯的“把输入发给模型”。

1.1 机制目的

  • 控制输入生命周期:handlePromptSubmit() 决定本次输入是立即执行、进入队列,还是通过 submit-interrupt 打断当前工具回合。
  • 统一输入形态:文本、粘贴文本、粘贴图片、content blocks、远程 bridge 消息都会在 processUserInputBase() 中被归一成内部消息结构。
  • 在模型前做确定性改写:exit 会重写成 /exitULTRAPLAN 会重写成 /ultraplan,remote slash command 只有 bridge-safe 时才在本地执行。

1.2 核心调用链

普通输入的主链路是:handlePromptSubmit()executeUserInput()processUserInput()processUserInputBase()processBashCommand/processSlashCommand/processTextPromptexecuteUserPromptSubmitHooks()onQuery()query()

  • 入口层:handlePromptSubmit.ts 先用 parseReferences()expandPastedTextRefs() 处理粘贴引用,再过滤孤儿图片。
  • 队列层:queryGuard 配合 enqueue() 保证主 REPL 一次只跑一个主 query;若当前工具都声明可取消,新输入会触发 interrupt。
  • 批处理层:executeUserInput() 对 queued commands 逐个调用 processUserInput(),且只有第一条命令携带 attachments 和 pastedContents,后续命令会 skipAttachments

1.3 输入编译的内部细节

  • 图片路径稳定化:processUserInputBase() 会先 storeImages() 落盘,再并行 resize 图片,并把尺寸与路径写成 isMeta 消息。
  • bridge 安全边界:remote bridge 输入默认不会触发本地 slash commands,只有 isBridgeSafeCommand() 允许的命令例外。
  • 显式 prompt routing:hasUltraplanKeyword() 命中后立即改写成 /ultraplan,而不是让模型自行推断。
  • 三路 lowering:bash 模式直接调用本地工具且 shouldQuery: false;slash command 再按 local-jsx / local / prompt 分派;普通文本走 processTextPrompt()

1.4 hooks 和附件如何进入模型上下文

getAttachmentMessages() 负责把文件、目录、IDE selection、memory、task、规则文件、agent/skill 线索整理成 attachment messages。之后 executeUserPromptSubmitHooks() 还可以阻断 continuation、附加 hook_additional_context,甚至用 warning message 替换原始 prompt。

1.5 query-time packing

  • 窗口裁剪:getMessagesAfterCompactBoundary()applyToolResultBudget()snipCompactIfNeeded()microcompact() 共同控制窗口大小。
  • 投影与折叠:contextCollapse.applyCollapsesIfNeeded() 在 autocompact 之前运行,尽量保留比 summary 更细的上下文粒度。
  • 稳定前缀:fetchSystemPromptParts()getUserContext()getSystemContext() 构造 cache-critical prefix,再由 prependUserContext()appendSystemContext() 进入最终请求。
工程判断:ClaudeCode 把“能确定性完成的上下文工程”前移到代码侧,把真正需要推理的部分留给模型。这样做的收益是更稳的 prompt cache、更少的上下文噪声,以及更明确的安全边界。

2. 工具体系与调度编排

ClaudeCode 的工具系统本质上是一套协议栈,而不是“模型调用本地函数”。工具定义、暴露范围、执行顺序、权限决策、hook 与 transcript 回写都在这一层完成。

2.1 Tool 抽象为什么是胖接口

  • 协议定义:src/Tool.ts 中的 Tool 至少包含 inputSchemavalidateInputisConcurrencySafeinterruptBehaviorbackfillObservableInputmapToolResultToToolResultBlockParam 等切面。
  • 运行时能力对象:ToolUseContext 不只是执行参数,还携带 messagesabortControllertoolDecisionsreadFileStategetAppState() 等整轮 query 的状态。

2.2 工具如何注册和筛选

  • 基础工具池:getAllBaseTools() 收集 built-in tools,并按 feature gate 决定是否引入某些工具,例如 ToolSearch。
  • 可见工具池:getTools(permissionContext) 再根据 mode、deny rules、REPL 隐藏规则和 isEnabled() 做二次过滤。
  • 最终暴露面:assembleToolPool() 将 built-in 与 MCP tools 合并,形成模型真正可见的工具集合。
  • 隐藏约束:deferred tool 的 schema 不一定真的发给模型;buildSchemaNotSentHint() 会在失败时提示先调用 ToolSearch。

2.3 单个工具调用的真实执行链

  • 入口:runToolUse() 先查当前可见工具池,不命中时再尝试 alias fallback。
  • schema 与业务校验:checkPermissionsAndCallTool() 先做 inputSchema.safeParse(),再做 tool.validateInput()
  • 输入改写:backfillObservableInput() 只在可观测副本上补字段,让 hooks 和 permissions 可见,但尽量不污染传给 tool.call() 的真实输入。
  • 执行协议:runPreToolUseHooks()resolveHookPermissionDecision()tool.call()runPostToolUseHooks() / runPostToolUseFailureHooks()

文件映射:src/tools.tssrc/Tool.tssrc/query.tssrc/services/tools/toolOrchestration.tssrc/services/tools/StreamingToolExecutor.tssrc/services/tools/toolExecution.ts

关键观察:主模型负责发出 tool_use,runtime 负责让这些 tool_use 在统一协议下执行、回写和恢复。

2.4 调度与并发语义

  • 普通批处理:partitionToolCalls()tool.isConcurrencySafe(parsedInput) 分并发批和串行批。
  • contextModifier 乱序保护:并发批里的工具可同时执行,但 contextModifier 会在批次结束后按原始 tool 顺序应用。
  • 流式执行:StreamingToolExecutor 在 assistant streaming 阶段边收到 tool_use 边执行,并维护 queued / executing / completed / yielded 状态。
  • 失败隔离:只有 Bash 错误会级联取消兄弟工具;普通读取类工具失败不会导致整个 batch 取消。

这套设计的优势是执行链统一、hook 和权限可以无缝插入、telemetry 可观测性强。代价是 Tool 抽象相当厚,工具作者必须理解 schema、permission、hook、mapping、interrupt、streaming 等多个维度。

3. Memory 记忆机制

这一层不是“有没有 memory 文件夹”这么简单。ClaudeCode 把连续性拆成 durable memory、session summary、transcript recovery 和 execution cache 几个层次,每层解决不同时间尺度的问题。

3.1 分层结构

  • durable memory:src/memdir/* 提供跨会话、文件化、按类型组织的 memory。
  • session memory:src/services/SessionMemory/sessionMemory.ts 维护当前会话的 markdown 摘要文件。
  • transcript continuity:src/utils/sessionStorage.ts / sessionRestore.ts 负责事实链持久化与恢复。
  • agent memory:src/tools/AgentTool/agentMemory.ts 等模块为子代理提供 user/project/local scope 记忆。
  • execution caches:readFileStatecontentReplacementState 更像执行缓存而不是语义记忆。

3.2 memdir 检索为什么不是 embedding

findRelevantMemories() 的路径是:scanMemoryFiles() 扫描 memory header,formatMemoryManifest() 组织候选,再用 sideQuery() 让小模型选最多 5 个文件名。这说明当前仓库并未展示向量库或 embedding index。

  • 异步预取:startRelevantMemoryPrefetch() 只拿最后一个真实用户消息作为 query,且完全不阻塞当前 turn。
  • 去重预算:已经 surfacing 过的 memory 会被过滤,避免每轮都重复附加同一份记忆。

3.3 session memory 的真实更新链

  • 触发阈值:sessionMemoryUtils.ts 默认要求初始化阈值 10000 tokens,更新阈值 5000 tokens,或 3 次工具调用,并且只在 querySource === 'repl_main_thread' 时运行。
  • 文件准备:setupSessionMemoryFile() 会创建 memory 文件、载入模板,并在读取前从 readFileState 中删除该路径,防止 dedup 返回 file_unchanged stub。
  • 更新方式:系统不会直接拼字符串,而是用 runForkedAgent() 启动受限 subagent,通过 createMemoryFileCanUseTool(memoryPath) 只允许它用 FileEditTool 编辑这一条确切路径。

3.4 transcript 与 continuity

ClaudeCode 的 continuity 核心不在单一 messages[]。真正的恢复基础是 transcript、compact boundary、forked-agent cache prefix 和 session restore 逻辑的组合。CacheSafeParams 负责稳定 systemPrompt/userContext/systemContext/forkContextMessages,让子代理复用 prompt cache 的同时,又能通过 createSubagentContext() 隔离父子线程的可变状态。

文件映射:src/memdir/findRelevantMemories.tssrc/utils/attachments.tssrc/services/SessionMemory/sessionMemory.tssrc/utils/sessionStorage.tssrc/utils/forkedAgent.ts

关键观察:memory 是分层协作系统。durable memory、session memory、transcript 和 execution cache 各自解决不同的问题,而不是互相替代。

重要设计原则:src/memdir/memoryTypes.ts 明确禁止把代码结构、架构、git 状态等可重新推导信息直接写入 memory。目标是让长期记忆保存“难以重新发现但值得长期保存的事实”,而不是复制当前仓库状态。

4. Dream 机制分析

从可见代码看,Dream 的真实角色是后台 memory consolidation。它的目标不是帮助当前对话“多想一步”,而是主动整理 durable memory,让未来会话更容易恢复上下文。

4.1 触发与门控

  • 入口:backgroundHousekeeping.ts 会调用 initAutoDream(),后者在闭包里维护 lastSessionScanAt,这是测试友好的设计。
  • 门控顺序:autoDream.ts 先检查 isGateOpen(),再读 lastConsolidatedAt,随后检查时间门限、scan throttle、session 数门限,最后才尝试拿锁。
  • 默认阈值:minHours = 24minSessions = 5,并通过 GrowthBook cached config 或用户设置覆盖。

4.2 锁和失败恢复

  • 锁文件双重语义:.consolidate-lock 的 mtime 既表示“锁存在”,又表示“上次 consolidation 时间”。
  • PID 校验:tryAcquireConsolidationLock() 写入 PID 后还会读回校验,避免两个 reclaiming 进程都误判自己拿到锁。
  • 失败回滚:rollbackConsolidationLock(priorMtime) 会把 mtime 回滚到尝试前的时间,确保失败的 dream 不会整体推迟下一次触发。

4.3 Dream 子代理如何运行

  • 子代理参数:runForkedAgent() 使用 querySource: 'auto_dream'forkLabel: 'auto_dream'skipTranscript: true,说明 Dream 是后台维护任务而不是普通 turn。
  • 权限约束:createAutoMemCanUseTool(memoryRoot) 只允许读 transcript / memory,并只允许在 auto-memory 目录内写。
  • 提示词结构:buildConsolidationPrompt() 把工作流拆成 Orient、Gather、Consolidate、Prune 四个阶段,并要求 index 文件保持尺寸上限。

4.4 UI 映射与不确定性

DreamTask.ts 并不实现 dream 逻辑,而是给后台 fork 映射一个可见 task。它只维护 starting / updating 两个 phase,并通过 assistant turn 的 Edit/Write tool_use 收集 filesTouched。源码注释明确承认这只是近似视图,无法捕获 bash 间接写入。

确认与不确定性:自动 Dream 是已确认实现。手动 /dream 在注释层高度可疑,但当前快照里 recordConsolidation() 没有可见调用点,因此不能当成已闭合事实。

5. 技术总结与架构启示

  • 整体架构意图:把主对话循环集中到 query(),把确定性的上下文工程前移到预处理和 runtime gating,把长期维护任务放到受限 subagent 和后台 housekeeping。
  • 最大优势:prompt cache 意识强、工具协议统一、memory 分层清晰、后台 consolidation 能主动维护上下文 substrate。
  • 最大复杂度来源:query.tsToolUseContext 承载了过多横切关注点,很多行为需要跨 compact、permissions、hooks、tool loop、memory 才能推理完整。
  • 关键 trade-off:系统获得了强 runtime 控制和强恢复能力,但代价是主循环厚重、feature gate 多、学习曲线陡,而且缺少统一 repo graph 时,深层项目理解仍依赖搜索与读取回合。

放在现代 AI coding tool 语境里,这套实现更像一个 runtime context operating system:它持续装配上下文、持续执行外部动作、持续治理窗口、并在后台维护未来会话的记忆 substrate。