Strona główna / Artykuły / Strukturalne zasady bezpieczeństwa dla agentów AI: Wewnątrz pipeline ResolveFlow

Strukturalne zasady bezpieczeństwa dla agentów AI: Wewnątrz pipeline ResolveFlow

Wyjaśnia, w jaki sposób agent oparty na LangGraph zapewnia oddzielenie procesu rozumowania od wykonywania zadań poprzez sprawdzanie na poziomie kodu, a nie instrukcje w formie promptów, w tym błąd pobierania danych, który pojawił się w trakcie pracy.

3446 słów

Większość demonstracji sztucznej inteligencji opartej na agentach follows ten sam podstawowy schemat: model decyduje o działaniu i natychmiast je wykonuje. Narzędzie jest dołączane do instrukcji, model je uruchamia, a narzędzie działa bez dalszych sprawdzeń. Może to wyglądać przekonująco w krótkim filmiku demonstracyjnym, ale to właśnie taka konfiguracja budzi obawy u osób zastanawiających się nad umożliwieniem systemom autonomicznym dostępu do czegokolwiek istotnego — ponieważ często jedyną ochroną przed błędną diagnozą lub szkodliwymi zmianami w działającym systemie jest zdanie ostrzegawcze zawarte w instrukcji.

Projekt opisany tutaj został stworzony w celu uniknięcia takiej zależności od pojedynczej instrukcji ostrzegawczej.

ResolveFlow przyjmuje link do issue na GitHubie, zbiera dowody potwierdzające, klasyfikuje problem do określonej kategorii, a następnie w zależności od tej kategorii wykonuje jedną z trzech czynności: podejmuje ustaloną, niepodlegającą negocjacjom akcję, rozpoczyna dochodzenie prowadzone za pomocą modeli językowych w oparciu o zebrane dowody lub przekazuje problem bezpośrednio do ludzkiego recenzenta. Zamiast pojedynczego cyklu, w którym model wywołuje funkcję narzędzia, system jest zbudowany jako maszyna stanowa LangGraph, oparta na jednej centralnej koncepcji:

Rozumowanie i wykonywanie działań są rozdzielone ze względu na sposób budowy systemu, a nie z powodu konwencji, których ma przestrzegać.

To oznacza, że nie chodzi tu po prostu o polecenie modelowi sprawdzenia najpierw z kimś innym. Zamiast tego drugi, niezależny wywołanie LLM analizuje i krytykuje diagnozę pierwszego modelu, zanim człowiek ją w ogóle zobaczy. A jedyna funkcja w całym kodzie, której wolno pisać z powrotem do GitHuba, weryfikuje specjalny flag oznaczający zgodę w ramach własnej logiki — nie dlatego, że graf ma kierować wywołaniami w ten sposób, ale dlatego, że sama funkcja odmówi wykonania bez obecności tego flaga, nawet jeśli przyszła zmiana w kodzie stworzy bezpośrednią ścieżkę omijającą standardowe kroki.

Pozostała część tego przewodnika opisuje, w jaki sposób faktycznie budowana jest ta systematyczna struktura, przy użyciu rzeczywistej implementacji, a także błąd, który pojawił się podczas rozwoju. Ten błąd stanowi cenną lekcję: model wskazujący na prawdziwy dokument źródłowy to nie to samo, co model wskazujący na źródło rzeczywiście istotne dla rozpatrywanego pytania.

Kształt łańcucha przetwarzania

Sześć etapów przebiega kolejno, a tylko jeden z nich ma uprawnienia do modyfikowania czegokolwiek poza samym łańcuchem przetwarzania:

  1. fetch_evidence — rzeczywiste wywołania API GitHub REST, które pobierają tekst treści problemu, jego wątek komentarzy oraz wyniki wszystkich testów CI.
  2. normalize_evidence — nieprzetworzony JSON zwrócony przez te wywołania jest sprawdzany i przekształcany w obiekt typu IssueEvidence.
  3. classify — lżeka procedura oparta na regułach, która nie wymaga żadnych wywołań modeli językowych.
  • generate_diagnosis — uruchamiany tylko wtedy, gdy krok klasyfikacji oznacza problem jako wymagający głębszego zbadania. Jest to wywołanie LLM połączone z kontekstem wzbogaconym o dane pobrane z zewnętrznych źródeł, a wynik jest ograniczony do określonego schematu.
  • independent_review — drugie, samodzielne wywołanie LLM, które krytycznie ocenia wynik pierwszego wywołania, opierając się na warunkach zatwierdzenia/odmowy obliczonych w prostym kodzie, a nie na subiektywnej ocenie.
  • await_approval → execute — to etap, w którym proces czeka na decyzję człowieka; dopiero po jej zatwierdzeniu wdrażany jest jeden węzeł uprawniony do publikacji czegokolwiek z powrotem na GitHubie.
  • Następne sekcje szczegółowo omawiają najważniejsze elementy.

    Klasyfikacja: celowo nie jest wywołaniem LLM

    Dostępność modeli językowych o dużej mocy obliczeniowej skłania do podejmowania każdej decyzji za ich pośrednictwem, nawet tych, które nie wymagają takiego rodzaju rozumowania. Krok klasyfikacji określa, w jakim stopniu dany problem może wejść na drogę kosztowną i bardziej ryzykowną – polegającą na diagnozie opartej na modelu, żądaniach o pobranie danych oraz ostatecznym zapisie. Ze względu na tę rolę kontrolną, musi on być tani, szybki i w pełni przewidywalny sam w sobie:

    def classify(state: GraphState) -> dict:
        if state["evidence"].has_failing_ci:
            return {"classification": "deterministic"}
        elif state["evidence"].is_information_sparse:
            return {"classification": "ai_investigation"}
        else:
            return {"classification": "human_review"}
    

    Krok ten daje trzy możliwe wyniki, z których każdy wymaga innego poziomu zaufania w dalszych etapach:

    1. deterministyczny — nieudana kontrola CI jest samodzielnym, jednoznacznym sygnałem mechanicznym. Nie ma potrzeby żadnych dalszych dochodzeń; problem jest po prostu oznaczony i przekazywany osobie odpowiedzialnej za konserwację.
  • ai_investigation — używane wtedy, gdy problem nie zawiera wystarczająco wielu szczegółów, aby można było natychmiast podjąć działania. To właśnie w tym przypadku umiejętności modelu językowego są naprawdę przydatne, ponieważ musi on ustalić, o co dokładnie chodzi.
  • human_review — przeznaczone dla wszystkiego, co niejasne lub co może mieć duży wpływ w przypadku błędnego potraktowania. Zamiast pozwalać systemowi zgadywać w takich sytuacjach, problem jest bezpośrednio przekazywany osobie.
  • Warto zaznaczyć, że to human_review, a nie ai_investigation, jest opcją awaryjną. Gdy system nie potrafi zrozumieć sytuacji, nie próbuje wymyślić sprytniej odpowiedzi.

    Diagnoza: wyszukiwanie, a następnie schemat, a nie tekst swobodny

    Gdy problem trafia do gałęzi ai_investigation, krok generate_diagnosis przeszukuje indeks Pinecone zawierający około 2 750 fragmentów pochodzących z rzeczywistych, zamkniętych problemów z czterech oddzielnych repozytoriów (facebook/react, langchain-ai/langchain, microsoft/terminal oraz vercel/next.js). Następnie te odzyskane fragmenty są wysyłane do LLM jako kontekst wspierający dla tej operacji:

    def generate_diagnosis(state: GraphState) -> dict:
        evidence = state["evidence"]
        query = f"{evidence.title}\n\n{evidence.body}"
        snippets = retrieve_evidence(query, k=3)
        snippet_block = "\n\n".join(
            f"[{s['id']}] (relevance: {s['score']:.2f}) {s['text']}" for s in snippets
        )
    
        prompt = (
            f"Issue: {evidence.title}\n{evidence.body}\n\n"
            f"Comments:\n{chr(10).join(evidence.comments) or '(none)'}\n\n"
            f"Evidence snippets (cite by id in square brackets):\n{snippet_block}"
        )
    
        llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
        structured_llm = llm.with_structured_output(Diagnosis)
        diagnosis = structured_llm.invoke([("system", _SYSTEM_PROMPT), ("human", prompt)])
    
        return {
            "diagnosis": diagnosis,
            "retrieved_ids": [s["id"] for s in snippets],
            "retrieved_scores": {s["id"]: s["score"] for s in snippets},
        }
    

    Kilka szczegółów w tym kroku diagnostyki wymaga bliższego przyjrzenia się.

    Najpierw kształt wyniku nie jest czymś, co zostaje wydobyte później — jest gwarantowany od samego początku. Diagnoza jest zdefiniowana jako model Pydantic:

    class Diagnosis(BaseModel):
        root_cause: str
        severity: Literal["low", "medium", "high"]
        missing_info: list[str] = Field(default_factory=list)
        recommended_next_steps: list[str]
        citations: list[str] = Field(
            default_factory=list,
            description="IDs of retrieved evidence/doc snippets that support each claim above",
        )
    

    Poprzez wywołanie .with_structured_output(Diagnosis) model jest zmuszany do używania dokładnie tej struktury podczas generowania odpowiedzi. Nie ma żadnych regularnych wyrażeń, które mogłyby później wyszukać przyczynę problemu w tekście — gdy wywołanie się udaje, to, co wraca, to już obiekt o określonym typie, a nie tekst wymagający interpretacji.

    Po drugie, cytaty nie dotyczą tonu ani pewności siebie — muszą być prawdziwymi identyfikatorami. W instrukcjach systemu jasno stwierdza się, że każdy cytat musi odpowiadać jednemu z podanych mu identyfikatorów fragmentów; nic wymyślonego, a także nic niepowiązanego z twierdzeniem, którego fragment faktycznie nie potwierdza. Ta ograniczenie wydaje się samo w sobie wystarczające. Tak nie jest — i właśnie dlatego istnieje kolejny etap w tym procesie.

    Niezależna ocena: model wyjaśnia, kod decyduje

    To jest bez wątpienia najważniejszy wybór architektoniczny w całym systemie.

    Węzeł independent_review wysyła drugi, zupełnie oddzielny wywołanie ChatOpenAI, z własnym promptem i bez wspólnego kontekstu z wywołaniem, które dostarczyło diagnozę. Jego zadaniem jest ocena tej diagnozy.

    Oto jednak kluczowa część: ostateczna decyzja o zatwierdzeniu lub eskalacji nigdy nie jest pozostawiana modelowi. Sprowadza się ona do trzech wartości logicznych obliczanych w zwykłym Pythonie, a wynik LLM jest redukowany do komentarza czytelnego dla człowieka, od którego logika zatwierdzania faktycznie nie zależy.

    def independent_review(state: GraphState) -> dict:
        evidence = state["evidence"]
        diagnosis = state["diagnosis"]
        retrieved_ids = set(state.get("retrieved_ids", []))
        retrieved_scores = state.get("retrieved_scores", {})
    
        groundedness_ok = bool(diagnosis.citations) and all(
            citation_id in retrieved_ids
            and retrieved_scores.get(citation_id, 0.0) >= MIN_RELEVANCE_SCORE
            for citation_id in diagnosis.citations
        )
        risk_ok = diagnosis.severity in _ALLOWED_SEVERITIES  # {"low", "medium"}
        permission_ok = True  # comment/label are the only writes available today
    
        llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
        reasoning = llm.invoke(
            [("system", _SYSTEM_PROMPT), ("human", prompt)]
        ).content  # human-readable critique — not what the gate checks
    
        outcome = "approve" if (groundedness_ok and risk_ok and permission_ok) else "escalate_to_human"
    
        return {"review_result": ReviewResult(
            outcome=outcome,
            groundedness_ok=groundedness_ok,
            risk_ok=risk_ok,
            permission_ok=permission_ok,
            reasoning=reasoning,
        )}
    

    To jest ogólny wzorzec, który musi przestrzegać każda wiarygodna wersja „LLM-as-judge”: model może się wyjaśnić, ale to kod dokonuje wywołania.

    Jeśli poprosisz jeden model o ocenę pracy innego modelu, a następnie po prostu zaufasz wynikowi przedstawionemu w jego odpowiedzi, faktycznie stworzyłeś system, którego niezawodność jest ograniczona właśnie tym, co próbujesz zweryfikować.

    W takim układzie wynik generowany przez LLM stanowi przydatną narrację dla ludzkiego czytelnika, ale mechanizm decyzyjny, który naprawdę ma znaczenie, nie może zostać przekonany do negatywnego wyniku za pomocą perswazyjnego sformułowania, ponieważ nie analizuje niczego, co napisał model, aby podjąć decyzję.

    Jeszcze jedna kwestia wartą uwagi: poziom wysokiej powagi nigdy nie występuje w _ALLOWED_SEVERITIES. Każda diagnoza oznaczona jako wysokiej powagi jest automatycznie przekazywana człowiekowi, bez względu na to, jak dokładne są jej odniesienia. Bycie prawidłowym i bezpiecznym pod kątem automatycznej aprobaty to po prostu nie to samo.

    Błąd: „grounded” nie jest tym samym co „relevant”

    Tutaj w praktyce sytuacja stała się naprawdę skomplikowana.

    Pierwotna weryfikacja groundedness_ok polegała po prostu na sprawdzeniu, czy identyfikator cytatu odpowiadał czemuś w pobranej kolekcji — prawdziwemu identyfikatorowi, a nie sfałszowanemu. Na papierze wydaje się to rozsądną weryfikacją. W rzeczywistości jednak nie wystarczała.

    Podczas testowania na małym korpusie 40 problemów przez pipeline przepuszczono prawdziwy przypadek pustego ciała w React (facebook/react#36932, „experimental_taintUniqueValue throws RangeError for large binary values”). Proces wydobywania danych przyniósł trzy fragmenty, wszystkie prawidłowe i poprawnie zidentyfikowane — ale żaden z nich nie miał nic wspólnego z tym konkretnym błędem. Pracując na tak słabych danych, model nadal dostarczył pewną siebie, szczegółową diagnozę, która okazała się całkowicie błędna: twierdził on o „problemie z kompatybilnością rozszerzenia React DevTools”. Wszystkie cytaty bez problemu przeszły test trafności. Sama diagnoza nadal była bezużyteczna.

    Rozwiązaniem było przestanie traktowania informacji „zostało wydobyte” jako zamiennika dla „jest istotne”. Zaktualizowano plik tools/retrieval.py, tak aby każdy fragment zwracał teraz swoją wartość podobieństwa kosinowego wraz z tekstem:

    def retrieve_evidence(query: str, k: int = 3) -> list[dict]:
        results = vector_store.similarity_search_with_score(query, k)
        return [
            {"id": doc.metadata["id"], "text": doc.page_content,
             "source": doc.metadata["source"], "score": score}
            for doc, score in results
        ]
    

    Następnie sprawdziłem, jak wyglądają rzeczywiste wyniki podobieństwa w porównaniu z żywym indeksem, zamiast zgadywać wartość progu. Zapytanie dobrze pasujące do prawdziwego treści w korpusie uzyskało wynik około 0,53–0,63. Zapytanie dotyczące czegoś całkowicie nieobecnego w czterech przetworzonych repozytoriach dało wynik około 0,21–0,22. Na podstawie tej różnicy independent_review.py wprowadza teraz sztywny minimalny próg: każdy przytoczony fragment musi osiągnąć wartość MIN_RELEVANCE_SCORE = 0,35. Ten próg został celowo umieszczony bliżej górnej granicy tej różnicy niż jej środka, ponieważ błędne awansowanie dobrej diagnozy jest znacznie mniejszym kosztem niż przepuszczenie złej diagnozy jako zatwierdzonej.

    Oprócz poprawy logiki oceniania, konieczne było powiększenie samego korpusu do wyszukiwania. Zaczęło się od 40 artykułów z jednego repozytorium, co dało około 130 fragmentów – na tyle małych, że losowy artykuł z rzeczywistego świata często nie miał żadnego prawdziwego, tematycznie pasującego elementu do wyświetlenia. Później liczba ta wzrosła do około 600 artykułów pobranych z czterech rzeczywistych repozytoriów, co przyniosło blisko 2 750 fragmentów.

    Gdy dostępny jest większy zbiór danych, to samo problemy taintUniqueValue powoduje teraz odrzucenie dwóch autentycznych raportów będących niemal duplikatami i umożliwia określenie dokładnej, konkretnej przyczyny: funkcja String.fromCharCode.apply przekracza limit liczby argumentów w silniku JavaScript podczas obsługi dużych buforów. Warto zauważyć, że independent_review nadal przekazuje ten przypadek do ludzkiej oceny, ponieważ jego stopień powagi jest sklasyfikowany jako wysoki, a znaleziska o wysokim stopniu powagi zawsze są przekazywane do dalszej analizy zgodnie z założeniami, niezależnie od tego, na jak pewną lub dokładną jest diagnoza. Dokładność nie zwalnia z konieczności przejścia przez bramkę kontrolną ryzyka.

    Główna lekcja z tego przypadku polega na tym, że odniesienie do rzeczywistego ID fragmentu danych jest warunkiem koniecznym, ale nie wystarczającym. Twierdzenie „model powołał się na coś” to inna stwierdzenie niż twierdzenie „model powołał się na coś prawdziwego i istotnego dla tego błędu”, a system, który weryfikuje tylko pierwsze z tych twierdzeń, wydaje się skrupulatny, podczas gdy w rzeczywistości jedynie potwierdza bezrefleksyjnie błędne informacje.

    Krok zatwierdzenia to rzeczywista pauza, a nie tylko pozorny stan ładowania

    Każdy dotychczas opisany etap proponuje jedynie określoną akcję. Nic nie jest zapisywane z powrotem do GitHuba aż do konkretnego momentu w procesie. Wszystko na wcześniejszych etapach – klasyfikacja, diagnoza, przegląd – jedynie proponuje działania; nic nie jest zapisywane do GitHuba aż do tego jedynego kroku.

    Węzeł await_approval komponuje dokładny komentarz (oraz, w razie potrzeby, dokładną etykietę), który miałby zostać opublikowany, stosując identyczną logikę niezależnie od tego, czy problem trafił przez ścieżkę deterministyczną, czy pochodzi z zatwierdzonej diagnozy ai_investigation. Następnie wywołuje metodę interrupt() z LangGraph:

    def await_approval(state: GraphState) -> dict:
        proposed_action = _build_proposed_action(state)
        approved = interrupt({
            "classification": state["classification"],
            "proposed_action": proposed_action,
        })
        return {"proposed_action": proposed_action, "approved": bool(approved)}
    

    Wywołanie interrupt() powoduje coś więcej niż tylko pokazanie ikony „czekania na zatwierdzenie”, gdy proces stoi w bezruchu — faktycznie zatrzymuje wykonywanie grafu w trakcie jego działania. Aby wznowić to wykonywanie później, konieczne jest zupełnie oddzielne żądanie w celu odnalezienia tego samego zatrzymanego wątku i kontynuacji od miejsca, w którym przerwano, przy użyciu wywołania typu result = await compiled_graph.ainvoke(Command(resume=True), config) z tym samym thread_id, który był używany podczas pierwotnego wykonywania. Aby to w ogóle zadziałało, stan grafu musi przetrwać dwie niepowiązane ze sobą żądania HTTP, co wyklucza możliwość korzystania z zwykłej pamięci wewnątrz procesu do przechowywania danych.

    Dopiero po sfinalizowaniu tego procesu uruchamiany jest węzeł execute. Warto tu zwrócić uwagę na dwa szczegóły. Po pierwsze, sprawdzenie uprawnień znajduje się wewnątrz samego kodu węzła, a nie jest wyrażone wyłącznie jako krawędź grafu — więc nawet jeśli w przyszłości jakaś modyfikacja przypadkowo doda bezpośrednią krawędź prowadzącą do execute, to sprawdzenie i tak to wykryje. Po drugie, execute publikuje wartość state["proposed_action"] dokładnie taką, jaka została zatwierdzona; nigdy później nie regeneruje tego komentarza. To, co zatwierdził człowiek, jest dokładnie tym, co zostaje opublikowane.

    def execute(state: GraphState) -> dict:
        if not state.get("approved"):
            raise PermissionError("execute() called without explicit approval")
    
        evidence = state["evidence"]
        action = state["proposed_action"]
        token = state.get("github_token")
    
        result = {
            "comment": post_comment(evidence.repo, evidence.issue_number,
                                     action["comment"], token=token)
        }
        if action.get("label"):
            try:
                result["label"] = add_label(evidence.repo, evidence.issue_number,
                                             action["label"], token=token)
            except requests.HTTPError as exc:
                result["label_error"] = str(exc)
    
        return {"execution_result": result}
    

    Dlaczego backend przechowywania dla wstrzymanych procesów jest ważniejszy, niż się wydaje

    Pierwotna implementacja opierała się na SqliteSaver, który przechowuje stan w lokalnym pliku na dysku samego backendu. Taka konfiguracja działa bez problemów na komputerze programisty.

    W środowisku produkcyjnym problem objawia się w sposób łatwy do przeoczenia: na hostingu darmowej taryfy, takim jak Render, przechowywanie danych na dysku nie jest trwałe pomiędzy restartami. Proces zostaje wyłączony po określonym czasie bezczynności i przy następnym żądaniu uruchamia się ponownie w zupełnie nowym kontenerze.

    Oto jak to wygląda w praktyce. Proces dochodzi do etapu await_approval i zatrzymuje się, czekając na decyzję użytkownika. Zanim ktoś kliknie „zatwierdź”, instancja darmowa przechodzi w stan bezczynności i jest wyłączana. Następne żądanie uruchamia nowy kontener z całkowicie pustą bazą danych. Gdy wykonywany jest kod Command(resume=...), nie ma już nic do wznowienia – historia zatrzymanego wątku zniknęła bez żadnego błędu ani ostrzeżenia.

    Rozwiązaniem jest AsyncPostgresSaver, wspierany przez rzeczywistą bazę danych Postgres (Neon w tym ustawieniu), która istnieje niezależnie od kontenera, w którym działa aplikacja:

    async with (
     AsyncPostgresSaver.from_conn_string(DATABASE_URL, serde=get_serde()) as saver,
     AsyncConnectionPool(DATABASE_URL, open=False,
     check=AsyncConnectionPool.check_connection) as pool),
    ):
    

    Nawet jeśli kontener zostanie zniszczony i odbudowany od zera, każdy wstrzymany wątek przetrwa, dopóki DATABASE_URL nadal wskazuje na tę samą bazę danych. Mechanizmy wstrzymywania i kontynuowania działania za pomocą interrupt() w ogóle się nie zmieniają — zmienia się jedynie miejsce, gdzie przechowywany jest stan wstrzymania.

    Należy to podkreślić osobno: zapisy dokonywane po zatwierdzeniu odbywają się pod tożsamością osoby, która je zatwierdziła, a nie pod jakimiś wspólnymi uprawnieniami do wdrażania. Aplikacja online umożliwia logowanie się każdemu użytkownikowi GitHuba, a każde odczytanie lub zapisanie w ramach danego procesu wykorzystuje własny token OAuth tej osoby. Wywołanie typu post_comment(evidence.repo, evidence.issue_number, action["comment"], token=token) używa tokena osoby zatwierdzającej, a nie tokena należącego do osoby, która przypadkowo wdrożyła aplikację.

    To ma znaczenie nie tylko ze względu na higienę uwierzytelniania. Przekształca to stwierdzenie „to osoba, która to zatwierdziła, to opublikowała” w coś, co sam GitHub może potwierdzić, sprawdzając autora komentarza, zamiast polegać jedynie na twierdzeniach przedstawianych przez interfejs aplikacji.

    Pozwala to również istniejącemu systemowi uprawnień GitHuba na skuteczne egzekwowanie zasad bez konieczności dodawania żadnego dodatkowego kodu: osoba, która jedynie przegląda repozytorium, którego nie posiada, może zostawić komentarz, ale do dodania etykiety potrzebny jest dostęp do edycji lub specjalne uprawnienia w tym repozytorium. execute.py radzi sobie z tym sprawnie – nieudana próba dodania etykiety jest traktowana jako częściowy sukces, zamiast powodować awarię całego żądania.

    Zapytania do modeli LLM i embeddingów nadal są przetwarzane przy użyciu kluczy API właściciela rozwiązania, niezależnie od tego, kto je inicjuje, i właśnie dlatego istnieje dzienny limit liczby zapytań na użytkownika, aby ograniczyć te koszty.

    Istnieje różnica, którą warto jasno określić: ocena regresji i ocena możliwości testują zupełnie różne rzeczy, a ich ocenianie w ten sam sposób to błąd, w który łatwo popaść. Priorytet routingu w funkcji classify(), logika bramy decyzyjnej w funkcji independent_review() oraz sprawdzenie uprawnień w funkcji execute() każde z nich ma dokładnie jedno poprawne zachowanie, a jedyne akceptowalne tempo prawidłowych wyników w takich testach to 100 procent, i tyle. Brama bezpieczeństwa, która zostanie ominięta choć raz podczas dowolnej liczby prób, stanowi krytyczną awarię – nie jest to wartość, którą można skorygować poprzez uśrednianie.

    To standard jest zupełnie inny od oceny tego, czy funkcja generate_diagnosis wytwarza dobre diagnozy. Tego rodzaju ocena jest przeprowadzana przez model, rzeczywiście ulepsza się z czasem i nigdy nie miała realnych szans osiągnięcia 100 procent. Traktowanie obu rodzajów sprawdzeń tak, jakby należały do tej samej skali, to powszechna pułapka – zabezpieczenie, które „przeważnie” działa, w rzeczywistości w ogóle nie pełni funkcji zabezpieczenia.

    Co jest aktualnie rzeczywistością

    Lepiej to nieprzesadnie przedstawić, niż przesadzać z opisem, więc oto prosty obraz sytuacji, a nie sprzedażowa prezentacja:

    Zbieranie dowodów odbywa się poprzez rzeczywiste żądania REST w GitHubie, które są przekształcane na obiekty typowane. Klasyfikacja opiera się na regułach i jest deterministyczna, bez udziału modeli językowych. Diagnoza pochodzi z rzeczywistego żądania do OpenAI, które generuje strukturyzowany wynik, oparty na prawdziwym wyszukiwaniu w Pinecone wśród około 2 750 fragmentów danych, które są ponownie pobierane co tydzień. Niezależna weryfikacja to odrębne żądanie do OpenAI, ale tym, co faktycznie kontroluje proces, jest kod, a nie opinia samego modelu. Proces zatwierdzenia przez człowieka polega na rzeczywistym przerwaniu działania za pomocą interrupt(), kontynuowaniu go przez Command(resume=...) oraz weryfikacji każdego bajtu od początku do końca. Frontend i backend są zarówno wdrożone — odpowiednio na Vercel i Render — w pełni asynchronicznie, a ich stan jest zapisywany w bazie danych Postgres. GitHub OAuth umożliwia każdemu odwiedzającemu zalogowanie się przy użyciu własnego konta; operacje zapisu są wykonywane w imieniu tej osoby, z ograniczeniem dziennym. Zestaw testów oceniających obejmuje testy regresji związane z kontrolą bezpieczeństwa, które r

    Wymagany jest wskaźnik zdawalności na poziomie 100 procent, a oceny są przypisywane według kodu. Ocena jakości diagnoz nie została jeszcze stworzona. Testy w tradycyjnym rozumieniu również nie zostały jeszcze napisane.

    Te dwa ostatnie braki nie są ukrywane — znajdują się na planie rozwoju, ponieważ rzeczywiście są najważniejszymi elementami do opracowania w kolejności, a nie dlatego, że zostały przeoczone.

    Lekcje wartych przyjęcia do własnego projektu

    Budowa agenta, który ma działać w rzeczywistych systemach, ujawnia szereg zasad, które wykraczają poza ramy tego konkretnego narzędzia do obsługi problemów na GitHubie:

    Zachowaj rozumowanie i wykonywanie zadań na oddzielnych ścieżkach kodu, a nie tylko w osobnych poleceniach. Instrukcja taka jak „zawsze pytaj przed działaniem” to nadal tylko zachowanie, a zachowania mają tendencję do zawodzenia dokładnie w momencie, gdy są najbardziej potrzebne. Funkcja, która kategorycznie odmawia działania, chyba że zostanie ustawiony zweryfikowany flag, stanowi prawdziwą granicę, a nie sugestię.

    Gdy mówisz, że krok przeglądu jest „niezależny”, upewnij się, że to dosłownie prawda: oddzielna próba wywołania, brak wspólnego śladu działania oraz werdykt wytwarzany przez kod, a nie przez tekst stworzony przez sam model. Model może wyjaśnić swoje rozumowanie, ale nie powinien sam go oceniać.

    Nie pozwól, by „model wskazał na coś rzeczywistego” zastąpiło „model wskazał na coś istotnego”. Rzeczywiście zmierz jakość wyszukiwania w odniesieniu do rzeczywistych zapytań, zanim ustalisz próg podobieństwa.

    Jeśli przerwa spowodowana udziałem człowieka ma znaczenie w twoim projekcie, koniecznie przetestuj, co się dzieje, gdy proces zostanie wznowiony przed odpowiedzią tego człowieka. Stan w pamięci i dyski tymczasowe wydają się działać poprawnie podczas testów lokalnych, ale potem zawodzą dokładnie w tych miejscach, gdzie awaria ma największe konsekwencje.

    Na koniec bądź szczery co do tego, co jest faktycznie ukończone, a co nadal stanowi tymczasowe rozwiązanie. Tabela statusu, która przyznaje się do swoich braków, budzi większe zaufanie niż plik README, który po cichu sugeruje, że wszystko jest gotowe.

    Literatura pokrewna

  • Zrozumienie agentów AI: cele, narzędzia, pamięć i pętla agenta — Przystępne dla początkujących wyjaśnienie, w jaki sposób agenci AI różnią się od chatbotów, obejmujące podstawowe komponenty, pętlę decyzyjną, poziomy autonomii oraz praktyczne zastosowania w rzeczywistym świecie.