Alain Lilic

DemoPrototyp einer Dokumenten-App

AEYEZ: Dokumente fotografieren, Fristen erkennen

Oberer Ausschnitt der Landingpage von AEYEZ am Desktop: Navigation mit Schaltfläche 'Jetzt testen', Hinweis 'KI-Dokumentenanalyse', Überschrift 'Deine Dokumente, Verstanden.' und ein Smartphone-Mockup, das ein Beispieldokument scannt.

Problem

Behördenbriefe, Rechnungen, Verträge und Kassenbons kommen auf Papier, und die wichtige Information steckt in wenigen Feldern: Absender, Betrag, Frist. Ich wollte eine App, die aus einem Foto strukturierte Daten und Fristen macht. Der Nutzer sollte selbst entscheiden, welcher KI-Anbieter seine Dokumente sieht, bis hin zu einem lokalen Modell.

Meine Rolle

Ich habe Idee und Umfang festgelegt, die Design- und Umsetzungspläne abgenommen und den Code geprüft.

Vorgehen

Ein pnpm-Monorepo mit Expo-App (React Native, Android und Web), einem schlanken Next.js-API-Server auf Vercel und einer Landingpage. Dokumente und Seiten liegen in SQLite auf dem Gerät, API-Schlüssel im SecureStore. Pro Aufgabe (Analyse, Chat, Übersetzung, Ordnervorschlag) wählt der Nutzer Anbieter und Modell; der Client schickt die Wahl als Header mit, auf Wunsch auch den eigenen Schlüssel.

Die Analyse ruft generateObject aus dem Vercel AI SDK mit einem zod-Schema auf: Dokumenttyp, zehn Untertypen, typisierte Felder wie Betrag, Datum oder IBAN und Fristen mit Priorität. Für OpenRouter geht es über Text: JSON aus der Antwort lösen, Feldnamen normalisieren, dann dasselbe Schema prüfen lassen. Ollama und Hugging Face spricht die App direkt an, ohne Umweg über den Server.

Architektur

  1. Erfassen

    Kamera, Galerie oder PDF in der Expo-App, mehrere Seiten pro Dokument

  2. Lokal speichern

    SQLite auf dem Gerät, Schlüssel im SecureStore

  3. AIClient

    Wählt pro Aufgabe Anbieter und Modell. Lokale Anbieter direkt, Cloud über das Backend

  4. Next.js-API auf Vercel

    4 Routen: analyze, chat, translate, suggest-folders. Header x-ai-provider, x-ai-model, x-ai-key

  5. Strukturierte Ausgabe

    generateObject mit zod-Schema, bei OpenRouter Text plus Normalisierung plus zod.parse

  6. App-Ansichten

    Dokumente, Fristen, Chat mit Dokumenten, Einstellungen

Entscheidung

Ein Schema als Vertrag für alle Anbieter

Jeder Anbieter liefert JSON etwas anders, und nicht jeder kann strukturierte Ausgabe. Statt pro Anbieter einen Parser zu pflegen, ist das zod-Schema die einzige Wahrheit: generateObject, wo es geht, sonst Text, Normalisierung und analysisSchema.parse. Was das Schema nicht besteht, erreicht die App nicht. Der Preis: Das Schema ist streng, eine falsch benannte Kategorie lässt die ganze Analyse scheitern statt nur ein Feld.

Code-Auszug

apps/web/app/api/analyze/route.ts (gekürzt)Seitlich scrollentypescript
const analysisSchema = z.object({
  type: z.enum(['document', 'image', 'other']),
  subtype: z.enum(['receipt', 'invoice', 'letter', 'gov_letter', 'form', 'contract', 'medical', 'id_document', 'certificate', 'bank']).nullable(),
  confidence: z.number().min(0).max(1),
  language: z.string().nullable(),
  needs_translation: z.boolean(),
  title: z.string().nullable(),
  vendor: z.string().nullable(),
  date: z.string().nullable(),
  message: z.string(),
  items: z.array(z.object({
    item_key: z.string(),
    item_value: z.string(),
    value_type: z.enum(['string', 'number', 'currency', 'date', 'iban', 'boolean']),
    category: z.enum(['line_item', 'meta', 'address', 'deadline', 'party', 'financial']),
    confidence: z.number(),
  })),
  deadlines: z.array(z.object({
    label: z.string(),
    date: z.string(),
    priority: z.enum(['low', 'normal', 'high', 'critical']),
    sender_email: z.string().nullable(),
  })),
});
Das Schema beschreibt, was die App aus jedem Foto erwartet. Dieselbe Definition steuert generateObject und prüft die Textantworten.

Ergebnisse

  • 197Commits zwischen 08.03. und 14.04.2026
  • 10Dokument-Untertypen im Schema, vom Kassenbon bis zum Behördenbrief
  • 7wählbare Anbieter: 5 in der Cloud, 2 lokal
  • 4API-Routen im Backend

Was ich daraus gelernt habe

Der Server springt mit eigenen Schlüsseln ein, wenn der Nutzer keinen mitschickt, die API hat kein Login, und CORS steht auf *. Vor jedem öffentlichen Test hätte ich zuerst Auth und ein Limit gebraucht. Außerdem habe ich zu früh in die Breite gebaut: ein Modul-System mit Kauf-Tracking und einen Scraper. Modul-Registry und Scraper habe ich im April wieder entfernt, über 1.100 Zeilen. Und es gibt keine automatisierten Tests, obwohl gerade die Normalisierung der Modellantworten welche gebraucht hätte.