RAGFlow引用機制揭秘:LLM引導與后端驗證如何協同工作?

昨天知識星球內有個提問:
RAGFlow 顯示引用為什么不通過提示詞直接顯示在回答中,而是通過分塊后和檢索片段比較向量相似度?判斷引用出處?能不能直接通過提示詞實現。

我當時給的回答是:
不能簡單地通過提示詞讓 LLM 直接、可靠地生成引用,因為這會引入幻覺風險。LLM 在生成內容時,為了讓回答顯得流暢和可信,可能會編造一個引用來源。此外,當輸出“這句話來自[2]”的時候,無法從技術上驗證,這也不符合生產實踐要求。換句話說,把生成答案和標注引用兩個步驟解耦,才能保證引用的客觀性。
本來這個對話就結束了,今天這個星友追評了下在 RAGFlow 的 Github 提了一個相關問題的 issue,結果 bot 的回答讓他有些困惑。我之前也沒有仔細了解過 RAGFlow 的相關源碼設計,就這這個問題實際看了下之后,覺得值得拿出來專門寫篇文章來做個拆解。
這篇試圖說清楚,為啥 RAGFlow 的最終回答中的引用顯示是后端完成的,LLM 通過提示詞引導生成的 [ID:i] 引用標記具體是什么作用,以及這種設計可以參考的工程化經驗。
以下,enjoy:
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.2markdown-content 組件
真正的魔法發生在 web/src/pages/chat/markdown-content/index.tsx。這個組件接收到原始字符串后,執行了最終的“查找與替換”操作。

# 代碼出處: web/src/pages/chat/markdown-content/index.tsx
import 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 cnotallow={getPopoverContent(chunkIndex)} key={i}>
<InfoCircleOutlined className={styles.referenceIcon} />
</Popover>
);
});
return replacedText;
},
// ...
);
return (
<Markdown
// ...
compnotallow={{
// 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: 0
Document: ... 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 直接回答用戶問題。但如果比如連續兩次回答的置信度都低于某個閾值,或者觸發了特定關鍵詞,系統會無縫切換到人工客服或預設的、基于規則的流程(確定性)。這也是目前業界常用的一種經典的平衡策略。


























