claude code原理浅析
claude code原理浅析

前置知识
在深入原理之前,先交代几个本文会反复出现的概念:
- Claude Code:Anthropic 推出的命令行 AI 编程助手。它不仅能回答问题,还能直接读写文件、执行命令、搜索代码——这类能自主调用工具、完成多步任务的 AI 通常称为 Agent(智能体)。
- 大模型(LLM):如 GPT、Claude 等,输入和输出都以 Token 为单位。Token 可粗略理解为「词片段」,大约 4 个字符 ≈ 1 个 Token。
- 上下文窗口:大模型一次能「看到」的文本长度上限,以 Token 计。一旦对话超过窗口,就必须压缩或丢弃部分内容——这也是本文后半部分「上下文窗口管理」要解决的核心问题。
- MCP(Model Context Protocol):Anthropic 提出的开放协议,定义了大模型与外部工具服务器之间的通信方式,可以类比为「AI 的 USB 接口」。
首先采用四层分层架构
- 引擎层是 Agent 的「大脑」,负责思考和调度。它的关键设计原则是不包含任何业务逻辑,它不知道怎么读文件、怎么改代码、怎么搜索,这些全是工具层的事。引擎层只做三件事:第一,协调,把用户输入、系统指令、历史对话拼在一起,发给大模型;第二,分发,大模型说「我要用某个工具」时,找到对应的工具并执行;第三,决策,根据大模型的返回决定是继续循环还是结束对话。这种设计的好处是:新增能力只需要新增一个工具,引擎层完全不用改。
- 工具层是 Agent 的全部「能力」,40 多个工具都在这一层。每个工具就是 Agent 的一个能力:执行 Shell 命令、读写文件、搜索代码、生成子 Agent……这些工具不是随便写的,它们遵循一个统一的规范。这个规范不仅定义了「工具能做什么」,还强制定义了三个安全属性:这个工具是只读的还是会改东西的?它是否具有破坏性需要额外确认?它能不能和其他工具同时执行?这三个属性不是「建议加上」的,而是类型系统强制要求的,漏了任何一个,代码就编译不过。这意味着每一把刀都有刀鞘,从出厂就配好了安全机制。
- 服务层是所有层共享的「基础设施」。这一层包括三样东西:调大模型 API(不管是谁要调,主循环也好、子 Agent 也好,都走这一层)、上下文压缩(后面会详细讲的五步压缩策略)、MCP 协议(和外部工具服务器通信的标准接口)。
- 安全与治理层有点特殊,权限系统决定哪些操作需要用户确认、哪些可以自动执行;Hook 系统允许在工具执行前后插入自定义行为(比如「每次 git push 前自动跑 lint」);Bash 安全模块会对 Shell 命令做语法级别的分析,检测命令注入、路径逃逸等危险模式,而不是简单地用正则匹配关键词。
Tool-Use Loop VS ReAct
这里说的 ReAct 不是前端框架 React,而是来自论文 ReAct: Synergizing Reasoning and Acting in Language Models 的 Agent 范式——让模型在「推理(Reason)」和「行动(Act)」之间交替:先思考下一步该做什么,再调用工具执行,再根据结果继续思考。Claude Code 没有沿用这一范式,而是采用了更直接的 Tool-Use Loop(工具调用循环)。

query()核心循环代码如下
async function* queryLoop(
params: QueryParams,
consumedCommandUuids: string[],
): AsyncGenerator<StreamEvent | Message, Terminal> {
let state: State = { messages, toolUseContext, turnCount: 1, ... }
while (true) {
// 步骤 1:压缩上下文(五步从轻到重)
// 步骤 2:调用大模型 API,流式接收
for await (const event of streamAPI(params)) {
yield event // 流式输出每个 token
}
// 步骤 3:分析模型返回
if (response.stopReason === 'end_turn') break // 完成了,跳出循环
// 步骤 4:执行工具调用(并发/串行编排)
const toolResults = await executeToolCalls(toolUseMessages)
// 步骤 5:更新 state,继续循环
state = { ...state, messages: updatedMessages, turnCount: turnCount + 1 }
continue
}
}

如果判断出任务是复杂任务,还会调用工具EnterPlanMode (只读)和 ExitPlanMode

System Prompt 的构造
把提示词分为多个 section(段落),然后再拼接
基本的角色定义与安全行为准则
关于失败处理:「先诊断再换方案」(而不是重试)
破坏性的操作需要用户确认
注入环境信息
环境信息,记忆文件,MCP服务根据用户动态变化
三级缓存机制:最高降低90%的费用
全局缓存(跨组织跨用户共享)→ 组织缓存(同一组织内跨会话共享)→ 会话缓存(同一个 Section 在一次会话内只计算一次)
缓存存的是 KV cache(大模型推理时为注意力机制预先计算的中间结果,命中后可直接复用、不必重算,从而大幅降低延迟和费用)

记忆系统
目前常见做法是对记忆做 embedding(把文本转换成高维向量,再通过向量相似度来检索相关记忆),每次对话做相似度检索
cc把记忆分为四类
export const MEMORY_TYPES = [
'user', // 用户画像:角色、偏好、知识水平
'feedback', // 行为反馈:该做什么、不该做什么
'project', // 项目动态:在做什么、截止日期、协作信息
'reference', // 外部指针:哪里能找到什么信息
] as const

关于保存时机和使用方式是由大模型进行解析
每条记忆都会保存为一个独立的.md文件,内容如下:
---
name: no-mock-database
description: 集成测试必须使用真实数据库,不能用 mock
type: feedback
---
集成测试必须使用真实数据库,不能用 mock。
**Why:** 上季度 mock 测试全部通过但生产环境迁移失败了。
**How to apply:** 在这个模块写测试时,始终连接真实数据库。
在一个用户的描述中,可能会拆分成多个不同类型的md文件
使用memory.md作为索引文件来进行索引(最多200行 25KB,也就是说记忆是比较轻量的),加入System prompt中
- [No Mock Database](feedback_no_mock_db.md) — tests must use real DB
- [User Preferences](user_preferences.md) — prefers terse responses
- [Auth Rewrite](project_auth_rewrite.md) — driven by compliance, not tech debt
使用小模型做记忆检索(Sonnet,即 Claude 系列中性价比更高的小型号)
扫描每个文件前30行,拼接起来
和用户当前输入一起发给sonnet,返回一个文件名列表,不是记忆内容本身
最后加载相应的md文件,注入上下文。(超过1天的记忆会进行提示)
召回是和API调用并行执行的,当用户提交消息时就开始了

这里其实需要注意一个点,就是用户提问-》AI回答这个过程其实包含了两次的记忆注入
- 第一轮:为“当前任务”规划:对话开始,系统就会将静态记忆(如项目级的
CLAUDE.md)作为高优先级的基础规则,注入到你第一轮提问的上下文中。这确保了AI在制定初始计划时,就能遵循你的基本偏好和项目规范。 - 后续轮:为“当前任务”执行:在你获得回答前,任务本身可能已触发多轮内部处理。在每一轮中,AI都可能调用工具来执行子任务。就在第一轮之后的某个工具调用完成、准备进入下一轮推理时,动态记忆(
startRelevantMemoryPrefetch()的结果)便会以<system-reminder>的形式被悄悄地注入到AI的上下文中。
上下文窗口管理
常见做法是做截断和上下文压缩
cc的想法是压缩一定有信息损失,所以能不压就不压,从最轻微的手段开始

1.大结果存磁盘
async function maybePersistLargeToolResult(
toolResultBlock: ToolResultBlockParam,
toolName: string,
): Promise<ToolResultBlockParam> {
const size = contentSize(content)
// 单个工具结果超过阈值(默认约 50KB)?
if (size <= threshold) {
return toolResultBlock // 没超,原样通过
}
// 超了!把完整内容存到磁盘文件
const result = await persistToolResult(content, toolUseId)
// 用一个 2KB 的预览替换原内容
const preview = buildLargeToolResultMessage(result)
return { ...toolResultBlock, content: preview }
}
存入磁盘后,如果后续还需要该文档内容,可以调用read进行读取
同一条消息里所有工具结果的总大小不能超过 200KB。如果超了,系统会挑出最大的那几个结果存磁盘,直到总量降到限制以内。
2.移除旧消息
使用snip,首先确保关键信息被保留(如待办事项列表或 Plan 模式的执行计划,用户做出的“允许/拒绝”工具调用的决策记录还有一些系统消息需要保留)
动态判断中间消耗的 token 数,如果超过阈值就触发
保留开头和结尾,只移除处于中间的早期过程数据(如模型尝试时的报错,探索性搜索和文件读取等)
清理完后会插入一个边界标记,并把会把压缩的token告诉第5层,防止重复压缩
3.裁剪旧的工具输出
经过上面两步,留下的是不太老也不太新的消息,但是可能出现30 分钟前读的一个文件,现在那个文件可能已经被改过了。
不是所有的工具输出都能被裁剪
const COMPACTABLE_TOOLS = new Set([
FILE_READ_TOOL_NAME, // 读文件 → 可以重新读
...SHELL_TOOL_NAMES, // 执行命令 → 可以重新执行
GREP_TOOL_NAME, // 搜索 → 可以重新搜
GLOB_TOOL_NAME, // 查找文件 → 可以重新查
WEB_SEARCH_TOOL_NAME, // 搜索网页 → 可以重新搜
FILE_EDIT_TOOL_NAME, // 编辑文件 → 结果可裁剪
FILE_WRITE_TOOL_NAME, // 写文件 → 结果可裁剪
])
可以被裁剪的,都是「可重新获取」的工具
裁剪逻辑如下:
// 收集所有可裁剪工具的结果 ID
const compactableIds = collectCompactableToolIds(messages)
// 保留最近 5 个,其余全部清理
const keepRecent = Math.max(1, config.keepRecent) // 至少保留 1 个
const keepSet = new Set(compactableIds.slice(-keepRecent))
const clearSet = compactableIds.filter(id => !keepSet.has(id))
//被裁剪的工具结果会被替换成一个标记:
export const TIME_BASED_MC_CLEARED_MESSAGE =
'[Old tool result content cleared]'
模型看到该标记可以选择重新执行工具
4.读时投影
如果经过前三步,发现上下文还是太大
所以考虑做全量摘要,首先考虑进入一个中间态(读时投影)
上下文窗口达到阈值时,触发折叠(AI模型对完整对话生成摘要,持久化提交日志和该摘要)可建立相应的映射关系
对于受保护的对话轮次,不会触发折叠
如:
- 最近的N个轮次:为了保留当前工作流的连续性。
- 系统级指令:如
CLAUDE.md文件中的项目规则和 TodoWrite 的待办事项列表。 - 关键决策记录:例如你对某个工具调用的“允许”或“拒绝”权限。

如果占用达到95%,则触发阻塞式保存。冻结当前主线程,强制生成并持久化一个“提交日志”
5.全量摘要
上下文长度达到一定阈值触发
如:
以 200K Token 的模型为例:有效窗口大约 180K(预留 20K 给输出),减去 13K 缓冲区,当上下文达到 167K Token 时触发。
首先生成摘要(提示词工程)
摘要替换旧消息,并打上标记
重新注入
压缩完系统还会从文件状态缓存(fileStateCache)中找出最近访问过的文件,按最后访问时间排序,挑选最多 5 个、总共不超过 50K Token 的文件内容重新注入。同时恢复活跃的 Skill(不超过 25K Token),如果有进行中的 Plan 也会恢复 Plan 文件。
如果全量摘要连续失败三次,会自动放弃,进入熔断模式,不会无限重试。
浙公网安备 33010602011771号