如果说切分(Chunking)是将书籍撕成书页,那么向量化(Embedding)就是将这些书页翻译成机器能瞬间理解的“坐标”。而向量库(Vector Database)则是存放这些坐标的高维地图。
对于基于 Claude Code (CC) 的外挂 RAG,这一层是系统的大脑皮层。它的质量直接决定了:
本章将带你深入向量化的黑盒,不只讲“怎么存”,更讲“怎么存才能让 CC 用得顺手”。
选择 Embedding 模型不仅仅是看排行榜(如 MTEB Leaderboard),你需要根据 CC 的使用场景在以下四个维度做权衡:
all-MiniLM-L6-v2。如果你在第 6 章切分的块很大(如 1000 tokens),尾部信息会被强行截断,导致“虎头蛇尾”的检索失效。jina-embeddings-v2。允许索引整个文件或大函数,适合“粗粒度”检索。对于 CC 外挂 RAG,本地模型通常是更优解。
| 特性 | 本地模型 (Local/Sidecar) | 云端模型 (OpenAI/Cohere) |
|---|---|---|
| 隐私 | 代码不出网,绝对安全 | 代码需上传第三方 |
| 成本 | 消耗本地 CPU/内存 | 按 Token 计费 ($$) |
| 延迟 | 无网络开销,取决于硬件 | 取决于网络 (100ms+) |
| 离线可用 | 是 | 否 |
| 推荐模型 | bge-m3, nomic-embed-text, stella |
text-embedding-3-small/large |
Rule of Thumb (经验法则) 既然使用 CC 的开发者通常在本地环境工作,推荐使用 Ollama 或 SentenceTransformers 运行一个 768 维度的本地模型。这能兼顾 90% 的语义效果和 100% 的数据隐私。
许多教程只教你存 id 和 vector,但这对外挂 RAG 远远不够。
CC 需要知道文件名、路径、代码语言、最后修改时间,甚至需要原始内容来做上下文填充。
+-----------------------------------------------------------------------+
| Vector Record Schema |
+-----------------------------------------------------------------------+
| ID (Primary Key) | 确定性哈希 (例如: sha256(file_path + content)) |
|------------------|----------------------------------------------------|
| Vector (Dense) | [0.012, -0.15, ... 0.88] (Float32 或 Int8) |
|------------------|----------------------------------------------------|
| Sparse Vector | {"api": 0.5, "key": 0.8, "auth": 0.2} (可选,用于混合检索)|
|------------------|----------------------------------------------------|
| Payload/Metadata | { |
| | "source": "local_repo", |
| | "repo_name": "my-backend-service", |
| | "rel_path": "src/controllers/auth.ts", <--- 关键 |
| | "abs_path": "/Users/me/projects/...", |
| | "chunk_index": 5, |
| | "total_chunks": 12, |
| | "start_line": 102, <--- 关键 |
| | "end_line": 150, <--- 关键 |
| | "language": "typescript", |
| | "last_modified": 1715000000, |
| | "content": "export class AuthController { ... }" |
| | } |
+-----------------------------------------------------------------------+
rel_path (相对路径): CC 最需要的字段。当 RAG 检索到相关代码时,需要告诉 CC:“去读 src/controllers/auth.ts”。content (原始内容): 也就是所谓的 chunk text。必须存入数据库。虽然这样会增加存储体积,但这避免了检索后还要去磁盘读文件的二次 IO 开销,且防止文件被修改后读取内容与索引不一致。纯向量检索(Dense Retrieval)有一个致命弱点:对精确关键词不敏感。
ERR_NV_009 或变量名 max_retry_count。 User Query: "How is max_retry_count used in redis config?"
/ \
/ \
[Sparse Encoder (BM25/Splade)] [Dense Encoder (Model)]
| |
{max:0.4, retry:0.6, ...} [0.1, 0.9, -0.3, ...]
| |
[Keyword Search] [Vector Search]
| |
(Hits List A) (Hits List B)
\ /
\ /
\-------> [Reciprocal Rank Fusion] (RRF 算法融合排名)
|
Final Top-K
Rule of Thumb 如果你的向量库(如 Chroma, Qdrant, Weaviate)支持混合检索,务必开启。如果不支持,作为简单的替代方案,可以在元数据中对
file_path和function_name做简单的关键词过滤。
对整个仓库进行 git pull 后,不要傻乎乎地全量重建索引。这不仅慢,还会因为大量 CPU 占用导致电脑卡顿。
你需要在向量库之外维护一个轻量级记录(如 sqlite 或 .json 文件),记录文件的“指纹”。
增量更新逻辑流程图:
[磁盘扫描] -> 获得当前文件列表 (Path, Mtime, Size)
|
v
[指纹计算] -> 计算文件内容 Hash (Fast Hash, e.g., XXHash/MD5)
|
+-----> 对比 [状态管理文件 (Registry)]
|
+---> Case 1: Hash 不存在 (新文件) -> [加入处理队列]
|
+---> Case 2: Hash 变了 (修改过的文件) -> [标记旧 ID 删除] -> [加入处理队列]
|
+---> Case 3: Hash 一致 (未变动) -> [跳过]
|
v
[清理阶段] -> 检查 Registry 中存在但磁盘扫描没见到的文件 (已删除文件) -> [从向量库删除]
版本控制的挑战:
如果用户切换了 git 分支(main -> feature-x),大量文件 Hash 会改变。
Q1: 维度与存储体积
你选择了 OpenAI text-embedding-3-large (3072 维) 且未进行降维。每个维度是 float32 (4 bytes)。
如果你有 100 万个 chunks。仅向量数据(不含元数据和索引开销)大约占用多少内存/磁盘?这对于本地运行的 RAG 合理吗?
Q2: 脏数据入库
你的 Ingest 脚本没有过滤文件类型。结果检索时,CC 经常收到一堆乱码,经检查是 .png 图片和 .pyc 字节码文件被强行当做文本读取并索引了。
请设计一个简单的规则集来防止这种情况。
Q3: 路径的相对性
你在 Metadata 中存储了绝对路径 /Users/tom/project/src/main.py。
当你想把这个向量库分享给同事 Jerry(路径为 /Users/jerry/work/project/...)使用时,会发生什么?CC 能找到文件吗?
Q4: 语义漂移与“过期”索引
代码库重构了。AuthController 类被从 auth.ts 移动到了 users.ts,且逻辑完全改变。
由于某种原因,你的增量更新逻辑只添加 users.ts 的新 chunk,却因为 bug 没有删除 auth.ts 的旧 chunk。
此时用户问“如何修改登录逻辑”,RAG 返回了新旧两份代码。这会对 CC 造成什么具体的困扰?
Q5: 量化 (Quantization) 的代价
为了节省内存,你启用了向量库的 Scalar Quantization (Int8),将 float32 压缩为 int8。
这大大提升了速度并降低了内存。
但是在什么特定的检索场景下,这种压缩会导致召回率显著下降?
Q6: 多租户/多项目隔离 你希望你的 RAG 服务同时支持多个本地项目。 如果不为每个项目启动一个单独的向量库实例,你应该如何在同一个库中隔离它们? 如果用户在 Project A 中提问,却检索到了 Project B 的代码,这是一个严重的安全/体验事故。
.gitignore 导致的“噪声风暴”node_modules 里的第三方库代码,或者是构建产物 dist/main.js(通常是一行超长的混淆代码).gitignore。Top-K 没有 Score Thresholduuid() 生成 ID。每次重建索引或增量更新时,即使内容没,ID 也变了。ID = hash(file_path + chunk_index) 或 ID = hash(chunk_content)。这样相同的文本永远生成相同的 ID,天然实现幂等写入。