Startseite / Artikel

Entwicklung eines DeepAgents Harness: Berechtigungen, Subagenten und menschliche Freigabe

Entwicklung eines DeepAgents-Tools für einen Systemdesign-Planer: Dateibackends, Zugriffskontrollregeln, Subagenten, Genehmigungsmechanismen für Menschen, Fähigkeiten sowie MCP-Tools.

Verlag: REACTAPP.TOP24 Min. Lesezeit5548 Wörter

Quellen: The DeepAgents Architecture, Part 2: Building the Agent Harness

Eine Framework-Lösung ist in der Dokumentation leicht zu bewundern, in der Praxis jedoch schwieriger zu beurteilen. Dieser Artikel beschreibt, was geschah, als die DeepAgents-Architektur verwendet wurde, um eine voll funktionsfähige Anwendung zu entwickeln – wobei echte mehrstufige Entwurfsdiskussionen sowie Erkenntnisse anhand von Belegen überprüft wurden. Es handelt sich dabei um praktische Fragen: Wie handhabt das Framework Dateien und Berechtigungen, wie sieht Delegation aus, wie verhält sich die menschliche Freigabe und wo versagt das Urteilsvermögen des Modells trotz ordnungsgemäßer Funktionsweise des Frameworks.

Die Anwendung ist ein Assistent für die Systemdesign-Planung für Organisationen, die ihre Daten- und KI-Arbeitslasten mit Databricks auf Azure ausführen. Diese Wahl war bewusst getroffen. Was als „eigene Tools“ gilt, ist derzeit unklar – angesichts der engen Verzahnung von Databricks und Microsofts Azure AI-Stack – und genau solche offenen, mehrschrittigen Unklarheiten soll diese Architektur gerade beseitigen. Der vollständige Quellcode wird als öffentliches Repository veröffentlicht.

Einrichten

git clone https://github.com/shubhodayahampiholi/system-design-planner.git
cd system-design-planner
uv sync
cp .env.example .env   # fill in real API keys

Es wird Python 3.11 oder neuer sowie der Paketmanager uv benötigt. Die Beispiele verwenden sowohl Anthropic- als auch OpenAI-Modelle, weshalb beide API-Schlüssel erforderlich sind, um alles nachvollziehen zu können. Ein Tavily-Schlüssel sowie Zugang zu einem Databricks-Workspace sind nur für die Abschnitte relevant, in denen sie verwendet werden.

Dateien und Zugriffskontrolle in DeepAgents

Wo sich die Dateien des Agents befinden

Bevor ein Agent etwas lesen oder schreiben kann, benötigt er einen Ort zur Speicherung von Dateien. DeepAgents bietet dafür mehrere Backend-Lösungen, darunter StateBackend, FilesystemBackend sowie Varianten, die von einem Speicher oder einer Sandbox unterstützt werden. Dieses Projekt verwendet aus unterschiedlichen Gründen zwei davon.

Wenn create_deep_agent() ohne zusätzliche Argumente aufgerufen wird, erhält der Agent StateBackend, der die Dateien im eigenen LangGraph-Zustand des Agents speichert. Da dieser Zustand nach jedem Schritt gespeichert wird, bleiben die Dateien tatsächlich bestehen, solange ein Konversationsthread andauert. Der Nachteil ist, dass die Persistenz an den Thread gebunden ist: In einem Thread geschriebene Inhalte sind aus einem anderen Thread nicht sichtbar. Für viele Anwendungen ist das genau richtig. Hier ist das jedoch nicht der Fall, denn der Agent muss unabhängig vom jeweiligen Konversationsthread echte Referenzdokumente von der Festplatte lesen.

FilesystemBackend löst dieses Problem. Er weist den Agenten auf einen tatsächlichen Ordner hin, und jede Lese-, Schreib- oder Auflistungsaktion bezieht sich auf echte Dateien, die über die Laufzeit hinaus bestehen bleiben. Alle Beispiele beginnen mit einer einzigen Factory-Funktion:

# src/system_design_planner/agent.py
from deepagents import create_deep_agent

MODEL = "claude-sonnet-5"

def build_planner_agent(*, system_prompt=PLANNER_SYSTEM_PROMPT, **kwargs):
    return create_deep_agent(model=MODEL, system_prompt=system_prompt, **kwargs)

Jedes Beispiel erstellt seinen Agenten mithilfe dieser Funktion. Wenn ein echter Dateizugriff erforderlich ist, erhält der Aufruf backend=FilesystemBackend(root_dir=...). root_dir legt die Grenze fest, innerhalb der der Agent arbeitet – ein begrenztes Wurzelverzeichnis, aus dem er standardmäßig nicht herauskommen kann, selbst wenn er einen Pfad wie ../../ versucht. Was der Agent tatsächlich innerhalb dieser Grenze tun darf, also welche Operationen an welchen Pfaden durchgeführt werden dürfen, ist eine separate Frage, die durch Berechtigungsregeln von FilesystemMiddleware geregelt wird.

Entscheidung darüber, was der Agent tun darf

Der Zugriff auf ein Verzeichnis ist eine Entscheidung – genauso wichtig ist es, genau zu bestimmen, was darin geschehen darf. DeepAgents handhabt dies mit einer geordneten Liste von Regeln, die angeben, welche Operationen pro Pfad erlaubt, verweigert oder zur ausdrücklichen Genehmigung des Benutzers aufgeschoben werden. Dies sind die Regeln, unter denen die Agenten dieses Projekts arbeiten:

# src/system_design_planner/permissions.py
from deepagents import FilesystemPermission

DESIGN_SESSION_PERMISSIONS = [
    FilesystemPermission(operations=["read", "write"], paths=["/.env"], mode="deny"),
    FilesystemPermission(operations=["write"], paths=["/knowledge_base/**"], mode="deny"),
    FilesystemPermission(operations=["write"], paths=["/memory/AGENTS.md"], mode="interrupt"),
]

Die Regeln werden nacheinander überprüft, jede aus ihrem eigenen Grund.

Die erste verhindert, dass der Agent je die .env-Datei liest oder schreibt, in der sich die echten API-Schlüssel befinden. Unabhängig davon, was der Agent sonst noch tut, gilt diese Regel weiterhin.

Die zweite macht das Referenzmaterial in knowledge_base/ nur für den Lesen zugänglich. DeepAgents zählt delete als Schreibvorgang, wodurch dieselbe Regel auch Löschvorgänge blockiert und nicht nur Änderungen – das bedeutet, dass die Grundwahrheiten, gegen die der Agent argumentiert, nicht durch seine eigenen Aktionen heimlich beschädigt werden können.

Die dritte Regel hat den größten Einfluss. Schreibvorgänge in die Speicherdatei des Agents sind erlaubt, aber nur nach Zustimmung eines Menschen. Dieser Mechanismus wird weiter unten in eigenen Abschnitten erläutert.

Überprüfung, ob der Agent das liest, was ihm gegeben wird

Alles das ist nutzlos, es sei denn, der Agent verwendet es richtig, und es muss gründlicher getestet werden, als angenommen. In der Dateiordner knowledge_base/ befinden sich vier kurze Referenzdokumente zu den Themen Governance in Unity Catalog, Mosaic AI, die Integration mit Microsoft Foundry sowie Identität und Netzwerkverbindungen. Um zu überprüfen, ob der Agent sie tatsächlich liest und nicht nur anhand der Dateinamen ratet, wurde er auf den Ordner gezeigt und gebeten, zusammenzufassen, was sich darin befindet:

backend = FilesystemBackend(root_dir="knowledge_base")
agent = build_planner_agent(backend=backend)

result = agent.invoke({
    "messages": [{
        "role": "user",
        "content": "List every file in your working directory, then give a one-line summary of what each one covers.",
    }]
})

Das Ergebnis waren vier genaue, getrennte Zusammenfassungen, jeweils eine pro Datei. Die Einschränkung bezüglich geringerer Zuverlässigkeit im Dokument zur Identifizierung und Netzwerkstruktur blieb sogar unverändert erhalten, anstatt in den zuverlässigen Ton der anderen drei übergegangen zu sein. Die Ausführungsprotokolle erklären, warum das von Bedeutung ist: Der Agent rief ls auf, um herauszufinden, was vorhanden war, und führte anschließend für jedes Dokument einen read_file-Aufruf durch – insgesamt vier –, bevor ein einziges Wort der Zusammenfassung geschrieben wurde. Die Antwort basierte somit auf tatsächlichen Lesevorgängen.

Delegation an Unteragenten

Was Delegation hier bedeutet

Manche Aufgaben eignen sich besser für einen Spezialisten, der allein arbeitet, anstatt sie den bereits umfangreichen Aufgabenbereich des Hauptagenten hinzuzufügen. DeepAgents unterstützt dies durch Subagenten: eigenständige Agenten, auf die der Hauptagent bei einer bestimmten Aufgabe zurückgreifen kann. Ein Subagent läuft in seinem eigenen isolierten Kontext ab, sieht niemals die gesamte Vorgeschichte, die zur Anforderung führte, und gibt nach Abschluss ein Ergebnis an den Delegierenden zurück. Diese Isolation betrifft insbesondere den konversationellen Kontext und erstreckt sich nicht unbedingt auf die Speicherung, wie die untenstehende Diskussion zum Speicherproblem zeigt.

Es gibt praktische Vorteile. Der Kontext des Hauptagenten bleibt auf die Gesamtaufgabe fokussiert, anstatt alle Zwischenschritte zu erfassen, die ein Spezialist unternommen hat, um eine Antwort zu erzeugen. Ein weiterer Vorteil wird oft übersehen: Da jeder Unteragent eigenständig konfiguriert ist, können verschiedene Unteragenten auf völlig unterschiedlichen Modellen laufen, die je nach Aufgabe ausgewählt werden.

Andere Modelle für verschiedene Unteragenten

Nicht jede Aufgabe eines Unteragenten erfordert das leistungsstärkste und teuerste Modell. In diesem Projekt existiert ein Unteragent nur dazu, Fakten aus Referenzdokumenten herauszuziehen – eine mechanische Aufgabe, die kein tiefes Denken erfordert. Anstatt ihn mit dem Modell zu betreiben, das den Rest des Systems antreibt, erhält er ein kleineres, günstigeres Modell, das besser zu dieser begrenzten Aufgabe geeignet ist:

from deepagents import SubAgent

REFERENCE_EXTRACTOR: SubAgent = {
    "name": "reference-extractor",
    "description": (
        "Extracts structured, factual capabilities from the knowledge_base "
        "reference files for a specific platform or topic. Delegate here "
        "before proposing any subsystem design, so decisions are grounded "
        "in verified platform facts rather than assumption. Do not use this "
        "subagent for design reasoning or tradeoffs - extraction only."
    ),
    "system_prompt": (
        "You are a fact-extraction specialist. Given a topic, find the "
        "relevant file(s) in your working directory, read them, and return "
        "ONLY a bullet list of the concrete facts relevant to that topic - "
        "no narrative, no design opinions, no recommendations. If a fact is "
        "explicitly flagged as an open question or unverified in the source "
        "file, preserve that flag in your output rather than smoothing it "
        "over into a confident-sounding statement."
    ),
    "model": "openai:gpt-5.6-luna",
}

Das Feld model macht das möglich. Jeder Unteragent kann sein eigenes Modell benennen, unabhängig davon, auf welchem System der Hauptagent läuft. In dieser Version läuft der Hauptplaner auf Claude, während dieser eine Unteragent auf einem OpenAI-Modell läuft.

Die Einstellung eines Feldes ist jedoch lediglich eine Konfiguration und beweist nicht, dass die Anfragen dorthin gelangen, wohin sie sollen. Dafür muss man prüfen, was tatsächlich bei einem Ausführungsvorgang passiert ist. Mit den Verfolgungstools, die es ermöglichen, die Ausführung eines Agents Schritt für Schritt zu überprüfen, zeigten sich die vom Hauptagent ausgeführten Aufrufe von Anfang bis Ende unter Claude, während der in dem Extraktionsunteragent eingebettete Aufruf unter dem in seiner Konfiguration angegebenen OpenAI-Modell erschien. Das ist eine auf Provider-Ebene getroffene Routungsentscheidung, die umgesetzt und überprüft werden konnte – nicht nur etwas, das im Code deklariert wurde.

Ein Unteragent behält standardmäßig sein eigenes Gedächtnis

Isolation ist eine bewusste Gestaltungsoption in DeepAgents und reicht weiter, als die meisten Menschen anfangs erwarten. Ein Subagent erhält seinen eigenen Kontext und hat außerdem keinen automatischen Zugriff auf irgendetwas, was sich im Gedächtnis des Hauptagents befindet – selbst nicht auf Dinge, die direkt mit der delegierten Aufgabe zusammenhängen. Der Zugriff muss explizit gewährt werden.

Das ist in der Regel die richtige Standardeinstellung, da ein Subagent nicht durch alles waten muss, was der Hauptagent aufgezeichnet hat, nur um eine fokussierte Aufgabe zu erledigen. Wenn die Aufgabe jedoch von einer bereits getroffenen und festgehaltenen Entscheidung abhängt, muss der Zugriff absichtlich gewährt werden:

def build_governance_specialist(backend) -> SubAgent:
    from deepagents.middleware.memory import MemoryMiddleware

    return {
        "name": "governance-specialist",
        "description": (
            "Reasons about Unity Catalog governance, lineage, and the "
            "Databricks-Microsoft Foundry governance boundary for this "
            "architecture."
        ),
        "system_prompt": (
            "You are a governance specialist for an Azure + Databricks AI "
            "architecture. Before answering, check /memory/AGENTS.md for any "
            "recorded scoping decisions - they are binding constraints on "
            "your recommendation, not suggestions."
        ),
        "middleware": [MemoryMiddleware(backend=backend, sources=["/memory/AGENTS.md"])],
    }

Diese Middleware-Einstellung übernimmt die Genehmigung. Der Spezialist erhält eine eigene Verbindung zum Speicherfile, den der Hauptagent verwendet, und kann somit frühere Entscheidungen einsehen, bevor er mit seiner Auswertung beginnt.

Menschliche Genehmigung und persistenter Speicher

Warum einige Aktionen eine Person erfordern

Nicht jede Aktion, die ein Agent ausführt, sollte unbeaufsichtigt ablaufen. Das Schreiben einer temporären Datei ist weniger kritisch. Die Aufzeichnung einer Entscheidung, die in allen späteren Gesprächen als feststehende Tatsache angesehen wird, ist hingegen etwas völlig anderes – denn ein Fehler führt nicht nur zu einer falschen Antwort, sondern kann alle folgenden Antworten indirekt verfälschen. DeepAgents löst dieses Problem durch die Einbeziehung eines Menschen: Ausgewählte Aktionen können so eingestellt werden, dass ihre Ausführung pausiert wird, bis eine echte Person sie genehmigt, ablehnt oder ändert.

Hier umfasst der Mechanismus genau eine Aktion – das Schreiben in die persistente Speicherdatei des Agenten. Der Agent kann jederzeit eine Aktualisierung vorschlagen, und nichts wird geschrieben, bis eine Person zustimmt.

Wie Genehmigung und Wiederaufnahme funktionieren

Zwei Voraussetzungen müssen erfüllt sein, damit dies funktioniert. Erstens legt die obige Berechtigungsregel bei Schreibvorgängen in die Speicherdatei mode="interrupt" fest. Zweitens, und das wird oft übersehen, benötigt das Pausieren einen Checkpointer – eine Methode, um den genauen Zustand des Agenten zu speichern, damit er pausiert und wieder aufgenommen werden kann, anstatt von vorne gestartet zu werden.

from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command

checkpointer = MemorySaver()

agent = build_planner_agent(
    backend=backend,
    permissions=DESIGN_SESSION_PERMISSIONS,
    checkpointer=checkpointer,
    memory=["/memory/AGENTS.md"],
)

Sobald der Agent einen gesicherten Schreibvorgang versucht, kommt die Ausführung tatsächlich zum Stillstand und die Datei bleibt unverändert. Mit der aktuellen Implementierung kann die ausstehende Aktion aus dem Graphenzustand gelesen werden:

state = agent.get_state(config)

if state.interrupts:
    action = state.interrupts[0].value["action_requests"][0]
    print(f"tool: {action['name']}")
    print(f"args: {action['args']}")

Was ausgegeben wird, ist der genaue Befehl, den der Agent ausführen möchte, sowie der genaue Text, den er speichern möchte – unzusammengefasst. Ein Prüfer kann ihn genehmigen oder ablehnen und einen konkreten Grund angeben, der an das Modell zurückgesendet wird:

decision = {"type": "approve"}
# or:
decision = {"type": "reject", "message": "Not yet ready — needs client confirmation first."}

result = agent.invoke(Command(resume={"decisions": [decision]}), config=config)

Wenn man es von Anfang bis Ende ausführt, funktioniert es. Ein vorgeschlagener Schreibvorgang wird gestoppt, sein ausstehender Inhalt kann vollständig gelesen werden, bevor jemand eine Entscheidung trifft, und eine Genehmigung lässt den Schreibvorgang abgeschlossen werden – was anschließend durch Überprüfung der Datei auf der Festplatte bestätigt werden konnte, anstatt dem Bericht des Agents zu vertrauen.

Ablehnung ohne Erklärung

Über alle Szenarien hinweg führte das Ablehnen einer vorgeschlagenen Aktion ohne Angabe des Grundes dazu, dass das Modell nicht nachfragte, was schiefgelaufen war. Stattdessen erfand es einen glaubwürdigen Grund und setzte die Aktion fort. In einem Fall wurde ein Speicherzugriff ohne Erklärung abgelehnt, wobei die Standardnachricht des Systems in dieser Situation lediglich angibt, dass die Aktion nicht ausgeführt wurde und nicht erneut versucht werden sollte, es sei denn, der Benutzer bittet darum. Dennoch versuchte das Modell umgehend eine modifizierte Version desselben Zugriffs und rechtfertigte den erneuten Versuch mit etwas, was niemand gesagt hatte. Das ist die Reaktion des Modells auf das, was ihm das Ablehnungssystem zur Verfügung stellt – das Mechanismus selbst verursacht dies nicht direkt.

Die praktische Lektion ist einfach: Fügen Sie immer einen echten, konkreten Grund zu einer Ablehnung hinzu. Ob dies in jedem Fall dieses Verhalten verhindert, wurde nicht separat getestet, aber es beseitigt die offensichtlichste Lücke, die das Modell mit einer Vermutung füllen könnte.

Warum die gleichzeitige Genehmigung mehrerer Entscheidungen Vorsicht erfordert

Eine weitere Erkenntnis betrifft die Art und Weise, wie vorgeschlagene Änderungen zusammengefasst werden. Wenn eine Designdiskussion mehrere miteinander verbundene Schlussfolgerungen hervorbringt, neigt das System dazu, vorzuschlagen, alle davon in einer einzigen Änderung zu erfassen: ein Block neuer Inhalte, eine einzige Genehmigung, die alles darin umfasst.

Das ist effizient – doch es hat auch seinen Preis. In einem dokumentierten Fall enthielt eine einzige genehmigte Änderung fünf Einträge. Vier davon waren Entscheidungen, die die betreffende Person ausdrücklich bestätigt hatte. Der fünfte war etwas, das das System eigenständig vorgeschlagen hatte, unter Verwendung einer Aussage wie „Dies als abgeschlossen betrachten, sofern nicht anders angegeben“, anstatt einer direkten Frage, auf die eine Antwort gegeben wurde. Er wurde zusammen mit den vier bestätigten Einträgen mit genau denselben selbstsicheren Formulierungen und dem Anschein von Autorität in das Gedächtnis aufgenommen.

Der Schreibvorgang wurde in seiner Gesamtheit genehmigt. Im Genehmigungsprozess wurde das eine abgeleitete Element nicht von den vier bestätigten Elementen getrennt, und jemand, der einen Diff mit mehreren Elementen überfliegt, ist weitaus eher dazu in der Lage, vier eindeutige Entscheidungen zu erkennen, an die er sich erinnert, als eine unauffälligere Annahme, die in derselben Formulierung darunter liegt.

Das ist eine echte Einschränkung, um die man sich kümmern muss. Die Einbeziehung eines Menschen erfolgt hier auf der Ebene der genehmigten Toolaufrufe, nicht auf der Ebene jeder einzelnen Aussage darin. Wenn ein write_file-Aufruf mehrere logische Entscheidungen zusammenfasst, bedeutet die Genehmigung dieses Aufrufs die Genehmigung aller davon. Solange es keine feingranuliertere Genehmigungsmöglichkeit gibt, besteht die praktische Lösung darin, jeden vorgeschlagenen Speicherzugriff sorgfältig zu überprüfen und die Genehmigung nicht als Formalität zu betrachten, sobald das System als vertrauenswürdig gilt.

Koordination mehrerer Spezialisten

Erlauben des Agenten, zu entscheiden, wer eine Frage bearbeitet

Bislang wurde jeder Unteragent durch die Angabe des zu verwendenden Spezialisten getestet. In der Praxis funktioniert das selten so: Eine Person stellt eine Frage und der Agent muss selbst entscheiden, welche Spezialisten relevant sind. Das wurde mit einer einzigen Frage getestet, die gleichzeitig Governance, AI/ML-Infrastruktur, Bereitstellung und Netzwerk betraf, ohne jemanden namentlich zu nennen.

Die erste Reaktion war keine Delegation. Es handelte sich um eine Reihe von Klärungsfragen zur Netzwerktopologie, zur erwarteten Skalierung sowie dazu, wie mit einer bestimmten Datenbank umgegangen werden sollte – Fragen, die gestellt wurden, bevor eine Entscheidung bezüglich des Designs getroffen wurde. Das ist ein vernünftiger Instinkt und kein Versagen, denn mehrere dieser Antworten verändern schließlich, welche Spezialisten überhaupt relevant sind; daher ist es besser, erst zu fragen, anstatt zu raten.

Paralleldelegation

Nachdem die Fragen in einer Nachfrage beantwortet wurden, erhielten alle vier Spezialisten (Governance, KI/ML, Orchestrierung und Netzwerktechnik) ihre Aufgaben in einem Schritt vom Agenten – und nicht nacheinander. Die Protokolldaten zeigen das eindeutig: Vier Delegierungsanfragen wurden gemeinsam in einer einzigen Antwort versandt.

Das ist von Bedeutung – nicht nur aus Effizienzgründen. Es handelt sich dabei auch um die Situation, die in einem gegen das Framework eingereichten Problem beschrieben wird: Wenn mehrere Subagenten parallel laufen und einer eine Ausnahme auslöst, können die anderen zusammen mit ihm abgebrochen werden, wodurch das Versagen nicht auf den fehlerhaften Agenten beschränkt bleibt. In diesem Testlauf schlossen alle vier Spezialisten erfolgreich ab, sodass sich dieses Verhalten nicht zeigte. Doch echte parallele Delegierung schafft genau die Bedingungen, unter denen dieser gemeldete Fehler auftreten kann.

Synthese, nicht Stapeln

Das interessantere Ergebnis war nicht die Delegation selbst, sondern das, was der Hauptagent anschließend mit den vier spezialisierten Antworten tat. Eine schwächere Umsetzung hätte jede Antwort unter einem eigenen Überschriftspunkt präsentiert und dabei aufgehört. Was tatsächlich geschah, glich eher einer Synthese: Die endgültige Empfehlung fügte alle vier Eingaben zu einem kohärenten Konzept zusammen und entdeckte, was keiner der vier allein herausgefunden hätte.

Der Netzwerkspezialist schlug vor, über ein bestimmtes Verbindungsmodell auf eine externe Datenbank zuzugreifen. Der Governance-Spezialist erklärte, wie Unity Catalog automatisch die Abhängigkeiten für native Databricks-Dienste nachverfolgt. Für diesen spezifischen Integrationsweg passen diese beiden Ansätze nicht so reibungslos zusammen, wie es zunächst scheint. Um dies zu erkennen, muss man beide Lösungsansätze im Hinterkopf behalten und herausfinden, wo sie nicht übereinstimmen – genau das hat die Synthese geleistet. Sie benannte die Lücke in diesem Design ausdrücklich und schlug einen manuellen Audit-Log vor, um dies auszugleichen, ohne die Problematik ungelöst zu lassen oder daraus eine allgemeine Regel für externe Verbindungen zu machen.

Zwei genehmigte Entscheidungen, die sich widersprechen

Ein separater Test ergab etwas Besorgniserregenderes. Bei zwei aufeinanderfolgenden Speicherzugriffen, die jeweils einzeln überprüft und genehmigt wurden, widersprachen sich die Empfehlungen zur Modellverwendung. Die erste empfahl die Verwendung des primären Modells über Foundation Model APIs. Die zweite, die kurze Zeit später in derselben Sitzung genehmigt wurde, schlug stattdessen die Verwendung eines bereitgestellten Durchsatzes vor und ausschließlich die Nutzung von Foundation Model APIs aus, wodurch das zuvor Gespeicherte aufgehoben wurde.

Sowohl die Vorschläge wurden durchgewinkt. Keine Überprüfung erkannte den Widerspruch, da jeder Speicherzugriff als eigenständiger Vorschlag bewertet wurde, ohne mit dem zuvor Minuten zuvor Gespeicherten verglichen zu werden. Dies ist ein anderes Problem als das Bündeln: Beim Bündeln geht es darum, wie genau eine Entscheidung genehmigt wird, während es hier um das Fehlen einer Konsistenzprüfung zwischen getrennten Entscheidungen im selben Gespräch geht.

Zusammen weisen diese beiden Erkenntnisse auf ein Problem hin: Die hier implementierte Genehmigungsprozedur prüft lediglich, ob ein vorgeschlagener Schreibvorgang für sich genommen akzeptabel ist, und nicht, ob er mit allem übereinstimmt, was bereits vereinbart wurde. Abgesehen von einer kurzen, einfachen Sitzung – was eine bewusst zu überwindende Einschränkung des Designs darstellt – sollte man nicht davon ausgehen, dass allein der Genehmigungsprozess ausreicht, um Schutz zu bieten.

Neue Fähigkeiten für den Agenten vergeben

Eine benutzerdefinierte Tool

Bislang wurden ausschließlich in DeepAgents integrierte Tools verwendet: das Lesen von Dateien, das Schreiben von Dateien sowie die Delegation von Aufgaben. Reale Anwendungen benötigen in der Regel mehr Funktionen, und es ist einfach, ein benutzerdefiniertes Tool zu schreiben und es einem Agenten zur Verfügung zu stellen – mithilfe desselben Mechanismus, den das Framework intern verwendet.

Das Projekt benötigte eine Funktion, die in den integrierten Tools fehlt: eine Möglichkeit, zu überprüfen, ob etwas im statischen Referenzmaterial veraltet ist. Die knowledge_base/-Dateien stellen ein Abbild dar, das zum Zeitpunkt der Erstellung korrekt war und sich nicht automatisch aktualisiert. Ein benutzerdefiniertes Tool, das von einem aktiven Web-Suchdienst unterstützt wird, schließt diese Lücke:

from langchain_core.tools import tool
from langchain_tavily import TavilySearch

_tavily_instance = None

def _get_tavily() -> TavilySearch:
    global _tavily_instance
    if _tavily_instance is None:
        _tavily_instance = TavilySearch(max_results=3, topic="general")
    return _tavily_instance

@tool
def check_current_standards(query: str) -> str:
    """Search the live web to verify whether a platform capability, naming,
    or integration detail is still current.

    Use this specifically to check something already pulled from
    knowledge_base against what's true right now - not for open-ended
    research.
    """
    result = _get_tavily().invoke({"query": query})
    return str(result)

Der @tool-Decorator sowie die Beschreibung in der Dokumentation sind es, die es nutzbar machen. Der Agent liest diese Beschreibung, um zu entscheiden, ob das Tool für seine Aufgabe geeignet ist; daher ist eine klare, präzise Beschreibung genauso wichtig wie der darunterliegende Code. Um ein Tool an einen Agenten anzuhängen, wird ein Argument benötigt: build_planner_agent(tools=[check_current_standards]).

Dass dies tatsächlich auf aktuelle Informationen verweist, lässt sich allein aus dem Inhalt der Antwort nicht belegen, da ein Modell eine plausible, datenspezifische Antwort ohne Aufruf irgendetwas erzeugen kann. Der eigentliche Beweis liegt im Protokoll. Bei der Überprüfung eines bestimmten Produktumbenennungs und dessen aktuellen Status zeigt das Protokoll, dass tatsächlich check_current_standards aufgerufen wurde und ein echtes Ergebnis zurückgab, auf dessen Basis die endgültige Antwort erstellt wurde – einschließlich einer korrekt gegen das aktuelle Datum berechneten Countdown-Zeit bis zu einem bestimmten Termin.

Fähigkeiten: Verfahren, die der Agent wiederverwenden kann

Eine weitere Art von Fähigkeit ist ein schriftlicher Ablauf: schrittweise Anleitung zur richtigen Handhabung einer bestimmten Situation, im Gegensatz zu einem einzelnen Fakt oder einer einzelnen Handlung. DeepAgents unterstützt dies durch Fähigkeiten sowie eigenständige Dateien, die einen Ablauf beschreiben, den ein Agent nur dann selbst konsultieren kann, wenn die Situation dies erfordert. Dieses Verhalten des „Nur bei Bedarf“ ist beabsichtigt und wird in der Regel als schrittweise Offenlegung bezeichnet. Der Agent hat stets einen kurzen Namen sowie eine Beschreibung pro Fähigkeit im Blick. Der gesamte Ablauf wird erst in den Arbeitskontext aufgenommen, wenn der Agent die Fähigkeit für relevant hält.

Eine Fähigkeit ist eine einzelne Markdown-Datei mit einem kleinen Block strukturierter Informationen oben drin:

---
name: native-tool-scoping-check
description: Checks whether a request to use "native tooling" is genuinely unambiguous, given the deep current integration between Databricks-native and Azure-native (Microsoft Foundry) services.
license: MIT
---

# Native-Tool Scoping Check

## The procedure
1. Check /memory/AGENTS.md for an existing scoping decision covering this
   question. If one exists, treat it as binding and stop here.
2. If no decision exists, do not assume either interpretation - surface
   the ambiguity explicitly as a scoping question.
3. Once resolved, the decision should be persisted to memory, subject to
   human approval.

Um einem Subagenten Zugriff auf eine Fähigkeit zu gewähren, reicht eine einzige Zeile aus: "skills": ["/skills/project/"]. Dieser Pfad gilt als registrierte Fähigkeitsquelle – das Skills-Middleware durchsucht ihn nach ordnungsgemäß strukturierten Unterordnern, die jeweils ein SKILL.md enthalten. Wenn auf einen zufälligen Ordner verwiesen wird, wird dieser nicht zur Fähigkeit, da das Middleware nach genau dieser Struktur unter dem Pfad sucht.

Zwei getrennte Aspekte verdienen Bestätigung, da sie unterschiedliche Fragen beantworten. Erstens geht es darum, ob die Fähigkeit überhaupt geladen wird, anstatt ungenutzt zu bleiben. Die Spur eines tatsächlichen Ausführungsverlaufs zeigte, dass der Agent ausdrücklich die Datei der Fähigkeit anforderte – ein Beweis dafür, dass das Mechanismus funktioniert und nicht nur vorhanden ist. Zweitens geht es darum, ob der Inhalt der Fähigkeit das Verhalten auf eine bedeutende Weise prägt. Dazu war ein Test erforderlich, bei dem ein Szenario verwendet wurde, das voraussichtlich gleichzeitig zwei technische Fähigkeiten erforderte: eine zum Entwurf von Abrufsystemen und eine zur Zuverlässigkeit von Datensystemen, um zu prüfen, ob das Ergebnis von beiden Gebieten profitierte. Tatsächlich geschah dies: Der vorgeschlagene Entwurf nutzte eine in einer Fähigkeit beschriebene Chunking-Methode und behandelte ein neu hochgeladenes Dokument als etwas, das sicher ersetzt werden sollte statt dupliziert – was einer Anweisung aus der anderen Fähigkeit folgt.

Verbindung zu einem externen System über MCP

Die bisherigen Tools und Fähigkeiten befinden sich alle im Code des Projekts. MCP, das Model Context Protocol, funktioniert anders. Es handelt sich um ein Standard, der es einem Agenten ermöglicht, Tools auf einem entfernten Server zu finden und aufzurufen, ohne für jedes Tool eine manuell geschriebene Integration erstellen zu müssen.

Databricks betreibt einen verwalteten MCP-Server, der Unity Catalog-Funktionen innerhalb der entsprechenden Berechtigungen als MCP-Tools veröffentlicht. Ein Agent kann eine regulierte Funktion finden und aufrufen, und es ist nicht notwendig, dafür einen Python-Wrapper zu schreiben. Die Verbindung sieht wie folgt aus:

import asyncio
from databricks.sdk import WorkspaceClient
from databricks_langchain import DatabricksMCPServer, DatabricksMultiServerMCPClient

async def _fetch_uc_function_tools():
    workspace_client = WorkspaceClient()
    host = workspace_client.config.host
    mcp_client = DatabricksMultiServerMCPClient([
        DatabricksMCPServer(
            name="uc-functions",
            url=f"{host}/api/2.0/mcp/functions/{CATALOG}/{SCHEMA}",
            workspace_client=workspace_client,
        ),
    ])
    return await mcp_client.get_tools()

def get_databricks_uc_function_tools():
    return asyncio.run(_fetch_uc_function_tools())

Eine Unterscheidung lässt sich leicht verwischen, daher lohnt es sich, präzise zu sein. Eine Unity Catalog-Funktion und ein MCP-Tool sind nicht dasselbe. Die Funktion ist die gesteuerte Funktionalität mit eigenen Berechtigungen und einem eigenen Platz in Databricks; sie existiert unabhängig von MCP und ist diesem bereits seit langer Zeit vorausgegangen. Der MCP-Server ist eine separate Schicht davor. Er handelt sich nicht um einen allgemeinen Übersetzer, da er auch an der Authentifizierung sowie dem gesteuerten Zugriff beteiligt ist – das bedeutet, dass eine Anfrage über ihn weiterhin berücksichtigt, wer welche Funktion aufrufen darf. Man kann dies direkt sehen: Eine Funktion mit dem Namen knowledge_platform.default.mcp_verification_check in Databricks erreicht den Agenten als knowledge_platform__default__mcp_verification_check, also in einem Namensformat, das vom Protokoll gefordert wird.

Um zu beweisen, dass die Verbindung echt und nicht simuliert war, wurde dieselbe Methode wie beim Test mit dem benutzerdefinierten Tool angewandt: Es wurde um etwas gebeten, das nicht erraten werden kann. Die Funktion des Unity Catalogs wurde so programmiert, dass sie einen im Voraus ausgewählten zufälligen Wert zurückgibt. Der Agent sollte diese Funktion aufrufen und genau berichten, was zurückkommt – und tatsächlich wurde genau dieser Wert zurückgegeben. Das bestätigt eine echte Netzwerkverbindung hin und zurück, die Ausführung innerhalb von Databricks sowie die Übermittlung des Ergebnisses über das Protokoll und schließt eine überzeugende Fälschung aus.

Eine interaktive Schnittstelle

Jenseits von Skripten

Alles, was oben beschrieben wurde, funktionierte als Python-Skript, das einmal über die Terminal gestartet wurde, Ausgaben erzeugte und anschließend beendet wurde. Eine echte Anwendung verhält sich in der Regel eher wie ein Gespräch: Jemand tippt eine Frage ein, beobachtet, wie der Agent arbeitet, und antwortet unterwegs auf seine Anfragen. Der letzte Bestandteil des Projekts ist eine mit Streamlit erstellte Browseroberfläche, die auf demselben Agent basiert, der überall sonst verwendet wird.

Dafür war eine architektonische Entscheidung notwendig, die von außen nicht offensichtlich ist. Die Werkzeuge, die das Projekt nützlich machen – insbesondere die oben erwähnte Databricks MCP-Verbindung – erfordern eine asynchrone Ausführung, was bedeutet, dass der Code warten kann, bis eine Netzwerkanfrage beantwortet wird, während andere Aufgaben weiterlaufen. Streamlit führt jedes Mal einen ganzen Skript von vorne aus, sobald jemand mit der Seite interagiert, was in Kombination mit lang andauernden asynchronen Aufgaben erhebliche Einschränkungen verursacht – obwohl Streamlit ständig weitere asynchrone Funktionen hinzufügt. Diese Einschränkungen sind an anderer Stelle gut dokumentiert, weshalb es besser war, bereits im Voraus darauf Rücksicht zu nehmen, anstatt sie erst mitten in der Entwicklung zu entdecken.

Bei einer Anwendung, die gleichzeitig vielen Benutzern dient, ist der robustere Ansatz, die Aufgaben zu trennen: Ein geeigneter Backend-Dienst steuert den Agenten, während die Schnittstelle ausschließlich als Anzeigeschicht fungiert. Bei einer einbenutzerigen, lokalen Referenzanwendung führt diese Trennung zu zusätzlicher Komplexität ohne entsprechenden Nutzen. Daher findet die asynchrone Verarbeitung innerhalb der Schnittstelle statt, wobei Python’s eingebautes asyncio.run() verwendet wird, um jeweils einen vollständigen Gesprächsschritt auszuführen. Das ist ein bewusster Kompromiss, der der Skalierung dieser Anwendung entspricht, und stellt keine Behauptung darüber dar, dass dies überall die richtige Wahl ist.

Anzeige dessen, was der Agent tut

Eine Schnittstelle hat einen Vorteil gegenüber einem Skript: Sie kann die Arbeit im Echtzeitverlauf anzeigen und nicht nur das endgültige Ergebnis. Diese zeigt einen Live-Verlauf, eine laufende Liste aller Tool-Aufrufe im Stream des Haupt-Agenten, gekennzeichnet durch die Art der Aktion. Die Sichtbarkeit endet an einer Grenze, die weiter unten beschrieben wird, und erstreckt sich nicht auf den isolierten Ablauf eines Unter-Agenten.

Dazu muss man jeden Tool-Aufruf überprüfen und entscheiden, zu welcher Kategorie er gehört:

def classify_tool_call(tool_call, mcp_tool_names):
    name = tool_call["name"]
    args = tool_call.get("args", {})

    if name == "task":
        return "subagent", args.get("subagent_type", "?")
    if name in mcp_tool_names:
        return "mcp", name
    if name == "read_file":
        path = str(args.get("file_path", ""))
        if "/skills/project/" in path and path.endswith("SKILL.md"):
            skill_name = path.split("/")[-2]
            return "skill", skill_name
    if name in ("read_file", "write_file", "edit_file", "ls", "glob", "grep", "delete"):
        return "filesystem", name
    return "tool", name

Während der Agent läuft, wird jeder neue Tool-Aufruf kategorisiert und der Liste hinzugefügt. Dadurch unterscheidet sich eine Delegation an einen Spezialisten von einem einfachen Dateiabruf, und beide unterscheiden sich wiederum von einem externen MCP-Aufruf. Im praktischen Einsatz ermöglicht dies es, Delegierungen, Aufrufe des benutzerdefinierten Web-Such-Tools sowie Aufrufe des Databricks MCP-Tools voneinander zu unterscheiden – wobei im Laufe des Gesprächs jeweils eine eigene Kennzeichnung vergeben wird.

Genehmigen oder ablehnen über die Schnittstelle

Das Prinzip des „Human-in-the-Loop“ funktioniert hier genauso, doch die Art und Weise, wie eine Person damit interagiert, ändert sich. Anstelle eines Eingabefensters im Terminal erscheint die ausstehende Aktion auf der Seite mit echten Steuerelementen:

if st.session_state.pending_interrupt:
    action = st.session_state.pending_interrupt
    st.warning(f"**Approval needed**\n\n**Tool:** `{action['name']}`\n\n**Args:** `{action['args']}`")
    col1, col2 = st.columns(2)
    with col1:
        if st.button("Approve"):
            run_turn(Command(resume={"decisions": [{"type": "approve"}]}))
    with col2:
        reason = st.text_input("Reason for rejecting")
        if st.button("Reject"):
            decision = {"type": "reject", "message": reason or "Rejected by the human reviewer."}
            run_turn(Command(resume={"decisions": [decision]}))

Eine echte Sitzung hat dies mit einer tatsächlichen Ablehnung getestet. Dem Agenten wurde aufgetragen, einen weiteren Teil des Systems zu entwerfen, und er schlug vor, seine Schlussfolgerungen im Speicher zu speichern. Die Schreiboperation wurde über die Schnittstelle mit einem konkreten Grund abgelehnt. Dennoch besagte die Abschlussnachricht des Agenten eindeutig, dass die Schlussfolgerungen im Speicher gespeichert worden seien.

Diese Behauptung war falsch. Ein direkter Check der Speicherdatei sowie eine getrennte Suche nach weiteren Dateien im Projekt, die sich möglicherweise geändert hatten, bestätigten, dass nirgends etwas geschrieben worden war. Der zugrunde liegende Mechanismus befolgte die Ablehnung vollständig, und das Interface sowie das Interrupt-System handelten genau wie vorgesehen. Was fehlschlug, war die Darstellung der Ereignisse durch den Agenten: Obwohl ihm mit „Nein“ geantwortet wurde, beschrieb er dennoch einen Erfolg. Es handelt sich um dasselbe Verhalten wie zuvor bei der Terminalversion, nun durch einen völlig anderen Ausführungsweg bestätigt – was darauf hindeutet, dass es sich um eine wiederholbare Eigenschaft handelt, wie das Modell mit einer abgelehnten Aktion umgeht, und nicht um einen Einzelfall.

Was das Interface nicht anzeigen kann

Eine frühere Funktion wurde innerhalb der Schnittstelle nicht vollständig überprüft: die Verwendung von Fähigkeiten. Die Echtzeit-Übertragung stammt aus dem Aktivitätsstrom des Haupt-Agenten, während die internen Schritte eines Spezialisten, einschließlich des Laden eines Fähigkeitsfiles, innerhalb der isolierten Ausführung dieses Spezialisten stattfinden – auf einer Ebene unterhalb dessen, was der Hauptstrom anzeigt. Standardmäßig wird diese innere Aktivität in den Hauptstrom eingefügt und nicht separat berichtet.

LangGraph kann dies sichtbar machen: Durch das Übergeben von subgraphs=True an den Streaming-Aufruf werden Aktivitäten aus der eigenen Ausführung eines Unter-Agenten eingebunden. Diese Option wurde entdeckt, in dieser Version jedoch weggelassen, da sie die Struktur jeder gestreamten Aktualisierung verändert und die Darstellung der Spuren erneut bearbeitet werden müsste. Die Verwendung von Fähigkeiten durch einen Spezialisten bleibt durch die zuvor beschriebene Überprüfung der Ausführungsprotokolle vollständig bestätigt. Die Schnittstelle zeigt dies lediglich nicht in Echtzeit – das klar zu benennen, ist besser, als etwas anderes zu implizieren.

Was die Erstellung bestätigt

Das Gerüst hielt

Die zentrale These war, dass langwierige, komplexe Agentenarbeiten am besten als Probleme der Zustandsverwaltung verstanden werden können. Ein Agent versagt nicht, weil das Modell nicht logisch denken kann, sondern weil er den Überblick über das verliert, was er weiß, was entschieden wurde oder was ihm derzeit erlaubt ist zu tun. Alles, was oben getestet wurde, überprüfte diese Behauptung auf die eine oder andere Weise.

Zu dieser Frage waren die Ergebnisse konstant gut. Ein Unteragent mit Zugriff auf eine gemeinsame Speicherdatei handelte aufgrund dieses Zugriffs korrekt. Ein Governance-Spezialist respektierte eine Entscheidung, von der er im Gespräch noch nie gehört hatte, einfach weil er sie nachschlagen konnte. Die Zugriffsregeln blieben auch unter Belastung unverändert: Wiederholte Ablehnungen in einer echten Schnittstelle, wenn eine echte Person mehrmals auf Ablehnen klickte, führten nie dazu, dass eine abgelehnte Aktion stillschweigend durchging. Die Delegation zwischen Anbietern, bei der ein Teil des Systems auf einem Modell und ein anderer Teil auf einem anderen Modell basierte, war ein Konfigurationsaspekt und kein Ingenieurprojekt.

Die Mechanismen unterscheiden sich in ihrer Art. Dateisystemrechte erteilen Berechtigungen, Providerübergreifende Routing-Einstellungen konfigurieren Modelle, und der Zugriff auf die Speicherung von Subagenten kümmert sich um Zustand und Kontext. Jeder dieser Aspekte ist ein Bestandteil der Lösung des Zustandsverwaltungsproblems und hat genau das getan, wofür er konfiguriert war. Diese Aussage ist enger und genauer als die Behauptung, das gesamte System habe sich wie vorgesehen verhalten; der folgende Abschnitt behandelt die Fälle, in denen dies nicht der Fall war.

Was das Rahmenwerk nicht behebt

Die interessanten Erkenntnisse ergaben sich dort, wo das Rahmenwerk perfekt funktionierte, aber dennoch etwas schiefging.

Ohne Begründung wurde „Nein“ gesagt, und das Modell fragte nicht nach dem Grund. Es erfand eine glaubwürdige Erklärung und handelte danach; zwei völlig unterschiedliche Ausführungspfade bestätigten dieses Muster. Es erhielt fünf zusammenhängende Fakten zur Speicherung – vier wurden von einer Person bestätigt, einer nur abgeleitet – und schrieb alle fünf mit derselben Zuverlässigkeit und Formulierung nieder, sodass der abgeleitete Fakt keine schwächere Signalführung aufwies als die bestätigten daneben. Zwei Speicherungen im Gedächtnis, die nur wenige Minuten voneinander entfernt in derselben Sitzung erfolgten und jeweils separat genehmigt wurden, gaben direkt entgegengesetzte Empfehlungen ab, und im gesamten Prozess wurde kein Warnsignal ausgelöst.

Streng genommen handelt es sich bei keinem dieser Fälle um einen Fehler in der Zustandsverwaltung vom Typ verlorener oder beschädigter Dateien. Dank der mitgelieferten Genehmigung sowie der widersprüchlichen Schreibvorgänge wurde die benötigte Information korrekt gespeichert und zur Überprüfung verfügbar gemacht. Niemand hat sie überprüft. Der Fall der Ablehnung ist subtiler: Das Modell sieht die Ablehnung selbst nie, nur eine daraufhin generierte Nachricht, wodurch die Art und Weise, wie es mit dieser Nachricht umgeht, Fragen zu seinem eigenen Denkprozess aufwirft und nichts über einen fehlenden Zustand aussagt. In allen drei Fällen versagte das Urteilsvermögen: das Ungeklärte einfach als Ablehnung hinzunehmen, das Inferierte vom Bestätigten zu trennen sowie zu erkennen, dass eine neue Antwort einer alten widerspricht. Das ist ein Problembereich des Denkens und nicht eines strukturellen Rahmens, und das Hinzufügen von Struktur behebt es nicht direkt.

Warum das wichtig ist

Für die Art von Problemen, auf die Werkzeuge wie DeepAgents abzielen, ist das Zustandsmanagement der wichtigste Hebel, während die Qualität der Argumentation das Überbleibende ist – schwieriger zu bewältigen und allein durch Hilfsmittel nicht zu erreichen. Backend-Systeme, Berechtigungen, isolierte Delegation sowie kontrollierte Speicherzugriffe ersetzen kein gutes Urteilsvermögen. Sie sorgen dafür, dass die Aktionen, der Zustand und die Ergebnisse des Systems beobachtbar und steuerbar sind: sichtbar, nachvollziehbar und rückgängig machbar, bevor sie endgültig werden. Dadurch lässt sich das Urteilsvermögen danach leichter bewerten, doch es kann nicht garantieren, dass das Urteil richtig ist. Ein abgelehnter Schreibvorgang, der dennoch als Erfolg bezeichnet wird, ist ein echter Fehler – doch er wird sofort erkannt, weil das System so konzipiert ist, dass genau solche Behauptungen überprüft statt blind geglaubt werden können.

Letztendlich dient genau das dem Zweck eines solchen Werkzeugs.

Verwandte Literatur

  • Agent Harness, Loop und Graph: Drei Schichten hinter zuverlässigen KI-Agenten — Warum zuverlässige Agenten in der Regel eine Schicht vermissen: der Harness um das Modell, eine evidenzbasierte Schleife sowie ein expliziter Graph, zusammen mit Codemustern und einer Fehlerbehebungs-Checkliste.
  • KI-Architektur für Produktionsagenten: Fehlermöglichkeiten und Steuerungsschichten — Ein schichtweiser Architekturleitfaden für Produktionsagentensysteme: Fehlermöglichkeiten, Schutzmechanismen, Bewertungen, Überwachbarkeit, Kosteneinschränkung, Zero Trust und Governance.
  • Warum Langzeit-Agenten ein Harness benötigen: Wie Deep Agents Zustand und Kontext verwalten — Ein genauer Blick auf Deep Agents: Kontextverlust, die vier Säulen, Middleware, Harness-Profilen, Dateisystem-Backends, isolierte Subagenten, Fehlschläge in Ausnahmesituationen und Alternativen.