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 又在主循环之外持续维护长期连续性。
1. 模型交互前预处理机制
这一层真正做的事,是把原始终端输入编译成主循环可消费的结构化 turn。它同时处理控制流、内容归一化、prompt 路由和上下文前置,不是单纯的“把输入发给模型”。
1.1 机制目的
- 控制输入生命周期:
handlePromptSubmit()决定本次输入是立即执行、进入队列,还是通过 submit-interrupt 打断当前工具回合。 - 统一输入形态:文本、粘贴文本、粘贴图片、content blocks、远程 bridge 消息都会在
processUserInputBase()中被归一成内部消息结构。 - 在模型前做确定性改写:
exit会重写成/exit,ULTRAPLAN会重写成/ultraplan,remote slash command 只有 bridge-safe 时才在本地执行。
1.2 核心调用链
普通输入的主链路是:handlePromptSubmit() → executeUserInput() → processUserInput() → processUserInputBase() → processBashCommand/processSlashCommand/processTextPrompt → executeUserPromptSubmitHooks() → 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()进入最终请求。
2. 工具体系与调度编排
ClaudeCode 的工具系统本质上是一套协议栈,而不是“模型调用本地函数”。工具定义、暴露范围、执行顺序、权限决策、hook 与 transcript 回写都在这一层完成。
2.1 Tool 抽象为什么是胖接口
- 协议定义:
src/Tool.ts中的Tool至少包含inputSchema、validateInput、isConcurrencySafe、interruptBehavior、backfillObservableInput、mapToolResultToToolResultBlockParam等切面。 - 运行时能力对象:
ToolUseContext不只是执行参数,还携带messages、abortController、toolDecisions、readFileState、getAppState()等整轮 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()。
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:
readFileState、contentReplacementState更像执行缓存而不是语义记忆。
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_unchangedstub。 - 更新方式:系统不会直接拼字符串,而是用
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/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 = 24,minSessions = 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 在注释层高度可疑,但当前快照里 recordConsolidation() 没有可见调用点,因此不能当成已闭合事实。5. 技术总结与架构启示
- 整体架构意图:把主对话循环集中到
query(),把确定性的上下文工程前移到预处理和 runtime gating,把长期维护任务放到受限 subagent 和后台 housekeeping。 - 最大优势:prompt cache 意识强、工具协议统一、memory 分层清晰、后台 consolidation 能主动维护上下文 substrate。
- 最大复杂度来源:
query.ts和ToolUseContext承载了过多横切关注点,很多行为需要跨 compact、permissions、hooks、tool loop、memory 才能推理完整。 - 关键 trade-off:系统获得了强 runtime 控制和强恢复能力,但代价是主循环厚重、feature gate 多、学习曲线陡,而且缺少统一 repo graph 时,深层项目理解仍依赖搜索与读取回合。
放在现代 AI coding tool 语境里,这套实现更像一个 runtime context operating system:它持续装配上下文、持续执行外部动作、持续治理窗口、并在后台维护未来会话的记忆 substrate。