Strona główna / Artykuły

Nagi agent: Co się psuje, gdy model nie ma żadnej ochrony

Stwórz taki sam niezabezpieczony agent podróżniczy w Claude Agent SDK oraz LangChain Deep Agents, a następnie obserwuj trzy awarie, aby zobaczyć, jakie elementy sterujące są brakujące.

Wydawca: REACTAPP.TOP9 min czytania2152 słów

Źródła: Harness Engineering: The Naked Agent: Why Your Framework Hands You a Loop, Not a Harness — I

Klient prosi agenta o ponowne zarezerwowanie lotu. Model brzmi pewnie siebie, wywołanie narzędzia udaje się, a odpowiedź mówi, że wszystko zostało załatwione. Wtedy ktoś zauważa, że data to 32 marca.

To właśnie w tym momencie rozpada się iluzja. Modelowi nie brakowało inteligencji. Pękło wszystko wokół niego, ponieważ żaden komponent nie wiedział, kiedy należy interweniować. To, co następuje, rozpoczyna się od „nagiego” agenta: bez mechanizmów weryfikacji, bez pamięci, bez możliwości odzyskania stanu, bez żadnej ochrony. Obserwacja jego porażki ujawnia, gdzie popełniają błędy agenty produkcyjne, i pokazuje, że środowisko działania modelu językowego ma większe znaczenie niż sam model. Ten sam uproszczony agent jest budowany dwa razy – za pomocą Claude Agent SDK oraz LangChain Deep Agents – i wtedy zawodzi na trzy konkretne sposoby, z których każdy wskazuje na funkcję kontrolną, którą musi zapewnić prawdziwy agent. Rezultatem jest metoda pozwalająca odróżnić problem modelu od problemu związku z narzędziami obsługującymi go.

Incydent z 32 marca

Wyobraź sobie agenta rezerwacji podróży, który otrzymuje wiadomość „32 marca” od rzeczywistego użytkownika. Model przekazał tę treść bezpośrednio do API lotów. Po około czterdziestu minutach i pięćdziesięciu dolarach wydanych na wywołania modelu, ktoś musiał tłumaczyć klientowi nieistniejącą rezerwację, mimo że temu ostatniemu zapewniano, iż wszystko jest już załatwione.

Model nic nie wygenerował. Przesłał dokładnie to, co otrzymał, ponieważ system sterujący nie decydował o tym, czy powinien to zrobić. To podsumowuje całą sprawę. Inteligencja pochodzi od modelu, a dyscyplina od systemu sterującego, więc gdy agent produkcyjny zachowuje się niewłaściwie, wina leży niemal zawsze w warstwie dyscypliny. Zastąpienie go silniejszym modelem lub udoskonalenie instrukcji nie zapewni kontroli, której nie ma, a tworzenie mechanizmów kontroli zaczyna się od zrozumienia, ile kosztuje ich brak. Dlatego punkt wyjścia jest prosty: jeden kruchy agent, dwa frameworki, brak systemu sterującego.

Dowody na to, że czas wykonywania dominuje

Dane z badań benchmarkowych potwierdzają tę tezę. Badacze z Princeton zamarzli model na poziomie GPT-4 Turbo i zmienili jedynie kod interfejsu łączącego go z bazą kodu. Odsetek problemów z SWE-bench, które udało się rozwiązać, wzrósł z 3,8% do 12,47%, czyli prawie trzykrotnie, podczas gdy wagi modelu pozostały nienaruszone. Praca zatytułowana „SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering” ukazała się w 2024 roku i została przyjęta na konferencji NeurIPS w tym samym roku. To projekt interfejsu przyniósł te korzyści, a nie inteligencja modelu.

Podstawowa zasada ta ma starszą nazwę – mechanical sympathy: tworzenie oprogramowania, które działa we współpracy z sprzętem lub platformą, a nie wbrew im. Koncepcja harness engineering rozszerza ten podejście na nowy substrat, czyli modele językowe, z ich oknem kontekstowym i limitem uwagi. Model określa górny limit, a harness decyduje o tym, w jakim stopniu system może go osiągnąć.

Zmiana sposobu myślenia wpływa na to, co robimy po niepowodzeniu:

  • Wcześniej: po błędnej odpowiedzi dostosowujemy instrukcję, dodajemy zastrzeżenia, próbujemy większego modelu i mamy nadzieję
  • Później: po błędnej odpowiedzi określamy, który element kontroli brakuje, usuwamy tę zasadę z instrukcji i przenosimy ją do czasu wykonywania

Instrukcje przekonują, a mechanizmy egzekucji narzucają zasady.

Pętla w każdej ramie

Wszystkie ramy dla agentów opierają się na jednej pięciostopniowej pętli. Dostarczamy narzędzia modelowi, otrzymujemy informację o ich użyciu, je wykonywamy, zwracamy wynik i otrzymujemy kolejny krok. Dokumentacja Anthropic dotycząca używania narzędzi, API Responses od OpenAI oraz mechanizm wykonywania agentów w LangChain sprowadzają się do tego samego. SDK-y przychodzą i odchodzą, ale struktura pozostaje niezmienna.

Prosta wersja wykorzystuje surowe SDK dostawcy, dwa narzędzia do przemieszczania się i nie ma żadnego zabezpieczenia:

import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY
TOOLS = [
    {"name": "search_flights",
     "description": "Search flights between two cities for a date.",
     "input_schema": {"type": "object", "properties": {
         "origin": {"type": "string"}, "destination": {"type": "string"},
         "date": {"type": "string", "description": "YYYY-MM-DD"}},
         "required": ["origin", "destination", "date"]}},
    {"name": "book_flight",
     "description": "Book a specific flight.",
     "input_schema": {"type": "object", "properties": {
         "flight_id": {"type": "string"}, "passenger_name": {"type": "string"}},
         "required": ["flight_id", "passenger_name"]}},
]
def run_naked(user_msg: str) -> str:
    messages = [{"role": "user", "content": user_msg}]
    while True:                                   # ① no iteration cap
        resp = client.messages.create(
            model="claude-sonnet-4-6", max_tokens=1024,
            tools=TOOLS, messages=messages,
        )
        if resp.stop_reason != "tool_use":
            return resp.content[0].text
        call = next(b for b in resp.content if b.type == "tool_use")
        result = dispatch(call.name, call.input)
                                             # ② direct side effect, no check
        messages.extend([
                                             # ③ whole history, every turn
            {"role": "assistant", "content": resp.content},
            {"role": "user", "content": [{"type": "tool_result",
                "tool_use_id": call.id, "content": result}]},
        ])

Czytaj to, mając na uwadze trzy potencjalne problemy:

  1. Pętla while True: nie ma żadnych ograniczeń. Zawodny plan nadal wywołuje model, aż wyczerpie się budżet, ponieważ nie liczy się liczba iteracji.
  2. Funkcja dispatch przekazuje argumenty dostarczone przez model bezpośrednio do efektu ubocznego, bez żadnej wcześniejszej weryfikacji, w wyniku czego wartość, która poprawnie się parsuje, ale nie może istnieć, trafia do API.
  3. Pełna historia wiadomości jest rozszerzana i przesyłana na nowo przy każdej turze, co po cichu powiększa kontekst, aż spada jego jakość.

Żadna z właściwości bezpieczeństwa, których byś chciał, nie powinna znajdować się wewnątrz tego pętla. Znajdują się one w kodzie, który jeszcze nie został napisany. Zastosowanie frameworka również nie rozwiązuje tego problemu. Claude Agent SDK oraz Deep Agents oferują lepiej skonstruowaną pętlę, z odpowiednim protokołem narzędzi i uporządkowaną ścieżką wykonywania, ale fabrycznie nie zawierają żadnych mechanizmów weryfikacji, żadnej pamięci, nad którą można by pracować, ani możliwości odzyskania stanu. Pętla po prostu istnieje, a zadanie zarządzania nią pozostaje przy deweloperze.

Oto ten sam „nagi” agent w każdym frameworku.

Wersja Claude Agent SDK

SDK otacza tę pętlę za nas. Narzędzia są deklarowane za pomocą dekoratora @tool, udostępniane jako serwer MCP w ramach tego samego procesu i uruchamiane poprzez funkcję query(). Brakuje jednak jakiejkolwiek kontroli: argumenty są sprawdzane jedynie pod kątem schematu, nic nie jest przechowywane między wywołaniami, a nie ma możliwości odzyskania stanu.

import asyncio
from claude_agent_sdk import (
    query, ClaudeAgentOptions, tool,
    create_sdk_mcp_server, AssistantMessage, ResultMessage,
)

@tool("search_flights", "Search flights between two cities for a date.",
      {"origin": str, "destination": str, "date": str})
async def search_flights(args):
                                              # ① no check that date exists
    hits = flights_api.search(**args)
    return {"content": [{"type": "text", "text": str(hits)}]}
@tool("book_flight", "Book a specific flight.",
      {"flight_id": str, "passenger_name": str})
async def book_flight(args):
                                               # ② destructive, ungated
    confirmation = flights_api.book(**args)
    return {"content": [{"type": "text", "text": confirmation}]}
server = create_sdk_mcp_server("travel", tools=[search_flights, book_flight])
async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"travel": server},
        allowed_tools=["mcp__travel__search_flights",
                       "mcp__travel__book_flight"],
    )
    async for msg in query(prompt="Rebook this customer for March 32nd.",
                           options=options):
        if isinstance(msg, AssistantMessage):
            for b in msg.content:
                if hasattr(b, "text"):
                    print(b.text)
        elif isinstance(msg, ResultMessage):
            print("done:", msg.subtype)
                                              # ③ no state survives this run

Wyróżniają się trzy kwestie:

  1. search_flights przyjmuje wszystko, co model proponuje i co pasuje do schematu, więc data, która nie może istnieć, trafia do wnętrza narzędzia.
  2. book_flight ma destruktywny charakter, a nic go nie chroni. Dekorator łączy go z pętlą bez żadnych mechanizmów weryfikacji czy zatwierdzenia.
  3. Gdy przychodzi ResultMessage i query() zwraca wynik, sesja się kończy, a nic z tej operacji nie przetrwa do następnego wywołania.

SDK dostarczyło prostą pętlę. Nie ma żadnej kontroli nad błędnymi datami, podwójnymi opłatami ani tym, co wymaga zapamiętania.

Wersja Deep Agents

Deep Agents obsługuje ten proces inaczej. Zwykłe narzędzia LangChain trafiają do funkcji create_deep_agent(), która zwraca skompilowany graf do wywołania funkcji invoke. Różnice są niewielkie: nie ma żadnego walidatora, a ponieważ nie podano checkpointera, graf zapomina o wszystkim pomiędzy wywołaniami.

from langchain.tools import tool
from deepagents import create_deep_agent

@tool
def search_flights(origin: str, destination: str, date: str) -> str:
    """Search flights between two cities for a date (YYYY-MM-DD)."""
    return str(flights_api.search(origin, destination, date))
                                                # ① no date check
@tool
def book_flight(flight_id: str, passenger_name: str) -> str:
    """Book a specific flight."""
    return flights_api.book(flight_id, passenger_name)
                                                 # ② ungated side effect
agent = create_deep_agent(
                                                 # ③ the loop, no controls
    model="anthropic:claude-sonnet-4-6",
    tools=[search_flights, book_flight],
)
result = agent.invoke({"messages": [{"role": "user",
    "content": "Rebook this customer for March 32nd."}]})
print(result["messages"][-1].content)
# Ask a follow-up in a second invoke, and it starts from zero: no thread,
# no memory.

Znowu trzy punkty:

  1. Ciało funkcji search_flights wysyła żądanie do API z surowym łańcuchem date, a nic nie potwierdza, że data jest prawdziwa, zanim żądanie zostanie wysłane.
  2. book_flight dokonuje rezerwacji w momencie, gdy model ją wybierze, bez żadnego pośrednika pomiędzy propozycją a skutkiem ubocznym.
  3. create_deep_agent zwraca graf, który uruchamia pętlę i niewiele więcej. Bez checkpointera nie ma żadnego wątku ani pamięci pomiędzy wywołaniami.

Trzy sposoby, w jakie „nagi” agent się psuje

Dwa frameworki dostarczyły dwa proste pętle i zerową dyscyplinę – taki właśnie wygląda „nagi” agent w 2026 roku. To nie jest też uproszczenie. To forma dziesiątek demonstracji agentów publikowanych co tydzień na GitHubie. Przy rzeczywistym ruchu zawodzi na trzy sposoby, a każda z tych awarii odpowiada kontroli, którą dodaje odpowiednia infrastruktura.

Niemal niemożliwy wpis. Po podaniu „32 marca” model robi to, co zwykle robi, i wypełnia pole date ciągiem znaków, który jest poprawnie sformatowany, ale pozbawiony znaczenia. Schemat jedynie potwierdza, że date to ciąg znaków, i nie może stwierdzić, czy dana data w ogóle istnieje. W rezultacie book_flight jest wywoływany wobec fikcyjnego planu podróży, a model zgłasza sukces.

Wywołanie to dotyczy funkcji book_flight z wymyślonym identyfikatorem lotu i imieniem pasażera, a funkcja zwraca „Zarezerwowano”. Akcja została uruchomiona, a w pętli nikt nie sprawdził, czy rzeczywiście należało to zrobić.

book_flight(flight_id=”AC-PHANTOM”, passenger_name=”J. Moffatt”)
# -> “Booked.” The action fired. Nothing in the loop asked whether it should.

Błąd wynikał z braku wyraźnej granicy działań – nie istniał żaden walidator, który oddzielałby to, co zaproponował model, od tego, co faktycznie się wydarzyło. Zapełnienie tej luki to zadanie kontraktów narzędziowych oraz walidatorów.

Rozdęty kontekst. Prosty pętla odsyła całą historię na każdym kroku. Długa wymiana informacji dotyczących ponownego planowania, pełna wyników wyszukiwania, tekstu zasad oraz prób ponownych działań, może zawierać ponad pięćdziesiąt tysięcy tokenów, a nic jej nie skraca. Pojawia się to, co w literaturze nazywa się zepsuciem kontekstu, i agent pomija kroki, które wcześniej w tym samym procesie wykonał poprawnie. Nie powstaje żaden błąd. Agent po prostu stopniowo traci efektywność, w sposób niewidoczny.

Tutaj brakowało pamięci roboczej oraz mechanizmu budowania kontekstu, które należą do kategorii pamięci i trwałego stanu oraz do procesu tworzenia kontekstu.

Błędy zamaskowane. Narzędzie zwraca dane o błędzie, a model, częściej, niż ktokolwiek by oczekiwał, tworzy uprzejmy podsumowanie, które pomija sam błąd. Jedna z analiz incydentów produkcyjnych opisała te narzędzia jako pewnych kłamców: baza danych zwracająca zero wierszy jest traktowana jako fakt, a nie jako błąd. Wybór odpowiedniego narzędzia nigdy nie stanowił trudnego zadania. Trudniejsze jest odróżnienie narzędzia, które rzeczywiście działa, od tego, które przekonująco imituje działanie. W tym przypadku nie zaobserwowano żadnego cyklu zwrotnego, a jego wprowadzenie należy do zakresu możliwości obserwacji i wykrywania odchyleń.

Traktuj to jako wzorzec diagnostyczny, a nie listę kłopotliwych sytuacji. We wszystkich trzech przypadkach model robił to, co robią modele, a otaczający go system nie był w stanie tego zauważyć. Żaden z nich nie dotyczy modelu, który nie był wystarczająco inteligentny.

Działający cykl w inżynierii harnessingu

Krok składający się z czterech etapów powtarza się podczas tworzenia każdej kontroli:

  1. Wprowadź funkcję harness oraz awarię, którą ona kontroluje.
  2. Zaimplementuj ją w obu frameworkach jako skondensowany fragment kodu.
  3. Wywołaj awarię, aby udowodnić, że kontrola jest obciążona.
  4. Zbadaj, jak reaguje harness.

Powyższe sekcje przeprowadziły ten proces trzy razy bez jego oznaczania: zlokalizowanie nagiej szwy, wywołanie awarii, którą ona powoduje, oraz nazwanie kontroli, do której odnosi się. Następnie zamykanie szwów odbywa się po jednej.

Spróbuj sam

Można to odczuć bez dalszego czytania. W edytorze kilka szybkich eksperymentów to pokazuje:

  • Skieruj „nagiego” agenta w dowolnym frameworku na instrukcję ponownego rezerwowania miejsca dla klienta na 32 marca i obserwuj, jak niemożliwa data trafia do funkcji book_flight
  • Ogranicz pętlę do pięciu iteracji. To jest pierwsza naprawdę napisana instrukcja sterująca, i należy ona do warstwy wykonawczej, a nie do interfejsu użytkownika
  • Niech book_flight zwraca {"is_error": true, ...} i sprawdź, czy końcowy podsumowanie odzwierciedla błąd lub go pomija
  • Zanim zaczniesz, popraw jedną wersję claude-agent-sdk oraz jedną wersję deepagents lub langchain, ponieważ obie te API zmieniają się między wersjami, i traktuj nazwy modeli (claude-sonnet-4-6, anthropic:claude-sonnet-4-6) jako zamienniki dla aktualnych wersji

Każda z tych platform oferuje uporządkowany, dobrze zaprojektowany mechanizm pętli, ale żadna z nich nie dostarcza narzędzi do ich kontrolowania. To cecha tej sytuacji, a nie wada narzędzi – to właśnie tutaj leży prawdziwa praca. Claude Code, OpenCode, Codex oraz Deep Agents należą do narzędzi służących do zarządzania agentami, a programista, który ma z nimi trudności, różni się od tego, który uzyskuje niezawodne wyniki poprzez opracowanie mechanizmów kontrolnych, których brakuje w tych pętlach. Model od dłuższego czasu stanowi już prostą część tego procesu. Niezawodność wynika z dyscypliny budowanej wokół niego, poprzez kolejne porażki, a pierwszym naturalnym elementem jest granica działania – pierwsza zasada pozwalająca opuścić prompt i funkcjonować w czasie rzeczywistym.

Literatura pokrewna