13. Tools mit dem LLM verbinden
In Kapitel 12 haben wir Tools erstellt, die ein Agent verwenden kann. Jetzt ist es an der Zeit, diese Tools mit dem LLM zu verbinden, damit der Agent sie tatsächlich nutzen kann.
Egal, wie gut ein Tool gebaut ist — das LLM kann es nicht verwenden, ohne zu wissen, dass es existiert. Es muss wissen, was verfügbar ist, und für jede Anfrage das richtige Tool auswählen. Wie wir jedoch in Kapitel 12 gelernt haben, führt das LLM Tools nicht direkt aus. Stattdessen fordert es an: „Rufe dieses Tool mit diesen Argumenten auf“, und unser Agent-Code ruft das Tool auf. Dieser gesamte Mechanismus wird Tool Calling genannt.
In diesem Kapitel binden wir Tools an das LLM, führen Tool-Call-Anfragen aus und geben die Ergebnisse an das LLM zurück — und implementieren so den vollständigen Zyklus. Dieser Zyklus ist die Grundlage der Agent-Schleife, die wir in Kapitel 14 aufbauen werden.
13.1) Tools binden und Tool-Call-Anfragen prüfen
Damit das LLM Tools verwenden kann, muss es zunächst wissen, was verfügbar ist. Wenn wir name, description und das Eingabeschema jedes Tools an das LLM übergeben, lernt es, wann und wie jedes Tool zu verwenden ist. Dieser Vorgang wird Tool Binding genannt.
Sobald Tools gebunden sind, wird das LLM bei einer Frage eine von zwei Aktionen ausführen. Wenn kein Tool benötigt wird, antwortet es wie gewohnt mit Text. Wenn ein Tool benötigt wird, gibt es eine strukturierte Anfrage zurück: „Rufe dieses Tool mit diesen Argumenten auf.“ Unser Agent prüft dann diese Anfrage und ruft das angegebene Tool auf.
Beginnen wir damit, wie Tools gebunden werden.
13.1.1) Tools mit bind_tools() binden
Tool Binding wird von einer einzigen Methode übernommen: bind_tools(). Jedes Chat-Modell, das Tool Calling unterstützt, stellt diese Methode bereit. Der Aufruf von bind_tools() gibt ein neues Modellobjekt zurück, an das die angegebenen Tools gebunden sind.
Binden wir das Tool get_weather, das wir in Kapitel 12 erstellt haben.
from langchain_openai import ChatOpenAI
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Gibt das aktuelle Wetter für eine bestimmte Stadt zurück."""
return f"In {city} ist es immer sonnig!"
# Bindet das Tool an das Modell — ein neues Modellobjekt mit gebundenem Tool wird zurückgegeben
llm_with_tools = llm.bind_tools([get_weather])Mehr braucht es nicht. Das zurückgegebene llm_with_tools ist ein Modell, das das Tool get_weather kennt. Beachten Sie, dass sich das ursprüngliche llm überhaupt nicht verändert hat — es weiß nach wie vor nichts von irgendwelchen Tools.
bind_tools() nimmt eine Liste von Tools als Argument entgegen und gibt ein neues Modellobjekt zurück, an das diese Tools gebunden sind. Intern konvertiert es die Metadaten jedes Tools (name, description und Eingabeschema) in ein Format, das der LLM-Anbieter versteht, sodass die Tool-Schemata bei jedem Modellaufruf mitgesendet werden.
13.1.2) Tool-Call-Anfragen prüfen
Was passiert, wenn wir eine Wetterfrage an ein Modell senden, an das ein Wetter-Tool gebunden ist?
response = llm_with_tools.invoke("Wie ist das Wetter in Paris?")
print(f"type: {type(response)}\n")
print(f"tool_calls: {response.tool_calls}\n")
print(f"content: {repr(response.content)}\n")Ausgabe:
type: <class 'langchain_core.messages.ai.AIMessage'>
tool_calls: [{'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}]
content: ''Da llm_with_tools weiterhin ein Chat-Modell ist, gibt es ein AIMessage zurück. Aber zwei Dinge sind an dieser Antwort anders.
Erstens: response.tool_calls enthält eine Liste von Tool-Call-Anfragen. Jede Anfrage ist ein Dictionary mit vier Schlüsseln:
name— der Name des aufzurufenden Tools. Dieser entspricht demnamedes Tools aus seinen Metadaten.args— die Argumente, die beim Aufruf des Tools übergeben werden. Das Modell hat das Eingabeschema und die Frage des Benutzers betrachtet, um{'city': 'Paris'}zu konstruieren.id— ein eindeutiger Bezeichner für diesen Aufruf. Das LLM verwendet diesen später, um Tool-Ausführungsergebnisse mit Tool-Call-Anfragen abzugleichen.type— immer'tool_call'.
Zweitens: response.content ist ein leerer String. Diese Antwort ist keine endgültige Textantwort — sie ist eine Tool-Call-Anfrage.
Um zu bestimmen, ob die Antwort eine Tool-Call-Anfrage ist, prüfen Sie tool_calls statt content. Wenn tool_calls nicht leer ist, handelt es sich um eine Tool-Call-Anfrage. Wenn es leer ist, hat das LLM direkt eine Textantwort geliefert.
13.2) Tools ausführen und Ergebnisse zurückgeben
Das LLM hat einen Aufruf des Tools get_weather angefordert. Jetzt muss unser Agent-Code das Tool ausführen und das Ergebnis an das LLM zurückgeben.
Die Verarbeitung einer Tool-Call-Anfrage umfasst drei Schritte:
- Extrahieren Sie den Tool-Namen und die Argumente aus
tool_callsund führen Sie das entsprechende Tool aus. - Konvertieren Sie das Ausführungsergebnis in ein
ToolMessage. - Senden Sie die vollständige Konversation (Benutzerfrage + Tool-Call-Anfrage des LLM + Tool-Ausführungsergebnis) an das LLM zurück, um eine Antwort zu erhalten.
Hier ist ein Diagramm dieses Prozesses:
13.2.1) Das Tool ausführen und ein ToolMessage erstellen
Zunächst extrahieren wir den Tool-Namen und die Argumente aus tool_calls und führen das entsprechende Tool aus. Um ein Tool anhand seines Namens nachzuschlagen, erstellen wir ein Dictionary mit dem Tool-Namen als Schlüssel.
# Ein Dictionary, um Tools anhand des Namens nachzuschlagen
tool_map = {get_weather.name: get_weather}Als Nächstes holen wir eine Tool-Call-Anfrage aus tool_calls, finden das passende Tool in tool_map und führen es aus.
if response.tool_calls:
tool_call = response.tool_calls[0]
# {'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
print(result) # Ausgabe: In Paris ist es immer sonnig!Um das Tool-Ausführungsergebnis an das LLM zurückzugeben, müssen wir es in ein ToolMessage verpacken. Ein ToolMessage hat zwei erforderliche Felder:
content— das Tool-Ausführungsergebnis als String.tool_call_id— dieidaustool_calls. Das LLM verwendet diese, um zu identifizieren, zu welcher Anfrage das Ergebnis gehört.
from langchain_core.messages import ToolMessage
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
tool_message = ToolMessage(
content=result,
tool_call_id=tool_call["id"], # muss mit der id der Anfrage übereinstimmen
)
print(tool_message)Ausgabe:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'Im obigen Code haben wir args übergeben, um das Tool auszuführen, und dann das Ergebnis mit der tool_call_id kombiniert, um das ToolMessage zu erstellen.
Es gibt einen einfacheren Weg, dies in einem einzigen Schritt zu erledigen. Statt nur args an .invoke() zu übergeben, übergeben Sie das gesamte tool_call-Dictionary. LangChain führt das Tool aus und gibt automatisch ein ToolMessage zurück.
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
print(tool_message)Ausgabe:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'Dasselbe Ergebnis, aber der Code ist viel einfacher, und es besteht keine Gefahr einer Nichtübereinstimmung der tool_call_id. Wir werden diesen Ansatz fortan verwenden.
13.2.2) Tool-Ergebnisse an das LLM senden
Nun übergeben wir das Tool-Ausführungsergebnis an das LLM. Wir senden den vollständigen Konversationsverlauf — die Benutzerfrage, die Tool-Call-Anfrage des LLM und das Tool-Ausführungsergebnis — an das LLM zurück, und es generiert eine Antwort auf Basis des Tool-Ergebnisses.
Fügen wir den gesamten Prozess von Anfang bis Ende in einem einzigen Code-Stück zusammen.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
# Tools definieren und binden
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Gibt das aktuelle Wetter für eine bestimmte Stadt zurück."""
return f"In {city} ist es immer sonnig!"
llm_with_tools = llm.bind_tools([get_weather])
tool_map = {get_weather.name: get_weather}
# Schritt 1: Die Frage des Benutzers senden und die Antwort des LLM erhalten
messages = [HumanMessage("Wie ist das Wetter in Paris?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
# Schritt 2: Wenn ein Tool-Call angefordert wurde, das Tool ausführen und das Ergebnis hinzufügen
if ai_msg.tool_calls:
tool_call = ai_msg.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
# Schritt 3: Den vollständigen Konversationsverlauf an das LLM zurücksenden
final_response = llm_with_tools.invoke(messages)
print(final_response.content)Ausgabe:
Das aktuelle Wetter in Paris ist sonnig. Einen schönen Tag noch!Das LLM hat eine endgültige Textantwort auf Basis des Tool-Ausführungsergebnisses generiert. Zurück in 13.1 war content ein leerer String — aber nachdem wir das Tool-Ergebnis zurückgegeben haben, haben wir eine tatsächliche Antwort erhalten.
Prüfen wir, welche Nachrichten sich in Schritt 3 in der Liste messages befinden.
for m in messages:
print(f"{type(m).__name__}: {m.content!r}")Ausgabe:
HumanMessage: "What's the weather in Paris?"
AIMessage: ''
ToolMessage: "It's always sunny in Paris!"Drei Nachrichten sind der Reihe nach gespeichert: die Frage des Benutzers, die Tool-Call-Anfrage des LLM (leerer Inhalt) und das Tool-Ausführungsergebnis. Das LLM hat die Frage des Benutzers gesehen, entschieden, dass es ein Tool benötigt, und einen Aufruf von get_weather angefordert. Der Agent-Code hat get_weather ausgeführt und das Ergebnis zur Nachrichtenliste hinzugefügt. Sobald es alles hatte, was es brauchte, generierte das LLM seine endgültige Antwort.
13.3) Dem LLM mehrere Tools geben
Reale Agents haben typischerweise mehrere Tools. Alles, was wir bisher behandelt haben, funktioniert mit mehreren Tools genau gleich.
Fügen wir calculate und search_web neben get_weather hinzu, um drei Tools zu binden.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Gibt das aktuelle Wetter für eine bestimmte Stadt zurück."""
return f"In {city} ist es immer sonnig!"
@tool
def calculate(expression: str) -> str:
"""Wertet einen einfachen arithmetischen Ausdruck wie '123 * 456' aus."""
return str(eval(expression)) # Warnung: eval() ist ein Sicherheitsrisiko. Nicht in der Produktion verwenden.
@tool
def search_web(query: str) -> str:
"""Durchsucht das Web nach aktuellen Informationen zu einem Thema."""
return f"Top-Ergebnis für '{query}': ..."
tools = [get_weather, calculate, search_web]
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}Stellen wir eine Rechenfrage.
response = llm_with_tools.invoke("Was ist 123 multipliziert mit 456?")
print(response.tool_calls)Ausgabe:
[{'name': 'calculate', 'args': {'expression': '123 * 456'}, 'id': 'call_xyz789', 'type': 'tool_call'}]Das LLM hat einen Aufruf von calculate angefordert. Es hat die description jedes Tools mit der Frage des Benutzers verglichen und festgestellt, dass calculate das richtige Tool war.
Wir können das Tool ausführen und mit demselben Muster aus 13.2 eine endgültige Antwort erhalten.
messages = [HumanMessage("Was ist 123 multipliziert mit 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content) # Ausgabe: 123 multipliziert mit 456 ergibt 56.088.In 13.2 haben wir nur die erste Anfrage mit tool_calls[0] verarbeitet, aber hier iterieren wir mit einer for-Schleife über alle Anfragen. Der Grund dafür ist, dass das LLM mehrere Tool-Calls gleichzeitig anfordern kann.
Mehrere gleichzeitige Tool-Anfragen
Sehen wir uns an, was passiert, wenn mehrere Tools gleichzeitig angefordert werden. Der Code ist derselbe — nur die Frage ändert sich.
messages = [HumanMessage("Wie ist das Wetter in Tokio, und was ist 123 * 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
print(f"Tool: {tool_call['name']}")
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content)Ausgabe:
Tool: get_weather
Tool: calculate
Das Wetter in Tokio ist derzeit sonnig, und 123 multipliziert mit 456 ergibt 56.088.Das LLM hat sowohl get_weather als auch calculate angefordert, und die for-Schleife hat alle Anfragen in tool_calls ausgeführt und ihre Ergebnisse zur Nachrichtenliste hinzugefügt. Das LLM hat diese Ergebnisse dann verwendet, um die endgültige Antwort zu generieren.