Strona główna / Artykuły

Przepływ pracy lokalnego agenta LLM stworzony z folderów i plików Markdown

Stwórz zamykany, wieloetapowy pipeline lokalnych agentów, w którym każdy etap to folder, prompty są edytowalnymi plikami Markdown, a człowiek sprawdza wyniki przed kosztownym krokiem.

Wydawca: REACTAPP.TOP25 min czytania5861 słów

Źródła: Build Your Own Local LLM Agent Workflow in 400 Lines of Python

Agent lokalny może mieć powtarzalny, sprawdzalny proces pracy bez konieczności używania żadnego frameworka. Tutaj uruchamiany jest proces wieloetapowy, w którym każdy etap to folder, a prompt dla danego etapu to zwykły plik markdown, który można przeczytać i edytować; ponadto człowiek sprawdza wyniki pomiędzy etapami. Sam agent się nie zmienia – zmienia się jedynie sposób przechowywania jego kontekstu, i to jest istotą sprawy.

Dlaczego agent lokalny tego potrzebuje

Agent już obsługuje wyszukiwanie w sieci, czytanie stron oraz działanie poprzez trwałą kartę przeglądarki. Brakowało jednak sposobu na wielokrotne uruchamianie zadań z możliwością sprawdzania ich przez człowieka w połowie procesu.

Powodem było to, że wszystko, co sterowało nim, znajdowało się w kodzie. Jeden prompt systemowy był tworzony podczas importu, a lista narzędzi była budowana przy uruchamianiu i przekazywana modelowi za każdym razem. Aby agent mógł pełnić inną rolę na kolejnym etapie, trzeba było edytować kod w Pythonie. Nie istniało miejsce na zatrzymanie się i sprawdzenie wyników pośrednich przed kosztownym krokiem. Jest to akceptowalne w przypadku pętli rozmów, ale niewygodne przy zadaniach składających się z kilku etapów.

W rezultacie powstała kompaktowa ścieżka badawczo-edukacyjna. Cztery etapy przenoszą pojedyncze zdanie opisujące kurs na wersję roboczą mini-kursu, przy czym po każdym etapie następuje kontrola. Numerowane foldery z zwykłym formatem Markdown zawierają teraz kontekst, który kieruje działaniami agenta, dzięki czemu struktura może być przeczytana i skorygowana przed rozpoczęciem pracy nad projektem, bez konieczności edytowania żadnego kodu.

Struktura opiera się na metodzie zwanej Interpretable Context Methodology, czyli ICM, która zastępuje orkiestrację na poziomie frameworka strukturą systemu plików.

W diagramie przepływ pracy przedstawiono jako folder o nazwie workspace. Każdy numerowany folder w katalogu pipeline/ stanowi etap, plik CONTEXT.md w każdym z tych folderów zawiera odpowiednie instrukcje, a katalog proposed/ służy jako obszar tymczasowy, w którym człowiek ręcznie wprowadza zmiany.

Numerowane foldery pełnią rolę etapów, pliki markdown przechowują informacje specyficzne dla każdego etapu, natomiast lokalne skrypty zajmują się zadaniami mechanicznymi, które nie wymagają użycia modeli. Projekt czerpie inspirację z pipeline’ów w systemie Unix oraz kompilatorów wieloetapowych, gdzie wynik jednego kroku jest podawany jako dane wejściowe dla następnego, a każdy pośredni rezultat to zwykły plik, który można otworzyć.

Czego potrzebujesz do uruchomienia

Dla modelu qwen3.5:9b w Ollamie, przy wyłączonym trybie rozmyślania i temperaturze 0,1, działanie jest możliwe. Przeglądanie i wyszukiwanie wykorzystują wcześniejsze rozwiązania: camofox-browser słucha na porcie 9500, a SearXNG na 8090, oba w środowisku Docker. Zanim rozpoczniesz, uruchom te dwa kontenery wraz z Ollamą i modelem obsługującym wywołania narzędzi.

Budowa dzieli się na pięć etapów:

  • Etap 1 uruchamia camofox-browser i SearXNG w Dockerze – to lokalne usługi są wykorzystywane późniejszymi etapami.
  • Etap 2 tworzy instrukcje systemowe agenta na podstawie plików w formacie markdown, zastępując jedną stałą wartość.
  • Etap 3 dostarcza sterownik, stosując w każdym etapie określone zasady sprawdzania i ograniczenia dotyczące narzędzi.
  • Etap 4 to kontrola planu: jeden etap wykorzystuje dwa wcześniejsze wyniki, a osoba odpowiedzialna zatwierdza plan przed przystąpieniem do kosztownego kroku.
  • W fazie 5 tworzone są projekty dla każdego modułu oraz dodawana jest folder proposed/, który zapewnia miejsce na późniejsze rozszerzenia.
  • Instalacja

    Każda faza znajduje się w osobnym podkatalogu i jest dostępna jako skrypt konsolowy poprzez plik pyproject.toml; dzięki temu po jednej instalacji można uruchomić dowolną fazę podając jej nazwę. Pierwszym krokiem jest sklonowanie repozytorium i jego zainstalowanie w trybie edytowalnym, przy użyciu środowiska wirtualnego:

    git clone https://github.com/jfjensen/local-LLM-agent-icm-workflow.git
    cd local-LLM-agent-icm-workflow
    
    python -m venv .venv
    
    # Windows PowerShell:
    .\.venv\Scripts\Activate.ps1
    
    pip install -e .
    

    Instalacja wprowadza mcp, ollama i httpx, a także rejestruje zestaw skryptów konsolowych. Etapy od 2 do 5 otrzymują odpowiednio uruchamiające procesy mcp-workflow-stage2 do mcp-workflow-stage5 oraz agenty mcp-agent-stage2 do mcp-agent-stage5. Dwa serwery MCP, mcp-search-part3 i mcp-browser-stage5, są kopiowane z wcześniejszych wersji, co zapewnia samowystarczalność repozytorium.

    Konfiguracja znajduje się w pliku config.toml w korzeniu repozytorium, który otrzymuje nowy sekcję [workflow]. Ponieważ każdy etap odczytuje pliki z katalogu workspace/ i zapisuje dane do history/ względem bieżącego katalogu, najprościej jest uruchomić etap bezpośrednio z jego własnej folderu:

    cd stage5
    mcp-workflow-stage5
    

    Etap 1: usługi lokalne

    Tutaj nie jest używany Python. Etap ten uruchamia parę usług, których agent używa podczas wyszukiwania: camofox-browser, ukryty przeglądarka, w której znajdują się narzędzia do czytania, oraz SearXNG, silnik wyszukiwania używany w elementie search. Każdy, kto już ma wcześniejszą konfigurację, może zachować ten sam plik docker-compose.yml.

    cd stage1
    docker compose up -d
    docker compose logs -f
    

    Kilka szczegółów jest warte uwagi:

    • Camofox słucha na porcie 9500, a SearXNG na porcie 8090 – to właśnie porty oczekuje plik config.toml, dzięki czemu agent może je znaleźć bez dodatkowych ustawień.
    • Plik compose dokonuje powiązania i montowania pliku searxng/settings.yml, co aktywuje API w formacie JSON i wyłącza ogranicznik szybkości, dzięki czemu wyszukiwanie działa już przy pierwszym uruchomieniu.
    • Obraz Camofox jest budowany tylko raz, z jego głównego repozytorium. Pierwsze skompilowanie jest wolne, ponieważ plik binarny Camoufox jest duży, a w pliku stage1/README.md opisane są wszystkie kroki.

    Aby sprawdzić stan systemu, użyj curl i wyślij żądania http://localhost:9500/health oraz http://localhost:8090/search?q=ollama&format=json. Oba powinny zwrócić odpowiedź w formacie JSON.

    Etap 1 obejmuje tylko te dwa usługi. Gdy Camofox zacznie odpowiadać na porcie 9500, a SearXNG na porcie 8090, system badawczy będzie miał czym się posłużyć.

    Etap 2: kontekst z plików

    To jest jedyna rzeczywista zmiana w kodzie. Agent wcześniej używał jednego promptu systemowego – ciągu znaków na poziomie modułu tworzonego przy uruchamianiu i wysyłanego bez zmian na każdym kroku, niezależnie od zadania. Pozwolenie mu na pełnienie różnych ról na poszczególnych etapach oznacza, że prompt musi pochodzić z edytowalnego źródła, a odczytywanie go z plików jest najprostszą opcją.

    Zatem pierwsza zmiana umożliwia przejęcie kontroli nad promptem systemowym:

    def build_messages_for_model(self) -> list[dict[str, Any]]:
        system = self.system_text if self.system_text is not None else SYSTEM_PROMPT
        return [{"role": "system", "content": system}] + self.messages
    

    Zachowanie jest proste:

    • Gdy nie ma ustawionego system_text, agent korzysta z tego samego, sztywno zdefiniowanego SYSTEM_PROMPT co wcześniej, więc interaktywny agent mcp-agent-stage2 działa dokładnie tak samo jak wcześniej.
    • Gdy system_text jest ustawiony, staje się on systemowym promptem dla danej sesji. Uruchamiający procesy workflow ustawia go na każdym etapie na podstawie plików, a klasa agenta w pozostałych przypadkach pozostaje nienaruszona.

    Jeśli chodzi o same pliki, przestrzeń robocza ICM to nic innego jak folder. stage2/workspace/ zawiera plik CLAUDE.md opisujący przestrzeń roboczą i jej układ, plik CONTEXT.md wymieniający etapy w kolejności, katalog _config/ z materiałami referencyjnymi, które pozostają niezmienne pomiędzy uruchomieniami, oraz katalog pipeline/ z numerowanymi etapami. Każdy katalog etapu ma swój własny plik CONTEXT.md oraz katalog output/.

    Dwa pliki najwyższego poziomu określają tożsamość i ścieżki przekierowań. CLAUDE.md stanowi warstwę 0 – to pierwszy element, który czyta agent:

    # Workspace: course-research
    
    You are a single agent operating inside an ICM workspace. This file tells
    you where you are and how the workspace is laid out. Read it first.
    
    This workspace turns a one-line course idea into a drafted mini-course,
    through four stages that run one at a time with a human review between each.
    
    Layout:
    
    - `CONTEXT.md` (next to this file): the workspace routing. It lists the
      stages in order and the shared resources they draw on.
    - `_config/`: shared reference material that stays the same across runs
      (the audience profile, the writing voice). This is the factory.
    - `pipeline/`: the numbered stages. Each stage folder holds a `CONTEXT.md`
      contract, an `output/` folder for what that stage produces, and
      sometimes a `references/` folder for stage-local reference material.
    - `pipeline/proposed/`: a staging area, not a stage. A human moves
      improvements from here into `_config/` once they are happy with them.
    
    You only ever work on one stage at a time. The stage's own `CONTEXT.md`
    tells you exactly what to read, what to do, and what to write. Do not read
    files the stage contract does not list.
    

    Obok niego CONTEXT.md stanowi warstwę 1, czyli routowanie, które wymienia etapy:

    # Routing
    
    The course-research pipeline runs in four stages, in this order:
    
    1. `01-scope`: turn the one-line course idea and the audience profile into
       a short list of scoped learning objectives.
    2. `02-research`: for each objective, search the web and read sources, and
       write structured notes with links. This is the only stage that uses
       tools.
    3. `03-outline`: turn the objectives and the notes into a module-by-module
       course outline. This is the gate before the expensive stage, so the
       outline is worth a careful human read.
    4. `04-draft`: draft each module from the approved outline and the notes.
    
    Each stage reads from the stage before it (its `output/` folder) and
    writes to its own `output/` folder. A human reviews each output before the
    next stage runs.
    
    Shared resources (in `_config/`, the same every run):
    
    - `audience.md`: who the course is for. Every stage should respect it.
    - `voice.md`: how the course should read. The draft stage follows it.
    

    Materiały referencyjne są przechowywane w katalogu _config/, konfigurowane raz i wykorzystywane przy każdym uruchomieniu. Poniżej znajduje się plik audience.md, plik z poziomu 3, który jest czytany na wszystkich etapach: określania zakresu, badań oraz tworzenia szkicu:

    # Audience
    
    The course is for working software developers who are comfortable with
    Python and the command line, but who have not run a language model on
    their own machine before.
    
    They want practical, hands-on knowledge they can apply the same day, not
    a survey of the field. Assume they can install software, edit a config
    file, and read a stack trace. Do not assume they know any machine-learning
    theory, model formats, or GPU terminology.
    
    Keep examples concrete and runnable. Prefer one worked example over three
    abstract options. When a trade-off matters, state it plainly and give a
    default.
    

    Kontrakt 01-scope/CONTEXT.md stanowi istotę projektu. To pełna wersja dla pierwszego etapu pipeline’u:

    # Stage: 01-scope
    
    ## Inputs
    - Layer 4 (working): input.md
    - Layer 3 (reference): ../../_config/audience.md
    
    ## Tools
    none
    
    ## Process
    Read the one-line course idea and the audience profile. Produce a short
    list of scoped learning objectives for the course, four to six of them.
    
    Each objective should be one sentence, start with a verb, and describe
    something the learner will be able to do by the end. Keep the scope tight:
    this is a short course, so leave out anything that does not fit a learner
    going from zero to a working local model. Do not pad the list to hit a
    number. Fewer, sharper objectives are better.
    
    Write the objectives as a numbered markdown list, nothing else.
    
    ## Outputs
    - objectives.md -> output/
    

    Jedyny wprowadzony materiał na poziomie warstwy 4, 01-scope/input.md, stanowi materiał roboczy dla tego kursu – to zwięzła idea, która zostaje przekształcona w pełny kurs:

    # Course idea
    
    A short, practical course that takes a developer from zero to running a
    local large language model on their own machine with Ollama, and calling
    it from a small Python script.
    

    Kilka uwag na temat sposobu czytania umowy:

    • Tabela „Wprowadzenia” wymienia dokładne pliki, które jest czytane na danej fazie, i oznacza każdy z nich warstwą: Warstwa 3 dla materiałów referencyjnych, które są stabilne podczas wszystkich wykonywań (profil odbiorców), Warstwa 4 dla materiałów używanych podczas bieżącego wykonywania (idee w formie jednej linijki). W przestrzeni roboczej nie ładowane są żadne inne pliki.
    • Linia „Narzędzia” pokazuje wartość none, ponieważ przekształcanie idei w cele nie wymaga żadnych narzędzi. Faza 3 wraca do tego punktu.
    • Część „Proces” zawiera instrukcje, których musi przestrzegać agent, i stanowią one kolejność działań użytkownika.
    • Linia „Wyniki” podaje nazwę pliku, który jest zapisywany przez daną fazę do jej własnej folderu output/.

    Zgodnie z umową, loader przekształca tabelę Inputs w rzeczywisty zawartość pliku i buduje na jej podstawie kontekst. Materiały referencyjne i materiały robocze pozostają oddzielone, ponieważ pełnią różne role: pierwsze to zbiór reguł, których model powinien przestrzegać, a drugie to surowy dane wejściowe, które należy przekształcić.

    # System text: identity + routing + the reference material (Layer 3).
    parts = []
    if layer0:
        parts.append(layer0.strip())
    if layer1:
        parts.append(layer1.strip())
    refs3 = [r for r in inputs if r.layer == 3 and r.exists]
    if refs3:
        block = ["# Reference material (follow these as constraints)"]
        for r in refs3:
            block.append(f"\n## {r.rel_path}\n\n{r.text.strip()}")
        parts.append("\n".join(block))
    system_text = "\n\n".join(parts).strip()
    

    Zachowanie ładowacza można opisać w następujący sposób:

    • Najpierw przychodzą informacje o tożsamości i routingu (CLAUDE.md, CONTEXT.md), które mówią agencie, gdzie się znajduje i jakie etapy istnieją.
    • Następnie pojawiają się pliki referencyjne warstwy 3 pod nagłówkiem przedstawiającym je jako ograniczenia. Pliki robocze warstwy 4 są zbierane oddzielnie, poniżej tekstu opisującego proces danego etapu, jako to, na czym agent powinien się skupić.
    • Wynikiem jest komenda systemowa dla danego etapu, utworzona na podstawie zwykłych plików, bez żadnych elementów, których etap nie wymagał.

    Zanim uruchomimy cokolwiek wobec modelu, warto sprawdzić, co generuje ładowacz, ponieważ ta część jest deterministyczna. W repozytorium znajduje się mała proba, probe_loader.py, która wyświetla złożony kontekst dla danego etapu wraz z przybliżoną liczbą tokenów i nigdy nie wywołuje modelu. Uruchom ją z foldera stage2 w odniesieniu do pierwszego etapu:

    cd .\stage2\
    python ..\probe_loader.py 01-scope
    

    Zdjęcie ekranu pokazuje pierwsze linie raportu sondy dotyczącego etapu skupienia, wraz z rozmiarem okna kontekstowego. Ten etap kończy się około 900 tokenami, z których każdy jest istotny, co sprawia, że koncepcja ICM staje się namacalna: małe okno skupienia na każdym etapie oraz brak jednego ogromnego promptu zawierającego wszystko.

    Aby faktycznie uruchomić ten etap, użyj mcp-workflow-stage2. Narzędzie to analizuje umowę, uruchamia agenta raz i tworzy plik output/objectives.md. W ramach przykładowej koncepcji, praktycznego krótkiego kursu na temat uruchamiania lokalnego LLM za pomocą Ollamy, otrzymano pięć klarownych celów – takich, które osoba może szybko przejrzeć, a następnie zachować lub usunąć.

    Bannер na zdjęciu ekranu pokazuje kontekst skupienia przewidziany przez sondę, wynoszący blisko 900 tokenów, a rezultatem etapu jest plik objectives.md. Cele po pierwszym uruchomieniu są pokazane poniżej; drugie uruchomienie sformułowałoby je nieco inaczej:

    1. Install the necessary software stack (Ollama) and verify it is successfully pulling and serving a local LLM like `llama3`.
    2. Configure environment variables to securely expose the local model's API endpoint for external access.
    3. Write a Python script using the `requests` library to send prompts to Ollama and parse JSON responses into readable text.
    4. Implement error handling in your script to gracefully manage connection timeouts or out-of-memory errors common with local inference.
    5. Build an interactive loop that allows you to chat with the model directly from a terminal-based Python application without using a GUI framework.
    

    Gdy kontekst pochodzi teraz z plików, brakującym elementem jest uruchamianie etapów w odpowiedniej kolejności oraz zatrzymywanie się pomiędzy nimi.

    Etap 3: sterownik, bramy i określenie zakresu narzędzi

    Przepływ pracy oznacza uporządkowane etapy z punktem kontrolnym wykonywanym przez człowieka pomiędzy nimi. Sterownik przegląda zatem spisane numerami foldery, uruchamia pierwszy etap z pustym wynikiem i zatrzymuje się.

    async def run_pipeline(workspace_dir: Path, run_all: bool = False) -> None:
        stages = stage_dirs(workspace_dir)
        for sd in stages:
            if stage_is_done(sd):
                print(f"[skip] {sd.name}: output already present")
                continue
            await run_stage(workspace_dir, sd.name)
            if run_all:
                continue
            # The review gate. Stop here so the human can read and edit the
            # output before the next stage reads it.
            print("\n--- review gate ---")
            print(f"Review and edit if needed:\n  {sd / 'output'}")
            return
    

    Sposób działania:

    • Stan przechowywany jest na dysku. Etap uważa się za zakończony, gdy jego folder output/ zawiera rzeczywisty plik, więc nie ma oddzielnego magazynu stanów. System plików jest tym stanem.
    • Brama to po prostu wczesne zakończenie działania. Gdy etap się zakończy, sterownik podaje, gdzie szukać dalej, i zatrzymuje się. Czytasz wynik, zmieniasz go w razie potrzeby i uruchamiasz ten sam polecenie ponownie, aby kontynuować.
    • --all to opcja umożliwiająca przejście bez zatrzymywania się, stosowana wtedy, gdy ufasz wynikowi. Domyślnie działanie jest kontrolowane bramami, ponieważ one stanowią istotę tego mechanizmu.

    Aby uruchomić pipeline, użyj mcp-workflow-stage3, przeczytaj to, co napisał, a następnie uruchom go ponownie, aby przejść dalej. Pierwsze uruchomienie obejmuje 01-scope i zatrzymuje się; drugie obejmuje fazę badawczą i również się zatrzymuje, i tak dalej.

    Każde wywołanie obsługuje jeden etap, podaje informacje o miejscu poszukiwań i zatrzymuje się przy bramie sprawdzającej; ponowne uruchomienie tego samego polecenia wznowia proces.

    Zakres narzędzi

    Badania to jedyny etap, w którym wykorzystywane są narzędzia, i to właśnie tam pojawiły się pierwsze poważne problemy. Agent łączy się z serwerami wyszukiwania i przeglądarki, a wcześniejsza wersja łączyła wszystkie narzędzia z każdego serwera w jedną prostą listę i prezentowała całą listę przy każdym kroku. Było to około piętnastu narzędzi, z których badania potrzebują mniej więcej pięciu. Pozostałe to narzędzia do edycji kartki i wykonywania działań, których badania w ogóle nie używają.

    Chociaż dostępnych jest piętnaście modeli, model 9B został przypisany do narzędzi interakcji. Wybrał open_tab i read_tab zamiast narzędzi do czytania, przy czym read_tab zwraca jedynie górną część strony. Dokumentacja Ollamy jest uruchamiana na Mintlify, więc ta górna część jest identyczna wszędzie – znajduje się tam ten sam tekst nawigacyjny. Model nadal czytał ten menu, otwierając kolejne karty, aż limit wywołań zakończył jego działanie, wyrzucając jako wynik przeprosiny składające się z 143 znaków.

    Rozwiązaniem jest przypisanie narzędzi do poszczególnych etapów. Umowa określa, jakie narzędzia są potrzebne, a w czasie wykonywania programu udostępniane są tylko one. Oto pełna umowa badawcza:

    # Stage: 02-research
    
    ## Inputs
    - Layer 4 (working): ../01-scope/output/objectives.md
    - Layer 3 (reference): ../../_config/audience.md
    - Layer 3 (reference): references/tool-use.md
    
    ## Tools
    search-server: search
    browser-server: summarize, extract, fetch_snippet, fetch_urls
    
    ## Process
    For each learning objective, find and read one or two good current sources,
    then write short, factual notes the outline and draft stages can rely on.
    
    Work objective by objective. For each one: run a web search, then read the
    most promising result with `summarize` or `extract`. Use `summarize(url,
    question=...)` for an open question about a page, and `extract(url, schema)`
    when you can name the fields you want (a version number, a default port).
    These read the whole page; do not try to page through a site by hand. Read
    each page at most once and do not re-read a page you have already read. Once
    you have one or two sources per objective, stop calling tools and write the
    notes. Pull out the concrete facts that matter for teaching this objective
    (commands, version notes, default ports, common pitfalls). Prefer official
    documentation and project sources over blog aggregators.
    
    Write the notes as markdown, grouped under a heading per objective. Under
    each heading, a few bullet points of facts, then a "Sources:" line with
    the URLs you actually read. Keep it factual. Do not draft course prose here;
    that happens later.
    
    ## Outputs
    - notes.md -> output/
    

    Nowe nazwy sekcji Tools to search dla serwera wyszukiwania oraz zaledwie cztery narzędzia z serwera przeglądarki, wszystkie typu czytnik. Czas wykonywania aplikacji stosuje to jako filtr na liście narzędzi przed tym, jak model ją otrzyma:

    def rebuild_ollama_tools(self, allowed: dict[str, set[str] | None] | None = None):
        out = []
        self._tool_to_server = {}
        for server_name, tools in self.mcp_tools_by_server.items():
            allow_set = allowed.get(server_name) if allowed else None
            for tool in tools:
                if allow_set is not None and tool.name not in allow_set:
                    continue
                prefixed = f"{server_name}_{tool.name}"
                self._tool_to_server[prefixed] = server_name
                out.append({
                    "type": "function",
                    "function": {
                        "name": prefixed,
                        "description": tool.description or "",
                        "parameters": tool.inputSchema,
                    },
                })
        self.ollama_tools = out
    

    Filtr działa w następujący sposób:

    • Mapa dopuszczeń jest specyficzna dla każdego serwera. Wartość None oznacza wszystkie narzędzia serwera, natomiast lista nazw oznacza tylko te określone narzędzia.
    • Każde narzędzie spoza listy dopuszczonych jest odrzucane, więc model nigdy go nie widzi. Moduł research widzi pięć narzędzi, podczas gdy moduły scope, outline i draft nie widzą żadnych.
    • Mapa jest budowana krok po kroku, więc ukryte narzędzie również nie może zostać wywołane; fałszywe wezwanie open_tab zostanie po prostu zwrócone jako nieznane narzędzie.

    Kontrakt badawczy dostarcza ponadto własny dokument referencyjny Layer 3, references/tool-use.md, dostosowany do tej fazy, który wyjaśnia, jak korzystać z pozostałych czytników:

    # Tool use (research stage only)
    
    You have two MCP servers for this stage. Use them like this.
    
    - `search-server_search(query, max_results)`: a web search. Start here for
      any lookup. It returns URLs with titles and snippets. After a search,
      your next action must be a browser-server call on the most promising URL.
      Do not stop after a search and do not answer from the snippets alone.
    
    The browser-server reader tools each return a small slice of a page, not
    the raw page:
    
    - `browser-server_fetch_snippet(url)`: the head of a page, for a quick look.
    - `browser-server_fetch_urls(url)`: the page's links, to decide what to read next.
    - `browser-server_fetch_structure(url)`: the page's heading outline.
    - `browser-server_extract(url, schema)`: pull named fields you can list in
      advance (a version number, a default port) using a JSON Schema.
    - `browser-server_summarize(url, question="")`: a prose summary of a whole
      page; pass `question` to focus it on what this objective needs.
    
    Prefer `extract` when you know the field names, `summarize` for an open
    question about a long page, and `fetch_snippet` for a quick confirmation.
    Prefer official documentation and project sources over blog aggregators.
    

    Jeszcze raz system plików pełni rolę ramy roboczej. W ramie roboczej zestaw narzędzi agenta to kod; tutaj jest to jedna linijka w formacie Markdown, którą może edytować każdy. Gdy czytniki były włączone, a narzędzia typu tab wyłączone, modele summarize i extract, które przeczytały całe strony za pomocą czytnika działającego w partiach, oraz notatki zawierały rzeczywiste ustalenia: polecenie instalacji, domyślną port 11434 oraz informacje o sposobie używania biblioteki Python, przy czym każde z tych elementów miało link do źródła.

    Gdy model nie potrafi przestać

    Kolejny problem był subtelniejszy. Wyposażony we właściwe narzędzia model czytał dobre strony, ale nigdy nie rozpoznawał, kiedy należy skończyć. Podczas jednego przebiegu tej samej strony była podsumowywana z tą samą pytaniem ponad dziesięć razy, bez żadnego nowego postępu, aż osiągnięto limit liczby wywołań. Użyteczne ustalenia znajdowały się w historii wiadomości, ale ponieważ nie napisano ostatecznej odpowiedzi, wynikiem ponownie były przeprosiny.

    Aby to rozwiązać, program teraz wykonuje dodatkowy krok za każdym razem, gdy runda zostaje przerwana: ostatnie wezwanie bez użycia narzędzi, polecające modelowi napisanie odpowiedzi na podstawie tego, co zgromadził.

    async def _finalize(self, stop_reason: str) -> dict:
        if not self.synthesize_on_stop:
            self.messages.append({"role": "assistant", "content": stop_reason})
            return {"role": "assistant", "content": stop_reason}
        self.messages.append({
            "role": "user",
            "content": ("Stop using tools now. Using only the information you have "
                        "already gathered above, write the final output the task "
                        "asked for. Do not call any tools."),
        })
        resp = ollama.chat(model=MODEL_NAME, messages=self.build_messages_for_model(),
                           options={"temperature": MODEL_TEMPERATURE}, think=MODEL_THINKING)
        msg = resp["message"]
        content = msg.get("content", "") if isinstance(msg, dict) else getattr(msg, "content", "")
        final = {"role": "assistant", "content": content or stop_reason}
        self.messages.append(final)
        return final
    

    Trzy szczegóły są istotne:

    • Jest to opcjonalne. Interaktywny agent pozostawia synthesize_on_stop wyłączone i po prostu zgłasza zatrzymanie, tak jak wcześniej, podczas gdy proces roboczy je włącza, aby każdy etap zawsze generował rzeczywisty wynik.
    • Pomaga to w kontynuowaniu pracy. Wyniki narzędzi znajdują się już w historii, więc wywołanie bez użycia narzędzia może zapisać z nich notatki, przekształcając nieudany proces w użyteczny plik notes.md zamiast przeprosin.
    • Pomaga mechanizm ochrony przed utknięciem w pętli. To samo wywołanie powtórzone trzy razy z rzędu kończy proces wcześniej, niezależnie od tego, czy wystąpił błąd, dzięki czemu model mniej czasu spędza na przetwarzaniu przed przejęciem kontroli przez mechanizm syntezy.

    Czysty proces badawczy trwał około siedmiu minut i dał nieco ponad 10 000 znaków notatek pochodzących z źródeł dla pięciu celów.

    Zdjęcie ekranu pokazuje zakończenie drugiego przebiegu narzędzia oraz prośbę o sprawdzenie uzyskanego wyniku. Dzięki wdrożeniu mechanizmów kontrolnych, określeniu zakresu pracy oraz procedur naprawczych badania są na tyle wiarygodne, by można było na nich budować dalej, a następny etap ma największe znaczenie.

    Etap 4: etap opracowania szkicu

    Ze wszystkich etapów to właśnie opracowanie szkicu wymaga najwięcej uwagi, ponieważ zaraz po nim następuje faza redagowania, która jest najdroższym krokiem. Błąd wykryty na etapie szkicu jest najłatwiejszy do poprawienia. W tym etapie łączy się dwa wcześniejsze wyniki – cele z pliku 01-scope oraz notatki badawcze z pliku 02-research – w szkic zorganizowany według modułów. Umowa stanowi:

    # Stage: 03-outline
    
    ## Inputs
    - Layer 4 (working): ../01-scope/output/objectives.md
    - Layer 4 (working): ../02-research/output/notes.md
    - Layer 3 (reference): ../../_config/audience.md
    
    ## Tools
    none
    
    ## Process
    Turn the objectives and the research notes into a module-by-module course
    outline. One module per objective is a reasonable default, but merge or
    split where the notes suggest it.
    
    For each module, give a short title and three to five bullet points naming
    what it covers, drawn from the notes. Order the modules so each one builds
    on the one before it. Note any module that depends on something from an
    earlier module.
    
    This outline is the last checkpoint before the course is drafted, so make
    it easy for a human to read and correct. Write it as markdown headings with
    bullet points. Do not draft the module text yet.
    
    ## Outputs
    - outline.md -> output/
    

    To te dwie linie warstwy 4 są nowością. Umożliwiają one realizację koncepcji wieloetapowej przekazywania danych z opisu w artykule ICM, ponieważ wynik jednego etapu trafia do następnego, który łączy dwa wcześniejsze rezultaty. Narzędzie do ładowania pobiera oba wyniki jako materiał roboczy, a model tworzy z nich szkic.

    Zainstaluj to za pomocą mcp-workflow-stage4. Ponieważ zakres prac i badania zostały zakończone, proces uruchamia 03-outline, zapisuje plik outline.md i zatrzymuje się w określonym punkcie.

    Uruchomienie mcp-workflow-stage4 generuje szkic i zatrzymuje się w tym punkcie, który jest ostatnią tanim miejscem na korektę planu przed jego ostatecznym sformułowaniem.

    Brama natychmiast udowodniła swoją wartość. Podczas jednego przebiegu zbadano zbyt dużo danych. Kurs jest skierowany do osób, które przechodzą od zera do modelu do biegania oraz krótkiego skryptu, jednak notatki zawierały informacje o kluczach API chmury i kwantyzacji pamięci KV, które model natrafił w dokumentacji i wiernie zapisał. Plan nauczenia odziedziczył to i powstał cały moduł zatytułowany „Zabezpieczanie połączenia za pomocą zmiennych środowiskowych”, pełen treści, których początkujący nie potrzebują.

    W tym przypadku nic nie poszło nie tak w procesie pracy. Nadmiar informacji pojawił się jako czytelny nagłówek w pliku tekstowym, zanim napisano jakikolwiek tekst kursu. Wystarczy usunąć moduł, przekształcić jego lekcje lub skierować je gdzie indziej, a przy opracowywaniu nie traci się na nim czasu. Naprawa polega na pojedynczej edycji pliku markdown, podczas gdy po opracowaniu trzeba by ponownie przeczytać pięć modułów i liczne lekcje, aby znaleźć ten, który nie powinien tam być.

    Przykładowy plan jest dostępny do edycji w VS Code, gdzie moduły lub lekcje poza zakresem są usuwane, zanim etap projektowania je w ogóle zobaczy.

    Na tym etapie skróć plan do wymaganych modułów i lekcji, a następnie uruchom proces ponownie. Liczba modułów to po prostu liczba nagłówków ## Module w pliku, więc redukcja planu określa również długość kursu.

    Etap 5: tworzenie projektów dla poszczególnych modułów

    Na tym etapie każdy moduł jest pisany na podstawie zatwierdzonego planu, przy użyciu informacji z notatek. Pierwsza próba polegała na napisaniu całego kursu za jednym razem, ale nie powiodła się – w niektórych przypadkach powstały cztery moduły, w innych jeden, po czym ogłoszono zwycięstwo. Mały model traktuje „napisanie całego kursu” jako jedno ogromne zadanie i ma tendencję do wcześniejszego zatrzymania się.

    Dlatego etap projektowania przechodzi iteracyjnie przez poszczególne moduły. Jego pełny schemat obejmuje instrukcję For each, która definiuje pętlę:

    # Stage: 04-draft
    
    ## Inputs
    - Layer 4 (working): ../02-research/output/notes.md
    - Layer 3 (reference): ../../_config/voice.md
    - Layer 3 (reference): ../../_config/audience.md
    
    ## Tools
    none
    
    ## For each
    module in ../03-outline/output/outline.md
    
    ## Process
    Draft the one module shown below, and only that module. Use the research
    notes for the facts (commands, ports, pitfalls) rather than inventing them,
    and follow the voice and audience guides. Write a few short paragraphs of
    teaching prose plus any commands the learner should run. Do not write the
    other modules. Start your output with the module's heading, and output only
    the drafted module, nothing else.
    
    This is a first draft for a human to revise, so aim for a clear, correct
    scaffold rather than a finished, polished module.
    
    ## Outputs
    - draft.md -> output/
    

    Kroku projektowania to jedyny etap, w którym jest czytany plik voice.md, ostateczny plik warstwy 3 w przestrzeni roboczej, który określa ton kursu:

    # Voice
    
    Write plainly, as one developer explaining something to another at the
    next desk. Short instructional sentences, followed by a longer one that
    explains why.
    
    - Lead with what to do, then explain the reasoning.
    - Use concrete names: real tools, real commands, real file paths.
    - Hedge honestly when something is a judgement call or can fail.
    - No marketing language. No hype. No "unlock", "seamless", "robust".
    - One idea per paragraph. Bullets only for listing the parts of a thing.
    

    Wiersz For each jest tym, co przekształca jeden nieograniczony etap w pętlę, a system wykonuje proces raz na moduł:

    fe = ctx.for_each
    print(f"  for each: {len(fe.modules)} {fe.item}(s) from {fe.rel_path}")
    pieces = []
    for i, (heading, body) in enumerate(fe.modules, 1):
        print(f"  [{i}/{len(fe.modules)}] {heading}")
        agent.messages = []
        item_block = (f"# The {fe.item} to write now\n\n"
                      f"## {heading}\n\n{body}")
        agent.messages.append({"role": "user", "content": ctx.user_text + "\n\n" + item_block})
        final = await agent.run_turn()
        pieces.append((final.get("content", "") or "").strip())
    content = "\n\n".join(p for p in pieces if p)
    

    To, co robi pętla:

    • Schemat jest dzielony na moduły przy nagłówkach ##, a pętla jest wykonywana raz dla każdego z nich. Liczba modułów nie jest ustalona na pięć; cztery moduły oznaczają cztery iteracje, a sześć – sześć, zgodnie z tym, co przeszło filtr.
    • Każde wezwanie rozpoczyna się od czystej tablicy. agent.messages jest co raz opróżniany, dzięki czemu moduł jest tworzony na podstawie dostępnych notatek bez odniesień do pozostałych modułów, co sprawia, że każda iteracja jest krótka i samodzielna.
    • Wyniki są łączone w jeden plik draft.md. Jedno ograniczone wezwanie na moduł, zamiast jednej prośby o „napisanie kursu”, pozwala modelowi 9B na ukończenie ich wszystkich.

    Wytyczna jest ogólna i nie uwzględnia specjalnych przypadków przy tworzeniu projektu. Na każdym etapie można użyć instrukcji For each, chociaż pozostałe trzy etapy nie mają takiej instrukcji i są wykonywane jako jedna tura. Uruchomienie mcp-workflow-stage5 pokazuje postęp od [1/5] do [5/5], a przykładowe uruchomienie wygenerowało nieco ponad 15 000 znaków w pięciu modułach.

    Etap projektowania przechodzi przez strukturę planu, wykonywając po jednej próbie z modelu na każdy moduł. Liczba tych prób odpowiada liczbie pozostałych elementów w planie, więc cztery lub sześć modułów oznacza odpowiednio cztery lub sześć rund wykonywania.

    Przestrzeń robocza zawiera również folder proposed/, którego pipeline nigdy nie wykonywa. Jest to obszar tymczasowy: gdy jakaś sesja zasugeruje ulepszenie, które powinno przetrwać ją samą – na przykład bardziej precyzyjny cel lub reguła głosowa, która ciągle się psuje – sugestia jest tam zapisywana jako plik, a osoba może przenieść ją do foldera _config/, jeśli się z tym zgodzi. Tutaj decydującą rolę odgrywa człowiek. Nic nie zmienia przestrzeni roboczej, dopóki ktoś ręcznie nie zatwierdzi zmiany, co zapewnia przegląd zgodnie z intencją ICM i daje późniejszej automatyzacji możliwość podłączenia się.

    Co działa, a co nie

    Model 9B tworzący moduły dostarcza przydatne ramy, ale nie gotowe materiały dydaktyczne. Zaletą jest przepływ pracy i jej interpretowalność, natomiast model lokalny generujący kurs bez nadzoru nie spełnia tego celu.

    Występują rzeczywiste problemy widoczne podczas sesji:

    • W projekcie powstała błędna jednostka systemd, w której linie [Unit] i [Service] połączyły się w jedną, a zmienne środowiskowe zostały wstawione do pola ExecStart.
    • Pojawiła się strona z numerem wersji, której nie ma w żadnych notatkach, a nazwy modeli zmieniały się pomiędzy modułami gemma2, gemma3 i gemma3:2b.
    • Ponieważ każdy moduł został napisany niezależnie i bez znajomości pozostałych, moduły 1 i 2 znacznie się pokrywały – każdy z nich zajmował się zmianami środowiskowymi oraz systemem systemd.
    • Proces analizy trwa około siedmiu minut, ponieważ każda strona przechodzi przez proces fragmentacyjnego podsumowywania; sama FAQ Ollamy zajęła 41 000 znaków w ośmiu fragmentach.
    • Jednym ze źródeł była ściana blokująca boty od Cloudflare. Czytelnik słusznie zauważył, że nie zawiera ona żadnej rzeczywistej treści, więc model przeszedł dalej, nie wymyślając niczego.

    Żadne z tych elementów nie czyni tego agenta deterministycznym procesem, więc każdy, kto go potrzebuje, powinien napisać skrypt. Zaletą jest to, że instrukcje znajdują się w plikach, które osoba może czytać i korygować pomiędzy poszczególnymi etapami, dzięki czemu wstępny projekt może zostać poprawiony już na wczesnym etapie, zanim pojawią się niespodzianki później. Zapis „cztery do sześciu celów” w umowie dotyczącej zakresu pracy jest jedynie wskazówką dla modelu, dopóki plik nie zostanie edytowany, a po edycji staje się faktem. Różnica pomiędzy tym, co model ma tendencję do robienia, a tym, co osoba ustala dokładnie, jest właśnie powodem istnienia tych etapów kontrolnych.

    Wszystko razem

    Ostatecznie dla każdego pipeline istnieje jedna folder pracy obok agenta, zawierająca plik CONTEXT.md opisujący każdy etap oraz folder output/ przechowujący wyniki. Agentem jest ten sam agent wielo-serwerowy co wcześniej, a oprogramowanie sterujące nim – czyli loader, gate-driven driver, mechanizm wybierania narzędzi oraz pętla dla poszczególnych modułów – mieści się w jednym pliku liczącym około 400 linii.

    Podsumowując, składniki to:

    • Przestrzeń robocza, w której kontekst każdego etapu znajduje się w numerowanych folderach z zwykłym formatem markdown, dostępnym do edycji bez konieczności modyfikowania kodu.
    • Loader, który tworzy prompt systemowy dla każdego etapu na podstawie plików, trzymając oddzielnie materiały referencyjne od tych używanych do pracy, w oknie o pojemności około 900 do 1 500 tokenów na etap.
    • Driver z mechanizmem gate’ów, który wykonywa jeden etap, czeka na interwencję użytkownika i kontynuuje pracę, przechowując swój stan na dysku.
  • Ograniczenie narzędzi do poszczególnych etapów, tak aby każdy etap obejmował tylko to, co określono w jego kontrakcie.
  • Rozwiązanie awaryjne, które zapisuje odpowiedź na podstawie zebranych wyników, gdy model kontynuuje badania, dzięki czemu wyjście nigdy nie jest puste.
  • Tworzenie projektu na poziomie poszczególnych modułów, przy czym liczba modułów odpowiada liczbie nagłówków w zatwierdzonej strukturze.
  • Fakt, że przestrzeń robocza to po prostu folder, oznacza, że inna praca wymaga jedynie innego folderu. Repozytorium zawiera drugi taki folder, workspace-rust, przeznaczony dla kursu Rust dla początkujących. Kopiuje on pierwszy folder i różni się tylko dwoma plikami: profilem odbiorców oraz krótkim opisem idei, natomiast wszystkie kontrakty etapów, plik routingu oraz referencje dotyczące używania narzędzi pozostają identyczne pod względem treści. Uruchomienie polecenia mcp-workflow-stage5 --workspace workspace-rust pozwala uzyskać kurs Rust z tej samej ścieżki przetwarzania. To właśnie jest koncepcja ICM dotycząca konfiguracji fabryki, a nie samego produktu.

    Dwa te przestrzenie robocze różnią się tylko plikiem audience.md oraz jednozdaniowym opisem idei: ta sama ścieżka przetwarzania, inny kurs.

    Istnieje możliwość rozszerzenia tego rozwiązania. Sekcja Verify w umowie etapu mogłaby zmusić ten etap do ponownego odczytania wcześniejszego wyniku i zaznaczenia odchyleń przed interwencją człowieka, co w artykule ICM opisano jako weryfikację międzyetapową. Jednozdaniowy podsumowanie poprzednich modułów mogłoby być wykorzystywane przy każdym projekcie, zmniejszając nakładanie się treści modułów 1 i 2. A folder proposed/ mógłby stać się czymś, co sam się wypełnia.

    Dlaczego budować tę lżejszą wersję ręcznie, skoro istnieją CrewAI i LangGraph? Są to dobre narzędzia, a procesy wymagające jednoczesnej pracy agentów, dynamicznego rozgałęziania lub rzeczywistego wdrożenia usprawiedliwiają ich większą wagę. Jednak wiele zadań ma charakter sekwencyjny, może być sprawdzane i powtarzane, więc nie ma potrzeby stosowania takich rozwiązań. Dla takich przypadków najprostsze rozwiązanie już istnieje na każdym komputerze: folder z plikami. Budowanie go najpierw pokazuje również, w których sytuacjach cięższe narzędzia zaczynają się opłacać.