agents

Ihr Agenten-Framework ist ein Monolith. Dem Browser ist das egal.

Letzte Woche habe ich einen LLM-Agenten in eine schlichte HTML-Datei gesetzt. Kein Node.js. Kein Webpack. Kein Docker-Container mit einem Python-Backend, das an ein anderes Python-Backend weiterreicht. Ein <script type="module">-Tag, ein paar Imports und ein Agent, der nachdenkt, Tools aufruft, Code schreibt und an Sub-Agenten delegiert, und das alles vollständig clientseitig orchestriert, im Browser. Die Inferenz läuft weiterhin auf einem entfernten Modellserver. Das lässt sich nicht vermeiden. Aber alles zwischen Ihren Nutzern und dieser API (die Reasoning-Schleife, die Tool-Aufrufe, die Code-Ausführung in der Sandbox, die Koordination mehrerer Agenten) läuft im Tab. Keine Middleware. Kein eigenes Backend.

Das gesamte Framework dahinter besteht aus 479 Zeilen JavaScript. Ich habe es geschrieben, um etwas zu beweisen.

Das Framework war nie das Produkt

So sieht es 2026 bei Agenten-Frameworks aus: Sie führen pip install für irgendetwas aus, das 400 transitive Abhängigkeiten nachzieht. Sie konfigurieren YAML-Dateien. Sie richten Vektordatenbanken, Message Queues und Tracing-Backends ein. Sie schreiben „Agenten“-Code, der in Wahrheit nur der Klebstoff zwischen sieben verschiedenen Diensten ist, von denen keine zwei im selben Prozess laufen.

Dann ruft Ihr Agent ein Tool auf.

Im Hintergrund wandert eine Anfrage von Ihrem Python-Prozess zu einem lokalen HTTP-Server, der ein Tool-Schema serialisiert und es per REST an einen weiteren Dienst schickt. Der deserialisiert es, führt eine Funktion aus und schickt das Ergebnis auf demselben Weg zurück. Das alles, um get_weather("Berlin") aufzurufen.

Das Problem ist nicht, dass diese Frameworks schlecht wären. Manche sind ausgezeichnet. Das Problem ist, dass Sie für die Kernabstraktion (ein LLM, das in einer Schleife nachdenkt und handelt) nichts davon brauchen.

Was, wenn der Agent dort läuft, wo die Nutzer ohnehin schon sind?

Der Browser ist eine Runtime. Eine gute. Er hat fetch. Er hat postMessage. Er hat iframes mit konfigurierbaren Sicherheits-Sandboxes. Er hat ein Event-System. Er hat einen Modul-Loader. Alles, was Sie für ein Agenten-Framework brauchen, steckt bereits in der Plattform.

smolagents-js nimmt das ernst. Inspiriert von der Python-Bibliothek smolagents von Hugging Face setzt es das vollständige ReAct-Muster (Think, Act, Observe) in reinem Browser-JavaScript um, mit null npm-Abhängigkeiten.

Nicht „null Produktionsabhängigkeiten“. Null Abhängigkeiten. Kein Runtime-Abschnitt in der package.json. Kein Bundler. Kein Build-Schritt. Sie brauchen zwar einen modernen Browser mit Unterstützung für ES-Module und einen OpenRouter-API-Key für den Modellzugriff, aber das Framework selbst bringt nichts mit, was Sie nicht selbst geschrieben haben.

<script type="module">
import { ToolCallingAgent, Model, tool } from './src/index.js';

Das ist das ganze Setup.

479 Zeilen. Reden wir darüber, was drinsteht.

Die Architektur ist absurd gut lesbar. Zehn Dateien. Jede macht genau eine Sache:

Datei Zeilen Was sie tut
events.js 20 Ein EventEmitter. Mehr nicht.
tool.js 33 Tool-Definition + Umwandlung ins OpenAI-Schema
model.js 36 HTTP-Client für OpenRouter
prompts.js 37 Vorlagen für System-Prompts
managed-agent.js 36 Verpackt einen Agenten als Tool
code-agent.js 43 Agent, der JavaScript schreibt und ausführt
tool-calling-agent.js 43 Agent, der Tools auswählt und aufruft
agent.js 105 Die ReAct-Schleife
sandbox.js 119 Code-Ausführung in der Sandbox per iframe
index.js 7 Öffentliche Exporte

Sie können das Ganze in einer Kaffeepause lesen. Nicht überfliegen, sondern lesen. Jeden Kontrollfluss verstehen. Jedes Event nachverfolgen. Genau darum geht es.

Die Schleife

Jedes Agenten-Framework landet am Ende bei derselben Schleife, egal wie viele Abstraktionen es übereinanderstapelt. smolagents-js stellt sie dorthin, wo Sie sie sehen können:

while (this.currentStep < this.maxSteps) {
  this.currentStep++;
  // THINK — ask the LLM what to do next
  this.emit('think', { agent: this.name, step: this.currentStep });
  const response = await this.callModel();
  // Is it done? If no action, we have our answer.
  const action = this.extractAction(response);
  if (!action) {
    this.emit('done', { result: response.content });
    return response.content;
  }
  // ACT — execute the tool or code
  this.emit('act', { step: this.currentStep, ...action });
  const result = await this.executeAction(action);
  // OBSERVE — feed the result back into the conversation
  this.appendActionToHistory(response, action, result);
  this.emit('observe', { step: this.currentStep, result });
}

Das ist agent.js. Die Basisklasse. Zwei Subklassen überschreiben extractAction und executeAction und setzen damit die beiden Strategien um:

ToolCallingAgent parst tool_calls im OpenAI-Stil aus der LLM-Antwort, sucht das passende Tool, ruft es auf und hängt das Ergebnis als Tool-Nachricht an.

CodeAgent zieht JavaScript aus den Markdown-Codeblöcken des LLM, injiziert die Tools als asynchrone Funktionen und führt den Code in einer Sandbox aus.

Dieselbe Schleife. Eine andere Auslegung von „Aktion“. Das LLM weiß nicht, in welcher der beiden es steckt, und es ist ihm auch egal.

Der Sandbox-Trick

Der CodeAgent muss vom LLM erzeugtes JavaScript sicher ausführen. In der Node.js-Welt würden Sie zu vm2 oder einem Docker-Container greifen. Im Browser haben Sie schon etwas Besseres: das iframe.

this._iframe = document.createElement('iframe');
this._iframe.sandbox = 'allow-scripts';
this._iframe.style.display = 'none';
document.body.appendChild(this._iframe);

Drei Zeilen. Das Attribut sandbox="allow-scripts" erzeugt einen Ausführungskontext ohne Zugriff auf das DOM der Elternseite, ohne Netzwerkzugriff, ohne Storage, ohne alles. Er kann nur JavaScript ausführen und per postMessage mit der Elternseite reden.

Tools werden im iframe zu Stubs, die Nachrichten weiterreichen:

async function get_weather(args) {
  parent.postMessage({ type: 'tool_call', tool: 'get_weather', args }, '*');
  return new Promise((resolve) => {
    window.__pending[callId] = { resolve };
  });
}

Der Agent schreibt Code, der get_weather() aufruft. Das iframe schickt eine Nachricht an die Elternseite. Die Elternseite führt das echte Tool aus. Das Ergebnis reist über postMessage zurück. Das Promise wird aufgelöst. Der Code läuft weiter.

Kein HTTP. Keine Debatten über Serialisierungsformate. Keine Container-Orchestrierung. Nur die Plattform, die tut, wofür sie gebaut wurde.

Warum es sich lohnt, den Quellcode zu lesen

Dieser Beitrag zitiert nicht ohne Grund so viel Code. Die meisten Dokumentationen von Agenten-Frameworks bringen Ihnen die API bei. Sie sagen Ihnen, welche Funktion Sie aufrufen, welche Konfiguration Sie setzen, welchen Decorator Sie ergänzen sollen. Wenn etwas kaputtgeht, bleibt Ihnen die Suche in GitHub Issues.

smolagents-js ist klein genug, dass der Quellcode die Dokumentation ist. Jede Abstraktion ist eine Datei. Jede Datei passt auf einen Bildschirm. Wenn Sie auf einen Bug stoßen, googeln Sie nicht. Sie lesen agent.js und wissen genau, wo die Schleife ist, wie sich die History aufbaut und warum das Modell gesehen hat, was es gesehen hat.

Das ist wichtiger, als man denkt. Agenten zu debuggen ist nicht deshalb schwer, weil die Bugs so subtil wären, sondern weil die Systeme undurchschaubar sind. Letzten Monat habe ich zwei Stunden lang einen fehlgeschlagenen Tool-Aufruf in einem beliebten Python-Framework nachverfolgt. Das Modell bekam ein fehlerhaftes Tool-Ergebnis, aber die Serialisierung verteilte sich auf drei Abstraktionsschichten, zwei Middleware-Hooks und eine Callback-Registry. Der eigentliche Bug war ein fehlender str()-Aufruf. In smolagents-js gehen Tool-Ergebnisse von executeAction über appendActionToHistory in das Nachrichten-Array. Eine Datei. Ein Pfad. Diesen Bug fänden Sie in Minuten.

Agenten als Tools. Tools als Agenten.

Das Multi-Agenten-Design hat eine gewisse rekursive Eleganz. Ein ManagedAgent umhüllt einen beliebigen Agenten und stellt ihn als Tool mit einem einzigen Parameter bereit: task.

Aus Sicht des Managers ist ein Sub-Agent einfach ein weiteres Tool. Er nimmt einen String und gibt einen String zurück. Dass hinter dieser Schnittstelle eine ganze ReAct-Schleife läuft, also ein weiteres LLM, das denkt, handelt und beobachtet, bleibt unsichtbar. Der Manager muss es nicht wissen.

So sieht das bei einer echten Aufgabe aus. Ein Manager koordiniert einen Researcher und einen Calculator, um diese Frage zu beantworten: „Wie viele Fußballstadien braucht es, damit die Bevölkerung von Tokio einen Sitzplatz hat?“

const researcher = new ToolCallingAgent({
  model, tools: [webSearchTool], maxSteps: 5
});
const calculator = new ToolCallingAgent({
  model, tools: [calculateTool], maxSteps: 5
});
const manager = new ToolCallingAgent({
  model,
  managedAgents: [
    new ManagedAgent({
      agent: researcher, name: 'researcher',
      description: 'Searches the web for factual information'
    }),
    new ManagedAgent({
      agent: calculator, name: 'calculator',
      description: 'Evaluates math expressions'
    })
  ],
  maxSteps: 8
});
const answer = await manager.run(
  'How many football stadiums would you need to seat the population of Tokyo?'
);

Der Manager entscheidet sich, researcher({ task: "population of Tokyo" }) aufzurufen. Der Researcher startet seine eigene ReAct-Schleife, ruft web_search auf, bekommt Ergebnisse und formuliert eine Antwort: „13,96 Millionen.“ Der Manager nimmt das und ruft calculator({ task: "13960000 / 80000" }) auf. Der Calculator durchläuft seine Schleife, wertet den Ausdruck aus und gibt „174,5“ zurück. Der Manager fasst zusammen: „Etwa 175 Stadien.“

Drei Agenten. Insgesamt sechs Reasoning-Schritte. Jeder feuert beim Arbeiten Events, und die verschachteln sich sauber:

[manager]      Step 1: thinking...
[manager]      Step 1: calling researcher({"task": "population of Tokyo"})
  [researcher] Step 1: thinking...
  [researcher] Step 1: calling web_search({"query": "Tokyo population"})
  [researcher] Step 1: result = Tokyo metro population: ~13.96 million
  [researcher] Done in 2 steps
[manager]      Step 2: thinking...
[manager]      Step 2: calling calculator({"task": "13960000 / 80000"})
  [calculator] Step 1: thinking...
  [calculator] Step 1: calling calculate({"expression": "13960000 / 80000"})
  [calculator] Done in 2 steps
[manager]      Done in 3 steps

Die Einrückung kommt von einem depth-Zähler, der jedes Mal hochzählt, wenn ein Event eine ManagedAgent-Grenze nach oben durchquert. Die UI hängt sich ohne Zusatzaufwand daran: Jede depth-Ebene ist eine CSS-Einrückung. Die gesamte Orchestrierungsschicht umfasst 36 Zeilen Code und erlaubt beliebig tiefe Agentenhierarchien, weil die Komposition bis ganz unten nur aus Funktionsaufrufen besteht.

Was es nicht kann

Zeit für Ehrlichkeit. Manches behandelt dieses Framework bewusst nicht, und manches kann es nicht behandeln, weil es dort läuft, wo es läuft.

Kein persistenter Speicher. Die History des Agenten liegt in einem JavaScript-Array. Schließen Sie den Tab, ist sie weg. Es gibt keine eingebaute Persistenz für Konversationen, keinen Vector Store, keine Retrieval-Augmented Generation. Wenn Sie Speicher über Sitzungen hinweg brauchen, müssen Sie ihn selbst anbinden: localStorage, IndexedDB, eine externe API. Das Framework gibt Ihnen das Nachrichten-Array. Was Sie damit machen, ist Ihr Problem.

Kein Streaming. Der Modell-Client macht ein einziges fetch und wartet auf die vollständige Antwort. Bei lang laufenden Schritten sieht der Nutzer „thinking…“, bis das Modell fertig ist. Streaming würde die Komplexität von model.js ungefähr verdoppeln und bis in die ReAct-Schleife durchschlagen. Es ist die offensichtlichste fehlende Funktion und die wahrscheinlichste nächste Ergänzung.

Keine Retry-Logik und kein Rate Limiting. Wenn ein API-Aufruf fehlschlägt, wirft der Agent einen Fehler. Es gibt kein exponentielles Backoff, keine Circuit Breaker, keine automatischen Wiederholungen. Für ein Deployment in Produktion hinter einem echten Produkt bräuchten Sie einen Wrapper um die Klasse Model. Für Prototypen und Demos ist die Einfachheit der richtige Kompromiss.

Keine Authentifizierungsabläufe. Der API Key ist ein String, den Sie übergeben. Es gibt keinen OAuth-Tanz, keinen Token-Refresh, keine Key-Rotation. Für eine Browser-First-Bibliothek ist das eine echte Einschränkung: Entweder bitten Sie die Nutzer, ihren Key einzufügen (so machen es die Beispiele), oder Sie leiten über Ihr eigenes Backend um (was den Sinn der Sache teilweise aushebelt).

Nur im Browser. Ohne Anpassungen läuft das nicht in Node.js. Die Sandbox hängt an document.createElement('iframe'). Der Modell-Client nutzt das fetch des Browsers. Wenn Sie serverseitige Agenten brauchen, ist das nichts für Sie, und das ist in Ordnung. Die Wette lautet, dass viele Anwendungsfälle für Agenten in den Client gehören und nicht auf den Server.

Das sind echte Beschränkungen. Manche lassen sich innerhalb des aktuellen Designs lösen. Manche sind grundsätzliche Kompromisse des Ansatzes „null Abhängigkeiten, nur Browser“. Die Frage ist, ob das, was es kann (Tool-Aufrufe, Code-Ausführung, Multi-Agenten-Orchestrierung, eine lesbare ReAct-Schleife), für Ihren Anwendungsfall reicht. Für Prototypen, Lehre und leichtgewichtige clientseitige Agenten lautet die Antwort ja. Für Produktionssysteme mit Tausenden gleichzeitigen Nutzern und regulatorischen Anforderungen natürlich nicht.

Aber 479 Zeilen, die die richtigen Dinge gut machen, sind ein besserer Ausgangspunkt als 50.000 Zeilen, die Sie nie ganz verstehen werden.

Was ich beweisen wollte, war nicht, dass klein immer besser ist. Sondern dass der Kern eines Agenten (die Schleife, die Tools, die Komposition) eine kleine Idee ist. Wenn eine kleine Idee unter einem großen System begraben wird, können Sie sie nicht mehr durchdenken. Sie können nicht debuggen, was Sie nicht lesen können. Sie können nicht erweitern, was Sie nicht verstehen. Die Frameworks werden weiter wachsen. Die Idee muss das nicht.

Ich habe smolagents-js gebaut, um LLM-Agenten lesbar zu machen. Es ist Open Source auf GitHub, unter MIT-Lizenz. Die Beispiele laufen in jedem modernen Browser mit einem OpenRouter-API-Key.

Aus dem Englischen übersetzt: zum Originalbeitrag

Gestalten Sie die Zukunft Ihres Unternehmens mit KI

Tauchen Sie ein in unsere spannenden Workshops und rüsten Sie Ihr Team mit den Werkzeugen und dem Wissen aus, um im Zeitalter der KI führend zu sein.

Kontaktieren Sie uns
Scroll to top