Ein lokaler AI-Agent mit Ollama: Eingeschränkte Tools, Validierung und strenge Schrittzahlbegrenzungen
Ein lokaler AI-Agent auf Ollama mit folgendem Verhalten: eingeschränkte Listen zugelassener Tools, begrenzter Löopschluss, strukturierte Ausgaben mit Pydantic, expliziter Zustand sowie ein Testset mit Fehlfallbeispielen.
Quellen: I Built a Local AI Agent With Ollama — and the Hard Part Wasn’t the Model
Die erste Version eines lokalen Rechnungsdienstleisters wirkt etwa fünf Minuten lang beeindruckend. Die Aufgabe ist einfach: einem kleinen Unternehmer dabei zu helfen, überfällige Rechnungen einzutreiben. Der Dienstleister kann Kunden suchen, unbezahlte Rechnungen finden, herausfinden, wie lange sie überfällig sind, und eine Erinnerung erstellen. Eine Testanfrage bittet ihn, die überfälligen Rechnungen von Acme Plumbing zu finden und eine freundliche Erinnerung vorzubereiten. Er antwortet sofort mit einer Rechnung, einem Saldo sowie einer gut formulierten E-Mail.
Das Problem ist, dass ein Teil davon erfunden ist. Der Kunde existiert, aber die Rechnungsnummer nicht. Der Betrag klingt plausibel, ist aber falsch. Der Dienstleister hat die Form eines erfolgreichen Ergebnisses erzeugt, ohne die eigentliche Arbeit zu leisten.
Diese Episode vermittelt die wichtigste Lektion bezüglich der Erstellung von Agenten: Intelligent klingen zu können bedeutet noch nicht Zuverlässigkeit. Ein Agent ist zuverlässig, wenn jede bedeutende Aussage auf eine kontrollierte Aktion oder eine zuverlässige Quelle zurückzuführen ist. Im Folgenden wird der Rechnungsagent aus Ollama und Python neu aufgebaut, wobei explizite Tools, Validierungen sowie ein kompakter Agentenloop hinzugefügt werden. Die Inferenz findet auf dem lokalen Rechner statt, doch die Lektionen gelten für nahezu jedes Agentenframework.
Warum Ollama
Eine lokale Einrichtung vermeidet es während der Entwicklung, Kundendaten an einen externen Modellanbieter zu senden, und ermöglicht außerdem den Wechsel zwischen offenen Modellen, ohne die Anwendung neu erstellen zu müssen. Ollama stellt die Modelle lokal über eine einfache Chat-API zur Verfügung. Auch Aufrufe von Tools sind möglich, sodass ein Modell eine Funktion anfordern kann, anstatt eine Antwort vorzutäuschen, die es nicht hat. Die Einrichtung ist kurz:
ollama pull qwen3
pip install ollama
Ein normales Chat-Anfrage-Format aus Python sieht so aus:
from ollama import chat
response = chat(
model="qwen3",
messages=[
{"role": "user", "content": "Explain what an overdue invoice is."}
],
)print(response.message.content)
Das ergibt einen Chatbot, noch keinen Agenten.
Ein Chatbot antwortet, ein Agent handelt schrittweise
Der Begriff Agent wird locker verwendet. Eine praktikable Definition ist ein Modell, das in einer Schleife läuft und Zugriff auf Werkzeuge, Zustände sowie Regeln hat, die bestimmen, was es als Nächstes tun darf. Für die Rechnungsaufgabe verläuft die Schleife wie folgt:
- Der Benutzer stellt eine Anfrage.
- Das Modell entscheidet, ob ein Werkzeug benötigt wird.
- Die Anwendung validiert die Anfrage nach einem Werkzeug.
- Die Anwendung ruft das Werkzeug auf.
- Das Ergebnis wird der Konversation hinzugefügt.
- Das Modell bittet um ein weiteres Werkzeug oder gibt eine endgültige Antwort.
Das Modell berührt niemals die Datenbank. Es kann lediglich die Anwendung bitten, ein von dem Entwickler definiertes Tool aufzurufen. Diese Grenze ist wichtig, denn Sprachmodelle sind gut darin, Absichten zu interpretieren und Texte zu generieren, während Anwendungscode besser in der Handhabung von Berechtigungen, Abfragen, Berechnungen und Geschäftsregeln ist.
Zuerst begrenzte Tools
Ein häufiger erster Fehler besteht darin, ein einziges, leistungsstarkes Tool zu verwenden, zum Beispiel manage_invoices, das suchen, aktualisieren, Entwürfe erstellen und versenden kann. Dadurch wird das Debuggen schwierig und ein Fehlbetrieb äußerst gefährlich. Kleine Tools mit jeweils nur einer Aufgabe funktionieren besser:
find_customerwandelt einen Kundennamen in eine echte Kunden-ID um.get_overdue_invoicesgibt die überfälligen Rechnungen für diese ID zurück.draft_remindererzeugt Text, sendet aber nichts.send_reminderführt die externe Aktion aus und benötigt eine Genehmigung.
Eine vereinfachte lokale Datenschicht liefert die Fakten:
CUSTOMERS = {
"acme plumbing": {
"customer_id": "cus_1042",
"name": "Acme Plumbing",
"email": "billing@example.com",
}
}
INVOICES = [
{
"invoice_id": "INV-2048",
"customer_id": "cus_1042",
"amount": 1850.00,
"days_overdue": 18,
}
]
def find_customer(name: str) -> dict:
customer = CUSTOMERS.get(name.strip().lower())
return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
matches = [
invoice
for invoice in INVOICES
if invoice["customer_id"] == customer_id
and invoice["days_overdue"] > 0
]
return {"invoices": matches, "count": len(matches)}
Betrachten Sie, was diese Funktionen unterlassen. Sie raten niemals. Ein fehlender Kunde führt zu einem expliziten Fehler, und fehlende passende Rechnungen ergeben eine leere Liste.
Echte Tools, kein imaginärer Zugriff
Ollama akzeptiert Python-Funktionen als Tools, und die Namen, Parameter sowie Docstrings liefern dem Modell das Notwendige, um die richtige Funktion auszuwählen.
import json
from ollama import chat
def find_customer(name: str) -> dict:
"""Find a customer by business name and return its verified record."""
customer = CUSTOMERS.get(name.strip().lower())
return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
"""Return overdue invoices for a verified customer ID."""
matches = [
invoice
for invoice in INVOICES
if invoice["customer_id"] == customer_id
and invoice["days_overdue"] > 0
]
return {"invoices": matches, "count": len(matches)}
TOOLS = {
"find_customer": find_customer,
"get_overdue_invoices": get_overdue_invoices,
}
Das Register-Wörterbuch ist mehr als nur eine Bequemlichkeit, da es sich um eine Allowlist handelt. Selbst wenn das Modell eine Funktion namens delete_customer anfordert, weist die Anwendung ab, weil es kein solches Werkzeug in TOOLS gibt.
Der Agent-Zyklus
Der Zyklus ist es, der die Aufrufe von Werkzeugen in einen Agenten umwandelt. Er beginnt mit einem Systemaufruf:
SYSTEM_PROMPT = """
You are an invoice assistant.
Rules:
- Never invent a customer, invoice, email address, balance, or date.
- Use find_customer before requesting invoices.
- Only use customer IDs returned by tools.
- If a tool returns an error or no records, explain that clearly.
- You may draft communication, but you cannot send it.
"""
def run_agent(user_request: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_request},
] for _ in range(6):
response = chat(
model="qwen3",
messages=messages,
tools=list(TOOLS.values()),
) messages.append(response.message) if not response.message.tool_calls:
return response.message.content for call in response.message.tool_calls:
name = call.function.name
arguments = call.function.arguments if name not in TOOLS:
result = {"error": "tool_not_allowed"}
else:
try:
result = TOOLS[name](**arguments)
except (TypeError, ValueError) as error:
result = {
"error": "invalid_tool_arguments",
"detail": str(error),
} messages.append(
{
"role": "tool",
"tool_name": name,
"content": json.dumps(result),
}
) return "I stopped because the task exceeded the maximum number of steps."
Die Obergrenze von sechs Iterationen ist absichtlich gewählt, denn ein Agent darf niemals endlos denselben Problemen nachgehen. Der Schleifenprozess betrachtet außerdem die Ausgabe der Tools als neue Beweismittel. Das Modell löst zunächst den Namen „Acme Plumbing“ auf, erhält cus_1042 zurück und beantragt erst danach Rechnungen für diese überprüfte ID.
Ein besseres Prompt war nicht die Lösung
Halluzinationen verleiten Entwickler dazu, einen weiteren Satz zum Systemprompt hinzuzufügen, beispielsweise die Aufforderung, präzise zu sein und niemals etwas zu erfinden. Diese Anweisung hilft zwar, stellt aber keine echte Kontrolle dar. Die zuverlässige Lösung ergab sich aus einer Umstrukturierung:
- Die Datenbank bleibt die Quelle der Wahrheit.
- Das Modell darf Daten anfordern, darf aber keine als gültig durchgehenden Einträge erfinden.
- Tool-Names sind in einer Erlaubnisliste aufgeführt und Argumente werden überprüft.
- Die Schleife hat eine feste Anzahl an Schritten.
- Lesen und Schreiben werden voneinander getrennt.
Benachrichtigungen beeinflussen das Verhalten, und der Code sorgt dafür, dass dies umgesetzt wird.
Gestrahlte Ausgabe an der Grenze
Freiformtext eignet sich für eine freundliche E-Mail, ist aber riskant, wenn ein anderer Teil der Anwendung die Antwort verarbeitet. Wenn der Agent ein Überprüfungsformular mit dem Kunden, den Rechnungsnummern, dem Gesamtbetrag und einem Entwurf zurückgeben muss, kann ein Pydantic-Modell diesen Vertrag definieren:
from pydantic import BaseModel, Field
class ReminderReview(BaseModel):
customer_name: str
invoice_ids: list[str]
total_due: float = Field(ge=0)
draft_subject: str
draft_body: str
requires_approval: bool = True
Ollama akzeptiert ein JSON-Schema über seinen format-Parameter:
review_response = chat(
model="qwen3",
messages=messages,
format=ReminderReview.model_json_schema(),
)
review = ReminderReview.model_validate_json(
review_response.message.content
)
Das garantiert, dass die Antwort die erwartete Struktur hat. Es beweist jedoch nicht, dass die Werte korrekt sind, weshalb invoice_ids und total_due vor dem Erscheinen der Bestätigungs-Schaltfläche weiterhin mit den tatsächlichen Ergebnissen des Tools verglichen werden. Die Schema-Validierung prüft, ob etwas die richtige Struktur aufweist. Die Geschäftslogik-Validierung prüft, ob etwas zulässig und korrekt ist. Ein Produktionsagent benötigt beides.
Der Zustand ist keine Speicherung
Lange Gespräche bringen ein weiteres Problem mit sich: Das Modell vergisst, welcher Kunde ausgewählt wurde, oder fügt eine alte Rechnung einer neuen Anfrage hinzu. Der Grund dafür ist, dass der gesamte Chat-Transkript als Anwendungs-Zustand behandelt wird. Der Chat bietet nützlichen Kontext, doch kritischer Zustand muss in einer expliziten Struktur gespeichert werden:
task_state = {
"customer_id": "cus_1042",
"verified_invoice_ids": ["INV-2048"],
"approved_actions": [],
}
Das Modell kann Ereignisse zusammenfassen, doch die Anwendung besitzt die überprüften IDs und Genehmigungen. Wenn das Gespräch aufgrund der Kontextfenstergröße abgeschnitten wird, bleiben diese Fakten erhalten.
Auch die Länge des Kontexts beeinträchtigt die Leistung, da mehr Kontext mehr Speicher verbraucht und längere Verarbeitungszeiten erfordert. Anstatt jede Nachricht und jede Datenbankzeile endlos zu übertragen, sollten nur die kleinstmögliche Menge an Informationen übermittelt werden, die für die aktuelle Entscheidung notwendig sind.
Lokal bedeutet nicht sicher
Durch die Verwendung von Ollama zur Ausführung eines Modells bleibt die Inferenz unter lokaler Kontrolle, doch dies sichert den Rest der Anwendung nicht. Ein Agent kann weiterhin Daten in Logs ausleiten, unsichere interne Endpunkte aufrufen, bösartige Anweisungen, die in Dokumenten versteckt sind, befolgen oder mit übermäßigen Berechtigungen handeln. Die üblichen Sicherheitsprinzipien für Backend-Systeme gelten weiterhin:
- Geben Sie jedem Tool nur die Berechtigungen, die es benötigt.
- Halten Sie Geheimnisse außerhalb von Prompts und Quellcode.
- Betrachten Sie den abgerufenen Text als unzuverlässige Daten und niemals als Systemanweisungen.
- Protokollieren Sie jeden Tool-Aufruf zusammen mit seinem Ergebnis, der Latenzzeit sowie der endgültigen Entscheidung.
- Entfernen Sie sensible Felder aus den Ergebnissen, bevor sie in den Kontext des Modells gelangen.
- Fordern Sie Genehmigung für externe oder irreversible Aktionen ein.
- Machen Sie wichtige Schreibvorgänge idempotent, damit Wiederholungen sie nicht doppelt erzeugen.
Das Modell sollte niemals die Sicherheitsgrenze darstellen.
Testing
Happy-Path-Demos verbergen die meisten Fehler der Agenten, weshalb es sich lohnt, ein kleines Bewertungsset zu erstellen. Die Fallbeispiele umfassen einen bekannten Kunden mit einer verspäteten Rechnung und einen ohne, einen Namen, der mit niemandem übereinstimmt, zwei ähnliche Firmennamen, einen Versuch der Übermittlung ohne Genehmigung, eine eingefügte Anweisung, die Regeln zu ignorieren, einen fehlerhaft formatierten Tool-Parameter sowie ein Tool, das abbricht. Für jeden Fall sollen die erwarteten Tools, zulässigen Aktionen und das endgültige Ergebnis aufgezeichnet werden, und das gesamte Set soll jedes Mal neu ausgeführt werden, wenn sich das Modell, die Systemanweisung oder die Toolbeschreibungen ändern.
Die nützlichste Messgröße ist nicht, ob die endgültige Antwort gut klingt, sondern ob der Agent die richtige Abfolge der überprüften Schritte befolgt hat.
Die funktionierende Version
Nach der Neugestaltung hinterlässt die ursprüngliche Anfrage einen solchen Verlauf:
User request
→ find_customer(name="Acme Plumbing")
→ verified customer_id: cus_1042
→ get_overdue_invoices(customer_id="cus_1042")
→ verified invoice: INV-2048, $1,850, 18 days overdue
→ generate draft
→ wait for human approval
Es ist weniger magisch als die erste Demo und weitaus nützlicher. Ein nicht existierender Kunde veranlasst den Agenten, anzuhalten und um Klarstellung zu bitten. Fällige Rechnungen werden nicht als solche gemeldet. Eine unbekannte Tool-Anfrage wird von der Anwendung abgelehnt, und ein zu lange laufender Loop stoppt sicher. Nichts davon ist spektakulär, doch all das stärkt das Vertrauen.
Die letzte Lektion
Ollama macht das Ausführen eines leistungsstarken lokalen Modells einfach, doch das Modell ist nur ein Teil. Der eigentliche Agent ist das umgebende System: der Loop, die Tool-Verträge, die Quelldaten, die Validierungsstufe, die Zustandsmaschine, die Berechtigungsregeln sowie der Genehmigungsprozess.
Für einen Anfänger sollte man auf die zehn Tools sowie auf vollständigen Zugriff auf die Anwendung verzichten. Beginnen Sie mit einem echten Problem und zwei eingeschränkten, nur zum Lesen geeigneten Tools, dokumentieren Sie jede Entscheidung, fügen Sie Fehlfallbeispiele hinzu und gewähren Sie erst Schreibzugriff, wenn der Lesepfad äußerst zuverlässig ist. Der entscheidende Wandel besteht darin, die Frage, wie man das Modell autonomer macht, durch die Frage zu ersetzen, wie man jeden Schritt beobachtbar, überprüfbar und sicher gestaltet. Autonomie eignet sich gut für Demonstrationen, während kontrollierte Autonomie ein nützliches Produkt hervorbringt.