Strona główna / Artykuły

Optymalizacja agenta Terraform za pomocą DSPy: Stwórz weryfikator przed optymalizatorem

Agenci Terraform tworzą schematy. Należy zbudować deterministyczny sprawdzacz, ustalić bazę dla agenta, a następnie użyć DSPy i GEPA do optymalizacji jego pliku umiejętności, uzyskując wiarygodne wyniki.

Wydawca: REACTAPP.TOP18 min czytania4065 słów

Źródła: Your Terraform Agent Is Probably Wrong Half the Time

Agent, który pisze kod infrastrukturalny, prawdopodobnie kiedyś stworzył podobny moduł: wygląda zupełnie rozsądnie, przypomina każdy moduł zatwierdzony przez recenzenta, a mimo to nie przechodzi testu terraform validate z powodu, którego nikt nie zauważy przy szybkim spojrzeniu.

To również jest mierzone. IaC-Eval to benchmark zawierający 458 starannie dobranej scenariuszy Terraform, a GPT-4 według doniesień osiąga jedynie 19,36% wyników pozytywnych@1 w tym teście, w porównaniu z 86,6% dla tego samego modelu w bardziej wymagających testach Pythona. Nowsze badania nad generowaniem IaC przez agenty pokazują 14,0% dla Qwen2.5-Coder 7B, 41,4% dla GPT-4o oraz 62,4% dla GPT-5.4.

Dlaczego kod infrastrukturalny jest o wiele trudniejszy niż Python? Poprawna konfiguracja musi jednocześnie spełniać cztery wymagania: ważną składnię, zgodność ze schematem używanej wersji dostawcy, spójną grafę zależności oraz przestrzeganie własnej polityki organizacji. Brak któregokolwiek z tych elementów sprawia, że całość jest błędna. Python, który jest poprawny w 90%, często nadal działa, podczas gdy Terraform, który jest poprawny w 90%, nie może zostać zastosowany.

Badania przyniosły również wyniki, które sprawiają, że standardowe rozwiązania wydają się wątpliwe. Prompting typu few-shot, metoda chain-of-thought oraz interakcja wieloetapowa pomogły jedynie nieznacznie. Jednak mniej więcej 79% niepowodzeń związanych z polityką wynikało z braku informacji w promptzie, a nie z ograniczeń modelu. Model był wystarczająco zdolny – po prostu nie zostały mu podane odpowiednie informacje.

To jest problem optymalizacji. Plan polega więc na użyciu działającego agenta Terraform, stworzeniu mechanizmu oceny potrzebnego do ustalenia, czy sytuacja się poprawia, a następnie uruchomieniu pełnego procesu DSPy w oparciu o tę ocenę: programowanie, ocena, optymalizacja. Wszystko to można uruchomić. Projekt towarzyszący jest podzielony na trzy foldery, po jednym na każdy etap:

agent/       the deepagents Terraform agent, its tools, prompts, and skills
eval/        the verifier, 38 tasks with Rego policies, and the benchmark
optimizer/   the DSPy program, metric, and GEPA compile

Agen startowy

Trzymaj tę sekcję krótką, ponieważ nie jest to najciekawsza część, a każdy agent może ją zastąpić. Ten konkretny agencik wyszukuje rzeczywiste schemat, tworzy pliki, które są sprawdzane, i waliduje je przed udzieleniem odpowiedzi.

W pliku agent/tools.py znajdują się trzy narzędzia. Agent wczytuje rzeczywisty schemat dostawcy przed zapisaniem plików. Tworzone przez niego pliki są formatowane i sprawdzane pod względem składni w trakcie tworzenia. Następnie agencik waliduje całą konfigurację i naprawia wszystkie problemy zgłaszane przez kompilator, a dopiero potem odpowiada.

Jeśli chodzi o architekturę, opiera się on na deepagents – frameworku LangChain do tworzenia agentów z narzędziami i umiejętnościami, choć ten framework praktycznie nie ma znaczenia. Liczy się przede wszystkim struktura:

from deepagents import create_deep_agent

agent = create_deep_agent(
    model="openrouter:openai/gpt-5.6-luna",
    tools=[provider_schema, write_terraform, validate_config],
    system_prompt=SYSTEM_PROMPT,
    skills=["./agent/skills"],     # SKILL.md — the thing we'll optimize
)

Katalog umiejętności zasługuje na bliższe przyjrzenie się, ponieważ wszystko inne tutaj go optymalizuje. Umiejętność to plik w formacie Markdown zawierający instrukcje, które agent ładuje obok swojego systemowego promptu, w formacie nazywanym Agent Skills, który szybko zyskuje popularność: Claude, GitHub Copilot oraz deepagents wszystkie czytają ten sam format SKILL.md. Dzięki temu problemy są dobrze rozdzielone. Systemowy prompt stanowi jedynie proste instrukcje (jak używać narzędzi, jak formatować wyniki), natomiast umiejętność zawiera wiedzę specjalistyczną, taką jak konwencje Terraform, domyślne ustawienia bezpieczeństwa oraz specyficzne cechy poszczególnych dostawców. Ponieważ ten sam plik może być używany przez kilka agentów, umiejętności są również przenośne.

Większość opisów DSPy skupia się na głównym punkcie wejścia programu, który służy optymalizatorowi. Pytanie postawione tutaj jest nieco inne: czy można optymalizować konkretną umiejętność? Prawdziwe zespoły poświęcają większość czasu na ręczną konfigurację tej umiejętności, ponieważ zawiera ona specjalistyczną wiedzę i jest nieustannie edytowana podczas przeglądów. W tym projekcie umiejętność ta została napisana ręcznie, znajduje się w git i przechodzi proces recenzji kodu tak jak każdy inny plik. Nigdy nie została ona zmierzona, co stanowi prawdziwy problem, a DSPy został stworzony właśnie po to, by temu zaradzić, ponieważ każda edycja tego pliku zmienia zachowanie systemu w produkcji bez żadnych testów.

Jak wygląda rzeczywisty błąd

Zacznijmy od rzeczywistego artefaktu z okresu przed wprowadzeniem jakichkolwiek poprawek. Powstał on podczas zapisanego testu bazowego, w którym agent nie miał żadnych narzędzi i pisał kod HCL bezpośrednio w swojej odpowiedzi. Prośba dotyczyła użycia pojemnika stanu z szyfrowaniem zarządzanym przez klienta:

  encryption {
    kms_key_name = var.encryption_key_name
  }

  lifecycle_rules {
    ...

Wygląda dobrze, ale uruchomienie walidatora na tym tekście pokazuje coś innego:

$ tofu validate

Error: Missing required argument
  on main.tf line 18, in resource "google_storage_bucket" "terraform_state":
The argument "default_kms_key_name" is required, but no definition was found.

Error: Unsupported argument
  on main.tf line 19, in resource "google_storage_bucket" "terraform_state":
An argument named "kms_key_name" is not expected here.

Error: Unsupported block type
  on main.tf line 22, in resource "google_storage_bucket" "terraform_state":
Blocks of type "lifecycle_rules" are not expected here. Did you mean
"lifecycle_rule"?

Trzy błędy pochodziły z jednego zasobu. Wymaganym argumentem jest default_kms_key_name, podczas gdy kms_key_name należy do innych zasobów Google, co właśnie sprawia wrażenie poprawności. Nazwa bloku została również zamieniona na formę liczby mnogiej, która nie istnieje.

Gdy błędy podstawowe zostały sklasyfikowane według typów, 80% błędów walidacji stanowiły wymyślone schematy: argumenty, bloki lub atrybuty, które wyglądają jak prawdziwe elementy HCL, ale nie istnieją w danym zasobie. Dostawca google posiada około tysiąca typów zasobów, które zmieniają się z każdą nową wersją, więc model ten wykorzystuje z pamięci wersjonowaną API, a pamięć generuje wiarygodne fikcje.

Te trzy narzędzia bezpośrednio adresują tę diagnozę. Samodzielnie dodawały one 27,7 punktu (wskaźnik zdawalności wzrósł z 41,7% do 69,4% w przypadku opisanego poniżej zestawu narzędzi). Błędy formatowania spadły do zera, ponieważ write_terraform uruchamia fmt zamiast oczekiwać, że model sam zajmie się odstępami. Błędy schematu również spadły do zera, ponieważ agent mógł wreszcie zobaczyć to, co widzi kompilator. To, co pozostało, składało się niemal wyłącznie z intencji – czy agent stworzył to, o co prośono – i właśnie tu wchodzi w grę DSPy.

Oto ta sama zadanie wykonywane przez agenta wyposażonego w narzędzia w obecnym ujęciu:

$ uv run python -m agent.run --task eval/tasks/backend-var-interpolation.json

task      backend-var-interpolation (opentofu)
model     openai/gpt-5.6-luna  engine=deepagents
files     main.tf, variables.tf
tools     {'write_terraform': 2, 'validate_config': 1, 'validate_pass': 1}
elapsed   56602ms

Były dwa zapisy i jedna weryfikacja, a agent odpowiada dopiero wtedy, gdy kompilator jest zadowolony. Plik z zadaniami oraz cel opentofu pochodzą z zestawu do oceny, do którego trafiają dalej wszystkie dane.

Skorer zamiast frameworka

Określenie, czy cokolwiek z tego jest pomocne, oznacza odpowiedź na pytanie „czy wynik jest dobry?„ w sposób bardziej konkretny niż jedynie subiektywne odczucie. Większość dziedzin wykorzystuje sędziego w postaci LLM, który porównuje opinie różnych modeli bez żadnej prawdy obiektywnej, a następnie sam ten sędzia musi zostać sprawdzony.

Terraform nie ma tego problemu, ponieważ zestaw narzędzi już działający w CI pełni rolę skorera:

  • tofu fmt -check wykrywa błędy formatowania.
  • tofu validate znajduje błędy składniowe, schematyczne oraz błędy odniesień.
  • tflint sygnalizuje przestarzałe argumenty oraz nieważne typy maszyn.
  • tofu test z mock_provider sprawdza semantykę bez użycia żadnych uprawnień chmurowych.
  • conftest z OPA Rego sprawdza intencję, pytając, czy agent stworzył to, o co prosił się.
  • Te pięć deterministycznych bram jest zaimplementowanych w eval/verifier/run.py. Nie bierze w tym udziału żaden „sędzia”, ocena nie kosztuje nic w postaci wywołań API, a żaden krok nie może wygenerować zasobu podlegającego opłacie, ponieważ w projekcie celowo nie ma żadnego elementu apply.

    Większość ludzi pomija ostatnią bramę, mimo że to właśnie ona ma największe znaczenie. validate potwierdza, że konfiguracja kompiluje się poprawnie. Rego sprawdza, czy bucket rzeczywiście ma włączone szyfrowanie, co jest wymagane i co czyni ją poprawną, a nie tylko ważną:

    package main
    import rego.v1
    
    deny contains "bucket must use a customer-managed KMS key" if {
        some name
        bucket := input.resource.google_storage_bucket[name][_]
        not bucket.encryption
    }
    

    Zespół, który już stosuje zasady poprzez OPA, prawdopodobnie ma wiele takich zasad w Terraform Cloud lub w swoim pipeline CI, które można ponownie wykorzystać bez zmian. Jest to podejście typu „shift-left”, stosowane wobec agentów. Terraform Cloud faktycznie odrzuca błędny plan podczas aplikacji, ale do tego czasu kod już istnieje, został przeczytany przez recenzenta i jest w kolejce do uruchomienia. Uruchomienie tego samego Rego po wygenerowaniu kodu pozwala wykryć naruszenia, zanim zostanie włożony cały ten wysiłek.

    Testuj testera. To krok, który najbardziej warto skopiować. Zasada, która zatwierdza wszystko, jest gorsza od żadnej, ponieważ przyznaje każdej wersji agenta tę samą ocenę i w milczeniu zgłasza brak zmian. Dlatego każda zasada w katalogu eval/tasks/policy/ zawiera jeden przykład zgodny z zasadami oraz jeden niezgodny, a skrypt sprawdza obie możliwości:

    $ cd eval/tasks && ./check_policies.sh
      ...
      ✓ vpc-subnet-firewall              good=0 bad=2
    
    ✅ 38/38 policies verified in both directions
    

    W ten sposób sformułowano trzydzieści osiem zadań. Obejmują one domyślne ustawienia bezpieczeństwa, połączenia między zasobami, zasięg w schematach dostawców, celowanie w konkretne dialekty, ograniczenia (czy agent dodaje elementy, których nikt nie chciał) oraz układ plików. Każde z nich łączy prompt z polityką Rego, która odzwierciedla jego intencję. Dialekty są traktowane specjalnie: w każdym zadaniu określono, czy ma ono na celu terraform, czy opentofu, ponieważ te narzędzia różnią się od siebie (OpenTofu oferuje szyfrowanie stanu oraz funkcję for_each dostawcy, natomiast Terraform oferuje Stacks); dlatego agent, który w tajemnicy celuje w niewłaściwe narzędzie, nie przechodzi weryfikacji.

    Diagram pięciu bram pokazuje również granicę pomiarową, której agent nie może przekroczyć, a łatwo przeceniać znaczenie tego faktu. W warunkach produkcyjnych agent może, a prawdopodobnie powinien, korzystać z narzędzia OPA, które sprawdza polityki przed udzieleniem odpowiedzi. To ten sam argument dotyczący przeniesienia działania na wcześniejszy etap, co przyczynia się do tworzenia lepszego kodu.

    Granica ma inne przeznaczenie. Tutaj te same zasady stanowią miarę, używaną do porównywania umiejętności, modeli oraz wyników wykonywania optymalizatorów. Gdyby agent mógł iterować w oparciu o tę samą zasadę, która ocenia eksperyment, wszystkie warianty zmierzałyby ku zaliczeniu, a porównanie nie przyniosłoby żadnych informacji. To jest zasada zestawu testowego zachowanego na później, zastosowana do narzędzi. Zasada brzmi: agent może widzieć dowolne sprawdzanie, z wyjątkiem tego, które aktualnie ocenia eksperyment. Jeśli agent produkcyjny ma być dostarczany wraz z narzędziem do zarządzania zasadami, należy ocenić również taką konfigurację, używając zasad zachowanych na później lub licząc potrzebne iteracje naprawy, zamiast sprawdzać, czy została ona zaliczona.

    Adoptowanie DSPy

    DSPy opisuje się jako framework do programowania modeli językowych, a nie do sterowania nimi za pomocą promptów. Jego dokumentacja dzieli proces pracy na trzy fazy: programowanie, ocenę i optymalizację, a sama kolejność ta stanowi naukę: optymalizator jest ostatnią rzeczą, którą modyfikuje się, a wszystko, co poprzedza to, sprawia, że optymalizator jest godny zaufania. Optymalizator wykorzystuje dokładnie trzy elementy: program, metrykę i dane.

    Wszystkie funkcje DSPy znajdują się w katalogu optimizer/, a konfiguracja polega na użyciu narzędzia uv:

    uv add dspy
    uv sync
    

    Faza 1: programowanie

    Podpis to skonstruowany kontrakt dla jednej wywołania LM, składający się z danych wejściowych, wyjściowych oraz dokumentacji opisującej zadanie. Znajduje się on w pliku optimizer/harness.py:

    import dspy
    
    class AuthorConfiguration(dspy.Signature):
        """Write a complete, valid Infrastructure-as-Code configuration."""
    
        request: str = dspy.InputField(
            desc="What the user wants built, in natural language."
        )
        target: str = dspy.InputField(
            desc="Which dialect to target: 'terraform' or 'opentofu'. These have "
                 "diverged — code targeting the wrong one will fail validation."
        )
        config: str = dspy.OutputField(
            desc="The complete configuration as fenced HCL blocks. Start each block "
                 "with a comment naming its file, e.g. '# main.tf'."
        )
    

    Samo pominięcie jest sednem. Nie ma żadnego stwierdzenia w stylu „jesteś ekspertem od Terraform”, żadnych przykładów z realizacją oraz żadnych prośb dotyczących formatu. Nazwy pól i ich opisy przekazują znaczenie, a mechanizm optymalizacji zajmuje się sformułowaniem tekstu.

    Każdy, kto uważa to za coś magicznego, może to rozwiać jednym zdaniem:

    dspy.inspect_history(n=1)
    

    Wydrukuje rzeczywisty komunikat utworzony przez adapter na podstawie podpisu. Rzeczywisty wynik z tego przykładu wygląda tak:

    System message:
    
    Your input fields are:
    1. `request` (str): What the user wants built, in natural language.
    2. `target` (str): Which dialect to target: 'terraform' or 'opentofu'. ...
    Your output fields are:
    1. `config` (str): The complete configuration as fenced HCL blocks. ...
    
    [[ ## request ## ]]
    {request}
    
    [[ ## target ## ]]
    {target}
    
    [[ ## config ## ]]
    {config}
    
    In adhering to this structure, your objective is:
            Write a complete, valid Infrastructure-as-Code configuration ...
    

    Nic nie jest ukryte. To zstrukturyzowany szablon utworzony na podstawie podpisu, i właśnie dlatego optymalizator może go bezpiecznie przepisać, a także dlatego powinien to być pierwszy element, który sprawdzić w przypadku dziwnego zachowania.

    Moduł stosuje określoną technikę do podpisu, przy czym oba te elementy zmieniają się niezależnie. Jeden podpis może być wykorzystywany jako pojedyncza operacja, z najpierw przeprowadzeniem rozumowania, lub jako element napędzający pętlę narzędziową:

    generate = dspy.Predict(AuthorConfiguration)             # one shot
    generate = dspy.ChainOfThought(AuthorConfiguration)      # reason first
    generate = dspy.ReAct(AuthorConfiguration, tools=[...])  # run a tool loop
    

    Ten agent wykorzystuje narzędzia, dlatego jest odzwierciedlony za pomocą dspy.ReAct i otrzymuje dokładnie te funkcje narzędzi, które wykorzystuje zainstalowany agent, zaimportowane bezpośrednio z pakietu agenta. Kod znajduje się w pliku optimizer/harness_deepagents.py:

    from agent.tools import make_tools            # the deployed agent's tools
    
    class TerraformAuthoringAgent(dspy.Module):
        def __init__(self, seed_instruction: str):
            super().__init__()
            # Seed the SIGNATURE before constructing ReAct, so DSPy appends its
            # tool protocol to your instruction instead of replacing it.
            seeded = AuthorConfiguration.with_instructions(seed_instruction)
            lc_tools = make_tools(work_dir, binary="terraform", stats={})
            self.react = dspy.ReAct(
                seeded,
                tools=[dspy.Tool(t.func, name=t.name, desc=t.description)
                       for t in lc_tools.values()],
                max_iters=10,
            )
    
        def forward(self, request: str, target: str = "terraform"):
            return self.react(request=request, target=target)
    

    Najważniejszą decyzją projektową w tym podejściu jest następująca: optymalizować ten sam program, który jest wdrażany. W jednym z wczesnych eksperymentów optymalizowano generator jednorazowy, a następnie wyniki przekazano do agenta używającego narzędzi, co sprawiło, że wszystkie liczby straciły sens, ponieważ instrukcje zostały dostosowane do sytuacji awaryjnych, których już nie występowało u rzeczywistego agenta.

    Instrukcją wyjściową jest istniejący plik SKILL.md, więc nie ma możliwości nowego startu, a prawdziwym pytaniem jest to, czy tekst napisany ręcznie można ulepszyć. DSPy nie wie, że dana ścieżka tekstowa stanowi umiejętność, a nie główną instrukcję, ponieważ widzi tylko tekst instrukcji z podpisu. To właśnie sprawia, że eksperymenty z umiejętnościami są możliwe do przeprowadzenia: wynik jest bezpośrednio zapisywany w formacie SKILL.md i działa dla każdego agenta, który potrafi odczytywać umiejętności.

    Faza 2: ocena

    Rodzaj danych w DSPy to dspy.Example, słownik, który wie, które pola są danymi wejściowymi. Ładowarka w pliku optimizer/dataset.py konwertuje ten zestaw bezpośrednio:

    dataset = [
        dspy.Example(
            task_id=t["id"],
            request=t["prompt"],
            target=t["target"],
            rego=t["rego"],                # path to this task's policy
        ).with_inputs("request", "target")
        for t in tasks
    ]
    

    Pola, które nie zostały oznaczone jako dane wejściowe, przekazują się jako metadane. Miernik wykorzystuje rego do znalezienia odpowiedniej polityki, a model nigdy jej nie widzi.

    Warto wyjaśnić, w jaki sposób wybierane są przykłady, ponieważ zbiór danych determinuje wszystko, czego może się nauczyć optymalizator. Nie pochodzą one z żadnego benchmarku. Naśladują one rzeczywiste zapytania – na przykład prośbę o szyfrowanie bufora stanu w GCS kluczem zarządzanym przez klienta lub zezwolenie dla konta usługowego na dostęp tylko do jednego bufora, bez żadnych dodatkowych uprawnień. Zakres obejmuje zatem podstawowe zasoby, którymi zajmuje się zespół platformy: przechowywanie danych, sieciowanie, IAM, SQL i Pub/Sub, a także rozwiązania przeznaczone na radzenie sobie z już znanych problemami: zasoby o złożonej strukturze, w których modele wymyślają argumenty, zadania kontrolne sprawdzające, czy agent nie dodaje zasobów, których nikt nie prosił, oraz powyższe zadania dialektalne. Niektóre z nich są celowo trudne, ponieważ zestaw, który od razu przechodzi wszystkie testy, nie może pokazać żadnych ulepszeń.

    Trzydzieści osiem przykładów wydaje się niewiele, i rzeczywiście tak jest. Celem jest szczegółowe omówienie każdego przykładu, ponieważ każdy ma własną politykę, oraz uczciwe podziały. Należy dokonać podziału przed optymalizacją i zachować zestaw testowy jako coś świętego: zestaw treningowy służy do uczenia się przez optymalizator, zestaw walidacyjny pomaga w wyborze kandydatów, a wyniki zestawu testowego są oceniane tylko raz, na końcu. Większość podejść w inżynierii promptów pomija tę dyscyplinę, a właśnie przez to ludzie przekonują się, że zmiana przyniosła efekt, mimo że tak nie było. Zdarzyło się to dwa razy w tym projekcie, i za każdym razem zwycięstwo na małym przykładzie walidacyjnym zmieniało się, gdy przykład ten urosnął.

    Metryka to element, do którego podłącza się sprawdzający, i jest to jedna funkcja pełniąca trzy zadania (optimizer/metric.py):

    def verify_metric(example, prediction, trace=None, pred_name=None, pred_trace=None):
        """Score with the SAME verifier that produces our benchmark numbers."""
        result = verify(
            config_dir=materialise(prediction.config),
            policy_dir=example.rego,
            target=example.target,
        )
    
        # Job 1 — bootstrapping (trace is set): a strict bool. Only outputs that
        # FULLY pass may become worked examples.
        if trace is not None:
            return result.passed
    
        # Job 2 — reflective optimization (pred_name is set): score AND feedback.
        # GEPA reads the text to understand *why* a candidate failed.
        if pred_name is not None:
            return dspy.Prediction(score=result.score, feedback=format_failures(result))
    
        # Job 3 — plain evaluation: a float.
        return result.score
    

    Trzymaj to proste. Jeśli metryka ponownie wdroży jakąkolwiek logikę oceny, liczba podlegająca optymalizacji i liczba publikowana będą się różnić, w rezultacie nic nie będzie mierzone.

    To właśnie w obszarze informacji zwrotnej ten podejście wyróżnia się. Optymalizatory refleksyjne wykorzystują tekst wyjaśniający niepowodzenie, a w tym przypadku taki tekst jest swobodny i dokładny: prawdziwe błędy kompilatora oraz rzeczywiste odrzucenia zgodnie z zasadami, takie jak te pokazane wcześniej. Pola zależne od oceny dostarczanej przez model językowy płacą za każdą ocenę i otrzymują znacznie gorsze informacje zwrotne, musząc ponadto zweryfikować samych oceniających.

    Zmierz bazę przed zrobieniem czegokolwiek innego, używając dspy.Evaluate:

    evaluate = dspy.Evaluate(devset=valset, metric=verify_metric,
                             num_threads=8, display_table=True)
    evaluate(program)
    

    Wynik testu z 3 przykładami wygląda w ten sposób, a pełna procedura zajmuje więcej czasu:

    Average Metric: 0.59 / 3 (19.6%): 100%|██████████| 3/3 [01:16<00:00, 25.60s/it]
    INFO dspy.evaluate.evaluate: Average Metric: 0.5882 / 3 (19.6%)
    WARNING dspy.evaluate.evaluate: Skipping table display since `pandas` is not installed.
    

    Dwie lekcje wynikają z tego fragmentu. Każdy „przykład” to pełny epizod agenta, a nie pojedynczy wywołanie API, więc należy odpowiednio dostosować budżet. Ponadto funkcja display_table wymaga biblioteki pandas, więc należy ją zainstalować w celu wyświetlania tabeli dla każdego przykładu.

    Jeśli z tego wyniknie jakaś zasada, niech będzie taka, że nic nie jest optymalizowane bez ustalenia punktu odniesienia. To właśnie ta liczba nadaje sens wynikom optymalizatora.

    Faza 3: optymalizacja

    Różne optymalizatory dostosowują różne elementy:

    • BootstrapFewShot syntetyzuje przykłady, które już zadziałały (demos).
    • MIPROv2 dostosowuje instrukcje i demos za pomocą wyszukiwania bayesowskiego.
    • GEPA dostosowuje instrukcje na podstawie analizy śladów niepowodzeń.

    Adjustowanie instrukcji jest odpowiednim rozwiązaniem dla kodu ograniczonego schematem, ponieważ badania nad IaC opartym na agentach wykazały, że przewyższa ono metodę wstrzykiwania demonstracji, właśnie dlatego, że demonstracje nie eliminują halucynacji związanych ze schematem. Dlatego GEPA jest najlepszym wyborem, co wynika z pliku optimizer/compile.py:

    gepa = dspy.GEPA(
        metric=verify_metric,
        max_metric_calls=150,
        reflection_lm=dspy.LM("openrouter/openai/gpt-5.6-luna", max_tokens=16000),
        num_threads=8,
        track_stats=True,
    )
    
    compiled = gepa.compile(student=program, trainset=trainset, valset=valset)
    compiled.save("compiled_state.json", save_program=False)
    

    Dwa te modele wykonują różne zadania. Model do wykonywania zadań generuje kod Terraform i jest tym, co jest ostatecznie wysyłane; model do analizy błędów bada niepowodzenia i opracowuje propozycje instrukcji. Uruchamia się on około 40 razy w porównaniu do około 900 uruchomień modelu do wykonywania zadań, więc inwestycja w silniejszy model w tym przypadku jest opłacalna.

    Kilka cennych praktycznych uwag:

    • Ustaw wartość num_threads. Podczas jednego uruchomienia o tym zapomniano, co spowodowało 69 minut kompilacji; późniejsze pomiary pokazały, że maszyna była nieaktywna przez 94% czasu, czekając na kolejne wywołania API.
    • „150 wywołań metrycznych” oznacza 150 epizodów agenta, co przy użyciu pętli narzędziowej daje w przybliżeniu 900 wywołań modelu językowego.
    • Zawsze wywołuj save() po skompilowaniu. Przy użyciu save_program=False wynikiem jest zwykły JSON: zoptymalizowany tekst instrukcji wraz z dowolnymi demonstracjami. Ten sam JSON umożliwia całkowite uniknięcie DSPy, ponieważ około dwadzieścia linijek kodu (optimizer/to_skill_md.py) przekształca go ponownie w plik SKILL.md, który jest używany przez agenta. W czasie działania DSPy nigdy nie jest importowane. Optymalizator stanowi jedynie krok budowania, a jego wyniki trafiają do pull requesta w postaci różnic, które osoba może przeczytać i odrzucić.

    GEPA również odkrywa rzeczy rzeczywiste. W słabszym modelu agent ciągle porzucał swój cykl narzędziowy: jedna wywołanie provider_schema, a potem przestawał bez zapisania niczego. GEPA nigdy o tym nie została poinformowana. Przeczytała ślady awarii i wpisała to do optymalizowanych instrukcji: nie kończć pracy tylko dlatego, że plik został zapisany, ponieważ udany zapis dowodzi jedynie, że HCL się parsuje, a validate_config jest konieczny do wykrywania błędów w schemacie dostawcy oraz nieważnych odniesień. To refleksja nad rzeczywistymi śladami wykonywania, która daje wskazówki, których sformułowanie wymagałoby godzin debugowania.

    Proces pracy nadal przynosi korzyści po optymalizacji

    Dwa kolejne elementy tej samej struktury ponownie wykorzystują już istniejącą miarę.

    Metryka może być również wykonywana w czasie inferencji. Funkcje BestOfN i Refine z biblioteki DSPy otaczają dowolny moduł, uruchamiają go tyle razy, ile określa wartość N, i zwracają pierwszy wynik, którego ocena osiąga określony próg:

    robust = dspy.Refine(module=program, N=3, reward_fn=verify_metric, threshold=1.0)
    

    Wzorzec powinien wydawać się znajomy, ponieważ pętla walidacji i naprawy agenta stosuje tę samą zasadę z kompilatorem jako funkcją nagrody. Dowody wskazują, że jest to najsilniejszy narzędzie w tym zakresie: w badaniu nad IaC iteracyjna naprawa z informacjami zwrotnymi od weryfikatora podniosła wynik modelu Qwen 7B z 14,0% do 62,9%, co jest lepsze od wszystkich wypróbowanych technik sterowania. Niezależnie od formy, model musi mieć dostęp do swojego weryfikatora w czasie wykonywania.

    Zmiany modeli stają się prostymi modyfikacjami jednej linijki kodu i są teraz mierzalne:

    program.set_lm(dspy.LM("openrouter/qwen/qwen3-coder-30b-a3b-instruct"))
    evaluate(program)
    

    To miało większe znaczenie, niż się spodziewano. Przejście w trakcie projektu na tańszy model spowodowało spadek jakości z 83,3% do 37,5%, przy czym prawie połowa prób zrezygnowała z użycia narzędzia i w ogóle nie wygenerowała żadnych plików. W tradycyjnym procesie pracy nic takiego nie wychodzi na jaw, a pierwszy sygnał pochodziłby od klienta. Dwanaście minut działania zestawu testowego ujawniło ten problem.

    Szczerzy wynik

    Dla tego agenta zaimprowizowane instrukcje nie okazały się lepsze od tych napisanych ręcznie.

    Porównanie przeprowadzono dziewięć razy, testując dwie implementacje agentów i dwa modele, w tym oddzielny zestaw testowy, w którym obie wersje uzyskały ten sam wynik – 92,9%. Ta wartość odnosi się do silniejszego modelu, natomiast wcześniejsze wyniki 41,7% i 69,4% dotyczą tańszego modelu w tym samym zestawie testowym. Dwa razy wynik wydawał się jednoznacznie korzystny, został zapisany, a następnie zmienił się po przetestowaniu większej liczby próbek.

    To jest najcenniejsza lekcja, jaką można się nauczyć, i nie oznacza to, że DSPy zawodzi. Konkretny i prawdopodobny powód może wyjaśnić, dlaczego nie zadziałało. Uzyskane przez GEPA rezultaty pochodzą z modułowych procesów pracy, w których każdy krok wykorzystuje inny prompt, dzięki czemu optymalizator może przypisać awarię konkretnemu modułowi i go naprawić. Agent natomiast używa jednego promptu na każdym kroku. Krytyka Benjamina Andersona zakłada, że to eliminuje właśnie tę lokalność, na której opiera się GEPA.

    Następują praktyczne wskazówki:

    • W przypadku prostego procesu, opisane korzyści prawdopodobnie się przenoszą.
    • W przypadku pętli agenta, przeprowadź własne testy, zanim założysz, że tak jest.
    • W obu przypadkach kieruj informacje zwrotne od narzędzia weryfikacyjnego bezpośrednio do agenta – ma to najlepsze potwierdzenie w opublikowanych pracach i tych danych.

    A ten test jest możliwy, ponieważ taki zestaw narzędzi istnieje.

    Jak to zmienia sytuację

    Modele językowe mają trudności z zarządzaniem infrastrukturą w formie kodu. Wynik 19,36% GPT-4 w teście IaC-Eval nie jest problemem, który można rozwiązać prostymi sugestiami czy optymistycznym myśleniem, a znane triki ledwo pomagają. Mimo to większość problemów związanych z politykami wynika z braku informacji, a nie braku umiejętności, więc istnieje rzeczywisty margines poprawek, który znajduje się w instrukcjach i narzędziach, a nie w wagach modelu.

    Proces pracy, który się sprawdził, to ten, który jest przedstawiony w dokumentacji DSPy, w dokładnej kolejności, w jakiej jest on nauczany. Jeśli chodzi o programowanie: sygnatury umowy, moduły odzwierciedlające to, co zostanie wdrożone, oraz funkcja inspect_history w przypadku wątpliwości co do wysyłanych danych. W zakresie oceny: przykłady z wyraźnie określonymi danymi wejściowymi, prosta metryka służąca jednocześnie jako informacja zwrotna dla optymalizatora, wartość bazowa przed jakąkolwiek zmianą oraz zbiór testowy używany tylko raz. Do optymalizacji, i to dopiero wtedy, przy użyciu tej samej metryki, zapisuje się tekst, który można przejrzeć w prośbie o połączenie kodu.

    Mimo że optymalizator nie przewyższył umiejętności napisanej ręcznie, użycie DSPy zmusiło do zapisania definicji „poprawnej” w postaci kodu wykonywalnego. Ten element nadal zachowywał swoje znaczenie. Wykrył regresję o 46 punktów po zamianie modelu, ujawnił, że umiejętność była ładowana tylko w 25% przypadków (problem rozwiązano poprzez przepisanie jednego zdania, po czym wskaźnik ładowania osiągnął 100%), a dwukrotnie zapobiegł opublikowaniu wyniku spowodowanego szumem jako zwycięstwa.

    W żadnym z tych przypadków DSPy nie jest bezwzględnie konieczne, a narzędzie weryfikacyjne tutaj go nie importuje. DSPy przydaje się wtedy, gdy zmusza do istnienia i ponownego wykorzystania określonej metryki – jedna definicja steruje zestawem testów regresji, porównaniami modeli, próbami ponownego przetworzenia danych w czasie inferencji oraz optymalizatorem. Zespoły, które już mają odpowiednią dyscyplinę, mogą nie potrzebować tego frameworku, a te, które jej nie mają, mogą go tam pozyskać. W obu przypadkach należy przestać wprowadzać edycje promptów, których nie da się zmierzyć.

    Podobne artykuły

  • Dlaczego metryki pamięci agentów wprowadzają w błąd i jak oceniać na podstawie własnych danych — Publiczne listy rankingowe pamięci zawierają błędne arkusze odpowiedzi i systemy oceny. Oddziel proces wyszukiwania od odpowiedzi, dodaj sprawdzanie spójności, świeżości, powstrzymywania się od działania oraz zapominania, a także śledź koszty.
  • Wybór frameworku dla agenta w zależności od charakteru zadań: sześć wymiarów i ścieżka decyzyjna — Wybierz LangGraph, CrewAI, AutoGen, Temporal lub nic wcale, testując sześć wymiarów zadań: determinizm, czas trwania, idempotencja, ślad audytowy, szybkość pracy zespołu oraz koszt na decyzję.