cc_rag_tutorial

第 4 章:外挂 RAG 的架构设计——模块划分、接口契约与时序

1. 开篇

在上一章,我们像法医一样解剖了 CC(Claude Code)的数据载荷。现在,我们要利用这些数据,为 CC 打造一个“第二大脑”。

很多开发者在尝试外挂 RAG 时,容易陷入“脚本陷阱”:写一个几百行的 Python 脚本,混杂着文件读取、OpenAI API 调用和简单的余弦相似度计算。这种“大泥球”架构在 Demo 阶段能跑通,但一旦接入 CC 进行高频交互,就会遇到三个致命问题:

  1. 延迟不可控:每次查询都重新加载 Embedding 模型,导致 CC 像卡死了一样。
  2. 上下文爆炸:缺乏精细的 Token 预算控制,直接撑爆 CC 的上下文窗口或导致巨额账单。
  3. 调试黑盒:CC 说“找不到文件”,你却不知道是检索挂了,还是 Rerank 把正确结果过滤掉了。

本章学习目标:

  1. 掌握生产级 RAG 的 Ingest-Index-Retrieve-Rerank-Serve 五层架构。
  2. 学会设计 “驻留型” vs “临时型” 架构以平衡性能与资源。
  3. 定义严格的 接口契约(Schema),让 RAG 像标准零件一样嵌入 CC。
  4. 设计 Token 预算算法,在“给得够多”和“给得太多”之间找到平衡。

2. 宏观架构:驻留服务 vs 临时命令

在深入模块之前,必须先决定你的 RAG 以什么形态存在。这直接决定了架构的复杂度。

形态 A:临时命令模式 (CLI Mode)

每次 CC 调用工具时,启动一个新进程(如 python search.py)。

形态 B:驻留服务模式 (Server/Daemon Mode) —— 推荐

启动一个本地 HTTP/RPC 服务(如 localhost:8000),CC 的工具只是一个轻量级 curl 包装。

Rule of Thumb (经验法则): 如果你要做本地 Embedding(如使用 HuggingFace 模型),必须采用形态 B(驻留服务)。否则 CC 每次思考都要等你几秒钟,你会疯的。


3. 核心模块详解(五层漏斗模型)

一个健壮的 RAG 系统就像一个漏斗,数据层层筛选,最终滴出精华。

[ Disk / Network ]
       |
       v
+-------------+
|  1. Ingest  |  <-- "脏"数据入口 (ETL)
+-------------+
       | (Clean Text + Metadata)
       v
+-------------+
|  2. Index   |  <-- 向量化与存储 (The Map)
+-------------+
       | (Vector DB)
       v
+-------------+      Query
|  3. Retrieve|  <-- 粗筛 (Recall Top-100)
+-------------+
       | (Candidates)
       v
+-------------+
|  4. Rerank  |  <-- 精排 (Precision Top-10)
+-------------+
       | (Ranked Chunks)
       v
+-------------+
|  5. Serve   |  <-- 包装与预算 (Format & Cut)
+-------------+
       |
       v
[ Claude Code ]

3.1 Ingest 层(摄取与清洗)

不仅仅是读文件。这一层决定了“垃圾进,垃圾出”。

3.2 Index 层(索引构建)

3.3 Retrieve 层(混合检索)

单一的向量检索(Dense Retrieval)在代码场景往往表现不佳(例如搜索具体的变量名 MAX_RETRY_COUNT)。

3.4 Rerank 层(重排裁判)

Retrieve 层为了速度(召回 100 个),牺牲了精度。Rerank 层使用更强的模型(Cross-Encoder)对这 100 个进行精细打。

3.5 Serve 层(服务与组装)

这是直接面对 CC 的门户。


4. 接口契约设计 (Interface Contract)

这是你和 CC 签订的协议。任何一方违反,系统就会崩溃。

4.1 输入契约 (Request Schema)

这是你在 CC 的工具定义(Tool Definition)中需要配置的 JSON Schema。

{
  "name": "search_codebase",
  "description": "Search the codebase for snippets relevant to a query. Use this when you need to understand how functions are defined or used.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "The natural language query or specific code symbol to search for."
      },
      "project_root": {
        "type": "string",
        "description": "Absolute path to the root of the project being worked on."
      },
      "file_pattern": {
        "type": "string",
        "description": "Optional glob pattern to restrict search (e.g., 'src/**/*.py')."
      },
      "max_results": {
        "type": "integer",
        "default": 10,
        "description": "Maximum number of code chunks to return."
      }
    },
    "required": ["query", "project_root"]
  }
}

4.2 输出契约 (Response Structure)

RAG 服务返回给 CC 的内容。建议包含结构化元数据人类可读文本两部分。

JSON 载荷示例:

{
  "status": "success",
  "meta": {
    "total_found": 42,
    "returned": 3,
    "search_time_ms": 150
  },
  "results": [
    {
      "file": "src/auth/login.py",
      "lines": [15, 30],
      "score": 0.92,
      "content": "def login(user, password):\n    # implementation..."
    },
    // ... more items
  ],
  "formatted_output": "<results>\n<item file='src/auth/login.py'>\n..." // 预组装好的Prompt片段
}

Rule of Thumb (经验法则): 虽然 CC 能读 JSON,但提供一段预格式化好的 formatted_output 往往效果更好。因为你可以控制换行、缩进和 XML 标签,确保 LLM 能够以一种“视觉上”清晰的方式阅读代码块。


5. 关键算法:Token 预算与上下文填充

在 Serve 层,如何把结果塞进有限的窗口?这里介绍 “贪婪填充算法” (Greedy Packing)

算法逻辑:

  1. 设定预算:例如 MAX_TOKENS = 4000
  2. 预留开销:减去 XML 包装标签和提示词的固定开销(约 100 tokens)。
  3. 排序:确保候选列表已经按 Rerank 分数降序排列。
  4. 循环填充
    • 取出分数最高的 chunk。
    • 计算其 Token 数。
    • if (current_tokens + chunk_tokens) <= MAX_TOKENS:
      • 加入结果集。
      • current_tokens += chunk_tokens
    • else:
      • 跳过(或者尝试截断,但代码通常不建议截断)。
  5. 停止:列表遍历完或预算耗尽。

6. 时序图:由于 CC 的同步特性

理解这个时序图对于性能优化至关重要。

User      CC (Agent)      RAG Tool (Client)     RAG Server (Daemon)
 |            |                   |                     |
 |--(Query)-->|                   |                     |
 |            |--(Think)--------->|                     |
 |            |                   |                     |
 |            |--(Tool Call)----->|                     |
 |            |  "search(q, n)"   |--(HTTP POST)------->|
 |            |                   |                     | [1. Embed Query]
 |            |                   |                     | [2. Vector Search]
 |            |                   |                     | [3. Rerank]
 |            |                   |                     | [4. Pack Context]
 |            |                   |<--(JSON Resp)-------|
 |            |<--(StdOut)--------|                     |
 |            |                   |                     |
 |            |--(Read Context)-->|                     |
 |            |--(Gen Answer)---->|                     |
 |<-(Reply)-- |                   |                     |

性能瓶颈点:


7. 本章小结

  1. 架构分层:不要把所有逻辑写在一个文件里。Ingest/Index/Retrieve/Serve 应该解耦。
  2. 驻留优先:为了用户体验,尽量编写一个后台常驻服务(Daemon)来承载 Embedding 模型,而不是每次 CLI 调用都冷启动。
  3. 接口契约:使用严格的 JSON Schema 定义输入,使用带有清晰 XML 标签的格式返回输出。
  4. 数据流:不仅要传内容,还要传 file_pathlines,这是 CC 能够“引用”而不是“瞎编”的基础。

8. 练习题

基础题 (巩固概念)

  1. 架构绘图:请在纸上画出,当用户新增一个 .md 文档时,数据是如何流经 Ingest 和 Index 层,最终到达 Vector DB 的?
  2. 接口设计:CC 需要知道检索结果的时效性。请修改 4.2 节的 Response Structure,增加一个字段来表示文件的“最后修改时间”。
  3. 组件选择:如果你的机器内存只有 8GB,且不能使用 GPU,你应该选择哪种 Rerank 策略?(A) 跑一个 500M 参数的 Cross-Encoder (B) 仅使用 Embedding 相似度,放弃 Rerank (C) 使用基于关键词匹配的简单打分。

挑战题 (实战思考)

  1. 多项目隔离:你的 RAG 服务同时服务于 Project A 和 Project B。这两个项目都有 utils.py。如何设计 Index 结构和 Retrieve 接口,确保在 Project A 提问时不会搜到 Project B 的代码?
  2. 冷启动优化:如果你必须使用“临时命令模式”(CLI Mode),有什么办法能将 Embedding 模型的加载时间从 3 秒优化到 0.5 秒以内?(Hint: ONNX, Quantization, 或者是某种系统级缓存机制?)
  3. 上下文窗口溢出:CC 的上下文窗口很大(如 200k),但并非无限。如果在一次长对话中,CC 连续调用了 10 次搜索,每次都返回 5k tokens 的内容。作为架构师,你应该在 Serve 层设计什么机制来避免之前的检索结果把窗口挤爆?(Hint: 这个问题涉及 CC 侧的历史管理,但 RAG 端能否配合?)
  4. 流式传输:CC 支持工具调用的流式输出吗?如果支持,你的架构如何调整以实“边搜边吐”?如果不支持,这意味着什么?
点击展开参考答案 **1. 架构绘图** * File Event -> Watcher -> Filter (.gitignore) -> Parser (Extract Text) -> Chunker -> Embedding Model -> Vector DB (Upsert). **2. 接口设计** * 在 `results` 数组的每个对象中增加 `"last_modified_iso": "2023-10-27T10:00:00Z"` 字段。 **3. 组件选择** * **推荐 (B) 或 (C)**。在 8GB 内存且无 GPU 的机器上跑 Cross-Encoder 会非常慢(可能超过 2-3 秒),严重影响体验。建议优化 Embedding 模型本身(使用 MTEB 排名靠前的轻量模型),或者使用混合检索(BM25+Vector)来替代重排层。 **4. 多项目隔离** * **Index 侧**:在 Vector DB 的 Metadata 中增加 `project_id` 或 `root_path` 字段。或者为每个项目创建一个独立的 Collection/Namespace。 * **Retrieve 侧**:输入契约中必须强制包含 `project_root`,查询时在向量库中执行 Filter 操作:`where project_root == input.project_root`。 **5. 冷启动优化** * **ONNX Runtime**:将 PyTorch 模型导出为 ONNX,启动速度快很多。 * **量化 (Quantization)**:使用 INT8 量化模型,减小模型体积,加快加载。 * **mmap**:某些库支持内存映射加载模型。 * **架构规避**:其实最好的办法是不要优化 CLI 冷启动,而是改用 Server 模式。 **6. 上下文窗口溢出** * 这是一个 trick question。RAG 服务本身是无状态的,不知道“第几次调用”。 * **解法 1(RAG 端)**:提供 `summarize=True` 参数,返回摘要而不是全文。 * **解法 2(CC 端/Prompt)**:系统提示词应指示 CC “当获得新信息且旧信息不再相关时,要在内心独白中明确忽略旧 context”。 * **解法 3(高级)**:RAG 返回的结果带上 `id`,CC 下次查询时可以说 `exclude_ids=[...]`,但这增加了复杂性。通常依赖 CC 自身的上下文滑动窗口机制。 **7. 流式传输** * 前大多数 Agent 框架(包括 CC)在工具调用时是**等待完整返回**的。这意味着架构必须是 Request-Response 模型。你不能“边搜边吐”。这意味着 Serve 层必须等所有步骤(包括 Rerank)做完才能一次性返回 JSON。这也是为什么延迟控制如此重要。

9. 常见陷阱与错误 (Gotchas)

🔴 陷阱 1:Eager Loading 的诅咒

🔴 陷阱 2:忽略 .gitignore

🔴 陷阱 3:返回绝对路径

🔴 陷阱 4:JSON 字符串转义地狱


< 上一章:CC 数据完整拆解 下一章:数据摄取 >