模組 04 · 第 7 課

專案:讓答疑助手讀懂專案文件

把切分、混合檢索、查詢改寫、帶引用的回答裝進 RepoBot,做出 v2。它在 v1 答錯的問題上答對了,還解決了多輪對話裡"那非同步呢"這種追問的檢索問題。

  • 約 60 分鐘
  • 難度:進階
  • 實測:2026-09-14 deepseek-flash,multilingual-e5-small,bge-reranker-base

程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。

這一課把 04 模組的所有東西裝進 RepoBot,做出第二版。你會看到一個在單次問答裡沒遇到過的問題:多輪對話中,使用者的追問往往本身搜不到任何東西。

做完的標準

  • 問"httpx 預設會自動跟隨重定向嗎?",回答是"不會",並列出來源 compatibility.md
  • 每個回答都用 [編號] 註明出處,結尾列出實際引用的文件。
  • 問文件裡沒有的內容(比如 HTTP/3),回答"文件裡沒有找到相關說明",不編造。
  • 連著問"怎麼給 httpx 設定超時?"和"那非同步客戶端呢?",第二個問題能檢索到超時相關的文件。
  • 第二次啟動時,不再重新計算文件的向量。
  • eval_retrieval.py 在 20 道評估題上,前 5 名命中率達到 100%。

結構

程式碼在 projects/repobot/v2/,比 v1 多了兩個檔案:

llm.py             客户端、计费、带重试的请求(从 v1 的 repobot.py 里挪出来)
retrieval.py       切分、BM25、向量检索、RRF、重排、查询改写(04 模块第 2~4 课)
repobot.py         对话程序:在 v1 的基础上加上检索和引用
eval_retrieval.py  用 20 道题评估检索效果(04 模块第 3 课)

retrieval.py 裡的程式碼,前幾課都逐段講過,這裡只講組裝時新遇到的三個問題。

問題一:追問搜不到東西

在 04 模組的練習程式碼裡,每個問題都是獨立的。可在對話裡,使用者會這樣問:

你:怎么给 httpx 设置 10 秒的超时?
你:那异步客户端呢?

第二個問題直接拿去檢索,"那非同步客戶端呢"會找到講非同步的文件,卻找不到講超時的,因為問題里根本沒有"超時"兩個字。

v2 的解決辦法是在查詢改寫這一步帶上最近的對話,讓模型先弄明白使用者到底在問什麼,再生成檢索詞:

REWRITE_PROMPT = """你要为一个 httpx 答疑助手生成文档检索词。httpx 的文档是英文的。

根据"最近的对话"理解用户"最新的问题"到底在问什么(比如"那异步呢"要结合上文补全),
然后输出一行英文检索词:包含问题的完整英文表述,以及文档里可能出现的参数名、类名、术语。只输出这一行。"""


class QueryRewriter:
    def rewrite(self, question, history=()):
        recent = "\n".join(f"{m['role']}: {m['content'][:300]}" for m in list(history)[-4:])
        ……
            text, self.last_usage = llm.chat([
                {"role": "system", "content": REWRITE_PROMPT},
                {"role": "user", "content": f"最近的对话:\n{recent or '(无)'}\n\n最新的问题:{question}"},
            ])

只取最近 4 條訊息,每條最多 300 個字,夠理解指代,又不會讓改寫這一步變得太貴。下面的執行結果裡能看到它的效果。

問題二:歷史裡存什麼

每一輪都要把檢索到的 5 段文件放進提示詞,大約一千個詞元。如果把它們也存進對話歷史,十輪之後歷史裡就有上萬個詞元的舊文件,又貴又容易干擾模型。

v2 的做法是:文件只放進這一輪的 user 訊息裡,存進歷史的只有使用者的原始問題和模型的回答:

            messages = ([{"role": "system", "content": SYSTEM}] + history +
                        [{"role": "user", "content": build_context(results) + f"\n\n问题:{question}"}])
            ……
        history += [{"role": "user", "content": question}, {"role": "assistant", "content": text}]

模型的回答裡已經包含了從文件裡得到的要點,後面的對話要用到時,從回答裡就能找到。

問題三:別每次啟動都重算向量

196 個文件塊,每次啟動都要用嵌入模型計算一遍,要十幾秒。文件不變的話,結果每次都一樣,完全可以快取:

        key = hashlib.sha256((EMBED_MODEL + json.dumps(self.chunks)).encode()).hexdigest()[:16]
        path = cache_dir / f"vectors-{key}.npy"
        if path.exists():
            self.matrix = np.load(path)
        else:
            self.matrix = self.embedder.encode(["passage: " + t for t in texts], normalize_embeddings=True, batch_size=32)
            np.save(path, self.matrix)

快取檔案的名字來自"嵌入模型名 + 所有文件塊的內容"的雜湊值。只要文件改了一個字,或者換了嵌入模型,雜湊就會變,就會重新計算,不會誤用過期的向量。查詢改寫的結果也同樣快取在 .cache/rewrites.json 裡。

檢索效果

cd projects/repobot/v2
pip install -r requirements.txt
export HF_ENDPOINT=https://hf-mirror.com
python eval_retrieval.py
python eval_retrieval.py --rerank
== 不加重排
前 1 名命中:80%
前 3 名命中:100%
前 5 名命中:100%
MRR:0.892
== 加重排
前 1 名命中:85%
前 3 名命中:95%
前 5 名命中:100%
MRR:0.902

不加重排的 MRR 是 0.892,比第 4 課"改寫 + 混合 RRF"的 0.772 高了不少。檢索的演算法完全一樣,唯一的區別是改寫的提示詞換成了上面這個新版本:"輸出一行英文檢索詞:包含問題的完整英文表述,以及……"。第 4 課的版本是"包含問題的英文翻譯,以及……",而且沒有對話上下文。

這再次說明,查詢改寫的提示詞對檢索效果影響很大。這也是為什麼每改一處都要重新跑評估:你以為只是順手改了一句提示詞,指標可能就變了。

在這個版本上,重排只把 MRR 從 0.892 提到了 0.902,前 3 名命中率還從 100% 降到了 95%。多花一秒錢換來的提升很小,所以 v2 預設不開重排,需要時加 --rerank

執行一次對話

printf '%s\n' "httpx 默认会自动跟随重定向吗?" "那默认最多跟随几次?" "怎么给 httpx 设置 10 秒的超时?" \
  "那异步客户端呢?" "httpx 支持 HTTP/3 吗?" "今天北京天气怎么样?" | python repobot.py --show-query

--show-query 會顯示每一輪的檢索詞和檢索到的文件。我執行的結果(刪掉了幾段程式碼示例,其他沒有改動):

你:httpx 默认会自动跟随重定向吗?
[检索词] Does httpx follow redirects by default? follow_redirects parameter, redirect, httpx.Client, AsyncClient, max_redirects, history, redirection behavior
[检索到] [1] compatibility.md  [2] quickstart.md  [3] compatibility.md  [4] compatibility.md  [5] http2.md
RepoBot:不会。与 `requests` 不同,HTTPX 默认**不跟随重定向**[1],对所有 HTTP 方法默认都不跟随重定向[2]。
(启用方法和示例代码省略)
来源:[1] compatibility.md  [2] quickstart.md
[回答输入 1047(缓存命中 0),输出 180,本轮 0.00060 美元(含改写),累计 0.00060 美元]

你:那默认最多跟随几次?
[检索词] httpx follow_redirects max_redirects default maximum number of redirects
[检索到] [1] quickstart.md  [2] compatibility.md  [3] advanced/resource-limits.md  [4] compatibility.md  [5] advanced/transports.md
RepoBot:文档里没有找到相关说明。检索到的片段只说明了 HTTPX 默认不跟随重定向,以及如何通过 `follow_redirects=True` 启用[1][2],但没有提到最大跟随次数的默认值。

建议查阅 HTTPX 官方文档中关于重定向或多重定向(`max_redirects`)的页面确认。
来源:[1] quickstart.md  [2] compatibility.md

你:怎么给 httpx 设置 10 秒的超时?
[检索词] httpx set timeout 10 seconds how to configure timeout Timeout timeout parameter httpx.Client(timeout=...) httpx.get(timeout=TimeOut(...)) DEFAULT_TIMEOUT_CONFIG connect read write pool timeout
[检索到] [1] advanced/extensions.md  [2] advanced/timeouts.md  [3] advanced/timeouts.md  [4] advanced/timeouts.md  [5] advanced/timeouts.md
RepoBot:可以给客户端实例设置默认超时,例如设置 10 秒对所有请求生效:
(代码示例省略)
来源:[2] advanced/timeouts.md  [4] advanced/timeouts.md

你:那异步客户端呢?
[检索词] asynchronous client timeout configuration httpx.AsyncClient(timeout=10.0) default timeout per-request async
[检索到] [1] advanced/timeouts.md  [2] advanced/timeouts.md  [3] advanced/timeouts.md  [4] compatibility.md  [5] quickstart.md
RepoBot:异步客户端同样支持超时设置,用法与同步客户端一致。
(代码示例省略)
不过需要说明:文档片段中给出的示例都写作 `httpx.Client(...)`,没有出现 `httpx.AsyncClient(...)` 的写法,因此片段里没有专门针对异步客户端的示例,但相同参数适用于客户端实例的默认超时与单请求超时设置[1][3]。
来源:[1] advanced/timeouts.md  [2] advanced/timeouts.md  [3] advanced/timeouts.md

你:httpx 支持 HTTP/3 吗?
[检索词] httpx HTTP/3 support HTTP/3 QUIC protocol
[检索到] [1] index.md  [2] advanced/proxies.md  [3] index.md  [4] index.md  [5] http2.md
RepoBot:文档里没有找到相关说明。

检索到的片段只提到 HTTPX 支持 HTTP/1.1 和 HTTP/2[3][4],并且 HTTP/2 默认不启用、需要安装可选依赖并显式开启[5],但没有提到 HTTP/3。

建议查阅 HTTPX 官方文档确认是否支持 HTTP/3。
来源:[3] index.md  [4] index.md  [5] http2.md

你:今天北京天气怎么样?
[检索词] Beijing weather forecast today current temperature conditions
[检索到] [1] troubleshooting.md  [2] logging.md  [3] troubleshooting.md  [4] exceptions.md  [5] troubleshooting.md
RepoBot:抱歉,我只负责 httpx 相关的答疑,不回答天气这类无关问题。如果你有 httpx 的使用问题,欢迎继续问我。
[回答输入 2481(缓存命中 640),输出 29,本轮 0.00069 美元(含改写),累计 0.00388 美元]

逐條看

重定向,答對了。v1 在這裡答錯,還編了版本歷史。v2 答"不會",並給出了兩個來源。

追問被正確理解了。"那預設最多跟隨幾次?"被改寫成了 httpx follow_redirects max_redirects default maximum number of redirects;"那非同步客戶端呢?"被改寫成了 asynchronous client timeout configuration httpx.AsyncClient(timeout=10.0),檢索到的都是 timeouts.md。沒有帶上對話上下文的改寫,第二個問題只會搜到講非同步的文件。

"預設最多跟隨幾次",它說文件裡沒有。這個回答是對的:httpx 的文件確實沒寫這個預設值,它只存在於原始碼裡(第 03 模組第 5 課我們在 httpx/_config.py 裡查到是 20)。可有意思的是,v1 憑記憶答對了這一題。v2 因為嚴格要求"只根據資料回答",反而答不上來。這是第 5 課說過的代價:資料裡沒寫的,就算模型知道,也不能說。

非同步客戶端,回答得很有分寸。文件的示例都是 httpx.Client,它如實說明了這一點,同時指出相同的參數也適用。這正是我們想要的:不假裝文件裡有它沒有的東西。

HTTP/3,沒有編造

天氣,拒絕了,但白花了錢。它仍然做了一次改寫和檢索,找了 5 段毫不相關的文件塞進提示詞。更好的做法是在檢索之前先判斷問題和 httpx 有沒有關係,無關就直接拒絕。06 模組第 5 課的"護欄"會做這件事。

費用。每輪大約 0.0006 到 0.0007 美元,包括改寫的那次呼叫,比 v1 的每輪 0.0001 到 0.0005 美元貴了一些,主要是多了約一千個詞元的文件。快取命中數在增長,因為 system 提示詞和對話歷史是固定的開頭,只有最後一條 user 訊息裡的文件每次不同。

v2 的問題

  • 文件沒寫的,它就答不了。預設重定向次數、Limits 的內部邏輯、某個異常到底在什麼情況下丟擲,這些答案都在原始碼裡。
  • 只能檢索一次。如果第一次檢索沒找對,它沒有機會換個檢索詞再試。
  • 不管什麼問題,都要先檢索。連天氣這種問題也不例外。

前兩個問題,需要讓 RepoBot 能自己決定"去查文件"、"去翻原始碼",並根據查到的結果決定下一步做什麼。這就是第 05 模組的智慧體。

這個專案要回答的幾個問題

  • 為什麼這樣設計? 查詢改寫 + BM25 + 向量 RRF,是在 20 道評估題上驗證過效果最好、又不需要額外算力的組合。重排提升很小,所以設為可選。
  • 會在哪裡失敗? 答案只在原始碼裡、不在文件裡的問題;需要綜合好幾篇文件的問題;檢索第一次沒找對的問題。
  • 怎麼評估? 檢索用 eval_retrieval.py,回答的質量用第 6 課的模型評委。
  • 出問題時看什麼? 開啟 --show-query,先看檢索詞對不對,再看檢索到的文件對不對,最後看模型有沒有正確使用文件。大部分問題出在前兩步。
  • 能不能更便宜? 可以在檢索前先判斷問題是否和 httpx 相關,無關的直接拒絕,省掉改寫、檢索和一千多個詞元的輸入。
  • 需要智慧體嗎? 到這一版為止還不需要,流程是固定的:改寫、檢索、回答。當它需要"檢索沒找到就換個方法"時,才需要。

練習

  1. 用 v1 第 5 課練習裡你自己出的 5 道題測一測 v2。有沒有哪道題 v1 答對了、v2 反而答不上來?
  2. QueryRewriter.rewrite 裡的 history 參數去掉(永遠傳空的歷史),再問一次"怎麼設定超時"和"那非同步客戶端呢",看第二個問題檢索到了什麼。
  3. repobot.py 裡,檢索之前先呼叫一次模型判斷"這個問題和 httpx 有沒有關係",無關就直接回復拒絕語,不做檢索。比較改動前後,問天氣那一輪的花費。

自測

1. 使用者問"那非同步客戶端呢?",直接拿這句話去檢索會有什麼問題?v2 是怎麼解決的?

這句話裡沒有"超時"之類的關鍵資訊,檢索只會找到講非同步的通用文件,找不到講非同步客戶端超時設定的內容。v2 在查詢改寫時帶上最近幾輪對話,讓模型先理解使用者實際在問"非同步客戶端怎麼設定超時",再生成完整的檢索詞。

2. 為什麼檢索到的文件只放在當前這一輪的訊息裡,不存進對話歷史?

每一輪的文件大約有一千個詞元,都存進歷史的話,對話越長曆史越大,又貴又容易干擾模型。模型的回答裡已經包含了從文件中得到的要點,存回答就夠了。

3. v1 憑記憶答對了"預設最多跟隨幾次重定向",v2 卻說文件裡沒有。這說明 v2 比 v1 差嗎?

不能這麼說。v2 只根據文件回答,文件裡確實沒有這個資訊,所以它如實說"沒有找到"。這是用"可能答不上來"換來"答出來的都有據可查"。v1 這次碰巧答對了,但它在別的問題上會很自信地答錯。要解決 v2 的這個問題,應該讓它能查到更多資料,比如原始碼,而不是放寬"只根據資料"的要求。

提問與討論

這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。

提問 +3 點,回答別人 +6 點。內容經審核後公開。

正在載入討論…