ragflow 显示引用为什么不通过提示词直接显示在回答中,而是通过分块后和检索片段比较向量相似度?判断引用出处?能不能直接通过提示词实现。
我当时给的回答是:
不能简单地通过提示词让 LLM 直接、可靠地生成引用,因为这会引入幻觉风险。LLM 在生成内容时,为了让回答显得流畅和可信,可能会编造一个引用来源。此外,当输出“这句话来自[2]”的时候,无法从技术上验证,这也不符合生产实践要求。换句话说,把生成答案和标注引用两个步骤解耦,才能保证引用的客观性。
本来这个对话就结束了,今天这个星友追评了下在 RAGFlow 的 Github 提了一个相关问题的 issue,结果 bot 的回答让他有些困惑。我之前也没有仔细了解过 RAGFlow 的相关源码设计,就这这个问题实际看了下之后,觉得值得拿出来专门写篇文章来做个拆解。
这篇试图说清楚,
为啥 RAGFlow 的最终回答中的引用显示是后端完成的,LLM 通过提示词引导生成的 [ID:i] 引用标记具体是什么作用,以及这种设计可以参考的工程化经验。
1.Issue 中的 BOT 误导
这个 Issue 的核心问题是:“RAGFlow 是如何生成引用标记的?”bot 的回答显得摇摆不定:起初它断言引用完全由后端生成,LLM 本身并不参与;https://github.com/infiniflow/ragflow/issues/8817
但在被用户以源码中的 citation_prompt 质疑后,它又提出了一种“双模式竞争”理论,暗示 LLM 和后端是两条可能冲突的独立路径。不过不看源码就能猜到,这种说法显然是不合理的。但是具体还是要从源码中找答案。
2.后端关键函数分析
要找到引用的源头,首先应该查看后端代码。在 RAGFlow 的源码中,我在 rag/nlp/search.py 文件里,找到了一个名为 insert_citations 的关键函数。
# 代码出处: rag/nlp/search.py (Dealer 类中)class Dealer: # ... 其他方法 ...
def insert_citations(self, answer, chunks, chunk_v, embd_mdl, tkweight=0.1, vtweight=0.9): # 1. 将LLM的纯文本回答切分成句子 pieces = re.split(r"(```)", answer) # ... 省略清洗和聚合代码 ... pieces_ = [...] # 得到干净的句子列表
# 2. 对每个句子,独立计算与所有知识块的混合相似度 ans_v, _ = embd_mdl.encode(pieces_) cites = {} for i, a in enumerate(pieces_): sim, _, _ = self.qryr.hybrid_similarity(ans_v[i], chunk_v, ...) mx = np.max(sim) * 0.99 if mx < thr: continue # 3. 记录下所有相似度足够高的知识块作为引用 cites[idx[i]] = list( set([str(ii) for ii in range(len(chunk_v)) if sim[ii] > mx]))
# 4. 将计算出的引用标记 [ID:c] 注入到句子末尾 res = "" for i, p in enumerate(pieces): res += p # ... if i in cites: for c in cites[i]: if c in seted: continue res += f" [ID:{c}]" seted.add(c) return res, seted
这段代码的逻辑很清晰的说明了以下三个问题:
1、输入是纯文本: 该函数的输入 answer 是 LLM 生成的纯净答案。它完全不关心 answer 是否已经带有 LLM 自己生成的引用标记。
2、独立计算: 函数的核心是 hybrid_similarity,它完全基于内容相似度(结合了向量语义和关键词文本)来独立判断每个句子与知识块的关联。这是一个从零开始、基于数据和算法的计算过程。
3、权威注入: 函数最后将自己计算出的引用 [ID:c] 注入到文本中,并返回最终结果。
初步结论非常明确,RAGFlow 的引用完全由后端算法基于内容相似度独立生成,拥有最终的、绝对的决定权。 它不依赖、不修改、也不信任 LLM 可能生成的任何引用。这当然也是符合最佳实践的做法。
3.前端对应溯源
进一步的问题是,既然知道后端生成了带有 [ID:i] 标记的字符串。那么前端是如何把这个文本标记变成一个可点击、可交互的链接的呢?
*3.1message-item 组件*
在 web/src/components/message-item/index.tsx 中,可以看到它负责渲染一个完整的消息气泡。但它并不亲自处理消息内容,而是将任务委托了出去。
# 代码出处: web/src/components/message-item/index.tsx// ... <div className={/* ... */}> {/* 关键:它将原始content和引用数据直接传递给MarkdownContent */} <MarkdownContent loading={loading} content={item.content} reference={reference} clickDocumentButton={clickDocumentButton} ></MarkdownContent> </div>// ...
*3.2.markdown-content 组件*
真正的魔法发生在 web/src/pages/chat/markdown-content/index.tsx。这个组件接收到原始字符串后,执行了最终的“查找与替换”操作。
# 代码出处: web/src/pages/chat/markdown-content/index.tsximport reactStringReplace from 'react-string-replace'; // 1. 引入关键的替换库import { currentReg } from '../utils'; // 2. 引入包含引用正则表达式的文件
// ...
const MarkdownContent = (/* ... */) => { // ... const renderReference = useCallback( (text: string) => { // 3. 使用 react-string-replace 对文本进行查找和替换 let replacedText = reactStringReplace(text, currentReg, (match, i) => { // 4. currentReg 就是匹配 [ID:i] 的正则表达式 // 对于每一个匹配到的 `match` (例如 "[ID:5]"), 执行以下逻辑: const chunkIndex = getChunkIndex(match); // 提取出数字 5
// 5. 返回一个可交互的 React 组件 (Popover) 来替换原始的 [ID:i] 文本 return ( <Popover content={getPopoverContent(chunkIndex)} key={i}> <InfoCircleOutlined className={styles.referenceIcon} /> </Popover> ); }); return replacedText; }, // ... );
return ( <Markdown // ... components={{ // 6. 通过重写组件渲染逻辑,确保所有文本都经过 renderReference 函数的处理 'custom-typography': ({ children }: { children: string }) => renderReference(children), // ... }} > {contentWithCursor} </Markdown> );};
前端的处理流程清晰地展现了“职责分离”原则。MessageItem 负责消息的整体结构,而 MarkdownContent 负责将后端生成的 [ID:i] 文本标记,通过查找替换的方式,转换为用户可以交互的 UI 组件。这再次证实了所有引用处理在数据到达前端之前,必须已经在后端全部完成了。
4.提示词引导生成的巧思
既然后端和前端的逻辑都很清晰,还没有回答的一个问题是,如果后端函数是引用的唯一来源,那为什么 RAGFlow 的源码中还要在 rag/prompts/citation_prompt.md 中写下引导 LLM 生成引用的规则,而这个引用最终并不会使用。
# 证据: rag/prompts/citation_prompt.md 的内容## Citation Requirements- Use a uniform citation format such as [ID:i] [ID:j]...- Citation markers must be placed at the end of a sentence...- A maximum of 4 citations are allowed per sentence.- DO NOT insert citations if the content is not from retrieved chunks.- ...- STRICTLY prohibit the use of strikethrough symbols...
## Example START: Here is the knowledge base:Document: ... ID: 0Document: ... ID: 1...
: What's Elon's view on dogecoin?
: Musk has consistently expressed his fondness for Dogecoin... He has referred to it as his favorite cryptocurrency [ID:0] [ID:1]....## Example END
看到这里,就知道为啥 GitHub 机器人所说的“双模式竞争”纯属瞎编了。citation_prompt 的真正目的,不是为了“结果”,而是为了“过程”。
换句话说,不是为了得到 LLM 生成的 [ID:i] 这个结果,而是为了规范 LLM 生成答案文本的整个过程。它通过这种方式向 LLM 施加了强烈的约束。
1、降低幻觉: 通过强制要求 LLM“必须为你的话找到出处”,系统在源头上极大地降低了内容幻觉。
2、保证内容质量: LLM 必须生成与原文高度相关的内容。
3、为后端铺路: 正是这份高质量的草稿,让后端的 insert_citations 函数能够游刃有余地进行精准的相似度匹配,并最终完成权威的标注工作。
5.写在最后
RAGFlow 的引用生成机制,也形象的展示了 LLM 应用落地的核心范式。生产实践可用的关键不在于对 LLM 能力的盲目相信(当然最好用最先进的 LLM),也不在于过多的依赖传统的规则引擎,而在于把 LLM 作为一个强大但需要被引导和验证的推理核心,并围绕它构建一套由确定性工程逻辑组成的脚手架,最后给出三个类似的样例作为参考:
1、AI Agent 与工具调用 (Tool Calling)
让 LLM 自由思考(Chain of Thought),分析用户意图,并决定需要调用哪个 API(工具)。但比如一旦 LLM 决定调用 get_weather(“北京”),这个 API 本身的执行过程是完全确定的。系统不会让 LLM 去“创造”天气数据,而是通过严格的函数调用获取真实、可信的结果。
2、结构化数据提取 (JSON Mode)
对于一段非结构化的用户评论:“我喜欢这款手机的屏幕,但电池太不给力了”,通过强制启用 JSON Mode,并提供 Pydantic 等模式定义,来约束 LLM 的输出必须符合{ “positive_feedback”: “屏幕”, “negative_feedback”: “电池” }这样严格的格式。LLM 可以在内容上发挥,但格式被约束,这也保证了下游程序的可解析性。
3、黑盒兜底机制 (Fallback)
在许多客服机器人中,首先尝试让 LLM 直接回答用户问题。但如果比如连续两次回答的置信度都低于某个阈值,或者触发了特定关键词,系统会无缝切换到人工客服或预设的、基于规则的流程(确定性)。这也是目前业界常用的一种经典的平衡策略。
如何学习大模型 AI ?
由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。
但是具体到个人,只能说是:
“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。
这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
第一阶段(10天):初阶应用
该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。
- 大模型 AI 能干什么?
- 大模型是怎样获得「智能」的?
- 用好 AI 的核心心法
- 大模型应用业务架构
- 大模型应用技术架构
- 代码示例:向 GPT-3.5 灌入新知识
- 提示工程的意义和核心思想
- Prompt 典型构成
- 指令调优方法论
- 思维链和思维树
- Prompt 攻击和防范
- …
第二阶段(30天):高阶应用
该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。
- 为什么要做 RAG
- 搭建一个简单的 ChatPDF
- 检索的基础概念
- 什么是向量表示(Embeddings)
- 向量数据库与向量检索
- 基于向量检索的 RAG
- 搭建 RAG 系统的扩展知识
- 混合检索与 RAG-Fusion 简介
- 向量模型本地部署
- …
第三阶段(30天):模型训练
恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。
到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?
- 为什么要做 RAG
- 什么是模型
- 什么是模型训练
- 求解器 & 损失函数简介
- 小实验2:手写一个简单的神经网络并训练它
- 什么是训练/预训练/微调/轻量化微调
- Transformer结构简介
- 轻量化微调
- 实验数据集的构建
- …
第四阶段(20天):商业闭环
对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。
- 硬件选型
- 带你了解全球大模型
- 使用国产大模型服务
- 搭建 OpenAI 代理
- 热身:基于阿里云 PAI 部署 Stable Diffusion
- 在本地计算机运行大模型
- 大模型的私有化部署
- 基于 vLLM 部署大模型
- 案例:如何优雅地在阿里云私有部署开源大模型
- 部署一套开源 LLM 项目
- 内容安全
- 互联网信息服务算法备案
- …
学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。
如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。