☰
15|RAG 优化:为什么检索到了,回答仍然不对? - eino教程
2026/10/20 11:57:49 网站建设 项目流程

15|RAG 优化:为什么检索到了,回答仍然不对?

《Eino 实战:用 Go 构建 AI 应用与智能体》· 第 15 篇
关键词:召回质量、MultiQuery、检索路由、重排序、评测
前置条件:已完成 13|知识入库 与 14|RAG 入门。

第 14 篇的流程跑通了,但上线后常见的情况是:检索确实返回了资料,回答却不对。

这可能发生在四个不同的环节。如果不先定位是哪一环,调参就只是在碰运气。这一篇按顺序给出排查路径,并介绍 Eino 提供的优化手段。


一、先定位:问题出在哪一环

RAG 有四个环节,每个环节都有各自的失效方式:

用户问题 ↓ ① 查询理解:问题表达与资料措辞不一致 检索 ↓ ② 召回质量:该被召回的没召回,或召回了无关的 重排与筛选 ↓ ③ 上下文组装:关键信息被淹没、被截断、被冲突内容干扰 生成 ↓ ④ 生成与引用:模型误读资料,或引用不支持结论 回答

排查顺序建议从后往前,或者从最容易验证的环节开始:

  1. 打印最终发给模型的完整消息(第 14 篇示例里的做法)。这一步能立刻区分是“资料没进去”还是“资料进去了但模型没用好”。
  2. 打印召回结果和分数。看该有的资料是否在里面。
  3. 打印切分后的片段。看关键信息是否被切碎或丢失。

下面这段程序模拟了“检索命中但召回质量不稳定”的情况——这正是最常见的一种(实机运行):

packagemainimport("context""fmt""log""sort""strings""github.com/cloudwego/eino/schema")// chunk 表示一个已入库的知识片段。typechunkstruct{IDstringContentstringSourcestring}varcorpus=[]chunk{{ID:"printer#0",Content:"打印机无法联网时,先检查网线是否插紧,再确认网段是否允许办公设备接入。",Source:"handbook/printer.md"},{ID:"vpn#0",Content:"远程访问需要先申请 VPN 账号,审批通过后由网络组开通。",Source:"handbook/vpn.md"},{ID:"ticket#0",Content:"工单超过 24 小时未分派,可申请催办。",Source:"handbook/ticket.md"},}// bigrams 生成字符二元组,用于中文的粗略相似度计算。funcbigrams(sstring)map[string]struct{}{r:=[]rune(s)set:=make(map[string]struct{})fori:=0;i+1<len(r);i++{set[string(r[i:i+2])]=struct{}{}}returnset}// retrieve 用 bigram 重叠度做模拟检索,便于在无向量库时观察召回质量。funcretrieve(querystring,kint)[]chunk{q:=bigrams(query)typescoredstruct{c chunk scorefloat64}varall[]scoredfor_,c:=rangecorpus{d:=bigrams(c.Content)iflen(d)==0{continue}hit:=0forg:=rangeq{if_,ok:=d[g];ok{hit++}}all=append(all,scored{c:c,score:float64(hit)/float64(len(d))})}sort.Slice(all,func(i,jint)bool{returnall[i].score>all[j].score})varhits[]chunkfor_,s:=rangeall{ifs.score>0&&len(hits)<k{hits=append(hits,s.c)}}returnhits}funcmain(){iferr:=run();err!=nil{log.Fatal(err)}}funcrun()error{_=context.Background()query:="打印机离线怎么处理"hits:=retrieve(query,3)fmt.Printf("查询: %q 命中 %d 条\n",query,len(hits))for_,h:=rangehits{fmt.Printf(" 来源=%s 内容=%s\n",h.Source,h.Content)}// 组装上下文:带上来源编号,便于回答引用。varb strings.Builderfori,h:=rangehits{fmt.Fprintf(&b,"[%d] 来源:%s\n%s\n",i+1,h.Source,h.Content)}systemPrompt:="你是工单助手。只依据参考资料回答,结论后标注来源编号;资料不足时明确说明。"messages:=[]*schema.Message{schema.SystemMessage(systemPrompt),schema.UserMessage("参考资料:\n"+b.String()+"\n问题:"+query),}fmt.Println("--- 最终发送给模型的用户消息 ---")fmt.Println(messages[len(messages)-1].Content)// 反例:无相关资料时必须显式处理,否则模型容易自由发挥。empty:=retrieve("报销流程是什么",3)fmt.Printf("\n无相关资料时命中数: %d\n",len(empty))iflen(empty)==0{fmt.Println("正确做法:明确说明没有依据,并转人工或补充信息,而不是让模型猜测。")}returnnil}

实机输出:

查询: "打印机离线怎么处理" 命中 1 条 来源=handbook/printer.md 内容=打印机无法联网时,先检查网线是否插紧,再确认网段是否允许办公设备接入。 --- 最终发送给模型的用户消息 --- 参考资料: [1] 来源:handbook/printer.md 打印机无法联网时,先检查网线是否插紧,再确认网段是否允许办公设备接入。 问题:打印机离线怎么处理 无相关资料时命中数: 0 正确做法:明确说明没有依据,并转人工或补充信息,而不是让模型猜测。

这里能直接看出一个典型问题:用户说的是“离线”,资料里写的是“无法联网”。在简单相似度算法下,这种同义不同词的差距会显著拉低召回分数。真实向量检索对这类问题更健壮,但依然会受影响——这是查询理解要解决的问题。


二、① 查询理解:用户的说法和资料的写法不一样

用户不会用资料的措辞提问。常见的差距:

用户说法资料写法
打印机离线打印机无法联网
网断了网络连接异常
怎么催申请催办流程

改善手段:

多查询检索(MultiQuery):用模型把一个问题改写成多个表达,分别检索后合并结果。这样能覆盖更多措辞。

// 概念示意:把原始问题扩展成多个查询变体。// 实现方式可能是:// - 调用模型生成改写(推荐,可指定数量的变体)// - 或用同义词规则扩展queries:=[]string{"打印机离线怎么处理","打印机无法联网 排查步骤","办公设备 网络连接失败 处理",}for_,q:=rangequeries{docs,err:=r.Retrieve(ctx,q,retriever.WithTopK(3))// 合并、去重}

Eino 的示例仓库里有MultiQueryRetriever的实现(基于模型生成查询变体),可以直接参考其结构。

检索路由:不同类型的问题走不同的检索源。比如“工单进度”查业务数据库,“操作规范”查文档库。路由错误会导致在一个错误的语料里搜索。


三、② 召回质量:该来的没来,不该来的来了

症状可能原因排查
该有的资料没被召回切分不当、Embedding 不匹配、TopK 太小打印召回列表;检查入库与检索用的嵌入模型是否一致
召回了大量无关内容阈值过低、TopK 过大、资料本身有问题提高阈值;减小 TopK;人工检查片段质量
只召回半句话切分切断了上下文检查切分粒度和重叠(第 13 篇)
总是召回同一篇该文档片段重复度高、索引存在重复去重;检查入库逻辑

最容易被忽略的是切分问题。第 13 篇实测出的那个只有句号的碎片就是例子:它可能因“太短所以什么都像”被召回,挤占上下文。

重排序

检索返回的结果,顺序未必可靠。重排序(Rerank)是对候选结果重新打分的独立步骤:先多召回一些(比如 20 条),再用更精细(也更贵)的模型筛出最相关的几条。

这样做的好处是把“召回”和“排序”解耦:召回阶段追求不漏,排序阶段追求精准。

// 概念示意:先宽召回,再精排。candidates,_:=r.Retrieve(ctx,query,retriever.WithTopK(20))top:=rerank(candidates,query,3)// 用更强的相关性判断挑出前 3 条

四、③ 上下文组装:资料对了,但被淹没了

即使检索正确,组装方式也会毁掉效果:

问题后果对策
塞入太多片段关键信息被稀释,Token 浪费控制数量,优先高相关片段
片段无分隔两段内容被连读明确分段,带来源标注
来源缺失无法核验始终带来源
冲突内容未标注模型自行选一个提示词要求列出冲突
无关内容混入干扰判断用阈值过滤

一个实用技巧:在片段前标注它来自哪个文档和章节,给模型更多判断依据:

[1] 来源:handbook/printer.md(第 2 章 网络排查) 打印机无法联网时……

五、④ 生成与引用:资料对,回答还是错

到这一步,资料已经正确进入上下文,但模型仍可能出错:

现象原因对策
张冠李戴把 A 文档的内容说成 B 文档的强调查看来源标注;减小片段数量
过度推断资料只说了一半,模型自行补全提示词要求“只陈述资料明确写出的内容”
引用不支持结论标了来源,但来源其实没这么说需要评测验证,不能只信“有引用”
该拒答却硬答缺乏“不知道”的出口明确允许说明资料不足

“有引用”不等于“引用正确”。这一点必须靠评测来发现,不能靠抽查几个好例子。


六、评测:没有它就只能凭感觉

RAG 优化最大的风险是改一处、坏一处。没有固定测试集,你无法判断改动是进步还是退步。

准备测试集

不需要很大,几十条真实问题就很有用:

问题 期望命中文档 要点 打印机离线怎么办 handbook/printer.md 先查网线再查网段 怎么申请 VPN handbook/vpn.md 需审批 工单多久没处理可以催办 handbook/ticket.md 24 小时 报销流程是什么 (无相关资料) 应明确说明无法回答

关注指标

指标测什么怎么测
召回命中率该命中的是否命中对比期望文档与实际召回
拒答正确率无资料时是否正确说明用无答案的问题测试
引用正确率标注的来源是否支持结论人工或模型辅助核对
答案要点覆盖关键点是否说出对照要点清单
延迟与成本每次问答的开销记录耗时与 Token 用量

最后一行的“无资料”用例尤其重要——它测试的是系统会不会编,而这个问题比“答得好不好”更关键。

改动前后都要跑

每次调整切分、TopK、阈值、提示词,都用同一套测试集对比。凭印象判断优化效果,几乎一定会走弯路。


七、优化的优先级建议

按投入产出比排序,建议的排查与优化顺序是:

  1. 打印实际发送的消息。零成本,能立刻区分是数据问题还是生成问题。
  2. 检查切分质量。人工读几十个片段,往往能发现大量问题。
  3. 准备最小测试集。没有它,后面所有改动都无法评估。
  4. 调 TopK 与阈值。低成本,见效快。
  5. 加入重排序。需要额外模型调用,但通常明显提升排序质量。
  6. 多查询与路由。针对特定类型的查询失效。
  7. 提示词与引用格式优化。最后再做。

不要一上来就换更大的模型。多数 RAG 问题的根因在数据侧。


八、常见问题

召回为空但资料里明明有
检查 Embedding 模型是否一致、切分是否把关键信息切碎、查询与资料是否措辞差异过大。

召回很多但都不相关
提高阈值、减小 TopK,检查片段是否过长混入无关内容。

答案引用了错误来源
先减小上下文中的片段数量,再考虑是否需要更强的排序。评测时把“引用正确率”单独度量。

改了提示词,效果时好时坏
很可能没有固定评测方式,只在看个例。建立测试集后对比。

优化之后某些问题反而变差了
这在 RAG 里很常见(比如提高阈值提高了精度但降低了召回)。用测试集量化取舍,而不是凭感觉。


九、小结

  • 回答不对可能来自四个环节:查询理解、召回质量、上下文组装、生成与引用,先定位再优化。
  • 打印最终消息是最有效的第一步排查手段。
  • 切分质量是常见根因,第 13 篇的碎片例子就是典型。
  • 多查询、检索路由改善查询理解;重排序解耦召回与排序。
  • 必须建立测试集,重点覆盖“无资料时是否正确拒答”。
  • 优先修数据侧,而不是换更大的模型。

第三篇《工具与知识》到此完成。下一篇进入第四篇,开始用 ADK 构建真正会自主决策的 Agent。


参考资料与版本说明

资料核对日期为2026 年 10 月 9 日:

  1. Retriever 使用说明
  2. Eino 官方示例仓库(检索相关示例)
  3. Eino 主仓库

本文示例按Eino v0.9.21编写,检索与相似度计算为自定义实现,已实机运行,正文输出为真实结果。多查询与重排序部分为概念示意与优化方向,未在本篇实机运行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询