Tool-Aufrufe: das Modell handeln lassen
Das Modell kann selbst keine Echtzeitdaten abfragen und keine Aktionen ausführen. Wir geben ihm ein echtes Tool, das auf PyPI die neueste Version eines Pakets nachschlägt, und gehen den gesamten Ablauf eines Tool-Aufrufs durch, einschließlich paralleler Aufrufe, Fehlerbehandlung und dem, was im Denkmodus zu beachten ist.
- Etwa 45 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.
Fragst du das Modell „Was ist die neueste Version von httpx?“, kann es nur aus den Trainingsdaten antworten: vielleicht eine Versionsnummer von vor einem Jahr, vielleicht eine erfundene. Bittest du es „Trag mir einen Termin in den Kalender ein“, kann es nur „Gut, ich habe ihn eingetragen“ antworten, und dann passiert nichts.
Ein Modell kann nur Text ausgeben. Damit es Echtzeitdaten abfragen und wirklich etwas tun kann, braucht es Werkzeuge: Du schreibst Funktionen und sagst dem Modell, welche es gibt; hält das Modell es für nötig, gibt es aus „ich möchte Funktion X mit diesen Parametern aufrufen“; dein Programm führt die Funktion tatsächlich aus und teilt dem Modell das Ergebnis mit; das Modell antwortet dem Nutzer auf dieser Grundlage. Dieser Mechanismus heißt Tool-Aufruf (tool calling), auch Funktionsaufruf (function calling).
Er ist die Grundlage der Agenten in Modul 05; in dieser Lektion klären wir jeden Schritt.
Der Ablauf
Zuerst der Überblick. Eine Frage mit Tool-Aufruf braucht mindestens zwei Aufrufe des Modells:
你的程序 模型
│ 1. 用户问题 + 工具说明书 │
│ ────────────────────────────────────────────▶ │
│ 2. "请调用 get_pypi_info(httpx)" │
│ ◀──────────────────────────────────────────── │
│ 3. 程序自己执行 get_pypi_info("httpx") │
│ 拿到结果 {"version": "0.28.1", ...} │
│ 4. 之前的全部消息 + 工具结果 │
│ ────────────────────────────────────────────▶ │
│ 5. "httpx 的最新版本是 0.28.1" │
│ ◀──────────────────────────────────────────── │
Entscheidend sind Schritt 2 und 3: Das Modell führt nie Code aus. Es gibt nur eine strukturierte „Aufrufanfrage“ aus; die Ausführung liegt vollständig bei deinem Programm. Du kannst prüfen, was es aufrufen will und ob die Parameter stimmen, und entscheiden, ob du ausführst oder ablehnst. Das ist für die Sicherheit sehr wichtig; Modul 05, Lektion 8 geht darauf ein.
Schritt eins: Tool und Beschreibung schreiben
Ein Tool ist eine ganz normale Python-Funktion. Hier eine wirklich nutzbare: Sie ruft die öffentliche Schnittstelle von PyPI auf und schlägt die neueste Version eines Pakets nach.
import httpx
def get_pypi_info(package: str) -> dict:
"""真正干活的函数:调用 PyPI 的公开接口。"""
r = httpx.get(f"https://pypi.org/pypi/{package}/json", timeout=10)
if r.status_code == 404:
return {"error": f"PyPI 上没有叫 {package} 的包"}
info = r.json()["info"]
return {"name": info["name"], "version": info["version"], "summary": info["summary"],
"requires_python": info["requires_python"]}
Nebenbei: Für die HTTP-Anfrage wird hier genau httpx verwendet. Es wurde bei der Installation von openai bereits als Abhängigkeit mitinstalliert.
Dann schreibst du eine „Beschreibung“, die dem Modell mitteilt, dass es dieses Tool gibt. Deinen Funktionscode sieht das Modell nicht; es sieht nur diese Beschreibung:
TOOLS = [
{
"type": "function",
"function": {
"name": "get_pypi_info",
"description": "查询一个 Python 包在 PyPI 上的最新版本、简介和支持的 Python 版本。",
"parameters": {
"type": "object",
"properties": {
"package": {"type": "string", "description": "PyPI 上的包名,例如 httpx"},
},
"required": ["package"],
},
},
}
]
FUNCTIONS = {"get_pypi_info": get_pypi_info}
name ist der Name des Tools, description sagt, was es kann, parameters beschreibt die Parameter mit JSON Schema. Anhand von description entscheidet das Modell, wann es das Tool benutzt, anhand von parameters, wie es die Parameter füllt. Wie gut die Beschreibung ist, bestimmt direkt, ob und wie richtig das Modell das Tool nutzt; Modul 05, Lektion 3 zeigt, wie man sie schreibt.
FUNCTIONS ist eine Zuordnung vom Tool-Namen zur echten Funktion; erhält das Programm eine Aufrufanfrage, findet es damit die auszuführende Funktion.
Schritt zwei: die Schleife
messages = [{"role": "user", "content": "httpx 和 requests 在 PyPI 上的最新版本分别是多少?各自要求什么 Python 版本?"}]
for step in range(1, 6): # 最多 5 轮,防止意外的死循环
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=TOOLS,
extra_body={"thinking": {"type": "enabled" if THINKING else "disabled"}},
)
message = response.choices[0].message
print(f"第 {step} 轮:finish_reason={response.choices[0].finish_reason}")
if not message.tool_calls:
print("最终回答:", message.content)
break
# 把模型的这条消息原样放回历史。开思考时,里面的 reasoning_content 也必须带上
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
args = json.loads(call.function.arguments)
print(f" 模型要求调用 {call.function.name}({args})")
try:
result = FUNCTIONS[call.function.name](**args)
except Exception as e: # 工具出错也要告诉模型,而不是让程序崩掉
result = {"error": f"{type(e).__name__}: {e}"}
print(f" 返回:{result}")
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False)})
Jede Runde: das Modell mit tools aufrufen. Enthält die Antwort keine tool_calls, hat das Modell die endgültige Antwort gegeben, fertig. Wenn doch, werden sie einzeln ausgeführt, die Ergebnisse als Nachrichten mit role gleich tool an den Verlauf angehängt und das Modell erneut aufgerufen.
Ein paar Stellen, auf die man achten muss:
- Die Aufrufanfrage des Modells kommt zurück in den Verlauf.
messages.append(message.model_dump(exclude_none=True))fügt diese Nachricht des Modells mit ihrentool_callsunverändert wieder ein. Fehlt dieser Schritt, sieht das Modell in der nächsten Runde einen Haufen Tool-Ergebnisse, ohne zu wissen, wer sie angefordert hat. tool_call_idmuss passen. Jede Aufrufanfrage hat eineid, und das zugehörige Tool-Ergebnis trägt dieselbetool_call_id. Fordert das Modell mehrere Tools auf einmal an, weiß es anhand dieser ID, welches Ergebnis zu welcher Anfrage gehört.- Die Parameter sind JSON als String.
call.function.argumentsist ein String wie'{"package": "httpx"}'und wird mitjson.loadsgeparst. - Tool-Fehler dürfen das Programm nicht zum Absturz bringen. Gib die Fehlermeldung als Ergebnis an das Modell zurück; oft passt es sich an, versucht es mit anderen Parametern noch einmal oder sagt dem Nutzer ehrlich, dass nichts zu finden war.
- Eine Obergrenze für Runden setzen. Das Modell kann in wiederholten Tool-Aufrufen hängen bleiben; die Grenze ist die letzte Sicherung.
Ergebnis
第 1 轮:finish_reason=tool_calls
模型要求调用 get_pypi_info({'package': 'httpx'})
返回:{'name': 'httpx', 'version': '0.28.1', 'summary': 'The next generation HTTP client.', 'requires_python': '>=3.8'}
模型要求调用 get_pypi_info({'package': 'requests'})
返回:{'name': 'requests', 'version': '2.34.2', 'summary': 'Python HTTP for Humans.', 'requires_python': '>=3.10'}
第 2 轮:finish_reason=stop
最终回答: 两个包在 PyPI 上的最新信息如下:
| 包名 | 最新版本 | 要求 Python 版本 | 简介 |
|---|---|---|---|
| **httpx** | 0.28.1 | >=3.8 | The next generation HTTP client. |
| **requests** | 2.34.2 | >=3.10 | Python HTTP for Humans. |
几点说明:
- **httpx** 支持范围更宽,Python 3.8 及以上都能用,兼容性更好。
- **requests** 这边要求 Python 3.10 及以上,门槛更高一些。
- 光看"最低版本要求"的话,httpx 覆盖的老版本 Python 更多;但如果你跑在 3.10+ 环境上,两者都没问题。
如果你告诉我项目所用的 Python 版本,我可以帮你判断具体该选哪个。
(Das sind die Versionsnummern vom 14. September 2026; wenn du es ausführst, hat PyPI vielleicht schon neuere.)
Der finish_reason der ersten Runde ist tool_calls, der dritte Fall aus der Tabelle in Modul 00, Lektion 3. Und das Modell hat in derselben Runde zwei Aufrufe angefordert: einmal httpx, einmal requests. Das nennt man parallele Tool-Aufrufe: Das Modell erkennt, dass die beiden Abfragen voneinander unabhängig sind, und stellt sie zusammen, was eine Runde hin und her spart. In der zweiten Runde bekommt das Modell zwei echte Datensätze und gibt die endgültige Antwort; die Versionsnummern kommen von PyPI, nicht aus seiner Fantasie.
Mit eingeschaltetem Denkmodus
Bei den Modellen von DeepSeek ist Denken standardmäßig an. Für Tools im Denkmodus gilt eine Regel: Der reasoning_content jeder früheren Runde muss unverändert an die API zurückgegeben werden. Ohne Tools ist es egal, der Server ignoriert ihn; mit Tools ist es Pflicht.
Der Code oben wandelt mit message.model_dump(exclude_none=True) die ganze Nachricht des Modells in ein Dictionary und legt sie zurück in den Verlauf; darin ist reasoning_content natürlich enthalten, also funktioniert es auch mit Denken:
python tool_calling.py --think
第 1 轮:finish_reason=tool_calls
模型要求调用 get_pypi_info({'package': 'httpx'})
返回:{'name': 'httpx', 'version': '0.28.1', 'summary': 'The next generation HTTP client.', 'requires_python': '>=3.8'}
模型要求调用 get_pypi_info({'package': 'requests'})
返回:{'name': 'requests', 'version': '2.34.2', 'summary': 'Python HTTP for Humans.', 'requires_python': '>=3.10'}
第 2 轮:finish_reason=stop
最终回答: 两个包在 PyPI 上的最新信息如下:
(后面的回答内容和不开思考时相近,这里省略)
Eine verbreitete Schreibweise pickt nur content und tool_calls heraus und baut von Hand ein Dictionary für den Verlauf. Ohne Denken geht das gut, mit Denken geht reasoning_content verloren. Mit model_dump unverändert zurücklegen ist am bequemsten.
Was tun, wenn das Modell falsche Parameter übergibt
Die vom Modell gefüllten Parameter sind nicht unbedingt richtig: ein nicht existierender Paketname, ein fehlender Pflichtparameter, ein falscher Typ. Mehrere Verteidigungslinien:
- Die Beschreibung klar schreiben. In der
descriptiondes Parameters Format und Beispiel angeben; „Paketname auf PyPI, zum Beispiel httpx“ ist besser als nur „Paketname“. - In der Funktion prüfen. Nicht annehmen, dass Parameter gültig sind; etwa ob der Paketname seltsame Zeichen enthält oder ein Zahlenwert in einem vernünftigen Bereich liegt.
- Fehler an das Modell zurückgeben. Im Code oben wird jede von der Funktion geworfene Ausnahme abgefangen und als
{"error": "..."}zurückgegeben. Für ein Paket, das es auf PyPI nicht gibt, gibt die Funktion selbst eine Fehlermeldung zurück. Sieht das Modell den Fehler, korrigiert es sich meist selbst oder sagt dem Nutzer ehrlich Bescheid. - Strikter Modus. Der strikte Modus von DeepSeek (
strict: true) aus Modul 02, Lektion 4 sichert, dass die Parameter der Struktur des Schemas folgen, aber nicht, dass der Inhalt stimmt.
Häufige Probleme
Das Modell hätte ein Tool aufrufen sollen, hat aber eine Antwort erfunden: Prüf, ob die description des Tools klar sagt, was es kann. Man kann auch in die system-Nachricht schreiben „bei Fragen zu Paketversionen immer mit get_pypi_info nachschlagen, nicht aus dem Gedächtnis antworten“. Wenn ein bestimmtes Tool unbedingt aufgerufen werden muss, kann man es mit tool_choice erzwingen.
Fehlermeldung, die Reihenfolge der Nachrichten stimme nicht: Meist fehlt vor einer tool-Nachricht die zugehörige assistant-Nachricht mit tool_calls, oder die tool_call_id passt nicht. Wie im Code oben: zuerst die Nachricht des Modells, dann die Tool-Ergebnisse einzeln.
Übungen
- Frag nach einem Paket, das es auf PyPI nicht gibt, etwa „Was ist die neueste Version von httpxx?“, und schau dir die Fehlermeldung des Tools an und wie das Modell dem Nutzer antwortet.
- Füg ein weiteres Tool
get_github_stars(repo)hinzu, das über die öffentliche GitHub-Schnittstellehttps://api.github.com/repos/{repo}die Sterne eines Repositorys abfragt (ohne Schlüssel, aber mit stündlichem Limit). Frag „Was sind die neueste Version und die GitHub-Sterne von httpx?“ und schau, ob das Modell gleichzeitig zwei verschiedene Tools aufruft. - Ersetze
messages.append(message.model_dump(exclude_none=True))durch ein handgeschriebenes Dictionary mit nurcontentundtool_calls, führ es mit--thinkaus und schau, was passiert.
Selbsttest
1. Hat beim Tool-Aufruf das Modell die Funktion get_pypi_info ausgeführt?
Nein. Das Modell hat nur eine strukturierte Anfrage der Art „ich möchte get_pypi_info mit dem Parameter httpx aufrufen“ ausgegeben. Ausgeführt hat die Funktion dein Programm. Die Ausführung liegt vollständig beim Programm; du kannst die Aufrufanfragen des Modells prüfen, ändern oder ablehnen.
2. Das Modell fordert zwei Tool-Aufrufe auf einmal an. Wie teilst du ihm die beiden Ergebnisse getrennt mit?
Jede Aufrufanfrage hat eine eindeutige id. Für jeden Aufruf fügst du eine Nachricht mit role gleich tool hinzu und trägst in tool_call_id die id der zugehörigen Anfrage ein; daran ordnet das Modell Ergebnisse und Anfragen einander zu.
3. Worauf muss man bei Tools im Denkmodus achten?
Der reasoning_content jeder früheren Nachricht des Modells muss unverändert an die API zurückgegeben werden. Am einfachsten legt man mit message.model_dump(exclude_none=True) die ganze Nachricht des Modells zurück in den Verlauf, statt nur content und tool_calls von Hand herauszupicken.
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…