news 2026/10/17 6:47:53

14|RAG 入门:构建带来源引用的知识问答 - eino教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
14|RAG 入门:构建带来源引用的知识问答 - eino教程

14|RAG 入门:构建带来源引用的知识问答

《Eino 实战:用 Go 构建 AI 应用与智能体》· 第 14 篇
关键词:Retriever、RAG、上下文组装、来源引用、无结果处理
前置条件:已完成 13|知识入库。

上一篇我们把资料整理成可检索的片段。这一篇把它接进问答流程:用户提问 → 召回相关片段 → 交给模型 → 生成有据可依的回答。

看似简单,但有几个决定成败的细节:检索不到时怎么办、答案怎么标来源、上下文怎么组织。这一篇逐个处理。


一、Retriever:接口只有一行

typeRetrieverinterface{Retrieve(ctx context.Context,querystring,opts...Option)([]*schema.Document,error)}

输入查询字符串,返回文档列表。就这么简单——切分、向量化、索引的复杂性都封装在实现内部。

常用的调用选项:

docs,err:=r.Retrieve(ctx,query,retriever.WithTopK(5),// 最多返回几条retriever.WithScoreThreshold(0.6),// 低于分数阈值的不返回)

两个参数都需要根据实际数据调,没有通用最优值:

  • TopK太小:可能漏掉关键资料。
  • TopK太大:无关内容进入上下文,既浪费 Token,又干扰模型判断。
  • 阈值太高:容易把本可用的资料过滤掉。

建议先用固定样本测出召回情况,再决定取值,而不是照抄示例里的数字。


二、上下文组装:把资料变成提示词

召回的结果需要组织成模型能读的形式。一个基本可用的格式:

[1] 来源:handbook/printer.md 打印机无法联网时,先检查网线是否插紧…… [2] 来源:handbook/vpn.md 远程访问需要先申请 VPN 账号……

对应代码:

funcbuildContext(docs[]*schema.Document)string{iflen(docs)==0{return""}varb strings.Builderfori,d:=rangedocs{fmt.Fprintf(&b,"[%d] 来源:%v\n%s\n\n",i+1,d.MetaData["source"],d.Content)}returnb.String()}

三个设计要点:

  1. 编号。让模型可以用[1]这样的方式引用,前端也能据此渲染成可点击的来源。
  2. 保留来源。直接来自第 13 篇的元数据。没有来源的答案,用户无法核验,也就难以信任。
  3. 分隔清晰。片段之间要断开,否则模型可能把两段内容连读成一段。

系统提示怎么写

schema.SystemMessage("你是工单助手。只依据提供的参考资料回答,并在结论后标注来源编号;资料不足时明确说明。")

这句话包含三个约束,缺一不可:

  • 只依据参考资料:抑制模型用自身知识补充。
  • 标注来源编号:让答案可核验。
  • 资料不足要说明:给出“拒绝回答”的出口。

但要清醒地认识到:这些是行为引导,不是硬约束。模型仍可能不标来源,或基于资料过度推断。第 15 篇会讨论如何验证和改善。


三、完整示例

下面这段程序用模拟检索器和模拟模型,不依赖任何外部服务即可运行,重点是把链路走通:

packagemainimport("context""errors""fmt""log""strings""github.com/cloudwego/eino/components/model""github.com/cloudwego/eino/components/prompt""github.com/cloudwego/eino/components/retriever""github.com/cloudwego/eino/compose""github.com/cloudwego/eino/schema")// ---- 检索器:用关键词匹配模拟向量检索,便于无依赖运行 ----typekeywordRetrieverstruct{docs[]*schema.Document}func(r*keywordRetriever)Retrieve(_context.Context,querystring,_...retriever.Option)([]*schema.Document,error){varhits[]*schema.Documentfor_,d:=ranger.docs{ifstrings.Contains(d.Content,query)||sharedKeyword(query,d.Content){// 复制一份并附带分数,避免修改原始文档。cp:=*d cp.WithScore(0.9)hits=append(hits,&cp)}}iflen(hits)>2{hits=hits[:2]}returnhits,nil}funcsharedKeyword(query,contentstring)bool{for_,kw:=range[]string{"打印机","离线","网络"}{ifstrings.Contains(query,kw)&&strings.Contains(content,kw){returntrue}}returnfalse}// ---- 模拟模型:把参考资料的来源整理进回答,便于验证链路 ----typemockModelstruct{}func(m*mockModel)Generate(_context.Context,in[]*schema.Message,_...model.Option)(*schema.Message,error){iflen(in)==0{returnnil,errors.New("空输入")}returnschema.AssistantMessage("(模拟回答)已根据参考资料生成建议。",nil),nil}func(m*mockModel)Stream(_context.Context,in[]*schema.Message,_...model.Option)(*schema.StreamReader[*schema.Message],error){sr,sw:=schema.Pipe[*schema.Message](1)gofunc(){defersw.Close()sw.Send(schema.AssistantMessage("(模拟流式回答)",nil),nil)}()returnsr,nil}// ---- 把检索结果组织成带来源的上下文 ----funcbuildContext(docs[]*schema.Document)string{iflen(docs)==0{return""}varb strings.Builderfori,d:=rangedocs{fmt.Fprintf(&b,"[%d] 来源:%v\n%s\n\n",i+1,d.MetaData["source"],d.Content)}returnb.String()}funcmain(){iferr:=run();err!=nil{log.Fatal(err)}}funcrun()error{ctx:=context.Background()r:=&keywordRetriever{docs:[]*schema.Document{{ID:"handbook/printer.md#0",Content:"打印机无法联网时,先检查网线是否插紧。",MetaData:map[string]any{"source":"handbook/printer.md"}},{ID:"handbook/vpn.md#0",Content:"远程访问需要先申请 VPN 账号。",MetaData:map[string]any{"source":"handbook/vpn.md"}},{ID:"handbook/ticket.md#0",Content:"工单超过 24 小时未分派可申请催办。",MetaData:map[string]any{"source":"handbook/ticket.md"}},}}query:="打印机离线怎么处理"// 1. 检索:传入查询,取回相关片段。docs,err:=r.Retrieve(ctx,query)iferr!=nil{returnfmt.Errorf("检索失败: %w",err)}// 2. 无结果时明确说明,而不是让模型自由发挥。iflen(docs)==0{fmt.Println("未检索到相关资料,请补充信息或转人工。")returnnil}for_,d:=rangedocs{fmt.Printf("命中: %s (score=%.2f)\n",d.ID,d.Score())}// 3. 组装上下文:保留来源,便于回答中引用。contextText:=buildContext(docs)tpl:=prompt.FromMessages(schema.FString,schema.SystemMessage("你是工单助手。只依据提供的参考资料回答,并在结论后标注来源编号;资料不足时明确说明。"),schema.UserMessage("参考资料:\n{context}\n问题:{question}"),)// 4. 用编排把模板和模型连起来。chain:=compose.NewChain[map[string]any,*schema.Message]()chain.AppendChatTemplate(tpl).AppendChatModel(&mockModel{})runnable,err:=chain.Compile(ctx)iferr!=nil{returnfmt.Errorf("编译失败: %w",err)}out,err:=runnable.Invoke(ctx,map[string]any{"context":contextText,"question":query})iferr!=nil{returnfmt.Errorf("生成失败: %w",err)}fmt.Println("回答:",out.Content)returnnil}

实机输出:

命中: handbook/printer.md#0 (score=0.90) 回答: (模拟回答)已根据参考资料生成建议。

示例里检索器用的是关键词匹配,因此只能召回明显相关的那一条。真实项目接入向量检索后,召回质量会好得多——但链路的形状是一样的:检索 → 组织上下文 → 生成。


四、无结果:RAG 里最重要的分支

很多 RAG 演示都假设“总能检索到东西”。真实场景里,用户问的往往是资料里没有的:

用户:报销流程是什么? 资料库:只有设备、网络、工单相关文档

此时有两种做法:

错误做法:把空的上下文交给模型,让它基于自身知识回答。结果是听起来很确定的幻觉。

正确做法:显式判断,明确告知:

iflen(docs)==0{fmt.Println("未检索到相关资料,请补充信息或转人工。")returnnil}

或者在提示词之外,用代码做兜底判断。下面这段程序对比了两种查询(实机运行):

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

处理策略

情况建议处理
完全无召回明确说明,转人工或请用户补充信息
召回分数都很低同上,或告知“仅供参考,请核实”
召回内容相互矛盾都呈现并标注来源,提示存在不一致
召回的是过期资料依赖元数据中的版本/时间判断,优先给最新

“我不知道”是一个合格答案。在业务系统里,含糊其辞比明确拒答风险更大。


五、RAG 能解决什么,不能保证什么

这一点在推销式文章里经常被含糊过去,但在工程上必须清楚。

RAG 能做的:

  • 让回答参考你提供的最新资料。
  • 让答案可以标注来源,供人核验。
  • 更新资料后无需重新训练模型。

RAG 不能保证的:

期望现实
有资料就不会答错模型可能误读资料、张冠李戴
检索到的都是对的可能召回不相关或过期内容
引用了来源就可靠引用可能不支持结论
覆盖所有问题资料没写的问题它答不了
提升模型本身的推理能力RAG 只提供信息,不改变模型能力

一句话概括:RAG 改善的是信息来源,不是模型的判断力。它把“模型凭记忆瞎说”变成“模型照着资料说,但不保证说得对”。


六、常见问题

回答完全没有用到资料
检查上下文是否真的传进了提示词(打印最终消息看一眼,别猜)。也检查系统提示是否明确要求“只依据参考资料”。

回答了但没标来源
要求标注来源是引导而非强制。可以在提示词中给出标注格式示例,也可以在代码后处理中校验并补标。

检索到相关资料,答案却是错的
模型可能误读了内容。检查片段是否被切断了上下文(第 13 篇的切分问题),或考虑在生成前加校验步骤(第 15 篇)。

不同资料说法冲突
在提示词中明确要求“资料冲突时都列出并标注来源”,不要让它自行选一个。

问了个资料里没有的问题,模型还是硬答
缺少“无结果”分支。加上显式判断,并在提示词中给出拒答许可。

召回数量设多少合适
先用一组真实问题做测试,观察召回命中情况再定。不要照抄默认值。


七、小结

  • Retriever 接口简单,复杂性封装在实现里;TopK与阈值需按数据实测调整。
  • 上下文组装要带编号和来源,让答案可核验。
  • 系统提示应同时约束“只依据资料”“标注来源”“不足要说明”。
  • 无召回必须显式处理,“我不知道”是合格答案。
  • RAG 改善信息来源,不保证正确性,也不改变模型的判断力。

下一篇《RAG 优化:为什么检索到了,回答仍然不对?》,我们从召回、切分、上下文和评测四个角度定位问题。


参考资料与版本说明

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

  1. Retriever 使用说明
  2. ChatTemplate 使用说明
  3. Eino 主仓库

本文示例按Eino v0.9.21编写,检索器与模型均为自定义模拟实现,已实机运行,正文输出为真实结果。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 6:46:19

案例 2.2 图片与声音开发实践博客

前言本次完成微信小程序《案例 2.2 图片与声音》开发,实现图片点击控制音频播放暂停基础功能,并按照作业要求扩展播放速率、控制台打印播放信息,记录完整开发过程、代码与踩坑要点。一、项目功能说明页面加载完成后,展示宝塔山图片…

作者头像 李华
网站建设 2026/10/17 6:45:18

电器元件-荧光灯(日光灯)

简介荧光灯就是咱们以前常说的日光灯,属于气体放电灯。 灯管里面抽走空气,充少量氩气水银,管壁涂一层荧光粉。原理简单说: 通电后,灯管里的水银会产生看不见的紫外线;紫外线打在管壁荧光粉上,紫…

作者头像 李华
网站建设 2026/10/17 6:44:57

机器学习之模型评估

模型评估理论基础: 欠拟合 (Underfitting):模型过于简单,无法捕捉数据真实规律(类比:学生没理解核心概念)。 特征:训练误差和测试误差都高;模型复杂度不足;特征工程不充分…

作者头像 李华
网站建设 2026/10/17 6:44:47

用Python给自家WiFi做安全体检:从握手包抓取到弱密码检测

前两天有位读者在后台问我:用Python到底能不能把隔壁WiFi的密码跑出来?我盯着这条消息看了半天,第一个念头不是技术行不行,而是这事绝对不能碰。但转念一想,这个方向本身确实有巨大价值——只不过要把它放在合规的框架…

作者头像 李华
网站建设 2026/10/17 6:44:05

一根线的显相:人体拓扑论

—— 关于人体拓扑生长原理及未来仿生体路径的假说 本文为作者(道影子)基于内观体悟所提出的一个思想假说,非实证科学结论。文中涉及 "维性力网"" 人体拓扑 " 等概念,均为本体系内的自洽设定,供同…

作者头像 李华