cc_rag_tutorial

第 5 章:数据摄取——从代码/文档到可检索语料

1. 开篇与目标

在 RAG 系统中,数据摄取 (Ingestion) 往往是最被低估,却最能决定系统上限的环节。很多人以为摄取就是 open(file, 'r').read(),但这在面向 Claude Code (CC) 的场景下是远远不够的。

CC 不仅仅是一个回答问题的聊天机器人,它是一个Agent(智能体)。它会根据检索到的信息去执行 lsviewedit 等命令。这意味着:

  1. 路径必须精准:如果你的 RAG 告诉 CC 某段代码在 file.py,但实际上它在 src/utils/file.py,CC 的 edit 命令就会失败。
  2. 上下文必须完整:代码不是孤立存在的,它依附于文件结构、Git 历史和模块依赖。
  3. 噪声必须极低: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 也应该忽略。

第二层:RAG 专用过滤 (The “.ragignore” Logic)

有些文件 git 需要,但 RAG 不需要(或者读不懂)。建议在项目根目录维护一个 .ragignore 列表。

2.3 解析策略:针对不同源的 ETL

A. 代码文件 (Source Code)

B. 结构化文件 (JSON/YAML/TOML)

CC 阅读大段 JSON 很吃力。如果必须索引配置文件(如 package.json 或 k8s YAML),建议做扁平化处理白名单提取

C. 文档与 Markdown

2.4 元数据增强 (Metadata Enrichment) —— CC 的生命线

这是本章最重要的部分。没有元数据的 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)

算法流程:

  1. Walk:遍历文件系统,获取 (path, mtime, size) 元组列表。
  2. Diff
    • 如果是新路径 -> 标记为 ADDED
    • 如果路径存在但 mtime 变了 -> 标记为 MODIFIED
    • 如果状态库中有但文件系统没了 -> 标记为 DELETED
  3. Action
    • ADDED/MODIFIED -> 进入解析、切分、Embedding 流程,写入向量库。
    • DELETED -> 根据 file_path 从向量库中删除所有相关 chunk。

3. 本章小结


4. 练习题

基础题 (50%)

Q1. 过滤器配置 (Rule of Thumb) 你正在编写一个 Python 脚本来遍历目录。请根据以下文件列表,写出过滤逻辑(伪代码或思路),说明处理方式(保留/丢弃/特殊处理)及理由。

  1. src/main.py
  2. .env.local
  3. tests/test_api.py
  4. docs/images/arch.png
  5. requirements.txt
  6. 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 困惑。 请在摄取阶段设计一种元数据策略,并在检索阶段配合使用,来解决这个问题。

提示:思考 domainproject_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)**。