Modul 03 · Lektion 5

Projekt: die erste Version des Frage-Antwort-Assistenten

Mehrstufige Gespräche, Streaming, Wiederholungen und Kostenstatistik zu RepoBot v1 zusammensetzen, einem Frage-Antwort-Assistenten für httpx. Mit 6 Fragen mit Musterlösung testen und sehen, was er richtig beantwortet und was er erfindet.

  • Etwa 60 Minuten
  • Niveau: Fortgeschritten
  • Getestet: 2026-09-14 deepseek-flash

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

Was dieses Modul behandelt hat, ist einzeln betrachtet nicht schwer. Diese Lektion setzt es zu einem vollständigen kleinen Programm zusammen: RepoBot v1, einem Assistenten, der auf der Kommandozeile Fragen zu httpx beantwortet. Er ist der Ausgangspunkt des Projekts, das sich durch den ganzen ersten Teil zieht; die nächsten Module verbessern ihn Version für Version.

Wann du fertig bist

Erst das Ziel festlegen. Nach dieser Lektion sollte alles Folgende gelten:

  • Mit python repobot.py kann man fortlaufend mit ihm reden, und er erinnert sich an die vorige Runde.
  • Antworten erscheinen Zeichen für Zeichen per Streaming.
  • Nach jeder Runde zeigt er Eingabe- und Ausgabe-Tokens, Cache-Treffer, die Kosten der Runde und die Gesamtkosten.
  • Fragen, die nichts mit httpx zu tun haben, lehnt er höflich ab.
  • Fällt das Netz aus oder meldet der Server einen Fehler, stürzt das Programm nicht ab, sondern bittet dich, erneut zu fragen.
  • Du kannst sagen, bei welchen Fragen er falsch antwortet und warum.

Der letzte Punkt ist der wichtigste. v1 ist absichtlich unvollkommen; erst wenn du seine Probleme klar siehst, weißt du, was v2 lösen muss.

Aufbau

Der vollständige Code steht in projects/repobot/v1/repobot.py, gut hundert Zeilen in vier Blöcken:

repobot.py
  SYSTEM        system 提示词:身份、范围、规则
  cost_usd      根据 usage 算钱(01 模块第 4 课)
  open_stream   发起流式请求,连接阶段出错自动重试(本模块第 2、4 课)
  answer        流式打印回答,拼出完整文本,检查是否被截断(本模块第 2 课)
  main          多轮对话循环,维护历史、打印花费(本模块第 1 课)

Jeder Block wurde in früheren Lektionen behandelt; unten geht es nur um die neuen Fragen, die beim Zusammensetzen auftauchen.

Der system-Prompt

SYSTEM = """你是 RepoBot,Python HTTP 客户端库 httpx 的答疑助手。

- 只回答和 httpx 有关的问题,包括它的用法、原理、报错排查,以及和 requests 等库的比较。
- 和 httpx 无关的问题,礼貌地说明你只负责 httpx,不要回答。
- 回答要简洁,能用代码说明的就给代码。
- 不确定的地方要明确说"我不确定",不要编造版本号、参数名或者更新日志。"""

Vier Regeln, jede für ein konkretes Problem (Modul 02, Lektion 1 hat diese Schreibweise gezeigt): Die erste steckt den Bereich ab; die zweite verhindert, dass er zu einem Allzweck-Assistenten wird, der über alles plaudert, was sowohl Produktpositionierung als auch Kostenkontrolle ist; die dritte regelt Länge und Form der Antworten; die vierte zielt auf Halluzinationen. Ob die vierte wirklich hilft, zeigt der Test unten.

Wie Streaming und Wiederholung zusammenpassen

call_llm aus Lektion 4 ist für Aufrufe ohne Streaming geschrieben. Streaming bringt eine Schwierigkeit mit: Fehler können auftreten, wenn schon die Hälfte ausgegeben ist.

RepoBot teilt einen gestreamten Aufruf deshalb in zwei Phasen:

def open_stream(messages, max_attempts=4):
    """发起流式请求。连接阶段出错会自动重试;开始输出之后再出错,就不重试了。"""
    for attempt in range(1, max_attempts + 1):
        try:
            return client.chat.completions.create(
                model=MODEL,
                messages=messages,
                stream=True,
                stream_options={"include_usage": True},
                max_tokens=4000,
                extra_body={"thinking": {"type": "enabled" if THINKING else "disabled"}},
            )
        except RETRYABLE as e:
            if attempt == max_attempts:
                raise
            wait = 2 ** (attempt - 1) + random.random()
            print(f"\n[{type(e).__name__},{wait:.1f} 秒后重试]", file=sys.stderr)
            time.sleep(wait)

Tritt der Fehler beim Verbindungsaufbau auf (Drosselung, Serverfehler, keine Verbindung), hat der Nutzer noch nichts gesehen, und man kann unbesorgt wiederholen. Hat die Ausgabe einmal begonnen, wird bei einem Fehler nicht mehr wiederholt, weil eine neu erzeugte Antwort nicht zur schon gesehenen ersten Hälfte passt. main fängt diesen Fehler ab, sagt dem Nutzer „diese Runde ist ungültig, du kannst noch einmal fragen“ und schreibt die Runde nicht in den Verlauf:

        try:
            text, usage = answer(messages)
        except openai.APIError as e:
            print(f"\n[出错了:{type(e).__name__},这一轮作废,可以再问一次]")
            continue

Die Funktion answer gibt aus und sammelt dabei die einzelnen Textstücke in einer Liste, die am Ende zur vollständigen Antwort zusammengesetzt und in den Verlauf geschrieben wird. Sie merkt sich auch finish_reason; ist er length, weist sie den Nutzer darauf hin, dass die Antwort abgeschnitten wurde.

Der Verlauf wird auf die einfachste Art gekürzt: nur die letzten 20 Nachrichten bleiben. Bei Fragen und Antworten kommt es selten vor, dass jemand über ein Dutzend Runden fragt und sich noch auf den Anfang bezieht.

Ausführen

cd projects/repobot/v1
pip install -r requirements.txt
python repobot.py

Ich habe 6 Fragen zum Testen vorbereitet. Fünf betreffen httpx und haben eindeutige Lösungen, die ich vorher im httpx-Quellcode nachgeschlagen habe; die letzte hat nichts mit httpx zu tun. Alles auf einmal per Pipe:

printf '%s\n' "httpx 默认会自动跟随重定向吗?" "那默认最多跟随几次重定向?" \
  "httpx 的 Client 默认连接池最多允许多少个连接?keep-alive 连接最多多少个?" \
  "httpx 0.27.2 这个版本修复了什么问题?" "怎么给 httpx 设置 10 秒的超时?给一行代码。" \
  "今天北京天气怎么样?" | python repobot.py

Unten die vollständige Ausgabe meines Laufs, kein Zeichen verändert, nur ein paar zu lange Codebeispiele entfernt. Deine Antworten werden anders formuliert sein und sogar in richtig und falsch abweichen können.

你:httpx 默认会自动跟随重定向吗?
RepoBot:是的,httpx 默认会自动跟随重定向(`follow_redirects=True` 是默认行为)。

注意和 requests 的区别:**requests 默认跟随,httpx 也是默认跟随**,但 httpx 早期版本默认是 `False`,0.20 之后改成了 `True`。如果你用的是很老的版本,行为可能不同。
(代码示例省略)
具体默认值建议以你所用版本的文档为准,我不确定所有版本都是 `True`。
[输入 118(缓存命中 0),输出 219,本轮 0.00030 美元,累计 0.00030 美元]

你:那默认最多跟随几次重定向?
RepoBot:默认最多跟随 **20 次**。
(代码示例省略)
补充一点:httpx 的 `TooManyRedirects` 继承自 `RequestError`,可以直接用 `httpx.RequestError` 捕获。

这个 `20` 是 httpx 的默认值,和 requests 的默认 `30` 不同。我不确定该默认值在所有历史版本里都是 20,建议以你所用版本的文档为准。
[输入 349(缓存命中 128),输出 221,本轮 0.00033 美元,累计 0.00063 美元]

你:httpx 的 Client 默认连接池最多允许多少个连接?keep-alive 连接最多多少个?
RepoBot:`httpx.Client` 的默认连接池上限是 **100 个连接**,其中默认 **20 个是 keep-alive 连接**。
(代码示例省略)
- `max_keepalive_connections`:池中保留的、可复用的空闲连接数上限,默认为 `max_connections` 的 20%(即 100 的 20% = 20)。如果显式只设 `max_connections`,这个值会按比例走。
- `keepalive_expiry`:空闲 keep-alive 连接的存活时间,默认 5 秒。
(后面还有两条补充说明,省略)
[输入 598(缓存命中 384),输出 405,本轮 0.00055 美元,累计 0.00118 美元]

你:httpx 0.27.2 这个版本修复了什么问题?
RepoBot:我不确定 httpx 0.27.2 具体修复了哪些问题,不想凭印象编造更新日志。

建议你直接看官方来源核实:

- 发布记录:https://github.com/encode/httpx/releases
- 更新日志文件:https://github.com/encode/httpx/blob/master/CHANGELOG.md

在 Releases 页面里找到 `0.27.2` 那个 tag,就能看到该版本的具体改动。如果你把 changelog 内容贴过来,我可以帮你解读这些改动对你用法的实际影响。
[输入 1022(缓存命中 768),输出 119,本轮 0.00022 美元,累计 0.00141 美元]

你:怎么给 httpx 设置 10 秒的超时?给一行代码。
RepoBot:```python
r = httpx.get("https://example.com", timeout=10.0)
```
(后面还有两种写法,省略)
[输入 1162(缓存命中 1024),输出 81,本轮 0.00014 美元,累计 0.00155 美元]

你:今天北京天气怎么样?
RepoBot:抱歉,我只负责 httpx 相关的问题,天气这类问题我答不了。如果你有 httpx 用法、报错或配置方面的问题,我很乐意帮忙。
[输入 1252(缓存命中 1024),输出 35,本轮 0.00012 美元,累计 0.00167 美元]

Frage für Frage prüfen

Ich habe die Antworten Satz für Satz mit dem httpx-Quellcode (httpx/_client.py, httpx/_config.py, httpx/_exceptions.py) abgeglichen.

Frage 1: falsch. Im Client von httpx steht follow_redirects: bool = False; standardmäßig werden Weiterleitungen nicht verfolgt, genau das ist ein wichtiger Unterschied zu requests. Das Modell lag nicht nur falsch, sondern erfand auch eine Versionsgeschichte „ab 0.20 auf True geändert“, um die falsche Antwort zu stützen. Am Ende setzte es noch „ich bin mir nicht sicher“ hinzu, aber davor war der Ton sehr bestimmt, und die meisten Nutzer würden es glauben.

Frage 2: richtig. Im Quellcode steht DEFAULT_MAX_REDIRECTS = 20. TooManyRedirects erbt tatsächlich von RequestError. (Dass requests standardmäßig 30 erlaubt, habe ich nicht im Quellcode von requests geprüft und zähle es nicht.)

Frage 3: Zahlen richtig, Erklärung erfunden. Im Quellcode steht DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20), keepalive_expiry ist standardmäßig 5 Sekunden; alle drei Zahlen stimmen. Aber „max_keepalive_connections ist standardmäßig 20 % von max_connections und skaliert mit, wenn man nur max_connections setzt“ ist erfunden: In der Klasse Limits ist der Standardwert von max_keepalive_connections None, und eine anteilige Berechnung gibt es nirgends. Diese Art „richtige Zahlen mit erfundenem Mechanismus“ ist am schwersten zu entdecken.

Frage 4: nichts erfunden. In Modul 01, Lektion 2 erfand das Modell bei derselben Frage eine nicht existierende Sicherheitslücke samt CVE-Nummer. Diesmal sagte es „ich bin mir nicht sicher“ und nannte, wo man nachschlagen kann. Der Unterschied ist der Satz im system-Prompt: „Wo du unsicher bist, sag deutlich ‚ich bin mir nicht sicher‘; erfinde keine Versionsnummern, Parameternamen oder Changelogs.“ Diese Regel hilft, aber Frage 1 und 3 zeigen, dass sie nicht jede Erfindung verhindert: Das Modell muss erst „merken“, dass es unsicher ist, bevor die Regel greift.

Frage 5: richtig.

Frage 6: sehr taktvoll abgelehnt.

Und die Rechnung: 6 Runden kosteten zusammen 0,00167 Dollar. Die Cache-Treffer steigen in jeder Runde (0, 128, 384, 768, 1024), weil system-Prompt und bisheriger Gesprächsverlauf einen festen Anfang bilden; der Cache aus Modul 01, Lektion 4 wirkt automatisch.

Wo v1 scheitert

6 Fragen: 2 ganz richtig, 1 taktvoll abgelehnt, 1 ehrlich „weiß ich nicht“, 1 mit richtigen Zahlen und eingeschmuggelter erfundener Erklärung, 1 komplett falsch samt erfundener Begründung.

Die Ursache ist eine einzige: Er kann nur aus dem Gedächtnis antworten. Die Trainingsdaten des Modells enthalten viel über httpx und requests, und ihre Nutzung ist sehr ähnlich, also vermischen sich die Erinnerungen. Frage 1 hat vermutlich das Verhalten von requests auf httpx übertragen.

Mit Prompt-Änderungen kommt man hier nicht weit. Die echte Lösung ist, dass er vor dem Antworten in der offiziellen httpx-Dokumentation nachschlägt, sich an die Dokumentation hält und dem Nutzer sagt, von welcher Seite die Antwort stammt. Das ist RAG im nächsten Modul.

Fragen, die dieses Projekt beantworten soll

Nach jeder Version prüfst du dein Design mit diesen Fragen:

  • Warum dieses Design? Kommandozeile + Streaming + gekürzter Verlauf ist die einfachste lauffähige Form. Erst etwas Brauchbares bauen, dann nach und nach Funktionen hinzufügen.
  • Wo wird er scheitern? Bei abgelegenen Standardwerten, Versionsunterschieden und Stellen, die requests ähneln, aber anders sind, antwortet er leicht falsch, und zwar mit sehr bestimmtem Ton.
  • Woran erkennt man, ob er gut ist? Bisher nur 6 von Hand geprüfte Fragen. Modul 06 baut daraus ein automatisch laufendes Evaluationsset.
  • Was schaut man sich bei Problemen an? Bisher nur die Ausgabe auf dem Bildschirm. Modul 06 fügt Logs hinzu.
  • Geht es billiger? Er ist schon sehr billig. flash ohne Denken kostet unter 0,001 Dollar pro Runde.
  • Braucht es wirklich einen Agenten? Nein. v1 ist nur ein Aufruf plus Gesprächsverlauf. Erst in Modul 05, wenn er selbst entscheiden muss, ob er in der Dokumentation oder im Quellcode sucht, kommt ein Agent in Frage.

Übungen

  1. Schalt mit --think den Denkmodus ein und stell die 6 Fragen erneut. Sind Frage 1 und 3 jetzt richtig? Was kostet es nun?
  2. Lösch die Regel „Wo du unsicher bist, sag deutlich …“ aus dem system-Prompt, stell Frage 4 ein paar Mal und schau, ob das Modell wieder beginnt, Changelogs zu erfinden.
  3. Denk dir selbst 5 Fragen zu httpx aus, am besten solche, deren Musterlösung du in Dokumentation oder Quellcode nachschlagen kannst, teste v1 damit und zähl, wie viele er richtig beantwortet. Heb die 5 Fragen auf; im nächsten Modul testest du damit v2.

Selbsttest

1. Warum wiederholt RepoBot nach Beginn der Ausgabe bei einem Fehler nicht mehr?

Der Nutzer hat schon einen Teil der Antwort gesehen. Ein neuer Versuch erzeugt von vorn eine neue Antwort, die wahrscheinlich nicht zur gesehenen ersten Hälfte passt; zusammen wäre das sehr verwirrend. Daher wird nur beim Verbindungsaufbau wiederholt (wenn der Nutzer noch nichts gesehen hat); nach Beginn der Ausgabe fordert man den Nutzer bei einem Fehler auf, erneut zu fragen.

2. Im system-Prompt steht „wenn unsicher, sag, dass du unsicher bist“. Warum hat RepoBot bei Frage 1 trotzdem erfunden?

Diese Regel greift nur, wenn das Modell „merkt“, dass es unsicher ist. Bei Frage 1 glaubte das Modell fälschlich, die Antwort zu kennen, und gab sehr bestimmt eine falsche Antwort. Prompts können Erfindungen verringern, aber nicht beseitigen. Beseitigen lassen sie sich, indem das Modell anhand echter Unterlagen antwortet, also mit RAG.

3. Warum steigt die Zahl der Cache-Treffer bei RepoBot von Runde zu Runde?

Jede Anfrage beginnt mit demselben system-Prompt und dem bisherigen Gesprächsverlauf. Was in der vorigen Runde die Anfrage war, wird in dieser Runde Teil des Anfangs; der Server kann früher Berechnetes wiederverwenden, und der getroffene Teil wird immer größer.

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…