Modul 04 · Lektion 7

Projekt: dem Assistenten die Projektdokumentation beibringen

Zerlegen, hybride Suche, Umformulieren der Anfrage und Antworten mit Quellenangaben in RepoBot einbauen und so v2 bauen. Er beantwortet die Fragen richtig, bei denen v1 falschlag, und löst das Suchproblem bei Rückfragen wie „und asynchron?“ in mehrstufigen Gesprächen.

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

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

Diese Lektion baut alles aus Modul 04 in RepoBot ein und macht daraus die zweite Version. Du triffst auf ein Problem, das es bei einzelnen Fragen nicht gab: In mehrstufigen Gesprächen findet eine Rückfrage des Nutzers für sich allein oft gar nichts.

Wann du fertig bist

  • Auf „Folgt httpx standardmäßig automatisch Weiterleitungen?“ lautet die Antwort „nein“, mit der Quelle compatibility.md.
  • Jede Antwort nennt ihre Quellen mit [Nummer] und listet am Ende die tatsächlich zitierten Dokumente.
  • Fragt man nach etwas, das nicht in der Dokumentation steht (etwa HTTP/3), antwortet er „in der Dokumentation wurde nichts dazu gefunden“ und erfindet nichts.
  • Fragt man nacheinander „Wie stelle ich in httpx ein Timeout ein?“ und „Und beim asynchronen Client?“, findet die zweite Frage die Dokumentation zu Timeouts.
  • Beim zweiten Start werden die Vektoren der Dokumentation nicht neu berechnet.
  • eval_retrieval.py erreicht bei den 20 Evaluationsfragen eine Top-5-Trefferquote von 100 %.

Aufbau

Der Code steht in projects/repobot/v2/, zwei Dateien mehr als bei v1:

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

Den Code in retrieval.py haben die vorigen Lektionen Stück für Stück erklärt; hier geht es nur um drei neue Probleme, die beim Zusammenbau auftauchen.

Problem eins: Rückfragen finden nichts

Im Übungscode von Modul 04 stand jede Frage für sich. In einem Gespräch fragen Nutzer aber so:

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

Sucht man direkt mit der zweiten Frage, findet „Und beim asynchronen Client?“ zwar die Dokumentation zu Asynchronität, aber nicht die zu Timeouts, denn das Wort „Timeout“ kommt in der Frage gar nicht vor.

v2 löst das, indem es beim Umformulieren der Anfrage die jüngsten Gesprächsrunden mitgibt: Das Modell versteht zuerst, was der Nutzer eigentlich fragt, und erzeugt dann die Suchbegriffe:

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}"},
            ])

Nur die letzten 4 Nachrichten, je höchstens 300 Zeichen: genug, um Bezüge zu verstehen, ohne den Umformulierungsschritt zu teuer zu machen. Die Wirkung siehst du im Ergebnis unten.

Problem zwei: was in den Verlauf gehört

Jede Runde legt die 5 gefundenen Abschnitte in den Prompt, etwa tausend Tokens. Speichert man sie auch im Gesprächsverlauf, liegen nach zehn Runden über zehntausend Tokens alte Dokumentation darin, teuer und ablenkend für das Modell.

v2 legt die Dokumente nur in die user-Nachricht dieser Runde; im Verlauf gespeichert werden nur die ursprüngliche Frage des Nutzers und die Antwort des Modells:

            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}]

Die Antwort des Modells enthält bereits die Kernpunkte aus der Dokumentation; braucht das spätere Gespräch sie, findet es sie in der Antwort.

Problem drei: nicht bei jedem Start die Vektoren neu berechnen

Die 196 Chunks bei jedem Start mit dem Embedding-Modell zu berechnen, dauert gut zehn Sekunden. Ändert sich die Dokumentation nicht, ist das Ergebnis jedes Mal gleich, also kann man es zwischenspeichern:

        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)

Der Name der Cache-Datei ist ein Hash aus „Name des Embedding-Modells + Inhalt aller Chunks“. Ändert sich an der Dokumentation ein Zeichen oder wechselt man das Embedding-Modell, ändert sich der Hash, es wird neu berechnet, und veraltete Vektoren werden nie versehentlich verwendet. Auch die Umformulierungen werden in .cache/rewrites.json zwischengespeichert.

Suchergebnisse

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

Ohne Reranking beträgt der MRR 0,892, deutlich höher als die 0,772 der „Umformulierung + hybride RRF“ in Lektion 4. Der Suchalgorithmus ist genau derselbe; der einzige Unterschied ist, dass der Prompt der Umformulierung durch die neue Version oben ersetzt wurde: „eine Zeile englische Suchbegriffe ausgeben: mit einer vollständigen englischen Formulierung der Frage sowie …“. Die Version aus Lektion 4 lautete „mit der englischen Übersetzung der Frage sowie …“ und hatte keinen Gesprächskontext.

Das zeigt erneut, wie stark der Prompt der Umformulierung die Suche beeinflusst. Deshalb lässt man nach jeder Änderung die Evaluation neu laufen: Du glaubst, nur nebenbei einen Satz im Prompt geändert zu haben, und die Kennzahlen haben sich verschoben.

In dieser Version hebt Reranking den MRR nur von 0,892 auf 0,902, und die Top-3-Trefferquote sinkt sogar von 100 % auf 95 %. Die zusätzliche Sekunde bringt wenig, also ist Reranking in v2 standardmäßig aus und bei Bedarf mit --rerank einzuschalten.

Ein Gespräch

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

--show-query zeigt pro Runde Suchbegriffe und gefundene Dokumente. Mein Ergebnis (ein paar Codebeispiele entfernt, sonst unverändert):

你: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 美元]

Runde für Runde

Weiterleitungen: richtig. v1 lag hier falsch und erfand eine Versionsgeschichte. v2 antwortet „nein“ und nennt zwei Quellen.

Die Rückfragen wurden richtig verstanden. „那默认最多跟随几次?“ (und wie oft höchstens standardmäßig?) wurde zu httpx follow_redirects max_redirects default maximum number of redirects umformuliert, „那异步客户端呢?“ (und beim asynchronen Client?) zu asynchronous client timeout configuration httpx.AsyncClient(timeout=10.0); gefunden wurde jeweils timeouts.md. Ohne Gesprächskontext beim Umformulieren hätte die zweite Frage nur Dokumentation zur Asynchronität gefunden.

„Wie oft höchstens standardmäßig“: Es sagt, das stehe nicht in der Dokumentation. Diese Antwort stimmt: Die httpx-Dokumentation nennt diesen Standardwert tatsächlich nicht, er steht nur im Quellcode (in Modul 03, Lektion 5 haben wir in httpx/_config.py den Wert 20 gefunden). Interessanterweise hat v1 diese Frage aus dem Gedächtnis richtig beantwortet. v2 kann sie nicht beantworten, weil es streng „nur anhand der Unterlagen“ antworten soll. Das ist der Preis, von dem Lektion 5 sprach: Was nicht in den Unterlagen steht, darf es nicht sagen, auch wenn das Modell es weiß.

Asynchroner Client: eine sehr abgewogene Antwort. Die Beispiele der Dokumentation nutzen alle httpx.Client; das sagt es offen und weist zugleich darauf hin, dass derselbe Parameter gilt. Genau das wollen wir: nicht so tun, als stünde in der Dokumentation, was dort nicht steht.

HTTP/3: nichts erfunden.

Wetter: abgelehnt, aber Geld verschwendet. Es hat trotzdem umformuliert und gesucht und 5 völlig irrelevante Abschnitte in den Prompt gestopft. Besser wäre, vor der Suche zu prüfen, ob die Frage mit httpx zu tun hat, und andernfalls direkt abzulehnen. Die „Leitplanken“ in Modul 06, Lektion 5 erledigen das.

Kosten. Pro Runde etwa 0,0006 bis 0,0007 Dollar einschließlich des Umformulierungsaufrufs, etwas teurer als die 0,0001 bis 0,0005 Dollar pro Runde bei v1, vor allem wegen der rund tausend Tokens Dokumentation. Die Cache-Treffer steigen, weil system-Prompt und Gesprächsverlauf einen festen Anfang bilden und sich nur die Dokumente in der letzten user-Nachricht jedes Mal ändern.

Die Probleme von v2

  • Was nicht in der Dokumentation steht, kann es nicht beantworten. Die Standardzahl der Weiterleitungen, die interne Logik von Limits, wann genau eine bestimmte Ausnahme geworfen wird: Diese Antworten stehen im Quellcode.
  • Es sucht nur einmal. Findet die erste Suche nichts Passendes, hat es keine Chance, mit anderen Suchbegriffen noch einmal zu suchen.
  • Egal welche Frage, es sucht zuerst. Selbst bei der Frage nach dem Wetter.

Die ersten beiden Probleme verlangen, dass RepoBot selbst entscheidet, „in der Dokumentation nachzuschlagen“ oder „im Quellcode zu suchen“, und je nach Ergebnis den nächsten Schritt wählt. Das ist der Agent in Modul 05.

Fragen, die dieses Projekt beantworten soll

  • Warum dieses Design? Umformulieren + BM25 + Vektor-RRF ist die Kombination, die sich bei den 20 Evaluationsfragen als die beste erwiesen hat und keine zusätzliche Rechenleistung braucht. Reranking bringt wenig und ist daher optional.
  • Wo wird es scheitern? Bei Fragen, deren Antwort nur im Quellcode und nicht in der Dokumentation steht; bei Fragen, die mehrere Dokumente zusammenführen müssen; bei Fragen, bei denen die erste Suche danebenliegt.
  • Wie bewertet man es? Die Suche mit eval_retrieval.py, die Qualität der Antworten mit dem Modell-Gutachter aus Lektion 6.
  • Was schaut man sich bei Problemen an? --show-query einschalten, zuerst prüfen, ob die Suchbegriffe stimmen, dann, ob die gefundenen Dokumente stimmen, zuletzt, ob das Modell die Dokumente richtig nutzt. Die meisten Probleme liegen in den ersten beiden Schritten.
  • Geht es billiger? Man kann vor der Suche prüfen, ob die Frage mit httpx zu tun hat, und andere direkt ablehnen; das spart Umformulierung, Suche und über tausend Eingabe-Tokens.
  • Braucht es einen Agenten? Bis zu dieser Version nicht; der Ablauf ist fest: umformulieren, suchen, antworten. Erst wenn es heißt „findet die Suche nichts, versuch es anders“, braucht man einen.

Übungen

  1. Teste v2 mit den 5 eigenen Fragen aus der Übung zu v1 (Modul 03, Lektion 5). Gibt es eine Frage, die v1 richtig beantwortet hat und v2 nicht beantworten kann?
  2. Entferne den Parameter history aus QueryRewriter.rewrite (immer einen leeren Verlauf übergeben), frag erneut „Wie stelle ich ein Timeout ein?“ und „Und beim asynchronen Client?“ und schau, was die zweite Frage findet.
  3. Ruf in repobot.py vor der Suche einmal das Modell auf, um zu prüfen, „ob diese Frage mit httpx zu tun hat“, und antworte andernfalls direkt mit einer Ablehnung, ohne Suche. Vergleiche die Kosten der Wetter-Runde vor und nach der Änderung.

Selbsttest

1. Der Nutzer fragt „Und beim asynchronen Client?“. Welches Problem entsteht, wenn man mit diesem Satz direkt sucht? Wie löst v2 das?

Der Satz enthält keine Schlüsselinformation wie „Timeout“, die Suche findet also nur allgemeine Dokumentation zur Asynchronität, nicht den Inhalt zu Timeout-Einstellungen des asynchronen Clients. v2 gibt beim Umformulieren der Anfrage die letzten Gesprächsrunden mit, damit das Modell zuerst versteht, dass der Nutzer eigentlich „wie stellt man beim asynchronen Client ein Timeout ein“ fragt, und dann vollständige Suchbegriffe erzeugt.

2. Warum kommen die gefundenen Dokumente nur in die Nachricht der aktuellen Runde und nicht in den Gesprächsverlauf?

Die Dokumente einer Runde umfassen etwa tausend Tokens; würden alle im Verlauf gespeichert, würde er mit jeder Runde größer, teurer und ablenkender für das Modell. Die Antwort des Modells enthält bereits die Kernpunkte aus den Dokumenten; es genügt, die Antwort zu speichern.

3. v1 hat „wie oft höchstens standardmäßig Weiterleitungen verfolgt werden“ aus dem Gedächtnis richtig beantwortet, v2 sagt, das stehe nicht in der Dokumentation. Ist v2 damit schlechter als v1?

So kann man das nicht sagen. v2 antwortet nur anhand der Dokumentation, und dort steht diese Information tatsächlich nicht, also sagt es ehrlich „nicht gefunden“. Es tauscht „kann manchmal nicht antworten“ gegen „alles, was es antwortet, ist belegt“. v1 lag diesmal zufällig richtig, beantwortet andere Fragen aber selbstsicher falsch. Um dieses Problem von v2 zu lösen, sollte man es mehr Unterlagen durchsuchen lassen, etwa den Quellcode, statt die Vorgabe „nur anhand der Unterlagen“ zu lockern.

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…