拆解 Claude Code 的 RAG 机制

Claude Code 没有向量数据库,没有 embedding 索引,但它能在百万行代码库里精准定位你需要的文件。这背后是一套完全不同于传统 RAG 的检索架构。

这不是你理解的 RAG

用过 RAG 的人对这套流程应该很熟:离线建索引,用户提问,向量检索 Top-K,拼入 prompt,生成回答。一条直线,跑一次就完事。

Claude Code 完全不是这么干的。它没有离线索引,检索过程由模型自己驱动。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Traditional RAG:
+--------+     +-----------+     +----------+     +--------+
| Query  | --> | Vector DB | --> | Top-K    | --> | LLM    |
|        |     | (offline  |     | chunks   |     | answer |
|        |     |  indexed) |     | injected |     |        |
+--------+     +-----------+     +----------+     +--------+

      One-shot: retrieve once, generate once.


Claude Code (Agentic RAG):
+--------+     +------------------+     +--------+
| Query  | --> | LLM decides what | --> | Tool   |
|        |     | to search for    |     | result |
+--------+     +-------+----------+     +---+----+
                       ^                    |
                       |   not enough?      |
                       +--------------------+
                       loop until satisfied

      Multi-hop: model drives retrieval in a loop.

传统 RAG 的检索策略是固定的,写死在代码里。Claude Code 的检索策略是动态的:模型根据当前上下文自行决定搜什么、搜几次、用哪个工具。

四层检索架构

Claude Code 的上下文不是一次性拼好的,而是分四层递进注入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+===============================================================+
|                     CONTEXT WINDOW                            |
+===============================================================+
|                                                               |
|  Layer 0: STATIC CONTEXT (loaded once at session start)       |
|  +---------------------------------------------------------+  |
|  | System Prompt | CLAUDE.md | Git Status | Memory Index   |  |
|  +---------------------------------------------------------+  |
|                                                               |
|  Layer 1: SMART PRE-INJECTION (before model sees the query)   |
|  +---------------------------------------------------------+  |
|  | Sonnet Memory Recall | @file mentions | Skill Discovery |  |
|  +---------------------------------------------------------+  |
|                                                               |
|  Layer 2: MODEL-DRIVEN RETRIEVAL (tool use loop)              |
|  +---------------------------------------------------------+  |
|  | Glob -> Grep -> Read -> ... (model decides)             |  |
|  +---------------------------------------------------------+  |
|                                                               |
|  Layer 3: DELEGATED RETRIEVAL (sub-agents)                    |
|  +---------------------------------------------------------+  |
|  | Explore Agent | Fork Agent (parallel research)          |  |
|  +---------------------------------------------------------+  |
|                                                               |
+===============================================================+

Layer 0:静态上下文

每次会话开始时,系统自动加载一批常驻上下文:System Prompt(工具使用指南、行为规范、环境信息),CLAUDE.md(项目级配置),Git Status(当前分支、最近 5 条 commit),以及 MEMORY.md 索引文件。

这里有个关键设计:System Prompt 被分为静态区和动态区,中间用一个边界标记隔开。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
system_prompt = [
    # --- Static (cross-session cacheable) ---
    intro_section,          # identity & rules
    system_section,         # tool instructions
    doing_tasks_section,    # task guidelines
    actions_section,        # safety rules
    using_tools_section,    # tool selection guide
    tone_and_style,         # output style
    output_efficiency,      # brevity rules

    "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__",  # <-- cache boundary

    # --- Dynamic (changes per session) ---
    session_guidance,       # session-specific guidance
    memory_prompt,          # persistent memory
    env_info,               # environment info
    language,               # language preference
    mcp_instructions,       # MCP server instructions
]

边界标记之前的内容可以跨会话共享 Prompt Cache,所有用户共用同一份缓存。之后的内容每个会话不同。静态部分几乎零成本。

Layer 1:智能预注入

用户发送消息后、模型开始推理前,系统并行执行一批预检索操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
User sends message: "Fix the auth bug in payment API"
                |
                v  (parallel, before model sees anything)
    +-----------+-----------+-----------+
    |           |           |           |
    v           v           v           v
 @mention   Memory      Skill       Agent
 file scan  Prefetch    Discovery   Listing
    |           |           |           |
    +-----+-----+-----+-----+-----+----+
          |                        |
          v                        v
   [Attachment Messages injected into conversation]

其中最精妙的是 Memory Prefetch。它用 Sonnet 模型做一次轻量级 sideQuery,从持久化记忆中挑选最多 5 个相关文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def prefetch_relevant_memories(user_query, messages):
    already_surfaced = collect_surfaced_memories(messages)
    recent_tools = collect_recent_successful_tools(messages)

    selected = sonnet_side_query(
        system="Select up to 5 memories useful for the query",
        user=f"Query: {user_query}\nAvailable: {memory_manifest}",
        recent_tools=recent_tools,
    )
    return load_selected_memories(selected)

sideQuery 是 Claude Code 的一个关键模式:主循环之外,用一个较小的模型做辅助决策,结果作为 attachment 注入到下一轮对话中。成本极低(Sonnet 比 Opus 便宜很多),但上下文相关性大幅提升。

Layer 2:模型驱动的检索

这是 Agentic RAG 的核心。模型拥有三个搜索工具,自主决定调用顺序和次数:

工具能力典型用法
Glob按文件名模式匹配src/**/*.ts,找文件结构
Grep按内容正则搜索function handleAuth,找实现
Read读取文件内容精确读取目标文件

模型通常按 Glob → Grep → Read 的顺序漏斗式缩小范围:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Model thinks: "I need to find the auth handler"
      |
      v
Glob("src/**/*auth*.ts")        --> 8 files found
      |
      v
Grep("handleAuth", path="src/") --> 3 files match
      |
      v
Read("src/api/auth/handler.ts") --> full content
      |
      v
(enough context, start working)

关键在于这不是固定流程。模型可能直接 Read(用户给了文件路径),可能 Grep 多次(第一次没找到,换个关键词),也可能同时发起多个并行搜索。这种灵活性是传统 RAG 做不到的。

Layer 3:子 Agent 委托检索

当搜索任务较重时(比如"帮我理解这个项目的认证架构"),模型可以启动一个 Explore Agent 来代劳:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Main Agent context:                Explore Agent context:
+---------------------------+      +---------------------------+
| User query                |      | Search directive          |
| ... (valuable context)    |      | Glob, Grep, Read results  |
|                           |      | ... (raw search output)   |
| [Agent tool call]  -------|----> | ... (lots of content)     |
|                           |      +---------------------------+
| [Agent result: summary] <-|----  | Final summary (concise)   |
|                           |      +---------------------------+
| (context stays clean!)    |
+---------------------------+

Explore Agent 的设计很克制:只读(不能创建、修改、删除任何文件),用 Haiku(最快的模型)运行,所有原始搜索结果留在子 Agent 的上下文里,只返回精炼的摘要,甚至不加载 CLAUDE.md(主 Agent 已经有了)。

这解决了 Agentic RAG 的一个核心问题:搜索过程本身会消耗大量上下文。如果所有 Glob/Grep/Read 结果都留在主上下文里,几轮搜索下来上下文就满了。子 Agent 充当了一个上下文防火墙。

工程细节亮点

搜索结果的 Token 预算控制

每个搜索工具都有结果裁剪机制,防止单次搜索淹没上下文:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
+-- Search Result Budget Controls --+
|                                   |
|  Grep:                            |
|    Default head_limit = 250 lines |
|    Max result size = 20KB chars   |
|    Max line width = 500 chars     |
|    (base64/minified auto-trimmed) |
|                                   |
|  Glob:                            |
|    Max 100 files returned         |
|    Sorted by mtime (newest first) |
|    Paths relativized to save toks |
|                                   |
|  Read:                            |
|    Max 25,000 tokens per read     |
|    Max 256KB file size            |
|    Default 2000 lines             |
|    Supports offset + limit paging |
|                                   |
+-----------------------------------+

Grep 的 head_limit=250 是一个很精妙的默认值:足够大,能覆盖绝大多数探索性搜索;足够小,不会一次灌入 6000+ token 的搜索结果。如果模型知道需要更多,可以传 head_limit=0(无限制)或用 offset 翻页。

Read 工具的去重优化

同一个文件被读两次很常见,先搜到,后编辑前再确认。Claude Code 会自动检测:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def read_file(path, offset, limit):
    existing = read_file_state.get(path)

    if existing and existing.offset == offset and existing.limit == limit:
        mtime = get_file_mtime(path)
        if mtime == existing.timestamp:
            return "File unchanged since last read."
            # saves ~25K tokens per hit

    content = read_file_content(path, offset, limit)
    read_file_state.set(path, content, mtime, offset, limit)
    return content

大约 18% 的 Read 调用命中去重,每次节省一整个文件的 token 开销。

路径相对化

一个不起眼但无处不在的优化:所有搜索结果中的绝对路径都被转换为相对路径。

1
2
3
4
5
6
7
# Before (absolute paths waste tokens):
/Users/john/projects/my-app/src/components/auth/LoginForm.tsx
/Users/john/projects/my-app/src/components/auth/AuthProvider.tsx

# After (relative to cwd):
src/components/auth/LoginForm.tsx
src/components/auth/AuthProvider.tsx

看起来微不足道,但在一个包含几十次搜索的会话中,这能省下几千个 token。

System Prompt 的缓存分区

前面提到的 __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ 配合 Anthropic API 的 scope: ‘global’ 缓存策略,让静态部分在所有用户之间共享缓存:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
API Request:
+--------------------------------------------------+
| system[0]: intro + tools + rules                 |  scope: 'global'
| system[1]: actions + style                       |  (shared across ALL users)
|                                                  |
|  ---- DYNAMIC BOUNDARY ----                      |
|                                                  |
| system[2]: session guidance                      |  scope: 'session'
| system[3]: memory + env info                     |  (per-user)
+--------------------------------------------------+
| messages: [user, assistant, ...]                 |
+--------------------------------------------------+

边界之前的内容(工具描述、行为准则等)对所有用户完全相同,只需要 cache 一次。你的第一次请求可能就命中了别人已经缓存好的 system prompt 前缀。这是全局级别的去重。

端到端流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
SESSION START
|
+-- Load system prompt (static + dynamic)
+-- Load CLAUDE.md into user context
+-- Load MEMORY.md index
+-- Snapshot git status + recent commits
|
USER SENDS: "Fix the auth bug in payment API"
|
+-- [Parallel prefetch, before model runs]
|   +-- Sonnet memory recall -> api_gotchas.md, user_prefs.md
|   +-- @file scan -> (none mentioned)
|   +-- Skill discovery -> (no matching skills)
|
+-- Prefetch results injected as attachment messages
|
MODEL TURN 1:
|   Thinks: "I need to find auth-related files"
|   +-- Grep("auth.*bug|payment.*auth", type="ts")    --> 5 files
|   +-- Read("src/api/payment/auth.ts")                --> 800 lines
|   +-- Read("src/api/payment/__tests__/auth.test.ts") --> 400 lines
|
MODEL TURN 2:
|   Thinks: "Found the bug, now fix it"
|   +-- Edit("src/api/payment/auth.ts", ...)
|   +-- Bash("npm test -- auth")
|
DONE (2 turns of retrieval + 1 turn of action)

对比

维度传统 RAGClaude Code
索引离线向量化无索引,实时搜索
检索策略固定(Top-K)动态(模型决定)
检索轮次单轮多轮循环
检索粒度固定 chunk文件名 → 内容 → 行级
上下文保护子 Agent 隔离
结果裁剪截断多层预算控制
缓存向量缓存Prompt Cache 分区 + Read 去重

Claude Code 的 RAG 本质上不是"检索增强生成",而是"生成驱动检索":模型先理解需求,再决定搜什么,搜完判断够不够,不够再搜。牺牲了一点延迟,换来了远超传统 RAG 的精准度和灵活性。