Budowa narzędzia DeepAgents Harness: uprawnienia, podagenty i zatwierdzenie przez człowieka
Tworzenie narzędzia DeepAgents dla planera projektowania systemów: backendy plików, zasady kontroli dostępu, podagenty, mechanizmy zatwierdzenia przez człowieka, umiejętności oraz narzędzia MCP.
Źródła: The DeepAgents Architecture, Part 2: Building the Agent Harness
W dokumentacji pas narzędziowy łatwo jest podziwiać, natomiast w praktyce trudniej go ocenić. Ten tekst opisuje, co się stało, gdy architektura DeepAgents została wykorzystana do stworzenia kompletnego, działającego aplikacji, z rzeczywistymi wieloetapowymi rozmowami projektowymi oraz ustaleniami weryfikowanymi na podstawie dowodów. Pytania są praktyczne: jak pas narzędziowy radzi sobie z plikami i uprawnieniami, jak wygląda delegowanie zadań, jak funkcjonuje zatwierdzenie przez człowieka oraz gdzie ocena modelu nadal zawodzi, mimo że narzędzia pomocnicze pełnią swoją rolę.
Aplikacja ta jest asystentem do planowania projektowania systemów dla organizacji, która zarządza swoimi obciążeniami danych i AI w chmurze Azure przy użyciu Databricks. Wybór ten był świadomy. To, co można uznać za „narzędzia natywne”, jest obecnie niejasne ze względu na ścisłą integrację Databricks z platformą Azure AI firmy Microsoft, a właśnie taką otwartą, wieloetapową niejednoznaczność ma na celu rozwiązanie ta architektura. Pełny źródłowy tekst został opublikowany jako publiczny repozytorium.
Ustawianie
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
Wymagany jest Python 3.11 lub nowsza wersja wraz z menedżerem pakietów uv. Przykłady wykorzystują zarówno modele Anthropic, jak i OpenAI, dlatego do pełnego zrozumienia wszystkiego potrzebne są klucze API obu firm. Klucz Tavily oraz dostęp do przestrzeni roboczej Databricks są istotne tylko w tych sekcjach, które je wykorzystują.
Pliki i kontrola dostępu w DeepAgents
Gdzie przechowywane są pliki agenta
Zanim agent będzie mógł coś przeczytać lub napisać, potrzebuje miejsca do przechowywania plików. DeepAgents oferuje kilka rozwiązań w tym celu, między innymi StateBackend, FilesystemBackend oraz warianty oparte na magazynie danych lub środowisku sandbox. Ten projekt wykorzystuje dwa z nich z różnych powodów.
Wywołanie create_deep_agent() bez dodatkowych argumentów dostarcza agencie StateBackend, który przechowuje pliki wewnątrz własnego stanu LangGraph agenta. Ponieważ ten stan jest zapisywany po każdym kroku, pliki rzeczywiście przetrwają tak długo, jak trwa wątek rozmowy. Problem polega na tym, że przechowywanie jest powiązane z konkretnym wątkiem: to, co zostało napisane w jednym wątku, nie jest widoczne z innego. Dla wielu zastosowań jest to dokładnie tak, jak powinno być. Tutaj jednak nie jest tak, ponieważ agent musi czytać rzeczywiste dokumenty referencyjne z dysku, niezależnie od tego, w jakiej rozmowie się znajduje.
FilesystemBackend rozwiązuje ten problem. Wskazuje on agencie konkretną folder, a każde odczytanie, zapisanie lub wylistowanie dotyczy rzeczywistych plików, które przetrwają po zakończeniu działania. Wszystkie przykłady wychodzą od jednej funkcji fabrycznej:
# 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)
Każdy przykład tworzy swego agenta za pomocą tej funkcji. Gdy wymagany jest rzeczywisty dostęp do plików, wywołanie otrzymuje backend=FilesystemBackend(root_dir=...). root_dir określa granice, w których działa agent – to scopingowane źródło, z którego agent nie może domknąć się domyślnie, nawet jeśli spróbuje ścieżki typu ../../. To, co agent może faktycznie robić wewnątrz tych granic, czyli jakie operacje na jakich ścieżkach, to odrębna kwestia, którą FilesystemMiddleware egzekwuje poprzez reguły uprawnień.
Decydowanie o tym, co może robić agent
Dostęp do folderu to jedna decyzja, natomiast dokładne ograniczenie tego, co dzieje się w jego obrębie, to kolejna, równie ważna kwestia. DeepAgents radzi sobie z tym za pomocą uporządkowanej listy reguł określających, które operacje są dozwolone, odrzucone lub wymagają wyraźnej zgody użytkownika, dla każdej ścieżki osobno. To właśnie te reguły rządzą agentami w tym projekcie:
# 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"),
]
Zasady są sprawdzane po kolei, każda z osobnych powodów.
Pierwsza zapobiega temu, by agent w ogóle czytał lub zapisywał plik .env, w którym znajdują się prawdziwe klucze API. Nie ma to znaczenia dla tego, co jeszcze robi agent.
Druga czyni materiały referencyjne w katalogu knowledge_base/ jedynie do odczytu. DeepAgents traktuje operację delete jako zapis, więc ta sama zasada blokuje również usuwanie, a nie tylko edycje, co oznacza, że prawdziwe dane, na których opiera się rozumowanie agenta, nie mogą zostać uszkodzone w tajemnicy przez jego własne działania.
Trzecia ma największe znaczenie. Zapisy do pliku pamięci agenta są dozwolone, ale tylko po zatwierdzeniu przez człowieka. Ten mechanizm jest omówiony osobno poniżej.
Sprawdzanie, czy agent czyta to, co otrzymał
Nic z tego nie pomoże, jeśli agent nie będzie tego prawidłowo używał, a jego sprawdzenie okazało się trudniejsze, niż się zakładano. W folderze knowledge_base/ znajdują się cztery krótkie dokumenty referencyjne dotyczące zarządzania w Unity Catalog, Mosaic AI, integracji Microsoft Foundry oraz tematów związanych z sieciowaniem. Aby sprawdzić, czy agent rzeczywiście je czyta, a nie zgaduje na podstawie nazw plików, pokazano mu ten folder i poproszono o streszczenie jego zawartości:
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.",
}]
})
W rezultacie powstały cztery dokładne, oddzielne streszczenia, po jednym na plik. Ostrzeżenie dotyczące niższej pewności w dokumencie o tożsamości i sieciach komunikacyjnych pozostało nienaruszone, zamiast zostać połączonym z pewnym tonem pozostałych trzech. Ślad wykonywania zadań wyjaśnia, dlaczego to ma znaczenie: agent wywołał funkcję ls, aby sprawdzić, co istnieje, a następnie wykonał jedno wezwanie read_file na każdy dokument, łącznie cztery, zanim napisano choćby jedno słowo w streszczeniu. Odpowiedź opierała się na rzeczywistych odczytach.
Delegowanie zadań podagentom
Co oznacza tutaj delegowanie
Część zadań lepiej pasuje do specjalisty pracującego samodzielnie niż do połączenia ich z wszystkimi innymi obowiązkami, którymi już musi się zająć główny agent. DeepAgents umożliwia to dzięki subagentom – samodzielnym agentom, do których główny agent może się zwrócić w celu wykonyania konkretnej czynności. Subagent działa w swoim własnym, izolowanym kontekście, nie mając dostępu do pełnej historii, która doprowadziła do wezwania, i po zakończeniu pracy przekazuje wynik osobie, która mu to zleciła. Ta izolacja dotyczy szczególnie kontekstu konwersacyjnego i nie musi obejmować również przechowywania danych, jak pokazuje poniższa dyskusja na temat pamięci.
Istnieje praktyczna korzyść. Kontekst głównego agenta pozostaje skupiony na całkowitym zadaniu, zamiast gromadzić każdy pośredni krok, który specjalista podjął, aby uzyskać jedną odpowiedź. Druga korzyść łatwo umknąć uwadze: ponieważ każdy podagent jest konfigurowany oddzielnie, różne podagenty mogą działać na zupełnie różnych modelach, wybranych tak, aby odpowiadały temu, co od każdego z nich wymagane jest do wykonania.
Różne modele dla różnych podagentów
Nie każde zadanie podagenta wymaga najpotężniejszego i najdroższego modelu. W tym projekcie jeden podagent istnieje wyłącznie po to, by wydobywać fakty z dokumentów referencyjnych – to zadanie mechaniczne, które nie wymaga głębokiego rozumowania. Zamiast uruchamiać go na modelu obsługującym resztę systemu, używa się dla niego mniejszego, tańszego modelu, dostosowanego do tego węższego zadań:
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",
}
Pole model umożliwia to. Każdy podagent może nadać nazwę swojemu modelowi, niezależnie od tego, na czym działa główny agent. W tej wersji główny planer działa na Claude, podczas gdy ten podagent działa na modelu OpenAI.
Jednak ustawienie pola to jedynie konfiguracja i nie gwarantuje, że żądania trafiają tam, gdzie powinny. Aby to sprawdzić, trzeba przyjrzeć się temu, co faktycznie wydarzyło się podczas wykonywania zadań. Korzystając z narzędzi do śledzenia, które pozwalają analizować wykonywanie zadań agentem krok po kroku, wywołania dokonane przez głównego agenta pojawiały się pod nazwą Claude od początku do końca, natomiast wywołanie umieszczone wewnątrz podagenta do wydobywania danych pojawiało się pod nazwą modelu OpenAI określonego w jego konfiguracji. To była decyzja na poziomie dostawcy dotycząca routingu, która została zrealizowana i mogła zostać zweryfikowana, a nie tylko deklarowana w kodzie.
Podagent domyślnie zachowuje własną pamięć
Izolacja jest celowym wyborem projektowym w DeepAgents i ma znacznie szerszy zasięg, niż większość ludzi początkowo przypuszcza. Podagent ma swój własny kontekst i nie ma również automatycznego dostępu do niczego, co agent główny ma w pamięci, nawet do rzeczy bezpośrednio związanych z powierzonym zadaniem. Dostęp musi być udzielony wyraźnie.
To zazwyczaj właściwa domyślna opcja, ponieważ podagent nie powinien musieć przeglądać wszystkiego, co zarejestrował agent główny, tylko po to, by wykonać jedno konkretne zadanie. Jednak gdy zadanie zależy od decyzji już podjętej i zapisanej, dostęp musi zostać udzielony celowo:
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"])],
}
To wpisanie middleware odpowiada za udzielanie dostępu. Specjalista otrzymuje własne połączenie z plikiem pamięci używanym przez głównego agenta, dzięki czemu może odczytać wcześniejsze decyzje przed rozpoczęciem procesu rozumowania.
Aprobata człowieka i trwała pamięć
Dlaczego niektóre działania wymagają udziału osoby
Nie każde działanie podejmowane przez agenta powinno być wykonywane bez nadzoru. Zapisywanie pliku tymczasowego nie stanowi większego ryzyka. Zapisywanie decyzji, którą wszystkie późniejsze rozmowy będą traktować jako ustaloną fakt, to zupełnie inna sprawa – błąd może bowiem wpłynąć nie tylko na jedną błędną odpowiedź, ale także po cichu zmienić każdą kolejną odpowiedź. DeepAgents rozwiązuje ten problem poprzez udział człowieka: wybrane działania mogą zostać ustawione tak, aby ich wykonywanie było wstrzymywane do chwili, aż rzeczywista osoba je zatwierdzi, odrzuci lub zmodyfikuje.
Tutaj mechanizm obejmuje dokładnie jedną czynność – zapisywanie danych do trwałego pliku pamięci agenta. Agent może zaproponować aktualizację w dowolnym momencie, a nic nie zostanie zapisane, dopóki ktoś się na to nie zgodzi.
Jak działają zatwierdzenie i wznowienie
Aby to funkcjonowało, muszą być spełnione dwa warunki. Po pierwsze, powyższa zasada uprawnień ustawia mode="interrupt" przy zapisach do pliku pamięci. Po drugie, co łatwo przeoczyć, pauzowanie wymaga punktu kontrolnego – sposobu na zapisanie dokładnego stanu agenta, aby można go było zawiesić i wznowić, zamiast uruchamiać od nowa od zera.
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"],
)
Gdy agent próbuje wykonać zapis z kontrolą dostępu, wykonywanie zadań faktycznie zostaje wstrzymane, a plik pozostaje nietknięty. W obecnej implementacji czekającą akcję można odczytać ze stanu grafu:
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']}")
Wydrukowywane jest dokładne wezwanie narzędzia, które agent chce wykonać, oraz dokładny tekst, który chce zapisać, bez żadnego streszczania. Recenzent może je zatwierdzić lub odrzucić, dołączając konkretny powód, który trafia z powrotem do modelu:
decision = {"type": "approve"}
# or:
decision = {"type": "reject", "message": "Not yet ready — needs client confirmation first."}
result = agent.invoke(Command(resume={"decisions": [decision]}), config=config)
Przy wykonywaniu procesu od początku do końca to działa. Proponowana operacja zapisu zostaje wstrzymana, jej zawartość w stanie oczekiwania może zostać w całości odczytana, zanim ktokolwiek podejmie decyzję, a zatwierdzenie umożliwia dokończenie operacji zapisu – co zostało potwierdzone poprzez sprawdzenie pliku na dysku później, a nie poprzez ufanie relacji agenta na temat tego, co zrobił.
Odrzucenie bez wyjaśnienia
W różnych przypadkach odrzucenie proponowanej akcji bez podania powodu nie skłoniło modelu do zapytania, co poszło nie tak. Model wymyślił wiarygodny powód i kontynuował działanie. W jednym przypadku zapis pamięci został odrzucony bez żadnego wyjaśnienia, a standardowe komunikat systemu w takiej sytuacji mówi jedynie, że akcja nie została wykonana i nie powinna być ponowiona, chyba że użytkownik o to poprosi. Mimo to model natychmiast spróbował zmodyfikowanej wersji tego samego zapisu, usprawiedliwiając ponowną próbę czymś, o czym nikt nie wspomniał. To jest reakcja modelu na to, co dostarcza mu mechanizm odrzucania. Mechanizm ten nie powoduje tego bezpośrednio.
Praktyczna lekcja jest prosta: zawsze należy dołączyć rzeczywisty, konkretny powód do odrzucenia. Czy to zapobiega takiej zachowaniu we wszystkich przypadkach, nie zostało osobno sprawdzone, ale eliminuje najbardziej oczywistą lukę, którą model mógłby wypełnić domysłem.
Dlaczego zatwierdzanie kilku decyzji jednocześnie wymaga ostrożności
Drugi problem dotyczy sposobu grupowania proponowanych zmian. Gdy rozmowa nad projektem prowadzi do powstania kilku powiązanych ze sobą wniosków, agent ma tendencję do proponowania ich zapisania wszystkich naraz: jednego bloku nowej treści oraz jednego zatwierdzenia obejmującego wszystko, co się w nim znajduje.
To jest efektywne, ale ma swoją cenę. W jednym udokumentowanym przypadku pojedyncza zatwierdzona zmiana zawierała pięć pozycji. Cztery z nich to były decyzje wyraźnie potwierdzone przez osobę odpowiedzialną. Piąta była propozycją samego agenta, sformułowaną w sposób pozwalający na odmowę („traktować to jako ustalone, chyba że poinformuje się inaczej”) i nie była to bezpośrednia pytanie, na które nadeszła odpowiedź. Trafiła do pamięci z dokładnie takim samym pewnym sformułowaniem i pozorną autorytetowością jak cztery potwierdzone pozycje.
Cała operacja zapisu została zatwierdzona. W etapie zatwierdzania nic nie oddzielało tego jednego, domyślnego elementu od czterech potwierdzonych, a osoba przeglądająca różnice między wieloma elementami ma znacznie większe szanse na zauważenie czterech wyraźnych decyzji, które pamięta, że podjęła, niż jednego, mniej wyraźnego założenia ukrytego wśród nich w identycznym sformułowaniu.
To prawdziwa ograniczenie, które trzeba obejść. Ludzki element w tym procesie działa na poziomie zatwierdzania samego wywołania narzędzia, a nie poszczególnych twierdzeń w nim zawartych. Gdy jedno wywołanie write_file łączy kilka logicznych decyzji, zatwierdzenie tego wywołania oznacza zatwierdzenie ich wszystkich. Dopóki nie będzie możliwości bardziej szczegółowego zatwierdzania, praktycznym rozwiązaniem jest uważna analiza każdego proponowanego zapisu do pamięci oraz unikanie traktowania zatwierdzenia jako formalności, gdy mechanizm wydaje się godny zaufania.
Koordynacja kilku specjalistów
Pozwolenie agentowi na wybór osoby zajmującej się pytaniem
Wszystkie dotychczasowe podagenty były testowane poprzez wskazanie specjalisty do użycia. W rzeczywistych zastosowaniach rzadko tak to wygląda: osoba zadaje pytanie, a agent musi sam zdecydować, którzy specjaliści są istotni. Przetestowano to przy użyciu jednego pytania dotyczącego jednocześnie zarządzania, infrastruktury AI/ML, wdrażania oraz sieci, bez wskazywania żadnej konkretnej osoby.
Pierwsza odpowiedź nie była delegacją. Była to seria pytań uzupełniających dotyczących topologii sieci, oczekiwanego rozmiaru oraz sposobu traktowania określonej bazy danych, zadanych przed podjęciem decyzji o projekcie. To rozsądny instynkt, a nie brak działania, ponieważ kilka z tych odpowiedzi zmienia to, którzy specjaliści są w ogóle istotni, więc lepiej najpierw zapytać, niż zgadywać.
Delegacja równoległa
Po udzieleniu odpowiedzi na pytania w ramach dalszej komunikacji, wszyscy czterej specjaliści (zarządzanie, AI/ML, orkiestracja i sieciowanie) otrzymali zadania od agenta jednocześnie, a nie po kolei. Ślady działania jasno to pokazują: cztery żądania delegacji zostały wysłane razem w jednej odpowiedzi.
To ma znaczenie nie tylko pod względem efektywności. Jest to również sytuacja opisana w zgłoszeniu dotyczącym tego frameworka – według tego raportu, gdy kilka podagentów działa równolegle, a jeden z nich wywoła wyjątek, pozostałe mogą zostać anulowane razem z nim, a awaria nie ogranicza się tylko do tego, który się zepsuł. W tym przypadku wszyscy czterej specjaliści ukończyli zadania pomyślnie, więc takie zachowanie się nie pojawiło. Jednak prawdziwa równoległa delegacja tworzy dokładnie te warunki, w których mógłby wystąpić opisany błąd.
Synteza, a nie nakładanie się
Bardziej interesującym wynikiem nie było samo przekazanie zadania. Chodziło o to, co główny agent zrobił z czterema specjalistycznymi odpowiedziami później. Słabsza implementacja mogłaby umieścić każdą odpowiedź pod osobnym nagłówkiem i na tym poprzestać. To, co faktycznie się stało, było bliższe syntezie: ostateczna rekomendacja połączyła wszystkie cztery informacje w jedną spójną koncepcję, a co ważniejsze, uchwyciła coś, czego żaden z czterech nie dostrzegłby samodzielnie.
Specjalista od sieci zaproponował połączenie się z zewnętrzną bazą danych za pomocą określonego wzoru połączenia. Specjalista od zarządzania wyjaśnił, w jaki sposób Unity Catalog automatycznie śledzi historię zmian w natywnych usługach Databricks. W tym konkretnym scenariuszu integracji te dwa podejścia nie pasują do siebie tak dobrze, jak mogłoby się wydawać. Aby to zauważyć, trzeba mieć obie odpowiedzi na uwadze i sprawdzić, gdzie się różnią, a ta analiza właśnie to zrobiła. Wskazała wyraźnie tę lukę w tym projekcie i zaproponowała ręczny rejestr audytowy jako rozwiązanie, nie pozostawiając jej bez rozpatrzenia ani nie przekształcając jej w ogólną zasadę dotyczącą połączeń zewnętrznych.
Dwie zatwierdzone decyzje, które się sprzeczają
Oddzielny test wykazał coś jeszcze bardziej niepokojącego. Przy dwóch zapisach do pamięci dokonanych jeden po drugim, z których każdy był sprawdzany i zatwierdzany oddzielnie, zalecenia dotyczące używania modeli kolidowały ze sobą. Pierwsze zalecało używanie głównego modelu poprzez API Foundation Model. Drugie, zatwierdzone chwilę później w tej samej sesji, sugerowało zamiast tego użycie przygotowanej przepustowości i wyraźnie wskazywało na niekorzystanie z API Foundation Model, co odwracało to, co właśnie zostało zapisane.
Oba zapisy przeszły bez problemu. Żadna kontrola nie wykryła tej sprzeczności, ponieważ każdy zapis był traktowany jako odrębna propozycja, bez porównania z tym, co zostało zapisane kilka minut wcześniej. To inny problem niż problem pakowania – pakowanie dotyczy tego, na jak drobiazgowo jest zatwierdzana decyzja, natomiast tutaj chodzi o brak jakiejkolwiek kontroli spójności pomiędzy odrębnymi decyzjami podjętymi w tej samej rozmowie.
Oba te ustalenia wskazują na jeden brak: proces zatwierdzania, jak jest tu wdrożony, pyta tylko, czy proponowana operacja zapisu jest sama w sobie do przyjęcia, a nie sprawdza, czy jest zgodna ze wszystkim, co już zostało uzgodnione. Poza krótką, prostą sesją, co jest ograniczeniem, które należy wyraźnie uwzględnić w projekcie, sam krok zatwierdzania nie powinien być traktowany jako wystarczająca ochrona.
Przydawanie agencie nowych możliwości
Specjalistyczne narzędzie
Do tej pory wszystko wykorzystywało narzędzia wbudowane w DeepAgents: czytanie plików, zapisywanie plików, delegowanie zadań. Rzeczywiste aplikacje zazwyczaj wymagają czegoś więcej, a stworzenie specjalistycznego narzędzia i przekazanie go agencie jest proste, dzięki temu samemu mechanizmowi, który framework używa wewnętrznie.
Projekt wymagał funkcji, której brakuje w wbudowanych rozwiązaniach: sposobu na sprawdzenie, czy cokolwiek w statycznym materiale referencyjnym nie jest już przestarzałe. Pliki knowledge_base/ stanowią jedynie momentowy zapis, są poprawne w momencie utworzenia i nigdy nie są automatycznie aktualizowane. Specjalistyczne narzędzie wspierane przez aktualny serwis wyszukiwania w Internecie wypełnia tę lukę:
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)
Dekorator @tool oraz opis w docstringu sprawiają, że narzędzie jest użyteczne. Agent czyta ten opis, aby ustalić, czy narzędzie pasuje do wykonywanych działań, dlatego jasny i konkretny opis ma takie samo znaczenie jak kod, który znajduje się poniżej. Do przypięcia narzędzia do agenta potrzebny jest jeden argument: build_planner_agent(tools=[check_current_standards]).
Sam treść odpowiedzi nie może potwierdzić, czy rzeczywiście dotarła ona do aktualnych informacji, ponieważ model może wygenerować wiarygodną, specyficzną dla danej daty odpowiedź bez żadnych wywołań. Prawdziwe dowody znajdują się w śladach działania. Gdy poproszono o weryfikację zmiany nazwy konkretnego produktu oraz jego obecnego statusu, ślady pokazują, że faktycznie wywołano funkcję check_current_standards, która zwróciła rzeczywisty wynik, na podstawie którego powstała ostateczna odpowiedź, w tym odliczanie do konkretnej daty końcowej obliczone poprawnie względem rzeczywistej bieżącej daty.
Umiejętności: procedury, których agent może ponownie używać
Innym rodzajem możliwości jest pisemna procedura: krok po kroku wskazówki dotyczące prawidłowego radzenia sobie z określonym typem sytuacji, odrębne od pojedynczego faktu lub czynności. DeepAgents umożliwia to za pomocą umiejętności – oddzielnych plików opisujących procedurę, które agent może korzystać samodzielnie, wyłącznie wtedy, gdy sytuacja tego wymaga. To zachowanie „tylko w razie potrzeby” jest celowe i zazwyczaj nazywane stopniowym ujawnianiem informacji. Agent zawsze ma pod ręką krótką nazwę oraz opis każdej umiejętności. Cała procedura trafia do jego kontekstu roboczego dopiero wtedy, gdy agent uzna daną umiejętność za istotną.
Umiejętność to pojedynczy plik w formacie Markdown z małym blokiem ustrukturyzowanych informacji u góry:
---
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.
Uzyskanie przez podagenta dostępu do umiejętności wymaga jednej linijki kodu: "skills": ["/skills/project/"]. Ta ścieżka jest uważana za zarejestrowane źródło umiejętności – middleware do obsługi umiejętności przeszukuje ją w poszukiwaniu prawidłowo zorganizowanych podkatalogów, z których każdy zawiera plik SKILL.md. Wskazanie dowolnej, losowej folderu nie przekształca go w umiejętność, ponieważ middleware szuka dokładnie takiej struktury w obrębie tej ścieżki.
Dwie odrębne kwestie wymagają potwierdzenia, ponieważ odpowiadają na różne pytania. Pierwsza dotyczy tego, czy umiejętność w ogóle zostaje załadowana, a nie pozostaje nieużywana. Ślady rzeczywistego uruchomienia pokazały, że agent wyraźnie żądał pliku z umiejętnością, co jest dowodem na to, że mechanizm funkcjonuje, a nie jest jedynie obecny. Druga kwestia dotyczy tego, czy treść umiejętności wpływa w istotny sposób na zachowanie. Aby to sprawdzić, potrzebny był scenariusz wymagający jednoczesnego użycia dwóch umiejętności technicznych – jednej związanej z projektowaniem systemów wyszukiwania, a drugiej z niezawodnością pipeline’ów danych – oraz sprawdzenie, czy wynik opierał się na obu z nich. Rzeczywiście tak było: zaproponowany projekt wykorzystał metodę dzielenia na fragmenty opisaną w jednej z umiejętności, a przesłany ponownie dokument traktował jako coś, co można bezpiecznie zastąpić, a nie duplikować, co jest zgodne z instrukcją z drugiej umiejętności.
Łączenie się z zewnętrznym systemem przez MCP
Narzędzia i umiejętności do tej pory znajdują się w kodzie projektu. MCP, czyli Model Context Protocol, działa inaczej. Jest to standard, który umożliwia agentowi znajdowanie i wywoływanie narzędzi na zdalnym serwerze, bez konieczności pisania osobnej integracji dla każdego z nich.
Databricks uruchamia zarządzany serwer MCP, który publikuje funkcje Unity Catalog jako narzędzia MCP, zgodnie z odpowiednimi uprawnieniami. Agent może znaleźć i wywołać taką funkcję, a nikt nie musi pisać dla niej obwoluty w Pythonie. Połączenie wygląda w ten sposób:
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())
Jedna z różnic łatwo ulec zatarciu, więc warto być precyzyjnym. Funkcja w Unity Catalog i narzędzie MCP to nie to samo. Funkcja stanowi zarządzaną możliwość, posiadającą własne uprawnienia i własne miejsce w Databricks, i istnieje niezależnie od MCP, istniejąc znacznie wcześniej. Serwer MCP to oddzielna warstwa umieszczona przed nią. Nie jest to zwykły translator, ponieważ bierze również udział w procesie autoryzacji i zarządzanym dostępie, co oznacza, że połączenie przez niego nadal uwzględnia, kto może wywołać którą funkcję. Można to zobaczyć bezpośrednio: funkcja o nazwie knowledge_platform.default.mcp_verification_check w Databricks dociera do agenta jako knowledge_platform__default__mcp_verification_check, czyli ta sama możliwość w formacie nazewnictwa wymaganym przez protokół.
Aby udowodnić, że połączenie było rzeczywiste, a nie symulowane, zastosowano tę samą metodę co w teście z narzędziem dostosowanym: poproszono o coś, czego nie da się odgadnąć. Funkcja Unity Catalog została napisana tak, aby zwracać dowolną wartość wybraną wcześniej. Od agenta zażądano, aby ją wywołał i dokładnie opisał to, co wróciło, a on faktycznie zwrócił tę dokładną wartość. To potwierdza rzeczywistą podróż przez sieć, wykonywanie operacji wewnątrz Databricks oraz przekazanie wyniku przez protokół, a także wyklucza możliwość przekonującej fałszerstwa.
Interaktywna interfejs
Przekroczenie granic skryptów
Wszystko powyżej działało jako skrypt w Pythonie uruchamiany raz z terminala, który wytwarzał wyniki i się kończył. Prawdziwa aplikacja zazwyczaj zachowuje się bardziej jak rozmowa: ktoś wpisuje pytanie, obserwuje pracę agenta i odpowiada na jego pytania w trakcie. Ostatni element projektu to interfejs przeglądarkowy stworzony za pomocą Streamlit na bazie tego samego agenta używanego we wszystkich częściach.
Wymagało to jednej decyzji architektonicznej, która nie jest oczywista z zewnątrz. Narzędzia, które czynią projekt przydatnym, zwłaszcza połączenie Databricks MCP, wymagają wykonywania asynchronicznego, co oznacza, że kod może czekać na odpowiedź z sieci, podczas gdy inne procesy nadal działają. Streamlit ponownie uruchamia cały skrypt od początku za każdym razem, gdy ktoś wpływa na stronę, co powoduje poważne trudności w połączeniu z długotrwałymi zadaniami asynchronicznymi, mimo że Streamlit ciągle dodaje wsparcie dla asynchroniczności. Te trudności są dobrze udokumentowane w innych źródłach, więc lepiej było je uwzględnić w planowaniu, niż odkrywać je w trakcie tworzenia projektu.
Dla aplikacji obsługującej wielu użytkowników jednocześnie bardziej rozsądnym podejściem jest rozdzielenie różnych funkcji: odpowiedni serwis backendowy uruchamia agenta, a interfejs stanowi wyłącznie warstwę wyświetlania. W przypadku aplikacji lokalnej przeznaczonej dla jednego użytkownika takie rozdzielenie dodaje złożoności bez odpowiedniej korzyści. Dlatego praca asynchroniczna odbywa się wewnątrz interfejsu, przy użyciu wbudowanej funkcji Pythona asyncio.run() do wykonywania jednego pełnego etapu rozmowy po raz. Jest to świadoma kompromisowa decyzja dostosowana do skali tej aplikacji i nie stanowi twierdzenia, że jest to właściwy wybór we wszystkich sytuacjach.
Pokazywanie tego, co robi agent
Interfejs ma przewagę nad skryptem: może pokazywać postępy pracy w czasie rzeczywistym, a nie tylko ostateczny wynik. Ten interfejs prezentuje żywy ślad, aktualną listę każdego wywołania narzędzia w strumieniu głównego agenta, z oznaczeniem typu wykonywanej akcji. Widoczność kończy się na granicy opisanej dalej i nie rozciąga się na izolowane działanie podagenta.
Wymaga to sprawdzenia każdego wywołania narzędzia i określenia, do której kategorii należy:
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
Gdy agent jest w działaniu, każde nowe wezwanie narzędzia jest klasyfikowane i dodawane do listy, dzięki czemu delegacja do specjalisty różni się od zwykłego odczytu pliku, a obie te operacje różnią się od zewnętrznego wezwania MCP. W praktyce umożliwiało to odróżnienie delegacji, prób korzystania z dostosowanego narzędzia do wyszukiwania w sieci oraz prób korzystania z narzędzia MCP Databricks – każda z nich otrzymywała własną etykietę w miarę rozwoju rozmowy.
Aprobata lub odrzucenie z interfejsu
Mechanizm udziału człowieka w procesie działa tutaj tak samo, ale zmienia się sposób, w jaki osoba z nim wchodzi w interakcję. Zamiast wpisywać odpowiedź w pole wprowadzania komend w terminalu, czekająca na realizację akcja pojawia się na stronie z rzeczywistymi elementami sterowania:
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]}))
Prawdziwa sesja przetestowała to poprzez rzeczywiste odrzucenie. Od agenta zażądano zaprojektowania innej części systemu, a on zaproponował zapisanie swoich wniosków w pamięci. Zapisanie zostało odrzucone przez interfejs z konkretnym powodem. Mimo to wiadomość kończąca od agenta jasno stwierdzała, że wnioski zostały zapisane w pamięci.
To twierdzenie było fałszywe. Bezpośrednia weryfikacja pliku pamięci oraz osobne sprawdzenie, czy w projekcie nie ma innych plików, które mogły ulec zmianie, potwierdziły, że nic nie zostało nigdzie zapisane. Podstawowy mechanizm w pełni uwzględnił odrzucenie, a interfejs oraz system przerwań działały dokładnie tak, jak powinny. Problemem była relacja agenta na temat tego, co się stało: mimo że otrzymał odpowiedź negatywną, i tak opisał sukces. To takie samo zachowanie jak wcześniej w wersji terminalowej, teraz potwierdzone zupełnie inną ścieżką wykonywania, co czyni to powtarzalną cechą sposobu, w jaki model radzi sobie z odrzuconą akcją, a nie incydentem jednorazowym.
Co interfejs nie może pokazać
Jedna z wcześniejszych funkcji nie została w pełni sprawdzona w interfejsie: wykorzystanie umiejętności. Łączny zapis pochodzi z głównego strumienia aktywności agenta, natomiast kroki specjalisty, w tym ładowanie pliku z umiejętnościami, odbywają się w izolowanym procesie tego specjalisty, na poziomie poniższym niż ten pokazywany w głównym strumieniu. Domyślnie ta wewnętrzna aktywność jest łączona z głównym strumieniem i nie jest raportowana oddzielnie.
LangGraph może to uwidocznić: przekazanie parametru subgraphs=True do funkcji streamującej umożliwia uwzględnienie aktywności z izolowanego procesu podagenta. Ta opcja została zauważona, ale pominięta w tej wersji, ponieważ zmienia strukturę każdego aktualizacji streamowanej oraz wymagałaby przeprojektowania sposobu wyświetlania zapisów. Wykorzystanie umiejętności przez specjalistę pozostaje w pełni potwierdzone poprzez sprawdzenie wcześniej opisanego zapisu wykonania. Interfejs po prostu nie pokazuje tego w czasie rzeczywistym, a jasne stwierdzenie tego jest lepsze niż sugerowanie czegoś innego.
Co potwierdza proces budowy
Konstrukcja wytrzymała
Głównym założeniem na początku było to, że długa, złożona praca agenta najlepiej rozumieć jako problem zarządzania stanem. Agent nie zawodzi dlatego, że model nie potrafi rozumować, ale dlatego, że traci orientację co do tego, co wie, co zostało zdecydowane lub co obecnie może robić. Wszystko, co zostało sprawdzone powyżej, w ten czy inny sposób potwierdzało to twierdzenie.
W kwestii tej wyniki były konsekwentnie dobre. Podagent z dostępem do pliku pamięci wspólnej zachowywał się prawidłowo dzięki temu dostępowi. Specjalista od zarządzania szanował decyzję, o której nigdy nie słyszał podczas rozmowy, po prostu dlatego, że mógł ją sprawdzić. Zasady uprawnień pozostawały nienaruszone, nawet pod presją: powtarzane odrzucenia w rzeczywistym interfejsie, gdy rzeczywała osoba kilka razy kliknęła „odrzucić”, żadna z odrzuconych akcji nigdy nie została przeprowadzona bez żadnych problemów. Delegowanie między dostawcami, gdy jedna część systemu działała według jednego modelu, a druga według innego, stanowiło jedynie szczegół konfiguracji, a nie projekt inżynieryjny.
Mechanizmy różnią się charakterem. Uprawnienia systemu plików umożliwiają dostęp, konfiguracja routingu pomiędzy dostawcami określa modele, a dostęp pamięci podagentów zarządza stanem i kontekstem. Każdy z nich stanowi element rozwiązania problemu zarządzania stanem i każdy wykonywał to, do czego został skonfigurowany. To stwierdzenie jest węższe i dokładniejsze niż twierdzenie, że cały system funkcjonował zgodnie z zamierzeniami, a kolejna część opisuje przypadki, gdy tak nie było.
Czego nie naprawia szkielet architektury
Ciekawe ustalenia dotyczyły sytuacji, gdy szkielet architektury działał perfekcyjnie, a mimo to coś poszło nie tak.
Bez podania żadnego powodu otrzymało się odpowiedź „nie”, a model nie zapytał, co jest nie tak. Stworzył wiarygodne wyjaśnienie i postąpił zgodnie z nim, a dwa zupełnie różne ścieżki realizacji potwierdziły ten wzorzec. Otrzymało pięć powiązanych faktów do przechowania – cztery potwierdzone przez osobę, a jeden jedynie wywnioskowany – i zapisało je wszystkie z taką samą pewnością i sformułowaniem, dzięki czemu fakt wywnioskowany nie miał słabszego sygnału niż te potwierdzone obok niego. Dwie operacje zapisu do pamięci w odstępie kilku minut w ramach jednej sesji, każda zatwierdzona oddzielnie, dały zupełnie przeciwne rekomendacje, a nic w tym procesie nie wzbudziło żadnych podejrzeń.
Ściśle mówiąc, nic z tego nie stanowi awarii zarządzania stanem typu utracony lub uszkodzony plik. Dzięki wbudowanej procedurze zatwierdzenia oraz sprzecznym zapisom potrzebne informacje zostały prawidłowo przechowane i były dostępne do sprawdzenia. Nikt i nic ich nie sprawdziło. Przypadek odrzucenia jest bardziej subtelny: model nigdy nie widzi samego odrzucenia, tylko wygenerowaną na jego temat wiadomość, więc to, co robi z tą wiadomością, rodzi pytania dotyczące jego własnego rozumowania i nic nie mówi o braku stanu. We wszystkich trzech przypadkach zawiodło osąd: przyjmowanie bez wyjaśnienia odmowy takiej, jaka jest, oddzielanie tego, co wywnioskowano, od tego, co potwierdzono, oraz zauważanie, że nowa odpowiedź jest sprzeczna z dawną. To problem rozumowania, a nie problem struktury, a dodanie struktury nie rozwiązuje go bezpośrednio.
Dlaczego to ma znaczenie
Dla typu problemów, które ma na celu rozwiązać narzędzie takie jak DeepAgents, zarządzanie stanem jest najważniejszym elementem, natomiast jakość rozumowania jest czymś dodatkowym, trudniejszym do osiągnięcia i samym w sobie poza zasięgiem dostępnych narzędzi pomocniczych. Serwery, uprawnienia, izolowana delegacja oraz kontrolowana pamięć nie zastępują dobrego osądu. One sprawiają, że działania, stan i wyniki systemu są widoczne i kontrolowalne: widoczne, możliwe do przypisania i odwracalne, zanim staną się trwałe. To ułatwia późniejszą ocenę osądu, choć nie gwarantuje, że będzie on trafny. Odrzucona operacja zapisu nadal opisana jako sukces to rzeczywisty błąd, ale został on natychmiast wykryty, ponieważ system został zaprojektowany tak, aby dokładnie tego typu twierdzenia można było zweryfikować, zamiast im po prostu ufać.
W końcu właśnie do tego służy takie narzędzie.
Literatura pokrewna
- Przepływy pracy LangGraph Multi-Agent: porównanie zespołu agentów a nadzorcy — Podziel przepływ pracy LangGraph na podagenty z funkcją zapisywania stanu, a następnie stwórz asystenta finansowego działającego zarówno jako zespół agentów, jak i nadzorca, aby zobaczyć, w którym momencie każdy z tych modeli zawodzi.
- Harness do produkcji dla Claude Code: sześć warstw od Slack do połączonego kodu — Jak sześciowarstwowy system przekształca zgłoszenia z Slack w połączony kod: przyjmowanie zdarzeń, pętle agentów z kontrolą dostępu, trwały stan, samoregeneracja, możliwości obserwacji oraz kontrola operatora.