在前面的章节中,我们已经拆解了 RAG 的各个零件:切分、向量化、检索。现在,我们要把这些零件组装成一台精密的机器。
本章将提供一个 Reference Implementation(参考实现) 的架构蓝图。我们的目标不是写一个仅用于演示的玩具,而是一个遵循 Local-First(本地优先) 原则、具备 增量更新 能力、且深度适配 CC(Claude Code)交互协议 的最小可用产品(MVP)。
文系统 -> 向量索引 -> 检索接口 -> CC 上下文 的完整链路。我们将系统划分为两个独立生命周期的子系统:构建器 (Builder/Ingestor) 和 服务器 (Server/Retriever)。这种读写分离的设计是生产级系统的基础。
[ 文件系统 (User Codebase) ]
|
| (1. 扫描与哈希比对)
v
+-------------------------------------------------------+
| 构建器 (Builder Pipeline) - 离线/触发式运行 |
| |
| [过滤器] -> 忽略 .gitignore, 二进制, 超大文件 |
| | |
| [解析器] -> 提取文本, 识别语言, 提取元数据(Path/Time) |
| | |
| [切分器] -> 语义块 (Chunking) + 绑定 Line No. |
| | |
| [向量化] -> 调用 Embedding 模型 (Batch 处理) |
+-------------------------------------------------------+
| |
| (写入向量) | (写入元数据 & 原始内容)
v v
[ 向量索引 (FAISS/Chroma) ] [ 关系库/KV (SQLite/JSONL) ]
^ ^
| (ANN 搜索) | (ID 查找 & 注水)
| |
+-------------------------------------------------------+
| 服务器 (Server/CLI) - 实时响应 CC 请求 |
| |
| CC (User/Agent) --> [Query] --> [意图识别/改写] |
| | |
| [格式化器 (XML Packer)] <------- [检索器 (Retriever)] |
+-------------------------------------------------------+
经验法则:在参实现中,最关键的设计不是选什么向量库,而是 Chunk ID 的生成策略。
- 错误做法:使用 UUID 或自增 ID。这会导致每次重新索引时,旧数据无法被覆盖,向量库无限膨胀。
- 正确做法:Deterministic ID (确定性 ID)。
ID = Hash(File_Path + Chunk_Content_Snippet)这样,只要文件路径和内容没变,ID 就永远一致,天生支持幂等写入(Idempotency)。
一个健壮的 RAG 项目需要清晰的物理结构。以下是推荐的目录布局:
my-cc-rag/
├── corpus/ # (可选) 如果你不是直接挂载当前目录,这是放置文档的地方
├── data/ # 数据持久化目录
│ ├── .registry.json # [关键] 增量更新注册表 (Path -> FileHash)
│ ├── embeddings.index # 向量索引文件
│ └── metadata.db # SQLite, 储 Chunk 文本和详细元数据
├── src/
│ ├── ingest/ # 写入侧逻辑
│ ├── retrieve/ # 读取侧逻辑
│ └── utils/ # 共享工具 (Hash计算, 文本清洗)
├── templates/ # Prompt 模板 (用于 Query 改写或结果包装)
└── config.yaml # 配置文件 (模型选择, 忽略列表, 切分参数)
为了让 CC 能够利用检索结果进行代码修改,我们的数据模型必须包含 定位信息。
Chunk 对象模型 (TypeScript 风格伪代码):
interface Chunk {
// 核心标识
id: string; // Deterministic Hash
// 向量化字段
text: string; // 实际切分后的文本内容
vector: number[]; // Embedding 向量
// CC 必需元数据 (Metadata)
source_file: string; // 相对路径 (如 src/utils.ts)
line_start: number; // 起始行号 (CC edit 需要)
line_end: number; // 结束行号
language: string; // 编程语言标识
// 辅助元数据
checksum: string; // 源文件的 Hash (用于检测过期)
last_modified: number; // 时间戳 (用于在 Prompt 中提示时效性)
breadcrumbs: string[]; // 面包屑导航 (如 ["src", "utils", "class Auth"])
}
这是系统的“消化系统”。为了保证性能,必须实现 增量更新。
不要盲目读取所有文件。
data/.registry.json:读取上次运行时的 { file_path: file_hash } 映射。.gitignore 规则(必须!否则你会索引 node_modules)。processing_queue。processing_queue,并标记需删除旧 Chunk。Skipping cached file...)。在切分时,不仅要切文字,还要保留行号。
line_start。INSERT/REPLACE 元数据。add 向量。data/.registry.json。这部分定义了 CC 如何使用你工具。
用户(或 CC)发出的 Query 往往是模糊的。
为了避免向量检索在这就“精确匹配”场景下的失灵(例如搜具体的变量名),MVP 也应包含简单的关键词匹配。
Top_K = 20。CamelCase 变量名),对召回结果进行二次过滤,保留包含该字符串的 Chunk。Top_K = 5。这是将 RAG 真正变成“外挂”的关键。你需要将 JSON 数据转换为 CC 能够理解其结构的 XML。
设计原则:
输出模板 (Payload):
<rag_response>
<meta>
<query_latency>120ms</query_latency>
<total_results>3</total_results>
</meta>
<!-- 结果 1:代码片段 -->
<result id="1" score="0.92">
<file_path>src/auth/jwt_handler.py</file_path>
<lines>45-58</lines>
<last_modified>2023-10-25</last_modified>
<content_block>
<![CDATA[
def verify_token(token):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
return None
]]>
</content_block>
</result>
<!-- 结果 2:文档片段 -->
<result id="2" score="0.85">
<file_path>docs/api_spec.md</file_path>
<lines>102-110</lines>
<content_block>
<![CDATA[
## Error Handling
If the token is expired, the API will return a 401 Unauthorized status
with the message "Token expired".
]]>
</content_block>
</result>
</rag_response>
Rule of Thumb:始终使用
<![CDATA[ ... ]]>包裹代码内容。 代码中常包含<>&等字符,直接放入 XML 会导致解析错误。CDATA 是最安全的做法。
你有两种方式将这个系统暴露给 CC:
用户手动运行命令,将结果贴给 CC,或通过 Pipe 传递。
python rag.py search "JWT 验证" | claudecode
编写一个简单的 MCP Server(Model Context Protocol),定义工具描述:
{
"name": "search_codebase",
"description": "Semantically search the local codebase for snippets. Use this when you don't know file paths.",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"file_pattern": { "type": "string", "description": "Optional glob pattern to filter files" }
},
"required": ["query"]
}
}
当 CC 决定调用 search_codebase 时,你的 Python 脚本执行检索逻辑,并返回上述 XML 字符串。
.index, .db)与代码。Hash(Path+Content) 解决重复索引问题。registry.json 避免每次全量重跑,节省时间和 Embedding 费用。file_path 和 lines 是外挂 RAG 可用的核心。metadata.db 中,应该存储绝对路径(/User/bob/project/src/a.py)还是相对路径(src/a.py)?为什么?
src/old_feature.py,但在运行检索时,向量库里还有它的 Chunk。这会造成什么后果?你的 Ingest 脚本应该如何处理这种情况?
ingest 脚本中使用了 os.path.abspath(),存入数据库的是 /Users/me/code/app/main.py。os.path.relpath(file_path, project_root)。.png, .pyc, .exe 或 .git/index 文件。mime 类型检测或扩展名白名单(只允许 .md, .py, .ts, .txt 等)。</content_block> 字符串。SentenceBERT 这样的本地模型,第一次 import 和加载模型权重可能需要 3-5 秒。