Projet : un assistant qui fouille le code source
Transformer RepoBot en agent – consulter d'abord la documentation, et si la réponse n'y est pas, fouiller le code source de httpx. Valeurs par défaut et logique des exceptions, sur lesquelles la v2 séchait, la v3 y répond juste, en indiquant fichier et numéro de ligne du code source.
- Environ 60 minutes
- Niveau : Intermédiaire
- Testé : 2026-09-14 deepseek-flash, multilingual-e5-small
Le code et les sorties des programmes sont reproduits tels qu’ils ont tourné : commentaires et sorties sont donc en chinois.
RepoBot v2 a une limite qu'il ne peut pas contourner : il ne lit que la documentation. « Combien de redirections httpx suit-il au maximum par défaut ? » : la documentation ne le dit pas, alors il ne peut que répondre « rien trouvé dans la documentation ». Pourtant, la réponse est bel et bien dans le code source de httpx, un grep suffit à la trouver.
Cette leçon transforme RepoBot en agent, qui décide lui-même s'il consulte d'abord la documentation ou le code source.
Critères d'achèvement
- À « combien de redirections httpx suit-il au maximum par défaut ? », la réponse est 20, avec la mention
[源码 httpx/_config.py:248](源码 = code source). - Pour une question dont la réponse est dans la documentation, il répond de préférence d'après la documentation, avec la mention
[文档 文件名](文档 = documentation, 文件名 = nom de fichier). - À une question sans rapport avec httpx, il refuse directement, sans appeler aucun outil.
- Un chemin comme
../passé àread_sourcepour sortir du dossier du code source est refusé. - Les 8 questions de
eval_agent.py(dont 5 dont la réponse n'est que dans le code source) reçoivent toutes une bonne réponse.
Structure
Le code se trouve dans projects/repobot/v3/ :
tools.py 四个工具:search_docs、read_doc、grep_source、read_source
agent.py 智能体循环(05 模块第 2 课的写法)
repobot.py 命令行对话程序
retrieval.py 检索,沿用 v2
llm.py 模型客户端、计费、重试,沿用 v2
eval_agent.py 8 道题的评估
Quatre outils
La recherche de la v2 est emballée dans un outil, auquel s'ajoutent trois nouveaux outils :
| Outil | Ce qu'il fait | Quand l'utiliser |
|---|---|---|
search_docs |
Recherche hybride dans la documentation, renvoie les 5 passages les plus pertinents | En premier pour les questions « comment utiliser » et « qu'est-ce que » |
read_doc |
Lit un fichier de documentation par lignes | Quand les passages trouvés ne sont pas assez complets |
grep_source |
Cherche un texte ou une expression régulière dans le code source de httpx | Quand la documentation ne contient pas la réponse |
read_source |
Lit un fichier de code source par lignes | Après que grep a trouvé le numéro de ligne |
Les descriptions sont écrites selon la méthode de la leçon 3, et chaque outil dit clairement quand l'utiliser. Par exemple grep_source :
@tool("在 httpx 的 Python 源码里搜索一段文本或正则表达式,返回文件路径、行号和那一行,最多 30 条。"
"文档里找不到答案时使用,比如某个参数的默认值、某个异常在什么情况下抛出、某个函数的内部逻辑。",
pattern="要搜索的文本或正则表达式,例如 DEFAULT_MAX_REDIRECTS 或 def raise_for_status")
def grep_source(pattern):
try:
regex = re.compile(pattern)
except re.error:
regex = re.compile(re.escape(pattern))
……
if not hits:
return f"源码里没有找到 {pattern},换个写法再试,比如只搜函数名或常量名"
Deux détails : l'expression régulière passée par le modèle peut être fautive ; si sa compilation échoue, l'outil se rabat sur une recherche de texte ordinaire au lieu de signaler une erreur ; et quand il ne trouve rien, le message renvoyé dit au modèle ce qu'il peut faire ensuite.
Au premier lancement, le code source est cloné automatiquement :
def ensure_source():
"""第一次运行时把 httpx 的源码克隆下来。"""
if not (SOURCE_DIR / "httpx").is_dir():
print(f"第一次运行,正在从 {SOURCE_REPO} 下载 httpx 源码……", flush=True)
SOURCE_DIR.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(["git", "clone", "--depth", "1", "--quiet", SOURCE_REPO, str(SOURCE_DIR)], check=True)
--depth 1 ne télécharge que la dernière version, sans tout l'historique, ce qui est bien plus rapide.
Les deux outils de lecture partagent une fonction de vérification, qui garantit que le modèle ne peut lire que les fichiers des dossiers prévus :
def inside(base, path):
"""把相对路径转成绝对路径,并确认它没有跑出 base 目录。跑出去了就返回 None。"""
target = (base / path).resolve()
return target if base in target.parents and target.is_file() else None
La leçon 8 l'a montré : chaque outil d'un agent est une porte d'entrée exploitable. RepoBot n'a besoin que de lire : ses quatre outils sont donc tous en lecture seule, et le périmètre de lecture est limité aux deux dossiers de documentation et de code source.
Le prompt : la documentation d'abord, le code source ensuite
SYSTEM = """你是 RepoBot,Python HTTP 客户端库 httpx 的答疑助手。你可以查 httpx 的官方文档和源码。
做法:
- 先用 search_docs 查文档。文档里有答案,就根据文档回答。
- 文档里没有答案(比如默认值、内部逻辑、某个异常什么时候抛出),再用 grep_source 和 read_source 查源码。
- 回答里注明依据:文档写成 [文档 文件名],源码写成 [源码 文件路径:行号]。
- 只根据查到的内容回答。查了还是找不到,就如实说没有找到,不要猜。
- 和 httpx 无关的问题,直接礼貌地说明你只负责 httpx,不要调用任何工具。
- 用中文回答,简洁,代码保持原样。"""
Pourquoi la documentation d'abord ? La documentation est écrite pour les utilisateurs et dit « comment il faut l'utiliser » ; le code source, ce sont des détails d'implémentation, qui peuvent contenir des comportements internes non garantis publiquement. Ce que la documentation peut expliquer, il ne faut pas aller le déterrer dans le code source. De plus, une recherche dans la documentation suffit généralement, alors que fouiller le code demande souvent plusieurs recherches et plusieurs lectures, ce qui coûte plus cher.
La règle « réponds en chinois » est la leçon tirée de la leçon 4 : après avoir lu beaucoup de matériau en anglais, un agent commence parfois sa réponse en anglais.
Exécution
cd projects/repobot/v3
pip install -r requirements.txt
export HF_ENDPOINT=https://hf-mirror.com
python repobot.py
Une question sur laquelle la v2 séchait, cette fois plus retorse : « Si l'on écrit seulement httpx.Limits(max_connections=200), que vaut max_keepalive_connections ? » La documentation dit seulement « (Defaults 20) », et l'on croit facilement que la réponse est 20.
你:只写 httpx.Limits(max_connections=200),max_keepalive_connections 是多少?
[1] search_docs({"query": "Limits max_connections max_keepalive_connections default"}) → [1] 文档 advanced/resource-limits.md
[1] grep_source({"pattern": "max_keepalive_connections"}) → httpx/_config.py:167: * **max_keepalive_connections** - Allow the conn
[2] read_source({"path": "httpx/_config.py", "start": 160, "end": 200}) → 160: """
RepoBot:**是 `None`**(即不限制 keep-alive 连接数)。
原因:`httpx.Limits(...)` 构造时 `max_keepalive_connections` 的参数默认值是 `None` [源码 httpx/_config.py:177],你不传它就保持 `None` [源码 httpx/_config.py:181]。
注意别把两个"默认"混淆:
- `Limits` 构造函数的默认值是 `None`
- 而 **客户端在没有传入 `limits` 时**使用的 `DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20)` [源码 httpx/_config.py:247],文档里说的 "(Defaults 20)" 指的是这个 [文档 advanced/resource-limits.md]
(后面的代码示例省略)
[3 次模型调用,3 次工具调用,本轮 0.00080 美元,累计 0.00080 美元]
Il a consulté à la fois la documentation et le code source, lu à l'étape 2 les lignes 160 à 200 de _config.py, puis distingué deux « valeurs par défaut » faciles à confondre. J'avais examiné ce code source à la leçon 5 du module 03 : dans le constructeur de Limits, max_keepalive_connections vaut None par défaut, et DEFAULT_LIMITS est à la ligne 247. La réponse est parfaitement juste.
Évaluation : 8 questions, trois exécutions
eval_agent.py contient 8 questions : pour 3, la réponse est dans la documentation ; pour 5, seulement dans le code source. Chaque question a une expression régulière ; si elle trouve une correspondance dans la réponse, celle-ci est jugée juste.
La première exécution a donné 8/8. Mais en relisant les réponses une par une, j'ai vu que pour la question Limits, il avait répondu « c'est 20, la valeur par défaut effective vient d'une constante au niveau du module », ce qui est faux. Il avait seulement mentionné None en passant dans l'explication, et mon expression régulière r"None" l'avait jugé « juste ».
C'est le problème évoqué à la leçon 6 du module 01 : le script de notation se trompe aussi. J'ai donc ajouté à chaque question une expression régulière « ne doit pas apparaître », pour bloquer le cas « mot-clé mentionné, conclusion fausse » :
QUESTIONS = [
# (问题, 必须出现, 不能出现, 答案在哪)
……
("只写 httpx.Limits(max_connections=200),max_keepalive_connections 是多少?", r"None", r"是\s*\**\s*`?20", "源码"),
]
Après correction, deux nouvelles exécutions d'affilée :
########## v3 评估第 1 次
答案在文档里的题:3/3 答对
答案在源码里的题:5/5 答对
平均每题 2.6 次模型调用,2.5 次工具调用,共 0.0067 美元
……
########## v3 评估第 2 次
答案在文档里的题:3/3 答对
答案在源码里的题:5/5 答对
平均每题 2.5 次模型调用,2.5 次工具调用,共 0.0063 美元
Ces deux fois, la question Limits a reçu une bonne réponse (« max_keepalive_connections vaudra None »). Sur trois exécutions, une question a été ratée une fois.
Cela montre deux choses. D'abord, les réponses d'un agent varient à chaque fois ; la même question est juste cette fois, fausse la prochaine, et une seule exécution de l'évaluation ne prouve pas grand-chose : il faut en faire plusieurs. Ensuite, plus les règles de notation automatique sont strictes, plus elles trouvent de vrais problèmes, mais plus elles risquent aussi de pénaliser des réponses justes. Ces deux points, le module suivant les traite de façon systématique.
Un piège d'ingénierie : threads et modèle local
Le script d'évaluation traite 4 questions à la fois avec un pool de threads. Un jour, j'ai lancé deux programmes d'évaluation en même temps ; l'un est resté bloqué plus de dix minutes sans produire une seule ligne de résultat, alors que l'utilisation du CPU approchait 400 %.
La cause était le modèle d'embedding local. Chaque thread l'appelle, et PyTorch lance lui-même plusieurs threads pour calculer. Plusieurs threads Python multipliés par les threads de PyTorch, multipliés par deux processus : le nombre de threads dépasse de loin le nombre de cœurs, tout le monde se dispute les ressources, et plus rien n'avance.
La solution : un verrou, pour qu'un seul thread à la fois appelle le modèle local :
_MODEL_LOCK = threading.Lock() # 同一时间只让一个线程调用本地的嵌入模型和重排模型
……
def vector_search(self, query, k):
with _MODEL_LOCK:
q = self.embedder.encode(["query: " + query], normalize_embeddings=True)[0]
Calculer le vecteur d'une question ne prend que quelques millisecondes ; faire la queue ne ralentit presque rien. L'attente des appels d'API au modèle, elle, reste parallèle.
Comparaison avec la v2
| v2 | v3 | |
|---|---|---|
| Déroulé | Fixe : réécriture, recherche, réponse | Décidé par l'agent |
| Sources consultables | Documentation | Documentation et code source |
| « Nombre maximal de redirections par défaut » | Rien trouvé dans la documentation | 20 [源码 httpx/_config.py:248] |
| Appels par question | 2 (réécriture + réponse) | Environ 2,5 en moyenne |
| Coût par question | Environ 0,0006 dollar | Environ 0,0008 dollar |
| Prévisibilité | Élevée | Faible, les étapes peuvent varier à chaque fois |
La v3 en fait plus, mais coûte plus cher et est moins prévisible. Dans notre évaluation, elle ne coûte qu'environ un tiers de plus que la v2, car la plupart des questions se règlent en deux ou trois étapes.
Les questions auxquelles ce projet doit répondre
- Pourquoi cette conception ? Les réponses absentes de la documentation sont dans le code source, et quand fouiller le code source, et où, ne peut pas être écrit à l'avance : d'où un agent. Tous les outils sont en lecture seule, limités à deux dossiers.
- Où va-t-il échouer ? La même question peut recevoir des réponses différentes à chaque fois ; l'agent peut trouver dans le code source un passage lié mais inexact et en tirer une conclusion fausse ; quand la question implique des relations d'appel entre plusieurs fichiers, il risque de ne pas tout lire.
- Comment évaluer ? Avec
eval_agent.py, exécuté plusieurs fois après chaque modification. La notation automatique est encore rudimentaire ; le module suivant l'améliore. - Que regarder en cas de problème ? Les appels d'outils de chaque étape sont affichés : on voit ce qu'il a cherché et quelles lignes il a lues. Le module suivant les enregistre dans un journal.
- Peut-on faire moins cher ? Oui : répondre d'abord avec le déroulé fixe de la v2, et ne lancer l'agent que si « rien n'a été trouvé dans la documentation » (la stratégie évoquée à la leçon 1).
- Faut-il vraiment un agent ? Pour les questions dont la réponse est dans le code source, oui. Pour la plupart des questions de documentation, en fait non ; c'est précisément ce qui fonde l'optimisation précédente.
Exercices
- Posez à la v3 une question qui demande de suivre la piste à travers plusieurs fichiers, par exemple « dans quelle fonction
httpx.getenvoie-t-il finalement réellement la requête réseau ? », et voyez jusqu'où il va et si sa conclusion est juste. - Implémentez la stratégie mixte évoquée à la leçon 1 : suivre d'abord le déroulé de la v2, et quand la réponse contient « rien trouvé dans la documentation », lancer l'agent de la v3. Comparez le coût total des 8 questions d'évaluation.
- Ajoutez à
eval_agent.pyun paramètre qui exécute chaque question 3 fois et compte les bonnes réponses par question. Quelles questions sont « justes de façon stable », lesquelles « tantôt justes, tantôt fausses » ?
Auto-test
1. Pourquoi faire consulter à RepoBot la documentation d'abord et le code source ensuite, plutôt que directement le code source ?
La documentation décrit l'usage garanti aux utilisateurs ; le code source contient beaucoup de détails d'implémentation internes qui ne sont pas forcément un comportement public stable. De plus, une recherche dans la documentation suffit généralement, alors que le code source demande plusieurs recherches et lectures, plus chères et plus lentes. Ce n'est que lorsque la documentation ne contient pas la réponse qu'il faut aller chercher dans le code source.
2. Le script d'évaluation affiche 8/8. Pourquoi relire quand même les réponses une par une ?
La notation automatique peut se tromper. Lors de la première exécution de cette leçon, la conclusion d'une question était fausse, mais le mot-clé figurait par hasard dans l'explication, et elle a été jugée juste. Seule une relecture humaine permet de savoir si les règles de notation sont fiables et de les améliorer, par exemple en ajoutant une condition « ne doit pas apparaître ».
3. Pourquoi plusieurs threads appelant en même temps le modèle d'embedding local ralentissent-ils au point de presque bloquer ? Comment le résoudre ?
PyTorch lance lui-même plusieurs threads à chaque calcul. Quand plusieurs threads Python appellent le modèle en même temps, le nombre de threads se multiplie, dépasse de loin le nombre de cœurs, et tous se disputent les ressources. La solution est un verrou, pour qu'un seul thread à la fois appelle le modèle local. Un calcul isolé est rapide ; faire la queue ne ralentit presque rien.
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…