Modul 05 · Lektion 9

Projekt: ein Frage-Antwort-Assistent, der im Quellcode sucht

RepoBot wird zu einem Agenten – erst in der Dokumentation nachschlagen, und steht es dort nicht, im Quellcode von httpx suchen. Standardwerte und Ausnahmelogik, an denen v2 scheiterte, beantwortet v3 richtig, mit Datei und Zeilennummer im Quellcode.

  • Etwa 60 Minuten
  • Niveau: Fortgeschritten
  • Getestet: 2026-09-14 deepseek-flash, multilingual-e5-small

Code und Programmausgaben stehen genau so da, wie sie gelaufen sind – Kommentare und Ausgaben sind daher auf Chinesisch.

RepoBot v2 hat eine Grenze, an der es nicht vorbeikommt: Es liest nur Dokumentation. „Wie oft verfolgt httpx standardmäßig höchstens Weiterleitungen?“ steht nicht in der Dokumentation, also kann es nur sagen „in der Dokumentation nichts gefunden“. Dabei steht die Antwort im Quellcode von httpx, ein grep genügt.

Diese Lektion macht RepoBot zu einem Agenten, der selbst entscheidet, ob er zuerst in der Dokumentation oder im Quellcode sucht.

Wann es fertig ist

  • Auf „Wie oft verfolgt httpx standardmäßig höchstens Weiterleitungen?“ lautet die Antwort 20, mit Angabe [源码 httpx/_config.py:248] (源码 = Quellcode).
  • Bei Fragen, deren Antwort in der Dokumentation steht, antwortet es bevorzugt aus der Dokumentation, mit Angabe [文档 文件名] (文档 = Dokumentation, 文件名 = Dateiname).
  • Fragen ohne Bezug zu httpx lehnt es direkt ab, ohne ein Tool aufzurufen.
  • Übergibt man read_source einen Pfad wie ../, der das Quellverzeichnis verlässt, wird er abgelehnt.
  • Alle 8 Fragen von eval_agent.py (bei 5 steht die Antwort nur im Quellcode) werden richtig beantwortet.

Aufbau

Code in 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 道题的评估

Vier Tools

Die Suche aus v2 wird als Tool verpackt, dazu kommen drei neue Tools:

Tool Was es tut Wann man es nutzt
search_docs Hybride Suche in der Dokumentation, liefert die 5 relevantesten Abschnitte Bei Fragen „wie benutzt man“ und „was ist“ zuerst
read_doc Liest Dokumentationsdateien zeilenweise Wenn die gefundenen Abschnitte nicht vollständig genug sind
grep_source Sucht Text oder reguläre Ausdrücke im Quellcode von httpx Wenn die Dokumentation die Antwort nicht enthält
read_source Liest Quelldateien zeilenweise Nachdem grep die Zeilennummer gefunden hat

Die Beschreibungen sind nach der Methode aus Lektion 3 geschrieben, und jedes Tool sagt klar, wann es zu nutzen ist. Am Beispiel 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},换个写法再试,比如只搜函数名或常量名"

Zwei Details: Der vom Modell übergebene reguläre Ausdruck kann fehlerhaft sein; scheitert das Kompilieren, fällt das Tool auf eine normale Textsuche zurück, statt einen Fehler zu melden. Und findet es nichts, sagt die Rückgabe dem Modell, was es als Nächstes tun kann.

Beim ersten Lauf wird der Quellcode automatisch geklont:

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 lädt nur die neueste Version herunter, nicht die ganze Geschichte, das geht viel schneller.

Die beiden Lese-Tools teilen eine Prüffunktion, die sicherstellt, dass das Modell nur Dateien in den vorgesehenen Verzeichnissen lesen kann:

def inside(base, path):
    """把相对路径转成绝对路径,并确认它没有跑出 base 目录。跑出去了就返回 None。"""
    target = (base / path).resolve()
    return target if base in target.parents and target.is_file() else None

Lektion 8 hat gezeigt: Jedes Tool eines Agenten ist ein möglicher Angriffspunkt. RepoBot muss nur lesen, also sind alle vier Tools reine Lese-Tools, und auch der Lesebereich ist auf die zwei Verzeichnisse Dokumentation und Quellcode beschränkt.

Der Prompt: erst Dokumentation, dann Quellcode

SYSTEM = """你是 RepoBot,Python HTTP 客户端库 httpx 的答疑助手。你可以查 httpx 的官方文档和源码。

做法:
- 先用 search_docs 查文档。文档里有答案,就根据文档回答。
- 文档里没有答案(比如默认值、内部逻辑、某个异常什么时候抛出),再用 grep_source 和 read_source 查源码。
- 回答里注明依据:文档写成 [文档 文件名],源码写成 [源码 文件路径:行号]。
- 只根据查到的内容回答。查了还是找不到,就如实说没有找到,不要猜。
- 和 httpx 无关的问题,直接礼貌地说明你只负责 httpx,不要调用任何工具。
- 用中文回答,简洁,代码保持原样。"""

Warum zuerst die Dokumentation? Dokumentation ist für Nutzer geschrieben und beschreibt, „wie man es benutzen soll“; Quellcode sind Implementierungsdetails und kann internes Verhalten enthalten, das nach außen nicht zugesichert ist. Was die Dokumentation beantworten kann, soll man nicht aus dem Quellcode ausgraben. Außerdem genügt in der Dokumentation meist eine Suche, während man im Quellcode oft mehrmals suchen und mehrere Abschnitte lesen muss, was teurer ist.

Die Regel „auf Chinesisch antworten“ ist die Lehre aus Lektion 4: Nachdem ein Agent viel englisches Material gelesen hat, beginnt die Antwort manchmal auf Englisch.

Ausführen

cd projects/repobot/v3
pip install -r requirements.txt
export HF_ENDPOINT=https://hf-mirror.com
python repobot.py

Eine Frage, an der v2 scheiterte, diesmal etwas kniffliger: „Wenn man nur httpx.Limits(max_connections=200) schreibt, welchen Wert hat max_keepalive_connections?“ Die Dokumentation sagt nur „(Defaults 20)“, und man hält die Antwort leicht für 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 美元]

Es suchte zugleich in Dokumentation und Quellcode, las in Schritt 2 die Zeilen 160 bis 200 von _config.py und unterschied dann zwei leicht verwechselbare „Standardwerte“. Ich habe diesen Quellcode in Modul 03, Lektion 5 nachgesehen: Im Konstruktor von Limits ist max_keepalive_connections standardmäßig None, DEFAULT_LIMITS steht in Zeile 247. Die Antwort ist völlig richtig.

Evaluation: 8 Fragen, dreimal ausgeführt

eval_agent.py hat 8 Fragen; bei 3 steht die Antwort in der Dokumentation, bei 5 nur im Quellcode. Zu jeder Frage gibt es einen regulären Ausdruck; passt er auf die Antwort, gilt sie als richtig.

Der erste Lauf ergab 8/8 richtig. Aber ich habe die Antworten einzeln gelesen und festgestellt, dass es bei der Limits-Frage antwortete: „Es ist 20, der tatsächlich wirksame Standard kommt aus einer Konstante auf Modulebene.“ Das ist falsch. Es hatte None nur nebenbei in der Erklärung erwähnt, und mein regulärer Ausdruck r"None" wertete das als „richtig“.

Das ist das Problem aus Modul 01, Lektion 6: Auch Bewertungsskripte irren sich. Also bekam jede Frage zusätzlich einen regulären Ausdruck für „darf nicht vorkommen“, um den Fall „Stichwort erwähnt, Aussage aber falsch“ abzufangen:

QUESTIONS = [
    # (问题, 必须出现, 不能出现, 答案在哪)
    ……
    ("只写 httpx.Limits(max_connections=200),max_keepalive_connections 是多少?", r"None", r"是\s*\**\s*`?20", "源码"),
]

Nach der Änderung lief es noch zweimal hintereinander:

########## v3 评估第 1 次
答案在文档里的题:3/3 答对
答案在源码里的题:5/5 答对
平均每题 2.6 次模型调用,2.5 次工具调用,共 0.0067 美元
……
########## v3 评估第 2 次
答案在文档里的题:3/3 答对
答案在源码里的题:5/5 答对
平均每题 2.5 次模型调用,2.5 次工具调用,共 0.0063 美元

In beiden Läufen war die Limits-Frage richtig („max_keepalive_connections ist dann None“). In drei Läufen war einmal eine Frage falsch.

Das zeigt zweierlei. Erstens: Die Antworten eines Agenten sind jedes Mal anders; dieselbe Frage ist diesmal richtig und nächstes Mal falsch, ein einzelner Evaluationslauf sagt wenig aus, man muss mehrmals laufen lassen. Zweitens: Je strenger die Regeln der automatischen Bewertung, desto eher findet sie echte Probleme, desto eher trifft sie aber auch richtige Antworten. Beides löst das nächste Modul systematisch.

Eine technische Falle: Threads und lokale Modelle

Das Evaluationsskript bearbeitet mit einem Thread-Pool 4 Fragen gleichzeitig. Einmal startete ich zwei Evaluationsprogramme gleichzeitig; eines hing über zehn Minuten fest, gab keine einzige Ergebniszeile aus und lastete die CPU mit fast 400 % aus.

Die Ursache war das lokale Embedding-Modell. Jeder Thread ruft es auf, und PyTorch startet beim Rechnen selbst mehrere Threads. Mehrere Python-Threads mal PyTorch-Threads mal zwei Prozesse ergeben weit mehr Threads als CPU-Kerne; alle konkurrieren, und keiner kommt voran.

Die Lösung ist eine Sperre, sodass immer nur ein Thread das lokale Modell aufruft:

_MODEL_LOCK = threading.Lock()  # 同一时间只让一个线程调用本地的嵌入模型和重排模型
……
    def vector_search(self, query, k):
        with _MODEL_LOCK:
            q = self.embedder.encode(["query: " + query], normalize_embeddings=True)[0]

Einen Vektor für eine Frage zu berechnen dauert nur wenige Millisekunden; das Anstehen bremst kaum. Die Wartezeit auf die Modell-API kann weiterhin parallel laufen.

Vergleich mit v2

v2 v3
Ablauf Fest: umformulieren, suchen, antworten Der Agent entscheidet selbst
Durchsuchbares Material Dokumentation Dokumentation und Quellcode
„Standardmäßig höchstens wie viele Weiterleitungen“ In der Dokumentation nichts gefunden 20 [源码 httpx/_config.py:248]
Aufrufe pro Frage 2 (umformulieren + antworten) im Schnitt etwa 2,5
Kosten pro Frage etwa 0,0006 US-Dollar etwa 0,0008 US-Dollar
Vorhersagbarkeit Hoch Niedrig, die Schritte können jedes Mal anders sein

v3 kann mehr, ist aber auch teurer und weniger vorhersagbar. In unserer Evaluation kostet es nur etwa ein Drittel mehr als v2, weil die meisten Fragen in zwei, drei Schritten gelöst sind.

Fragen, die dieses Projekt beantworten muss

  • Warum dieser Entwurf? Antworten, die nicht in der Dokumentation stehen, stehen im Quellcode; wann und wo man im Quellcode suchen muss, lässt sich nicht vorab festschreiben, also ein Agent. Alle Tools sind reine Lese-Tools, der Bereich ist auf zwei Verzeichnisse begrenzt.
  • Wo kann es scheitern? Dieselbe Frage kann jedes Mal anders beantwortet werden; der Agent kann im Quellcode einen verwandten, aber falschen Codeabschnitt finden und daraus eine falsche Aussage ableiten; betrifft eine Frage Aufrufbeziehungen über mehrere Dateien, liest er sie vielleicht nicht vollständig.
  • Wie wird evaluiert? Mit eval_agent.py, nach jeder Änderung mehrmals ausführen. Die automatische Bewertung ist noch grob; das nächste Modul verbessert sie.
  • Was sieht man sich bei Problemen an? Die Tool-Aufrufe jedes Schritts werden ausgegeben; man sieht, wonach gesucht und welche Zeilen gelesen wurden. Das nächste Modul zeichnet das als Log auf.
  • Geht es billiger? Ja: zuerst mit dem festen Ablauf von v2 antworten und den Agenten nur starten, wenn „in der Dokumentation nichts gefunden“ herauskommt (die Strategie aus Lektion 1).
  • Braucht es wirklich einen Agenten? Für Fragen, deren Antwort im Quellcode steht, ja. Für die meisten Dokumentationsfragen eigentlich nicht; genau darauf beruht die vorige Optimierung.

Übungen

  1. Stell v3 eine Frage, die über mehrere Dateien verfolgt werden muss, etwa „In welcher Funktion schickt httpx.get die Netzwerkanfrage letztlich tatsächlich ab?“, und sieh, wie weit es kommt und ob die Aussage stimmt.
  2. Setz die gemischte Strategie aus Lektion 1 um: erst den Ablauf von v2, und kommt in der Antwort „in der Dokumentation nichts gefunden“ vor, den Agenten von v3 starten. Vergleiche die Gesamtkosten der 8 Evaluationsfragen.
  3. Gib eval_agent.py einen Parameter, der jede Frage 3-mal ausführt, und zähle, wie oft jede richtig beantwortet wird. Welche Fragen sind „stabil richtig“, welche „mal richtig, mal falsch“?

Selbsttest

1. Warum schlägt RepoBot zuerst in der Dokumentation und dann im Quellcode nach, statt direkt im Quellcode?

Die Dokumentation beschreibt die Nutzung, die den Nutzern zugesichert ist; der Quellcode enthält viele interne Implementierungsdetails, die nicht unbedingt stabiles äußeres Verhalten sind. Außerdem genügt in der Dokumentation meist eine Suche, während man im Quellcode mehrmals suchen und lesen muss, was teurer und langsamer ist. Nur wenn die Dokumentation keine Antwort hat, muss man im Quellcode suchen.

2. Das Evaluationsskript zeigt 8/8 richtig. Warum soll man die Antworten trotzdem einzeln lesen?

Die automatische Bewertung kann sich irren. Beim ersten Lauf dieser Lektion war die Aussage einer Frage falsch, nur kam in der Erklärung zufällig das Stichwort vor, und sie wurde als richtig gewertet. Nur durch Lesen weiß man, ob die Bewertungsregeln verlässlich sind, und kann sie verbessern, etwa um eine Bedingung „darf nicht vorkommen“.

3. Warum wird es fast bis zum Stillstand langsam, wenn mehrere Threads gleichzeitig das lokale Embedding-Modell aufrufen? Wie löst man das?

PyTorch startet bei jeder Berechnung selbst mehrere Threads. Rufen mehrere Python-Threads gleichzeitig das Modell auf, vervielfacht sich die Zahl der Threads weit über die Zahl der CPU-Kerne, und alle konkurrieren um Ressourcen. Die Lösung ist eine Sperre, sodass immer nur ein Thread das lokale Modell aufruft. Eine einzelne Berechnung ist schnell; das Anstehen bremst kaum.

Fragen und Diskussion

Hängst du in dieser Lektion fest? Frag hier. Und wenn du die Frage von jemandem beantworten kannst, tu es gern.

Eine Frage bringt 3 Punkte, eine Antwort 6. Beiträge erscheinen nach der Prüfung.

Diskussion wird geladen…