Strona główna / Artykuły

Tworzenie pętli doskonalenia agenta przy użyciu LangGraph i ocen LangSmith.

Krok po kroku pętla LangGraph i LangSmith: zbieranie śladów, trzy warstwy oceny, grupowanie niepowodzeń, zestawy danych do regresji offline oraz testy przed wdrożeniem.

Wydawca: REACTAPP.TOP24 min czytania5607 słów

Źródła: Architecting the Agent Improvement Loop: Production-Grade Eval Pipelines

Najmniej wysiłku wymaga wprowadzenie agenta do użycia. To, co sprawia, że staje się on lepszy później, odróżnia projekt amatorski od systemu, który wytrzymuje próbę czasu. Wiele zespołów wypuszcza pierwszą wersję, a potem spędza miesiące na odpowiadaniu na skargi pojedynczo. Każda poprawka jest reakcją na konkretny przypadek, każde rozwiązanie pozostaje lokalne, a agent jako całość nigdy się nie poprawia – gromadzą się jedynie workarounds.

Zwykłą przyczyną jest traktowanie agenta jak zwykłego oprogramowania. W zwykłych programach kod wyjaśnia, co robi system, więc błąd oznacza konieczność przeglądania źródła kodu, aż się on pojawi. W przypadku agentów to założenie nie jest prawdziwe. Kod opisuje jedynie to, co system ma dozwolone robić, podczas gdy zapisy działania rejestrują to, co faktycznie zrobił. Ta luka stanowi podstawę każdego poważnego procesu oceny, w tym tych opisanych przez Anthropic w swoich materiałach inżynieryjnych oraz przez LangChain w dokumentacji LangSmith.

Poniżej przedstawiono kompletny cykl doskonalenia dla agenta LangGraph. Zaczyna się od małego agenta ReAct, który rejestruje swoje ślady w LangSmith, dodaje trzy warstwy oceny, identyfikuje powtarzające się wzorce niepowodzeń, przekształca je w trwały zbiór testów offline oraz przeprowadza ocenę regresji przed wdrożeniem jakiejkolwiek poprawki. Każdy etap zawiera kod, wyjaśnienie oraz oczekiwany wynik.

Cykl funkcjonuje w następujący sposób:

  1. Agent obsługuje rzeczywisty zapytanie użytkownika i tworzy ślad obejmujący każde wezwanie LLM, wezwanie narzędzia oraz wynik pośredni.
  2. Weryfikacje oparte na kodzie oceniają każdy ślad pod kątem formatu i poprawności narzędzia, a specjalista od LLM ocenia pomocność i trafność odpowiedzi, natomiast ludzie sprawdzają subtelne niepowodzenia.
  3. Ślady o niskich ocenach są grupowane według wzorców niepowodzeń, takich jak niewłaściwe narzędzie, błędy w rozumowaniu lub pominięcie intencji użytkownika.
  • Każda powtarzająca się awaria staje się przypadkiem testu offline z znanym oczekiwanym zachowaniem.
  • Zanim zostanie wydana nowa wersja agenta, jest ona testowana w ramach coraz większej bazy testów.
  • Do produkcji trafiają tylko te zmiany, które poprawiają standardy, a każda naprawiona awaria pozostaje na zawsze w bazie testów.
  • W ten sposób agent stale się udoskonala, zamiast jedynie gromadzić poprawki. Sześć etapów to:

    • Etap 1, podstawa: agent LangGraph ReAct z narzędziem LangSmith do śledzenia działań.
    • Etap 2, zbieranie danych: pobieranie wyników z produkcji w postaci ustrukturyzowanych zapisów.
    • Etap 3, ocena: sprawdzanie kodu, ocena przez model językowy oraz przegląd przez człowieka.
    • Etap 4, wykrywanie wzorców: przekształcanie ocenionych danych w kategorie awarii.
    • Etap 5, zestawy testów offline: przekształcanie awarii w zbiór danych bezpieczny pod kątem regresji.
  • Etap 6, zamknięcie pętli: ocena przed wysyłką i podniesienie bazowego poziomu.
  • Etap 1: agent podstawowy

    Pierwszy etap tworzy niewielkiego agenta obsługi klienta, który odpowiada na pytania dotyczące konta. Posiada dwa narzędzia – jedno służy do wyszukiwania informacji o koncie, a drugie do sprawdzania ostatnich zamówień – oraz wykorzystuje gotowego agenta ReAct od LangGraph, który stanowi standardowy wzorzec dla agentów używających narzędzi. Aktywowano śledzenie przez LangSmith, dzięki czemu każdy przebieg, wezwanie LLM oraz wezwanie narzędzia są automatycznie rejestrowane.

    Do prawidłowego funkcjonowania tego rozwiązania potrzebne są trzy elementy:

    1. LangGraph StateGraph stanowi podstawową strukturę grafową, łączącą węzeł decydujący o dalszych działaniach z węzłem wykonywającym narzędzie. Pomocnik create_react_agent otacza ten wzorzec.
  • Funkcja śledzenia LangSmith jest włączana, gdy zmienna środowiskowa LANGSMITH_TRACING=true jest ustawiona. Po tym każda wywołanie LangChain lub LangGraph generuje ślad bez żadnych zmian w kodzie.
  • Narzędzia to funkcje Pythona ozdobione anotacją @tool z LangChain, które umożliwiają agentowi dostęp do każdego z nich poprzez nazwę i opis, które pomagają w wyborze narzędzia.
  • Ustawienie środowiska

    Najpierw należy zainstalować biblioteki:

    # Install the libraries we need
    pip install langgraph langchain langchain-openai langsmith
    

    Zainstalowano cztery pakiety. langgraph dostarcza silnik wykonywania grafów oraz gotowy agent ReAct. langchain oferuje dekorator @tool oraz typy wiadomości. langchain-openai otacza model czatowy OpenAI interfejsem LangChain. langsmith to biblioteka klienta używana do łączenia się z backendem LangSmith w celu śledzenia i oceny.

    Następnie przychodzą zmienne środowiskowe, które włączają śledzenie bez modyfikacji kodu:

    export LANGSMITH_TRACING=true
    export LANGSMITH_API_KEY=your_langsmith_key
    export LANGSMITH_PROJECT=agent-improvement-loop
    export OPENAI_API_KEY=your_openai_key
    

    LANGSMITH_TRACING=true to parametr sterujący. LANGSMITH_PROJECT określa nazwę projektu, który grupuje ślady działania, dzięki czemu można je później filtrować. Pozostałe dwa parametry to dane uwierzytelniające do LangSmith oraz dostawcy modeli.

    Gdy te cztery zmienne są ustawione, każde wezwanie LangChain lub LangGraph w procesie wysyła ślad działania do LangSmith. Nie ma potrzeby niczego pakować ani modyfikować, ponieważ śledzenie odbywa się w tle.

    Budowanie agenta

    Najpierw importy:

    # Step 1: Import the pieces we need
    from langchain_openai import ChatOpenAI
    from langchain_core.tools import tool
    from langgraph.prebuilt import create_react_agent
    

    Importy zawierają trzy komponenty. ChatOpenAI otacza API do uzupełniania rozmów w OpenAI, dostępne dla LangChain. Dekorator tool przekształca zwykłą funkcję w Pythonie w funkcję wywoływalną, którą może używać agent. create_react_agent to gotowy pomocnik LangGraph, który tworzy graf w stylu ReAct do wywoływania narzędzi.

    Następnie dwa narzędzia:

    # Step 2: Define two tools the agent can call
    @tool
    def get_account_info(account_id: str) -> str:
        """Look up basic information for an account by account_id."""
        fake_accounts = {
            "A100": "Account A100: plan=pro, status=active, email=ada@example.com",
            "A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
        }
        return fake_accounts.get(account_id, f"No account found with id {account_id}")
    
    @tool
    def get_recent_orders(account_id: str) -> str:
        """Return the three most recent orders for a given account_id."""
        fake_orders = {
            "A100": "Orders for A100: #9001 shipped, #9002 processing, #9003 returned",
            "A200": "Orders for A200: no orders in the last 90 days",
        }
        return fake_orders.get(account_id, f"No orders for {account_id}")
    

    Każde narzędzie przyjmuje account_id i zwraca ciąg znaków. Dokumentacja każdej funkcji jest ważna, ponieważ model LLM czyta ten tekst podczas decydowania, które narzędzie wywołać. Dekorator pobiera nazwę funkcji, parametry oraz dokumentację, aby stworzyć schemat widziany przez model. W rzeczywistym systemie te elementy wysyłałyby zapytania do bazy danych lub API, ale w demonstracji zwracane są sztywne ciągi znaków, dzięki czemu rozwiązanie pozostaje samodzielne i powtarzalne.

    Na koniec składa się agent:

    # Step 3: Wire the tools into a ReAct agent
    model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    tools = [get_account_info, get_recent_orders]
    agent = create_react_agent(model, tools)
    

    Model czatu jest tworzony przy temperaturze zero dla zapewnienia powtarzalności. Narzędzia trafiają na listę i są przekazywane wraz z modelem do funkcji create_react_agent, która zwraca skompilowany LangGraph StateGraph, który już zawiera krok rozumowania, krok uruchamiania narzędzia oraz warunkowe łącza je łączące.

    Testowanie agenta

    Prosta zapytanie służy do jego przetestowania:

    # Step 4: Invoke the agent with a simple query
    result = agent.invoke({
        "messages": [{"role": "user", "content": "What plan is account A100 on?"}]
    })
    
    for message in result["messages"]:
        print(message.type, ":", message.content)
    

    Wywołanie przekazuje słownik zawierający klucz messages. Dzieje się tak dlatego, że create_react_agent opiera się na z góry przygotowanym schemacie MessagesState, którego stan to po prostu lista wiadomości z czatu i jest rozszerzany przez agenta w miarę jego działania oraz wywoływania narzędzi. Następnie pętla wyświetla każdy typ wiadomości wraz z jej treścią, dzięki czemu cała ścieżka jest widoczna.

    Oczekiwany wynik:

    human : What plan is account A100 on?
    ai :
    tool : Account A100: plan=pro, status=active, email=ada@example.com
    ai : Account A100 is on the pro plan.
    

    Agent otrzymał wiadomość od użytkownika, a następnie wygenerował wiadomość AI, która uruchomiła wezwanie narzędzia (jego treść jest pusta, ponieważ zawiera jedynie wezwanie narzędzia bez tekstu). Narzędzie zwróciło ciąg reprezentujący konto, a agent zakończył działanie odpowiedzią. Otwarcie LangSmith pokazuje teraz jeden nowy ślad w projekcie agent-improvement-loop, przy czym każdy krok jest zapisywany.

    W tym kodzie nic nie rejestrowało tych śladów. Gdy tylko w środowisku ustawione jest LANGSMITH_TRACING=true, każde wezwanie wysyła dane wejściowe, wyniki oraz kroki pośrednie do LangSmith. To właśnie ta możliwość obserwacji jest podstawą dla dalszych etapów pętli.

    Pierwszy etap pozostawia agenta wykonującego wezwania narzędzi pod zarządem LangGraph, przy czym każde uruchomienie generuje ślad. Jednak ślad widoczny na panelu kontrolnym nie jest jeszcze sygnałem potrzeby ulepszeń – trzeba go wydobyć i podjąć wobec niego działania, co jest zadaniem następnego etapu.

    Etap 2: zbieranie śladów

    Na tym etapie ślady są pobierane z LangSmith do kodu analitycznego. Ślad to jeden kompletny wykonywany proces agenta, zawierający dane wejściowe, wyjściowe, wywołania narzędzi oraz opóźnienia. Klient Python LangSmith wymienia wykonywania z projektu, filtruje je według okna czasowego i statusu oraz przekształca je w obiekty, które mogą być użyte na etapie oceny.

    Kilka terminów ułatwia czytanie kodu:

    1. Wykonywanie to pojedynczy zapisany fragment pracy, czy to całe wywołanie agenta, czy jedno wywołanie LLM w jego obrębie. Jedno wezwanie agenta tworzy drzewo wykonywań.
    2. Wykonywanie korzeniowe to wykonywanie najwyższego poziomu jednego wywołania agenta – to właśnie ono jest wymieniane, gdy chce się jeden zapis na zapytanie użytkownika.
    3. Projekt to przestrzeń nazw, do której należy wykonywanie, ustawiona za pomocą LANGSMITH_PROJECT. Filtrowanie według projektu oddziela wykonywania produkcyjne od tych używanych podczas lokalnego rozwoju.

    Pobieranie najnowszych wykonywań

    Najpierw tworzy się klienta:

    # Step 1: Create a LangSmith client
    from langsmith import Client
    from datetime import datetime, timedelta
    
    client = Client()
    

    Client jest importowany z pakietu langsmith i tworzony bez żadnych argumentów. Samodzielnie odczytuje wartość LANGSMITH_API_KEY z środowiska, więc żadne dane uwierzytelniające nie są przekazywane.

    Następnie wyświetlany jest spis ostatnich uruchomień roota:

    # Step 2: List recent root runs from our project
    recent_runs = list(client.list_runs(
        project_name="agent-improvement-loop",
        is_root=True,
        start_time=datetime.utcnow() - timedelta(days=1),
    ))
    
    print(f"Found {len(recent_runs)} root runs in the last 24 hours")
    

    client.list_runs jest wywoływany z trzema filtrami. project_name ogranicza zapytanie do konkretnego projektu, is_root=True zachowuje tylko wywołania agenta najwyższego poziomu, a nie każde zagnieżdżone wywołanie LLM lub narzędzia, natomiast start_time ogranicza wyniki do ostatnich 24 godzin. Iterator jest leniwy, więc list() zmusza go do utworzenia rzeczywistej listy.

    Oczekiwany wynik:

    Found 42 root runs in the last 24 hours
    

    Zachowanie tylko istotnych pól

    Prosta eksploatacja LangSmith zawiera wiele pól, ale do oceny potrzebne są tylko pięć elementów: dane wejściowe, dane wyjściowe, opóźnienie, ewentualne błędy oraz wyniki dodatkowych wykonań pokazujące, jakie narzędzia zostały użyte.

    # Step 3: Build a compact trace record from each run
    def compact_trace(run):
        return {
            "run_id": str(run.id),
            "inputs": run.inputs,
            "outputs": run.outputs,
            "error": run.error,
            "latency_ms": (run.end_time - run.start_time).total_seconds() * 1000
                if run.end_time else None,
            "tool_calls": [
                child.name for child in client.list_runs(
                    trace_id=run.trace_id, run_type="tool"
                )
            ],
        }
    
    traces = [compact_trace(run) for run in recent_runs]
    print(f"Collected {len(traces)} compact traces")
    print("Example tool calls:", traces[0]["tool_calls"])
    

    Ten pomocnik przyjmuje surowy wynik wykonywania i sprowadza go do małego słownika. Dane wejściowe i wyjściowe (run.inputs, run.outputs) zawierają zapytanie użytkownika oraz ostateczną odpowiedź, natomiast run.error ma wartość None w przypadku sukcesu, a w razie błędu jest to ciąg znaków. Opóźnienie wynika z odejmowania start_time od end_time. Wywołania narzędzi wymagają drugiego zapytania do LangSmith z użyciem run_type="tool" oraz tego samego trace_id, co daje informacje tylko o wykonaniach narzędzi w tym drzewie wykonywań.

    Oczekiwany wynik:

    Collected 42 compact traces
    Example tool calls: ['get_account_info']
    

    Celem nie jest kopiowanie śladów z LangSmith i porzucenie go. Wersja skompresowana jest po prostu łatwa do oceny, natomiast pełny ślad pozostaje w LangSmith do szczegółowego debugowania. Zachowanie bogatej wersji na panelu sterowania oraz uproszczonej wersji do analizy to rozsądny sposób na oddzielenie możliwości obserwacji od oceny.

    Sprawdzanie zbieracza

    # Step 4: Inspect the first trace end to end
    import json
    print(json.dumps(traces[0], indent=2, default=str))
    

    Pierwszy zapis jest wyświetlany jako wcięty JSON za pomocą json.dumps. Argument default=str sprawia, że biblioteka json przekształca w ciąg znaków wszystko, co nie może zostać bezpośrednio zserializowane, co obejmuje typy datetime i UUID zwracane przez LangSmith.

    Oczekiwany wynik:

    {
      "run_id": "b1d2e3f4-...",
      "inputs": {"messages": [{"role": "user", "content": "What plan is account A100 on?"}]},
      "outputs": {"messages": [{"role": "ai", "content": "Account A100 is on the pro plan."}]},
      "error": null,
      "latency_ms": 1423.51,
      "tool_calls": ["get_account_info"]
    }
    

    Każda seria produkcji ma teraz uporządkowany, czysty zapis, który zostanie wzbogacony na etapie oceniania. Zbieranie śladów nie kosztuje wiele, a trudność polega na tym, by je uczynić przydatnymi. Wiele zespołów kończy pracę na panelu kontrolnym i zastanawia się, dlaczego możliwość obserwacji nigdy nie przekłada się na poprawę. Ślady bez ocen to szum, a ślad bez oceny to po prostu linijka z logiem.

    Etap 3: ocenianie śladów

    Trzy warstwy oceniania dostarczają jeden wzbogacony ślad. Post z dziedziny inżynierii firmy Anthropic na temat ocen opisuje dokładnie taki sam układ: oceniające oparte na kodzie, które są szybkie, obiektywne i powtarzalne; oceniające oparte na modelach, które są elastyczne i skalowalne; oraz oceniający ludzie, którzy stanowią złoty standard, ale są drodzy. Tutaj buduje się po jednym przykładzie każdego z nich.

    1. Sprawdzania oparte na kodzie to szybkie, deterministyczne funkcje, które weryfikują fakty strukturalne, takie jak to, czy agent użył odpowiedniego narzędzia, czy też czy wynik ma oczekiwany format. Pozwalają one bez żadnych kosztów wykryć oczywiste błędy.
    2. Sędzia w postaci LLM to drugi model, który ocenia subiektywne cechy, takie jak pomocność, trafność i ton. Może być wykorzystywany na każdym zapisie, ale musi zostać skalibrowany względem ludzkiego osądu, zanim ktokolwiek mu zaufa.
    3. Ludzka weryfikacja polega na tym, że ekspert z danej dziedziny analizuje zapisy, które zostały przeoczone przez automatyzację lub ocenione niespójnie. To właśnie one stanowią prawdę podstawową, według której skalibrowane są pozostałe dwa poziomy.

    Poziom 1: poprawność narzędzia

    # Step 1: Define a rule that says which tool SHOULD be called for a given query
    import re
    
    def expected_tool_for_query(query: str) -> str:
        q = query.lower()
        if re.search(r"\b(order|shipment|shipped|return)\b", q):
            return "get_recent_orders"
        if re.search(r"\b(plan|account|status|email)\b", q):
            return "get_account_info"
        return "any"
    
    def score_tool_correctness(trace):
        query = trace["inputs"]["messages"][0]["content"]
        expected = expected_tool_for_query(query)
        actual = trace["tool_calls"][0] if trace["tool_calls"] else None
        if expected == "any":
            return 1.0
        return 1.0 if actual == expected else 0.0
    

    Zdefiniowano dwie funkcje. expected_tool_for_query stosuje wyrażenie regularne do tekstu zapytania, aby odgadnąć, który narzędzie powinno się nim zająć. Jest to prosta heurystyka, przydatna do wykrywania oczywistych błędów routingu, ale niewystarczająca w środowisku produkcyjnym. score_tool_correctness bierze zapytanie użytkownika z zapisu działania, oblicza oczekiwane narzędzie i porównuje je z pierwszym narzędziem, które faktycznie zostało wywołane przez agenta. Brak zgodności daje wynik 0.0, a zgodność – 1.0.

    Wersja produkcyjna zastąpiłaby wyrażenie regularne silniejszym klasyfikatorem lub silnikiem reguł opartym na intencjach. Zasada pozostaje ta sama: sprawdzenie deterministyczne odbywa się przed kosztownymi narzędziami oceny opartymi na LLM.

    Warstwa 2: sędzia oparty na LLM oceniający użyteczność

    # Step 2: Score qualitative properties with an LLM judge
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    
    judge_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    
    judge_prompt = ChatPromptTemplate.from_messages([
        ("system", "You are an expert reviewer of AI agent responses. "
                   "Rate the response on a scale of 1 to 5 for helpfulness. "
                   "Respond with only a single integer, nothing else."),
        ("user", "User question: {question}\n\nAgent response: {response}\n\nScore:"),
    ])
    
    def score_helpfulness(trace):
        question = trace["inputs"]["messages"][0]["content"]
        response = trace["outputs"]["messages"][-1]["content"]
        chain = judge_prompt | judge_model
        result = chain.invoke({"question": question, "response": response})
        try:
            return int(result.content.strip()) / 5.0
        except ValueError:
            return 0.0
    

    Niewielki łańcuch LangChain jest tworzony poprzez przekierowanie szablonu promptu do modelu służącego jako sędzia. Komunikat systemowy ogranicza ten model do zwracania jednej liczby całkowitej w zakresie od 1 do 5, dzięki czemu wynik jest rozpatrywany w sposób deterministyczny. Wewnątrz score_helpfulness pytanie użytkownika oraz ostateczna odpowiedź agenta są pobierane z zapisu działania systemu, łańcuch jest uruchamiany, a liczba całkowita skalowana na wartość zmiennoprzecinkową w zakresie od 0 do 1, aby mogła zostać łatwo połączona z oceną narzędzia. Jako sędzia wykorzystywany jest gpt-4o-mini, ponieważ jest tani i szybki.

    Sędziowie oparte na modelach LLM są stochastyczne, więc dwa uruchomienia na tym samym zapisie działania mogą zwrócić nieco różne oceny. Zalecenia firmy Anthropic sugerują kalibrację takiego sędziego na podstawie zestawu oznaczonych przez ludzi danych przed jego użyciem, a trzeci warstwa właśnie takiego zestawu tworzy.

    Warstwa 3: przegląd przez ludzi jako kolejka adnotacji

    LangSmith posiada wbudowaną kolejkę adnotacji, w której użytkownicy oznaczają ślady na panelu sterowania. Aby przykład był samodzielny, ta sama struktura jest zmodelowana w kodzie.

    # Step 3: Model a human review signal
    def score_human(trace, human_labels: dict) -> float | None:
        run_id = trace["run_id"]
        return human_labels.get(run_id)
    
    human_labels = {
        "b1d2e3f4-...": 1.0,
        "c2e3f4g5-...": 0.0,
    }
    

    Funkcja wyszukuje identyfikator wykonywania w słowniku etykiet dostarczonych przez ludzi. Jeśli taka etykieta istnieje, jest zwracana, a jeśli nie, funkcja zwraca None, co oznacza, że jeszcze nie zostało to przeanalizowane. W środowisku produkcyjnym ten słownik znajdowałby się za API kolejki anotacji LangSmith. Ważne jest to, jak zachowują się oceny: są rzadkie, ponieważ tylko niektóre ślady je otrzymują, są autorytatywne, ponieważ przy rozbieżnościach przeważają nad innymi, i wpływają na kalibrację sędziego LLM.

    Łączenie trzech warstw

    # Step 4: Run all three scorers and attach scores to each trace
    def enrich(trace):
        trace["scores"] = {
            "tool_correctness": score_tool_correctness(trace),
            "helpfulness": score_helpfulness(trace),
            "human": score_human(trace, human_labels),
        }
        return trace
    
    enriched = [enrich(t) for t in traces]
    print(json.dumps(enriched[0]["scores"], indent=2))
    

    Funkcja enrich dodaje wszystkie trzy wyniki do śladu i zwraca go. Jest stosowana do każdego zebranego podczas zbierania śladu, co daje listę wzbogaconych śladów, z których każdy zawiera ustrukturyzowany słownik scores. Reguły oparte na regex generują wartość tool_correctness, oceniający LLM wytwarza wartość helpfulness, a human to liczba zmienna lub None.

    Oczekiwany wynik:

    {
      "tool_correctness": 1.0,
      "helpfulness": 0.8,
      "human": null
    }
    

    Trzy oceny różnią się ze względu na cel. Weryfikacja poprawności narzędzia wskazuje, że użyto właściwego narzędzia, natomiast sędzia oparty na LLM ocenia, że odpowiedź zasługuje na 4 z 5 punktów. Człowiek sprawdzający, który dołączy później, może stwierdzić, że narzędzie było właściwe, ale sformułowanie nie pasowało do tonu marki. Każda warstwa wykrywa to, co umyknie pozostałym, więc ich łączenie zapewnia lepszą obsługę i dokładniejsze wyniki po różnych cenach.

    Teraz każda ścieżka zawiera trzy oceny: jedną opartą na regułach, jedną ocenioną przez LLM oraz, jeśli dostępna, jedną przez człowieka. Oceny bez żadnej struktury są niemal tak mało pomocne jak brak ich w ogóle, dlatego następnie przychodzą wzorce.

    Etap 4: znajdowanie wzorców awarii

    Gdy ślady zawierają wyniki, to samo zawiłość ma tendencję do powtarzania się. Agent wybiera niewłaściwy narzędzie dla określonego sformułowania. Jego rozumowanie błądzi przy zapytaniach wieloetapowych. Odpowiedzi są technicznie poprawne, ale nie odpowiadają temu, czego chciał użytkownik. Każdy wzorzec wymaga innego rodzaju naprawy – czy to przepisania opisu narzędzia, aktualizacji instrukcji lub przebudowy procesu pracy. Nazwanie wzorca jest trudne, natomiast jego naprawa zazwyczaj jest prosta.

    LangChain oferuje Insights Agent, stworzony właśnie do rozwiązywania takich problemów na dużą skalę. Przetwarza ślady, grupuje je według przyczyn awarii i pomaga zespołom określić, co wymaga najpierw uwagi. Uproszczona wersja tego samego rozwiązania została zaimplementowana poniżej w kodzie.

    Filtrowanie awarii

    # Step 1: Keep only traces where at least one score is low
    def is_low_scoring(trace):
        scores = trace["scores"]
        if scores["human"] is not None and scores["human"] < 0.5:
            return True
        if scores["tool_correctness"] < 0.5:
            return True
        if scores["helpfulness"] < 0.6:
            return True
        return False
    
    failures = [t for t in enriched if is_low_scoring(t)]
    print(f"{len(failures)} of {len(enriched)} traces flagged as low scoring")
    

    is_low_scoring przeprowadza trzy sprawdzenia. Ocena człowieka poniżej 0,5 oznacza ślad jako nieudany. W przeciwnym razie decydują oceny automatyczne: poprawność narzędzia poniżej 0,5 oznacza błędne skierowanie agenta, a pomocność poniżej 0,6 oznacza, że sędzia LLM nie polubił odpowiedzi. Progi te można dostosować, a są to rozsądne domyślne wartości dla pierwszej próby. Filtr przetwarza wzbogacone ślady z poprzedniego etapu, aby utworzyć zbiór nieudanych przypadków.

    Oczekiwany wynik:

    9 of 42 traces flagged as low scoring
    

    Grupowanie błędów w kategorie

    Niewielka heurystyka nadaje tutaj błędom tagi. W środowisku produkcyjnym zadanie to mogłyby wykonać embeddingi z algorytmami klastrowania lub Insights Agent od LangChain, który robi dokładnie to na podstawie śladów działania LangSmith.

    # Step 2: Tag each failure with a category based on its scores
    def categorize(trace):
        scores = trace["scores"]
        if scores["tool_correctness"] < 0.5:
            return "wrong_tool"
        if scores["helpfulness"] < 0.6 and scores["tool_correctness"] >= 0.5:
            return "unhelpful_answer"
        if scores["human"] is not None and scores["human"] < 0.5:
            return "human_flagged"
        return "other"
    
    from collections import Counter
    categories = Counter(categorize(t) for t in failures)
    print(categories.most_common())
    

    Funkcja categorize sprawdza elementy w kolejności priorytetów. Niewłaściwy narzędzie stanowi najpoważniejszy problem strukturalny, dlatego jest oznaczane jako pierwsze. Nieprzydatna odpowiedź, mimo użycia właściwego narzędzia, wskazuje na problemy z sformułowaniem zadania lub generacją odpowiedzi. Błąd zauważony przez człowieka, który przeszedł niezauważony dla systemu automatycznego, trafia do osobnej kategorii. Standardowy obiekt Counter w Pythonie liczy elementy każdej kategorii i jest importowany bezpośrednio w tym miejscu, ponieważ to jego pierwsze użycie.

    Oczekiwany wynik:

    [('wrong_tool', 5), ('unhelpful_answer', 3), ('human_flagged', 1)]
    

    Kategoria wskazuje na rozwiązanie problemu. Seria pięciu błędów związanych z niewłaściwym narzędziem wskazuje na opis narzędzia lub instrukcję kierowania. Trzy bezużyteczne odpowiedzi pomimo użycia właściwego narzędzia wskazują na prompt generujący te odpowiedzi. Błąd zauważony tylko przez człowieka oznacza, że automatyczne systemy oceny coś przeoczyły, więc prawdopodobnie potrzebny jest nowy oceniający.

    Analiza pojedynczego wzorca

    # Step 3: Print the inputs and outputs for every wrong_tool failure
    wrong_tool = [t for t in failures if categorize(t) == "wrong_tool"]
    for trace in wrong_tool[:3]:
        print("QUERY:", trace["inputs"]["messages"][0]["content"])
        print("TOOLS CALLED:", trace["tool_calls"])
        print("RESPONSE:", trace["outputs"]["messages"][-1]["content"])
        print("---")
    

    Zbiór niepowodzeń został zawężony do przypadków wrong_tool, a pętla wyświetla zapytanie użytkownika, narzędzia faktycznie wywołane oraz ostateczną odpowiedź dla pierwszych trzech. To właśnie tutaj osoba lub bardziej zaawansowany model językowy analizuje te błędy i formułuje hipotezę, na przykład że każde zapytanie zawierające „subscription” uruchamia funkcję get_recent_orders zamiast get_account_info. Ta hipoteza staje się celem naprawy.

    Oczekiwany wynik:

    QUERY: Is my subscription active?
    TOOLS CALLED: ['get_recent_orders']
    RESPONSE: I could not find any recent orders to confirm your subscription status.
    ---
    QUERY: Tell me about my subscription to your product
    TOOLS CALLED: ['get_recent_orders']
    RESPONSE: There are no recent orders for this account.
    ---
    

    Trzy przykłady na ekranie jasno pokazują ten wzorzec. „Subskrypcja” jest semantycznie bliższa „stanie konta” niż „zamówieniom”, a mimo to agent kieruje zapytania na podstawie powierzchownego pokrycia się słów kluczowych. Prawdopodobnym rozwiązaniem jest jednozdaniowa modyfikacja dokumentacji get_account_info poprzez dodanie informacji o subskrypcji lub krótkie powiadomienie systemowe wyjaśniające tę różnicę.

    Faza kończy się wzorcami niepowodzeń, które zawierają dane o ich liczbie, kategoriach oraz przykładach. Wzorzec naprawiony dziś ma niewielką wartość, jeśli zmiana innego inżyniera ponownie go uszkodzi w przyszłym tygodniu, dlatego naprawa musi zostać potwierdzona testem.

    Faza 5: zestawy testów offline

    Tutaj błędy przekształcają się w testy statyczne. Każdy rzeczywisty błąd w produkcji jest dodawany do zbioru danych LangSmith jako przykład zawierający dane wejściowe (zapytanie użytkownika), oczekiwane zachowanie (odpowiedni narzędzie lub akceptowalna odpowiedź) oraz opcjonalne metadane, takie jak kategoria błędu. Po zapisaniu nigdy nie znika, więc ustalony błąd nie może się potajemnie pojawić ponownie.

    Ocena offline w LangSmith składa się z trzech części:

    1. Zbiór danych to nazwana kolekcja przykładów, z których każdy zawiera dane wejściowe oraz, opcjonalnie, oczekiwane wyniki.
    2. Funkcja docelowa to kod poddawany testowi; w przypadku agenta jest to zazwyczaj jego metoda .invoke.
    3. Ewaluatorzy to funkcje, które oceniają wynik funkcji docelowej w odniesieniu do każdego przykładu, podobnie jak funkcje oceniające z etapu scoringu.

    Tworzenie zbioru danych

    # Step 1: Create a LangSmith dataset for our agent
    dataset_name = "agent-production-failures"
    
    dataset = client.create_dataset(
        dataset_name=dataset_name,
        description="Production traces where the agent failed. Each example captures the user query and the expected tool call.",
    )
    print(f"Created dataset: {dataset.id}")
    

    Ten sam client z etapu zbierania danych wywołuje funkcję create_dataset z nazwą i opisem, a następnie zwraca obiekt zestawu danych, którego .id można później odwołać. W rzeczywistym wdrożeniu zastosowano by kontrolę typu „stworz jeśli nie istnieje”, ale demo zakłada pierwsze uruchomienie.

    Oczekiwany wynik:

    Created dataset: 3f2a1b0c-...
    

    Zmiana niepowodzeń w przykłady

    # Step 2: Convert each failure trace into a dataset example
    examples = []
    for trace in failures:
        query = trace["inputs"]["messages"][0]["content"]
        expected_tool = expected_tool_for_query(query)
        examples.append({
            "inputs": {"question": query},
            "outputs": {"expected_tool": expected_tool},
            "metadata": {"category": categorize(trace), "source_run_id": trace["run_id"]},
        })
    
    client.create_examples(dataset_id=dataset.id, examples=examples)
    print(f"Added {len(examples)} examples to {dataset_name}")
    

    Kod przechodzi przez każdą ścieżkę błędu i tworzy przykładowy słownik składający się z trzech pól. inputs zawiera pytanie użytkownika, outputs określa zachowanie do sprawdzenia – w tym przypadku narzędzie, które powinno zostać wywołane – a metadata zawiera kategorię oraz oryginalny identyfikator wykonywania, dzięki czemu każdy przykład można prześledzić do jego źródła. Jedno wezwanie client.create_examples przesyła wszystko naraz.

    Oczekiwany wynik:

    Added 9 examples to agent-production-failures
    

    Rzeczywiste wdrożenia zazwyczaj pozwalają osoby dokonującej przeglądu decydować o oczekiwanym wyniku, ponieważ heurystyka regex jest jedynie punktem wyjścia. Osoba zatwierdzająca każdy przykład dostarcza bardziej pewnej wartości referencyjnej, do czego służy kolejka adnotacji w LangSmith.

    Ewaluator regresji

    # Step 3: Write an evaluator that checks the agent's tool call against the expected tool
    def tool_match_evaluator(inputs: dict, outputs: dict, reference_outputs: dict):
        actual_messages = outputs.get("messages", [])
        tool_called = None
        for m in actual_messages:
            if getattr(m, "tool_calls", None):
                tool_called = m.tool_calls[0]["name"]
                break
        expected = reference_outputs["expected_tool"]
        score = 1.0 if (expected == "any" or tool_called == expected) else 0.0
        return {"key": "tool_match", "score": score}
    

    tool_match_evaluator spełnia wymagania formatu oczekiwanego przez LangSmith: otrzymuje inputs przykładu, rzeczywiste outputs agenta oraz reference_outputs przykładu. Przechodzi przez wiadomości wyjściowe agenta, znajduje pierwszą zawierającą wezwanie do narzędzia i wyodrębnia jego nazwę. Następnie porównuje ją z oczekiwanym narzędziem i zwraca słownik zawierający key oraz score. Key staje się nazwą metryki wyświetlanej w LangSmith. Struktura jest zgodna z oficjalnym przewodnikiem szybkiego rozpoczęcia pracy z oceną w LangSmith.

    Funkcja docelowa

    # Step 4: Wrap our agent in a function that takes a LangSmith example and returns its output
    def run_agent(inputs: dict) -> dict:
        result = agent.invoke({
            "messages": [{"role": "user", "content": inputs["question"]}]
        })
        return result
    

    run_agent jest celowo utrzymywany w minimalnej formie. Na podstawie inputs przykładu z zestawu danych wywołuje on agenta i zwraca dowolny surowy słownik, który wraca – co odpowiada temu, czego oczekuje funkcja evaluate w LangSmith. Agent, który jest wywoływany, został stworzony na etapie budowy podstawy.

    Celowe jest utrzymywanie tego otoczenia w prostocie. Wszystko, co znajduje się w jego wnętrzu, jest uruchamiane przy każdej ocenie i zwiększa koszty, natomiast proste przekierowanie oznacza, że evaluator testuje agenta, a nie to otoczenie.

    Zatem zestaw danych istnieje, evaluator jest podłączony, a każda awaria w produkcji staje się trwałym przypadkiem testowym. Ostatni etap uruchamia ten evaluator przed wprowadzeniem jakichkolwiek zmian.

    Etap 6: zamknięcie pętli

    To jest brama regresji. Agent otrzymuje aktualizację, na przykład nowy opis narzędzia skierowany na błąd routingu „subskrypcji” wykryty wcześniej. Przed wdrożeniem cały zbiór danych o poprzednich awariach jest przetwarzany za pomocą nowej wersji. LangSmith przechowuje oba te przetwarzania jako eksperymenty i pokazuje, przykład po przykładzie, gdzie nowa wersja poprawiła sytuację, a gdzie pogorszyła ją. Wdrożenie następuje tylko wtedy, gdy nowa baza porównawcza przewyższa stary model i nic z wcześniej naprawionego nie uległo regresji.

    Przeprowadzanie oceny bazy porównawczej

    # Step 1: Run the evaluator across the dataset with the current agent
    from langsmith.evaluation import evaluate
    
    experiment_results = evaluate(
        run_agent,
        data=dataset_name,
        evaluators=[tool_match_evaluator],
        experiment_prefix="agent-v1-baseline",
        max_concurrency=4,
    )
    

    evaluate jest importowane z langsmith.evaluation. Otrzymuje cel run_agent, nazwę zestawu danych (którą LangSmith odnosi do zestawu z poprzedniego etapu), listę oceniających, prefiks eksperymentu służący do oznaczenia sesji oraz wartość konkurencji równą czterem, aby ocena mogła przebiegać równolegle. Zwraca obiekt z wynikami eksperymentu wskazujący na sesję w LangSmith.

    Oczekiwany wynik:

    View the evaluation results for experiment: 'agent-v1-baseline-...' at:
    https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
    
    9/9 runs | avg tool_match: 0.33
    

    Z dziewięciu nieudanych przykładów obecny agent nadal kieruje większość z nich błędnie, i to jest punkt odniesienia.

    Rozsyłka poprawki i ponowna jej ocena

    Poprawką zalecaną na etapie analizy wzorców jest przepisanie dokumentacji get_account_info językiem opisującym subskrypcje.

    # Step 2: Update the tool docstring to teach the agent about subscriptions
    @tool
    def get_account_info_v2(account_id: str) -> str:
        """Look up basic information for an account, including plan, status,
        subscription tier, and contact email. Use this tool for any question
        about the account itself, including subscription status."""
        fake_accounts = {
            "A100": "Account A100: plan=pro, status=active, email=ada@example.com",
            "A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
        }
        return fake_accounts.get(account_id, f"No account found with id {account_id}")
    
    tools_v2 = [get_account_info_v2, get_recent_orders]
    agent_v2 = create_react_agent(model, tools_v2)
    
    def run_agent_v2(inputs: dict) -> dict:
        result = agent_v2.invoke({
            "messages": [{"role": "user", "content": inputs["question"]}]
        })
        return result
    

    Narzędzie do informacji o koncie zostało przedefiniowane dzięki bogatszej dokumentacji, która wyraźnie wspomina o stanie subskrypcji. Ponieważ to właśnie dokumentację czyta model językowy przy wyborze narzędzia, jej rozszerzenie bezpośrednio eliminuje problemy z kierowaniem zapytań. Agent został odbudowany przy użyciu zaktualizowanego narzędzia i umieszczony w nowym celu run_agent_v2. Wszystko inne pozostało bez zmian.

    # Step 3: Run the evaluator on the v2 agent
    experiment_results_v2 = evaluate(
        run_agent_v2,
        data=dataset_name,
        evaluators=[tool_match_evaluator],
        experiment_prefix="agent-v2-docstring-fix",
        max_concurrency=4,
    )
    

    Tożsama ocena jest wykonywana ponownie na tym samym zbiorze danych przy użyciu tego samego oceniającego, a zmienia się tylko cel oceny. Nowy experiment_prefix umożliwia odróżnienie tej sesji w interfejsie LangSmith, aby można ją było porównać z bazowym wynikiem. LangSmith przekazuje każdy przykład przez agenta wersji v2, przechowuje pełny zapis działania i ocenia każdy wynik za pomocą oceniającego.

    Oczekiwany wynik:

    View the evaluation results for experiment: 'agent-v2-docstring-fix-...' at:
    https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
    
    9/9 runs | avg tool_match: 0.89
    

    Wskaźnik wzrósł z 0,33 do 0,89. Osiem z dziewięciu błędów zostało naprawionych, a dziewiąty to wysoce niejednoznaczne zapytanie, które może należeć do kolejki anotacji przez ludzi.

    Before (v1):       After (v2):
    wrong_tool: 5      wrong_tool: 1
    unhelpful: 3       unhelpful: 0
    human: 1           human: 0
    avg score: 0.33    avg score: 0.89
    

    Kilka czynników sprawiło, że to porównanie jest wiarygodne:

    1. Ocena wyjściowa dała konkretną ocenę przed jakąkolwiek zmianą w kodzie.
    2. Poprawka polegała na jednej precyzyjnej modyfikacji, pojedynczym dokstringu.
    3. Ponowna ocena sprawdziła dokładny wpływ tej modyfikacji na dobrany zbiór rzeczywistych błędów.
    4. Zbiór danych testowych pozostał niezmieniony, więc porównanie było dokładne.
    5. Każdy oryginalny ślad błędu jest teraz traktowany jako test, dzięki czemu późniejsze zmiany nie mogą go potajemnie uszkodzić.

    To właśnie sprawia, że mechanizm ten jest składany. Każda poprawka dodaje przykłady do zbioru danych, a każda przyszła wersja jest sprawdzana pod kątem wszystkich wcześniejszych błędów. Proces wdrażania staje się z czasem szybszy, a nie wolniejszy, ponieważ pewność siebie rośnie wraz z rozwojem zbioru danych.

    Cały cykl teraz jest wykonywany od początku do końca. Zbierane są ślady i wzbogacane o oceny, pojawiają się wzorce, błędy stają się testami offline, ocena następuje przed wdrożeniem, a zmiana jest przyjmowana tylko wtedy, gdy poziom bazowy rośnie. Każdy cykl zaczyna się od wyższego poziomu.

    Jedno pełne codzienne wykonywanie

    Jeden driver może uruchomić cały cykl raz dziennie, korzystając z świeżych danych produkcyjnych.

    # Full loop driver
    def run_improvement_loop():
        # 1. Pull traces
        runs = list(client.list_runs(
            project_name="agent-improvement-loop",
            is_root=True,
            start_time=datetime.utcnow() - timedelta(days=1),
        ))
        traces = [compact_trace(r) for r in runs]
    
        # 2. Enrich with scores
        enriched = [enrich(t) for t in traces]
    
        # 3. Find failures and categorize them
        failures = [t for t in enriched if is_low_scoring(t)]
        categories = Counter(categorize(t) for t in failures)
    
        # 4. Add failures to the dataset
        new_examples = [{
            "inputs": {"question": t["inputs"]["messages"][0]["content"]},
            "outputs": {"expected_tool": expected_tool_for_query(
                t["inputs"]["messages"][0]["content"])},
            "metadata": {"category": categorize(t), "source_run_id": t["run_id"]},
        } for t in failures]
    
        if new_examples:
            client.create_examples(dataset_id=dataset.id, examples=new_examples)
    
        # 5. Re-evaluate the current agent against the full dataset
        result = evaluate(
            run_agent_v2,
            data=dataset_name,
            evaluators=[tool_match_evaluator],
            experiment_prefix="daily-regression",
        )
    
        return {
            "traces_collected": len(traces),
            "failures_found": len(failures),
            "by_category": dict(categories),
            "examples_added": len(new_examples),
        }
    
    print(run_improvement_loop())
    

    Ta funkcja łączy ze sobą sześć etapów. Pobiera wczorajsze ślady, ocenia je, klasyfikuje błędy, dodaje nowe błędy jako przykłady zbiórki danych i przeprowadza ocenę regresji. Zwraca mały słownik z danymi dashboardu, który można codziennie zapisywać lub wysyłać do Slacka.

    Oczekiwany wynik:

    {
      "traces_collected": 186,
      "failures_found": 12,
      "by_category": {"wrong_tool": 4, "unhelpful_answer": 6, "human_flagged": 2},
      "examples_added": 12,
      "regression_score": 0.87
    }
    

    To, co pokazuje wynik: w tym dniu odnotowano 186 śladów produkcji bez żadnych zmian w kodzie agenta, a dwanaście z nich uzyskało co najmniej jedną niską ocenę. Obecnie jest więcej nieprzydatnych odpowiedzi niż błędnych wywołań narzędzi, co wskazuje, że naprawa routingu przyniosła efekty, a kolejna poprawka powinna dotyczyć generowania odpowiedzi. Ocena regresji pozostała na poziomie 0,87, więc żadna z zmian w tym tygodniu nie spowodowała powrotu wcześniejszych problemów. Dwanaście nowych przykładów dołączyło do stałego zbioru danych, gdzie będą służyć do zapobiegania powtórzeniom.

    Pięć ulepszeń, które warto wprowadzić w najbliższym czasie

    Każda z nich odnosi się do funkcji LangChain, LangGraph lub LangSmith, które można włączyć bez konieczności przepisywania całej architektury.

    1. Zastąp klasyfikator oparty na regex embeddingami. OpenAIEmbeddings może przekształcić każde zapytanie w embedding, dzięki czemu ślady niepowodzeń grupują się według podobieństwa semantycznego, a nie zgodnie z regułami słów kluczowych, co ujawnia subtelniejsze problemy, takie jak niezgodność tonu.
    2. Zamień wbudowanego sędziego LLM na bibliotekę OpenEvals. Szybki start LangSmith wykorzystuje openevals wraz z gotowymi promptami, takimi jak CORRECTNESS_PROMPT, które radzą sobie lepiej niż samodzielnie stworzony sędzia i dostarczają warianty bez odniesień do standardów do monitorowania online.
    3. Dodaj oceny wieloetapowe. Narzędzia do ocen wieloetapowych w LangSmith przeprowadzają ocenę przez sędziego LLM na całej długości rozmowy, a nie tylko pojedynczych odpowiedzi, co pozwala wykryć zmiany w sposobie rozumowania, których nie dostrzegają narzędzia oceniające pojedyncze odpowiedzi w agentach opartych na czacie.
  • Zastosuj Insights Agent od LangChain do skaliowanego wykrywania wzorców. Kategoryzator regex radzi sobie z dziesiątkami awarii, ale przy tysiącach automatycznie kieruje Insights Agent na klustery projektów, grupując ślady i ujawniając sposoby awarii, korzystając z tej samej struktury zestawu danych.
  • Wprowadzaj zmiany poprzez wdrożenie LangGraph z stopniowym rozpowszechnianiem. Utrzymywanie wersji v2 w projekcie canary z ustawieniem LANGSMITH_PROJECT=agent-canary przez jeden dzień, a następnie porównywanie jej wyników w trybie rzeczywistym z projektem kontrolnym, oznacza, że zostanie ona zastąpiona w produkcji tylko wtedy, gdy będzie wyraźnie lepsza.
  • Po każdym razie pętla powtarza się, korzystając z wyższej bazy porównawczej, co przynosi lepsze ślady, lepsze oceny oraz lepszego agenta.

    Literatura pokrewna

  • Oceny pętli zamkniętej dla agenta przetwarzania obrazów w produkcji: co przedstawiło Uber Eats — Studium przypadku projektu oceny stojącego za agentem poprawy zdjęć w Uber Eats: ponowne sprawdzanie tras, pass@K, diagnoza odchyłek, automatyczna kalibracja oraz warstwowe mechanizmy kontroli jakości.