プロジェクト:アシスタントにプロジェクトのドキュメントを読ませる
分割、ハイブリッド検索、クエリの書き換え、引用つきの回答を RepoBot に組み込み、v2 を作ります。v1 が間違えた質問に正しく答え、マルチターン会話での「じゃあ非同期は?」のような追加の質問の検索問題も解決します。
- 約 60 分
- 難易度:中級
- 検証:2026-09-14 deepseek-flash、multilingual-e5-small、bge-reranker-base
コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。
この課では、モジュール 04 のすべてを RepoBot に組み込んで、第 2 版を作ります。1 回きりの質疑応答では出会わなかった問題にぶつかります。マルチターン会話では、ユーザーの追加の質問は、それ単独で検索しても何も見つからないことが多いのです。
完成の目安
- 「httpx は既定でリダイレクトに自動で従いますか?」と聞くと、答えは「従わない」で、出典として
compatibility.mdを示す。 - どの回答も [番号] で出典を明記し、最後に実際に引用したドキュメントを並べる。
- ドキュメントにない内容(たとえば HTTP/3)を聞くと、「ドキュメントに関連する説明は見つかりませんでした」と答え、でっち上げない。
- 「httpx でタイムアウトを設定するには?」に続けて「じゃあ非同期クライアントは?」と聞くと、2 問目でタイムアウト関連のドキュメントが検索される。
- 2 回目の起動では、ドキュメントのベクトルを計算し直さない。
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 秒的超时?
你:那异步客户端呢?
2 問目をそのまま検索すると、「じゃあ非同期クライアントは?」では非同期についてのドキュメントは見つかっても、タイムアウトについてのドキュメントは見つかりません。質問に「タイムアウト」という言葉がまったく入っていないからです。
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 つの断片をプロンプトに入れるので、約 1000 トークンになります。これも会話の履歴に保存すると、10 ターン後には履歴に 1 万トークンを超える古いドキュメントがたまり、高くつくうえにモデルを惑わせやすくなります。
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 よりかなり高くなっています。検索のアルゴリズムはまったく同じで、唯一の違いは書き換えのプロンプトを上の新しい版に替えたことです。「英語の検索語を 1 行出力する:質問の完全な英語での表現と……を含める」。第 4 課の版は「質問の英訳と……を含める」で、会話の文脈もありませんでした。
これはまたしても、クエリの書き換えのプロンプトが検索の効果に大きく影響することを示しています。どこか一か所を変えるたびに評価を実行し直す理由もこれです。プロンプトを一文ついでに直しただけのつもりでも、指標が変わっているかもしれません。
この版では、リランキングで MRR は 0.892 から 0.902 にしか上がらず、上位 3 件のヒット率はかえって 100% から 95% に下がりました。1 秒を余分に費やして得られる改善はわずかなので、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 でした。会話の文脈を含めない書き換えなら、2 問目では非同期についてのドキュメントしか見つからなかったでしょう。
「既定では最大何回まで従うか」、ドキュメントにないと答えた。この回答は正しいのです。httpx のドキュメントにはこの既定値が確かに書かれておらず、ソースコードにしかありません(モジュール 03 第 5 課で httpx/_config.py を調べて 20 だとわかりました)。面白いことに、v1 はこの問題に記憶で正解していました。v2 は「資料だけに基づいて答える」ことを厳しく求められているので、かえって答えられないのです。これは第 5 課で述べた代償です。資料に書かれていないことは、モデルが知っていても言えません。
非同期クライアント、節度のある回答。ドキュメントの例はどれも httpx.Client なので、そのことをありのままに説明しつつ、同じパラメータが使えると指摘しています。これこそ望んでいたことです。ドキュメントにないものをあるかのように装わないのです。
HTTP/3、でっち上げなし。
天気、断ったが、お金を無駄にした。それでも書き換えと検索を 1 回ずつ行い、まったく無関係なドキュメントを 5 つプロンプトに詰め込みました。検索の前に、質問が httpx と関係あるかを判断し、関係なければそのまま断るほうがよいでしょう。モジュール 06 第 5 課の「ガードレール」がこれを行います。
費用。1 ターンあたり約 0.0006~0.0007 ドルで、書き換えの呼び出しを含みます。v1 の 1 ターン 0.0001~0.0005 ドルより少し高く、主な理由は約 1000 トークンのドキュメントが増えたことです。キャッシュヒット数は増えていきます。system プロンプトと会話の履歴が固定の冒頭になっていて、毎回違うのは最後の user メッセージの中のドキュメントだけだからです。
v2 の問題
- ドキュメントに書かれていないことには答えられない。既定のリダイレクト回数、
Limitsの内部ロジック、ある例外がどんな状況で投げられるのか。これらの答えはソースコードにあります。 - 検索が 1 回しかできない。最初の検索で正しく見つからなければ、検索語を変えてもう一度試す機会がありません。
- どんな質問でも、まず検索する。天気のような質問でも例外ではありません。
最初の二つの問題を解決するには、RepoBot が「ドキュメントを調べる」「ソースコードを調べる」を自分で決め、調べた結果に応じて次に何をするかを決められるようにする必要があります。それがモジュール 05 のエージェントです。
このプロジェクトで答えるべき問い
- なぜこの設計にしたのか? クエリの書き換え + BM25 + ベクトルの RRF は、20 問の評価問題で最もよい効果が確かめられ、追加の計算資源も要らない組み合わせです。リランキングの改善はわずかなので、オプションにしました。
- どこで失敗するのか? 答えがドキュメントではなくソースコードにしかない質問、複数のドキュメントを総合する必要がある質問、最初の検索で正しく見つからなかった質問。
- どうやって評価するのか? 検索は
eval_retrieval.pyで、回答の質は第 6 課の LLM 評価者で。 - 問題が起きたら何を見るのか?
--show-queryをオンにして、まず検索語が正しいか、次に検索されたドキュメントが正しいか、最後にモデルがドキュメントを正しく使っているかを見ます。問題の大部分は最初の二つのステップにあります。 - もっと安くできるか? 検索の前に質問が httpx と関係あるかを判断し、関係なければそのまま断れば、書き換え、検索、1000 トークンを超える入力を省けます。
- エージェントは必要か? この版まではまだ不要です。流れは固定で、書き換え、検索、回答です。「検索で見つからなければ別の方法を試す」必要が出てきたとき、初めて必要になります。
練習問題
- v1 の第 5 課の練習で自分で作った 5 問で v2 をテストしてください。v1 は正解したのに、v2 はかえって答えられない問題はありますか。
QueryRewriter.rewriteからhistory引数を取り除き(常に空の履歴を渡す)、「タイムアウトを設定するには」と「じゃあ非同期クライアントは?」をもう一度聞いて、2 問目で何が検索されるか見てください。repobot.pyで、検索の前にモデルを 1 回呼んで「この質問は httpx と関係があるか」を判断させ、関係なければ検索せずにそのまま断りの言葉を返すようにしてください。変更の前後で、天気を聞いたターンの費用を比べます。
確認テスト
1. ユーザーが「じゃあ非同期クライアントは?」と聞いたとき、この文をそのまま検索するとどんな問題がありますか?v2 はどう解決していますか?
この文には「タイムアウト」のような重要な情報が含まれていないので、検索では非同期についての一般的なドキュメントしか見つからず、非同期クライアントのタイムアウト設定についての内容は見つかりません。v2 はクエリの書き換えのときに最近の数ターンの会話を含め、モデルにまずユーザーが実際には「非同期クライアントでタイムアウトをどう設定するか」を聞いていると理解させてから、完全な検索語を作らせます。
2. 検索したドキュメントを今のターンのメッセージにだけ入れ、会話の履歴に保存しないのはなぜですか?
毎ターンのドキュメントは約 1000 トークンあり、すべて履歴に保存すると会話が長くなるほど履歴が大きくなり、高くつくうえにモデルを惑わせやすくなります。モデルの回答にはドキュメントから得た要点がすでに含まれているので、回答を保存すれば十分です。
3. v1 は「既定では最大何回までリダイレクトに従うか」に記憶で正解したのに、v2 はドキュメントにないと言いました。これは v2 が v1 より劣っているということですか?
そうは言えません。v2 はドキュメントだけに基づいて答え、ドキュメントには確かにこの情報がないので、「見つからなかった」と正直に言っています。これは「答えられないかもしれない」ことと引き換えに、「答えたことはすべて根拠をたどれる」ことを得ているのです。v1 は今回たまたま正解しましたが、別の質問では自信満々に間違えます。v2 のこの問題を解決するには、「資料だけに基づく」という要求を緩めるのではなく、ソースコードのようにより多くの資料を調べられるようにすべきです。
質問と議論
このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。
質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。
議論を読み込んでいます…