Projet : l'assistant lit la documentation du projet
Intégrer découpage, recherche hybride, réécriture de requête et réponses avec citations dans RepoBot pour produire la v2. Elle répond juste aux questions sur lesquelles la v1 se trompait, et règle aussi le problème de recherche des relances du type « et en asynchrone ? » dans une conversation à plusieurs tours.
- Environ 60 minutes
- Niveau : Intermédiaire
- Testé : 2026-09-14 deepseek-flash, multilingual-e5-small, bge-reranker-base
Le code et les sorties des programmes sont reproduits tels qu’ils ont tourné : commentaires et sorties sont donc en chinois.
Cette leçon intègre tout le module 04 dans RepoBot pour en faire la deuxième version. Vous verrez un problème jamais rencontré en question-réponse isolée : dans une conversation à plusieurs tours, la relance de l'utilisateur, prise seule, ne permet souvent de rien trouver.
Critères d'achèvement
- À « httpx suit-il automatiquement les redirections par défaut ? », la réponse est « non », avec la source
compatibility.md. - Chaque réponse indique ses sources par des [numéros] et liste à la fin les documents réellement cités.
- Pour un contenu absent de la documentation (par exemple HTTP/3), il répond « 文档里没有找到相关说明 » (rien de pertinent n'a été trouvé dans la documentation), sans inventer.
- En posant à la suite « comment définir le délai d'expiration de httpx ? » puis « et pour le client asynchrone ? », la seconde question trouve la documentation sur les délais.
- Au deuxième démarrage, les vecteurs de la documentation ne sont pas recalculés.
- Sur les 20 questions d'évaluation,
eval_retrieval.pyatteint 100 % de réussite dans les 5 premiers.
Structure
Le code se trouve dans projects/repobot/v2/, avec deux fichiers de plus que la v1 :
llm.py 客户端、计费、带重试的请求(从 v1 的 repobot.py 里挪出来)
retrieval.py 切分、BM25、向量检索、RRF、重排、查询改写(04 模块第 2~4 课)
repobot.py 对话程序:在 v1 的基础上加上检索和引用
eval_retrieval.py 用 20 道题评估检索效果(04 模块第 3 课)
Le code de retrieval.py a été expliqué morceau par morceau dans les leçons précédentes ; seuls les trois nouveaux problèmes rencontrés à l'assemblage sont traités ici.
Problème 1 : les relances ne trouvent rien
Dans le code d'exercice du module 04, chaque question était indépendante. Mais dans une conversation, l'utilisateur demande ceci :
你:怎么给 httpx 设置 10 秒的超时?
你:那异步客户端呢?
Cherchée telle quelle, la seconde question « 那异步客户端呢 » (et pour le client asynchrone ?) trouve la documentation sur l'asynchrone, mais pas celle sur les délais, car le mot « 超时 » (délai d'expiration) n'y figure même pas.
La v2 résout cela en ajoutant la conversation récente à l'étape de réécriture : le modèle comprend d'abord ce que l'utilisateur demande vraiment, puis génère les termes de recherche :
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}"},
])
Seuls les 4 derniers messages sont pris, 300 caractères au plus chacun : assez pour comprendre les références, sans rendre cette étape trop chère. Le résultat de l'exécution ci-dessous en montre l'effet.
Problème 2 : que stocker dans l'historique
À chaque tour, les 5 passages trouvés vont dans le prompt, environ mille tokens. Si on les stockait aussi dans l'historique, au bout de dix tours celui-ci contiendrait des dizaines de milliers de tokens de vieux documents, chers et susceptibles de distraire le modèle.
La v2 ne met les documents que dans le message user de ce tour ; l'historique ne stocke que la question d'origine de l'utilisateur et la réponse du modèle :
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}]
La réponse du modèle contient déjà l'essentiel tiré des documents ; si la suite de la conversation en a besoin, on le retrouve dans la réponse.
Problème 3 : ne pas recalculer les vecteurs à chaque démarrage
196 morceaux de documentation à faire passer par le modèle d'embedding à chaque démarrage, cela prend plus de dix secondes. Si la documentation ne change pas, le résultat est toujours le même : on peut le mettre en cache :
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)
Le nom du fichier de cache vient d'une empreinte de « nom du modèle d'embedding + contenu de tous les morceaux ». Qu'un seul caractère de la documentation change, ou que le modèle d'embedding change, et l'empreinte change aussi : tout est recalculé, sans risque d'utiliser des vecteurs périmés. Les résultats de réécriture sont eux aussi mis en cache dans .cache/rewrites.json.
Efficacité de la recherche
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
Sans reranking, le MRR vaut 0,892, bien au-dessus du 0,772 de « réécriture + hybride RRF » de la leçon 4. L'algorithme de recherche est identique ; la seule différence est le nouveau prompt de réécriture ci-dessus : « produis une ligne de termes de recherche en anglais : la formulation anglaise complète de la question, et… ». La version de la leçon 4 disait « la traduction anglaise de la question, et… », sans contexte de conversation.
Cela montre encore que le prompt de réécriture influence beaucoup la recherche. C'est aussi pourquoi il faut relancer l'évaluation à chaque modification : on croit n'avoir changé qu'une phrase du prompt en passant, et les indicateurs bougent.
Sur cette version, le reranking ne fait passer le MRR que de 0,892 à 0,902, et la réussite dans les 3 premiers descend même de 100 % à 95 %. Une seconde de plus pour un gain aussi faible : la v2 désactive donc le reranking par défaut, à activer avec --rerank si besoin.
Une conversation
printf '%s\n' "httpx 默认会自动跟随重定向吗?" "那默认最多跟随几次?" "怎么给 httpx 设置 10 秒的超时?" \
"那异步客户端呢?" "httpx 支持 HTTP/3 吗?" "今天北京天气怎么样?" | python repobot.py --show-query
--show-query affiche à chaque tour les termes de recherche et les documents trouvés. Mon résultat (quelques exemples de code supprimés, rien d'autre de modifié) :
你: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 美元]
Tour par tour
Redirections : juste. La v1 se trompait ici, en inventant un historique de versions. La v2 répond « non », avec deux sources.
Les relances sont bien comprises. « 那默认最多跟随几次? » (et combien au maximum par défaut ?) devient httpx follow_redirects max_redirects default maximum number of redirects ; « 那异步客户端呢? » (et pour le client asynchrone ?) devient asynchronous client timeout configuration httpx.AsyncClient(timeout=10.0), et les documents trouvés sont tous timeouts.md. Sans le contexte de conversation dans la réécriture, la seconde question ne trouverait que la documentation sur l'asynchrone.
« Combien de redirections au maximum par défaut » : il dit que la documentation n'en parle pas. C'est la bonne réponse : la documentation de httpx ne donne effectivement pas cette valeur par défaut, qui n'existe que dans le code source (à la leçon 5 du module 03, nous l'avons trouvée dans httpx/_config.py : 20). Détail intéressant : la v1 avait répondu juste de mémoire à cette question. La v2, parce qu'elle exige strictement de « répondre uniquement d'après les documents », ne sait plus répondre. C'est le prix évoqué à la leçon 5 : ce que les documents ne disent pas, le modèle ne peut pas le dire, même s'il le sait.
Client asynchrone : une réponse bien mesurée. Les exemples de la documentation utilisent tous httpx.Client ; il le dit honnêtement, tout en indiquant que les mêmes paramètres s'appliquent. C'est exactement ce qu'on veut : ne pas prétendre que la documentation contient ce qu'elle ne contient pas.
HTTP/3 : pas d'invention.
La météo : refusée, mais de l'argent gaspillé. Il a quand même fait une réécriture et une recherche, et mis dans le prompt 5 passages sans le moindre rapport. Mieux vaudrait juger, avant la recherche, si la question a un rapport avec httpx, et refuser directement sinon. Les « garde-fous » de la leçon 5 du module 06 feront cela.
Coût. Environ 0,0006 à 0,0007 dollar par tour, réécriture comprise, un peu plus que les 0,0001 à 0,0005 dollar de la v1, surtout à cause d'environ mille tokens de documents en plus. Les tokens servis depuis le cache augmentent, car le prompt system et l'historique forment un début fixe ; seuls les documents du dernier message user changent à chaque fois.
Les défauts de la v2
- Ce que la documentation ne dit pas, il ne sait pas y répondre. Le nombre de redirections par défaut, la logique interne de
Limits, les conditions exactes dans lesquelles telle exception est levée : ces réponses sont dans le code source. - Il ne cherche qu'une fois. Si la première recherche n'a pas trouvé juste, il n'a pas l'occasion de réessayer avec d'autres termes.
- Quelle que soit la question, il cherche d'abord. Même pour la météo.
Les deux premiers défauts demandent que RepoBot puisse décider lui-même de « consulter la documentation » ou de « fouiller le code source », et choisir l'étape suivante selon ce qu'il trouve. Ce sont les agents du module 05.
Les questions auxquelles ce projet doit répondre
- Pourquoi cette conception ? Réécriture de requête + BM25 + RRF vectoriel est la combinaison validée comme la plus efficace sur les 20 questions d'évaluation, sans puissance de calcul supplémentaire. Le reranking apporte peu, il est donc optionnel.
- Où va-t-elle échouer ? Questions dont la réponse n'est que dans le code source et pas dans la documentation ; questions qui demandent de combiner plusieurs documents ; questions dont la première recherche n'a pas trouvé juste.
- Comment évaluer ? La recherche avec
eval_retrieval.py, la qualité des réponses avec le modèle juge de la leçon 6. - Que regarder en cas de problème ? Activer
--show-query, vérifier d'abord les termes de recherche, puis les documents trouvés, enfin si le modèle a bien utilisé les documents. La plupart des problèmes se situent aux deux premières étapes. - Peut-on faire moins cher ? Oui : juger avant la recherche si la question concerne httpx, et refuser directement sinon, en économisant réécriture, recherche et plus de mille tokens d'entrée.
- Faut-il un agent ? Pas jusqu'à cette version : le déroulé est fixe (réécrire, chercher, répondre). Il en faudra un quand il devra « changer de méthode si la recherche n'a rien trouvé ».
Exercices
- Testez la v2 avec les 5 questions que vous avez écrites dans l'exercice de la leçon 5 sur la v1. Y en a-t-il une à laquelle la v1 répondait juste et que la v2 ne sait plus traiter ?
- Retirez le paramètre
historydeQueryRewriter.rewrite(en passant toujours un historique vide), reposez « comment définir le délai d'expiration ? » puis « et pour le client asynchrone ? », et voyez ce que trouve la seconde question. - Dans
repobot.py, avant la recherche, appelez une fois le modèle pour juger « cette question a-t-elle un rapport avec httpx ? » ; si non, répondez directement par un refus, sans recherche. Comparez le coût du tour sur la météo avant et après la modification.
Auto-test
1. L'utilisateur demande « et pour le client asynchrone ? ». Quel problème pose une recherche avec cette seule phrase ? Comment la v2 le résout-elle ?
La phrase ne contient aucune information clé comme « délai d'expiration » ; la recherche ne trouve que la documentation générale sur l'asynchrone, pas le réglage du délai du client asynchrone. La v2 ajoute les derniers tours de conversation à la réécriture de requête : le modèle comprend d'abord que l'utilisateur demande en fait « comment définir le délai du client asynchrone », puis génère des termes de recherche complets.
2. Pourquoi les documents trouvés ne sont-ils placés que dans le message du tour courant, et pas stockés dans l'historique ?
Chaque tour apporte environ mille tokens de documents ; en les stockant tous dans l'historique, plus la conversation s'allonge, plus l'historique grossit, ce qui coûte cher et risque de distraire le modèle. La réponse du modèle contient déjà l'essentiel tiré des documents ; stocker la réponse suffit.
3. La v1 a répondu juste de mémoire au « nombre maximal de redirections par défaut », tandis que la v2 dit que la documentation n'en parle pas. La v2 est-elle moins bonne que la v1 ?
Non. La v2 ne répond que d'après la documentation, qui ne contient effectivement pas cette information ; elle dit donc honnêtement « rien trouvé ». On échange « risque de ne pas savoir répondre » contre « tout ce qu'on répond est vérifiable ». La v1 a eu raison par hasard cette fois, mais elle se trompe avec assurance sur d'autres questions. Pour résoudre ce défaut de la v2, il faut lui donner accès à plus de sources, comme le code source, plutôt que d'assouplir l'exigence « uniquement d'après les documents ».
Questions et discussion
Bloqué sur cette leçon ? Posez votre question ici. Et si vous pouvez répondre à quelqu'un, n'hésitez pas.
Une question rapporte 3 points, une réponse 6. Les messages paraissent après vérification.
Chargement de la discussion…