claude code原理浅析

claude code原理浅析

img

前置知识

在深入原理之前,先交代几个本文会反复出现的概念:

  • 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(工具调用循环)。

img

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
  }
}

img

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

System Prompt 的构造

把提示词分为多个 section(段落),然后再拼接

基本的角色定义与安全行为准则

关于失败处理:「先诊断再换方案」(而不是重试)

破坏性的操作需要用户确认

注入环境信息

环境信息,记忆文件,MCP服务根据用户动态变化

三级缓存机制:最高降低90%的费用

全局缓存(跨组织跨用户共享)→ 组织缓存(同一组织内跨会话共享)→ 会话缓存(同一个 Section 在一次会话内只计算一次)

缓存存的是 KV cache(大模型推理时为注意力机制预先计算的中间结果,命中后可直接复用、不必重算,从而大幅降低延迟和费用)

img

记忆系统

目前常见做法是对记忆做 embedding(把文本转换成高维向量,再通过向量相似度来检索相关记忆),每次对话做相似度检索

cc把记忆分为四类

export const MEMORY_TYPES = [
  'user',      // 用户画像:角色、偏好、知识水平
  'feedback',  // 行为反馈:该做什么、不该做什么
  'project',   // 项目动态:在做什么、截止日期、协作信息
  'reference', // 外部指针:哪里能找到什么信息
] as const

img

关于保存时机和使用方式是由大模型进行解析

每条记忆都会保存为一个独立的.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调用并行执行的,当用户提交消息时就开始了

img

这里其实需要注意一个点,就是用户提问-》AI回答这个过程其实包含了两次的记忆注入

  • 第一轮:为“当前任务”规划:对话开始,系统就会将静态记忆(如项目级的CLAUDE.md)作为高优先级的基础规则,注入到你第一轮提问的上下文中。这确保了AI在制定初始计划时,就能遵循你的基本偏好和项目规范。
  • 后续轮:为“当前任务”执行:在你获得回答前,任务本身可能已触发多轮内部处理。在每一轮中,AI都可能调用工具来执行子任务。就在第一轮之后的某个工具调用完成、准备进入下一轮推理时,动态记忆startRelevantMemoryPrefetch()的结果)便会以<system-reminder>的形式被悄悄地注入到AI的上下文中。

上下文窗口管理

常见做法是做截断和上下文压缩

cc的想法是压缩一定有信息损失,所以能不压就不压,从最轻微的手段开始

img

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 的待办事项列表。
  • 关键决策记录:例如你对某个工具调用的“允许”或“拒绝”权限。

img

如果占用达到95%,则触发阻塞式保存。冻结当前主线程,强制生成并持久化一个“提交日志”

5.全量摘要

上下文长度达到一定阈值触发

如:

以 200K Token 的模型为例:有效窗口大约 180K(预留 20K 给输出),减去 13K 缓冲区,当上下文达到 167K Token 时触发

首先生成摘要(提示词工程)

摘要替换旧消息,并打上标记

重新注入

压缩完系统还会从文件状态缓存(fileStateCache)中找出最近访问过的文件,按最后访问时间排序,挑选最多 5 个、总共不超过 50K Token 的文件内容重新注入。同时恢复活跃的 Skill(不超过 25K Token),如果有进行中的 Plan 也会恢复 Plan 文件。

如果全量摘要连续失败三次,会自动放弃,进入熔断模式,不会无限重试。

posted @ 2026-08-25 18:12  Sun-Wind  阅读(7)  评论(0)    收藏  举报