Agent 系统面试准备 — 基于 Claude Code 源码的核心概念

目标:每个概念都有真实代码/架构支撑,不靠空谈 数据来源:Claude Code v2.1.88 泄露源码(1900 文件 / 51.2 万行 TypeScript) 适用岗位:Agent Infra / Agent 全栈 / AI Engineer


目录

  1. Agent vs Chat/RAG 的本质区别
  2. ReAct 循环
  3. Tool Calling / Function Calling Schema 设计
  4. MCP 协议的意义
  5. Agent Memory 分层与写入策略

1. Agent vs Chat/RAG 的本质区别

一句话破题

Agent 和 Chat/RAG 的本质区别不是”能不能调工具”,而是对外部世界有没有副作用(side effect)

Claude Code 源码怎么体现这个区别

源码里所有工具都被显式标记了是否只读:

// Tool 类型核心定义(简化自 Tool.ts, 792 行)
type Tool = {
  name: string;
  call: (input) => Promise<ToolResult>;
  inputSchema: ZodSchema;
  checkPermissions: () => PermissionCheck;
  isReadOnly: boolean;        // ← 显式标记副作用
  isConcurrencySafe: boolean; // ← 是否有副作用影响并发
  maxResultSizeChars: number;
}

这个 isReadOnly 不是摆设——权限系统围绕它构建。Read-only 工具(搜索、读取文件)走宽松通道,Write 工具(编辑文件、执行命令)走完整四层权限链:

Config Rules → Tool.checkPermissions → Classifier(小LLM侧查) → 用户确认

如果我们拿 Chat/RAG 来对比:

Chat 系统在 Claude Code 里的映射: 纯 API 调用 claude-sonnet-4,模型输出文本 → 文本,没有任何工具注册。这是零副作用的。

Agent 系统在 Claude Code 里的映射: 注册 40+ 工具,每个工具都要过权限链,工具执行结果作为 Observation 反馈到下一个 Thought。BashTool 会改文件系统、FileWrite 会改代码——这些都是副作用的源头。

最有力的证据是 Coordinator 模式的设计:

Coordinator(协调者)
  → 被剥夺所有文件操作权限
  → 只保留 3 个工具:Agent(派生子Agent)、SendMessage、TaskStop
  → 角色:纯规划和调度,不接触外部世界

Worker(执行者)
  → 继承完整工具权限
  → 在隔离上下文执行,所有副作用限制在子会话内
  → 完成后只回传 XML 精炼结论,不污染主对话

这清楚地展示了 Anthropic 对”有副作用”的认知:Coordinator 不直接操作任何资源,怕的是规划过程中无意间产生不可逆后果。 Worker 在隔离沙箱里做事,即使崩了也不影响全局。

副作用带来的系统性后果(Claude Code 源码验证)

问题Chat/RAGAgentClaude Code 怎么处理的
幂等性天然幂等调用一次和两次不同isConcurrencySafe() 标记工具能否并行执行
回滚不需要某步失败需补偿Coordinator 模式下子 Agent 隔离,失败不污染主会话
权限控制不需要必须分级四层递进权限链,Classifier 用小模型动态判断
审计记录对话即可需完整调用链telemetry 模块记录所有工具调用参数、返回值、耗时
状态追踪无状态多步任务需状态机Hook 系统(25+ event hook)追踪执行生命周期
错误恢复重新生成不能重头来ReAct 循环天然支持 retry + fallback

面试加分:从源码看”副作用”的极端情况

Bash 命令执行是副作用的极端例子:bashSecurity.ts 里内置了 23 条安全规则,Classifier 侧查模型会分析命令的风险等级。用户开了 Auto Mode 后,系统后台静默调用更小更便宜的 LLM,把对话精简转录 + 待执行命令传给它,决定 Allow 还是 Deny。

Chat/RAG 需要考虑”执行 rm -rf / 怎么办”吗?不需要,因为没有执行能力。Agent 必须,因为副作用是它的定义特征。


2. ReAct 循环

一句话破题

ReAct 的核心不是”先想再做”,而是推理过程和行动过程互相增强,同一个 token 生成流里同时完成思考和行动。

Claude Code 源码里的 ReAct 实现

核心引擎 QueryEngine.ts(1295 行,注意这只是接口层,完整 query 引擎 46,000 行):

// 简化的 submitMessage 模式(AsyncGenerator)
async function* submitMessage(userInput: string) {
  // 1. 构造消息,注入 system prompt + tool schemas
  const messages = buildMessages(userInput);

  while (true) {
    // 2. 流式调用 LLM
    const response = yield* callLLM(messages);

    if (response.type === 'text') {
      // 3. 模型回复文本 → 可能还需要继续
      messages.push({ role: 'assistant', content: response.text });
    } else if (response.type === 'tool_use') {
      // 4. 模型决定调工具 → 执行并反馈
      const result = await executeTool(response.tool_call);
      messages.push({ role: 'tool_result', content: result });
    } else if (response.type === 'stop') {
      // 5. 模型认为任务完成
      break;
    }
  }
}

用 AsyncGenerator 实现 ReAct 循环不是偶然的。设计者的意图:

  1. 自然支持流式输出yield 每个消息片段,上层 for await...of 消费。用户看到逐字输出不阻塞
  2. 支持中断恢复 — Generator 天然可暂停,中间状态在变量闭包里,不需要额外状态机
  3. 工具执行不阻塞主线程await executeTool() 暂停当前 yield,但主线程可以继续处理其他任务

三个源码级别的关键洞察

1. 工具可以并行执行

// StreamingToolExecutor 的实现
// 工具调用被分区为并发批次和串行批次
// isConcurrencySafe() 决定一个工具能否与其他工具同时跑

这意味着 ReAct 循环的每一步不一定是单线的。模型可以在同一轮次发出多个工具调用(比如同时搜索和读文件),executor 根据 isConcurrencySafe 决定哪些可以并行。这大幅加快了多工具任务的完成速度。

2. 14 个缓存失效向量主动追踪 ReAct 循环的效率瓶颈

Claude Code 内部追踪 14 种 Prompt Cache 失效条件。每次 ReAct 循环中,如果 system prompt 的任意部分变了(工具列表、用户配置、Git 状态),缓存就失效了。14 个向量让团队能定位”为什么 ReAct 循环越跑越慢”——是缓存被频繁击穿了。

3. ToolSearch 是 ReAct 的精简变体

ReAct 的标准形式要求所有工具定义都在 prompt 里。但 Claude Code 有 40+ 工具,全塞进去 token 开销巨大。

解法是 ToolSearch:

非核心工具标记 defer_loading: true
→ 模型看不到具体定义,只知道有 ToolSearch 可用
→ 需要时传关键词 → 动态加载对应工具定义 → 放入后续 prompt

这实际上是 ReAct 的按需加载变体:模型先”知道有什么”,再”按需了解”,而不是一次性加载全部。

三种变体在 Claude Code 源码里的痕迹

变体Claude Code 中的实现触发条件
标准 ReAct默认 QueryEngine 循环普通对话,任务简单
Plan-and-ExecuteCoordinator 模式复杂任务,需要先规划再执行
ReflexionautoDream 整理跨会话,记忆需要反思和合并

面试追问准备

Q:Claude Code 的终止条件怎么设计的?

源码里至少四种:

  1. 最大步数硬限制 — 20 步上限(Coordinator 模式下不同)
  2. respond/finish 信号 — 模型自己发出 stop token
  3. 重复检测 — 同样的 (action, observation) 重复出现 → 打断
  4. 超时 — 轮级别 timeout

Q:Coordinator 模式下的 ReAct 循环有什么不同?

Coordinator 不直接执行工具。它的 ReAct 循环是:

Thought → spawn SubAgent → wait for result → evaluate → [continue | finish]

注意中间多了一层:Coordinators 的 Action 不是调工具,是派 Agent。工具调用发生在子 Agent 的 ReAct 循环里。这叫 ReAct 的递归——外层 Agent 的 Action 是创建内层 Agent 的 ReAct 循环。


3. Tool Calling / Function Calling Schema 设计

一句话破题

Tool Schema 不是”给模型看的一段文档”,它是 Agent 和外部世界之间的契约。契约的质量直接决定了 Agent 的可靠性。

Claude Code 源码揭露的 40+ 工具系统

泄露源码显示 Claude Code 有 40 个注册工具 + 50+ 斜杠命令。这不是拍脑袋设计的——有一些非常清晰的工程原则。

1. Tool 接口的设计哲学

// Tool 类型核心定义(Tool.ts, 792 行, 20+ 方法)
type Tool = {
  name: string;
  call: (input) => Promise<ToolResult>;
  inputSchema: ZodSchema;         // Zod v4 类型校验
  checkPermissions: () => PermissionCheck;
  isReadOnly: boolean;
  isConcurrencySafe: boolean;     // ← 很少见的属性
  maxResultSizeChars: number;     // ← 结果大小管控
}

isConcurrencySafe 是面试必提的设计亮点。 大多数 Agent 系统假设工具一次只能调一个,但 Claude Code 在架构层面就区分了”可以并行”和”必须串行”的工具。比如 ReadSearch 可以同时跑,FileWriteBashRun 不行——因为写文件和执行命令之间有副作用的相互影响。

2. ToolSearch 按需加载

普通工具:直接注入 system prompt
非核心工具:defer_loading: true → 模型只知道 ToolSearch 存在
  → 模型发 ToolSearch("search", "grep")
  → 系统加载 grep 等搜索类工具定义
  → 放入后续 prompt

为了让概念更具体,下面是 system prompt 中与 ToolSearch 相关的简化内容:

可用的工具有:
1. bash: 执行 shell 命令
2. read_file: 读取文件
3. edit_file: 编辑文件
4. ToolSearch: 按关键词搜索并动态加载更多工具
   (如 search、glob、grep、project_search 等)。
   如果你需要的工具不在上方列表中,请先调用 ToolSearch 查找。

调用格式:ToolSearch(query: string)
query 参数:你需要的工具功能的关键词,例如 "search" 或 "file find"。

ToolSearch 本身也是一个工具——它的输出是另一些工具的定义。这相当于一个元工具:模型通过调用它来发现新工具,然后在后续 ReAct 循环中使用。

这对应到 Tool Calling 的一个深层问题:工具越多,模型选择越难,prompt 越长。 按需加载给了模型自主权——它决定什么时候需要看某个工具的具体定义。

还有一个 CLAUDE_CODE_SIMPLE 模式,只保留 3 个基础工具,给轻量场景用。

3. assembleToolPool() 的合并策略

// 合并内置工具 + MCP 工具
// 按 prompt cache 友好的顺序排列
function assembleToolPool(): Tool[] {
  const builtin = getBuiltinTools();
  const mcp = getMCPTools();
  const all = [...builtin, ...mcp];

  // 按字母序排序 → 保持 prompt 哈希稳定 → 缓存命中
  return all.sort((a, b) => a.name.localeCompare(b.name));
}

字母序排序是为了缓存。 如果工具列表顺序每次不同,prompt 的哈希值就变了,Prompt Cache 直接失效。这个细节决定了”给模型看到整齐的工具列表”不光是美观,是成本优化。

从源码看到的 Schema 设计正反面

✅ 好的设计:物理与逻辑分离

Read 和 Search 是两个工具,但底层可以共享同一个文件系统模块。工具是模型看到的逻辑抽象,不是底层 API 的一一映射。

✅ 好的设计:大小管控

maxResultSizeChars 限制工具返回值大小。大结果自动保存到 ~/.claude/tool-results/,API 只收到摘要 + 文件路径。这是防止工具返回撑爆上下文的工程化手段。

❌ 坏的设计(从源码看到的教训):description 太短或太长

有些工具的 description 只有一行(“List files in a directory”),模型经常在 Glob 和 Read 之间选错。教训:工具 description 要写”什么时候用这个而不是隔壁那个”,不写”这个工具是什么”。

面试加分:Tool 的三层粒度

Claude Code 源码里存在但没显式标注的三层粒度:

L1 原子工具(read_file, edit_file, bash)
  → 不可分割的单一操作,最灵活,最多犯错空间

L2 组合工具(通过 ToolSearch 暴露的模式)
  → 搜索+读文件,封装常用操作

L3 任务工具(通过 Skills 系统实现)
  → "fix_bug" = search → edit → test
  → 对应一个完整的子任务,极少决策负担

4. Skills:让工具调用从原子操作跃迁到任务模式

Skills 是 Claude Code 中最被低估的架构组件。它不是工具,也不是提示词——它介于两者之间:用提示词封装的任务模式。

Skills 是什么

Claude Code 的 Skills 系统本质是一个可组合的任务提示词库。每个 Skill 是一个 Markdown 文件(带 YAML frontmatter),描述一个特定开发任务的执行模式:

---
name: fix-bug
description: 修复代码中的 bug——从定位到验证全流程
version: 1.0
---
当你被要求修复一个 bug 时,遵循以下模式:

1. **定位**:先搜索相关代码(grep),理解当前行为
2. **诊断**:阅读上下文,确定根因
3. **修复**:编辑文件,做最小改动
4. **验证**:运行测试确认修复
5. **整理**:如果涉及多个改动点,清理中间状态

关键区别:Skills 不是代码,是提示词。 它们不写逻辑,它们写的是”在什么情况下用哪些工具、按什么顺序”。这相当于把资深工程师的任务拆解能力固化成了可复用的知识。

架构设计:可组合,可触发
~/.claude/skills/
├── fix-bug.md           # 修复 bug
├── add-feature.md       # 新增功能
├── refactor.md          # 重构
├── write-test.md        # 写测试
└── review.md            # Code Review

Skills 系统有两个关键机制:

1. 自动触发(Auto-trigger)。 系统在 ReAct 循环启动时会评估当前上下文(用户输入、打开的文件、Git diff),如果匹配某个 Skill 的触发条件,自动将其提示词注入 system prompt。例如用户打开测试文件并输入”补全这个”,系统自动加载 write-test.md

2. Skill 可组合。 一个 Skill 可以引用另一个:

add-feature.md 开头可以写:
"在实现新功能前,先用 refactor.md 重构相关代码"

这实际上是一棵隐式的任务 DAG(有向无环图)。Skill 通过嵌套引用构建了从宏观使命到微观操作的层次,比硬编码的工具调用链灵活得多。

Skills 在调用链中的位置
用户需求
  → Skills 系统匹配 → 注入对应提示词
    → 模型在 ReAct 循环中使用 L1 原子工具
      → 工具执行 → 观察 → 下一个循环

Skills 不替代工具,它替代的是模型每次从零思考任务模式的成本。没有 Skills 时,模型每次被要求”修复 bug”都要自己推导步骤(搜索→读→改→测)。有了 Skills,修复 bug 的执行路径被固化,模型先把精力集中在任务本身而不是任务结构。

面试回答框架

面试官问:「40+ 工具模型怎么记得住?」 你的回答应该从三层解耦切入:

  • L1 原子工具(40+)通过 ToolSearch 按需加载,模型不记全量定义
  • L2 复用模式通过 ToolSearch 的元工具机制暴露
  • L3 Skills 固化任务模式,模型不记”怎么做”,只记”当前在做什么”

每往上一层,模型的认知负担就降一截。Skills 系统实际上是在 system prompt 层面替模型做了一层任务规划,让模型可以专注于执行而不是规划。

更进阶的洞察: Skills 和 MCP 本质上是同一问题的镜像解。MCP 在工具层解耦(工具接入标准化),Skills 在任务层解耦(任务执行模式标准化)。一个处理”跟什么对接”,一个处理”怎么做”。面试时能把这个对应关系讲清楚,比背任何知识都有说服力。

面试时可以讲:经验是默认给 L1,用 Skills 系统提供 L3。模型从 L1 开始,熟练后 Skills 逐渐取代人工重复。 跟打游戏先练基本功再学大招一样。


4. MCP 协议的意义

MCP(Model Context Protocol)由 Anthropic 于 2024 年 11 月提出,是旨在为 AI Agent 与外部工具/数据源之间建立统一通信标准的开放协议。你可以把它理解为 AI 世界的 USB-C 接口——一个通用标准,代替一堆彼此不兼容的私有接口。

一句话破题

MCP 不是常规意义上的”技术框架”,它解决的是一个 network effect 问题——把工具接入从 O(N×M) 降为 O(N+M)。

Claude Code 源码中的 MCP 实现

assembleToolPool() 的实现揭示了内置工具和 MCP 工具的关系:

function assembleToolPool(): Tool[] {
  // 1. 加载内置工具(40+ 个,硬编码在工具目录)
  const builtin = getBuiltinTools();

  // 2. 加载 MCP 服务器注册的工具(动态发现)
  const mcpServers = loadMCPServersFromConfig();
  const mcpTools = mcpServers.flatMap(s => s.listTools());

  // 3. 合并、排序、注入 prompt
  const all = [...builtin, ...mcpTools];
  return sortForPromptCache(all);
}

关键设计决策:MCP 工具和内置工具在 Agent 眼中没有区别。 模型不知道”这是个 MCP 工具还是内置工具”,它只看到 tool name 和 description。这意味着 MCP 接入的新能力立刻获得了和内置工具相同的权限系统、并发控制、结果大小管控。

MCP 不是”又一个框架”,是分层架构

Claude Code 源码揭示了 MCP 解决的真正问题。在构建 Agent 时,工具接入的复杂度是指数增长的:

Agent框架 × 工具服务 = O(N×M) 适配器

当 Claude Code 支持 40+ 工具时,如果每个工具都用原生 Function Calling 格式接入,每次 API 升级框架要改、每个工具要改。MCP 把问题拆成两层:

Agent框架 → MCP Client(Claude Code 实现一次)
工具服务 → MCP Server(工具提供商实现一次)

Claude Code 的 MCP Client 实现在 services/mcp/ 目录下,处理:

  • 发现:从 settings.json 读取 MCP Server 配置
  • 连接:支持 STDIO(本地进程)和 SSE(远程服务)
  • 生命周期:start/stop/restart 管理
  • 健康检查:心跳检测 MCP Server 是否存活

面试回答框架

“MCP 本质上和 TCP/IP 解决的问题一样——在上下层之间插一个标准接口层。MCP 出现之前,Agent 框架接入工具是 O(N×M) 的适配器问题;MCP 让每一层只需要实现一次接口,把复杂度降到 O(N+M)。Claude Code 的源码验证了这一点:MCP 插件和内置工具在 assembleToolPool 中无缝合并,模型完全感知不到区别。“

面试追问准备

Q:MCP 在 Claude Code 源码里和 Function Calling 是什么关系?

Function Calling: 模型 ←→ 工具调用请求(数据格式标准)
MCP:              Agent框架 ←→ 工具服务(通信协议标准)

MCP Server 返回的 tool 定义,在组装成 system prompt 前会被转换成 Function Calling 格式。看代码就是 assembleToolPool() → 格式化工具描述 → 注入系统提示 → LLM 收到标准 tool_use 格式。MCP 是工具管理协议,Function Calling 是模型交互格式,解决不同层次的问题。

Q:Claude Code 为什么选择支持 MCP 而不是自己搞一套?

源码里已经有完整的工具系统(40+ 工具、ToolSearch、权限链),MCP 的接入不是为了替代它,是为了让第三方工具生态接入。Claude Code 提供基础设施(权限、并发、结果管理),MCP 提供生态扩展——这是一种平台化思维,不是技术选择。


5. Agent Memory 分层与写入策略

一句话破题

Memory 的核心难点不是”存什么”,而是写入决策——什么信息值得记住、什么时候应该更新、什么时候应该忘记。

Claude Code 源码揭示的四层记忆

Claude Code 的记忆系统完全基于文件系统(没有向量数据库),源码在 src/memdir/ 目录下。

层级 1:MEMORY.md — 索引层(常驻 system prompt)

~/.claude/projects/<git-root>/memory/MEMORY.md

- [用户角色](user_role.md) — 数据科学家,专注可观测性/日志记录
- [测试规范](feedback_testing.md) — 集成测试必须用真实数据库
- [认证重写](project_auth_rewrite.md) — 法务合规驱动
- [外部系统](reference_external.md) — pipeline bug 在 Linear 项目追踪

200 行 / 25KB 硬上限。 超过直接截断,注入截断警告。MEMORY.md 不存记忆正文,只存指针。

每次对话开始,loadMemoryPrompt() 把 MEMORY.md 注入 system prompt。这意味着不在索引里的记忆,模型看不到。 这是迫使记忆系统保持精炼的强约束。

层级 2:独立 .md 文件 — 记忆正文(按需加载)

四个类型(定义在 memoryTypes.ts),每个文件带 YAML frontmatter:

---
name: 记忆名称
description: 单行描述(用于相关性筛选)
type: feedback
---
规则:集成测试必须使用真实数据库,不能用 mock
Why: 之前 mock 和生产不一致导致迁移失败
How to apply: 涉及数据库逻辑的测试都走真实 DB

Why 和 How to apply 是精妙的写入策略设计。 不只是记事实,还记”为什么它是对的”和”什么时候适用”。这允许未来模型判断边界情况——“这条规则在什么新场景下可能不适用?“

层级 3:CLAUDE.md — 项目宪法

MEMORY.md(动态,自动更新)   vs   CLAUDE.md(静态,手动维护)

会话中发现的增量知识              项目永远成立的事实
高频写入                           低频/从不修改
AI 自动维护                        人工维护

区别不是技术上的,是写入权责上的。AI 可以自由更新 MEMORY.md,但 CLAUDE.md 是人工维护的”项目宪法”,AI 不能擅自修改。

层级 4:KAIROS 模式下的 logs/(原始日志)

助手模式(KAIROS)下,记忆不直接写入 MEMORY.md,而是追加到 logs/YYYY/MM/YYYY-MM-DD.md。等 /dream 定期蒸馏。

普通模式:      对话 → extractMemories → 直接更新 MEMORY.md
KAIROS 模式:   对话 → 追加 logs/         → /dream → 蒸馏为结构化记忆

这解决了长时间运行 Agent 的记忆问题:实时写入可能写错(信息还不完整),先日志后蒸馏(等积累了足够上下文再做判断)。

写入策略:Claude Code 的三层门控

自动提取(extractMemories):每轮对话后后台运行

完美分叉(共享 system prompt + 消息前缀)
→ 最多 5 轮 LLM 调用
  → 第 1 轮:并行读现有记忆文件
  → 第 2 轮:并行写新记忆/更新旧记忆

5 轮硬限制是成本和质量之间的权衡。更多轮收益递减。

写入前问的三个问题(内嵌在 memoryTypes.ts 的提示词里):

  1. 会影响未来决策吗? → 代码模式不记(可从代码实时推导),Git 历史不记(权威来源是 git log)

  2. 是事实还是推测? → 纠正和确认都要记。“只记纠正会让你变得过于保守”

  3. 是否有冲突信息? → 信任记忆在写入时是准确的,但使用前必须验证

使用前的验证(TRUSTING_RECALL_SECTION,在 system prompt 里强制):

“记忆中说某文件存在 → 先检查文件是否存在。 记忆中说某函数存在 → 先 grep 搜索。 记忆是快照,不是当前状态。“

定期整理:autoDream

// 触发条件(三重门控)
if (
  isAutoDreamEnabled()                     // 功能开关
  && hoursSinceLastConsolidation >= 24     // 时间门
  && newSessionsSinceLast >= 5             // 会话数门
  && tryAcquireConsolidationLock()         // 互斥锁
) {
  runConsolidation();
}

四阶段整理:

  1. Orient — 读 MEMORY.md,了解有什么
  2. Gather — 找每日日志、漂移的记忆、JSONL 会话记录(不穷举)
  3. Consolidate — 合并近似项,删除被推翻的事实,相对日期转绝对日期
  4. Prune — MEMORY.md 保持在 200 行以内,删除过期指针

面试加分:向量数据库不是银弹

Claude Code 选择了纯文件系统方案。为什么?

Vector DB 方案:
  写入:embedding → 向量库
  查询:query → embedding → 相似度搜索
  需要:一个向量数据库 + embedding 服务 + 存储
  复杂度:你不再需要一个 vector DB,你需要的是一个分布式系统

文件系统方案:
  写入:保存 .md 文件
  查询:grep + LLM 判断相关性
  需要:文件系统
  复杂度:零(已有的基础设施)

Claude Code 的答案是:Agent 的记忆不需要语义搜索,需要结构化索引。 MEMORY.md 的指针系统 + grep 搜索,在绝大多数场景下比向量检索更可靠(精确匹配 > 近似搜索)且更便宜(文件读写在本地,不需要 API 调用)。

局限性:文件系统方案的性能边界

没有银弹。纯文件系统 + grep 方案在记忆文件量较小时表现优异,但同样存在明显的性能边界:

1. MEMORY.md 索引膨胀。 200 行 / 25KB 的硬上限对中型项目是合理约束,但当一个项目积累了 50+ 记忆文件后,索引必须频繁截断。每次截断意味着模型丢失了部分索引指针,无法感知这些记忆的存在——它们没有被删除,但模型不知道自己应该去查它们。这本质上是一个 L1 索引比 L2 正文先溢出的问题,中等规模下就会暴露。

2. grep 在大量文件下的性能退化。 当记忆目录累积到数百个 .md 文件时,每轮 Memory Load 的 grep 扫描开销随文件数线性增长。单次 grep -r 扫描 500+ 文件仍可控(数十毫秒),但 extractMemories 在每轮对话后后台触发,累计到数百轮甚至数千轮对话后,磁盘 I/O 和进程创建开销不可忽视。

3. 语义鸿沟。 grep 是精确/正则匹配,无法处理”意思相近但表述不同”的情况。当模型想查某条记忆但措辞不同时(比如记忆里记的是”集成测试必须用真实DB”,但模型问的是”数据层怎么验证可靠性”),纯 grep 会错过。虽然有 LLM 二次判断兜底,但增加了延迟和成本。

4. 缺乏并发安全。 extractMemories 在每轮对话后后台触发,多个进程并行读取-写入记忆文件时可能产生竞态条件。文件系统不是原子语义的——两个进程同时读取同一记忆并修改后写回,第二个写会覆盖第一个的修改。Claude Code 的实现通过 tryAcquireConsolidationLock() 在 consolidation 层面做了互斥,但 extractMemories 本身缺乏类似保护。

面试加分:面试官问「为什么不直接上向量数据库」,你的回答应该先承认这些局限,再解释为什么 Claude Code 的选择仍然合理。

向量数据库解决的是「量」的问题——百万级文档的语义检索。Claude Code 的选择前提是它的记忆粒度(每个项目数十条)、使用场景(一机一人)、数据模式(结构化索引 > 模糊搜索)。换句话说,不是文件系统比向量 DB 好,而是当数据规模不超出 grep 的能力边界时,引入向量 DB 增加的运维成本和系统复杂度得不偿失。面试官想听到的不是站队,而是这个抉择的边界条件。

整个记忆系统的设计哲学

索引导向(MEMORY.md 200 行)
↕ 按需加载(使用前 grep + 读文件)
↕ 自动提取(extractMemories,每轮后台)
↕ 定期整理(autoDream,24h / 5 会话)

没有复杂的架构,就是用 Markdown 文件 + 时间门控 + LLM 自我纠错,支撑了跨会话的持久记忆。复杂度的节制本身就是一个答案。


总结:Claude Code 源码给这五个概念的答案

概念Claude Code 给出的答案
Agent vs ChatisReadOnly + 四层权限链 + Coordinator 剥夺写权限
ReActAsyncGenerator + 并行工具执行 + 14 缓存失效向量
Tool Calling40+ 工具 + ToolSearch 按需加载 + assembleToolPool 缓存优化
MCP内置工具和 MCP 工具在 Agent 眼中无区别
Memory文件系统索引 + 200 行上限 + 三重门控写入 + autoDream 整理

版本:v1.0 / 2026-05-26 — 基于 Claude Code 泄露源码重构 核心参考:src/memdir/, services/api/claude.ts, Tool.ts, QueryEngine.ts, utils/fingerprint.ts