Site
Alle Artikel

Veröffentlicht 25. Sept. 202613 Min. Lesezeit

MVA, das Model View Agent Pattern: Eine Backend-Architektur für die Ära der KI-Agenten

MVC ging davon aus, dass der Konsument Ihrer Schnittstelle Kontext aus Erfahrung erschließt. Ein Agent kann das nicht. MVA ersetzt die View durch einen Presenter, eine Wahrnehmungsschicht, die Regeln, Guardrails und Affordanzen genau dann an Daten anhängt, wenn ein Agent sie liest. Das Pattern, der Code, die Token-Mathematik und fünf Anwendungsfälle, in denen es sich rechnet.

Renato Marinho

Von Renato Marinho

Founder · Vinkius

The MVA pattern: a raw database row crosses the Model boundary where undeclared fields are dropped, the Presenter wraps the surviving fields into a six block perception package of data, ui, embeds, hints, rules and actions, and the agent reads that package and picks the next tool from the offered affordances

MVA steht für Model, View, Agent, also Modell, Ansicht, Agent. Es ist ein Architekturpattern für das Backend, mit dem ein KI-Agent spricht, und es existiert, weil der Konsument Ihrer Schnittstelle die Spezies gewechselt hat.

Fünfzig Jahre lang haben wir Schnittstellen für einen Konsumenten entworfen, der folgern kann. Ein Mensch liest amount_cents: 45000 und weiß, dass das vierhundertfünfzig Dollar sind, weil er schon Rechnungen gesehen hat. Ein Agent liest dasselbe Feld und muss raten. Manchmal rät er fünfundvierzigtausend Dollar. Manchmal erfindet er ein Zahlungstool, das nicht existiert, oder überspringt einen Pflichtworkflow, weil ihm niemand sagte, dass der Workflow da war. Keines davon ist ein Prompting-Problem. Alle sind Architekturprobleme.

Ich habe das Pattern benannt und geschrieben, während ich MCP Fusion baute, das TypeScript-Framework für produktive MCP-Server, in dem MVA die Standardweise ist, wie ein Tool entsteht. Das ist die Erklärung, die ich einem Senior Engineer geben würde, der mich fragt, was MVA ist, warum es kein umbenanntes MVC ist, und wo es sich tatsächlich rechnet.

MVA in einem Absatz

MVA ersetzt die menschenorientierte View aus MVC durch einen Presenter: eine Wahrnehmungsschicht zwischen Ihren Domänendaten und dem Sprachmodell, die entscheidet, was der Agent sieht, wie er es interpretieren soll und was er als Nächstes tun darf. Das Model definiert weiterhin Ihre Domänendaten und validiert sie. Der Presenter umschließt diese Daten mit einer Schema-Grenze, Interpretationsregeln, serverseitig gerenderten visuellen Blöcken und expliziten nächsten Aktionen. Der Agent, ein beliebiges LLM, erhält ein strukturiertes Wahrnehmungspaket statt rohem JSON und hört auf zu raten, weil er Anweisungen bekommt, die mit den Daten reisen.

SchichtWas sie istVerantwortung
ModeldefineModel()Domänendaten: Feldtypen, Defaults, welche Felder vor dem Agenten verborgen bleiben, welche vor Massenzuweisung geschützt sind
ViewDer PresenterWahrnehmung: Schema-Grenze, Regeln, UI-Blöcke, Kürzungslimits, nächste Aktionen, PII-Redaktion
AgentBeliebiges LLMKonsumiert das Paket, urteilt, ruft das nächste Tool aus der angebotenen Liste auf

Warum MVC aufhört zu funktionieren

MVC ging von einem Konsumenten aus, der visuelle UI rendert, Räumlichkeitssinn hat, aus sichtbaren Optionen wählt und den nächsten Schritt nicht halluziniert. Ein Agent hat keine dieser Eigenschaften, und vier strukturelle Fehlermodi erscheinen in dem Moment, in dem Sie ein LLM auf eine rohe API richten.

Kontexthunger. Daten kommen ohne Interpretationsregeln an. { amount_cents: 45000 } hat keine Regel angehängt, also stützt sich das Modell auf den Feldnamen, und Feldnamenkonventionen sind kein verlässliches Signal. Ich habe dasselbe Feld in drei verschiedenen Konversationen als Dollar, als Euro und als "wird verarbeitet" gerendert gesehen.

Aktionsblindheit. Nachdem der Agent die Daten gelesen hat, muss er entscheiden, was als Nächstes kommt. Ohne Affordanzen erfindet er Toolnamen, ruft billing.process_payment auf, obwohl das Tool billing.pay heißt, oder bricht ab, weil er nicht wusste, dass eine Aktion existierte.

Wahrnehmungsdrift. Dieselbe Rechnung kommt aus zwei Tools unterschiedlich geformt zurück, das eine sagt amount in Dollar und state: "open", das andere amount_cents und status: "pending". Das Modell kann sie nicht als eine Entität zusammenführen und verhält sich innerhalb Ihres eigenen Produkts inkonsequent.

Sicherheitsleck. Rohes JSON trägt jede Spalte, die die Abfrage ausgewählt hat: internal_margin, tenant_id, password_hash. Alles davon landet im Kontextfenster und kann in der Antwort auftauchen, die der Nutzer liest.

Jedes davon ist ein Architekturdefizit. Prompt-Engineering repariert keine fehlende Ausgabegrenze, denn der Prompt ist nicht der Ort, an dem die Grenze leben würde.

MVA im Code: die drei Schichten

Das Model deklariert die Domänenentität einmal. Felder, die Sie hier nicht deklarieren, können den Agenten nicht erreichen.

import { defineModel } from '@mcpfusion/core';

export const InvoiceModel = defineModel('Invoice', m => {
  m.casts({
    id:           m.string('Invoice identifier'),
    amount_cents: m.number('Amount in cents. Divide by 100 to display.'),
    status:       m.enum('Payment status', ['paid', 'pending', 'overdue']),
  });

  m.hidden(['tenant_id']);
  m.guarded(['id']);
  m.fillable({
    create: ['amount_cents', 'status'],
    update: ['status'],
  });
});

Der Presenter ist die View. Er wird auf Domänenebene definiert, nicht pro Tool, also bedient ein InvoicePresenter jedes Tool, das eine Rechnung zurückgibt.

import { createPresenter, ui, suggest } from '@mcpfusion/core';

export const InvoicePresenter = createPresenter('Invoice')
  .schema(InvoiceModel)
  .rules((invoice, ctx) => [
    'CRITICAL: amount_cents is in CENTS. Divide by 100 before display.',
    ctx?.user?.role !== 'admin'
      ? 'RESTRICTED: Mask exact totals for non admin viewers.'
      : null,
    invoice.status === 'overdue'
      ? 'WARNING: This invoice is overdue. Mention urgency proactively.'
      : null,
  ])
  .ui((invoice) => [
    ui.summary(`Invoice ${invoice.id} · ${invoice.status}`),
    ui.echarts({ series: [{ type: 'gauge', data: [{ value: invoice.amount_cents / 100 }] }] }),
  ])
  .agentLimit(50, (omitted) =>
    ui.summary(`50 shown, ${omitted} hidden. Filter by status or date range.`),
  )
  .suggest((invoice) => [
    suggest('billing.pay', 'Process immediate payment'),
    invoice.status === 'overdue'
      ? suggest('billing.escalate', 'Escalate to collections')
      : null,
  ].filter(Boolean));

Das Tool ist die Agentenoberfläche. Der Handler gibt Rohdaten zurück; .returns() ist die Stelle, an der die Wahrnehmungsschicht angehängt wird.

import { initMCPFusion } from '@mcpfusion/core';

const f = initMCPFusion<AppContext>();

export const getInvoice = f.query('billing.get_invoice')
  .describe('Get an invoice by ID')
  .withString('invoice_id', 'The exact invoice ID')
  .returns(InvoicePresenter)
  .handle(async (input, ctx) => {
    return ctx.db.invoices.findUnique({
      where: { id: input.invoice_id },
      include: { client: true },
    });
  });

export const createInvoice = f.action('billing.create')
  .describe('Create an invoice')
  .fromModel(InvoiceModel, 'create')
  .returns(InvoicePresenter)
  .handle(async (input, ctx) => ctx.db.invoices.create({ data: input }));

.fromModel(InvoiceModel, 'create') leitet die Eingabeparameter aus dem fillable-Profil des Models ab, daher können Eingabe- und Ausgabeschema nicht auseinanderdriften. Eine Deklaration, zwei Grenzen, beide zur Compile-Zeit erzwungen.

Was der Agent tatsächlich erhält

Wenn der Handler zurückkehrt, setzt die Pipeline ein strukturiertes Wahrnehmungspaket zusammen: bis zu sechs Inhaltsblöcke in fester Reihenfolge.

Block 1  DATA     {"id":"INV-001","amount_cents":45000,"status":"pending"}
                  (tenant_id and internal_margin rejected by the schema boundary)
Block 2  UI       [echarts gauge: 450.00]  with a pass through directive:
                  "send this block to the interface, do not redraw it"
Block 3  EMBEDS   rules and blocks from ClientPresenter, merged by .embed()
Block 4  HINTS    situational notes added by the handler or middleware
Block 5  RULES    [DOMAIN RULES] CRITICAL: amount_cents is in CENTS...
Block 6  ACTIONS  → billing.pay: Process immediate payment
                  → billing.escalate: Escalate to collections

Die Reihenfolge ist keine Kosmetik. Sprachmodelle gewichten das Ende des Kontexts stärker, also landen die Interpretationsregeln und die verfügbaren Aktionen zuletzt, genau dort, wo das Model gleich entscheidet, was es aufruft. Daten kommen zuerst, weil sie die Interpretation begründen, die folgt.

Der Kontrast zu dem, was ein roher MCP-Server zurückgibt, ist das ganze Argument in einem Bild:

// raw SDK: one text block, JSON.stringify, no boundary
return {
  content: [{ type: 'text', text: JSON.stringify(invoice) }],
};
// the agent receives all columns, zero rules, zero next actions

Fünf Anwendungsfälle, in denen MVA sich rechnet

1. Billing und Fintech. Das Rechnungsbeispiel oben ist nicht dekorativ. Geld ist die Domäne, in der eine falsche Vermutung ein Supportticket oder ein Compliance-Vorfall ist. Die Cent-Regel, die rollenbasierte Maskierungsregel und die Zahlungsaffordanz sind drei Zeilen an einem Presenter, und alle Billing-Tools erben sie. Ein Junior-Developer kann kein Billing-Tool ausliefern, das die Teile-durch-hundert-Regel vergisst, denn die Regel liegt nicht in seinem Tool.

2. Betrieb im großen Maßstab. Ein tasks.list-Aufruf gegen eine unbegrenzte Tabelle liefert dreitausend Zeilen zu je etwa fünfhundert Tokens, was rund 1,5 Millionen Tokens in einem Kontextfenster sind, das sie nicht fassen kann. .agentLimit(50, ...) kürzt das Array und hängt einen lehrenden Block an, der die tatsächlichen Filterparameter nennt: status, assignee, sprint_id, due_before. Der nächste Aufruf des Modells ist eine gefilterte Abfrage. Nur zu kürzen würde ihm immer dieselbe gekürzte Seite geben; kürzen plus lehren erzeugt eine sich selbst korrigierende Schleife.

3. Regulierte und persönliche Daten. Gesundheit, HR und jedes CRM mit PII brauchen eine stärkere Grenze als "bitte zeig das nicht". redactPII kompiliert Objektpfade wie patients[*].diagnosis zu Maskierungsfunktionen, also erreicht der maskierte Wert das Modell, während die UI-Blöcke und die Regeln weiterhin die echten Daten sehen. Kombiniert mit m.hidden() sind die Felder, die niemals die Datenbank verlassen dürfen, genau die Felder, die nie deklariert wurden.

4. Multi-Tenant-SaaS. Die Regeln im Presenter erhalten den Request-Kontext, also wird dieselbe Rechnung von einem Admin anders wahrgenommen als von einer Viewer-Rolle, und Daten formatieren sich nach der Tenant-Locale ohne einen zweiten Codepfad. Das ersetzt eine Klasse von handgepflegter Prompt-Verzweigung pro Tenant, die Teams normalerweise pflegen und normalerweise falsch machen.

5. Freigabe- und Bestellworkflows. Affordanzen werden aus dem aktuellen Zustand berechnet, nicht aus einer statischen Liste. Ein offener Auftrag suggeriert orders.approve; ein versendeter Auftrag nicht. Das ist HATEOAS für Tools: Dem Model wird gesagt, welche Übergänge aus dem Zustand, in dem es wirklich ist, legal sind, was die häufigste Art halluzinierter Tool-Aufrufe entfernt.

Es gibt einen sechsten Fall, den zu nennen sich lohnt, weil er der ist, in dem die meisten Teams stehen. Sie haben bereits ein React-Dashboard, eine REST-API und eine Datenbank. MVA verlangt nicht, dass Sie sie ersetzen. Sie behalten MVC für Menschen, fügen eine agentenorientierte Oberfläche hinzu, und beide konsumieren dasselbe Domänenmodell. Dieses duale Schnittstellenmuster ist dort, wo die meiste "KI-native" Arbeit wirklich landet, und es zu benennen macht die Migration zu einem begrenzten Projekt statt zu einer Neuschreibung.

Die Token-Mathematik

Die andere Stelle, an der MVA sich rechnet, ist die Rechnung. Der übliche Fix für eine fehlende Wahrnehmungsschicht besteht darin, Domänenregeln in den globalen Systemprompt zu stopfen, der bei jedem Aufruf mitgeschickt wird, ob die Domäne aktiv ist oder nicht. Ein Produkt mit fünfzehn Domänenentitäten landet bei etwa zweitausend Tokens Regeln pro Request, und siebenundachtzig Prozent davon sind in jedem Zug irrelevant.

Context Tree Shaking: ein globaler Systemprompt schickt etwa zweitausend Token Regeln auf jeden der zehn Züge, insgesamt etwa zwanzigtausend, siebenundachtzig Prozent irrelevant; die am Presenter hängenden Regeln schicken nur die aktive Domäne, etwa zweihundert Token pro Zug, zweitausend gesamt, eine Ersparnis von rund achtzehntausend Token pro Konversation

Context Tree Shaking ist der Name der Alternative: Regeln hängen am Presenter und erscheinen nur, wenn diese Domäne aktiv ist. Eine Konversation über zehn Züge sinkt von etwa zwanzigtausend Token Regeln auf etwa zweitausend, und die Regeln, die ankommen, sind die richtigen. Der Nebeneffekt zählt genauso wie die Kosten: Wenn die Rechnungsregeln in einer Aufgaben-Konversation abwesend sind, kann das Modell die Teile-durch-hundert-Regel nicht falsch auf Aufgabenanzahlen anwenden. Ich habe genau diesen Fehler in Produktion gesehen, und er verschwindet, wenn die Regel von vornherein nicht im Kontext steht.

MVA gegen MVC, REST, GraphQL und RPC

KonsumentAusgabeNächste AktionenGrenze
MVCMenschlicher BrowserHTML pro SeiteLinks und ButtonsView-Template
RESTClient-CodeRohes JSONHATEOAS-Links sind URLsKeine
GraphQLClient-CodeClient-gewählte FelderKeineNur Query-Ebene
RPCClient-CodeRohe typisierte DatenKeineNur Eingabe
MVAKI-AgentWahrnehmungspaketToolnamen aus dem ZustandEingabe und Ausgabe

REST mit HATEOAS ist der nächste Verwandte und verdient die klarste Unterscheidung. HATEOAS bettet Links in die Antwort ein, was der richtige Instinkt ist, aber ein Link ist eine URL zu einem HTTP-Endpunkt und ein Agent ruft Tools beim Namen auf. MVA-Affordanzen geben Toolnamen mit semantischen Gründen zurück, berechnet aus dem aktuellen Datenzustand, zusammen mit Regeln und visuellen Blöcken, die REST nirgends transportieren kann. GraphQL löst, welche Felder zu holen sind, was nie der Flaschenhals war; der Flaschenhals ist, wie die Felder nach dem Holen zu interpretieren sind.

Was MVA nicht ist

Es ist kein Ersatz für MVC. Wenn Ihr Konsument ein Mensch mit Browser ist, bleiben MVC und MVVM korrekt, und kein Teil von MVA macht ein Dashboard besser. Es ist auch kein Agenten-Framework: Es plant nicht, verwaltet keinen Speicher, wählt kein Modell. Es ist der Vertrag zwischen Ihren Daten und dem Modell, das sie liest, und es macht diesen Vertrag explizit, validiert und testbar.

Es ist auch nicht alles oder nichts. Sie können heute Nachmittag einen Presenter an ein Tool eines Legacy-Servers hängen und die anderen fünfzig in Ruhe lassen.

Wie man anfängt

npm install @mcpfusion/core @modelcontextprotocol/sdk
npx mcpfusion create

Scaffolden Sie ein Projekt, definieren Sie ein Model, definieren Sie einen Presenter, setzen Sie .returns() auf eine Query. Der schnellste Test, ob das Pattern etwas tut, ist, das Tool-Ergebnis vorher und nachher zu lesen: rohes JSON, dann das Sechs-Block-Paket. Der Unterschied ist das Argument.

Wer die vollständige Tour durch einen so gebauten Connector will: Der MVA-Schnitt, der Presenter und selbstheilende Fehler sind mit Quellcode erklärt in Einen eigenen MCP-Connector bauen. Wer die Runtime-Seite will, wie Agenten-Traffic im Maßstab sicher bleibt, findet das in KI-Agenten sicher in der Produktion betreiben.

Häufige Fragen

Ist MVA nur für MCP? Das Pattern ist protokollunabhängig, und MCP Fusion ist seine Referenzimplementierung. Alles, was ein LLM an ein Ende eines Tool-Aufrufs setzt, profitiert von einer Wahrnehmungsschicht. MCP ist lediglich dort, wo die Grenze am sichtbarsten ist, weil MCP-Server standardmäßig rohes JSON ausliefern.

Ersetzt MVA MVC? Nein, und das duale Schnittstellenmuster ist die ehrliche Einordnung. Behalten Sie MVC für das menschliche Dashboard, fügen Sie MVA für die Agentenoberfläche hinzu, teilen Sie das Domänenmodell zwischen beiden.

Was ist ein Presenter? Ein Objekt auf Domänenebene, das Rohdaten in Agentenwahrnehmung verwandelt: ein Schema, Regeln, UI-Blöcke, eine Kürzungsrichtlinie, nächste Aktionen und Redaktionspfade. Eines pro Entität, geteilt über alle Tools, die diese Entität zurückgeben.

Wie reduziert MVA Halluzinationen? Indem es die Situationen abschafft, in denen Halluzinieren der billigste Zug ist. Wenn die nächsten Aktionen explizit sind, erfindet das Modell keine Toolnamen. Wenn das Schema streng ist, werden halluzinierte Felder mit der Liste der gültigen abgelehnt. Wenn Fehler Wiederherstellungshinweise tragen, versucht das Modell richtig erneut, statt sich zu drehen.

Funktioniert es mit meinem Modell? Ja, wenn das Modell Tools aufrufen kann. Claude, GPT, Gemini und jeder MCP-kompatible Client lesen das Paket auf dieselbe Weise, weil die Wahrnehmungsschicht auf dem Server liegt.

Warum ich es gebaut habe

Jeder Architekturwechsel, den ich miterlebt habe, hatte denselben Auslöser: Der Konsument änderte sich, und die alte Architektur nahm einen Konsumenten an, den es nicht mehr gab. Der Browser tat es dem Desktop an. Der Mobile tat es dem Browser an. Der Agent tut es jetzt, und die Annahme, die zerbrach, ist eine, die wir nie aufgeschrieben haben: dass der Konsument die Lücken füllen würde.

MVA ist der Name dafür, die Lücken in der Schnittstelle zu schließen, statt zu hoffen, dass das Modell sie schließt. Schreiben Sie die Grenze, schreiben Sie die Regeln, schreiben Sie die Aktionen, und lassen Sie das Modell tun, was es wirklich gut kann.

Themenmvaarchitectureagentsmcpmcpfusion