Predykcyjne AI: Daj swojemu agentowi laboratorium Docker do uruchamiania modeli
Spis treści
Każde demo agenta AI pokazuje to samo: “zapytaj swojego agenta o dane.” Agent odpytuje bazę danych, podsumowuje wyniki, może tworzy wykres.
Ale spróbuj poprosić go o uruchomienie regresji wielomianowej na 24 miesiącach danych sprzedażowych i prognozę na następne 6 miesięcy. Nagle twój agent oparty na chacie uderza w ścianę. Nie może zrobić import sklearn. Nie może wykonać Pythona. Może rozumować o tym, jakiego modelu użyć, ale nie może go faktycznie uruchomić.
Oczywista poprawka: daj mu środowisko Pythona. Ale jest pytanie, o którym nikt nie mówi - czy to środowisko powinno być domyślnym runtimem agenta (jak Claude Code), czy raczej narzędziem, które agent wybiera, kiedy chce?
Zbudowaliśmy demo, które odpowiada na to pytanie.
TL;DR
- Wzorzec “environment as a tool” pozwala agentowi AI selektywnie używać sandboxa Docker tylko wtedy, gdy jest potrzebny, unikając narzutu przy prostych zapytaniach.
- Podejście delegacji do sub-agenta utrzymuje głównego agenta w czystości - opisuje co przewidzieć, a sub-agent wymyśla jak napisać kod Pythona.
- Strukturalne wyjście Pydantic do wykresów bije generowanie obrazów - wyślij dane, pozwól frontendowi renderować z Chart.js.
- Strumieniowanie WebSocket z
agent.iter()daje wgląd w czasie rzeczywistym w tekst, wywołania narzędzi i wyniki. - Najtrudniejsza część to integracja frontendowa - łączenie serii wykresów z różnymi zakresami dat, przechwytywanie wyjść narzędzi i strumieniowanie przez WebSocket.
Demo: Agent do analityki predykcyjnej
Zbudowaliśmy pełnowymiarową aplikację demo - asystenta analitycznego opartego na chacie, który potrafi:
- Odpytywać dane - filtrować i agregować miesięczne dane sprzedażowe (3 produkty, 3 regiony, 24 miesiące)
- Uruchamiać predykcje - sub-agent pisze i wykonuje Python (sklearn, pandas) wewnątrz izolowanego kontenera Docker
- Generować wykresy - strukturalne wyjście Pydantic renderowane jako wykresy liniowe Chart.js w przeglądarce
Główny agent ma trzy narzędzia. Dwa są proste (zapytaj JSON, zwróć dane wykresu). Trzecie jest interesujące - uruchamia sub-agenta, który ma pełny dostęp do sandboxa Docker.
Architektura: Środowisko jako narzędzie
Oto kluczowa decyzja projektowa: sandbox Docker jest narzędziem, a nie domyślnym środowiskiem agenta.
analytics_agent: Agent[AnalyticsDeps, str] = Agent( "openai:gpt-4.1", deps_type=AnalyticsDeps,)Główny agent to zwykły agent Pydantic AI. Nie żyje wewnątrz Dockera. Ma trzy narzędzia zarejestrowane z @analytics_agent.tool:
query_data- czyta plik JSON, filtruje rekordy, zwraca wyniki. Docker nie jest potrzebny.predict- tworzy sub-agenta z dostępem do Dockera, deleguje zadanie predykcji.generate_chart- zwraca strukturalnyLineChartData(model Pydantic), który frontend renderuje jako wykres Chart.js.
Agent decyduje kiedy użyć Dockera. Jeśli zapytasz “pokaż mi łączną sprzedaż według produktu”, wywołuje query_data - szybko, bez narzutu. Jeśli zapytasz “przewidź sprzedaż Widget Alpha na następne 6 miesięcy”, wywołuje predict - który uruchamia sub-agenta wewnątrz Dockera.
Narzędzie Predict: Sub-agent z laboratorium Docker
To jest główny wzorzec. Narzędzie predict nie wykonuje kodu samo - deleguje do sub-agenta, który ma pełny dostęp do kontenera Docker:
@analytics_agent.toolasync def predict( ctx: RunContext[AnalyticsDeps], task_description: str,) -> str: """Run a prediction using Python in a Docker sandbox.""" sandbox = ctx.deps.sandbox
# Write sales data into the Docker container sandbox.write("/workspace/sales_data.json", data_content)
# Create a sub-agent with Docker tools console_toolset = create_console_toolset( include_execute=True, require_write_approval=False, require_execute_approval=False, )
sub_agent: Agent[SandboxDeps, str] = Agent( "openai:gpt-4.1", system_prompt="You are a data science code executor...", deps_type=SandboxDeps, toolsets=[console_toolset], )
result = await sub_agent.run( f"Perform this prediction task:\n\n{task_description}", deps=SandboxDeps(backend=sandbox), ) return result.outputCo dzieje się krok po kroku:
- Główny agent otrzymuje: “Przewidź sprzedaż Widget Alpha na następne 6 miesięcy”
- Główny agent wywołuje
predict(task_description="...") - Dane sprzedażowe są zapisywane do kontenera Docker pod
/workspace/sales_data.json - Tworzony jest nowy sub-agent z
create_console_toolset()- dający muls,read,write,executei inne operacje plikowe - Sub-agent pisze skrypt Pythona używając pandas + sklearn
- Sub-agent wykonuje skrypt wewnątrz Dockera
- Wyniki płyną z powrotem do głównego agenta, który wyjaśnia je użytkownikowi
Sub-agent nie ma pojęcia, że jest sub-agentem. Widzi tylko system prompt mówiący “jesteś wykonawcą kodu data science” i narzędzia do czytania/pisania/wykonywania plików. Sandbox Docker jest kompletnie przezroczysty.
Strukturalne wykresy z Pydantic
Trzecie narzędzie - generate_chart - demonstruje strukturalne wyjście. Zamiast zwracać surowy tekst, zwraca model Pydantic:
class DataPoint(BaseModel): x: str # e.g. "2024-01" y: float
class ChartSeries(BaseModel): name: str data_points: list[DataPoint]
class LineChartData(BaseModel): title: str x_label: str y_label: str series: list[ChartSeries]Narzędzie generate_chart pobiera parametry wykresu od LLM i zwraca zserializowany LineChartData ze specjalnym prefiksem (CHART_DATA:). Serwer przechwytuje ten prefiks w strumieniu WebSocket i wysyła go do frontendu jako wiadomość chart_data:
if result_str.startswith(CHART_DATA_PREFIX): chart_json = result_str[len(CHART_DATA_PREFIX):] await websocket.send_json( {"type": "chart_data", "data": json.loads(chart_json)} )Frontend odbiera to i renderuje z Chart.js. Żadnych obrazów, żadnego base64, żadnego matplotlib - tylko strukturalne dane płynące od agenta do przeglądarki.
Pytanie o środowisko: Narzędzie vs. Domyślne
To jest pytanie projektowe, które wspomniałem na początku. Są dwa sposoby na danie agentowi środowiska do wykonywania kodu:
Opcja A: Środowisko jako narzędzie (co zbudowaliśmy)
Agent żyje poza Dockerem. Ma narzędzie predict, które deleguje do sub-agenta wewnątrz Dockera. Agent decyduje kiedy go użyć.
Opcja B: Domyślne środowisko (jak Claude Code) Agent żyje wewnątrz Dockera. Każda komenda, którą uruchamia, każdy plik, który czyta - wszystko jest w sandboxie. Środowisko jest zawsze dostępne.
Kiedy każde z nich ma sens:
| Środowisko jako narzędzie | Domyślne środowisko | |
|---|---|---|
| Najlepsze do | Zadania domenowe (predykcje, analiza danych, code review) | Agenci do kodowania ogólnego przeznaczenia |
| Kontrola agenta | Agent decyduje kiedy użyć sandboxa | Agent zawsze działa w sandboxie |
| Narzut | Płaci koszt Dockera tylko gdy potrzebny | Zawsze uruchomiony |
| Elastyczność | Może swobodnie mieszać narzędzia | Wszystko przechodzi przez sandbox |
| Złożoność | Wymaga wzorca delegacji sub-agenta | Prostsze - agent po prostu ma narzędzia |
Dla naszego demo analityki predykcyjnej, Opcja A jest wyraźnie właściwa. Agent głównie odpowiada na pytania o dane (Docker nie jest potrzebny) i uruchamia Dockera tylko gdy musi wykonać kod sklearn. Uczynienie Dockera domyślnym środowiskiem dodałoby niepotrzebne opóźnienie do każdej interakcji.
Ale dla agenta kodującego jak Claude Code, Opcja B ma sens - całym zadaniem agenta jest czytanie, pisanie i wykonywanie kodu. Środowisko jest produktem.
Realne wyniki
Oto co demo faktycznie produkuje. Zapytaj go o “analizę sezonowych wzorców Widget Beta i predykcję na następne 12 miesięcy”:
Sub-agent wybrał wygładzanie wykładnicze Holta-Wintersa (odpowiednie dla danych sezonowych), uruchomił je wewnątrz Dockera i zwrócił strukturalne predykcje. Główny agent następnie wywołał generate_chart z danymi historycznymi i prognozowanymi jako osobne serie.
Cały przepływ - od wiadomości użytkownika do wyrenderowanego wykresu - odbywa się przez pojedyncze połączenie WebSocket ze strumieniowaniem w czasie rzeczywistym tekstu, wywołań narzędzi i danych wykresów.
Protokół strumieniowania WebSocket
Serwer używa agent.iter() z Pydantic AI do strumieniowania w czasie rzeczywistym. Każdy token modelu, wywołanie narzędzia i wynik narzędzia jest strumieniowany do frontendu:
async with analytics_agent.iter( user_message, deps=deps, message_history=message_history,) as run: async for node in run: if Agent.is_model_request_node(node): # Stream text deltas and tool call deltas async with node.stream(run.ctx) as stream: async for event in stream: if isinstance(event, PartDeltaEvent): if isinstance(event.delta, TextPartDelta): await ws.send_json({ "type": "text_delta", "content": event.delta.content_delta }) elif Agent.is_call_tools_node(node): # Stream tool execution events ...Frontend pokazuje karty narzędzi, które rozwijają się, żeby pokazać argumenty i wyniki, tekst strumieniowany token po tokenie i wykresy renderowane inline - wszystko przez jeden WebSocket.
Jeden gotcha: Chart.js multi-serie z różnymi zakresami
Natrafiliśmy na ciekawy bug podczas developmentu. Przy rysowaniu “Historical” (2024-01 do 2025-12) obok “Forecast” (2026-01 do 2026-06), Chart.js używał tylko etykiet z pierwszej serii. Punkty prognozy były mapowane na historyczne daty.
Rozwiązanie: połącz wszystkie unikalne etykiety x ze wszystkich serii, a potem użyj mapy lookup per seria z null dla brakujących dat:
const allLabels = [...new Set( chartData.series.flatMap((s) => s.data_points.map((dp) => dp.x)))].sort();
const datasets = chartData.series.map((s, i) => { const lookup = new Map(s.data_points.map((dp) => [dp.x, dp.y])); return { label: s.name, data: allLabels.map((x) => lookup.get(x) ?? null), spanGaps: false, // ...styling };});Mała rzecz, ale to ten rodzaj buga, który sprawia, że twoja prognoza wygląda kompletnie źle, podczas gdy dane są faktycznie poprawne.
Kluczowe wnioski
- “Środowisko jako narzędzie” to właściwy wzorzec, gdy twój agent tylko czasami potrzebuje wykonania kodu. Nie płać narzutu Dockera przy każdej interakcji.
- Delegacja do sub-agenta utrzymuje głównego agenta w czystości. Główny agent opisuje co przewidzieć. Sub-agent wymyśla jak napisać kod Pythona.
- Strukturalne wyjście Pydantic do wykresów bije generowanie obrazów. Wyślij dane, pozwól frontendowi renderować. Łatwiejsze do stylowania, interaktywne i bez blobów base64.
- Strumieniowanie WebSocket z
agent.iter()daje wgląd w czasie rzeczywistym w to, co agent robi - tekst, wywołania narzędzi i wyniki. - Najtrudniejsza część to nie agent - to integracja frontendowa (łączenie serii wykresów z różnymi zakresami dat, przechwytywanie wyjść narzędzi, strumieniowanie przez WebSocket).
Wypróbuj sam
pydantic-ai-backend - sandbox Docker, console toolset i abstrakcje backendowe dla agentów Pydantic AI
Pełne demo jest w examples/predictive_analytics/:
pip install pydantic-ai-backend[docker,console]export OPENAI_API_KEY=your-keyuvicorn examples.predictive_analytics.server:app --port 8000Powiązane artykuły
Od create-react-app do create-ai-app: Nowy standard dla aplikacji AI
W 2016 roku create-react-app ustandaryzował budowanie frontendów. W 2026 roku aplikacje AI potrzebują tego samego moment...
AGENTS.md: Jak przygotować repozytorium dla agentów AI (Copilot, Cursor, Codex, Claude Code)
Każde narzędzie AI do kodowania czyta Twoje repozytorium inaczej. Sprawdź, jak AGENTS.md — wschodzący standard — daje im...
Od zera do produkcyjnego agenta AI w 30 minut — szablon full-stack z 5 frameworkami AI
Krok po kroku: konfigurator webowy, wybierz preset, wybierz framework AI, skonfiguruj 75+ opcji, docker-compose up — dzi...