一句话总结:RAG = 先从你的“资料库”里把相关资料找出来(Retrieval),再要求大模型只基于这些资料回答(Generation),并把引用来源一起返回,从而减少胡编乱造。
1. RAG 在解决什么问题
大模型(LLM)很强,但有一个致命问题:它擅长“生成看起来合理的文本”,却不保证内容真的来自真实资料。
你问它一个它不确定的问题,它也可能:
- 讲得很像真的(语言非常顺)
- 但实际上是在“编”(俗称幻觉)
RAG 的核心思路是给大模型“加一个外部记忆”:
- 你把资料存进一个可检索的知识库(例如 FastAPI 文档)
- 用户提问时,系统先去知识库找相关资料
- 再把这些资料作为上下文交给 LLM,让它只基于资料回答
- 同时把资料来源(引用)返回,便于核对
2. RAG 的四个阶段
把 RAG 想象成“开卷考试”:
- 入库(Ingest):把书分章节、做索引、放到书架上
- 文档读取/抓取
- 切分为小块(chunk)
- 生成每个 chunk 的 embedding 向量
- 写入向量库(Chroma)与元信息表(Postgres)
- 检索(Retrieve):根据题目去翻书,找最相关的几页
- 对用户问题也做 embedding
- 在向量库做相似度检索(vector search)
- 同时做关键词检索(BM25)
- 合并为混合检索(Hybrid Search)
- 重排(Rerank):把找来的候选资料再精挑细选,筛出最有用的几段
- 先拿一批候选(candidate)
- 用更精细的规则/模型重新排序
- 选出最终 top_k 段上下文
- 生成(Generate):把“开卷资料”交给 LLM,让它写答案
- 拼上下文 context
- 让 LLM 基于 context 作答
- 返回 answer + citations(引用片段/来源)
2.1 架构图:从文档到答案
下面这张图就是 DevAssist 跑通的 RAG 版本(偏工程化的“最小闭环”)。
你可以把它理解成两条“输入输出”:
- 文档输入:文档 → chunk → embedding → Chroma(可检索)
- 问题输入:问题 → 检索出 chunk → 拼上下文 → LLM 输出答案(附引用)
3. 专业词汇解释,要不然根本不懂
下面这些词看懂了,RAG 就不神秘了:
- RAG(Retrieval-Augmented Generation):检索增强生成;先检索资料,再生成答案。
- 知识库/文档库:你自己存的资料集合,比如 FastAPI 文档、团队 Wiki。
- Chunk(文本块):把长文切成很多小段,每段是一条可检索单位。
- Chunk Size:每段 chunk 的目标长度(例如 512 或 800)。
- Overlap(重叠):相邻 chunk 之间重复一小段,避免关键句刚好被切断。
- 语义切分(Semantic Chunking):尽量按段落/句子/代码块边界切,而不是硬切长度。
- Embedding(向量):把文本转为数字向量;语义相近的文本向量更接近。
- 向量数据库(Vector DB):存“向量 + 原文 + 元数据”的数据库,用来按语义相似度检索。这里用的是 Chroma。
- Vector Search(向量检索):根据向量距离找相似 chunk。
- BM25:传统关键词检索算法,对专有名词、API 名称、代码符号很敏感。
- Hybrid Search(混合检索):向量检索 + BM25 合并,兼顾语义与关键词。
- Rerank(重排):第二次排序;让“更相关”的 chunk 排更前。
- Top-K:取前 K 个;例如 top_k=5 表示最终给 LLM 5 段上下文。
- Candidate Multiplier(候选扩展倍数):先检索更多候选(top_k * multiplier),再 rerank 回 top_k。
- Citation(引用):答案里附带资料来源与片段,便于核对与追溯。
- SSE(Server-Sent Events):服务端流式推送输出给前端的方式,用于实时显示生成过程。
4. DevAssist 的 RAG 架构拆解
这一阶段(RAG 全链路)对应项目的 backend/app/rag/ 模块,核心文件如下:
4.1 切分器(Chunking)
split_text:定长切分split_text_semantic:语义切分(保护 Markdown fenced code block)
文件:
backend/app/rag/splitter.py
代码(核心实现,直接摘自项目源码;建议配合上面的流程图一起看):
| |
4.2 向量化(Embedding)
职责:把文本列表变成向量列表(支持分批请求)。
文件:
backend/app/rag/embedder.py
代码(embed_texts 的关键实现:分批请求 + 对齐顺序 + 统一打日志):
| |
4.3 向量库(Chroma)
职责:统一 collection 的获取/创建。
文件:
backend/app/rag/chroma.py
代码(collection 管理器:连接 + 统一入口):
| |
4.4 入库(Ingestion)
职责:chunk → embed → store(写入 Chroma)→ persist meta(写入 Postgres documents 表)。
文件:
backend/app/rag/ingestion.py- API:
backend/app/api/ingest.py - 脚本:
backend/scripts/ingest_docs.py(批量 ingest 本地 docs 目录)backend/scripts/ingest_fastapi_docs.py(从 GitHub 拉 FastAPI 官方文档)
代码(入库业务核心:chunk → embed → store → persist meta):
| |
4.5 检索(Hybrid Retrieval)
职责:向量检索 + BM25 检索合并。
文件:
backend/app/rag/retriever.pybackend/app/rag/bm25.py- API:
backend/app/api/search.py
代码(混合检索的核心:vector + BM25 合并去重 + 排序):
| |
4.6 重排(Rerank)
职责:对候选 chunks 二次排序。当前实现是“关键词重叠打分”的轻量版本,支持最小阈值与空结果兜底。
文件:
backend/app/rag/reranker.py
代码(完整实现:打分规则 + 阈值过滤 + 空结果兜底):
| |
4.7 生成(Generate)
职责:retrieve → rerank → build context → 调 LLM → 返回 answer + citations。
文件:
backend/app/rag/generator.py
代码(生成主链路:retrieve → rerank → build context → 调 LLM → 返回 citations):
| |
5. 手把手跑通(命令即可复现)
下面这组命令是“从 0 到跑通 RAG”的最短路径。
5.1 启动服务
| |
5.2 Ingest:把 FastAPI 文档写入知识库
先小规模跑通(控制成本):
| |
验证写入量(count > 0 即可):
| |
5.3 Eval(离线,不调用 LLM):只测“检索有没有把资料找回来”
| |
说明:
--no-llm:不生成答案,直接用数据集里的reference_answer当 answer(但仍会真实检索 contexts),适合调检索参数。--embedding-metrics:输出 embedding 版本指标,跨语言更稳。
6. 如何评测:到底在测什么
评测模块的设计目标是:不依赖复杂的裁判模型,也能快速对比“检索/重排/切分参数”是否有改进。
6.1 三个启发式指标(直觉解释)
Faithfulness(忠实度)
回答里的关键 token 是否能在 contexts 中找到。
直觉:回答有没有依据。Answer Relevance(相关性)
问题的关键 token 是否在回答中出现。
直觉:有没有答到点上。Context Recall(上下文召回)
参考答案的关键 token 是否被 contexts 覆盖。
直觉:检索是否把关键资料找回来了。
对应代码:
backend/app/rag/evaluator.py
6.2 为什么要加 embedding 指标
如果你的“问题/答案”是中文,但知识库文档是英文:
- token 重叠会天然偏低(语言不一致)
- embedding 相似度能更好反映“语义接近”
因此评测脚本在开启 --embedding-metrics 后,会额外输出:
- emb_relevance = cosine(question, answer)
- emb_faithfulness = cosine(answer, contexts_joined)
- emb_context_recall = cosine(reference_answer, contexts_joined)
6.3 离线评测结果
说明:
- 这是离线评测(
--no-llm),不调用 LLM 生成答案,主要衡量“检索 + 重排 + context 拼接”是否把关键资料找回来了。 - 数据集是中文问答,知识库是英文文档,因此优先参考 embedding 版本指标(跨语言更稳定)。
结果摘要(avg,embedding 版本):
| 配置 | 知识库集合(collection) | Top-K | 候选扩展倍数(candidate_multiplier) | 重排阈值(rerank_min_score) | 平均 embedding 相关性(emb_relevance) | 平均 embedding 忠实度(emb_faithfulness) | 平均 embedding 召回率(emb_context_recall) |
|---|---|---|---|---|---|---|---|
| 基线(512) | fastapi_docs | 5 | 4 | 0.0 | 0.627 | 0.542 | 0.542 |
| 调参(512) | fastapi_docs | 5 | 6 | 0.0 | 0.627 | 0.543 | 0.543 |
| chunk 对比(800) | fastapi_docs_cs800 | 5 | 4 | 0.0 | 0.627 | 0.546 | 0.546 |
原始报告与结果文件位置:
- 报告:
data/eval_reports/fastapi_rag_report.md - 输出:
data/eval_reports/*.json
7. 评测数据集长什么样
评测数据集采用 JSONL(每行一个 JSON),示例结构:
| |
字段含义:
question:要评测的问题reference_answer:参考答案(用于计算 context recall;在--no-llm模式下可作为 answer)collection_name:默认去哪个 Chroma collection 检索(例如 fastapi_docs)
8. 怎么判断“做得好不好”
先给一个很实用的判断方式:
8.1 先判定“流程是否跑通”
满足以下任意两点就说明闭环打通了:
- Chroma collection 的
count > 0(说明入库成功) - eval_runner 能跑完并输出平均分,同时
output文件里有每条样本的 contexts -(若启用生成)能从generate_answer()拿到 answer + citations
8.2 再判定“效果是否足够好”
你需要先明确“你要优化哪一段”:
- 你想优化 检索召回:优先看
context_recall/emb_context_recall - 你想优化 生成质量:需要启用真实生成(不要
--no-llm),再引入 LLM-as-judge 或人工抽检
注意:如果是“中文问答 + 英文文档”,token 类指标会偏低,不适合作为唯一标准,更建议参考 embedding 指标。
9. 下一步:如何把它从“能用”变成“好用”
按收益从高到低排序:
全量 ingest 文档
小规模 ingest 只能证明流程,无法代表真实效果上限。更强的 reranker
当前是轻量关键词重叠;升级为 cross-encoder/LLM rerank 通常能明显提升“Top-K 的质量”。评测升级为“生成评测”
现在的--no-llm更像“检索评测”;要衡量最终用户体验,需要把生成纳入评测。同语种评测集
中英混合会让 token 指标失真;准备英文问答或把问题翻译成英文,会更容易定位问题出在哪一环。
