第 5 章:数据摄取——从代码/文档到可检索语料
1. 开篇与目标
在 RAG 系统中,数据摄取 (Ingestion) 往往是最被低估,却最能决定系统上限的环节。很多人以为摄取就是 open(file, 'r').read(),但这在面向 Claude Code (CC) 的场景下是远远不够的。
CC 不仅仅是一个回答问题的聊天机器人,它是一个Agent(智能体)。它会根据检索到的信息去执行 ls、view、edit 等命令。这意味着:
- 路径必须精准:如果你的 RAG 告诉 CC 某段代码在
file.py,但实际上它在 src/utils/file.py,CC 的 edit 命令就会失败。
- 上下文必须完整:代码不是孤立存在的,它依附于文件结构、Git 历史和模块依赖。
- 噪声必须极低:CC 的上下文窗口昂贵且有限,混入
node_modules 或压缩后的 JS 代码不仅浪费钱,还会干扰 CC 对项目架构的理解。
本章的目标是构建一个工业级的数据摄取流水线 (Ingestion Pipeline)。我们将从杂乱的工程目录中提取出干净、标准化、富含元数据的“文档对象”,为下一章的切分(Chunking)做好准备。
2. 核心论述
2.1 摄取流水线架构 (The Ingestion Pipeline)
一个健壮的摄取系统不仅仅是遍历文件,它是多道工序的组合。
[ 原始代码仓库 (Raw Repo) ]
|
v
+-----------------------+ +-----------------------+ +-----------------------+
| 1. 遍历与发现 (Scan) | -> | 2. 过滤与黑名单 (Gate)| -> | 3. 类型检测 (Detect) |
| - os.walk / glob | | - .gitignore 解析 | | - 扩展名映射 |
| - 符号链接处理 | | - .ragignore (自定义) | | - libmagic / 只有文本 |
| - Git 变更检测 (增量) | | - 大文件/二进制熔断 | | - 编码探测 (UTF-8?) |
+-----------------------+ +-----------------------+ +-----------------------+
|
+------------------------------------------------------------+
|
v
+-----------------------+ +-----------------------+ +-----------------------+
| 4. 清洗与标准化 (ETL) | -> | 5. 元数据提取 (Meta) | -> | 6. 统一封装 (Pack) |
| - 移除 BOM 头 | | - 相对路径 (关键!) | | - 生成 Document 对象 |
| - 统一换行符 (\n) | | - 修改时间 / Git Info | | - 附加 Hash 签名 |
| - PII 脱敏 (可选) | | - 语言标识 (Python..) | | - 准备进入 Chunking |
+-----------------------+ +-----------------------+ +-----------------------+
|
v
[ 待切分的标准化文档列表 ]
2.2 过滤策略:什么是“噪声”?
在 RAG 中,少即是多。我们需要维护两层过滤网:
第一层:通用工程过滤 (The “.gitignore” Logic)
绝大多数情况下,git 忽略的文件,RAG 也应该忽略。
- 依赖库:
node_modules/, venv/, site-packages/, target/dependency/。
- 原因:这些是第三方代码,体积巨大。除非你的 RAG 是为了做“全网代码检索”,否则不要索引它们。CC 需要修改的是你的代码,不是 React 的源码。
- 构建产物:
dist/, build/, bin/, obj/, __pycache__/, .class。
- 系统文件:
.DS_Store, Thumbs.db。
第二层:RAG 专用过滤 (The “.ragignore” Logic)
有些文件 git 需要,但 RAG 不需要(或者读不懂)。建议在项目根目录维护一个 .ragignore 列表。
- 锁文件:
package-lock.json, yarn.lock, go.sum。
- 特征:包含成千上万行 Hash 值。
- 处理:直接屏蔽。它们对语义理解毫无帮助,且会迅速填满 Token 窗口。
- 大型静态资源:
.svg (除非你需要检索图标代码), .json (如果是大型数据转储), .csv (大数据集)。
- 压缩/混淆代码:
jquery.min.js。一行代码 50KB 长,没有任何语义价值。
2.3 解析策略:针对不同源的 ETL
A. 代码文件 (Source Code)
- 编码修复:这是最容易崩的地方。不要假设所有文件都是 UTF-8。建议使用
chardet 或类似库进行探测。如果检测到二进制或无法解码,Fail fast(跳过并记录日志),不要让整个流程崩溃。
- 语言标记:建立严格的
扩展名 -> 编程语言 映射表(例如 .rs -> Rust)。这个 language 字段后续会用于 Embedding 模型的选择(某些模型对特定语言优化)以及 Markdown 代码块的渲染。
B. 结构化文件 (JSON/YAML/TOML)
CC 阅读大段 JSON 很吃力。如果必须索引配置文件(如 package.json 或 k8s YAML),建议做扁平化处理或白名单提取。
- 策略 1:白名单
对于
package.json,只提取 scripts 和 dependencies,丢弃 devDependencies 或其他元数据。
- 策略 2:路径扁平化 (Flattening)
将深层嵌套的 JSON 转换为“路径-值”对的文本行,更利于向量检索。
- 原数据:
{"server": {"port": 8080, "host": "0.0.0.0"}}
- 转换后:
server.port: 8080
server.host: 0.0.0.0
C. 文档与 Markdown
- 清理:移除纯装饰性的 HTML 标签(如
<div align="center">,徽章图片链接)。
- 链接重写:Markdown 中的相对链接
[Docs](./docs/intro.md) 在 RAG 检索出的片段中会失效。
- 高级技巧:在摄取阶段,将相对链接转换为包含“文件名”文本描述,或者保留原样但在元数据中记录依赖关系。
这是本章最重要的部分。没有元数据的 Chunk 是一座孤岛。
当 CC 拿到一个代码片段时,它需要知道“我在哪”、“我是谁”、“我什么时候来的”。
以下是必须附加到每个文档对象上的元数据字段:
| 字段名 |
必选/可选 |
描述与用途 |
file_path |
核心 |
相对于项目根目录的路径 (如 src/auth/login.ts)。CC 的 edit_file 工具直接依赖此字段。绝对不要只存文件名。 |
file_name |
必选 |
文件名 (如 login.ts)。用于文件名关键词匹配。 |
file_type |
必选 |
扩展名或语言 (如 typescript)。 |
mod_time |
必选 |
最后修改时间戳。用于判断代码新鲜度,防止 CC 引用过时的废弃代码。 |
repo_name |
可选 |
仓库名。如果你是多仓库 RAG,这字段用于隔离上下文。 |
git_hash |
可选 |
当前文件的 Git Commit Hash。用于版本溯源。 |
git_last_msg |
高级 |
最后一次提交的 Commit Message。这非常有价值,它告诉了 CC “这段代码最近为什么被修改” (例如 “Fix NPE in login”)。 |
2.5 增量摄取 (Incremental Ingestion) 策略
对于大项目,每次全量扫描是不现实的。你需要维护一个轻量级的 索引状态库 (State Store)。
算法流程:
- Walk:遍历文件系统,获取
(path, mtime, size) 元组列表。
- Diff:
- 如果是新路径 -> 标记为
ADDED。
- 如果路径存在但
mtime 变了 -> 标记为 MODIFIED。
- 如果状态库中有但文件系统没了 -> 标记为
DELETED。
- Action:
ADDED/MODIFIED -> 进入解析、切分、Embedding 流程,写入向量库。
DELETED -> 根据 file_path 从向量库中删除所有相关 chunk。
3. 本章小结
- 路即正义:对于 CC 而言,
file_path 的准确性与代码内容同等重要。务必使用项目根目录相对路径。
- 清洗大于模型:把
node_modules 和 package-lock.json 喂给最好的模型也是浪费。好的过滤规则能让 RAG 效果翻倍。
- Fail Safe:处理编码错误和二进制文件时要优雅降级,不要阻塞流水线。
- 元数据先行:在文本切分之前,先把 Git 信息、时间戳等元数据“焊”在文档对象上。
4. 练习题
基础题 (50%)
Q1. 过滤器配置 (Rule of Thumb)
你正在编写一个 Python 脚本来遍历目录。请根据以下文件列表,写出过滤逻辑(伪代码或思路),说明处理方式(保留/丢弃/特殊处理)及理由。
src/main.py
.env.local
tests/test_api.py
docs/images/arch.png
requirements.txt
venv/lib/python3.9/site-packages/requests/...
点击查看参考答案
**参考答案:**
1. **`src/main.py` -> 保留**。核心源码。
2. **`.env.local` -> 丢弃 (高危)**。包含敏感密钥,绝对禁止入库。建议在代码中硬编码正则 `^\.env.*` 进行过滤。
3. **`tests/test_api.py` -> 保留**。测试代码是理解业务逻辑的绝佳文档,CC 经常通过阅读测试来理解如何调用 API。
4. **`docs/images/arch.png` -> 丢弃**。二进制图像,文本 RAG 无法处理(除非你有多模态模型,否则作为噪声处理)。
5. **`requirements.txt` -> 保留**。重要的依赖元数据。
6. **`venv/...` -> 丢弃**。属于第三方库环境,体积大且非用户代码。
Q2. 元数据结构设计
CC 用户提问:“修改一下登录页面的验证逻辑”。
检索系统找到了 login.ts。为了让 CC 能成功执行修改,你的 Document 对象(在进入向量库前)的 JSON 结构应该长什么样?请写出一个示例 JSON。
点击查看参考答案
**参考答案:**
```json
{
"content": "export function validateLogin(user) { ... }", // 实际内容
"metadata": {
"file_path": "frontend/src/pages/login/login.ts", // 关键:相对路径,CC edit_file 用
"file_name": "login.ts",
"language": "typescript",
"last_modified": "2023-10-27T10:00:00Z",
"source": "local_repo"
},
"id": "frontend/src/pages/login/login.ts" // ID 通常建议用路径哈希或路径本身,方便去重
}
```
Q3. 编码灾难
你的扫描器在读取一个古老的 GB2312 编码的中文注释文件时崩溃了。请写出一段 Python 伪代码,展示如何健壮地读取文件内容(尝试 UTF-8,失败则尝试其他或忽略)。
点击查看参考答案
**参考答案:**
```python
def safe_read(file_path):
# 优先级 1: UTF-8 (绝大多数情况)
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except UnicodeDecodeError:
pass
# 优先级 2: 试常见的旧编码 (GBK, Latin-1)
for encoding in ['gb18030', 'latin-1']:
try:
with open(file_path, 'r', encoding=encoding) as f:
content = f.read()
print(f"Warning: {file_path} read as {encoding}")
return content
except UnicodeDecodeError:
continue
# 优先级 3: 放弃,返回空或 None,记录错误日志
print(f"Error: Failed to decode {file_path}. Skipping.")
return None
```
*注:生产环境建议使用 `chardet` 库检测,但上述 fallback 逻辑是必备的兜底。*
挑战题 (50%)
Q4. 单体大仓 (Monorepo) 的隔离策略
你的公司有一个巨大的 Monorepo,里面包含 mobile-app (React Native), backend-api (Go), data-pipeline (Python) 三个完全独立的项目。
如果用户问 CC:“如何启动服务器?”,简单的 RAG 可能会检索到三个项目的启动脚本,导致 CC 困惑。
请在摄取阶段设计一种元数据策略,并在检索阶段配合使用,来解决这个问题。
提示:思考 domain 或 project_scope 字段。
点击查看参考答案
**参考答案:**
**摄取阶段 (Ingestion)**:
分析文件路径的前缀。
* 如果路径以 `backend-api/` 开头,给文档打上元数据 `project: backend`。
* 如果路径以 `mobile-app/` 开头,打上 `project: mobile`。
* 如果是根目录通用的(如 `README.md`),可以打上 `project: global` 或包含所有标签。
**检索/交互阶段 (Retrieval)**:
1. **隐式推断**:如果用户当前打开的文件在 `backend-api/` 目录下(CC 上下文通常包含当前工作目录或活跃文件),则在检索时自动添加过滤器 `filter={project: backend}`。
2. **显式询问**:如果在根目录,RAG 可以返回各个项目的摘要,或者 CC 可能会追问“你想启动哪个服务器?”(前提是你的 System Prompt 鼓励澄清)。
Q5. 敏感信息扫描 (Secret Scanning)
为了防止 RAG 成为泄密源,你需要在摄取阶段实现一个简易的“敏感信息熔断器”。
请列出至少 3 种需要检测的正则模式(概念即可),并设计:如果在一个 1000 行的代码文件中发现了 1 个密钥,你是选择丢弃整个文件,还是仅仅掩盖那一行?请阐述理由(从安全性 vs 实用性角度)。
点击查看参考答案
**参考答案:**
**正则模式**:
1. `AWS_ACCESS_KEY_ID = "AKIA..."` (特定格式)
2. `-----BEGIN RSA PRIVATE KEY-----` (私钥头)
3. `api_key = "sk-..."` (常见的 API Key 模式)
**策略选择:掩盖那一行 (Redaction/Masking) 通常优于 丢弃整个文件。**
* **理由**:
* **实用性**:一个 1000 行的核心逻辑文件(如 `config.py`)可能包含大量重要的配置逻辑,只有 1 行是敏感的 Password。如果丢弃整个文件,RAG 就会对该模块“失明”,导致 CC 法回答相关架构问题。
* **安全性**:将敏感行替换为 ``。
* **例外**:如果是 `.pem`, `id_rsa`, `.p12` 这种**纯密钥文件**,应直接丢弃整个文件。
</details>
**Q6. 上下文增强:Git Blame 的妙用**
普通的 RAG 只看代码现在的样子。但 CC 有时需要知道“谁最近改了这里”或者“这个改动是为了修复什么 Bug”。
请设计一个流程,在摄取代码文件时,如何利用 `git` 命令提取信息并将其转化为 RAG 可用的文本数据?
你需要构造一段文本,这段文本会被加到 chunk 的末尾或开头,给 LLM 提供额外的背景。
点击查看参考答案
**参考答案:**
**流程**:
1. 在扫描到文件(如 `utils.py`)时,执行 `git log -n 1 --pretty=format:"%an|%s|%cd" -- path/to/utils.py`。
2. 获取输出:`Alice|Fix NPE in user inputs|2023-10-10`。
3. **构造增强文本 (Context Header)**:
在将文件容送入 Embedding 之前,在文件头部插入一段注释风格的描述:
```text
---
File: src/utils.py
Last Modified By: Alice
Last Commit: Fix NPE in user inputs (2023-10-10)
---
[代码内容...]
```
**价值**:
当用户问“为什么这里要判空?”时,即便代码里没写注释,RAG 检索到的这段 Commit Message 也能让 CC 推断出“哦,这是为了修复之前的 NPE Bug”。
---
## 5. 常见陷阱与错误 (Gotchas)
1. **符号链接死循环 (Symlink Loops)**
* *现象*:扫描程序卡死,内存暴涨。
* *原因*:`folder_a` 包含一个指向 `folder_b` 的链接,而 `folder_b` 又链回 `folder_a`。
* *解决*:使用 `os.walk(followlinks=False)`(默认通常是 False,但要注意检查)。或者维护一个 `visited_inodes` 集合,记录已访问文件的 inode 编号。
2. **忽略了 `.gitignore` 的层级性**
* *现象*:根目录 `.gitignore` 过滤了,但子模或嵌套目录里的 `.gitignore` 规则没生效。
* *解决*:简单的读取根目录 `.gitignore` 是不够的。标准的 Git 行为是级联的。建议直接调用 `git ls-files` 命令来获取文件列表,而不是自己重新实现一套 `.gitignore` 解析逻辑。这是最稳妥的“不做重复造轮子”的方法。
3. **错误的相对路径**
* *现象*:在 Docker 或不同机器上构建索引时,记录了绝对路径 `/app/code/src/main.py`。
* *后果*:当 CC 在用户的本地机器 `/Users/bob/code` 上运行时,RAG 返回的路径不匹配,CC 找不到文件。
* *解决*:**永远只存储相对于项目根目录 (Repository Root) 的路径**。
4. **把 Minified JS 当作源码**
* *现象*:检索出了 `jquery.min.js` 的片段,内容是一行 10 万个字符的乱码。
* *解决*:设置单行最大字符数限制(如单行超过 500 字符则视为 Minified/Binary),或者根据 `.min.` 命名惯例过滤。
5. **频繁的全量索引**
* *现象*:每次写代码都触发 RAG 重建,导致 CPU 占用过高,电脑卡顿。
* *解决*:摄取应该是一个**低频全量 + 高频增量**的过程,或者由用户手动触发(“Refresh Context”)。对于本地外挂 RAG,监听文件系统变更事件(File Watcher)来更新索引是一个好主意,但要记得加**防抖 (Debounce)**。