帶引用的回答
把檢索到的文件塊編號交給模型,要求它每句話註明出處、資料裡沒有就說沒有。再用程式檢查引用是否有效,並看看它在文件沒有答案的問題上會不會編。
- 約 35 分鐘
- 難度:進階
- 實測:2026-09-14 deepseek-flash,multilingual-e5-small
程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。
檢索做好了,找到的資料要交給模型。怎麼交、交了之後怎麼要求它,決定了最後的回答是否可靠。
這一課要達到三個目標:模型只根據資料回答,每句話註明依據哪一段資料,資料裡沒有答案時老實說沒有。然後寫一段程式,自動檢查它有沒有做到。
給資料編號
檢索用的是上一課效果最好的"改寫 + 混合 RRF",取前 5 塊。每一塊用一個帶編號和來源的標籤包起來:
def build_context(results):
parts = []
for i, (_, file, text) in enumerate(results, 1):
parts.append(f'<doc id="{i}" source="{file}">\n{text}\n</doc>')
return "<docs>\n" + "\n".join(parts) + "\n</docs>"
放進提示詞後是這個樣子(內容省略):
<docs>
<doc id="1" source="compatibility.md">
## Redirects
Unlike `requests`, HTTPX does **not follow redirects by default**. ……
</doc>
<doc id="2" source="compatibility.md">
……
</doc>
……
</docs>
问题:httpx 默认会自动跟随重定向吗?怎么开启?
編號是給模型引用用的,source 是給模型參考、也方便程式最後展示給使用者的。第 02 模組第 1 課講過用標籤把材料和指令分開,這裡的 <doc> 標籤還有一個作用:清楚地標出每一段資料的邊界,模型不會把兩段資料混成一段。
提示詞
SYSTEM = """你是 httpx 的答疑助手。只根据 <docs> 里的文档片段回答用户的问题。
规则:
- 每句话后面用 [编号] 注明依据的是哪个片段,可以同时引用多个,如 [1][3]。
- 片段里没有的信息不要写,哪怕你自己知道。
- 如果这些片段不足以回答问题,就直接说"文档里没有找到相关说明",不要猜。
- 用中文回答,简洁,代码保持原样。"""
第二條"哪怕你自己知道"很重要。模型對 httpx 本來就有很多瞭解,但它的瞭解可能是錯的,比如預設跟隨重定向。要求它只用資料,就是用資料的正確性替代模型記憶的正確性。代價是:資料裡恰好沒寫、而模型其實知道的東西,它也不能說了。第 7 課會遇到這樣的例子。
第三條給了模型一個明確的"退路"。沒有這條,資料不夠時模型往往會用自己的知識補上,而這正是我們要避免的。
執行結果
我準備了 4 個問題:兩個文件裡有明確答案的;一個文件裡完全沒有提到的(HTTP/3,我搜過 httpx 的全部文件,沒有出現過);一個答案需要仔細看資料的(httpx 有沒有自帶重試)。完整程式碼見 code/04-rag/citations.py,執行結果:
问:httpx 默认会自动跟随重定向吗?怎么开启?
检索到:[1] compatibility.md [2] compatibility.md [3] quickstart.md [4] http2.md [5] compatibility.md
答:httpx 默认**不会**自动跟随重定向,所有 HTTP 方法都是如此 [1][3]。
你可以在单个请求上显式开启 [1][3]:
```python
response = client.get(url, follow_redirects=True)
```
或者在创建 `Client` 时就默认开启 [1]:
```python
client = httpx.Client(follow_redirects=True)
```
开启后,可以通过 `response.history` 属性查看被跟随的重定向响应列表,按发生顺序排列 [3]。
检查:引用了 [1, 3],不存在的编号 无,没有引用的句子 0 句(输入 1002 词元)
问:httpx 支持 HTTP/3 吗?
检索到:[1] async.md [2] http2.md [3] advanced/transports.md [4] http2.md [5] advanced/transports.md
答:文档里没有找到相关说明。
检查:引用了 [],不存在的编号 无,没有引用的句子 1 句(输入 1128 词元)
未引用:文档里没有找到相关说明。
问:httpx 自带请求失败自动重试的功能吗?
检索到:[1] third_party_packages.md [2] advanced/transports.md [3] compatibility.md [4] async.md [5] advanced/transports.md
答:httpx 自带连接层面的重试功能,通过直接实例化 transport 来使用:当发生 `httpx.ConnectError` 或 `httpx.ConnectTimeout` 时,请求会按给定的次数重试,从而在链路不稳定的网络下更平滑地运行 [2]。
(代码示例省略)
如果只需要专门的 HTTPX 重试层,可以看 httpx-retries,它是一个 for HTTPX 的 retry layer [1]。若需要其他形式的重试行为(例如处理读/写错误,或针对 `503 Service Unavailable` 做出反应),文档建议考虑 tenacity 这类通用工具 [2]。
检查:引用了 [1, 2, 4],不存在的编号 无,没有引用的句子 0 句(输入 908 词元)
(關閉 SSL 校驗那道題的結果也正確,這裡省略了。)
重定向:答對了,而且引用了兩個來源,compatibility.md 和 quickstart.md 都講到了這一點。
HTTP/3:文件裡沒有,模型就說沒有,一個字都沒多說。對比第 01 模組第 2 課,同一個模型在沒有資料、沒有規則約束時,會編出一個不存在的 CVE 編號。
重試:這是最能說明問題的一道題。"httpx 有沒有自帶重試",憑記憶回答很容易答成"有"或者"沒有"。照著資料,它給出了準確的、有分寸的回答:自帶的只是連線失敗時的重試,讀寫錯誤和 503 要用別的工具。這正是 httpx 文件原文的意思,第 01 模組第 2 課裡我去翻文件核實過同一件事。
用程式檢查引用
模型說它引用了 [2],可 [2] 是不是真的存在?每句話是不是都有引用?這些可以用程式檢查:
def check_citations(text, n_docs):
"""找出回答里所有的 [数字],检查它们是否都在 1..n_docs 范围内,以及有多少句话没有引用。"""
cited = [int(x) for x in re.findall(r"\[(\d+)\]", text)]
invalid = sorted({c for c in cited if not 1 <= c <= n_docs})
prose = re.sub(r"```.*?```", "", text, flags=re.S) # 代码块是照抄文档的,不要求逐行引用
sentences = [s.strip() for s in re.split(r"(?<=[。!?])|\n+", prose) if len(s.strip()) > 8]
# 以冒号结尾的句子是在引出下面的代码,引用通常写在代码块后面,也不算
uncited = [s for s in sentences if not re.search(r"\[\d+\]", s) and not s.endswith((":", ":"))]
return sorted(set(cited)), invalid, uncited
它做兩件事:
- 引用的編號是否存在。只給了 5 段資料,模型卻引用了 [7],說明它在編造出處。這種情況在 4 道題裡一次都沒出現,但在資料很多、回答很長時是會發生的。
- 哪些句子沒有引用。沒有引用的句子,可能是模型自己加的內容。
這個檢查器我改過一次。第一版把程式碼塊裡的每一行程式碼都當成"沒有引用的句子",於是每道帶程式碼的題都報出一堆誤報。程式碼是從文件裡照抄的,不需要逐行引用,就先把程式碼塊去掉再檢查。現在唯一的"未引用"是"文件裡沒有找到相關說明"這一句,它本來就不需要引用。
程式檢查有明顯的侷限:它只能檢查格式,不能檢查內容。模型引用了 [2],但 [2] 裡是不是真的有這句話的依據,程式判斷不了。這需要人來看,或者讓另一個模型來判斷,就是下一課要做的"忠實度"評估。
把引用展示給使用者
對使用者來說,[1]、[3] 這樣的編號沒有意義,要把它們換成真正的出處。RepoBot v2 的做法是:回答結束後,找出回答裡實際引用了哪些編號,列出對應的文件:
来源:[1] compatibility.md [2] quickstart.md
做成網頁時,可以把編號做成連結,點選直接跳到文件的對應位置。使用者能自己核對,出了錯也能一眼看出是哪段資料誤導了模型。
可能出現的問題
模型不引用,或者只在最後引用一次。在提示詞裡給一個例子,展示你期望的引用格式。
引用了,但引錯了。回答裡說的是 A 段的內容,卻標了 [B]。這在資料之間內容相似時容易出現。可以要求模型在引用時順便摘錄原文的關鍵句,便於核對,代價是回答會變長。
資料之間互相矛盾。比如一段是舊版本的文件,一段是新版本的。模型可能隨便挑一個,也可能把兩者混在一起。解決辦法在檢索這一側:給文件塊加上版本資訊,只檢索當前版本的文件。
過於保守。資料裡其實有答案,只是說得比較隱晦,模型卻回答"文件裡沒有"。這是要求"只根據資料"的副作用。可以在提示詞裡說明"資料裡間接提到的也可以回答,但要說明是推斷"。
練習
- 在
citations.py里加一個問題:"httpx 和 requests 哪個效能更好?"文件裡沒有直接比較兩者效能的內容,看看模型怎麼回答。 - 刪掉提示詞裡"片段裡沒有的資訊不要寫,哪怕你自己知道"這一條,再問 HTTP/3 的問題,看模型會不會用自己的知識回答,回答的內容是否正確。
- 給
check_citations加一個檢查:如果回答裡引用了某一段,那這一段裡至少應該出現回答中的某個程式碼識別符號(比如follow_redirects)。這是一個粗略的"內容檢查",想想它會在哪些情況下誤判。
自測
1. 提示詞裡為什麼要寫"哪怕你自己知道,片段裡沒有的也不要寫"?
模型自己的知識可能是錯的,比如它記錯了 httpx 的預設重定向行為。要求只根據資料回答,就是讓資料的正確性代替模型記憶的正確性,並且讓每句話都有據可查。代價是資料裡恰好沒寫的資訊,模型就不能說了。
2. 程式檢查通過了(引用編號都存在、每句話都有引用),能說明回答是可靠的嗎?
不能。程式只能檢查格式:編號是否存在、有沒有標引用。它判斷不了引用的那段資料是否真的支援這句話。內容上是否忠實,需要人工檢查,或者讓另一個模型來判斷。
3. 為什麼要給模型一個"文件裡沒有找到相關說明"的明確說法?
沒有明確的退路時,模型在資料不夠的情況下傾向於用自己的知識補全,這恰恰可能引入錯誤。給它一個固定的說法,它就知道"不知道"也是一個可以接受的回答,程式也可以根據這句話識別出檢索沒有找到答案的情況。
提問與討論
這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。
提問 +3 點,回答別人 +6 點。內容經審核後公開。
正在載入討論…