Samolecząca zespół wielu agentów w LangGraph: architekt, tester, debugger
Stwórz maszynę stanową LangGraph w Pythonie, w której Architekt, Programista, środowisko testowe pytest, Debuger i Przeglądający współpracują i kontynuują proces w pętli, aż testy się udają.
Źródła: Build a Self-Healing Multi-Agent Team with LangGraph
Sztuczna inteligencja jest obecna niemal we wszystkich warstwach pracy z oprogramowaniem – od generowania kodu, przez analizę danych, aż po wieloetapowe rozumowanie. Mimo to każdy, kto prosi duży model językowy o coś złożonego, napotyka ten sam problem. Odpowiedź uzyskana za jednym razem może na pierwszy rzut oka wydawać się poprawna, ale zawodzi, gdy musi sprostać wymaganiom oraz kilku specjalnym przypadkom. Zwykłą metodą jest ręczny proces: kopiowanie dziennika błędów, wklejenie go do czatu, proszenie o naprawę i powtarzanie tego, aż w końcu wszystko zadziała.
Odpowiednia strategia ma również swoje wady. Przekazanie jednemu monolitycznemu agentowi dziesiątek narzędzi oraz długiej listy instrukcji zwykle zwiększa liczbę błędów, ponieważ jego kontekst ulega zanieczyszczeniu, a charakterystyki narzędzi zaczynają się ze sobą mieszać. Jakość wyników spada, gdyż agent nie jest w stanie jednocześnie realizować wszystkich tych instrukcji.
Przedstawiony poniżej projekt rozwiązuje oba te problemy za pomocą modułowej, hierarchicznej maszyny stanów wielu agentów napisanej w Pythonie z użyciem LangGraph. Przykładem jest samonaprawiający się zespół inżynierów oprogramowania składający się z pięciu ról: architekta, programisty, środowiska testowego Pytest opartego na podprocesach, debuggera oraz osoby sprawdzającej. Ten wzorzec można zastosować w wielu innych procesach inżynieryjnych opartych na sztucznej inteligencji.
Rozbicie dużego zadania na specjalistyczne podagenty, przydzielanie każdemu odpowiedniej ilości możliwości rozumowania, wykonywanie wyników w kontrolowanym środowisku oraz dodanie ciągłego obiegu informacji umożliwiają zespołowi testowanie własnej pracy, wykrywanie problemów i ich naprawę przed przekazaniem zweryfikowanego rezultatu. Najpierw spójrzmy na architekturę.
Kształt systemu określają dwie zasady.
Pierwszym rozwiązaniem jest hierarchiczne podziałanie modeli na różne poziomy z adaptacyjnymi stopniami myślenia. Role różnią się poziomem trudności, więc nie powinny wszystkie korzystać z tego samego modelu ani tego samego budżetu obliczeniowego:
- Architekt działa na modelu
gemini-pro-latestz poziomem myśleniahigh. Wykonywa on najtrudniejsze zadania wymagające intensywnego rozumowania, takie jak określanie granic problemu, przewidywanie sytuacji konkurencyjnych oraz projektowanie rygorystycznych zestawów testowychpytest. - Rozwijający korzysta z modelu
gemini-3.7-flashz poziomem myśleniamedium. Jest to zrównoważona konfiguracja umożliwiająca szybką syntezę kodu zgodnie z wyraźnymi specyfikacjami Architekta. - Uruchamiający testy to deterministyczny podproces w Pythonie, który lokalnie uruchamia
pytestw około 15 milisekund i w ogóle nie zużywa tokenów API LLM.
gemini-3.7-flash z poziomem myślenia high, ponieważ śledzenie surowych ścieżek stosu aż do błędnej linii wymaga analizy przyczynowej źródła problemu.gemini-flash-lite-latest z poziomem myślenia low. Sprawdzanie stylu oraz pisanie podsumowania dla użytkownika wymagają jedynie prostego rozumowania.Drugą zasadą jest ścisłe ograniczenie zakresu narzędzi, czyli zasada najmniejszych uprawnień stosowana wobec agentów. Żaden podagent nie ma nieograniczonego dostępu do terminala i systemu plików. Każdy otrzymuje tylko dane wejściowe oraz narzędzia niezbędne do wykonywania swojej roli. Programista nie może uruchamiać dowolnych poleceń w shellu, a wykonawca testów po prostu je uruchamia i raportuje to, co faktycznie się stało, bez proszenia modelu językowego o interpretację lub wymyślanie wyniku. Wąski zakres utrzymuje każdego agenta skupionym i zmniejsza ryzyko halucynacji.
Koncepcje LangGraph
LangGraph funkcjonuje jak wspólna tablica. Każdy członek zespołu, czyli każdy podagent, wykonywa swoje zadanie i zapisuje wynik z powrotem w tym samym wspólnym miejscu, dzięki czemu wszyscy mogą zobaczyć kontekst, który zespół dotychczas stworzył. LangGraph pełni rolę orkiestratora: decyduje, który agent ma działać dalej, i przekazuje odpowiedni stan.
Tutaj istotne są cztery koncepcje:
- Stan, reprezentowany przez
TypedDict, jest tą wspólną tablicą. Wszystkie agenty czytają i zapisują dane do jednego słownika w Pythonie o nazwieAgentTeamState, który zawiera prośbę użytkownika, specyfikację, testy jednostkowe, obecny projekt kodu, wyniki testów oraz liczbę rund debugowania. - Węzły to członkowie zespołu. Węzeł to po prostu funkcja w Pythonie, która otrzymuje aktualny stan, wykonywa swoje zadanie (wywołuje model językowy, uruchamia testy) i zwraca aktualizacje, które są ponownie łączone ze wspólnym stanem.
StateGraph oraz ich skompilowania. Można go uruchomić, a jego zdarzenia mogą być przesyłane w czasie rzeczywistym w miarę postępów przepływu pracy.Ustawianie projektu
Zacznij od nowego katalogu i środowiska wirtualnego. Te polecenia w terminalu tworzą je oba i aktywują środowisko:
mkdir langgraph-self-healing-team
cd langgraph-self-healing-team
python3 -m venv .venv
source .venv/bin/activate
Plik requirements.txt wymienia następnie zależności:
langgraph>=0.2.20
langchain-google-genai>=2.0.0
langchain-openai>=1.6.0
langchain-core>=0.3.0
pydantic>=2.7.0
pytest>=8.0.0
rich>=13.7.0
python-dotenv>=1.0.1
Zainstalowanie ich w środowisku wirtualnym odbywa się za pomocą jednego polecenia:
pip install -r requirements.txt
Każdy pakiet ma swoje zadanie:
langgraphkoordynuje pracę wielu agentów, zarządzając stanem, przekazywaniem danych między węzłami oraz cyklicznymi pętlami sprzężenia zwrotnego.langchain-google-genaiłączy się z modelami Google Gemini i udostępnia informacje o ich poziomie rozumowania.langchain-openaijest opcjonalny. Dodaje wsparcie dla OpenRouter lub OpenAI, dzięki czemu modele takie jak GPT 5.6 lub Deepseek v4 Pro mogą być użyte zamiast Gemini, jeśli nie chce się polegać wyłącznie na tym ostatnim.pytestjest narzędziem do wykonywania testów w lokalnej przestrzeni sandbox procesów potomnych. Uruchamia generowany zestaw testów i zwraca rzeczywiste wyniki.richformatuje wyjście z terminala za pomocą kolorów, tabel i wyróżniania składni, co ułatwia śledzenie i debugowanie procesu pracy agentów.python-dotenvautomatycznie ładuje klucze API oraz ustawienia modeli z pliku.env.
Plik .env znajdujący się w korzeniu projektu zawiera dane uwierzytelniające oraz konfigurację modelu:
# API Keys
GEMINI_API_KEY="your_api_key_here"
# OPENROUTER_API_KEY=""
# OPENAI_API_KEY=""
# Model Tier Selection & Adaptive Thinking Levels (low, medium, high)
ARCHITECT_MODEL="gemini-pro-latest"
ARCHITECT_THINKING_LEVEL="high"
CODER_MODEL="gemini-3.7-flash"
CODER_THINKING_LEVEL="medium"
DEBUGGER_MODEL="gemini-3.7-flash"
DEBUGGER_THINKING_LEVEL="high"
REVIEWER_MODEL="gemini-flash-lite-latest"
REVIEWER_THINKING_LEVEL="low"
# Maximum Self-Healing Retry Loops
MAX_DEBUG_ITERATIONS=3
Gdy środowisko jest gotowe, aplikację można budować krok po kroku.
Krok 1: stan wspólny w src/state.py
Najpierw należy zdefiniować schemat danych przekazywanych między agentami. Stwórz katalog src zawierający plik state.py.
Pierwszą definicją jest model TestResult napisany za pomocą Pydantic. Reprezentuje on ustrukturyzowany wynik wykonywania pytest w środowisku sandbox:
from typing import TypedDict, List, Dict, Any, Optional
from pydantic import BaseModel, Field
class TestResult(BaseModel):
passed: bool = Field(description="Whether all tests in the test suite passed")
exit_code: int = Field(description="Exit code of the pytest execution process")
output: str = Field(description="Captured stdout and stderr from pytest")
passed_count: int = Field(default=0, description="Number of passed tests")
failed_count: int = Field(default=0, description="Number of failed tests")
error_summary: Optional[str] = Field(default=None, description="Concise error traceback summary")
TestResult rejestruje, czy zestaw testów przeszedł z powodzeniem, kod wyjścia procesu, ilość testów, które przeszły i nie przeszły, oraz surowy wynik testów. Zawiera również pole error_summary z tylko istotną częścią śladu błędu, którą debugger wykorzystuje do znalezienia i naprawienia problemu.
Następnie znajduje się główne pole AgentTeamState w formie TypedDict. Jest to „tablica” przemieszczająca się po grafie: każdy węzeł odczytuje to, czego potrzebuje, wykona swoją pracę i aktualizuje pola, od których zależą kolejne węzły.
class AgentTeamState(TypedDict):
"""
The shared whiteboard state passed between all nodes in the LangGraph team.
"""
user_request: str
specs: str
tests: str
code: str
test_results: Dict[str, Any]
iteration: int
max_iterations: int
debug_history: List[str]
review_summary: str
status: str
Każde pole pełni określoną rolę w procesie pracy:
user_requestzawiera oryginalny problem zgłoszony przez użytkownika, dzięki czemu każdy agent może zobaczyć początkową intencję przez cały czas trwania procesu.specsoraztestspochodzą od Architekta. Programista implementuje rozwiązanie zgodnie ze specyfikacją, a Uruchamiający testy wykonywa generowane testy w odniesieniu do kodu.codeto aktualna implementacja w języku Python. Programista wpisuje tam treść najpierw, a Debugger może ją zastąpić po nieudanym uruchomieniu.test_resultsprzechowuje ustrukturyzowane wyniki z środowiska testowego, które decydują o tym, kto zajmie się sprawdzeniem – Recenzent czy Debugger.iterationorazmax_iterationskontrolują pętlę samonaprawczą. Zapobiegają one nieskończonym próbom ze strony Debuggera i ustalają maksymalną liczbę prób, na przykład trzy.
debug_history przechowuje przyczynę podstawową oraz rozwiązanie z każdej rundy debugowania, a także zapis zmian w kodzie.review_summary otrzymuje końcowy raport recenzenta po tym, jak kod przejdzie weryfikację.status pokazuje, na jakim etapie obecnie znajduje się proces: generowanie kodu, wykonywanie testów, debugowanie lub gotowość do recenzji.Krok 2: poziomy modeli i stopnie rozumowania w src/config.py
Jednolita fabryka LLM przydziela odpowiedni model oraz poziom rozumowania (low, medium lub high) każdej roli podagenta. Plik src/config.py zaczyna się od słownika konfiguracji domyślnej:
import os
from typing import Dict, Any, Optional
from dotenv import load_dotenv
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_openai import ChatOpenAI
load_dotenv()
# Role Configurations & Adaptive Thinking Levels
DEFAULT_CONFIG: Dict[str, Dict[str, Any]] = {
"architect": {
"model": os.getenv("ARCHITECT_MODEL", "gemini-pro-latest"),
"thinking_level": os.getenv("ARCHITECT_THINKING_LEVEL", "high"),
"temperature": 0.2,
},
"coder": {
"model": os.getenv("CODER_MODEL", "gemini-3.7-flash"),
"thinking_level": os.getenv("CODER_THINKING_LEVEL", "medium"),
"temperature": 0.2,
},
"debugger": {
"model": os.getenv("DEBUGGER_MODEL", "gemini-3.7-flash"),
"thinking_level": os.getenv("DEBUGGER_THINKING_LEVEL", "high"),
"temperature": 0.1,
},
"reviewer": {
"model": os.getenv("REVIEWER_MODEL", "gemini-flash-lite-latest"),
"thinking_level": os.getenv("REVIEWER_THINKING_LEVEL", "low"),
"temperature": 0.2,
},
}
MAX_DEBUG_ITERATIONS = int(os.getenv("MAX_DEBUG_ITERATIONS", "3"))
Każda rola otrzymuje model oraz poziom myślenia dostosowane do trudności jej zadania, co zapewnia równowagę między siłą rozumowania, szybkością a kosztem, nie pogarszając przy tym ogólnej jakości:
architectidebuggermają ustawiony poziomhigh, ponieważ projektowanie systemu oraz diagnoza błędów wymagają dogłębnych, wieloetapowych dedukcji.coderma ustawiony poziommedium. Musi jedynie postępować zgodnie z planem przygotowanym przez architekta, więc wystarczy zrównoważona, szybka i realistyczna synteza.reviewerma ustawiony poziomlow, aby zaoszczędzić tokeny, ponieważ jego zadaniem jest jedynie napisanie podsumowania po przejrzeniu wyników.
W tym samym pliku znajduje się również funkcja get_llm():
def get_llm(
role: str,
model_override: Optional[str] = None,
thinking_level_override: Optional[str] = None,
) -> BaseChatModel:
"""
Unified LLM Factory. Returns an initialized LangChain ChatModel configured
specifically for the sub-agent role with adaptive thinking levels.
"""
role_conf = DEFAULT_CONFIG.get(role, DEFAULT_CONFIG["coder"])
model_name = model_override or role_conf["model"]
thinking_level = thinking_level_override or role_conf["thinking_level"]
temp = role_conf["temperature"]
openrouter_key = os.getenv("OPENROUTER_API_KEY")
openai_key = os.getenv("OPENAI_API_KEY")
gemini_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
# 1. OpenRouter Provider (Handles open models like deepseek/deepseek-r1:free)
if openrouter_key and ("/" in model_name or not gemini_key):
return ChatOpenAI(
model=model_name,
api_key=openrouter_key,
base_url="https://openrouter.ai/api/v1",
temperature=temp,
)
# 2. Google Gemini Provider
if "gemini" in model_name.lower() or gemini_key:
kwargs: Dict[str, Any] = {
"model": model_name,
"temperature": temp,
}
if gemini_key:
kwargs["google_api_key"] = gemini_key
if thinking_level and thinking_level.lower() in ["low", "medium", "high"]:
kwargs["thinking_level"] = thinking_level.lower()
return ChatGoogleGenerativeAI(**kwargs)
# 3. Direct OpenAI Provider Fallback
if openai_key:
return ChatOpenAI(
model=model_name,
api_key=openai_key,
temperature=temp,
)
return ChatGoogleGenerativeAI(model=model_name, temperature=temp)
Dwie rzeczy czynią tę funkcję przydatną. Przekazuje ona thinking_level (low, medium, high) bezpośrednio do ChatGoogleGenerativeAI, dzięki czemu intensywność rozumowania może być dostosowywana do danej roli. Ponadto każdy klient, który jest zwracany, implementuje BaseChatModel z LangChain, więc węzły grafu wywołują metodę .invoke() w ten sam sposób, niezależnie od tego, jaki dostawca stoi za daną rolą.
Krok 3: deterministyczna środowisko testowe pytest w pliku src/sandbox.py
Częstym błędem w projektowaniu agentów jest proszenie modelu językowego o sprawdzenie w swoim „umyśle” kodu, który właśnie napisał. Model może z pewnością przewidzieć, że kod działa poprawnie, mimo obecności błędów w nim samym, ponieważ modele regularnie tworzą iluzje co do poprawności swoich wyników. Nawet gdy model zidentyfikuje niektóre błędy, nie ma gwarancji, że udało mu się je wszystkie znaleźć.
Bardziej niezawodną metodą jest pozwolenie kodowi na samodzielne sprawdzenie się. Zamiast polegać na ocenie modelu, lokalny węzeł wykonywania zapisuje utworzony kod i testy do tymczasowego katalogu oraz uruchamia pytest w izolowanym podprocesie Pythona. Rezultatem jest obiektywny wynik „przejdzie” lub „nie przejdzie”, który może zostać bezpośrednio wprowadzony z powrotem do procesu pracy.
Stwórz plik sandbox.py w folderze src. Zaczyna się on od funkcji pomocniczej _extract_error_summary(), która pobiera najważniejsze szczegóły błędu z surowego wyniku pytest, aby debugger widział tylko użyteczną część ścieżki błędu, a nie cały raport:
import os
import re
import sys
import tempfile
import subprocess
from typing import Dict, Any
def _extract_error_summary(pytest_output: str) -> str:
"""
Extracts the most relevant failure section (FAILURES / ERRORS traceback)
from the raw pytest output to provide dense, actionable signal to the Debugger.
"""
if "=== FAILURES ===" in pytest_output:
failures_part = pytest_output.split("=== FAILURES ===")[-1]
if "=== short test summary info ===" in failures_part:
failures_part = failures_part.split("=== short test summary info ===")[0]
return failures_part.strip()
if "=== ERRORS ===" in pytest_output:
return pytest_output.split("=== ERRORS ===")[-1].strip()
# Fallback to the last 25 lines
lines = pytest_output.splitlines()
return "\n".join(lines[-25:])
Pomocnik usuwa kody kolorów terminala oraz standardowe nagłówki testów, pozostawiając dokładny opis błędu w twierdzeniu (assert result == expected) wraz z śladem stosu. Dzięki temu debugger otrzymuje krótki, zwięzły opis tego, co poszło nie tak, bez zbędnych informacji.
Główny mechanizm uruchamiania, run_tests_in_sandbox(), znajduje się w tym samym pliku:
def run_tests_in_sandbox(code: str, tests: str, timeout_seconds: int = 15) -> Dict[str, Any]:
"""
Executes unit tests against the generated code in an isolated temporary directory.
Runs locally in a Python subprocess with zero LLM API calls ($0.00 / 0 tokens).
"""
with tempfile.TemporaryDirectory() as temp_dir:
code_file = os.path.join(temp_dir, "solution.py")
test_file = os.path.join(temp_dir, "test_solution.py")
# Write solution code and test files into the temporary directory
with open(code_file, "w", encoding="utf-8") as f:
f.write(code)
with open(test_file, "w", encoding="utf-8") as f:
f.write(tests)
# Build execution command using the active Python interpreter
python_exec = sys.executable
cmd = [python_exec, "-m", "pytest", "-v", "test_solution.py"]
try:
result = subprocess.run(
cmd,
cwd=temp_dir,
capture_output=True,
text=True,
timeout=timeout_seconds,
)
output = (result.stdout + "\n" + result.stderr).strip()
exit_code = result.returncode
except subprocess.TimeoutExpired as e:
output = f"Execution Timed Out after {timeout_seconds} seconds! Possible infinite loop.\n" + (e.stdout or "")
exit_code = -1
passed = (exit_code == 0)
passed_count = 0
failed_count = 0
passed_match = re.search(r"(\d+)\s+passed", output)
if passed_match:
passed_count = int(passed_match.group(1))
failed_match = re.search(r"(\d+)\s+failed", output)
if failed_match:
failed_count = int(failed_match.group(1))
elif not passed and exit_code != 0:
failed_count = 1
error_summary = None
if not passed:
error_summary = _extract_error_summary(output)
return {
"passed": passed,
"exit_code": exit_code,
"output": output,
"passed_count": passed_count,
"failed_count": failed_count,
"error_summary": error_summary,
}
Kilka szczegółów sprawia, że ta weryfikacja jest wiarygodna bez żadnej opinii modelu językowego:
tempfile.TemporaryDirectory()zapewnia każdemu uruchomieniu nowe środowisko pracy. Tam zapisywany jest generowany kod oraz testy, a katalog jest oczyszczany po zakończeniu wykonywania.timeout=15ogranicza czas wykonywania. Kod uwięziony w nieskończonej pętli lub działający zbyt długo ma swoją podproces eliminowaną, dzięki czemu nie może zablokować całego procesu agenta.exit_code == 0to deterministyczny sygnał sukcesu lub niepowodzenia odpytest. Znak zero oznacza, że zestaw testów zakończył się pomyślnie, a każdy inny kod wskazuje na problem, więc wynik oraz ślad wywołań trafiają do narzędzia do debugowania.
error_summary. Ta struktura znajduje się w wspólnym stanie, a graf wykorzystuje ją do decydowania, czy kontynuować, czy wejść w pętlę samonaprawczą.Krok 4: system wysyła pytania do każdego specjalisty w src/prompts.py
W systemie wielu agentów pytania powinny być krótkie, jednoznaczne i dobrze określone pod kątem zakresu. Każdy agent potrzebuje jasnego obrazu swoich zadań, obowiązków i ograniczeń, bez zbędnych instrukcji oraz bez nakładania się z obowiązkami sąsiadów. Plik prompts.py w katalogu src definiuje cztery odrębne pytania systemowe dla specjalistycznych ról:
ARCHITECT_SYSTEM_PROMPT = """You are the Lead Software Architect and Quality Assurance Principal.
Your role is to analyze the user's software engineering requirements and design:
1. Architectural Specification: Detailed function/class signatures, data structures, and edge-case requirements.
2. Comprehensive Pytest Suite: A rigorous set of unit tests in `pytest` format.
IMPORTANT TEST RULES:
- The tests MUST import from `solution` (e.g. `from solution import ClassName, function_name`).
- Include test cases for standard happy paths AND tricky edge cases (e.g., empty inputs, boundary limits, invalid types, concurrency/ordering if applicable).
- Write self-contained tests without external network dependencies.
Provide your response in two distinct markdown sections:
### SPECIFICATIONS
<detailed specifications and API contracts>
### TESTS
```python
<complete pytest code>
```
"""
CODER_SYSTEM_PROMPT = """You are a Senior Python Developer.
Your role is to write clean, idiomatic, and robust Python code that implements the provided Architecture Specifications and passes 100% of the Pytest Suite.
RULES:
- Implement ONLY the implementation code (do NOT include tests in your output).
- Use proper typing, error handling, and docstrings.
- Output ONLY the executable Python code inside a ```python ``` code block. Do not include extraneous conversational text.
"""
DEBUGGER_SYSTEM_PROMPT = """You are an Expert Debugger and Causal Root-Cause Analyst.
A Python solution was executed against its test suite in an isolated sandbox and FAILED.
Your job:
1. Carefully inspect the failing Pytest Traceback and Error Summary.
2. Cross-reference the traceback with the provided Implementation Code and Test Suite.
3. Identify the EXACT root cause (e.g., off-by-one error, incorrect data structure, missing edge-case branch).
4. Provide a 2-3 sentence diagnosis explaining the bug.
5. Provide the COMPLETE, corrected Python implementation code.
Output format:
### DIAGNOSIS
<2-3 sentence root-cause diagnosis>
### PATCHED_CODE
```python
<complete corrected python code>
```
"""
REVIEWER_SYSTEM_PROMPT = """You are a Principal Code Reviewer.
The multi-agent engineering team has completed the code and passed all unit tests.
Your job is to provide a concise, high-level code quality summary:
- Verify correctness and adherence to original user requirements.
- Note any key algorithmic strengths or future optimization ideas.
- Keep the summary under 150 words.
"""
Polecenia są skonstruowane z dwoma celami:
- Jasne delikatory wyjścia.
ARCHITECT_SYSTEM_PROMPTwymaga sekcji### SPECIFICATIONSi### TESTS, co umożliwia parserowi łatwe oddzielenie opisu architektury od kodu testowego do wykonania. - Strażne rozdzielenie zadań. Kodowcy poleca się pisać tylko kod, a nie testy, natomiast debugger musi przedstawić zwięzłą diagnozę w dwóch zdaniach obok kompletnego bloku kodu po naprawie. Każdy podagent ma jasno określony zakres działania, co zmniejsza ryzyko halucynacji.
Krok 5: tworzenie węzłów podagentów w pliku src/nodes.py
To jest główny silnik wykonywania w przepływie pracy, gdzie definiowane są funkcje węzłów. Węzeł LangGraph to zwykła funkcja w Pythonie. Bierze on udział w wspólnym stanie AgentTeamState, wykonywa określoną czynność, taką jak wezwanie modelu LLM lub uruchomienie testów, a następnie zwraca dane wymagające aktualizacji. LangGraph łączy je ze wspólnym stanem, dzięki czemu kolejny węzeł zawsze ma najnowszy kontekst.
Stwórz plik nodes.py w folderze src i dodawaj węzły po jednym.
Pomocnicze narzędzia do wydobywania kodu i dzielenia specyfikacji
Dwa małe narzędzia pomagają utrzymać porządek w procesie. Modele LLM często umieszczają kod w ramkach Markdown lub zaczynają go od rozmownych wprowadzeń, nawet gdy im się tego nie prosi. Te narzędzia zajmują się takim formatowaniem, aby reszta procesu zawsze otrzymywała czysty, wykonalny kod w Pythonie. Importy i narzędzia znajdują się na początku pliku src/nodes.py:
import re
from typing import Dict, Any, List
from langchain_core.messages import SystemMessage, HumanMessage
from src.state import AgentTeamState
from src.config import get_llm
from src.sandbox import run_tests_in_sandbox
from src.prompts import (
ARCHITECT_SYSTEM_PROMPT,
CODER_SYSTEM_PROMPT,
DEBUGGER_SYSTEM_PROMPT,
REVIEWER_SYSTEM_PROMPT,
)
def _get_text(content: Any) -> str:
"""
Extracts a clean string from either a standard string or list-of-dict content blocks
returned by modern LangChain message objects.
"""
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, dict) and "text" in item:
parts.append(str(item["text"]))
return "\n".join(parts).strip()
return str(content).strip()
def _extract_code(text: Any) -> str:
"""
Extracts executable Python code from markdown backticks using regex.
If no backticks are found, falls back to returning the cleaned raw text.
"""
clean_text = _get_text(text)
match = re.search(r"```(?:python)?\s*\n(.*?)\n```", clean_text, re.DOTALL)
if match:
return match.group(1).strip()
return clean_text.strip()
def _parse_architect_output(raw_content: Any) -> tuple[str, str]:
"""
Splits the Architect's raw output into its two distinct deliverables:
the architectural specification text and the pytest test suite.
"""
text = _get_text(raw_content)
specs = text
tests = ""
if "### TESTS" in text:
parts = text.split("### TESTS")
specs = parts[0].replace("### SPECIFICATIONS", "").strip()
tests = _extract_code(parts[1])
elif "```python" in text:
tests = _extract_code(text)
specs = text.split("```python")[0].strip()
return specs, tests
Każdy pomocnik zajmuje się typową osobliwością wyników generowanych przez LLM:
_get_text()normalizuje format odpowiedzi. W zależności od dostawcy i narzędzia LangChain, odpowiedź może przyjść w postaci zwykłej ciągu znaków lub jako strukturyzowana treść, np. lista zawierająca polatypeitext. Pomocnik przekształca obie formy na zwykły ciąg znaków w Pythonie._extract_code()wydobywa kod z ramek Markdown. Wykorzystuje regularne wyrażenia z flagąre.DOTALL, dzięki czemu kod wieloliniowy w Pythonie jest identyfikowany niezależnie od tego, czy model napisałpythonpo otwierających cudzysłowach, czy nie. Gdy brak jest takiej ramki, pomocnik korzysta z już oczyszczonego tekstu odpowiedzi.
_parse_architect_output() dzieli odpowiedź Architekta przy użyciu delimitera ### TESTS. Wszystko przed tym znacznikiem staje się specs, a wszystko po nim tests. Dzięki temu specyfikacja w języku naturalnym jest oddzielona od kodu testowego, który może być uruchomiony później przez Test Runnera.Węzeł Lead Architect
Lead Architect stanowi punkt wejścia dla zespołu i pracuje według podejścia test-first. Zanim powstanie jakakolwiek implementacja, analizuje żądanie użytkownika, definiuje struktury danych i interfejsy klas, a także projektuje kompletny zestaw testów pytest, który obejmuje zarówno oczekiwane scenariusze prawidłowe, jak i ważne przypadki graniczne. Dzięki temu programista ma jasny kontrakt do realizacji.
Funkcja ta jest dodawana do tego samego pliku:
def architect_node(state: AgentTeamState) -> Dict[str, Any]:
"""
Sub-Agent 1: Lead Architect (High Reasoning Tier).
Analyzes user requirements, designs architectural specifications,
and authors an exhaustive Pytest suite covering happy paths and edge cases.
"""
llm = get_llm("architect")
messages = [
SystemMessage(content=ARCHITECT_SYSTEM_PROMPT),
HumanMessage(content=f"Requirements:\n{state['user_request']}"),
]
response = llm.invoke(messages)
specs, tests = _parse_architect_output(str(response.content))
return {
"specs": specs,
"tests": tests,
"status": "specs_ready",
}
Trzy elementy wyróżniają się w tym kodzie:
- Wybór modelu.
get_llm("architect")konfigurujegemini-pro-latestz ustawieniemhighdla procesowania myślowego. Silny model umiejący przeprowadzać dogłębne rozumowanie okazuje się tu przydatny, ponieważ zadaniem jest planowanie na podstawie wymagań oraz przewidywanie przypadków krawędziowych, takich jak bezpieczeństwo wątków, puste zbiory czy warunki brzegowe. - Budowa wiadomości. Prompty systemowy
ARCHITECT_SYSTEM_PROMPTjest umieszczany w obiekcieSystemMessage, natomiast surowy prompt użytkownika zstate['user_request']staje sięHumanMessage. - Aktualizacje stanu. Rezultatem jest słownik zawierający
specs,testsorazstatus = "specs_ready", a LangGraph łączy wszystkie te elementy wAgentTeamState.
Węzeł programisty
Gdy już istnieją specyfikacja i testy, kontrola przechodzi do programisty. Jego zadaniem jest napisanie czytelnego, idiomatycznego kodu w Pythonie, który realizuje specyfikację i przechodzi wszystkie testy stworzone przez architekta. Funkcja ta znajduje się w pliku src/nodes.py pod nazwą developer_node():
def developer_node(state: AgentTeamState) -> Dict[str, Any]:
"""
Sub-Agent 2: Developer / Coder (Fast Workhorse Tier).
Synthesizes the Python implementation code grounded in the specifications
and the explicit assertions of the Pytest suite.
"""
llm = get_llm("coder")
prompt = f"""Requirements:
{state['user_request']}
Architect Specifications:
{state['specs']}
Pytest Suite:
```python
{state['tests']}
```
Write the complete Python implementation code to satisfy all tests.
"""
messages = [
SystemMessage(content=CODER_SYSTEM_PROMPT),
HumanMessage(content=prompt),
]
response = llm.invoke(messages)
code = _extract_code(response.content)
return {
"code": code,
"status": "code_generated",
}
Sposób działania:
- Wybór modelu.
get_llm("coder")wybiera modelgemini-3.7-flashz trybem myśleniamedium. Architekt już ustalił schemat API oraz wymagania testowe, więc programista nie musi niczego przeprojektowywać i może skupić się na szybkim oraz dokładnym kodowaniu. Średni wysiłek wystarcza w tym celu, a także pomaga ograniczyć zużycie tokenów i koszty rozwoju. - Opartość na zestawie testów. Wartość
state['tests']jest bezpośrednio włączana do promptu programisty. Widok dokładnych wymagań, które model musi spełnić, pomaga mu skupić się na oczekiwanych nazwach metod, typach argumentów oraz wartościach zwracanych. - Aktualizacje stanu. Po wydobyciu zwykłego kodu Pythona z odpowiedzi, węzeł zwraca
{"code": code, "status": "code_generated"}.
Węzeł deterministycznego środowiska testowego
To jeden z najważniejszych węzłów systemu. Zamiast pytać model językowy, czy kod jest poprawny, uruchamia się zestaw testów pytest na kodzie w izolowanym podprocesie. Testy wykonywane są lokalnie, więc ten krok nie wywołuje żadnych żądań do API modelu językowego i zazwyczaj trwa około 15 milisekund.
Funkcja test_runner_node() jest dodawana do pliku src/nodes.py:
def test_runner_node(state: AgentTeamState) -> Dict[str, Any]:
"""
Sub-Agent 3: Deterministic Test Sandbox ($0.00 / 0 Tokens).
Executes tests against the code in an isolated temporary subprocess sandbox.
Returns structured pass/fail metrics and concise error tracebacks upon failure.
"""
test_results = run_tests_in_sandbox(
code=state["code"],
tests=state["tests"],
timeout_seconds=15,
)
new_status = "tests_passed" if test_results["passed"] else "tests_failed"
return {
"test_results": test_results,
"status": new_status,
}
- Determinizm z użyciem zeru tokenów. Funkcja
run_tests_in_sandbox()otrzymuje wartościstate["code"]orazstate["tests"]. Żaden model nie jest wywoływany, więc ten krok nie kosztuje żadnych tokenów. - Określanie statusu. Wynik
test_results["passed"] == True(co oznaczaexit_code == 0) ustawiastatusna"tests_passed", natomiast wszelkie inne przypadki ustawiają go na"tests_failed". - Wyjście w postaci ustrukturyzowanego stanu. Zwracane wartości
test_resultszawierają wyniki wykonywania testów, liczbę testów zakończonych pomyślnie oraz nieuproszczony zarys ścieżki wywołań. To właśnie ten element jest sprawdzany podczas kierowania przepływu przez warunki.
Węzeł debuggera samoregenerującego się
Gdy tylko sandbox zgłasza błędy, kieruje ruch do Debuggera, specjalisty od ustalania przyczyn źródłowych. Ten analizuje trzy elementy: uszkodzony kod, założenia testowe oraz dokładny ślad wywołań pytest, aby ustalić przyczynę niepowodzenia, a następnie stosuje precyzyjne rozwiązanie. Funkcja debugger_node() znajduje się w pliku src/nodes.py:
def debugger_node(state: AgentTeamState) -> Dict[str, Any]:
"""
Sub-Agent 4: Debugger & Fixer (High Reasoning Tier).
Performs causal root-cause analysis on execution tracebacks and stack traces.
Applies surgical code patches and logs the diagnosis to the debug history.
"""
llm = get_llm("debugger")
test_results = state.get("test_results", {})
error_summary = test_results.get("error_summary", test_results.get("output", "Unknown error"))
current_iteration = state.get("iteration", 0) + 1
prompt = f"""The implementation failed its unit tests in the execution sandbox.
Current Implementation Code:
```python
{state['code']}
```
Pytest Suite:
```python
{state['tests']}
```
Execution Error Traceback:
{error_summary}
Please analyze the root cause of the failure and output the corrected Python code.
"""
messages = [
SystemMessage(content=DEBUGGER_SYSTEM_PROMPT),
HumanMessage(content=prompt),
]
response = llm.invoke(messages)
response_text = str(response.content)
# Extract diagnosis summary and patched code block
diagnosis = "Debugger applied code patch based on traceback."
if "### DIAGNOSIS" in response_text and "### PATCHED_CODE" in response_text:
diag_part = response_text.split("### PATCHED_CODE")[0]
diagnosis = diag_part.replace("### DIAGNOSIS", "").strip()
patched_code = _extract_code(response_text)
# Append the diagnosis to our audit trail
history = list(state.get("debug_history", []))
history.append(f"Iteration {current_iteration}: {diagnosis}")
return {
"code": patched_code,
"iteration": current_iteration,
"debug_history": history,
"status": "code_patched",
}
Sposób działania:
- Wysoki poziom przetwarzania rozumowania. Funkcja
get_llm("debugger")przywraca modelgemini-3.7-flashz ustawieniemhighdla procesu myślenia. Analiza ścieżki wykonywania oznacza śledzenie drogi wykonania, znajdowanie problematycznych przypadków granicznych oraz rozważanie, jak zmieniły się zmienne, co wymaga znacznego wysiłku. Model ten został wybrany jako kompromis pomiędzy głębią analizy a szybkością. Model profesjonalny również sprawdzi się w tej roli, a system może nawet zmienić model w trakcie działania, jeśli debugger nadal utknie po jednej lub dwóch pętlach. - Gęsty kontekst błędu. Do modelu trafia tylko skrócona wersja
error_summary, na przykładAssertionError: assert False == True, where False = allow_request('user_1').
current_iteration = state.get("iteration", 0) + 1 każde przejście przez debugger zwiększa licznik prób o jedynkę.debug_history.state['code'] jest zastępowany przez patched_code, a węzeł zgłasza status = "code_patched".Węzeł Reviewer
Po zakończeniu każdego udanego testu lub po osiągnięciu limitu prób kontrola przechodzi do węzła Reviewer, który stanowi ostateczną bramkę jakościową. Sprawdza ogólną jakość kodu, upewnia się, że dokumentacja jest jasna i przydatna, oraz tworzy krótkie podsumowanie gotowego rozwiązania dla użytkownika. Funkcja reviewer_node() jest dodawana do pliku src/nodes.py:
def reviewer_node(state: AgentTeamState) -> Dict[str, Any]:
"""
Sub-Agent 5: Code Reviewer (Fast Mode Tier).
Validates code quality, verifies requirement adherence,
and produces the final executive deliverable summary.
"""
llm = get_llm("reviewer")
test_results = state.get("test_results", {})
prompt = f"""Requirements:
{state['user_request']}
Final Implementation Code:
```python
{state['code']}
```
Test Results:
Passed: {test_results.get('passed', False)} ({test_results.get('passed_count', 0)} passed, {test_results.get('failed_count', 0)} failed)
Iterations Taken: {state.get('iteration', 0)}
Debug History: {state.get('debug_history', [])}
Provide a concise, professional code quality review and verification summary.
"""
messages = [
SystemMessage(content=REVIEWER_SYSTEM_PROMPT),
HumanMessage(content=prompt),
]
response = llm.invoke(messages)
return {
"review_summary": _get_text(response.content),
"status": "completed",
}
Jak to działa:
- Wybór modelu. Funkcja
get_llm("reviewer")zwraca modelgemini-flash-lite-latestprzy poziomie myślenialow, odpowiednim do szybkiego tworzenia streszczeń. Na tym etapie nie jest potrzebne wiele rozumowania, więc lżejszy model o niższej zdolności analitycznej zapewnia lepszą szybkość i niższe koszty. - Przegląd kontekstowy. Osoba przeglądająca otrzymuje ostateczną implementację, dane dotyczące liczby udanych i nieudanych prób z środowiska testowego, liczbę wykonywanych iteracji debugowania oraz pełną historię debugowania.
- Aktualizacja stanu. Węzeł zwraca wartość
{"review_summary": _get_text(response.content), "status": "completed"}.
Krok 6: logika routingu w pliku src/edges.py
Logika podstawowa oraz węzły agentów zostały już stworzone, więc następny krok to routowanie. Węzły wykonywają obliczenia, a krawędzie decydują, dokąd trafia kontrola. Krawędź warunkowa działa jak dynamiczny router: sprawdza słownik stanów zwrócony przez węzeł i odpowiada nazwą następnego węzła do wykonania.
Funkcja routowania znajduje się w nowym pliku edges.py w katalogu src:
from typing import Literal
from src.state import AgentTeamState
def route_after_test_runner(state: AgentTeamState) -> Literal["reviewer", "debugger"]:
"""
Conditional edge router evaluated immediately after `test_runner` completes.
Decides whether to trigger the self-healing Debugger loop or proceed to Reviewer.
"""
test_results = state.get("test_results", {})
passed = test_results.get("passed", False)
iteration = state.get("iteration", 0)
max_iterations = state.get("max_iterations", 3)
# 1. Happy Path: 100% of unit tests passed -> Proceed directly to Review
if passed:
return "reviewer"
# 2. Self-Healing Loop: Tests failed and within retry budget -> Trigger Debugger
if iteration < max_iterations:
return "debugger"
# 3. Safety Fallback: Exceeded maximum retries -> Exit loop to Reviewer with warning
return "reviewer"
Router analizuje wyniki testów oraz liczbę dotychczasowych prób naprawy i obsługuje trzy przypadki:
- Szlak pomyślny (
passed == True). Gdy wszystkie testy przechodzą przy pierwszej próbie, proces przechodzi bezpośrednio do osoby przeglądającej, a nie ma potrzeby wydawania dodatkowego czasu ani zasobów na debuggera. - Samonaprawa (
passed == Falseprzy jednoczesnymiteration < max_iterations). Testy, które nie przeszły, ale wciąż są dostępne próby naprawy, wysyłają kod do debuggera, który analizuje błąd i próbuje go naprawić. - Osiągnięty limit ponownych prób (
iteration >= max_iterations). Gdy debugger zużył wszystkie trzy dozwolone próby, a testy nadal nie przeszły, proces przechodzi do osoby przeglądającej, zamiast w nieskończoność powracać do tej samej procedury. Ten sztywny limit samonaprawy zapobiega zatrzymaniu się procesu z powodu sprzecznych lub niejednoznacznych wymagań.
Krok 7: budowa grafu w pliku src/graph.py
Knoty oraz mechanizmy routingu zostały już przygotowane, więc teraz wszystko zostanie połączone. Bezpośrednie przekazy danych oraz warunkowe krawędzie informacyjne łączą te node’y, a rezultatem jest StateGraph, który może uruchomić cały proces. Umieść ten kod w nowym pliku graph.py znajdującym się w katalogu src:
from langgraph.graph import StateGraph, END
from src.state import AgentTeamState
from src.nodes import (
architect_node,
developer_node,
test_runner_node,
debugger_node,
reviewer_node,
)
from src.edges import route_after_test_runner
def build_self_healing_graph() -> StateGraph:
"""
Assembles the Multi-Agent Self-Healing Engineering Team into a LangGraph StateGraph.
Configures node registrations, linear handoffs, cyclic feedback edges, and conditional routing.
"""
# 1. Initialize StateGraph with our Shared Whiteboard State Schema
workflow = StateGraph(AgentTeamState)
# 2. Register Sub-Agent Nodes (Mapping node names to Python callable functions)
workflow.add_node("architect", architect_node)
workflow.add_node("developer", developer_node)
workflow.add_node("test_runner", test_runner_node)
workflow.add_node("debugger", debugger_node)
workflow.add_node("reviewer", reviewer_node)
# 3. Define Graph Entry Point
workflow.set_entry_point("architect")
# 4. Add Deterministic Direct Handoff Edges
workflow.add_edge("architect", "developer")
workflow.add_edge("developer", "test_runner")
# 5. Add the Cyclic Self-Healing Feedback Edge
workflow.add_edge("debugger", "test_runner")
# 6. Add Conditional Routing Edge for Sandbox Test Evaluation
workflow.add_conditional_edges(
"test_runner",
route_after_test_runner,
{
"debugger": "debugger",
"reviewer": "reviewer",
},
)
# 7. Add Termination Edge
workflow.add_edge("reviewer", END)
# 8. Compile the Graph into an Executable State Machine
return workflow.compile()
Budowa odbywa się krok po kroku:
StateGraph(AgentTeamState)tworzy graf z stanami, wykorzystując schematAgentTeamState, dzięki czemu każdy węzeł może osiągnąć ten sam wspólny stan w miarę postępu pracy.workflow.add_node(name, func)rejestruje każdą funkcję w Pythonie jako węzeł. Każda z nich ma jedną rolę: projektowanie testów, generowanie kodu, wykonywanie testów lub naprawianie błędów.workflow.set_entry_point("architect")określa punkt startowy. Uruchomienie lub przepływ danych przez graf z początkowym stanem zapoczątkowuje wykonywanie od węzłaarchitect.
workflow.add_edge("architect", "developer") oraz workflow.add_edge("developer", "test_runner") tworzą początkową linię prostą. Gdy architekt przygotuje specyfikację i testy, przejmuje kontrolę programista, a gdy ten stworzy kod, następuje testowanie przez osobę odpowiedzialną za testy.workflow.add_edge("debugger", "test_runner") zamyka pętlę samonaprawczą, która przekształca zwykły proces w system samonaprawczy. Błąd, który został już naprawiony, nie kieruje przepływu pracy do osoby odpowiedzialnej za przeglądanie. Naprawiony kod wraca do osoby odpowiedzialnej za testy, aby naprawa mogła zostać ponownie sprawdzona.workflow.add_conditional_edges("test_runner", route_after_test_runner, ...) sprawia, że LangGraph wywołuje route_after_test_runner(state) po zakończeniu testów. Jego wartość zwracana kieruje przepływ pracy albo do debugger, albo do reviewer.workflow.add_edge("reviewer", END) zakończa wykonywanie. Po tym, jak reviewer ukończy kontrolę jakości i stworzy podsumowanie, wykonywanie grafu jest zakończone.workflow.compile() przekształca definicję grafu w graf gotowy do uruchomienia, walidując jego strukturę i przygotowując go do wykonywania.Krok 8: punkt wejścia CLI w pliku src/main.py
Ostatni element to punkt wejścia w linii poleceń stworzony za pomocą rich, który w czasie rzeczywistym przekazuje informacje o postępach poszczególnych podagentów za pomocą stylizowanych paneli terminala. Plik main.py w katalogu src zaczyna się od funkcji do wyświetlania napisu nagłówkowego oraz tabeli konfiguracji:
import os
import sys
import argparse
import time
import warnings
# Suppress internal SDK library warnings
warnings.filterwarnings("ignore")
# Ensure project root is in sys.path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.syntax import Syntax
from src.config import DEFAULT_CONFIG, MAX_DEBUG_ITERATIONS
from src.graph import build_self_healing_graph
from src.state import AgentTeamState
console = Console()
def print_banner():
console.print(
Panel.fit(
"[bold cyan]LangGraph Self-Healing Multi-Agent Engineering Team[/bold cyan]\n"
"[dim]Heterogeneous Model Tiering | Sub-Agent Tool Scoping | Autonomous Pytest Sandbox[/dim]",
border_style="cyan",
)
)
def print_config_table():
table = Table(title="Multi-Agent Team Configuration and Model Tiers", border_style="dim")
table.add_column("Sub-Agent Role", style="bold yellow")
table.add_column("Assigned Model", style="cyan")
table.add_column("Thinking Level", style="green")
table.add_column("Primary Responsibility", style="white")
table.add_row(
"Architect",
DEFAULT_CONFIG["architect"]["model"],
str(DEFAULT_CONFIG["architect"]["thinking_level"]).upper(),
"Specifications and Pytest Edge-Case Suite",
)
table.add_row(
"Developer",
DEFAULT_CONFIG["coder"]["model"],
str(DEFAULT_CONFIG["coder"]["thinking_level"]).upper(),
"Fast Code Synthesis from Specification",
)
table.add_row(
"Test Runner",
"Deterministic Python ($0.00 / 0 Tokens)",
"N/A",
"Subprocess Pytest Sandbox Execution",
)
table.add_row(
"Debugger",
DEFAULT_CONFIG["debugger"]["model"],
str(DEFAULT_CONFIG["debugger"]["thinking_level"]).upper(),
"Causal Traceback Analysis and Patching",
)
table.add_row(
"Reviewer",
DEFAULT_CONFIG["reviewer"]["model"],
str(DEFAULT_CONFIG["reviewer"]["thinking_level"]).upper(),
"Code Verification and Executive Summary",
)
console.print(table)
console.print()
Są one dotyczone wyłącznie stylizacji terminala i są oczywiste, więc nie wymagają szczegółowego omówienia. Ten sam plik kontynuuje się funkcją run_agent_pipeline():
def run_agent_pipeline(user_request: str):
print_banner()
print_config_table()
console.print(Panel(f"[bold white]User Request:[/bold white]\n{user_request}", border_style="blue"))
initial_state: AgentTeamState = {
"user_request": user_request,
"specs": "",
"tests": "",
"code": "",
"test_results": {},
"iteration": 0,
"max_iterations": MAX_DEBUG_ITERATIONS,
"debug_history": [],
"review_summary": "",
"status": "starting",
}
graph = build_self_healing_graph()
start_time = time.time()
current_state = initial_state
console.print("[bold green]Starting LangGraph Multi-Agent Workflow...[/bold green]\n")
# Stream graph execution node by node
for event in graph.stream(initial_state):
for node_name, node_output in event.items():
current_state.update(node_output)
if node_name == "architect":
console.print(
Panel(
f"[bold yellow][1/5] Architect Completed Specifications & Tests[/bold yellow]\n\n"
f"[bold]Specifications:[/bold]\n{current_state['specs'][:300]}...\n\n"
f"[bold]Generated Pytest Suite:[/bold]\n[dim]({len(current_state['tests'].splitlines())} lines of test code designed)[/dim]",
border_style="yellow",
)
)
elif node_name == "developer":
console.print(
Panel(
f"[bold cyan][2/5] Developer Completed Initial Implementation[/bold cyan]\n"
f"[dim]Synthesized {len(current_state['code'].splitlines())} lines of Python code.[/dim]",
border_style="cyan",
)
)
elif node_name == "test_runner":
results = current_state["test_results"]
if results.get("passed", False):
console.print(
Panel(
f"[bold green][3/5] Test Runner Sandbox: PASSED[/bold green]\n"
f"[green]All {results.get('passed_count', 0)} unit tests passed cleanly in subprocess sandbox.[/green]",
border_style="green",
)
)
else:
console.print(
Panel(
f"[bold red][3/5] Test Runner Sandbox: TESTS FAILED[/bold red]\n"
f"[red]{results.get('failed_count', 0)} failed, {results.get('passed_count', 0)} passed.[/red]\n\n"
f"[bold white]Traceback Snippet:[/bold white]\n[dim red]{results.get('error_summary', '')[:400]}[/dim red]",
border_style="red",
)
)
elif node_name == "debugger":
history = current_state.get("debug_history", [])
latest_diag = history[-1] if history else "Patch applied."
console.print(
Panel(
f"[bold magenta][4/5] Self-Healing Debugger (Iteration {current_state.get('iteration', 1)})[/bold magenta]\n"
f"[magenta]{latest_diag}[/magenta]\n\n"
f"[dim]Re-routing patched code to Test Runner Sandbox...[/dim]",
border_style="magenta",
)
)
elif node_name == "reviewer":
console.print(
Panel(
f"[bold white][5/5] Reviewer Final Report[/bold white]\n"
f"{current_state.get('review_summary', '')}",
border_style="white",
)
)
elapsed = round(time.time() - start_time, 2)
# Display Final Deliverables
console.print("\n" + "=" * 80)
console.print(f"[bold green]Pipeline Completed in {elapsed}s[/bold green]\n")
console.print("[bold cyan]Final Verified Implementation (solution.py):[/bold cyan]")
syntax_code = Syntax(current_state["code"], "python", theme="monokai", line_numbers=True)
console.print(syntax_code)
console.print("\n[bold yellow]Comprehensive Pytest Suite (test_solution.py):[/bold yellow]")
syntax_tests = Syntax(current_state["tests"], "python", theme="monokai", line_numbers=True)
console.print(syntax_tests)
# Summary Metrics Table
summary_table = Table(title="Execution Summary Metrics", border_style="cyan")
summary_table.add_column("Metric", style="bold")
summary_table.add_column("Value", style="green")
summary_table.add_row("Total Elapsed Time", f"{elapsed} seconds")
summary_table.add_row("Self-Healing Iterations", str(current_state.get("iteration", 0)))
summary_table.add_row("Pytest Pass Status", "Passed (100%)" if current_state["test_results"].get("passed") else "Failed")
summary_table.add_row("Unit Tests Passed", str(current_state["test_results"].get("passed_count", 0)))
console.print(summary_table)
run_agent_pipeline() to miejsce, gdzie wszystkie elementy się łączą. Ustawia wspólny stan AgentTeamState, tworzy samoregenerującą się strukturę grafową i przesyła dane o wykonywaniu węzłów po kolei. Po zakończeniu każdego węzła aktualizuje bieżący stan i wyświetla postęp w panelach Rich: specyfikację i testy architekta, kod programisty, wyniki wykonywania testów, każdą rundę debugowania oraz końcowy raport recenzenta. Na koniec wyświetla zweryfikowaną implementację, utworzoną bazę testów pytest oraz metryki wykonywania, takie jak całkowity czas trwania, liczba iteracji debugowania i wyniki testów.
Parser argumentów CLI zamyka ten plik:
def main():
parser = argparse.ArgumentParser(description="LangGraph Self-Healing Multi-Agent Engineering Team")
parser.add_argument(
"--prompt",
type=str,
default="Build an in-memory Sliding Window Rate Limiter class in Python with thread safety, per-user limits, and a clean method `allow_request(user_id: str) -> bool`.",
help="Software engineering prompt for the multi-agent team",
)
args = parser.parse_args()
run_agent_pipeline(args.prompt)
if __name__ == "__main__":
main()
Uruchamianie go w limiterze szybkości
W celu prawdziwego testu zespół otrzymuje niesamowitą, algorytmiczną zadanie. Ten polecenie wymaga limitera szybkości typu przesuwane okno bezpiecznego pod względem wątków:
python src/main.py --prompt "Build an in-memory Sliding Window Rate Limiter class in Python with thread safety, per-user limits, and a method allow_request(user_id: str) -> bool."
Wynik w terminalu pokazuje następującą sekwencję:
- Na etapie 1 z 5 główny architekt analizuje wymagania dotyczące ogranicznika szybkości i tworzy wielowątkową serię testów
pytestobejmującą usuwanie zapisów czasowych, egzekwowanie progów w systemie przesuwanych okien oraz obsługę równoczesnych żądań z wielu wątków. - Na etapie 2 z 5 programista pisze pierwszą klasę
SlidingWindowRateLimiteropartą nacollections.dequeorazthreading.Lock. - Na etapie 3 z 5 narzędzie do wykonywania testów w iteracji 0 uruchamia
pytest. Test obciążeniowy dotyczący równoczesności zawodzi, ponieważ usuwanie zapisów czasowych odbywa się poza zablokowaną sekcją krytyczną, co powoduje sytuację wyścigu zegarów, dzięki której kilka wątków może przekroczyć dozwoloną liczbę żądań.
_prune_expired_timestamps() została wywołana przed uzyskaniem dostępu do self._lock, a rozwiązanie polega na umieszczeniu operacji usuwania zapisanych czasów w bloku synchronizowanym.pytest na kodzie poprawionym, a zestaw testów pokazuje wynik „zielony” z komunikatem 6 passed in 0.04s.Zdjęcie ekranu z rzeczywistym wyjściem terminala z tego uruchomienia:
Pełne wyjście zawiera również kod w Pythonie, który został napisany, oraz przypadki testowe użyte do jego sprawdzenia. Przedstawienie ich wszystkich tutaj zwiększyłoby długość tekstu bez większej wartości.
Gdzie ten wzorzec się stosuje
Łączenie specjalistycznych podagentów, różnych modeli oraz poziomów myślenia dla każdego z nich, wraz z deterministycznym środowiskiem wykonywania, przekształca kruchy proces oparty na jednym poleceniu w bardziej niezawodny system samonaprawczy.
Demonstracją była zespół do generowania kodu, ale ten sam wzorzec maszyny stanów pasuje do wielu innych procesów, takich jak łańcuchy przetwarzania danych, orkiestracja dynamicznych API oraz walidacja strukturalna dokumentów. Podstawa jest prosta: nie polegaj na jednym modelu do wykonywania wszystkiego. Przypisz każdemu agentowi konkretne zadanie i pozwól systemowi sprawdzać oraz korygować swoją pracę w trakcie jej wykonywania.
Wyróżniają się trzy kluczowe wnioski:
- Nigdy nie pozwól modelowi językowemu oceniać własnego kodu. Przekaż wykonywanie zadania lokalnemu, deterministycznemu środowisku podprocesów uruchamiającemu
pytest– jest ono szybkie, bezpłatne i daje dokładną odpowiedź o powodzeniu lub niepowodzeniu.