Головна / Статті

Робочий процес локального агента LLM, створений з папок та файлів Markdown

Створіть закриту, багатоступеневу систему обробки запитів місцевими агентами, де кожен етап є окремою папкою, запити формується у форматі Markdown, а людина перевіряє результат перед виконанням дорогого кроку.

Видавець: REACTAPP.TOP25 хв читання5861 слів

Джерела: Build Your Own Local LLM Agent Workflow in 400 Lines of Python

Локальний агент може мати повторюваний та перевіряємий робочий процес без використання будь-яких фреймворків. Тут налаштовується багатоетапна робота, у якій кожен етап є папкою, а інструкція для кожного етапу — звичайним файлом Markdown, який людина може прочитати та відредагувати; крім того, людина перевіряє результат між етапами. Сам агент не змінюється; змінюється лише спосіб зберігання його контексту, і саме в цьому полягає суть.

Чому локальному агенту це потрібно

Агент вже здатний до пошуку в Інтернеті, читання сторінок та виконання дій через постійно відкриту вкладку браузера. Бракувало лише способу для повторного виконання завдань із можливістю перевірки людиною на серединному етапі.

Причиною було те, що все, що керувало ним, знаходилось у коді. Один системний запит формувався під час імпорту, а єдиний список інструментів створювався під час запуску та передавався моделі щоразу під час виконання завдань. Щоб змусити агента виконувати іншу роль на певному етапі, потрібно було редагувати код на Python. Щоб зупинитися та переглянути проміжний результат перед виконанням дорогого за ресурси кроку, не було можливості зупинитися. Це прийнятно для чат-циклу, але незручно для завдань із кількома етапами.

У результаті утворюється компактний конвеєр для створення курсів та досліджень. Чотири етапи передають одне речення, що описує курс, до чернетки міні-курсу; після кожного етапу є етап перевірки. Нумеровані папки у форматі plain markdown тепер містять контекст, який керує агентом, тож можна прочитати та виправити структуру перед початком роботи над чернеткою, при цьому код не змінюється.

Структура ґрунтується на методі під назвою Interpretable Context Methodology, або ICM, який замінює оркестрацію на рівні фреймворку на структуру файлової системи.

У діаграмі робочий процес показаний як папка під назвою workspace. Кожна пронумерована папка внутрішньо у pipeline/ є етапом; файл CONTEXT.md кожного етапу є його інструкцією, а папка proposed/ є тимчасовою зоною, де людина вручну підтверджує зміни.

Пронумеровані папки виконують роль етапів, файли Markdown містять контекст кожного етапу, а локальні скрипти виконують механічну роботу, яка не потребує моделей. Дизайн запозичує елементи з Unix-пайплайнів та багатоетапних компіляторів, де результат одного кроку подається наступному, а кожен проміжний результат — це звичайний файл, який можна відкрити.

Що потрібно для роботи

Для моделі qwen3.5:9b робота в Ollama здійснюється із вимкненим режимом міркувань та температурою 0.1. Функції перегляду та пошуку використовують попередні рішення: camofox-browser працює на порту 9500, а SearXNG — на порту 8090; обидва знаходяться в Docker. Перш ніж починати, необхідно запустити ці два контейнери разом із Ollama та моделлю, яка підтримує виклики інструментів.

Процес створення поділяється на п’ять етапів:

  • На етапі 1 у Docker запускаються camofox-browser та SearXNG — локальні сервіси, які використовуються на наступних етапах.
  • На етапі 2 створюється системний запит агента на основі файлів у форматі markdown, при цьому замінюється один жорстко закодований рядок.
  • На етапі 3 надається драйвер із застосуванням механізмів перевірки та обмежень щодо використання інструментів на кожному етапі.
  • Етап 4 — це етап планування: на ньому один етап використовує два попередні результати, а людина схвалює подальший план перед виконанням дорогого кроку.
  • На 5-му етапі створюються чернетки для кожного модуля, а також додається папка proposed/, яка залишає простір для подальших розширень.
  • Встановлення

    Кожен етап знаходиться у власному підкаталозі та доступний як скрипт консолі через pyproject.toml; тому після одноразового встановлення ви можете запустити будь-який етап за його назвою. Першим кроком є клонування репозиторію та його встановлення у режимі для редагування за допомогою віртуального середовища:

    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 .
    

    Під час встановлення додаються mcp, ollama та httpx, а також реєструється набір скриптів консолі. На кожному з етапів 2–5 створюється виконувач робочого процесу (mcp-workflow-stage2 до mcp-workflow-stage5) та агент (mcp-agent-stage2 до mcp-agent-stage5). Два сервери MCP — mcp-search-part3 та mcp-browser-stage5 — копіюються з попередніх версій, що забезпечує самодостатність репозиторію.

    Конфігурація знаходиться у файлі config.toml у корені репозиторію, до якого додається новий розділ [workflow]. Оскільки кожен етап читає дані з директорії workspace/ та записує історію у директорію history/, відносно поточної директорії, найкраще запускати етап зсередини його власної папки:

    cd stage5
    mcp-workflow-stage5
    

    Етап 1: локальні сервіси

    Тут не використовується Python. На цьому етапі запускається пара сервісів, які агент використовує під час пошуку: camofox-browser — таємничий браузер, у якому розташовані інструменти для читання, та SearXNG — пошуковий двигун під search. Ті, хто вже налаштував систему раніше, можуть залишити той самий файл docker-compose.yml.

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

    Є кілька деталей, які варто знати:

    • Camofox працює на порту 9500, а SearXNG — на порту 8090, які очікує файл config.toml, тому агент може знайти обидва без додаткової налаштування.
    • У файлі compose використовується метод bind-mount для searxng/settings.yml, що увімкнює JSON API та вимикає обмежувач швидкості, тож пошук працює вже під час першого запуску.
    • Зображення Camofox створюється один раз з його вихідного репозиторію. Перший процес створення є повільним через великий розмір бінара Camoufox, а у файлі stage1/README.md описані кроки цього процесу.

    Щоб перевірити стан системи, скористайтеся curl для запиту http://localhost:9500/health та http://localhost:8090/search?q=ollama&format=json. Обидва запити мають повертати відповідь у форматі JSON.

    На першому етапі є лише ці два сервіси. Як тільки Camofox почне працювати на порту 9500, а SearXNG — на порту 8090, у системі буде що використовувати для пошуку.

    Етап 2: контекст з файлів

    Це єдине справжнє зміна в коді. Раніше агент використовував один системний запит — рядок на рівні модуля, який створювався під час запуску та надсилався без змін у кожному кроці, незалежно від завдання. Щоб агент міг виконувати різні ролі на кожному етапі, запит має походити з редаговуваного джерела, і читання його з файлів є найпростішим варіантом.

    Отже, перша зміна дозволяє перевизначити системний запит:

    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
    

    Поведінка є простою:

    • Якщо параметр system_text не встановлений, агент продовжує використовувати той самий жорстко закодований SYSTEM_PROMPT, як і раніше, тож інтерактивний агент mcp-agent-stage2 діє точно так само, як і раніше.
    • Коли параметр system_text встановлений, він стає системним запитом для виконання. Запускач робочого процесу встановлює його для кожної стадії з файлів, а клас агента при цьому залишається незмінним.

    Що стосується самих файлів, робоче простір ICM — це не що інше, як папка. stage2/workspace/ містить CLAUDE.md, у якому описується робочий простір та його структура, CONTEXT.md з переліком етапів у послідовності, каталог _config/ із довідковими матеріалами, які залишаються незмінними між запусками, та каталог pipeline/ з пронумерованими етапами. Кожен каталог етапу має власний файл CONTEXT.md та каталог output/.

    Два верхньорівневі файли визначають ідентичність та шляхи обробки. CLAUDE.md є рівнем 0 — першим елементом, який читає агент:

    # 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.
    

    Поруч з ним CONTEXT.md є шаром 1, тобто мапуванням, яке перелічує етапи:

    # 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.
    

    Спільні матеріали для посилань знаходяться у директорії _config/; вони налаштовуються один раз та використовуються під час кожного запуску. Нижче наведений файл audience.md — це файл рівня 3, який читається на всіх етапах: визначення обсягу, дослідження та складання огляду:

    # 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.
    

    Контракт 01-scope/CONTEXT.md є основою проектування. Це повна версія для першої стадії пайплайну:

    # 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/
    

    Єдиний вхідний матеріал рівня 4, 01-scope/input.md, є робочим матеріалом для проведення курсу — це однорядкова ідея, яка перетворюється на курс:

    # 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.
    

    Деякі примітки щодо способу читання контракту:

    • У таблиці «Вхідні дані» перелічені саме ті файли, які читає кожен етап, і кожен з них позначений шаром: Шар 3 — для матеріалів-довідок, які залишаються стабільними під час кількох виконань (профіль аудиторії), Шар 4 — для робочих матеріалів цього конкретного виконання (однорядкова ідея). Інші файли з простору роботи не завантажуються.
    • У рядку «Інструменти» вказано жодних, оскільки для перетворення ідеї на цілі не потрібні інструменти. На цьому етап 3 повертається до початку.
    • У розділі «Процес» містяться інструкції, яких дотримується агент, і вони стають наступним кроком для користувача.
    • У рядку «Вихідні дані» вказується назва файлу, який етап зберігає у власну папку output/.

    Згідно з контрактом, завантажувач перетворює таблицю вхідних даних на справжній вміст файлу та створює на їх основі контекст. Контрольні дані та робочі матеріали залишаються окремо, оскільки виконують різні функції: перші — це набір правил, яких має дотримуватися модель, а другі — це необроблені вхідні дані, які їй потрібно перетворити.

    # 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()
    

    Поведінка завантажувача можна описати так:

    • Спочатку йдуть інформація про ідентичність та маршрутизацію (CLAUDE.md, CONTEXT.md), які вказують агенту, де він знаходиться та які етапи існують.
    • Далі йдуть файли-довідники 3-го рівня під заголовком, який представляє їх як обмеження. Файли для роботи 4-го рівня збираються окремо, під текстом процесу етапу, як те, на чому має діяти агент.
    • Результатом є запит системи для цього етапу, створений зі звичайних файлів без жодних елементів, яких не вимагав етап.

    Перш ніж виконувати щось із моделлю, варто перевірити, що генерує завантажувач, адже ця частина є детермінованою. У репозиторії є невеликий засіб перевірки — probe_loader.py, який виводить зібраний контекст етапу разом із приблизною кількістю токенів та ніколи не викликає модель. Запустіть його з папки stage2 щодо першого етапу:

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

    На скріншоті показані перші рядки звіту зонду для етапу обмеженого контексту разом із розміром вікна контексту. На цьому етапі утворюється близько 900 токенів, кожен з яких є значущим, що робить ідею ICM конкретною: невелике вікно обмеженого контексту на кожному етапі та відсутність одного величезного запиту, який би містив усе.

    Щоб справді запустити цей етап, використовуйте mcp-workflow-stage2. Він аналізує контракт, запускає агента один раз та створює файл output/objectives.md. Для прикладної ідеї — практичного короткого курсу з запуску локального LLM за допомогою Ollama — було отримано п’ять чітких цілей, які людина може переглянути та потім залишити або видалити.

    На банері на скріншоті показаний обмежений контекст, який прогнозував зонд — близько 900 токенів, а результатом етапу є файл objectives.md. Цілі одного запуску відображаються нижче; другий запуск може сформулювати їх трохи інакше:

    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.
    

    Оскільки тепер контекст надходить з файлів, залишається лише виконати етапи у правильному порядку та зупинятися між ними.

    Етап 3: драйвер, шлюзи та визначення обсягу інструментів

    Робочий процес означає послідовні етапи з людським контрольним пунктом між ними. Тож драйвер сканує пронумеровані папки, виконує перший етап із порожнім результатом та зупиняється.

    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
    

    Як це працює:

    • Стан зберігається на диску. Етап вважається завершеним, коли його папка output/ містить реальний файл, тому окремого сховища стану немає. Сам файлова система є станом.
    • „Брама“ — це просто передчасне завершення виконання. Як тільки етап завершується, драйвер повідомляє, де шукати результат, і зупиняється. Ви читаєте отриманий результат, за потреби змінюєте його та запускаєте ту саму команду знову, щоб рухатися далі.
    • --all — це спосіб виконання без зупинок, коли ви довіряєте результату. За замовчуванням використовується механізм „брам“, оскільки саме він є ключовим елементом.

    Щоб запустити конвеєр, використовуйте mcp-workflow-stage3, прочитайте те, що він записав, а потім запустіть його знову, щоб просунутися далі. Перший запуск обробляє 01-scope та зупиняється; другий — етап досліджень та також зупиняється, і так далі.

    Кожне викликання обробляє одну стадію, повідомляє, де шукати, та зупиняється біля контрольної точки; запуск того самого команди відновлює її роботу.

    Обмеження інструментів

    Дослідження — це єдина стадія, яка використовує інструменти, і саме там з’явилися перші справжні проблеми. Агент підключається до серверів пошуку та браузера, а попередня версія об’єднувала всі інструменти з усіх серверів у один плоский список та пропонувала весь цей список щоразу. Це приблизно п’ятнадцять інструментів, з яких для дослідження потрібно близько п’яти. Решта — це інструменти для роботи з вкладками та діями на сторінці, якими дослідження ніколи не користується.

    Оскільки було запропоновано всі п’ятнадцять інструментів, модель 9B перейшла до роботи з інтерактивними інструментами. Вона обрала open_tab та read_tab замість звичайних читачів, причому read_tab повертає лише верхню частину сторінки. Документація Ollama розміщена на Mintlify, тому ця верхня частина є ідентичною скрізь – там однаковий текст навігації. Модель продовжувала читати цей меню, відкриваючи одну вкладку за іншою, поки ліміт кількості викликів не припинив її роботу, вивівши як результат вибачення довжиною 143 символи.

    Рішенням є обмеження інструментів кожної стадії. У контракті перераховані необхідні інструменти, і під час виконання доступні лише вони. Ось повний дослідницький контракт:

    # 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/
    

    Нові назви розділу Інструменти — search для сервера пошуку та лише чотири інструменти з сервера браузера, усі вони є читачами. Система під час виконання застосовує це як фільтр до списку інструментів ще до того, як модель його отримає:

    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
    

    Фільтр працює так:

    • Мапа дозволів є індивідуальною для кожного сервера. Значення None означає, що дозволені всі інструменти сервера, а набір імен — лише ці конкретні інструменти.
    • Будь-який інструмент, який не входить до списку дозволів, відхиляється, тож модель ніколи його не бачить. У дослідженні зараз враховуються п’ять інструментів, тоді як у функціях scope, outline та draft жодних інструментів немає.
    • Мапа будується поетапно, тому прихований інструмент також не може бути викликаний; спроба виклику open_tab, створена штучно, просто поверне результат як невідомий інструмент.

    Контракт на дослідження також постачає власний документ з посиланнями рівня 3 — references/tool-use.md, призначений лише для цього етапу, у якому пояснюється, як використовувати залишені читачі:

    # 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.
    

    Ще раз файлова система виконує роль фреймворку. У фреймворку набір інструментів агента — це код; тут це просто один рядок у форматі Markdown, який може виправляти будь-хто. З увімкненими читачами та вимкненими інструментами вкладок моделі summarize та extract, які читають цілі сторінки за допомогою розділеного читача, та нотатки містили справжні результати: команду встановлення, стандартний порт 11434 та інструкції щодо використання бібліотеки Python, кожен з посиланням на джерело.

    Коли модель не може зупинитися

    Друга проблема була більш тонкою. Озброєна правильними інструментами модель читала якісні сторінки, але ніколи не розуміла, коли потрібно закінчувати. Під час одного запуску та сама сторінка була узагальнена з тим самим запитанням понад десять разів, без жодного прогресу до тих пір, доки не було досягнуто ліміту кількості викликів. Корисні результати залишалися у історії повідомлень, але оскільки не було написано кінцевої відповіді, результатом знову стало вибачення.

    Щоб вирішити цю проблему, програма тепер виконує додатковий крок щоразу, коли процес обробки переривається: останній запит без використання інструментів, який наказує моделі написати відповідь на основі зібраної нею інформації.

    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
    

    Три моменти є важливими:

    • Це добровільний режим. Інтерактивний агент залишає параметр synthesize_on_stop вимкненим та просто повідомляє про зупинку, як і раніше, тоді як робочий процес його увімкнює, щоб кожен етап завжди записував справжній результат.
    • Це допомагає врятувати роботу. Результати використання інструментів вже знаходяться в історії, тож виклик без інструментів може скопіювати з них примітки, перетворюючи неуспішну спробу на придатний файл notes.md замість вибачень.
    • Захист від застрягання у циклі також допомагає. Один і той самий виклик, повторений тричі поспіль, тепер завершує процес раніше, незалежно від наявності помилок, тож модель менше часу працює перед тим, як візьме керування на себе процес синтезу.

    Чистий етап дослідження тривав близько семи хвилин та дав трохи більше 10 000 символів приміток для п’яти цілей.

    На скріншоті показано, як другий етап виконання інструменту завершується та пропонує переглянути отриманий результат. Завдяки налаштованим критеріям, визначенню меж та процедурам врятування даних дослідження є достатньо надійним для подальшої роботи, і саме наступний крок має найвище значення.

    Етап 4: етап створення огляду

    Серед усіх етапів саме створення огляду вимагає найбільшої уваги, адже за ним слідує розробка проекту, яка є найдорожчим етапом. Помилку, виявлену на етапі огляду, легко виправити. На цьому етапі об’єднуються два попередніх результати – цілі з 01-scope та нотатки з дослідження з 02-research – у огляд, структурований за модулями. У контракті сказано:

    # 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/
    

    Ці дві лінії рівня 4 — це те, що є новим. Вони утверджують концепцію багатоетапної передачі даних з роботи ICM у практичну форму, адже вихід однієї стадії подається наступній, яка об’єднує два попередні результати. Завантажувач бере обидва результати як робочий матеріал, а модель створює огляд.

    Запустіть його за допомогою mcp-workflow-stage4. Оскільки завдання та дослідження вже завершені, процес виконує 03-outline, записує outline.md та зупиняється на контрольному етапі.

    Запуск mcp-workflow-stage4 призводить до створення огляду та зупинки на контрольному етапі — останньому доступному моменті для корекції напрямку перед розробкою деталей.

    Цей гейт одразу довів свою корисність. Під час одного з етапів збору даних було накопичено занадто багато інформації. Курс призначений для тих, хто починає з нуля та створює модель для бігу за допомогою короткого скрипту, проте у нотатках з’явилися дані про ключі API хмарних сервісів та квантовізацію KV-пам’яті, які модель знайшла у документації та вірно записала. Схема курсу успадкувала це та додала цілу розділ під назвою „Забезпечення безпеки вашого з’єднання за допомогою змінних середовища“, наповнену матеріалом, який не потрібен початківцям.

    У робочому процесі тут не виникло жодних проблем. Зайва інформація з’явилась у вигляді читабельного заголовка у звичайному файлі ще до того, як було написано будь-який текст курсу. Достатньо видалити цей розділ, змінити його уроки або перенаправити їх в інше місце, і під час розробки не доведеться витрачати жодного часу на нього. Для виправлення достатньо зробити одну правку у файлі Markdown, тоді як після завершення розробки довелося б перечитати п’ять розділів та численні уроки, щоб знайти той, якого не повинно бути.

    Приклад структури вже доступний для редагування у VS Code на етапі перевірки — там модулі чи уроки, які не входять до сфери дії, видаляються ще до того, як проект потрапить на етап чернетки.

    На цьому етапі скоротіть структуру до необхідних модулів та уроків, а потім запустіть процес знову. Кількість модулів визначається кількістю заголовків ## Module у файлі, тож скорочення структури також впливає на тривалість курсу.

    Етап 5: створення чернетки для кожного модуля

    На цьому етапі кожен модуль створюється на основі схваленої структури, з використанням фактів із нотаток. Перша спроба передбачала створення всього курсу за один раз, але це виявилося неможливим: у деяких випадках формувалося чотири модулі, у інших — лише один, після чого процес вважався завершеним. Невелика модель сприймає завдання „написати весь курс“ як одне величезне завдання та схильна зупинятися рано.

    Тому на етапі чернетки процес обробляє модулі по одному. Його повний алгоритм включає директиву For each, яка оголошує цей цикл:

    # 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/
    

    Етап підготовки проєкту — єдиний, на якому читається voice.md, остаточний файл рівня 3 у робочому просторі, який визначає тон курсу:

    # 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.
    

    Рядок For each є тим, що перетворює безкінечну послідовність операцій на цикл, а система виконання запускає процес один раз на кожен модуль:

    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)
    

    Що робить цей цикл:

    • Огляд розділяється на модулі біля заголовків ##, і цикл виконується по одному разу для кожного з них. Кількість модулів не є фіксованою — п’ять; чотири модулі означають чотири проходження, а шість — шість, що відповідає кількості тих, які пройшли перевірку.
    • Кожен виклик починається з чистого стану. agent.messages щоразу спорожнюється, тому модуль створюється на основі наявних нотаток без згадки про інші модулі, що дозволяє зберігати кожен етап невеликим та самодостатнім.
    • Результати об’єднуються в один файл draft.md. Один обмежений виклик на модуль замість єдиної запиту «написати курс» дозволяє моделі 9B виконати всі завдання.

    Ця інструкція є загальною та не передбачає особливих правил для формулювання. На будь-якому етапі можна використати For each, хоча на трьох інших етапах немає таких інструкцій, тож вони виконуються за один крок. При запуску mcp-workflow-stage5 відображається прогрес від [1/5] до [5/5], а приклад виконання створив трохи більше 15 000 символів у п’яти модулях.

    На етапі розробки обробляється огляд з одним викликом моделі на кожен модуль. Кількість викликів дорівнює кількості модулів у огляді, тож чотири або шість модулів означають чотири або шість виконань.

    Робоче простір також містить папку proposed/, яку конвеєр ніколи не виконує. Це місце тимчасового зберігання: коли певна операція пропонує покращення, яке має залишитися у дії після неї — наприклад, більш чітку мету або виправлення постійних проблем у формулюванні правил голосового керування — ця пропозиція зберігається там у вигляді файлу, а людина переміщує її до папки _config/, якщо погоджується. Тут важлива участь людини. Ніщо не змінює робоче простір, поки хтось вручну не схвалить зміни, що дозволяє зберегти процес перевірки, як це передбачено ICM, та створює можливість для подальшої автоматизації.

    Що працює, а що — ні

    Модель об’ємом 9B, яка створює модулі, генерує придатну структуру, але не завершені матеріали для навчання. Перевагою є робочий процес та його інтерпретованість, проте модель, яка самостійно пише курс без нагляду, не є рішенням проблеми.

    Існують реальні недоліки, які спостерігаються під час виконання завдань:

    • У проєкті сміливо було створено пошкоджену одиницю systemd, у якій рядки [Unit] та [Service] злилися в один, а змінні середовища були додані до поля ExecStart.
    • З’явився рядок з версією, якого немає в жодних примітках, а назви моделей постійно змінювалися між модулями gemma2, gemma3 та gemma3:2b.
    • Оскільки кожен модуль був написаний окремо та не знав про інші, модулі 1 та 2 значно перетиналися, кожен з них охоплюючи питання змінних середовища та systemd.
    • Обробка даних триває приблизно сім хвилин, оскільки кожна сторінка проходить через функцію часткового підсумовування; лише FAQ Ollama зайняла 41 000 символів у восьми частинах.
    • Одним із джерел була система перевірки ботів від Cloudflare. Читач правильно зафіксував, що вона не містила жодного реального контенту, тому модель просто перейшла далі, не вигадуючи нічого.

    Жоден з цих елементів не робить агента детерміністичною системою, тож кожен, хто потребує такої системи, повинен написати власний скрипт. Перевага полягає у тому, що інструкції знаходяться у файлах, які людина читає та коригує між етапами, тож первинний варіант можна виправити на певному етапі, щоб згодом не стикнутися з несподіванками. Рядок про „чотири–шість цілей“ у контракті є лише натяком для моделі до моменту редагування файлу, а після цього — точним фактом. Саме через розбіжність між тим, що модель схильна робити, та тим, що людина визначає чітко, існують ці етапи перевірки.

    Усе разом

    У кінцевому підсумку для кожної конвеєрної лінії існує окрема папка робочого простору поруч із агентом, у якій знаходиться контракт CONTEXT.md для кожної стадії та папка output/ для зберігання результатів. Агент залишається тим самим багатосерверним агентом, що й раніше, а движок, який ним керує — тобто завантажувач, драйвер із контрольними точками, механізм визначення інструментів та цикл для кожного модуля — поміщається у єдиному файлі довжиною близько 400 рядків.

    Ось короткий огляд складових:

    • Робочий простір, де контекст кожної стадії зберігається у пронумерованих папках у форматі звичайного Markdown, які можна редагувати без змін у коді.
    • Завантажувач, який створює системні запити кожної стадії з файлів, розділяючи посилання та робочі матеріали, у вікні обсягом приблизно від 900 до 1 500 токенів на стадію.
    • Драйвер із контрольними точками, який виконує одну стадію, зупиняється на час для людини та продовжує роботу, зберігаючи свій стан на диску.
  • Визначення обсягу роботи інструменту за етапами, обмежуючи кожен етап тим, що вказано у відповідному контракті.
  • Альтернативний варіант, який записує відповідь на основі зібраних результатів, якщо модель продовжує пошук, тож результат завжди буде не порожнім.
  • Розробка окремо для кожного модуля, де кількість модулів дорівнює кількості заголовків у схваленій структурі.
  • Якщо робоче простір — це просто папка, то для різних завдань потрібна лише різна папка. У репозиторії є ще одна папка workspace-rust для курсу з Rust для початківців. Вона копіює першу папку та відрізняється лише двома файлами: профілем аудиторії та однорядковою ідеєю, тоді як усі контракти етапів, файли маршрутизації та посібники з використання інструментів залишаються абсолютно ідентичними. Виконання команди mcp-workflow-stage5 --workspace workspace-rust дає курс з Rust з тієї ж системи обробки. Це і є концепція ICM щодо налаштування фабрики, а не самого продукту.

    Два робочі простори відрізняються лише файлом audience.md та однорядковою ідеєю: один і той самий процес, але різний курс.

    Є можливості для розширення цього. Розділ Verify у контракті етапу міг би змусити його перечитати попередній результат та виявити відхилення ще до перегляду людиною, що у статті ICM описується як міжетапна перевірка. Однорядкове узагальнення попередніх модулів могло б надаватися для кожного проєкту, скорочуючи перекриття між модулями 1 та 2. А папка proposed/ могла б стати чимось, що самостійно заповнюється.

    Чому створювати цю легку версію вручну, коли існують CrewAI та LangGraph? Це хороші інструменти, і лише тоді, коли потрібні одночасні агенти, динамічне розгалуження чи реальне розгортання, їхня вага має сенс. Однак багато завдань є послідовними, підлягають перевірці та можуть виконуватися знову, і в них немає такої потреби. Для таких випадків найпростіша відповідь вже існує на кожній машині — це папка з файлами. Спочатку створивши її, можна також побачити, де важчі інструменти починають приносити користь.