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

这对应到 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
  → 对应一个完整的子任务,极少决策负担

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


4. MCP 协议的意义

一句话破题

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 调用)。

整个记忆系统的设计哲学

索引导向(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