Aufbau eines Agentenverbesserungszyklus mit LangGraph- und LangSmith-Bewertungen.
Ein schrittweiser LangGraph- und LangSmith-Zyklus: Datenerfassung, drei Bewertungsebenen, Clusterbildung bei Fehlern, Offline-Regressionssätze und Vorabbewertungen.
Quellen: Architecting the Agent Improvement Loop: Production-Grade Eval Pipelines
Es erfordert am wenigsten Aufwand, einen Agenten in die Produktion zu bringen. Erst das Verbessern danach unterscheidet ein Hobbyprojekt von einem stabilen System. Viele Teams veröffentlichen eine erste Version und verbringen anschließend Monate damit, nacheinander Beschwerden zu bearbeiten. Jeder Patch ist eine Reaktion auf einen einzelnen Fall, jede Korrektur bleibt lokal begrenzt, und der Agent verbessert sich insgesamt nie. Es häufen sich nur Workarounds an.
Die übliche Ursache ist, einen Agenten wie herkömmliche Software zu behandeln. In gewöhnlichen Programmen erklärt der Code, was das System tut, sodass ein Fehler bedeutet, den Quellcode bis zur Findung des Fehlers durchzulesen. Bei Agenten gilt diese Annahme nicht. Der Code beschreibt nur, was dem System erlaubt ist zu tun, während die Spuren aufzeichnen, was es tatsächlich getan hat. Diese Lücke bildet die Grundlage jedes seriösen Bewertungspipelines, einschließlich derer, die Anthropic in seinen technischen Schriften und LangChain in der LangSmith-Dokumentation beschreibt.
Was folgt, ist ein vollständiger Verbesserungslauf für einen LangGraph-Agenten. Er beginnt mit einem einfachen ReAct-Agenten, erfasst dessen Spuren in LangSmith, fügt drei Bewertungsebenen hinzu, identifiziert wiederkehrende Fehlermuster, wandelt diese Fehler in einen dauerhaften Offline-Testdatensatz um und führt vor dem Veröffentlichen jeder Korrektur eine Regressionsevaluation durch. Jede Phase enthält Code, eine Erklärung sowie die erwarteten Ausgaben.
Der Lauf funktioniert wie folgt:
- Der Agent bearbeitet eine echte Benutzeranfrage und erzeugt eine Spur, die jede LLM-Aufruf, Tool-Aufruf sowie Zwischenergebnisse umfasst.
- Codebasierte Überprüfungen bewerten jede Spur hinsichtlich Format und Korrektheit des Tools, ein LLM-Bewerter bewertet Hilfreichkeit und Relevanz, und Menschen prüfen die subtilen Fehler.
- Spuren mit niedrigen Bewertungen werden in Fehlermuster wie das falsche Tool, eine Abweichung im Denkprozess oder ein übersehener Intent eingeteilt.
So wird ein Agent im Laufe der Zeit besser, anstatt nur Patches anzuhäufen. Die sechs Phasen sind:
- Phase 1, Grundlage: Ein LangGraph ReAct-Agent mit LangSmith-Tracking.
- Phase 2, Trace-Sammlung: Die Produktionsergebnisse werden als strukturierte Datensätze abgerufen.
- Phase 3, Bewertung: Code-Prüfungen, eine LLM-Bewertung und menschliche Überprüfung.
- Phase 4, Mustererkennung: Die bewerteten Traces werden in Fehlerkategorien umgewandelt.
- Phase 5, Offline-Testsets: Fehler werden in ein für Regressionstests sicheres Datensatz umgewandelt.
Phase 1: Der Grundlagen-Agent
In der ersten Phase wird ein kleiner Kundenservice-Agent erstellt, der Fragen zu einem Konto beantwortet. Er verfügt über zwei Tools – eines zur Abfrage von Kontoinformationen und eines zur Überprüfung neuer Bestellungen – und nutzt LangGraphs vorgefertigten ReAct-Agent, das Standardmuster für Agenten mit Toolaufrufen. Die LangSmith-Trace-Funktion ist aktiviert, sodass jeder Ausführungsvorgang sowie jeder LLM- und Toolaufruf automatisch aufgezeichnet wird.
Drei Komponenten sorgen dafür, dass dies funktioniert:
- Der LangGraph StateGraph bildet die zugrundeliegende Struktur und leitet den Datenfluss zwischen einem Knoten, der entscheidet, was zu tun ist, und einem Knoten, der ein Tool ausführt. Der Helper
create_react_agentumhüllt dieses Muster.
LANGSMITH_TRACING=true gesetzt ist. Danach erzeugt jeder Aufruf von LangChain oder LangGraph eine Überwachungsdatenstruktur, ohne dass Code geändert werden muss.@tool von LangChain dekoriert sind, wodurch jede Funktion dem Agenten mit einem Namen und einer Beschreibung zur Verfügung gestellt wird, die die Auswahl des Tools beeinflusst.Umgebungs-Einrichtung
Zuerst muss man die Bibliotheken installieren:
# Install the libraries we need
pip install langgraph langchain langchain-openai langsmith
Vier Pakete werden installiert. langgraph stellt die Graphen-Laufzeitumgebung sowie den vorkompilierten ReAct-Agent bereit. langchain liefert den @tool-Decorator sowie Nachrichtentypen. langchain-openai umhüllt das OpenAI-Chat-Modell in die LangChain-Schnittstelle. langsmith ist die Client-Bibliothek, die zum Zugriff auf den LangSmith-Backend für Tracking und Bewertungen verwendet wird.
Danach kommen die Umgebungsvariablen, die das Tracking einschalten, ohne den Code zu verändern:
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=your_langsmith_key
export LANGSMITH_PROJECT=agent-improvement-loop
export OPENAI_API_KEY=your_openai_key
LANGSMITH_TRACING=true ist der Schalter. LANGSMITH_PROJECT gibt dem Projekt einen Namen, das die Spuren gruppiert, wodurch diese später gefiltert werden können. Die anderen beiden Variablen sind Anmeldeinformationen für LangSmith und den Modellanbieter.
Sobald diese vier Variablen gesetzt sind, sendet jeder Aufruf von LangChain oder LangGraph im Prozess eine Spur an LangSmith. Es ist nicht notwendig, etwas zu umhüllen oder zu verändern, da die Spurverfolgung im Hintergrund abläuft.
Erstellung des Agenten
Zuerst die Imports:
# Step 1: Import the pieces we need
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
Die Importe beinhalten drei Komponenten. ChatOpenAI umhüllt die OpenAI-Chat-Completions-API für LangChain. Der tool-Decorator wandelt eine einfache Python-Funktion in eine aufrufbare Funktion um, die vom Agenten verwendet werden kann. create_react_agent ist ein fertiggestellter LangGraph-Hilfsfunktion, der einen ReAct-stilen Graphen zur Aufrufung von Tools erstellt.
Dann die beiden Tools:
# Step 2: Define two tools the agent can call
@tool
def get_account_info(account_id: str) -> str:
"""Look up basic information for an account by account_id."""
fake_accounts = {
"A100": "Account A100: plan=pro, status=active, email=ada@example.com",
"A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
}
return fake_accounts.get(account_id, f"No account found with id {account_id}")
@tool
def get_recent_orders(account_id: str) -> str:
"""Return the three most recent orders for a given account_id."""
fake_orders = {
"A100": "Orders for A100: #9001 shipped, #9002 processing, #9003 returned",
"A200": "Orders for A200: no orders in the last 90 days",
}
return fake_orders.get(account_id, f"No orders for {account_id}")
Jedes Tool akzeptiert einen account_id und gibt eine Zeichenkette zurück. Die Dokumentation jeder Funktion ist wichtig, da das LLM diesen Text liest, um zu entscheiden, welches Tool aufgerufen werden soll. Der Dekorator zieht den Funktionsnamen, die Parameter sowie die Dokumentation heran, um das Schema zu erstellen, das vom Modell verwendet wird. In einem echten System würden diese Funktionen eine Datenbank oder ein API abfragen, doch die Demo gibt hardcodierte Zeichenketten zurück, damit sie selbstständig und reproduzierbar bleibt.
Schließlich wird der Agent zusammengesetzt:
# Step 3: Wire the tools into a ReAct agent
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tools = [get_account_info, get_recent_orders]
agent = create_react_agent(model, tools)
Das Chat-Modell wird mit einer Temperatur von null erstellt, um Reproduzierbarkeit zu gewährleisten. Die Tools werden in eine Liste aufgenommen und zusammen mit dem Modell an create_react_agent übergeben, welches einen kompilierten LangGraph StateGraph zurückgibt, der bereits einen Schlussfolgerungs-Schritt, einen Tool-Ausführungsschritt sowie die bedingten Verbindungen zwischen ihnen enthält.
Testen des Agents
Eine einfache Anfrage dient dazu, ihn zu testen:
# Step 4: Invoke the agent with a simple query
result = agent.invoke({
"messages": [{"role": "user", "content": "What plan is account A100 on?"}]
})
for message in result["messages"]:
print(message.type, ":", message.content)
Die Aufruffunktion übergeben einen Dictionary, der eine messages-Schlüssel enthält. Das liegt daran, dass create_react_agent auf dem vordefinierten MessagesState-Schema beruht, dessen Zustand lediglich eine Liste von Chatnachrichten ist und vom Agenten erweitert wird, während dieser nachdenkt und Tools aufruft. Anschließend gibt der Loop jeden Nachrichtentyp zusammen mit seinem Inhalt aus, sodass der gesamte Ablauf sichtbar ist.
Die erwartete Ausgabe:
human : What plan is account A100 on?
ai :
tool : Account A100: plan=pro, status=active, email=ada@example.com
ai : Account A100 is on the pro plan.
Der Agent erhielt die Benachrichtigung des Nutzers und sendete anschließend eine KI-Nachricht, die einen Tool-Aufruf auslöste (ihr Inhalt ist leer, da sie lediglich einen Tool-Aufruf enthält und keinen Text). Das Tool gab den Kontostring zurück, und der Agent beendete die Kommunikation mit einer Antwort. Beim Öffnen von LangSmith wird nun eine neue Spur im agent-improvement-loop-Projekt angezeigt, wobei jeder Schritt aufgezeichnet wird.
In diesem Code wurde die Spur nicht erfasst. Sobald LANGSMITH_TRACING=true im Umfeld gesetzt ist, sendet jeder Aufruf Eingaben, Ausgaben sowie Zwischenschritte an LangSmith. Genau diese umgebungsbezogene Überwachungsfähigkeit ist die Grundlage für den weiteren Ablauf des Loops.
In der ersten Phase läuft ein Agent, der Tool-Aufrufe durchführt, unter LangGraph – bei jedem Lauf wird dabei eine Spur erzeugt. Eine in der Anzeige vorhandene Spur ist jedoch noch kein Signal für eine Verbesserung. Sie muss extrahiert und weiterverarbeitet werden, was die Aufgabe der nächsten Phase ist.
Phase 2: Erfassung von Spuren
In dieser Phase werden Spuren aus LangSmith in den Analysecode übertragen. Eine Spur stellt einen vollständigen Agentenaufruf dar, einschließlich Eingaben, Ausgaben, Toolaufrufen und Latenzzeiten. Der LangSmith Python-Client listet die Aufrufe des Projekts auf, filtert sie nach Zeitfenster und Status und wandelt sie in Objekte um, die die Bewertungsphase verwenden kann.
Mehrere Begriffe erleichtern das Verständnis des Codes:
- Ein Aufruf ist ein einzelner aufgezeichneter Vorgang – entweder ein vollständiger Agentenaufruf oder ein darin eingebetteter LLM-Aufruf. Mehrere Agentenaufrufe bilden zusammen einen Baum von Aufrufen.
- Ein Wurzelaufruf ist der oberste Ebene-Aufruf eines Agentenaufrufs; er wird angezeigt, wenn pro Benutzeranfrage ein Eintrag gewünscht ist.
- Ein Projekt ist der Namensraum, zu dem ein Aufruf gehört und wird über
LANGSMITH_PROJECTfestgelegt. Das Filtern nach Projekt trennt Produktionsaufrufe von lokalen Entwicklungsaufrufen.
Aufrufe der letzten Zeit abrufen
Zuerst wird der Client erstellt:
# Step 1: Create a LangSmith client
from langsmith import Client
from datetime import datetime, timedelta
client = Client()
Client wird aus dem langsmith-Paket importiert und ohne Argumente erstellt. Er liest LANGSMITH_API_KEY automatisch aus der Umgebung ein, sodass keine Anmeldeinformationen übergeben werden müssen.
Anschließend werden die jüngsten Root-Ausführungen aufgelistet:
# Step 2: List recent root runs from our project
recent_runs = list(client.list_runs(
project_name="agent-improvement-loop",
is_root=True,
start_time=datetime.utcnow() - timedelta(days=1),
))
print(f"Found {len(recent_runs)} root runs in the last 24 hours")
client.list_runs wird mit drei Filtern aufgerufen. project_name beschränkt die Abfrage auf das Projekt, is_root=True behält nur die Aufrufe auf Ebene des obersten Agenten bei und nicht jeden verschachtelten Aufruf eines LLM oder einer Tool, und start_time beschränkt die Ergebnisse auf die letzten 24 Stunden. Der Iterator ist träge, weshalb list() ihn in eine echte Liste umwandelt.
Die erwartete Ausgabe:
Found 42 root runs in the last 24 hours
Nur die wichtigen Felder beibehalten
Ein unverarbeiteter LangSmith-Ausführungsvorgang enthält viele Felder, doch zur Bewertung sind nur fünf Dinge erforderlich: die Eingabe, die Ausgabe, die Latenzzeit, eventuelle Fehler sowie die Unter-Ausführungen, die zeigen, welche Tools verwendet wurden.
# Step 3: Build a compact trace record from each run
def compact_trace(run):
return {
"run_id": str(run.id),
"inputs": run.inputs,
"outputs": run.outputs,
"error": run.error,
"latency_ms": (run.end_time - run.start_time).total_seconds() * 1000
if run.end_time else None,
"tool_calls": [
child.name for child in client.list_runs(
trace_id=run.trace_id, run_type="tool"
)
],
}
traces = [compact_trace(run) for run in recent_runs]
print(f"Collected {len(traces)} compact traces")
print("Example tool calls:", traces[0]["tool_calls"])
Dieser Helper nimmt einen Rohausführungsvorgang entgegen und reduziert ihn auf ein kleines Dictionary. Die Eingaben und Ausgaben (run.inputs, run.outputs) enthalten die Anfrage des Benutzers sowie die endgültige Antwort, während run.error bei Erfolg None ist und bei Fehler einen String enthält. Die Latenz ergibt sich aus der Differenz zwischen start_time und end_time. Für Tool-Aufrufe ist eine weitere LangSmith-Anfrage mit run_type="tool" sowie derselben trace_id erforderlich, wodurch nur die Tool-Ausführungen dieses Ausführungstrees angezeigt werden.
Die erwartete Ausgabe:
Collected 42 compact traces
Example tool calls: ['get_account_info']
Die Idee besteht nicht darin, Spuren aus LangSmith zu kopieren und ihn anschließend aufzugeben. Die kompakte Version ist einfach leicht zu bewerten, während die vollständige Spur in LangSmith bleibt, um detailliert zu debuggen. Eine ansprechende Ansicht im Dashboard zusammen mit einer vereinfachten Version für die Analyse ist eine sinnvolle Methode, um Observabilität von Bewertung zu trennen.
Überprüfung des Sammlers
# Step 4: Inspect the first trace end to end
import json
print(json.dumps(traces[0], indent=2, default=str))
Der erste Datensatz wird als eingefasster JSON mit json.dumps ausgegeben. Der Parameter default=str sorgt dafür, dass json alles, was er nicht selbst serialisieren kann, in einen String umwandelt – das beinhaltet die Datums- und UUID-Typen, die LangSmith zurückgibt.
Die erwartete Ausgabe:
{
"run_id": "b1d2e3f4-...",
"inputs": {"messages": [{"role": "user", "content": "What plan is account A100 on?"}]},
"outputs": {"messages": [{"role": "ai", "content": "Account A100 is on the pro plan."}]},
"error": null,
"latency_ms": 1423.51,
"tool_calls": ["get_account_info"]
}
Jede Produktionscharge verfügt nun über eine saubere, strukturierte Aufzeichnung, die in der Bewertungsphase weiter ergänzt wird. Das Sammeln von Spuren kostet wenig, die eigentliche Herausforderung liegt darin, sie nützlich zu machen. Viele Teams bleiben bei der Kontrollleiste stehen und fragen sich, warum aus der Beobachtbarkeit nie eine Verbesserung wird. Spuren ohne Bewertungen sind nur Rauschen, und eine Spur ohne Bewertung ist lediglich eine Logzeile.
Phase 3: Bewertung der Spuren
Drei Bewertungsebenen liefern eine einzige, erweiterte Spur. Der Engineering-Beitrag von Anthropic zu den Bewertungen beschreibt genau diese Struktur: kodbasierte Bewertungstools, die schnell, objektiv und reproduzierbar sind, modellbasierte Bewertungstools, die flexibel und skalierbar sind, sowie menschliche Bewertungsmitarbeiter, die zwar das Goldstandard darstellen, aber teuer sind. Hier werden jeweils ein Beispiel jeder Art entwickelt.
- Codebasierte Überprüfungen sind schnelle, deterministische Funktionen, die strukturelle Fakten überprüfen, wie beispielsweise ob der Agent das richtige Werkzeug verwendet hat oder ob die Ausgabe die erwartete Formatierung aufweist. Sie erkennen offensichtliche Fehler ohne zusätzlichen Aufwand.
- Ein LLM-Bewerter ist ein weiteres Modell, das subjektive Eigenschaften wie Hilfreichkeit, Relevanz und Ton bewertet. Es kann skaliert werden, da es auf jeder Aufzeichnung ausgeführt wird, muss jedoch vor dem Vertrauen durch Menschen anhand menschlicher Beurteilungen kalibriert werden.
- Menschliche Überprüfung setzt einen Fachexperten ein, der sich mit den Aufzeichnungen befasst, die von der Automatisierung übersehen wurden oder ungleichmäßig bewertet wurden. Dies stellt die Referenzwahrheit dar, anhand derer die anderen beiden Ebenen kalibriert werden.
Ebene 1: Richtigkeit des Werkzeugs
# Step 1: Define a rule that says which tool SHOULD be called for a given query
import re
def expected_tool_for_query(query: str) -> str:
q = query.lower()
if re.search(r"\b(order|shipment|shipped|return)\b", q):
return "get_recent_orders"
if re.search(r"\b(plan|account|status|email)\b", q):
return "get_account_info"
return "any"
def score_tool_correctness(trace):
query = trace["inputs"]["messages"][0]["content"]
expected = expected_tool_for_query(query)
actual = trace["tool_calls"][0] if trace["tool_calls"] else None
if expected == "any":
return 1.0
return 1.0 if actual == expected else 0.0
Zwei Funktionen sind definiert. expected_tool_for_query wendet eine reguläre Ausdrucksregel auf den Abfragetext an, um zu ermitteln, welches Tool diese abarbeiten sollte. Das ist eine grobe Heuristik, die gut geeignet ist, offensichtliche Routingfehler aufzudecken, aber nicht für die Produktion. score_tool_correctness nimmt die Benutzerabfrage aus dem Trace, berechnet das erwartete Tool und vergleicht es mit dem ersten Tool, das der Agent tatsächlich aufgerufen hat. Ein Nichtübereinstimmen ergibt eine Bewertung von 0,0 und ein Übereinstimmen eine Bewertung von 1,0.
In einer Produktionsversion würde man die reguläre Ausdrucksregel durch einen leistungsstärkeren Klassifikator oder ein Regelwerk ersetzen, das auf Intentionen ausgerichtet ist. Das Prinzip bleibt dasselbe: Eine deterministische Überprüfung erfolgt vor den aufwändigen, auf LLMs basierenden Evaluatoren.
Schicht 2: Ein LLM als Richter für Hilfreichkeit
# Step 2: Score qualitative properties with an LLM judge
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
judge_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
judge_prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert reviewer of AI agent responses. "
"Rate the response on a scale of 1 to 5 for helpfulness. "
"Respond with only a single integer, nothing else."),
("user", "User question: {question}\n\nAgent response: {response}\n\nScore:"),
])
def score_helpfulness(trace):
question = trace["inputs"]["messages"][0]["content"]
response = trace["outputs"]["messages"][-1]["content"]
chain = judge_prompt | judge_model
result = chain.invoke({"question": question, "response": response})
try:
return int(result.content.strip()) / 5.0
except ValueError:
return 0.0
Eine kleine LangChain-Kette wird erstellt, indem ein Prompt-Muster in das Judge-Modell eingespeist wird. Die Systemnachricht beschränkt den Judge daran, nur eine ganze Zahl von 1 bis 5 zurückzugeben, sodass das Ergebnis deterministisch interpretiert werden kann. Innerhalb von score_helpfulness werden die Benutzerfrage sowie die endgültige Agentenantwort aus dem Trace abgerufen, die Kette wird ausgeführt und die ganze Zahl auf einen Float-Wert zwischen 0 und 1 skaliert, damit sie gut mit der Tool-Score kombiniert werden kann. gpt-4o-mini dient als Judge, weil es günstig und schnell ist.
LLM-Judges sind stochastisch, wodurch zwei Ausführungen am selben Trace leicht unterschiedliche Scores liefern können. Anthropics Richtlinien empfehlen, einen Judge vor der Nutzung anhand einer von Menschen gelabelten Datensammlung zu kalibrieren, und die dritte Schicht erstellt genau diese Datensammlung.
Schicht 3: menschliche Überprüfung als Annotation-Warteschlange
LangSmith verfügt über eine eingebaute Anmerkungswarteschlange, in der Nutzer die Spuren im Dashboard kennzeichnen können. Um das Beispiel selbstständig zu halten, wird in der Code-Implementierung derselbe Aufbau nachgebildet.
# Step 3: Model a human review signal
def score_human(trace, human_labels: dict) -> float | None:
run_id = trace["run_id"]
return human_labels.get(run_id)
human_labels = {
"b1d2e3f4-...": 1.0,
"c2e3f4g5-...": 0.0,
}
Die Funktion sucht in einem Wörterbuch mit von Menschen bereitgestellten Labels nach einer Lauf-ID. Wenn ein Label vorhanden ist, wird es zurückgegeben; falls nicht, gibt die Funktion None zurück, was bedeutet, dass der Lauf noch nicht überprüft wurde. In der Produktion würde dieses Wörterbuch hinter LangSmiths Annotation-Queue-API liegen. Entscheidend ist das Verhalten der Scores: Sie sind spärlich, da nur einige Spuren sie erhalten; sie sind autoritativ, weil sie bei Meinungsverschiedenheiten die anderen überschreiben; zudem dienen sie zur Kalibrierung des LLM-Judgers.
Vereinigung der drei Schichten
# Step 4: Run all three scorers and attach scores to each trace
def enrich(trace):
trace["scores"] = {
"tool_correctness": score_tool_correctness(trace),
"helpfulness": score_helpfulness(trace),
"human": score_human(trace, human_labels),
}
return trace
enriched = [enrich(t) for t in traces]
print(json.dumps(enriched[0]["scores"], indent=2))
Funktion enrich fügt alle drei Werte zu einer Spur hinzu und gibt sie zurück. Sie wird auf jede während der Sammlung erfasste Spur angewendet, wodurch eine Liste von bereicherten Spuren entsteht, die jeweils ein strukturiertes scores-Diktum enthalten. Die regelbasierte Regex erzeugt tool_correctness, der LLM-Judge erzeugt helpfulness, und human ist ein Float oder None.
Erwartetes Ausgabeformat:
{
"tool_correctness": 1.0,
"helpfulness": 0.8,
"human": null
}
Die drei Bewertungen sind absichtlich unterschiedlich. Die Prüfung auf Tool-Korrektheit besagt, dass das richtige Tool verwendet wurde, während der LLM-Bewerter angibt, die Antwort verdient 4 von 5 Punkten. Ein später hinzukommender menschlicher Prüfer könnte sagen, das Tool war richtig, aber die Formulierung passte nicht zum Ton der Marke. Jede Schicht erkennt das, was die anderen übersehen, wodurch durch das Stapeln von Schichten eine umfassendere Abdeckung sowie zu unterschiedlichen Kosten ein verlässlicheres Ergebnis erzielt werden.
Jeder Verlauf enthält nun drei Bewertungen: eine auf Regeln basierende, eine vom LLM bewertete und gegebenenfalls eine menschliche Bewertung. Bewertungen ohne Struktur sind fast genauso nutzlos wie gar keine, weshalb anschließend Muster herangezogen werden.
Phase 4: Erkennung von Fehlerrmustern
Sobald die Protokolle Werte enthalten, tritt derselbe Fehler tendenziell immer wieder auf. Der Agent wählt für eine bestimmte Formulierung das falsche Werkzeug. Sein Denkprozess gerät bei mehrschrittigen Anfragen ins Stocken. Seine Antworten sind technisch korrekt, verfehlen aber das, was der Benutzer wollte. Jedes Muster erfordert eine andere Art der Korrektur – sei es eine überarbeitete Beschreibung des Werkzeugs, eine Aktualisierung der Anweisungen oder eine umstrukturierte Arbeitsabläufe. Die Benennung des Musters ist der schwierige Teil, während die anschließende Behebung in der Regel einfach ist.
LangChain bietet einen Insights Agent, der speziell für diese Aufgabe in großem Maßstab entwickelt wurde. Er verarbeitet die Protokolle, gruppiert sie nach dem Fehlertyp und hilft Teams dabei, zu bestimmen, was zuerst Aufmerksamkeit verdient. Eine vereinfachte Version desselben Konzepts ist im folgenden Code enthalten.
Filtern nach Fehlern
# Step 1: Keep only traces where at least one score is low
def is_low_scoring(trace):
scores = trace["scores"]
if scores["human"] is not None and scores["human"] < 0.5:
return True
if scores["tool_correctness"] < 0.5:
return True
if scores["helpfulness"] < 0.6:
return True
return False
failures = [t for t in enriched if is_low_scoring(t)]
print(f"{len(failures)} of {len(enriched)} traces flagged as low scoring")
is_low_scoring führt drei Überprüfungen durch. Eine menschliche Bewertung unter 0,5 stuft den Verlauf als fehlerhaft ein. Andernfalls entscheiden die automatisierten Bewertungen: Eine Richtigkeit des Tools unter 0,5 bedeutet, dass der Agent falsch geleitet hat, und eine Hilfreichkeit unter 0,6 bedeutet, dass der LLM-Bewerter die Antwort nicht mochte. Die Schwellenwerte können angepasst werden, und dies sind sinnvolle Standardwerte für die erste Überprüfung. Der Filter wendet sich an die erweiterten Verläufe aus der vorherigen Phase, um die Liste der fehlerhaften Fälle zu erstellen.
Die erwartete Ausgabe:
9 of 42 traces flagged as low scoring
Fehler in Kategorien einteilen
Eine einfache Heuristik kennzeichnet hier die Fehler. In der Produktion würden Embeddings mit Clustering den Job erledigen, oder der Insights Agent von LangChain, der genau das auf Basis von LangSmith-Spuren durchführt.
# Step 2: Tag each failure with a category based on its scores
def categorize(trace):
scores = trace["scores"]
if scores["tool_correctness"] < 0.5:
return "wrong_tool"
if scores["helpfulness"] < 0.6 and scores["tool_correctness"] >= 0.5:
return "unhelpful_answer"
if scores["human"] is not None and scores["human"] < 0.5:
return "human_flagged"
return "other"
from collections import Counter
categories = Counter(categorize(t) for t in failures)
print(categories.most_common())
Funktion categorize prüft in Prioritätsreihenfolge. Ein falsches Werkzeug stellt das größte strukturelle Problem dar und wird daher zuerst gekennzeichnet. Eine unbrauchbare Antwort, bei der das richtige Werkzeug verwendet wurde, deutet auf Probleme bei der Erstellung der Anfrage oder der Antwort hin. Ein von einem Menschen als Fehler markierter Fall, den die Automatisierung durchgelassen hat, landet in einer eigenen Kategorie. Pythons Standardklasse Counter zählt jede Kategorie und wird direkt dort importiert, da es sich um die erste Verwendung handelt.
Erwartetes Endergebnis:
[('wrong_tool', 5), ('unhelpful_answer', 3), ('human_flagged', 1)]
Die Kategorie gibt Hinweise auf das geeignete Mittel. Eine Serie von fünf Fehlern mit dem falschen Werkzeug weist auf die Beschreibung des Werkzeugs oder die Anweisung zur Routenbestimmung hin. Drei unbrauchbare Antworten trotz des richtigen Werkzeugs deuten auf die Anweisung hin, die die Antworten erzeugt. Ein Fehler, der nur von einem Menschen festgestellt wurde, bedeutet, dass die automatischen Bewertungssysteme etwas übersehen haben, weshalb vermutlich eine neue Bewertung erforderlich ist.
Betrachtung eines einzelnen Musters
# Step 3: Print the inputs and outputs for every wrong_tool failure
wrong_tool = [t for t in failures if categorize(t) == "wrong_tool"]
for trace in wrong_tool[:3]:
print("QUERY:", trace["inputs"]["messages"][0]["content"])
print("TOOLS CALLED:", trace["tool_calls"])
print("RESPONSE:", trace["outputs"]["messages"][-1]["content"])
print("---")
Der Fehlerbereich wurde auf Fälle von wrong_tool eingegrenzt, und die Schleife gibt die Benutzeranfrage, die tatsächlich aufgerufenen Tools sowie die endgültige Antwort für die ersten drei aus. An dieser Stelle kann eine Person oder ein intelligenteres LLM die Fehler untersuchen und eine Hypothese aufstellen, beispielsweise dass jede Anfrage mit „subscription“ stattdessen get_recent_orders anruft anstelle von get_account_info. Diese Hypothese wird zum Ziel für die Korrektur.
Die erwartete Ausgabe:
QUERY: Is my subscription active?
TOOLS CALLED: ['get_recent_orders']
RESPONSE: I could not find any recent orders to confirm your subscription status.
---
QUERY: Tell me about my subscription to your product
TOOLS CALLED: ['get_recent_orders']
RESPONSE: There are no recent orders for this account.
---
Drei Beispiele auf dem Bildschirm machen das Muster klar. „Abonnement“ steht semantisch näher zu „Kontostatus“ als zu „Bestellungen“, doch der Agent leitet aufgrund einer oberflächlichen Überschneidung der Schlüsselwörter weiter. Die wahrscheinliche Lösung besteht darin, die Dokumentation von get_account_info in einer einzigen Zeile zu ändern, indem man Begriffe zum Abonnement hinzufügt, oder einen kurzen Systemhinweis einzufügen, der den Unterschied erklärt.
Die Phase endet mit Fehlerrichtmustern, die Zahlenangaben, Kategorien und Beispiele enthalten. Ein heute behobenes Muster ist wenig wert, wenn eine Änderung eines anderen Entwicklers es nächste Woche wieder zerstört – daher muss die Lösung durch einen Test gesichert werden.
Phase 5: Offline-Testsuite
Hier verwandeln sich Fehlschläge in statische Tests. Jeder echte Produktionsfehler wird in ein LangSmith-Datensatz hinzugefügt, wobei als Beispiel die Eingabe (die Benutzeranfrage), das erwartete Verhalten (das richtige Tool oder eine akzeptable Antwort) sowie optional Metadaten wie die Fehlerkategorie enthalten sind. Sobald er gespeichert wurde, verlässt er das Datensatz nie wieder, sodass ein derselbe Fehler nicht erneut auftreten kann.
Eine Offline-Bewertung in LangSmith besteht aus drei Teilen:
- Ein Datensatz ist eine benannte Sammlung von Beispielen, wobei jedes Beispiel Eingaben sowie optional erwartete Ausgaben enthält.
- Die Zielfunktion ist der getestete Code, der bei einem Agenten in der Regel seine
.invoke-Methode ist. - Bewertungsfunktionen sind Funktionen, die die Ausgabe des Ziels im Vergleich zu jedem Beispiel bewerten, ähnlich wie die Bewertungsfunktionen in der Bewertungsphase.
Datensatz erstellen
# Step 1: Create a LangSmith dataset for our agent
dataset_name = "agent-production-failures"
dataset = client.create_dataset(
dataset_name=dataset_name,
description="Production traces where the agent failed. Each example captures the user query and the expected tool call.",
)
print(f"Created dataset: {dataset.id}")
Derselbe client aus der Sammelphase ruft create_dataset mit einem Namen und einer Beschreibung auf und gibt ein Dataset-Objekt zurück, dessen .id später referenziert werden kann. Eine echte Implementierung würde dies mit einer Überprüfung auf „Erstelle nur, wenn nicht vorhanden“ schützen, aber die Demo geht von einem ersten Ausführungsvorgang aus.
Die erwartete Ausgabe:
Created dataset: 3f2a1b0c-...
Fehler in Beispiele umwandeln
# Step 2: Convert each failure trace into a dataset example
examples = []
for trace in failures:
query = trace["inputs"]["messages"][0]["content"]
expected_tool = expected_tool_for_query(query)
examples.append({
"inputs": {"question": query},
"outputs": {"expected_tool": expected_tool},
"metadata": {"category": categorize(trace), "source_run_id": trace["run_id"]},
})
client.create_examples(dataset_id=dataset.id, examples=examples)
print(f"Added {len(examples)} examples to {dataset_name}")
Der Code durchläuft jede Fehlerverfolgungsspur und erstellt ein Beispiel-Dict mit drei Feldern. inputs enthält die Anfrage des Benutzers, outputs den zu testenden Verhaltensmodus – in diesem Fall das Tool, das aufgerufen werden sollte – und metadata enthält die Kategorie sowie die ursprüngliche Lauf-ID, damit jedes Beispiel auf seine Quelle zurückverfolgt werden kann. Ein einziger batchierter Aufruf von client.create_examples lädt alles hoch.
Erwartetes Ausgabeformat:
Added 9 examples to agent-production-failures
Bei echten Implementierungen kann in der Regel ein Prüfer das erwartete Ergebnis bestimmen, da die Regex-Heuristik lediglich ein Ausgangspunkt ist. Eine Person, die jedes Beispiel freigibt, liefert eine zuverlässigere Referenzwahrheit – genau dafür dient LangSmiths Annotationsschleife.
Der Regressionsevaluator
# Step 3: Write an evaluator that checks the agent's tool call against the expected tool
def tool_match_evaluator(inputs: dict, outputs: dict, reference_outputs: dict):
actual_messages = outputs.get("messages", [])
tool_called = None
for m in actual_messages:
if getattr(m, "tool_calls", None):
tool_called = m.tool_calls[0]["name"]
break
expected = reference_outputs["expected_tool"]
score = 1.0 if (expected == "any" or tool_called == expected) else 0.0
return {"key": "tool_match", "score": score}
tool_match_evaluator folgt der von LangSmith erwarteten Signatur: Er erhält die inputs des Beispiels, die tatsächlichen outputs des Agents sowie die reference_outputs des Beispiels. Er durchsucht die Ausgabennachrichten des Agents, findet die erste, die einen Toolaufruf enthält, und extrahiert den Namen des Tools. Anschließend vergleicht er diesen mit dem erwarteten Tool und gibt ein Dictionary zurück, das einen key sowie einen score enthält. Der key wird zum Namen der in LangSmith angezeigten Metrik. Die Struktur entspricht dem offiziellen LangSmith-Evaluierungs-Quickstart.
Die Zielfunktion
# Step 4: Wrap our agent in a function that takes a LangSmith example and returns its output
def run_agent(inputs: dict) -> dict:
result = agent.invoke({
"messages": [{"role": "user", "content": inputs["question"]}]
})
return result
run_agent wird absichtlich auf ein Minimum beschränkt. Angesichts der Eingaben eines Datensatzbeispiels ruft es den Agenten auf und gibt das zurück, was als rohes Dictionary kommt – was mit dem übereinstimmt, was die evaluate-Funktion von LangSmith erwartet. Der aufgerufene Agent wurde bereits in der Grundlegungsphase erstellt.
Die bewusste Einfachheit des Wrappers ist beabsichtigt. Alles, was darin platziert wird, wird bei jeder Bewertung ausgeführt und erhöht somit die Kosten; eine einfache Weiterleitung bedeutet hingegen, dass der Bewertungsmechanismus den Agenten und nicht den Wrapper testet.
Sobald der Datensatz vorhanden ist und der Bewertungsmechanismus verbunden wurde, gilt jeder Produktionsfehler als dauerhafter Testfall. In der Endphase wird dieser Bewertungsmechanismus vor jedem Ausliefern einer Änderung ausgeführt.
Phase 6: Schließung des Kreislaufs
Dies ist das Regressionstor. Der Agent erhält eine Änderung, beispielsweise eine aktualisierte Toolbeschreibung zur Behebung des zuvor festgestellten „Subscription“-Routing-Fehlers. Vor dem Veröffentlichen wird das gesamte Datensatz der früheren Fehler durch die neue Version geprüft. LangSmith speichert beide Durchläufe als Experimente und zeigt Beispiel für Beispiel an, wo sich die neue Version verbessert hat und wo sie nachgelassen hat. Eine Veröffentlichung findet nur statt, wenn die neue Baseline die alte übertrifft und keine zuvor behobenen Probleme wieder auftreten.
Ausführung der Baseline-Bewertung
# Step 1: Run the evaluator across the dataset with the current agent
from langsmith.evaluation import evaluate
experiment_results = evaluate(
run_agent,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="agent-v1-baseline",
max_concurrency=4,
)
evaluate wird aus langsmith.evaluation importiert. Es erhält das Ziel run_agent, den Namen des Datensatzes (den LangSmith auf den Datensatz aus der vorherigen Phase verweist), die Liste der Evaluatoren, einen Experiment-Präfix zur Kennzeichnung des Laufs sowie eine Konkurrenzstufe von vier, damit die Bewertung parallel ausgeführt wird. Es gibt ein Experiment-Ergebnis-Objekt zurück, das auf den Lauf in LangSmith verweist.
Erwartetes Ausgabeformat:
View the evaluation results for experiment: 'agent-v1-baseline-...' at:
https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
9/9 runs | avg tool_match: 0.33
Von neun fehlerhaften Beispielen leitet der aktuelle Agent dennoch die meisten falsch weiter – und das ist der Ausgangspunkt.
Eine Korrektur versenden und erneut messen
Die vom Musterverarbeitungsschritt vorgeschlagene Lösung besteht darin, die get_account_info-Docstring in Sprache zu schreiben, die Abonnements beschreibt.
# Step 2: Update the tool docstring to teach the agent about subscriptions
@tool
def get_account_info_v2(account_id: str) -> str:
"""Look up basic information for an account, including plan, status,
subscription tier, and contact email. Use this tool for any question
about the account itself, including subscription status."""
fake_accounts = {
"A100": "Account A100: plan=pro, status=active, email=ada@example.com",
"A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
}
return fake_accounts.get(account_id, f"No account found with id {account_id}")
tools_v2 = [get_account_info_v2, get_recent_orders]
agent_v2 = create_react_agent(model, tools_v2)
def run_agent_v2(inputs: dict) -> dict:
result = agent_v2.invoke({
"messages": [{"role": "user", "content": inputs["question"]}]
})
return result
Das Tool zur Anzeige von Kontoinformationen wurde mit einer ausführlicheren Dokumentation neu definiert, die ausdrücklich den Abonnementstatus erwähnt. Da die Dokumentation das ist, was das LLM beim Auswählen eines Tools liest, behebt eine Erweiterung dieser Dokumentation direkt das Routing-Problem. Der Agent wird mit dem aktualisierten Tool neu aufgebaut und in ein neues run_agent_v2-Target eingebettet. Alles andere bleibt unverändert.
# Step 3: Run the evaluator on the v2 agent
experiment_results_v2 = evaluate(
run_agent_v2,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="agent-v2-docstring-fix",
max_concurrency=4,
)
Die identische Bewertung wird erneut am selben Datensatz mit demselben Bewertungsmechanismus durchgeführt, wobei nur das Zielwert ändert. Ein neuer experiment_prefix sorgt dafür, dass die Ausführung in der LangSmith-Oberfläche getrennt bleibt, damit sie mit dem Baseline verglichen werden kann. LangSmith leitet jedes Beispiel durch den v2-Agenten, speichert jede vollständige Ausführungsspur und bewertet jeden Output mit dem Bewertungsmechanismus.
Erwarteter Ausgabeinhalt:
View the evaluation results for experiment: 'agent-v2-docstring-fix-...' at:
https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
9/9 runs | avg tool_match: 0.89
Die Punktzahl stieg von 0,33 auf 0,89. Acht der neun Fehler wurden behoben, und der neunte ist eine wirklich unklare Anfrage, die möglicherweise in die Warteschlange für menschliche Anmerkungen gehört.
Before (v1): After (v2):
wrong_tool: 5 wrong_tool: 1
unhelpful: 3 unhelpful: 0
human: 1 human: 0
avg score: 0.33 avg score: 0.89
Mehrere Faktoren sorgten dafür, dass dieser Vergleich zuverlässig war:
- Die Ausgangsbewertung ergab bereits vor jeder Codeänderung eine konkrete Punktzahl.
- Die Korrektur bestand aus einer einzigen, präzisen Änderung – einem einzelnen Dokumentstring.
- Die erneute Bewertung maß die genaue Wirkung dieser Änderung anhand einer sorgfältig ausgewählten Sammlung realer Fehlerfälle.
- Das Testdatenset blieb unverändert, sodass der Vergleich vergleichbar war.
- Jeder ursprüngliche Fehlerfall wird nun als Test festgehalten, damit spätere Änderungen ihn nicht heimlich beeinträchtigen können.
Deshalb ist der Effekt schrittweise anwachsend. Jede Korrektur fügt Beispiele zum Datenset hinzu, und jede zukünftige Version wird gegenüber allen bisherigen Fehlern bewertet. Dadurch wird die Bereitstellung im Laufe der Zeit schneller und nicht langsamer, da das Vertrauen mit dem Datenset wächst.
Der gesamte Zyklus läuft nun von Anfang bis Ende. Es werden Spuren gesammelt und mit Scores angereichert, Muster treten zutage, Fehler werden zu Offline-Tests, die Bewertung erfolgt vor dem Versand, und eine Änderung wird nur dann vorgeschlagen, wenn der Baseline ansteigt. Jeder Zyklus beginnt auf einem höheren Niveau.
Ein vollständiger täglicher Lauf
Ein einzelner Driver kann den gesamten Zyklus einmal an einem Tag mit frischen Produktionsdaten ausführen.
# Full loop driver
def run_improvement_loop():
# 1. Pull traces
runs = list(client.list_runs(
project_name="agent-improvement-loop",
is_root=True,
start_time=datetime.utcnow() - timedelta(days=1),
))
traces = [compact_trace(r) for r in runs]
# 2. Enrich with scores
enriched = [enrich(t) for t in traces]
# 3. Find failures and categorize them
failures = [t for t in enriched if is_low_scoring(t)]
categories = Counter(categorize(t) for t in failures)
# 4. Add failures to the dataset
new_examples = [{
"inputs": {"question": t["inputs"]["messages"][0]["content"]},
"outputs": {"expected_tool": expected_tool_for_query(
t["inputs"]["messages"][0]["content"])},
"metadata": {"category": categorize(t), "source_run_id": t["run_id"]},
} for t in failures]
if new_examples:
client.create_examples(dataset_id=dataset.id, examples=new_examples)
# 5. Re-evaluate the current agent against the full dataset
result = evaluate(
run_agent_v2,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="daily-regression",
)
return {
"traces_collected": len(traces),
"failures_found": len(failures),
"by_category": dict(categories),
"examples_added": len(new_examples),
}
print(run_improvement_loop())
Diese Funktion verbindet die sechs Phasen miteinander. Sie lädt die Spuren vom Vortag herunter, bewertet sie, kategorisiert die Fehler, fügt neue Fehler als Beispiele für das Datensatz hinzu und führt eine Regressionsbewertung durch. Sie gibt ein kleines Dashboard-Diktum zurück, das täglich protokolliert oder in Slack gepostet werden kann.
Die erwartete Ausgabe:
{
"traces_collected": 186,
"failures_found": 12,
"by_category": {"wrong_tool": 4, "unhelpful_answer": 6, "human_flagged": 2},
"examples_added": 12,
"regression_score": 0.87
}
Was die Ausgabe zeigt: An diesem Tag wurden 186 Produktionsspuren erzeugt, ohne dass sich der Agent-Code änderte, und zwölf davon erhielten mindestens eine schlechte Bewertung. Es gibt nun mehr unbrauchbare Antworten als falsche Tool-Aufrufe, was darauf hindeutet, dass die Korrektur der Routenfunktion Früchte trug und dass die nächste Korrektur in der Antwortgenerierung ansetzen sollte. Die Regressionsbewertung blieb bei 0,87, sodass keine der wöchentlichen Änderungen eine frühere Störung wieder auslöste. Zwölf neue Beispiele wurden dem dauerhaften Datensatz hinzugefügt, wo sie dazu dienen, Wiederholungen zu vermeiden.
Fünf Verbesserungen, die als Nächstes vorgenommen werden sollten
Jede dieser Verbesserungen bezieht sich auf eine Funktion von LangChain, LangGraph oder LangSmith, die ohne Neubau der Architektur aktiviert werden kann.
- Ersetzen Sie den Regex-Klassifikator durch Embeddings.
OpenAIEmbeddingskann jede Anfrage einbetten, sodass Fehlerverläufe nach semantischer Ähnlichkeit und nicht nach Schlagwortregeln gruppiert werden, was subtilere Probleme wie Tonunterschiede aufdeckt. - Ersetzen Sie den eingebetteten LLM-Bewerter durch die OpenEvals-Bibliothek. Der LangSmith-Quickstart verwendet
openevalsmit vorgefertigten Prompts wieCORRECTNESS_PROMPT, die besser funktionieren als selbst entwickelte Bewerter und Varianten ohne Referenzdaten für die Online-Überwachung bieten. - Fügen Sie Mehrfachgesprächs-Bewertungen hinzu. LangSmiths Mehrfachgesprächs-Bewertungen führen einen LLM-Bewerter über gesamte Konversationen statt nur einzelne Antworten durch, wodurch ein Abweichen in der Argumentation erkannt wird, das bei Einzelantwort-Bewertern in chatbasierten Agenten übersehen wird.
LANGSMITH_PROJECT=agent-canary nutzen und anschließend seine tatsächlichen Ergebnisse mit denen des Kontrollprojekts vergleichen, wird die Produktionsumgebung nur dann ersetzt, wenn v2 deutlich besser ist.Der Kreislauf wiederholt sich anschließend jedes Mal mit einer höheren Ausgangsbasis – besseren Spuren, besseren Bewertungen und einem besseren Agenten.