当前位置: 首页 > news >正文

LangChain-Chatchat 开发与应用(五) RAG核心链路深挖-检索到重排序到生成的技术细节

RAG 核心链路深挖:检索 → 重排序 → 生成的技术细节

标签:RAG | 向量检索 | Rerank | 相似度计算 | 流式输出 | Prompt 工程


一、从一个"玄学"问题开始

做 RAG 的同学应该都有过这种经历:

同样的文档、同样的问题,昨天回答得挺好,今天突然就不准了。

你啥也没改,但它就是"抽风"了。

这种"玄学"现象的背后,其实是有技术原因的。今天咱们就把 RAG 的核心链路拆开,看看每个环节到底在干什么,以及为什么会影响最终效果。


二、RAG 链路全景图

先把完整链路画清楚:

用户提问 ↓ [查询向量化] ──→ Query Embedding ↓ [向量检索] ──→ 粗排召回 Top-K(比如 100 条) ↓ [重排序] ──→ 精排选出 Top-N(比如 5 条)← 可选环节 ↓ [上下文拼接] ──→ 把选中的文档 + 问题拼成 Prompt ↓ [LLM 生成] ──→ 流式输出回答 ↓ [来源标注] ──→ 标注引用的文档

咱们逐个环节深挖。


三、环节 1:查询向量化

3.1 Embedding 的本质

Embedding 就是把文本变成向量(一堆数字)。

"退款政策是什么?" ↓ Embedding 模型 [0.023, -0.156, 0.891, ..., -0.034] ← 1024 维向量

关键特性:语义相近的文本,向量距离也近。

"退款政策是什么?" ≈ "怎么申请退货?" ≈ "钱怎么退回来?" ↓ ↓ ↓ 向量 A 向量 B 向量 C distance(A, B) 很小 distance(A, D) 很大 (D = "公司成立于 2020 年")

3.2 查询向量化的坑

坑 1:查询和文档的"语言风格"不一致

文档里写的是:“退货流程说明:用户需在收货后 7 天内提出申请…”

用户问的是:“我买了东西不想要了咋办?”

如果 Embedding 模型不够强,这两种表达可能匹配不上。

解决方案:

  • 用更强的 Embedding 模型(bge-large-zh > m3e-base)
  • 加 Rerank 模型做精排
  • 混合检索(向量 + 关键词)互补

坑 2:短查询的信息量不足

用户只问了一个词:“退款”

这个词的向量可能跟很多不相关的内容也"有点像"。

解决方案:

  • 查询扩展:把"退款"扩展成"退款政策、退款流程、退货退款"
  • HyDE(Hypothetical Document Embedding):让 LLM 先写一个假设的回答,再对这个回答做向量化
# HyDE 伪代码asyncdefhyde_retrieval(query):# 1. 让 LLM 写一个假设的回答hypothetical_answer=awaitllm.generate(f"请简要回答这个问题:{query}")# 2. 对假设回答做向量化(信息更丰富)query_embedding=embed(hypothetical_answer)# 3. 用假设回答的向量去检索docs=vector_store.search(query_embedding)returndocs

四、环节 2:向量检索

4.1 相似度计算

向量检索的核心是相似度计算,常用两种方法:

余弦相似度(Cosine Similarity)

importnumpyasnpdefcosine_similarity(a,b):"""计算两个向量的余弦相似度,范围 [-1, 1]"""returnnp.dot(a,b)/(np.linalg.norm(a)*np.linalg.norm(b))# 实际使用中,FAISS 等库已经优化好了

点积(Dot Product)

defdot_product(a,b):"""简单向量点积"""returnnp.dot(a,b)

区别:

  • 余弦相似度:只关心方向,不关心长度(适合语义匹配)
  • 点积:同时考虑方向和长度(适合需要区分重要性的场景)

4.2 近似最近邻(ANN)算法

当向量库里有几百万条数据时,暴力计算每条的相似度太慢了。需要用近似算法:

┌─────────────────────────────────────────┐ │ 暴力搜索 (Flat) │ │ - 精确,但 O(N) 复杂度 │ │ - 适合数据量 < 10万 │ ├─────────────────────────────────────────┤ │ IVF (Inverted File Index) │ │ - 把向量空间分成多个区域 │ │ - 先找最近的几个区域,再区域内搜索 │ │ - 速度快,精度略有损失 │ ├─────────────────────────────────────────┤ │ HNSW (Hierarchical Navigable Small World)│ │ - 构建多层图结构 │ │ - 贪心搜索,层层逼近 │ │ - 速度和精度的最佳平衡 │ └─────────────────────────────────────────┘

Chatchat 默认用 FAISS,推荐配置:

# 小数据量(< 10万条)index=faiss.IndexFlatIP(dimensions)# 暴力搜索,精确# 中数据量(10万 ~ 100万)index=faiss.IndexIVFFlat(quantizer,dimensions,nlist)# 大数据量(> 100万)或要求高速度index=faiss.IndexHNSWFlat(dimensions,M=32)

4.3 检索参数调优

kb_settings.yaml中:

# 向量检索返回数量VECTOR_SEARCH_TOP_K:10# 相似度阈值(低于这个分的不要)SCORE_THRESHOLD:0.5# 是否启用混合检索DEFAULT_SEARCH_TYPE:"mix"

参数调优建议:

场景TOP_KSCORE_THRESHOLD说明
精确问答50.6只要最相关的
开放式问答100.4多给点上下文
文档内容少30.5避免引入噪声
文档内容多150.5增加召回率

五、环节 3:重排序(Rerank)

5.1 为什么需要 Rerank?

向量检索有个问题:它只关心"语义相似",不关心"是否真正回答了问题"。

举个例子:

问题:"怎么申请退款?" 向量检索 Top 3: 1. "退款政策说明:用户可在收货后 7 天内申请退款..." ← 相关 ✓ 2. "退款金额将在 3-5 个工作日内原路返回..." ← 相关 ✓ 3. "公司成立于 2020 年,主营电子产品..." ← 不相关 ✗(但可能含"退款"字样)

Rerank 的作用就是:在召回的候选集里,重新判断哪些真正相关。

5.2 Rerank 的原理

Rerank 模型通常是一个交叉编码器(Cross-Encoder)

向量检索(双编码器): 查询 ──→ Embedding ──→ 向量 文档 ──→ Embedding ──→ 向量 相似度 = cosine(查询向量, 文档向量) 问题:查询和文档是独立编码的,没有交互 Rerank(交叉编码器): [查询 + 文档] ──→ 联合编码 ──→ 相关性分数 优势:查询和文档可以"互相看",判断更准确 代价:计算量大,只能对少量候选做精排

5.3 Chatchat 中启用 Rerank

# model_settings.yamlDEFAULT_RERANK_MODEL:bge-reranker-large# kb_settings.yaml# 向量检索召回数量(给 Rerank 的候选集)VECTOR_SEARCH_TOP_K:20# Rerank 后最终保留数量RERANK_TOP_K:5

流程变成:

用户提问 ↓ 向量检索召回 Top 20 ↓ Rerank 模型精排,选出 Top 5 ↓ 送给 LLM 生成回答

5.4 Rerank 的效果

实测数据(同一批测试集):

配置准确率延迟
无 Rerank72%500ms
+ Rerank (base)81%800ms
+ Rerank (large)86%1200ms

结论:Rerank 能显著提升准确率,但有延迟成本。对精度要求高的场景建议开启。


六、环节 4:上下文拼接

6.1 为什么需要拼接?

LLM 的输入是一个字符串,但咱们有检索到的文档 + 用户问题,需要拼成一个完整的 Prompt。

6.2 拼接策略

Chatchat 默认用“Stuff”策略——把所有文档直接塞进 Prompt:

【系统提示】 你是一个专业的客服助手,请基于参考资料回答问题。 【参考资料】 文档 1:退款政策说明:用户可在收货后 7 天内... 文档 2:退款金额将在 3-5 个工作日内... 文档 3:... 【用户问题】 怎么申请退款? 【要求】 1. 基于参考资料回答 2. 标注信息来源 3. 不要编造

Stuff 策略的问题:

  • 文档太多时,超出 LLM 的上下文长度
  • 无关文档会干扰 LLM 的判断

其他策略:

策略原理适用场景
Stuff全部塞进去文档少、上下文够
Map-Reduce每篇文档单独问,再汇总文档多、需要综合
Refine逐篇迭代优化答案需要高精度综合

Chatchat 默认用 Stuff,因为 RAG 场景下检索到的文档通常已经经过筛选,数量可控。

6.3 上下文长度管理

# 伪代码:上下文长度控制defbuild_prompt(docs,query,max_context_length=3000):""" 把文档拼进 Prompt,但不超过最大长度 """context=""used_docs=[]fordocindocs:# 预估加上这篇文档后的长度candidate=context+f"\n文档:{doc.page_content}\n"iflen(candidate)>max_context_length:break# 超长了,停止添加context=candidate used_docs.append(doc)prompt=f"基于以下资料回答问题:\n{context}\n\n问题:{query}"returnprompt,used_docs

七、环节 5:LLM 生成

7.1 生成参数详解

# model_settings.yamlLLM_MODEL_CONFIG:qwen2-instruct:model:qwen2-instructtemperature:0.7# 创造性 vs 确定性max_tokens:4096# 最大输出长度top_p:0.9# 核采样frequency_penalty:0# 重复惩罚presence_penalty:0# 新颖性惩罚

参数调优指南:

参数作用调大调小
temperature随机性更有创意更确定
top_p采样范围更多样更集中
max_tokens输出长度回答更长回答更短
frequency_penalty重复惩罚减少重复允许重复

RAG 场景推荐:

  • temperature: 0.3~0.5(RAG 需要确定性,不要"发挥")
  • max_tokens: 2048(根据回答长度调整)

7.2 流式输出(SSE)

Chatchat 支持流式输出,用户体验更好:

# 伪代码:SSE 流式输出fromfastapiimportStreamingResponseasyncdefstream_chat(request):asyncdefgenerate():# 调用 LLM 的流式接口asyncforchunkinllm.astream(prompt):yieldf"data:{json.dumps({'content':chunk})}\n\n"yield"data: [DONE]\n\n"returnStreamingResponse(generate(),media_type="text/event-stream")

SSE(Server-Sent Events)原理:

  • HTTP 长连接,服务器持续推送数据
  • 前端逐字显示,像打字机效果
  • 比 WebSocket 简单,单向通信足够

八、环节 6:来源标注

8.1 为什么需要来源标注?

RAG 的回答可能出错,让用户知道"这个回答是从哪来的",可以增加可信度,也便于人工复核。

8.2 Chatchat 的来源标注实现

# 伪代码:来源标注asyncdefknowledge_base_chat_with_source(request):# 1. 检索文档docs=retrieve_documents(request.query)# 2. 构建 Prompt(要求模型标注来源)prompt=f""" 基于以下资料回答问题,并在回答中标注信息来源(格式:[来源: 文档名])。 资料:{format_docs_with_source(docs)}问题:{request.query}"""# 3. 生成回答answer=awaitllm.generate(prompt)# 4. 返回(前端高亮显示来源)return{"answer":answer,"source_documents":[{"title":doc.metadata["source"],"content":doc.page_content}fordocindocs]}

九、完整链路的效果调优

9.1 调优优先级

按效果影响从大到小排序:

1. Embedding 模型质量(影响检索)⭐⭐⭐⭐⭐ 2. 文本分块策略(影响检索)⭐⭐⭐⭐⭐ 3. Rerank 模型(影响精排)⭐⭐⭐⭐ 4. Prompt 模板(影响生成)⭐⭐⭐⭐ 5. LLM 质量(影响生成)⭐⭐⭐ 6. 检索参数 TOP_K(影响召回)⭐⭐⭐ 7. 生成参数 temperature(影响风格)⭐⭐

9.2 诊断流程

回答不好时,按这个顺序排查:

Step 1: 检查检索结果 └─ 打印 retrieved_docs,看是否包含正确答案 └─ 如果不包含 → 调分块、换 Embedding、加 Rerank Step 2: 检查 Prompt └─ 打印最终 Prompt,看上下文是否完整 └─ 如果上下文不对 → 调分块、调 TOP_K Step 3: 检查 LLM 输出 └─ 看 LLM 是否"胡编" └─ 如果胡编 → 加强 Prompt 约束、降低 temperature

十、小结

这篇咱们把 RAG 的核心链路彻底拆开了:

✅ 查询向量化:Embedding 原理、HyDE 查询扩展
✅ 向量检索:相似度计算、ANN 算法、参数调优
✅ 重排序:Cross-Encoder 原理、效果与成本权衡
✅ 上下文拼接:Stuff/Map-Reduce/Refine 策略
✅ LLM 生成:参数详解、流式输出 SSE
✅ 来源标注:实现原理和用户体验
✅ 效果调优:优先级排序和诊断流程

核心认知:

RAG 不是"检索 + 生成"的简单拼接,而是一个系统工程

检索质量决定了"上限",LLM 质量决定了"下限"。

优化要抓重点:先搞定 Embedding 和分块,再考虑其他。



你在调优 RAG 效果时,哪个环节给你带来的提升最大?Embedding、Rerank 还是 Prompt?欢迎分享经验!

http://www.jsqmd.com/news/849559/

相关文章:

  • 2026年口碑好的温室大棚配件/温室大棚/云南玻璃温室大棚横向对比厂家推荐 - 品牌宣传支持者
  • 从Lambda变量捕获机制,深入理解Java的final与effectively final设计哲学
  • 用NE555和运放搭个“乐高”:从1kHz方波到奇次谐波合成的完整电路实验
  • 第11篇 安全配置实战:SASL_SSL + SCRAM-SHA-512
  • 别再死记硬背了!用Python+PyTorch实战理解脑电10-20系统与蒙太奇(附代码)
  • 【Perplexity文献管理终极指南】:20年科研老炮亲授AI时代参考文献零误差管理法
  • 工业级RK3399K核心板深度解析:宽温设计、AI加速与嵌入式开发实战
  • STM32F103 ADC多通道采样,用DMA搬运数据到底有多省心?一个数组搞定所有
  • 第三章 WXML 表单组件全览与实战
  • 【AI语音实战】从VAD到声纹:构建智能对话系统的核心技术栈
  • Grounding DINO实战评测:对比GLIP、OV-DETR,在COCO和LVIS数据集上到底强在哪?
  • 告别文献混乱!用Zotero+OneDrive打造你的跨设备论文库(附ZotFile插件配置)
  • LangChain-Chatchat 开发与应用(六) Agent能力揭秘-让大模型不仅能聊天还能干活
  • 别再手动点点点了!用Inspect.exe + Python搞定Windows桌面自动化测试
  • 别再死记硬背了!用5个真实业务场景拆解SAP EWM的‘上架策略’与‘仓位确定逻辑’
  • PFC2D5.0_从零构建边坡开挖与稳定性分析模型
  • 雀巢冰淇淋在华投资的首家冰淇淋工厂迎来成立40周年 | 美通社头条
  • 告别FTP!用Go写的Filebrowser,一个命令搞定Windows/Linux跨平台文件管理
  • Cinemachine - Unity相机进阶:从基础到实战的镜头艺术
  • 数据科学工具链实战指南:从核心工具到架构选型
  • 2026年耐磨性好的品牌注塑机配件订制/注塑机配件订制/耐高温注塑机配件订制优质供应商推荐 - 品牌宣传支持者
  • 女神异闻录5:皇家版2026最新官方破解版加修改器免费下载 一键转存 永久更新 (看到速转存 资源随时走丢)
  • 从DOCK 6.11新特性到实战:RDKit集成与描述符驱动的药物设计
  • Oracle数据清洗实战:TRIM函数与LTRIM/RTRIM的深度对比与应用
  • 中兴B862AV3.2M盒子救砖记:免拆机、免ADB,一根双公头USB线搞定刷机
  • 别再傻傻分不清了!用大白话讲明白BLE开发里的GATT和GAP到底啥关系
  • 惠州三岛新材料一站式密封胶解决方案!耐高温密封胶、导热硅胶、玻璃胶、导热垫片、环氧AB胶、平面密封胶生产厂家甄选 - 栗子测评
  • 麦当劳中国启动2026全国招聘周招募新生代人才
  • 使用curl命令直接测试taotoken聊天补全接口的详细指南
  • Honeywell 51304650-100终端组件