Ein lokaler LLM-Agent-Arbeitsablauf, der aus Ordnern und Markdown-Dateien erstellt wird
Erstellen Sie einen gesicherten, mehrstufigen Pipeline für lokale Agenten, wobei jede Stufe ein Ordner ist, die Anfragen in editierbarem Markdown vorliegen und ein Mensch die Ausgabe vor dem kostspieligen Schritt überprüft.
Quellen: Build Your Own Local LLM Agent Workflow in 400 Lines of Python
Ein lokaler Agent kann einen wiederholbaren, überprüfbaren Arbeitsablauf ohne Einsatz eines Frameworks realisieren. Die hier beschriebene Einrichtung führt eine mehrstufige Aufgabe aus, wobei jede Stufe ein Ordner ist, die Anweisung für jede Stufe eine einfache Markdown-Datei darstellt, die von Menschen gelesen und bearbeitet werden kann, und ein Mensch überprüft die Ausgabe zwischen den Stufen. Der Agent selbst ändert sich nicht; nur die Art und Weise, wie sein Kontext abgelegt wird, ändert sich – und das ist der eigentliche Punkt.
Warum ein lokaler Agent das benötigt
Der Agent kümmert sich bereits um die Websuche, das Lesen von Seiten und das Handeln über einen persistenten Browser-Tab. Was fehlte, war eine Möglichkeit, eine Aufgabe wiederholt auszuführen, wobei ein Mensch dabei halbwegs eingreifen kann.
Der Grund dafür war, dass alles, was es steuerte, im Code enthalten war. Ein Systemprompt wurde bei der Importierung erstellt und eine einzige Werkzeugliste beim Start aufgebaut, die dem Modell bei jedem Schritt übergeben wurde. Um den Agenten in einem anderen Schritt eine andere Rolle spielen zu lassen, musste man Python bearbeiten. Um vor einem aufwändigen Schritt anzuhalten und sich die Zwischenergebnisse anzusehen, gab es keinen Punkt zum Anhalten. Das ist für einen Chat-Loop akzeptabel, aber unpraktisch für Aufgaben mit mehreren Phasen.
Das Ergebnis ist ein kompaktes Verfahren zur Erstellung von Kursen durch Forschung. Vier Phasen führen einen einzigen Satz, der einen Kurs beschreibt, zu einem Entwurf eines Mini-Kurses, wobei nach jeder Phase eine Überprüfung stattfindet. Nummerierte Ordner mit reinem Markdown enthalten nun den Kontext, der den Agenten steuert, sodass der Überblick vor Beginn der Erstellung gelesen und korrigiert werden kann, ohne dass dabei Code verändert wird.
Die Struktur folgt einer Methode namens Interpretable Context Methodology, kurz ICM, bei der die Orchestrierung auf Framework-Ebene durch eine Dateisystemstruktur ersetzt wird.
Das Diagramm zeigt den Workflow als Ordner mit dem Namen workspace. Jeder nummerierte Ordner unter pipeline/ stellt eine Phase dar; das CONTEXT.md-File jeder Phase enthält ihre Anweisungen, und proposed/ ist ein Zwischenspeicherbereich, in dem ein Mensch Änderungen manuell freigibt.
Nummerierte Ordner fungieren als Phasen, Markdown-Dateien enthalten den Kontext pro Phase, und lokale Skripte übernehmen die mechanischen Aufgaben, für die kein Modell benötigt wird. Das Design orientiert sich an Unix-Pipelines und mehrstufigen Compilern, bei denen die Ausgabe eines Schritts als Eingabe für den nächsten dient und jedes Zwischenergebnis eine normale Datei ist, die geöffnet werden kann.
Was Sie zum Laufen benötigen
Für das Modell qwen3.5:9b wird unter Ollama mit deaktiviertem Denkprozess und einer Temperatur von 0,1 gearbeitet. Das Surfen und Suchen nutzen frühere Implementierungen: camofox-browser lauscht auf Port 9500 und SearXNG auf Port 8090, jeweils in Docker. Starten Sie vorab diese beiden Container sowie Ollama und ein Modell, das Toolaufrufe unterstützt.
Der Build erfolgt in fünf Phasen:
- In Phase 1 werden camofox-browser und SearXNG in Docker gestartet, auf die die lokalen Dienste der späteren Phasen zugreifen.
- In Phase 2 wird die Systemanweisung des Agenten aus einer Mappe mit Markdown-Dateien erstellt, wobei dabei eine fest programmierte Zeichenkette ersetzt wird.
- In Phase 3 wird der Driver bereitgestellt, wobei pro Phase Überprüfungen sowie Limits für Tools angewendet werden.
- In Phase 4 findet die Überprüfung des Entwurfs statt: Eine Phase nutzt zwei frühere Ausgaben, und eine Person gibt dem Plan vor dem kostspieligen Schritt ihre Zustimmung.
proposed/-Ordner hinzugefügt, der Platz für spätere Erweiterungen lässt.Installation
Jede Phase befindet sich in ihrem eigenen Unterordner und wird über pyproject.toml als Konsole-Skript bereitgestellt, sodass Sie nach einer Installation jede Phase über ihren Namen ausführen können. Der erste Schritt besteht darin, das Repository zu klonen und es in editierbarem Modus unter Verwendung einer virtuellen Umgebung zu installieren:
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 .
Die Installation bringt mcp, ollama und httpx mit sowie registriert eine Reihe von Konsole-Skripten. Die Stufen 2 bis 5 erhalten jeweils einen Workflow-Runner (mcp-workflow-stage2 bis mcp-workflow-stage5) sowie einen Agenten (mcp-agent-stage2 bis mcp-agent-stage5). Zwei MCP-Server, mcp-search-part3 und mcp-browser-stage5, werden aus früheren Iterationen übernommen, wodurch das Repository selbstständig bleibt.
Die Konfiguration befindet sich in config.toml im Wurzelverzeichnis des Repositoriums, wo eine neue [workflow]-Seite hinzugefügt wird. Da jede Stufe ihren workspace/-Ordner liest und ihre history/-Dateien relativ zum aktuellen Verzeichnis schreibt, ist es am einfachsten, eine Stufe aus ihrem eigenen Ordner auszuführen:
cd stage5
mcp-workflow-stage5
Phase 1: Die lokalen Dienste
Es wird hier kein Python verwendet. In dieser Phase werden die beiden Dienste gestartet, die der Agent bei der Suche nutzt: camofox-browser, der versteckte Browser, auf dem die Leserwerkzeuge laufen, sowie SearXNG, die Suchmaschine unter search. Jeder, der bereits die frühere Einrichtung verwendet, kann denselben docker-compose.yml beibehalten.
cd stage1
docker compose up -d
docker compose logs -f
Einige Details sind erwähnenswert:
- Camofox hört auf Port 9500 und SearXNG auf Port 8090 – den Ports, die
config.tomlerwartet – sodass der Agent beides ohne zusätzliche Einstellungen findet. - Die compose-Datei bindet
searxng/settings.ymlein, wodurch die JSON-API aktiviert und der Rate-Limiter deaktiviert wird, sodass die Suche bereits beim ersten Start funktioniert. - Das Camofox-Bild wird einmal aus seinem Quellrepositorium erstellt. Die erste Erstellung ist langsam, da das Camoufox-Binärdatei groß ist;
stage1/README.mdlistet die Schritte auf.
Zum Überprüfen des Zustands sollten Sie mit curl http://localhost:9500/health sowie http://localhost:8090/search?q=ollama&format=json aufrufen. Beide sollten mit JSON antworten.
Phase 1 umfasst nur diese beiden Dienste. Sobald Camofox auf Port 9500 und SearXNG auf Port 8090 antworten, hat die Forschungsplattform etwas, worauf sie sich beziehen kann.
Phase 2: Kontext aus Dateien
Dies ist die einzige tatsächliche Codeänderung. Zuvor enthielt der Agent eine Systemanweisung – einen Modul-Ebene-String, der bei Start erstellt und unverändert in jeder Runde gesendet wurde, unabhängig von der Aufgabe. Wenn der Agent in jedem Schritt eine andere Rolle übernehmen soll, muss die Anweisung von einem editierbaren Ort stammen, und das Lesen aus Dateien ist die einfachste Option.
Die erste Änderung ermöglicht es daher, die Systemanweisung zu überschreiben:
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
Das Verhalten ist einfach:
- Falls
system_textnicht gesetzt ist, greift der Agent auf denselben fest programmiertenSYSTEM_PROMPTwie zuvor zurück, sodass der interaktive Agentmcp-agent-stage2genauso funktioniert wie zuvor. - Wird
system_textgesetzt, wird dieser Wert zum Systemprompt für die Ausführung. Der Workflow-Runner setzt ihn pro Phase aus Dateien ein, wobei die Agentklassen ansonsten unverändert bleiben.
Was die Dateien selbst angeht, so ist ein ICM-Arbeitsbereich nichts anderes als eine Ordnerstruktur. stage2/workspace/ enthält ein CLAUDE.md, das den Arbeitsbereich und seine Gliederung beschreibt, ein CONTEXT.md, das die Phasen in der Reihenfolge auflistet, einen _config/-Ordner mit Referenzmaterialien, die zwischen den Ausführungen unverändert bleiben, sowie einen pipeline/-Ordner mit nummerierten Phasen. Jeder Phasenordner verfügt über sein eigenes CONTEXT.md-Dokument sowie einen output/-Ordner.
Die beiden Dateien auf höchster Ebene geben Identität und Routing vor. CLAUDE.md gehört zur Schicht 0 – es ist das Erste, was der Agent liest:
# 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.
Neben diesem ist CONTEXT.md die Schicht 1, die Routing-Informationen, die die Phasen auflisten:
# 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.
Das gemeinsam genutzte Referenzmaterial befindet sich in _config/, wird einmal konfiguriert und bei jedem Ausführungsvorgang verwendet. Unten finden Sie audience.md, eine Datei der Stufe 3, die in den Phasen Scope, Forschung und Überblick gelesen wird:
# 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.
Der Vertrag 01-scope/CONTEXT.md bildet den Kern des Designs. Dies ist die vollständige Version für die erste Pipeline-Stage:
# 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/
Die einzige Eingabe auf Layer 4, 01-scope/input.md, ist das Arbeitsmaterial für die Durchführung – die einzeilige Idee, die in einen Kurs umgewandelt wird:
# 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.
Eine kurze Erklärung dazu, wie der Vertrag gelesen wird:
- In der Tabelle „Eingaben“ werden die genauen Dateien aufgelistet, die eine Phase liest, wobei jede mit einer Schicht versehen ist: Schicht 3 für Referenzmaterialien, die bei mehreren Durchläufen konstant bleiben (das Publikumprofil), Schicht 4 für das Arbeitsmaterial dieses spezifischen Durchlaufs (die einzeilige Idee). Nichts anderes aus dem Arbeitsbereich wird geladen.
- In der Spalte „Tools“ steht
keine, denn um eine Idee in Ziele umzuwandeln, sind keine Tools erforderlich. In Phase 3 kehrt man dazu zurück. - Der Abschnitt „Prozess“ enthält die Anweisungen, denen der Agent folgt, und dies wird zur Reihe des Benutzers.
- In der Spalte „Ausgaben“ wird der Name der Datei angegeben, die die Phase in ihre eigene
output/-Ordner schreibt.
Aus dem Vertrag heraus wandelt der Lader die Inputs-Tabelle in echten Dateiinhalten um und erstellt auf dieser Grundlage den Kontext. Referenzen und Arbeitsmaterialien bleiben getrennt, da sie unterschiedliche Rollen erfüllen: Die ersten sind eine Reihe von Regeln, denen das Modell folgen soll, die zweiten sind rohe Eingaben, die es transformieren muss.
# 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()
Das Verhalten des Laders lässt sich wie folgt beschreiben:
- Zuerst kommen Identität und Routing (
CLAUDE.md,CONTEXT.md), die dem Agent mitteilen, wo er sich befindet und welche Phasen vorhanden sind. - Darauf folgen Referenzdateien der Schicht 3 unter einem Überschriftspunkt, der sie als Einschränkungen darstellt. Die Arbeitsdateien der Schicht 4 werden getrennt unter dem Prozesstext der jeweiligen Phase zusammengefasst und zeigen an, worauf sich der Agent konzentrieren soll.
- Das Ergebnis ist die Systemanweisung für diese Phase, die aus einfachen Dateien erstellt wird und nichts enthält, was von der Phase nicht gefordert wurde.
Vor dem Ausführen von etwas am Modell lohnt es sich, zu überprüfen, was der Lader erzeugt, da dieser Teil deterministisch ist. Das Repository liefert einen kleinen Prüfcode, probe_loader.py, der den zusammengestellten Kontext einer Phase zusammen mit einer ungefähren Tokenanzahl ausgibt und das Modell niemals aufruft. Führen Sie ihn aus der stage2-Ordner gegen die erste Phase aus:
cd .\stage2\
python ..\probe_loader.py 01-scope
Das Screenshot zeigt die ersten Zeilen des Berichts der Probe für die Scope-Phase zusammen mit der Größe des Kontextfensters. In dieser Phase entstehen etwa 900 Tokens, von denen jedes relevant ist – das macht die Idee des ICM greifbar: ein kleines, auf die jeweilige Phase begrenztes Fenster, ohne dass ein einziger riesiger Prompt alles enthält.
Um die Phase tatsächlich auszuführen, verwendet man mcp-workflow-stage2. Dieser Tool analysiert den Vertrag, führt den Agenten einmal aus und erstellt output/objectives.md. Für das beispielhafte Konzept, einen praktischen Kurzkurs zum Ausführen eines lokalen LLM mit Ollama, ergeben sich fünf übersichtliche Ziele, die man durchscannen und anschließend beibehalten oder entfernen kann.
Der Banner im Screenshot zeigt den vom Tool vorhergesagten Kontextumfang von fast 900 Tokens, und das Ergebnis der Phase ist objectives.md. Die Ziele einer Ausführung erscheinen darunter; bei einer zweiten Ausführung würden sie etwas anders formuliert werden:
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.
Sobald der Kontext nun aus Dateien stammt, fehlt nur noch die Abwicklung der Phasen in der richtigen Reihenfolge sowie das Anhalten dazwischen.
Phase 3: Der Treiber, die Tore und die Abgrenzung der Tools
Ein Workflow umfasst geordnete Phasen mit einem menschlichen Kontrollpunkt dazwischen. Der Treiber durchsucht daher die nummerierten Ordner, führt die erste Phase mit einem leeren Ausgabeergebnis aus und stoppt anschließend.
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
So funktioniert es:
- Der Zustand wird auf der Festplatte gespeichert. Eine Phase gilt als abgeschlossen, wenn ihre
output/-Ordner eine echte Datei enthält – es gibt daher keinen separaten Zustandsspeicher. Das Dateisystem ist der Zustand. - Die Schleuse dient lediglich dazu, den Ablauf vorzeitig zu beenden. Sobald eine Phase abgeschlossen ist, gibt der Treiber an, wo nachzusehen ist, und stoppt. Man liest die Ausgabe, ändert sie bei Bedarf und führt den gleichen Befehl erneut aus, um weiterzukommen.
--allist die Ausweichmöglichkeit, um ohne Stoppen direkt weiterzumachen, wenn man das Ergebnis vertraut. Standardmäßig wird der Ablauf durch Schleusen gesteuert, da genau das der Zweck der Schleusen ist.
Um den Pipeline-Ablauf auszuführen, verwendet man mcp-workflow-stage3, liest, was dieser geschrieben hat, und führt ihn anschließend erneut aus, um weiterzukommen. Die erste Ausführung umfasst 01-scope und stoppt; die zweite umfasst die Forschungsphase und stoppt usw.
Jede Aufrufung bearbeitet eine Phase, gibt an, wo gesucht werden soll, und stoppt am Überprüfungsportal; das Ausführen derselben Anweisung setzt sie fort.
Tool-Einsatz
Forschung ist die einzige Phase, bei der Tools verwendet werden, und genau dort traten die ersten echten Probleme auf. Der Agent verbindet sich mit den Such- und Browser-Servern, und die frühere Version fügte alle Tools aller Server zu einer einzigen flaten Liste zusammen und zeigte diese gesamte Liste bei jedem Schritt an. Das sind etwa fünfzehn Tools, wovon die Forschung ungefähr fünf benötigt. Der Rest sind Tab- und Aktionstools zum Arbeiten mit einer Seite, die die Forschung niemals verwendet.
Obwohl alle fünfzehn Tools angeboten wurden, konzentrierte sich das 9B-Modell auf die Interaktionswerkzeuge. Es wählte open_tab und read_tab anstelle der Lesefunktionen, wobei read_tab lediglich den oberen Teil einer Seite zurückgibt. Die Ollama-Dokumentation läuft auf Mintlify, weshalb dieser obere Teil überall identisch ist – derselbe Navigationstext. Das Modell las weiterhin dieses Menü und öffnete Tab für Tab, bis die Aufrufbegrenzung die Ausführung mit einer 143 Zeichen langen Entschuldigung als Ausgabe beendete.
Die Lösung besteht darin, die Tools je nach Phase einzuschränken. Ein Vertrag listet die benötigten Tools auf, und zur Laufzeit werden nur diese bereitgestellt. Hier ist der vollständige Forschungsvertrag:
# 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/
Die neuen Namen der Tools-Kategorie lauten search für den Suchserver und nur vier Tools vom Browserserver – alles Lesegeräte. Die Laufzeitumgebung wendet dies als Filter auf die Tool-Liste an, bevor das Modell sie erhält:
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
Der Filter funktioniert wie folgt:
- Die allow-map gilt pro Server.
Nonefür einen Server bedeutet alle seine Tools, während eine Liste von Namen nur diese Tools beinhaltet. - Jedes Tool, das nicht in der allow-Liste enthalten ist, wird ignoriert, sodass das Modell es niemals sieht. Research erkennt derzeit fünf Tools, während scope, outline und draft keines erkennen.
- Die Karte wird schrittweise neu erstellt, wodurch auch versteckte Tools nicht aufgerufen werden können; ein fiktiver
open_tab-Aufruf würde einfach als unbekanntes Tool zurückgegeben.
Der Forschungsvertrag enthält außerdem seine eigene Layer-3-Referenz, references/tool-use.md, die auf dieser Stufe beschränkt ist und erklärt, wie die verbleibenden Leser verwendet werden können:
# 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.
Nochmals übernimmt das Dateisystem die Rolle des Frameworks. In einem Framework besteht das Werkzeugset eines Agenten aus Code; hier handelt es sich um eine Markdown-Zeile, die jeder bearbeiten kann. Mit den Lesefunktionen aktiviert und den Tab-Werkzeugen deaktiviert, lieferten die Modelle summarize und extract, die ganze Seiten über den in Blöcken arbeitenden Leser durchgingen, sowie die Notizen echte Erkenntnisse: den Installationsbefehl, die Standardportnummer 11434 sowie Anleitungen zur Verwendung der Python-Bibliothek – jeweils mit einem Quelllink.
Wenn das Modell nicht aufhören kann
Ein weiteres Problem war subtiler. Mit den richtigen Werkzeugen las das Modell gute Seiten, erkannte aber nie, wann es aufhören sollte. Bei einem Lauf wurde dieselbe Seite mit derselben Frage mehr als ein Dutzend Mal zusammengefasst, ohne dass es zu neuen Fortschritten kam, bis die Aufrufgrenze erreicht war. Die nützlichen Erkenntnisse befanden sich zwar im Nachrichtenverlauf, doch da keine endgültige Antwort erstellt wurde, war das Endergebnis erneut eine Entschuldigung.
Um das zu bewältigen, macht der Ausführende nun einen zusätzlichen Schritt, sobald eine Runde vorzeitig beendet wird: einen letzten Aufruf ohne Werkzeuge, bei dem das Modell angewiesen wird, seine Antwort auf der Grundlage dessen zu verfassen, was es bereits gesammelt hat.
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
Drei Aspekte sind wichtig:
- Es handelt sich um eine freiwillige Funktion. Der interaktive Agent lässt
synthesize_on_stopdeaktiviert und berichtet wie zuvor lediglich vom Stoppen, während der Workflow es aktiviert, damit eine Phase stets echte Ausgaben erzeugt. - Es rettet die Arbeit. Die Ergebnisse der Tools befinden sich bereits in der Historie, sodass der auftretende Aufruf ohne Tools die Notizen daraus aufnehmen kann und so einen aus dem Ruder gelaufenen Vorgang in ein nutzbares
notes.mdverwandelt – anstelle einer Entschuldigung. - Ein Schutzmechanismus gegen feststeckende Schleifen hilft. Derselbe Aufruf, der dreimal hintereinander erfolgt, beendet den Vorgang nun frühzeitig – unabhängig davon, ob ein Fehler auftritt oder nicht – sodass das Modell weniger Zeit mit Berechnungen verbringt, bevor die Synthese übernimmt.
Ein reibungsloser Forschungsvorgang dauerte etwa sieben Minuten und lieferte für die fünf Ziele etwas mehr als 10.000 Zeichen an aus Quellen stammenden Notizen.
Der Screenshot zeigt, wie das zweite Toollaufende abgeschlossen wird und um eine Überprüfung des Ergebnisses gebeten wird. Mit den Kontrollpunkten, der Abgrenzung des Umfangs und den Rettungsmaßnahmen ist die Forschung ausreichend zuverlässig, um darauf aufzubauen, wobei der nächste Kontrollpunkt am wichtigsten ist.
Phase 4: Der Ausarbeitungskontrollpunkt
Von allen Phasen erfordert die Ausarbeitung den größten Aufwand, denn danach folgt das eigentliche Entwerfen, welches der kostspieligste Schritt ist. Ein Fehler, der bereits in der Ausarbeitungsphase entdeckt wird, lässt sich am günstigsten beheben. In dieser Phase werden zwei vorherige Ergebnisse – die Ziele aus 01-scope sowie die Forschungsnotizen aus 02-research – zu einem nach Modulen geordneten Überblick zusammengefasst. Der Vertrag besagt:
# 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/
Diese beiden Layer-4-Schritte sind das Neue. Sie machen den mehrstufigen Übergang aus dem ICM-Paper konkret, da die Ausgabe einer Stufe der nächsten Stufe zugeführt wird und diese zwei frühere Ergebnisse zusammen verwendet. Der Lader nimmt beide als Arbeitsmaterial auf, und das Modell erzeugt den Überblick.
Führen Sie es mit mcp-workflow-stage4 aus. Da der Umfang und die Forschung abgeschlossen sind, führt die Ausführung 03-outline aus, schreibt outline.md und hält an der Schleuse an.
Durch Ausführung von mcp-workflow-stage4 entsteht der Überblick, und es wird an der Schleuse gestoppt – dem letzten günstigen Punkt, um den Ablauf vor der Erstellung des Entwurfs zu korrigieren.
Das Tor erwies sich sofort als nützlich. Während eines Laufs sammelte die Forschung zu viel Daten. Der Kurs richtet sich an Personen, die von nichts aus zu einem Laufmodell werden wollen, und enthält außerdem einen kurzen Script-Beispiel, doch die Notizen wanderten zu Cloud-API-Schlüsseln und KV-Cache-Quantisierung – Themen, auf die das Modell in den Dokumentationen gestoßen war und die es getreu aufgezeichnet hatte. Der Entwurf übernahm das und entwickelte einen ganzen Modul namens „Sichere Ihre Verbindung mit Umgebungsvariablen“, der voller Inhalte ist, die Anfänger nicht benötigen.
Im Arbeitsablauf gab es hier keine Probleme. Der Überschuss zeigte sich als lesbarer Überschriftentext in einer einfachen Datei, noch bevor irgendein Kurstext geschrieben wurde. Man kann das Modul löschen, seine Lektionen umgestalten oder es an einen anderen Ort verweisen – beim Erstellen des Entwurfs wird niemals Zeit damit verschwendet. Die Korrektur erfordert lediglich eine einzige Änderung in einer Markdown-Datei, während man nach dem Erstellen des Entwurfs fünf Module sowie zahlreiche Lektionen noch einmal durchgehen müsste, um das falsch platzierte Element zu finden.
Ein Beispiel für ein Entwurfsgerüst ist in VS Code zur Bearbeitung verfügbar – dort werden Module oder Lektionen, die außerhalb des Aufgabenbereichs liegen, bereits vor dem Entwurfsphase ausgeschlossen.
In dieser Phase sollte das Gerüst auf die gewünschten Module und Lektionen reduziert werden, anschließend wird der Prozess erneut gestartet. Die Anzahl der Module entspricht einfach der Anzahl der ## Module-Überschriften in der Datei; dadurch wird auch die Länge des Kurses bestimmt.
Phase 5: Erstellung der Entwürfe pro Modul
Durch die Erstellung der Entwürfe wird jedes Modul auf Basis des genehmigten Gerüsts verfasst, wobei Fakten aus den Notizen entnommen werden. Der erste Versuch sah vor, den gesamten Kurs in einem Schritt zu erstellen – doch das misslang: Bei einigen Durchläufen entstanden vier Module, bei anderen nur eines, und man gab sich bereits als Sieger aus. Ein kleines Modell betrachtet „den gesamten Kurs schreiben“ als eine riesige Aufgabe und neigt dazu, früh aufzuhören.
Deshalb durchläuft die Entwurfsphase stattdessen nacheinander die einzelnen Module. Der vollständige Ablauf umfasst eine For each-Anweisung, die den Schleifenprozess definiert:
# 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/
Die Erstellung ist die einzige Phase, in der voice.md gelesen wird – das endgültige Layer-3-Datei im Arbeitsbereich, die den Ton des Kurses definiert:
# 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.
Die For each-Zeile ist es, die eine offene Abfrage in eine Schleife umwandelt, und der Laufzeitmechanismus führt den Prozess einmal pro Modul aus:
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)
Was die Schleife tut:
- Der Überblick wird an den
##-Überschriften in Module aufgeteilt, und die Schleife wird für jedes Modul einmal ausgeführt. Die Anzahl ist nicht auf fünf festgelegt; vier Module bedeuten vier Durchläufe und sechs bedeuten sechs, entsprechend dem, was die Filterung überstanden hat. - Jeder Aufruf beginnt sauber.
agent.messageswird jedes Mal geleert, sodass ein Modul mit den vorhandenen Notizen erstellt wird, ohne Erinnerung an seine „Geschwister“, wodurch jeder Durchgang klein und selbstständig bleibt. - Die Ergebnisse werden zu einem einzigen
draft.mdzusammengefügt. Ein begrenzter Aufruf pro Modul anstelle einer einzigen Anfrage „Kurs schreiben“ ermöglicht es dem 9B-Modell, sie alle abzuschließen.
Die Richtlinie ist allgemein und nicht speziell für das Erstellen von Entwürfen ausgelegt. Jede Phase kann For each deklarieren, wobei die anderen drei keine Richtlinie haben und in einer einzigen Runde ausgeführt werden. Die Ausführung von mcp-workflow-stage5 zeigt die Fortschrittsanzeige von [1/5] bis [5/5], und die Beispielausführung schrieb insgesamt etwas über 15.000 Zeichen in fünf Modulen.
In der Entwurfsphase wird die Übersicht nacheinander durchlaufen, wobei pro Modul eine Modellaufruf stattfindet. Die Anzahl entspricht der noch vorhandenen Anzahl von Modulen – also bedeuten vier oder sechs Module vier oder sechs Ausführungen.
Der Arbeitsbereich enthält außerdem ein proposed/-Verzeichnis, das von der Pipeline niemals ausgeführt wird. Es handelt sich dabei um einen Pufferbereich: Wenn eine Ausführung eine Verbesserung vorschlägt, die länger bestehen sollte – beispielsweise ein präziseres Ziel oder ein ständig fehlerhaftes Entwurfssystem für Sprachregeln – wird der Vorschlag dort als Datei gespeichert. Wenn jemand zustimmt, verschiebt er ihn in _config/. Der menschliche Entscheider ist hier entscheidend. Nichts ändert den Arbeitsbereich, bis jemand eine Änderung manuell freigibt – dadurch bleibt die Überprüfung, wie vom ICM vorgesehen, erhalten, und es entsteht Raum für spätere Automatisierungen.
Was funktioniert und was nicht
Ein 9B-Modell, das die Module entwirft, erzeugt brauchbare Grundstrukturen, aber keine fertigen Lehrinhalte. Der Vorteil liegt im Workflow und seiner Interpretierbarkeit; ein lokales Modell, das einen Kurs unbeaufsichtigt erstellt, ist hingegen nicht das Ziel.
Die Unzulänglichkeiten sind tatsächliche Probleme, die bei den Ausführungen auftreten:
- Der Entwurf erzeugte kühn eine fehlerhafte systemd-Unit, bei der die
[Unit]- und[Service]-Zeilen miteinander verschmolzen sowie Umgebungsvariablen inExecStarteingefügt wurden. - Eine Versionsangabe erschien, die in den Anmerkungen überhaupt nicht vorkam, und Modellnamen wechselten zwischen den Modulen von
gemma2,gemma3undgemma3:2b. - Da jedes Modul unabhängig entwickelt wurde und nichts vom Rest wusste, überschnitten sich Module 1 und 2 erheblich, wobei jeweils Umgebungsvariablen sowie systemd behandelt wurden.
- Die Verarbeitung dauert etwa sieben Minuten, da jede Seite durch den in Blöcke unterteilten Zusammenfasser geleitet wird; allein die Ollama-FAQ umfasste 41.000 Zeichen in acht Blöcken.
- Eine Quelle war eine Cloudflare-Bot-Prüfwand. Der Leser stellte richtig fest, dass sie keinen echten Inhalt enthielt, sodass das Modell ohne Erfindungen fortfuhr.
Nichts davon macht den Agenten zu einem deterministischen Prozess, und wer einen solchen benötigt, sollte ein Skript schreiben. Der Vorteil besteht darin, dass die Anweisungen in Dateien enthalten sind, die eine Person zwischen den Schritten liest und korrigiert, sodass Unklarheiten bereits frühzeitig beseitigt werden und später nicht überraschend auftauchen. Die Angabe „vier bis sechs Ziele“ im Umfangsvertrag dient dem Modell zunächst nur als Hinweis, bis die Datei bearbeitet wurde, danach handelt es sich um eine feste Festlegung. Der Unterschied zwischen dem, was das Modell tendenziell tut, und dem, was eine Person festlegt, ist genau der Grund für die Existenz dieser Kontrollpunkte.
Alles zusammen
Am Ende gibt es für jede Pipeline eine Arbeitsordnerstruktur neben dem Agenten, mit einem CONTEXT.md-Dokument für jede Phase sowie einem output/-Ordner, der die Ergebnisse weiterleitet. Der Agent ist derselbe Mehrserver-Agent wie zuvor, und die Laufzeitumgebung, die ihn antreibt – also der Lader, der gesteuerte Antrieb, die Tool-Klassifizierung sowie der Schleifenprozess pro Modul – passt in eine einzige Datei mit etwa 400 Zeilen.
Zusammenfassend die Bestandteile:
- Ein Arbeitsbereich, in dem der Kontext jeder Phase in nummerierten Ordnern im reinen Markdown-Format gespeichert ist und ohne Änderung des Codes bearbeitet werden kann.
- Ein Lader, der den Systemprompt jeder Phase aus Dateien erstellt, Referenzen und Arbeitsmaterial getrennt hält und dabei ein Fokusfenster von etwa 900 bis 1.500 Token pro Phase verwendet.
- Ein Antrieb mit Steuermechanismen, der eine Phase ausführt, eine Pause für den Benutzer einlegt und anschließend fortsetzt, wobei sein Zustand auf der Festplatte gespeichert bleibt.
Dass ein Arbeitsbereich lediglich eine Dateiordner ist, bedeutet, dass für unterschiedliche Aufgaben nur verschiedene Ordner benötigt werden. Das Repository enthält einen weiteren Ordner namens workspace-rust für einen Einführungskurs in Rust. Er kopiert den ersten Ordner und weicht nur in zwei Dateien ab – dem Zielgruppenprofil sowie der einzeiligen Idee – während alle Phasenverträge, die Routing-Datei sowie die Referenz zur Tool-Nutzung bytegenau identisch bleiben. Durch Ausführung von mcp-workflow-stage5 --workspace workspace-rust erhält man einen Rust-Kurs aus derselben Pipeline. Das ist ICMs Konzept der Konfiguration der Fabrik anstelle des Produkts.
Die beiden Arbeitsumgebungen unterscheiden sich nur in audience.md sowie der einzeiligen Beschreibung: derselbe Ablauf, unterschiedlicher Kurs.
Es gibt Raum zur Erweiterung. Ein Verify-Abschnitt in einem Stage-Vertrag könnte es ermöglichen, dass eine Stufe einen früheren Ausgangswert erneut liest und Abweichungen bereits vor dem Eingreifen eines Menschen markiert – wie im ICM-Paper als cross-stage Verification skizziert. Eine einzeilige Zusammenfassung der vorherigen Module könnte bei jedem Entwurfsaufruf verwendet werden, um die Überschneidungen zwischen Modul 1 und 2 zu verringern. Zudem könnte die proposed/-Ordnerstruktur zu etwas werden, das sich selbst auffüllt.
Warum diese dünne Version manuell erstellen, wenn es bereits CrewAI und LangGraph gibt? Es handelt sich dabei um gute Tools, und ein Arbeitsablauf, der konkurrierende Agenten, dynamisches Verzweigen oder eine tatsächliche Bereitstellung erfordert, rechtfertigt ihr Gewicht. Viele Aufgaben sind jedoch sequenziell, überprüfbar und wiederholbar und benötigen das nicht. Für solche Fälle existiert bereits die einfachste Lösung auf jedem Gerät: ein Ordner mit Dateien. Zunächst diese zu erstellen, zeigt außerdem auf, ab wann schwerere Tools ihren Nutzen zeigen.