İçeriğe geç / Skip to content / Zum Inhalt

Einen KI-Agenten mit Space Bunny Alpha bauen

Ahmet Balaman

5 Min. Lesezeit

Vibe CodingSpace Bunny AlphaKI-AgentOpenRouterKITypeScript
Einen KI-Agenten mit Space Bunny Alpha bauen

Zwischen dem Aufruf eines Modells und dem Bauen eines Agenten gibt es genau einen Unterschied: die Schleife. Ein Modell antwortet einmal und hält an. Ein Agent liest die Anweisung in der Antwort, führt die Arbeit tatsächlich aus, gibt das Ergebnis zurück und wiederholt, bis eine Antwort da ist.

In diesem Artikel bauen wir diese Schleife von Anfang bis Ende. Der Einrichtungsartikel behandelt die API-Verbindung, der 1M-Kontext-Artikel das Budgetmanagement. Hier geht es darum, wie ein Modell ein Programm ausführt.

Warum ein Agent ein "Harness" ist, stand im Harness-Artikel; hier schreiben wir den konkreten Code.

Was das Modell nicht tut, was der Agent tut

Vor der Schleife muss die Grenze klar sein. Space Bunny Alpha erzeugt einen Werkzeugaufruf, es führt ihn nicht aus.

Aufgabe des Agenten Aufgabe des Modells
Die Datei lesen Entscheiden, welche Datei gelesen wird
Den Befehl ausführen Sagen, welcher Befehl laufen soll
Den Fehler abfangen Erklären, warum der Fehler passiert ist
Das Ergebnis formatieren Deuten, was das Ergebnis bedeutet
Die Schleife steuern Vorschlagen, wann zu stoppen ist

Die praktische Folge: Es reicht nicht, dem Modell "öffne diese Datei" zu sagen, denn der Code, der sie tatsächlich öffnet, ist Ihrer. Der gesamte Agent ist diese Brücke.

Die Agentenschleife: das Modell erzeugt einen Werkzeugaufruf, der Agentencode führt ihn aus, das Ergebnis wandert zurück in den Verlauf und eine neue Runde beginnt; ohne Werkzeugaufruf endet die Schleife mit einer Antwort

Das Werkzeugschema schreiben

Sie teilen dem Modell mit, welche Werkzeuge es nutzen darf, über ein tools-Array. Je strenger das Schema, desto treffer der Aufruf. Beginnen wir mit zwei Werkzeugen:

const tools = [
  {
    type: "function" as const,
    function: {
      name: "read_file",
      description: "Liest den Inhalt einer Datei mit Zeilennummern.",
      parameters: {
        type: "object",
        properties: {
          path: {
            type: "string",
            description: "Dateipfad relativ zum Projektstamm, zum Beispiel lib/agent.ts",
          },
        },
        required: ["path"],
        additionalProperties: false,
      },
    },
  },
  {
    type: "function" as const,
    function: {
      name: "list_files",
      description: "Listet die Dateien in einem Verzeichnis auf.",
      parameters: {
        type: "object",
        properties: {
          dir: {
            type: "string",
            description: "Aufzulistendes Verzeichnis, zum Beispiel lib",
          },
        },
        required: ["dir"],
        additionalProperties: false,
      },
    },
  },
];

Je detaillierter die description, desto besser. Das Modell entscheidet anhand der Beschreibungen, nicht anhand der Werkzeugnamen. "Dateipfad relativ zum Projektstamm" statt "sucht irgendetwas" ist kein Detail, an das Sie sich später erinnern müssen, sondern der Unterschied zwischen einem richtigen und einem falschen Aufruf.

Lassen Sie additionalProperties: false und required nicht weg. Ohne diese zwei Zeilen kann das Modell ein benötigtes Feld auslassen oder eines erfinden, das Sie nicht wollten.

Die Schleife bauen

Das Herzstück eines Agenten ist eine einzige while-Schleife. Die Logik: Nachrichtenhistorie an das Modell senden; enthält die Antwort einen Werkzeugaufruf, ausführen und das Ergebnis an die Historie anhängen; sonst die Schleife verlassen.

import OpenAI from "openai";
import { readFileSync, readdirSync } from "node:fs";
import { resolve, sep } from "node:path";

const client = new OpenAI({
  apiKey: process.env.OPENROUTER_API_KEY,
  baseURL: "https://openrouter.ai/api/v1",
  timeout: 180_000,
  maxRetries: 2,
});

type Msg = OpenAI.Chat.ChatCompletionMessageParam;

const PROJ_ROOT = process.cwd();

/** Verhindert, dass der vom Modell gelieferte Pfad den Projektstamm verlässt. */
function safePath(relative: string): string {
  const full = resolve(PROJ_ROOT, relative);
  if (full !== PROJ_ROOT && !full.startsWith(PROJ_ROOT + sep)) {
    throw new Error(`Projektstamm darf nicht verlassen werden: ${relative}`);
  }
  return full;
}

function runTool(name: string, rawArgs: string): string {
  const args = JSON.parse(rawArgs || "{}") as { path?: string; dir?: string };

  try {
    if (name === "read_file" && args.path) {
      return readFileSync(safePath(args.path), "utf8").slice(0, 40_000);
    }
    if (name === "list_files" && args.dir) {
      return readdirSync(safePath(args.dir)).join("\n");
    }
    return `Unbekanntes Werkzeug: ${name}`;
  } catch (error) {
    // Fehler als Text an das Modell zurückgeben; die Schleife nicht zerstören.
    return `Fehler: ${(error as Error).message}`;
  }
}

const messages: Msg[] = [
  {
    role: "system",
    content:
      "Sie sind Softwareentwickler. Lesen Sie zuerst die Dateien, dann erklären Sie. " +
      "Schlagen Sie keine Änderung vor, bevor Sie die Datei sicher gelesen haben.",
  },
  {
    role: "user",
    content: "Listen Sie die Dateien in lib/ auf und erklären Sie, was agent.ts tut.",
  },
];

const MAX_TURNS = 8;

for (let turn = 0; turn < MAX_TURNS; turn += 1) {
  const completion = await client.chat.completions.create({
    model: "stealth/space-bunny-alpha",
    messages,
    tools,
    tool_choice: "auto",
    reasoning_effort: turn === 0 ? "high" : "low",
  });

  const message = completion.choices[0]?.message;
  if (!message) break;

  messages.push(message);

  const calls = message.tool_calls ?? [];
  if (calls.length === 0) {
    console.log(message.content);
    break;
  }

  for (const call of calls) {
    const result = runTool(call.function.name, call.function.arguments);
    messages.push({
      role: "tool",
      tool_call_id: call.id,
      content: result,
    });
  }
}

Vier Details machen diese Schleife produktionsreif.

safePath und die Pfadgrenze. Lösen Sie den Pfad des Modells direkt mit resolve auf, kann es ../../.env schreiben. Zu prüfen, dass er unter PROJ_ROOT bleibt, ist der Sicherheitsschritt, der beim Schreiben eines Agenten am häufigsten übersehen wird. Die Validierungslogik aus dem Sicherheitsartikel gilt hier auch für das Dateisystem.

Fehler nicht schlucken, sondern zurückgeben. runTool fängt den Fehler ab und hängt die Meldung als role: "tool" an. Das Modell kann dann lernen, dass die Datei fehlt, und einen anderen Pfad versuchen. Fangen Sie den Fehler ab und verwerfen Sie ihn, wiederholt das Modell denselben Fehler.

Ein Turn-Limit setzen. Ohne MAX_TURNS kann das Modell endlos Schleifen laufen. Acht Turns sind für die meisten echten Aufgaben weit mehr als genug.

Reasoning pro Turn abstimmen. Im ersten Turn entdeckt das Modell Dateien, wo high statt max nötig sein kann. In späteren Turns interpretiert es nur noch, was es gelesen hat, wo low reicht. Wie der Vergleichsartikel erklärt, kommt Reasoning mit max als Standard an, und das ist echte Latenz pro Turn.

Werkzeugfehler so zurückgeben, dass das Modell sie versteht

Der catch-Block in runTool ist kurz, aber entscheidend. Eine rohe Fehlermeldung (ENOENT: no such file or directory, open 'lib/agent.ts') ist ein starkes Signal, sagt aber nicht, was als Nächstes zu tun ist. Dem Modell zu sagen, was passiert, hilft:

function runTool(name: string, rawArgs: string): string {
  const args = JSON.parse(rawArgs || "{}") as { path?: string; dir?: string };

  try {
    if (name === "read_file" && args.path) {
      return readFileSync(safePath(args.path), "utf8").slice(0, 40_000);
    }
    if (name === "list_files" && args.dir) {
      return readdirSync(safePath(args.dir)).join("\n");
    }
    return `Unbekanntes Werkzeug: ${name}. Verfügbare Werkzeuge: read_file, list_files.`;
  } catch (error) {
    const message = (error as Error).message;
    if (message.includes("ENOENT")) {
      const dir = args.path ? args.path.split("/").slice(0, -1).join("/") || "." : ".";
      return `Fehler: ${args.path} nicht gefunden. Rufen Sie zuerst list_files für ${dir} auf und verwenden Sie den echten Dateinamen.`;
    }
    return `Fehler: ${message}. Sie können einen anderen Pfad versuchen.`;
  }
}

Diese wenigen Zeilen heben die Selbstkorrekturquote des Agenten sichtbar. Das Modell arbeitet nun mit "tun Sie das" weiter statt mit "etwas ist schiefgelaufen".

Die Ausgabe auf JSON festlegen

Der letzte Turn eines Agenten muss keinen freien Text erzeugen. Muss eine Dateiänderungs-Zusammenfassung oder eine Klassifikationsentscheidung JSON sein, gibt es zwei Wege.

Weg eins: response_format mit Schema. Sie senden eine letzte Anfrage und erzwingen das Schema, nachdem die Schleife endet. Das JSON-Schema aus dem Einrichtungsartikel gilt hier ebenfalls.

Weg zwei: JSON als Werkzeug zurückgeben. Die Schleife endet nie wirklich; das Modell ruft immer wieder submit_result auf. Das überlässt die "fertig"-Entscheidung dem Modell, während das Schema die Form garantiert:

const tools = [
  readFileTool,
  listFilesTool,
  {
    type: "function" as const,
    function: {
      name: "submit_result",
      description:
        "Wird aufgerufen, wenn die Aufgabe erledigt ist. Liefert das Ergebnis als JSON. " +
        "Muss aufgerufen werden, nachdem alle relevanten Dateien gelesen wurden.",
      parameters: {
        type: "object",
        properties: {
          summary: { type: "string", description: "Zusammenfassung der geleisteten Arbeit" },
          changedFiles: {
            type: "array",
            items: { type: "string" },
            description: "Pfade der geänderten Dateien",
          },
          confidence: {
            type: "integer",
            minimum: 0,
            maximum: 100,
            description: "Wie sehr der Aufgabe vertraut wurde, 0-100",
          },
        },
        required: ["summary", "changedFiles", "confidence"],
        additionalProperties: false,
      },
    },
  },
];

Der Vorteil des zweiten Wegs: Das Modell muss die Dateien lesen, die es braucht, bevor es submit_result aufruft. Sie schließen die Schleife, wenn submit_result erscheint:

for (let turn = 0; turn < MAX_TURNS; turn += 1) {
  // ... Anfrage ...

  const submit = calls.find((c) => c.function.name === "submit_result");
  if (submit) {
    const result = JSON.parse(submit.function.arguments) as {
      summary: string;
      changedFiles: string[];
      confidence: number;
    };
    console.log(result);
    process.exit(0);
  }

  // ... andere Werkzeuge ausführen ...
}

console.error(`Der Agent hat in ${MAX_TURNS} Turns kein Ergebnis geliefert.`);
process.exit(1);

Vergessen Sie das Turn-Limit nicht. Verbraucht das Modell seine Turns, ohne submit_result aufzurufen, bricht das Programm mit einem Fehler ab, weil es eines nie gesehen hat. Das ist immer noch besser als eine still halb erledigte Aufgabe in Produktion.

Wann ein anderes Modell besser ist

Space Bunny Alpha ist nicht für jeden Agenten die richtige Wahl. Drei Ergebnisse lassen sich aus der Fähigkeitstabelle im Vergleichsartikel ableiten.

tool_choice akzeptiert nur auto. Wenn Sie kein bestimmtes Werkzeug erzwingen müssen, ist das in Ordnung. Braucht Ihr Ablauf einen deterministischen ersten Schritt, müssen Sie ihn entweder im Prompt-Text stark betonen, oder dieses Modell passt nicht zu diesem Ablauf.

Es gibt kein seed. Zwei Aufrufe mit demselben Prompt garantieren nicht dieselbe Antwort. Braucht der Agent in Tests reproduzierbare Ausgaben, nehmen Sie ein Modell mit seed.

Kontext kostet nicht nichts. Wenn Sie lange Sitzungen budgetieren, sehen Sie in der Monatstabelle im Vergleichsartikel nach.

Kurz gesagt: Space Bunny Alpha lohnt sich für Erkundung, Experimente und überall dort, wo die Kosten wirklich null sein müssen. Für deterministische Abläufe und Code, in dem Reproduzierbarkeit gefordert ist, sind andere Modelle die bessere Wahl.

Quellen

Kommentare