Skip to content

第 27 章 RAG 与 Agent 项目

第 25、26 章讲的是"拿一个已有项目做审查",本章讲的是相反方向——从 0 到 1 用 Claude Code 构建一个可演示、可评估的 AI 应用。素材全部来自一个真实落地的 RAG 知识库问答 demo(GLM-5.2 + bge-small-zh + ChromaDB + Streamlit),包括一组用控制变量实验证明中文 embedding 选型重要性的硬数据。这一章既是实战项目复盘,也是从"检索增强"通向"Agent"的过渡桥梁。

27.1 结论:RAG 是 Agent 最小可评估形态

一句话结论:用 Claude Code 开发 RAG 系统,核心价值不是把 demo 跑起来,而是用 eval 闭环把"选型决定天花板"这件事变成可量化的硬数据。本章的 demo 跑通只花了几个小时,但真正让项目有面试说服力的,是后续三版控制变量对比实验——bge-small-zh(中文)Recall@1 = 100% 对 all-MiniLM-L6-v2(英文)44%,一组数据讲清中文 embedding 不可用英文模型。

这条结论的背后是第 06 章可执行闭环的迁移:代码项目的"完成"是测试通过,RAG 项目的"完成"是 eval 指标达标。没有 eval,RAG 调优全凭手感——你说换了个 embedding 效果更好,凭什么?有了 eval,每一次选型变更都留下数据,形成 v1→v2→v3 的对比表,这才是工程化 RAG 与"跑通一个 demo"的根本区别。

本章覆盖四件事:

  1. RAG 架构:7 模块职责切分(ingest/retrieve/generate/rag/llm/eval/app)
  2. 开发工作流:用 Claude Code 跑 Plan → 分模块实现 → eval 验证 → 迭代四步
  3. 三版对比实验:embedding 选型与 prompt 约束两个维度的控制变量数据
  4. 从 RAG 到 Agent:RAG 是检索增强,Agent 是工具调用 + 规划,两者如何衔接

诚实声明:本项目的 LLM 实为智谱 GLM-5.2,通过 Anthropic 兼容协议接入(ANTHROPIC_BASE_URL 指向 open.bigmodel.cn/api/anthropic),不是 Claude。config.py 支持切换到真正的 Claude API,但开发与评估全程用的都是 GLM-5.2。面试或复盘时必须如实说明,不要把 GLM 的成绩记到 Claude 头上——这是第 06 章"客观真实"纪律的底线。

27.2 项目全景:技术栈与 7 模块架构

技术栈:

选型说明
LLMGLM-5.2(Anthropic 兼容协议)非 Claude;config.py 可切换
EmbeddingBAAI/bge-small-zh-v1.5本地推理,中文优化,384 维,首次下载 ~95MB
向量库ChromaDBcosine 相似度,持久化,hnsw:space=cosine
UIStreamlit三栏:配置管理 / 问答 / 检索片段
文档解析PyPDF2 + 自写切分器PDF/txt/md + 段落聚合切分
运行环境Python 3.9 + venv隔离,禁止污染系统 Python

7 模块架构(职责严格正交,每个模块只做一件事):

text
data/docs/ ──ingest(切分+embedding+入库)──▶ ChromaDB

用户提问 ──retrieve(Top-K+score)──▶ generate(LLM+引用标注) ──▶ 答案

                                     eval(Recall@1/3/5 + KW + 延迟 + Token)

支撑模块:
  config.py   集中配置(路径/模型/参数),唯一配置入口
  src/llm.py  Claude/GLM client 构造 + chat() 封装 + 指数退避重试
  src/rag.py  串联 retrieve→generate,CLI 入口
  app.py      Streamlit UI

各模块行数与职责:

模块行数核心职责
config.py唯一配置入口:路径、模型名、chunk_size/overlap、top_k、rerank 开关
src/llm.py90client 构造、chat() 封装、529 overloaded_error 指数退避重试
src/ingest.py118文档读取(md/txt/pdf)、段落聚合切分、bge embedding、ChromaDB upsert
src/retrieve.py77懒加载 embedder/collection、Top-K cosine 检索、预留 rerank 接口
src/generate.py42SYSTEM_PROMPT 约束、上下文拼接、[1][2] 引用标注
src/rag.py46串联 retrieve→generate,CLI 入口
src/eval.py135跑 28 题、Recall@1/3/5 分档、KW、延迟、Token,输出 markdown 报告
app.pyStreamlit 三栏 UI

模块切分的原则是单一职责 + 可独立验证retrieve.py 能单独 python src/retrieve.py "问题" 跑通,generate.py 能单独跑,eval.py 能端到端跑。这样调试时能快速定位问题在检索还是在生成——这是 RAG 工程化的关键,因为 RAG 的错误链特别长(切分错?embedding 差?检索没命中?prompt 没约束?LLM 幻觉?),模块边界清晰才能二分定位。

27.3 用 Claude Code 开发 RAG 的工作流

这个项目不是"让 Claude 一次写完",而是严格走第 06 章的可执行闭环 + 第 07 章的 Plan Mode。四步:

Step 1:Plan Mode 建立架构事实模型

进 Plan Mode(只读),让 Claude 先理解"RAG 系统有哪些正交模块、每个模块的输入输出边界、数据流怎么走"。这一步产出的是模块清单与接口契约,不是代码。关键约束:

  • 7 模块职责必须正交(ingest 只管入库,不管检索;retrieve 只管检索,不管生成)
  • config.py 是唯一配置入口,其他模块禁止硬编码参数
  • 密钥从环境变量读,不进代码/commit(第 04 章 CLAUDE.md 红线)

Step 2:分模块实现(小步快跑)

按依赖顺序逐模块实现,每个模块写完立即跑通验证,不让 Claude 一次生成 7 个文件。顺序:

text
config.py → llm.py(验证:chat() 能返回) → ingest.py(验证:向量库建成)
→ retrieve.py(验证:Top-K 返回) → generate.py(验证:答案带引用)
→ rag.py(验证:CLI 端到端) → eval.py(验证:报告生成) → app.py

每步的验证命令写进任务 Prompt,不靠 Claude 猜。例如 ingest.py 的完成标准是"运行 python src/ingest.pyvectorstore/ 目录下生成 ChromaDB collection,打印 共 N chunks"。

Step 3:eval 验证(闭环的核心)

python src/eval.py 跑 28 题,生成 results/eval_report.md。这一步是整个项目的承重墙——没有 eval,后面所有优化都是凭感觉。eval 脚本一次性产出:Recall@1/3/5、Keyword 命中率、平均检索/生成延迟、总 Token(in/out),以及逐题详情表。

Step 4:迭代(控制变量对比)

每次只改一个维度,重跑 eval,数据记进 results/optimization_log.md。这就是 27.4 节的三版对比。关键纪律:控制变量——v2 只换 embedding 不动 prompt,v3 只动 prompt 不动 embedding,这样才能把指标变化归因到具体维度。同时改两个维度,数据就废了。

这四步的本质是把第 06 章的可执行闭环从"代码"迁到"RAG":完成状态从"测试通过"换成"Recall@1 达标 + 无答案题正确拒绝 + Token 成本可控",而这些都是机器可检查的。一旦可检查,Claude 就能自己跑 eval、读指标、提下一版优化建议。

27.4 三版对比实验(核心亮点)

这是项目最有说服力的部分,也是面试时讲清"为什么这样选型"的硬数据。三版都是控制变量实验:每次只改一个维度,其余同 v1 基线。

基线 v1:bge-small-zh + 有 prompt 约束,28 题。

指标
Recall@125/25 = 100.0%(25 题有期望源)
Recall@3100.0%
Recall@5100.0%
Keyword 命中率28/28 = 100.0%
平均检索延迟0.22s(首次加载模型 ~6s,缓存后 <0.05s)
平均生成延迟3.52s
总 Token(in/out)33912 / 2005

三版对比表:

版本embeddingpromptRecall@1Recall@5KW结论
v1bge-small-zh(中)有约束100%100%100%最优基线
v2MiniLM(英)有约束44.0%92.0%82.1%embedding 选型 -56pt
v3bge-small-zh无约束100%100%96.4%prompt 影响拒绝行为

27.4.1 v2:换英文 embedding,Recall@1 暴跌到 44%

控制变量:仅换 embedding(bge-small-zhall-MiniLM-L6-v2,英文模型,同 384 维),其余同 v1(chunk 512/50、top_k 5、有 prompt 约束)。

结果:Recall@1 从 100% 暴跌到 44%,-56 个百分点。Top-1 精度受影响最大,Top-5 因范围宽尚可(92%),KW 也因检索不准导致生成缺关键词(82.1%)。

根因:英文 embedding 模型对中文分词与语义建模不足,同一概念的中文 query 与中文文档在向量空间里对不齐。这是中文 RAG 落地最容易踩的坑——英文模型维度相同(都是 384),看起来"能跑",但召回质量崩塌。

铁证:中文 RAG 必须选中文 embedding 模型。这个结论用一组数据(100% vs 44%)讲完,胜过千言万语。

27.4.2 v3:移除 prompt 约束,KW 略降

控制变量:仅改 system prompt(移除"只使用上下文/不知道就说不知道/标注引用"四条约束,只留"根据上下文回答问题"),embedding 恢复 bge。

结果:检索指标不变(Recall@1/5 仍 100%,证明 prompt 不影响检索),KW 从 100% 降到 96.4%,-3.6pt。差异集中在 q24(GL vs GC 区别题):有约束时 LLM 说"未明确区别"(正确拒绝),无约束时倾向编造区别。

根因:prompt 约束的核心价值在干扰区分题与无答案题的拒绝行为,不影响检索。本项目 KW 降幅有限(-3.6pt),是因为 GLM-5.2 本身较谨慎,即使无约束也会主动拒绝无答案题(q21-23)。换更激进的模型,约束作用会更显著。

27.4.3 三个结论

  1. embedding 选型是中文 RAG 质量的决定因素(v2: Recall@1 -56pt,最显著)
  2. prompt 约束影响拒绝/幻觉行为(v3: 干扰题拒绝率降,但因模型本身谨慎影响有限)
  3. 当前 v1 配置在小知识库(37 chunks)上已达最优;生产规模(千+ chunks)下 rerank、chunk 调优、混合检索等优化才会显著

这三条结论之所以可信,是因为每条都有控制变量实验背书,不是凭感觉。这就是 eval 闭环的价值——把"我觉得"变成"数据说"。

27.5 评估体系:28 题与 Recall@K 分档

eval 体系是本项目的另一承重墙。设计原则:题集必须覆盖不同难度,指标必须分档,无答案题必须单独评估。

27.5.1 28 题测试集构成

类别题数作用
mba(简单)10直接对应单一文档,验证基础召回
faq(简单)10多源验证(daily_stock_analysis FAQ)
no_answer(无答案)3幻觉控制:知识库无信息时是否拒绝
hard_distinguish(干扰区分)2检索精度:相似内容能否区分(GL vs GC)
cross_chunk(跨文档)3多块整合:答案需多个 chunk 拼接

20 简单 + 8 难度题。v1 基线在简单题上 100%,无法体现优化空间——所以追加 8 题难度题 + 把指标从 Recall@5 升级到 Recall@1/3/5 分档,才能展示 v2-v3 的差异。这是 eval 集设计的关键:题集不具代表性,指标再精确也没用(见 27.8 失败边界)。

27.5.2 指标设计

  • Recall@1/3/5:检索 Top-K 是否命中相关文档。@1 最严格,最能体现优化;@5 范围宽,容易虚高。v2 的 44% vs 92% 差距正是在 @1 与 @5 之间——只有分档才能暴露"Top-1 精度崩塌"这个真问题
  • Keyword 命中率:答案是否含期望关键词。对无答案题(relevant_source 为空),不评 recall,只评答案是否含拒绝语(如"根据现有知识库无法回答")
  • 延迟:retrieve / generate 分开计,定位瓶颈在检索还是在 LLM
  • Token(in/out):成本指标,选型对比时要看性价比

27.5.3 eval 脚本实现要点

src/eval.py 的核心逻辑(135 行):

  • data/eval.jsonl(每题含 id/category/question/relevant_source/expected_keywords)
  • 每题跑 retrieve → generate,记录 Top-K sources、答案、延迟、Token
  • relevant_source 为空时跳过 recall 评估,只评 KW
  • 输出 results/eval_report.md:汇总指标表 + 逐题详情表 + 答案摘录

关键设计:relevant_source 为空 = 无答案题,recall 记为 (不参与计数),KW 评估答案是否含拒绝语。这样无答案题不会因为"检索没命中"被误判为失败——它们压根不该有命中。

27.6 两个踩坑

27.6.1 hf-mirror 镜像不兼容,改用官方源

现象:sentence-transformers 加载 bge-small-zh 时,用 HF_ENDPOINT=https://hf-mirror.com 镜像报 FileMetadataError,huggingface_hub 的元数据校验失败。

排查:用 curl 直测,hf-mirror 的响应 header 与 huggingface_hub 的校验逻辑不兼容(镜像改了 header 格式)。改用 huggingface.co 官方源直连,curl 验证可通。

解法:get_embedder() 里注释说明不使用镜像,直接 SentenceTransformer(config.EMBEDDING_MODEL),首次下载 ~95MB 走官方源。代价是首次加载稍慢,但稳定性优先。

教训:国内镜像不是万能的,尤其涉及 SDK 元数据校验时。遇到镜像报错,先用 curl 验证官方源是否可直连,能直连就别用镜像——少一个故障源。

27.6.2 mba/*.htm 导航噪音,改从 data.mjs 提取

现象:mba/ 目录是上海大学 MBA 中心网站还原,含大量 .htm 页面。直接用 BeautifulSoup 解析 htm,90% 内容是导航噪音(移动端菜单、重复链接、面包屑),不是 <nav> 标签所以常规过滤失效。

排查:翻 mba/tools/ 目录,发现 data.mjs 是网站的结构化数据源(JavaScript 模块,含所有栏目的结构化字段)。这是单一数据源,比解析渲染后的 htm 干净得多。

解法:写预处理脚本 scripts/prepare_data.mjs,从 data.mjs 提取结构化数据,输出 data/docs/*.md。htm 只作补充,不作主源。

教训:解析网页前先找有没有结构化数据源(data.mjs / data.json / API 接口)。直接解析渲染后的 HTML 是最后手段,噪音处理成本远高于从结构化源提取。这条经验对所有"网站语料入库"场景通用。

27.7 从 RAG 到 Agent

RAG 与 Agent 不是对立关系,而是递进关系。理解两者的边界,才知道什么时候该用 RAG、什么时候该上 Agent。

27.7.1 本质区别

维度RAGAgent
核心检索增强(retrieve → generate)工具调用 + 规划(Tool + Planning Loop)
决策单轮:query → context → answer多轮:Goal → Plan → Step → 验证 → 调整
工具1 个(retriever)N 个(读/写/执行/委派/...)
失败模式检索没命中 / LLM 幻觉工具调用错 / 规划跑偏 / 不停住
典型场景知识库问答多步骤任务(重构、调研、部署)

RAG 的本质是给 LLM 外挂一个知识库,让它回答时有据可依。Agent 的本质是给 LLM 外挂一组工具 + 一个规划循环,让它能自主完成多步骤任务。RAG 是 Agent 的子集——一个 Agent 可以把 retriever 作为它的一个工具,但 RAG 系统本身不具备规划能力。

27.7.2 从 RAG 升级到 Agent 的路径

本项目的 RAG 是单轮:用户问 → 检索 → 生成。如果要升级成 Agent,需要加三件事(对应第 24 章骨架):

  1. 工具系统:把 retriever 包装成一个 tool,再加几个(如计算器、数据库查询、API 调用),让 LLM 按需调用
  2. 规划循环:Goal → Plan → Step,让 LLM 自己拆解任务、分步执行、验证结果
  3. 安全红线:工具调用的权限控制(读操作自动允许,写操作需确认,破坏性操作永不自动)

举个具体场景:用户问"对比 GL 项目和 GC 项目的学费、师资、就业去向"。RAG 单轮的做法是检索 Top-K → 生成,但答案可能因为 chunk 切分不全而漏信息。Agent 的做法是:拆成三个子查询(GL 学费、GC 学费、师资对比)→ 各自检索 → 交叉验证 → 汇总。这就是规划循环的价值。

第 24 章给出了 Agent 产品的可复用骨架(三模块:工具系统 / 安全红线 / 规划循环),第 23 章拆解了 Claude Code / Codex / Cursor 三家架构哲学。本章的 RAG 项目是那条路径的起点——先把检索这一件事做对、做可评估,再往上加工具与规划。

27.8 失败边界

承认边界比夸大能力更重要。RAG 项目有三类典型失败:

27.8.1 embedding 选型错误导致召回崩塌

本项目 v2 已用数据证明:英文 embedding 在中文场景 Recall@1 暴跌 56pt。更隐蔽的失败是选了"看起来支持中文"但实际训练语料不足的模型。解法:任何 embedding 选型都必须跑 eval 对比,不能只看模型卡片宣称支持中文。bge-small-zh 是中文场景的稳妥默认,但也要有自己的 eval 数据背书。

27.8.2 eval 集不具代表性

v1 基线在 20 题简单题上 100%,看起来完美,但这个数据无法体现优化空间。如果止步于此,你会得出"系统已经最优"的错误结论。追加了 8 题难度题(无答案/干扰区分/跨文档)后,v2-v3 的差异才暴露。解法:eval 集必须覆盖难度梯度,简单题验证基础、难度题暴露边界。只跑简单题的 eval 比没有 eval 更危险——它给你虚假的信心。

27.8.3 把 RAG 当万能答案

RAG 能回答知识库里有的问题,不能回答知识库里没有的问题。本项目的无答案题(q21-23:学费具体金额、2026 招生人数、主任姓名)就是测这个边界——知识库无信息时,系统必须说"根据现有知识库无法回答",不能编。v3 移除 prompt 约束后,KW 降到 96.4%,说明约束移除后模型开始倾向编造。解法:SYSTEM_PROMPT 必须含"不知道就说不知道"的硬约束,且 eval 必须含无答案题测幻觉控制。

27.8.4 规模外推的风险

本项目知识库仅 37 chunks,v1 达 100%。这个数据不能外推到生产规模。千+ chunks 场景下,chunk 切分策略、rerank(如 BAAI/bge-reranker-base cross-encoder 精排)、混合检索(向量 + BM25)等优化才会显著。面试或复盘时必须诚实说明规模局限——在小规模上 100% 不代表生产可用,只代表在该规模下选型正确。

27.9 核心心法

本章所有内容可以浓缩成一句:

RAG 的工程化 = 选型决定天花板 + eval 决定能不能发现天花板。

选型(embedding / prompt / chunk 策略)决定系统能做到多好,eval 决定你能不能看清当前在哪、下一步改什么。没有 eval,所有选型都是赌博;有 eval,每一次改动都留下数据,RAG 就从"时灵时不灵"变成可度量、可迭代的工程系统。

用 Claude Code 开发 RAG 的工作流,本质是第 06 章可执行闭环的实例化:Plan Mode 建架构 → 分模块实现 → eval 验证 → 控制变量迭代。这套工作流不只适用于 RAG,任何"选型敏感 + 可量化评估"的 AI 应用都能复用——比如推荐系统的召回排序、分类模型的阈值调优。掌握 eval 闭环,你就掌握了 AI 应用工程化的底层节奏。

从 RAG 再往上一步,就是 Agent:给 LLM 装上工具系统、规划循环、安全红线。那是第 24 章骨架与第四部分 Agent 工程的领地。


上一章:红队对抗式审查 下一章:内容生产工作流