RAG 怎么做:架构与落地
你大概率是被一个具体场景逼到这篇文章的:客服知识库接了大模型,结果它一本正经地编造了一条根本不存在的退货政策;或者产品手册更新了,模型答案还停留在三个月前的旧版本。这两个坑背后是同一句话:模型的知识永远停在训练截止那一刻,而你的业务文档天天在变。RAG(检索增强生成)干的事很朴素——查询到来时先去你自己的文档库里翻出相关片段,塞进 prompt,再让模型”看着资料”回答,而不是凭记忆瞎编。这是目前企业把大模型接到自己业务上,投入产出比最高的一条路,没有之一。
但”RAG 很简单,不就是查一下塞进去吗”这句话害了不少团队——demo 阶段效果惊艳,一上生产环境准确率就跳水到 60% 以下。问题几乎都出在下面这条流水线的某个环节没抠细节,切块切错了、embedding 选错了、没做重排、上下文拼装顺序不对。这篇文章按顺序把每个环节的关键决策点、真实踩坑和排查方法过一遍,跟着抠一遍,比看十篇”RAG 是什么”的科普文有用。
RAG 完整流程
原始文档
│
▼ 1. 文档预处理
切块(Chunking)
│
▼ 2. 向量化
Embedding 模型 → 向量
│
▼ 3. 存储
向量数据库(Chroma / Milvus / Weaviate / pgvector)
│
─────────────────(以上为离线索引阶段)
│
▼ 4. 在线检索(用户查询时)
用户问题 → Embedding → ANN 检索 → Top-K 候选
│
▼ 5. 重排(可选但强烈推荐)
Cross-Encoder Reranker → 精排 Top-3
│
▼ 6. 拼上下文
System Prompt + 检索文档片段 + 用户问题
│
▼ 7. LLM 生成
最终回答(可附来源引用)
关键决策点对比
切块策略
| 策略 | 原理 | 适用 | 注意 |
|---|---|---|---|
| 固定大小(Fixed-size) | 按 token 数切,设重叠窗口 | 通用文本 | 可能割断语义 |
| 递归字符分割 | 按段落→句子→词递归分割 | 结构化文档 | LangChain 默认方案 |
| 语义切块(Semantic) | embedding 相似度变化点切割 | 长叙述文本 | 计算成本更高 |
| 小到大(Small-to-Big) | 小块检索,大块注入上下文 | 精度与上下文平衡 | 需双层索引 |
推荐起点:chunk_size=512 token,overlap=50 token,递归分割。
切块这一步看着最简单,踩坑却最多,说三个我实际遇到过的:
- 中文按字符数切等于按 token 数切,是两回事。英文 1 个 token 约等于 4 个字符,中文一个汉字常常就是 1~2 个 token,用英文经验设的 chunk_size 拿来切中文文档,实际 token 数会比预期多出 50% 以上,超窗口的概率也跟着涨。切中文文档前,先用你要用的模型的 tokenizer 实际测一下”512 token 对应多少个汉字”,别直接套英文教程的数字。
- 表格和代码块最怕被递归分割”腰斩”。LangChain 默认的递归分割器按段落/句子切,遇到一张跨越多个”句子”的 Markdown 表格,很可能从表格中间断开,检索回来的片段变成半张表,模型看着云里雾里。对含大量表格、代码的文档,建议先用正则把表格/代码块整体摘出来当作独立的不可分割单元,跟周围文本分开处理。
- overlap 不是越大越好。overlap 设到 200(chunk_size 的近 40%)看似能减少语义割裂,但代价是同一段内容在索引里重复出现好几次,检索 Top-K 里塞满了高度重复的片段,反而挤掉了本该出现的其他相关内容。50~100 token 的 overlap 通常够用,如果还是频繁割裂语义,先考虑换语义切块,而不是无限调大 overlap。
自查方法:切完块之后,随手打开 10~20 个 chunk 读一遍,能不能看懂”这段话在说什么”——看不懂就是切坏了,别急着往下一步走。
Embedding 模型选型
| 模型 | 维度 | 语言 | 特点 |
|---|---|---|---|
| text-embedding-3-small | 1536 | 多语言 | OpenAI 官方,性价比高 |
| text-embedding-3-large | 3072 | 多语言 | 精度更高,成本翻倍 |
| BAAI/bge-m3 | 1024 | 中英双语强 | 开源,本地部署首选 |
| mxbai-embed-large | 1024 | 英文强 | Ollama 可直接拉取 |
中文场景推荐 bge-m3 或 text-embedding-3-small,二选一先测 Hit Rate。
怎么测:拿你自己的 50 条真实问答对,分别用两个模型建索引、检索 Top-5,人工核对”正确答案所在的文档是否在 Top-5 里”,算出 Hit Rate@5 再比较,不要只看模型介绍页的跑分——那些跑分大多在英文通用语料上测的,跟你的垂直领域文档表现经常对不上号。
选型上还有两个容易被忽略的点:
- 维度不是越高越准。3072 维的 text-embedding-3-large 比 1536 维的 small 版本存储和检索开销直接翻倍(向量数据库的内存占用、ANN 索引构建时间都跟维度线性相关),而实际语义精度提升在很多场景只有几个百分点。先用 small 版本把流程跑通,Hit Rate 不达标再考虑升级到 large,别一上来就上最贵的。
- 换 embedding 模型必须重建索引,这条经常被新手忽略——因为不同模型的向量空间完全不兼容,同一个 chunk 用 A 模型和 B 模型编码出来的向量不能放在同一个索引里混着检索,也不能直接替换模型而不重新跑一遍全量 embedding。如果你的文档库有几十万条,重建一次的时间和 API 调用成本(按 token 计费)都得提前算进选型决策里,别等上线了才发现换模型意味着重新跑一遍全量索引。
向量数据库选型
| 数据库 | 部署 | 规模 | 特点 |
|---|---|---|---|
| Chroma | 本地/Docker | <100万向量 | 开发调试首选,零配置 |
| pgvector | Postgres 插件 | 中等规模 | 已有 PG 直接用 |
| Milvus | 自建/云 | 亿级 | 生产大规模首选 |
| Weaviate | 自建/云 | 中大规模 | 自带 GraphQL,多租户强 |
选型别只看”能不能用”,要看你团队现有的技术栈。已经有 Postgres 在跑的团队,直接装 pgvector 插件就能建向量索引,运维成本几乎为零;反过来如果为了尝鲜专门起一套 Milvus 集群,光是学习 collection、partition、索引类型(HNSW/IVF)这些概念就要花不少功夫,中小规模场景性价比并不高。判断标准很简单:
- 数据量 <100 万向量、还在验证阶段 → Chroma,本地跑起来一行代码的事。
- 已有 Postgres、数据量中等(几十万到几百万) → pgvector,不引入新组件。
- 数据量上亿、需要水平扩展、有专职运维 → Milvus。
- 需要多租户隔离、想要 GraphQL 查询接口 → Weaviate。
从 Chroma 迁移到 Milvus 时最容易忘的一件事:Chroma 默认用余弦相似度(cosine),Milvus 建索引时要显式指定 metric_type,如果建索引时选成了 L2 距离,同一批向量算出来的相似度排序会完全不一样,检索结果”看起来能跑但排序不对”,这个坑排查起来很费时间,因为程序不会报错,只是结果悄悄变差。
检索策略
稠密检索(Dense Retrieval):向量相似度,召回语义相近内容,对近义词友好。
稀疏检索(BM25):传统关键词匹配,对专有名词、产品型号等精确词召回好。
混合检索(Hybrid)= 稠密 + 稀疏,两路结果 RRF(倒数排名融合)合并,是目前生产最佳实践。
# 伪代码:RRF 融合
def rrf_merge(dense_results, sparse_results, k=60):
scores = {}
for rank, doc in enumerate(dense_results):
scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank + 1)
for rank, doc in enumerate(sparse_results):
scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
这段 RRF 融合代码的核心思路是”不看绝对分数、只看排名”——稠密检索和稀疏检索给出的相似度分数完全不在一个量纲上(向量余弦相似度在 0~1 之间,BM25 分数可能是几十上百),直接加权求和没有意义,RRF 用 1 / (k + rank) 把两路结果都换算成”排名倒数”再相加,天然规避了量纲不统一的问题。公式里的 k=60 是一个经验平滑常数,作用是压低排名靠后的文档对总分的影响权重(rank 越大,1/(k+rank) 越接近 0,但不会突变),k 取 60 是业界常用的默认值,一般不需要自己调,除非你的两路召回结果长度差异特别大再考虑调整。
实操中混合检索最常见的翻车点:稀疏检索(BM25)如果没有针对中文做分词预处理,直接按字符切分去匹配关键词,专有名词、产品型号这些本该被精确命中的词反而召回率很差——因为”iPhone15Pro”这种连写词被切成了单字符,跟查询词的字符级重合度很低。给 BM25 加一层基于业务词典的分词(比如把产品型号、专有名词加入自定义词典),召回率能有明显提升,这一步经常被跳过,导致混合检索里稀疏那一路形同虚设。
重排(Reranking)
向量检索召回精度有上限,重排是精度提升的”第二道过滤”:
- Cross-Encoder:把问题和每个候选文档拼在一起打分,精度最高,但逐一打分延迟较高。
- API 重排(如 Cohere Rerank、BGE-Reranker):一次调用,返回重排后列表。
经验:检索 Top-20,重排取 Top-3 注入上下文,比直接检索 Top-3 准确率高 15%~30%。
为什么会有这么大的提升:向量检索本质是”粗筛”,它衡量的是整段文档 embedding 和查询 embedding 的整体相似度,一段文档哪怕只有一句话真正切题、其余大段无关内容,整体相似度依然可能排到前面;而 Cross-Encoder 重排是把”查询+候选文档”整体拼在一起喂给模型重新打分,模型能看到两者的逐词交互,对”是否真正回答了这个问题”的判断精度远高于向量距离这种粗粒度的相似度度量。代价是 Cross-Encoder 没法像向量检索一样预先建索引批量算好,只能查询时对每个候选文档现算一次,候选数量一多延迟就上去了——这也是为什么重排前必须先用向量检索粗筛出一个不大的候选集(Top-20 左右),而不是直接对全库文档做 Cross-Encoder 打分。
用 API 重排(Cohere Rerank / BGE-Reranker 云端接口)时最容易踩的两个坑:
- 429 限流:批量重排请求打太快,接口返回 429 Too Many Requests,多数 SDK 默认不会自动重试,直接抛异常导致整条 RAG 链路失败。生产环境务必加指数退避重试(比如失败后等 1s、2s、4s 依次重试,最多 3 次),而不是让一次限流拖垮整个用户请求。
- 候选文档总长度超过重排接口单次限制:Top-20 候选如果单条很长(比如没有严格控制在 512 token 内),拼一起送重排接口容易超出接口的单次输入长度上限,报错信息通常是类似 “input too long” 的提示。解决办法很直接:重排前把候选文档统一截断到接口允许的长度,标题+正文摘要通常比全文更有效。
本地部署 Cross-Encoder(比如用 sentence-transformers 加载 bge-reranker-large)能省掉这层网络调用和限流问题,但要有 GPU 支撑,CPU 上跑 Top-20 重排的延迟可能到秒级,是否自建取决于你的 QPS 和延迟预算。
上下文拼装
拼装顺序影响模型对信息的权重,建议:
[System Prompt]
你是一名企业知识库助理,根据以下参考资料回答用户问题。
若资料中无相关信息,直接说"资料中未提及",不要编造。
[参考资料]
---来源:{doc_source}---
{chunk_1}
---
{chunk_2}
---
[用户问题]
{user_question}
重要原则:
- 明确告诉模型”不知道就说不知道”,减少幻觉。
- 注入来源(文件名/URL),方便用户核查,也让模型引用来源更自然。
- 控制上下文总长度在模型窗口 60%~70% 以内,留空间给推理。
评测:RAG 专属指标
| 指标 | 含义 | 工具 |
|---|---|---|
| Hit Rate@K | 正确文档在 Top-K 中的比例 | RAGAS |
| MRR | 平均倒数排名,衡量正确文档排名 | RAGAS |
| Context Precision | 检索结果中相关文档占比 | RAGAS |
| Answer Faithfulness | 回答是否忠于检索内容 | RAGAS / 人工 |
| Answer Relevance | 回答是否解决了用户问题 | 人工抽查 |
建议先用 50~100 条人工标注的问答对跑基准,再迭代切块和检索策略。
常见问题
切块越小越好吗? 不是。太小(<100 token)会失去句间语义联系,导致检索结果碎片化;太大(>1000 token)会引入噪声,稀释相关信息。建议 300~600 token 之间,根据文档类型调整。
向量检索为什么找不到我明明写了的内容? 通常是 embedding 语义距离问题:问法和文档描述差异大,或专有名词没有被向量空间正确编码。解决方案:加 BM25 混合检索、对文档做同义词增强、或对问题做查询扩展(Query Expansion)。
如何处理多文档互相矛盾的情况? 在 system prompt 中明确优先级规则(如”最新文档优先”);或在重排时加入时间权重;对高冲突场景建议让模型列出两种说法并标注来源,由用户判断。
← 返回 应用模式总览:从 Prompt 到 Agent | 应用模式专题
相关阅读:AI Agent 开发实战 · 提示工程实用技巧
多模型接入遇到 embedding 供应商不稳定?力达云聚合 API 支持统一切换 embedding 提供商,无需改代码。