KI-Modelle
14 Minuten
24.4.2026

Praxis-Test: ChatGPT 5.5 vs. Claude Opus 4.7 lösen dieselbe Coding-Aufgabe

Praxis-Test: ChatGPT 5.5 vs. Claude Opus 4.7 lösen dieselbe Coding-Aufgabe

Benchmarks messen, was Modelle können. Praxis-Tests zeigen, wie sie es tun. Dieser Artikel dokumentiert einen direkten Vergleich: Beide Modelle erhielten exakt denselben Prompt und sollten einen produktionsreifen TypeScript-Endpunkt implementieren – mit Rate-Limiting, Input-Validierung, strukturiertem Error-Handling und JWT-Authentifizierung. Die Aufgabe ist bewusst mehrstufig gewählt, denn erst bei komplexen, realistischen Anforderungen zeigen sich die charakteristischen Unterschiede im Denk- und Coding-Stil der Modelle.


Die Aufgabe: Ein produktionsreifer API-Endpunkt

Der Prompt lautete wörtlich:

"Implementiere einen TypeScript/Express-Endpunkt POST /api/v1/users/:id/messages mit folgenden Anforderungen: (1) JWT-Authentifizierung – nur der Nutzer selbst oder ein Admin darf Nachrichten senden. (2) Rate-Limiting mit Token-Bucket-Algorithmus: 10 Requests pro Minute pro Nutzer, Burst bis 20. (3) Input-Validierung mit Zod: content (string, 1–2000 Zeichen), priority (enum: low/medium/high, optional, default: medium). (4) Strukturiertes Error-Handling nach RFC 7807 (Problem Details). (5) Strukturiertes JSON-Logging mit Request-ID-Tracing. Schreibe produktionsreifen Code mit vollständigen TypeScript-Typen."

Dieser Prompt ist absichtlich vollständig spezifiziert – er testet nicht, ob die Modelle Anforderungen selbst formulieren können, sondern wie sie eine klar definierte, mehrstufige Aufgabe umsetzen.


GPT-5.5: Der pragmatische Sprinter

GPT-5.5 lieferte seinen Code in 47 Sekunden (Time-to-Complete), mit einer Time-to-First-Token von 3,1 Sekunden. Das Modell begann ohne Präambel direkt mit dem Code und fügte am Ende eine kurze Erklärung hinzu.

Der Code von GPT-5.5

typescript
// POST /api/v1/users/:id/messages
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import jwt from 'jsonwebtoken';
import { randomUUID } from 'crypto';

// ── Types ──────────────────────────────────────────────────────────────────
interface JwtPayload { sub: string; role: 'user' | 'admin'; }

interface ProblemDetail {
  type: string; title: string; status: number;
  detail: string; instance: string; requestId: string;
}

// ── Token Bucket ───────────────────────────────────────────────────────────
const buckets = new Map<string, { tokens: number; lastRefill: number }>();
const RATE = 10; const BURST = 20; const WINDOW_MS = 60_000;

function consume(userId: string): boolean {
  const now = Date.now();
  let b = buckets.get(userId) ?? { tokens: BURST, lastRefill: now };
  const elapsed = now - b.lastRefill;
  b.tokens = Math.min(BURST, b.tokens + (elapsed / WINDOW_MS) * RATE);
  b.lastRefill = now;
  if (b.tokens < 1) { buckets.set(userId, b); return false; }
  b.tokens -= 1;
  buckets.set(userId, b);
  return true;
}

// ── Validation ─────────────────────────────────────────────────────────────
const MessageSchema = z.object({
  content: z.string().min(1).max(2000),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
});

// ── Helpers ────────────────────────────────────────────────────────────────
const problem = (res: Response, status: number, title: string,
  detail: string, reqId: string): void => {
  const body: ProblemDetail = {
    type: `https://agentic-coding.ch/errors/${title.toLowerCase().replace(/ /g, '-')}`,
    title, status, detail,
    instance: res.req.originalUrl, requestId: reqId,
  };
  res.status(status).type('application/problem+json').json(body);
};

const log = (level: string, msg: string, meta: object) =>
  console.log(JSON.stringify({ ts: new Date().toISOString(), level, msg, ...meta }));

// ── Handler ────────────────────────────────────────────────────────────────
export async function postMessage(req: Request, res: Response, next: NextFunction) {
  const reqId = randomUUID();
  log('info', 'request', { reqId, method: req.method, path: req.path });

  // Auth
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return problem(res, 401, 'Unauthorized', 'Missing bearer token', reqId);

  let caller: JwtPayload;
  try { caller = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload; }
  catch { return problem(res, 401, 'Unauthorized', 'Invalid or expired token', reqId); }

  const { id } = req.params;
  if (caller.role !== 'admin' && caller.sub !== id)
    return problem(res, 403, 'Forbidden', 'You may only send messages as yourself', reqId);

  // Rate limit
  if (!consume(caller.sub))
    return problem(res, 429, 'Too Many Requests',
      'Rate limit exceeded. Retry after 60 seconds.', reqId);

  // Validate
  const parsed = MessageSchema.safeParse(req.body);
  if (!parsed.success)
    return problem(res, 422, 'Unprocessable Entity',
      parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '), reqId);

  const { content, priority } = parsed.data;

  // Business logic (placeholder)
  log('info', 'message_sent', { reqId, userId: id, priority, length: content.length });
  res.status(201).json({ id: randomUUID(), userId: id, content, priority,
    createdAt: new Date().toISOString() });
}

Analyse: GPT-5.5s Herangehensweise

GPT-5.5 schrieb den Code in einem einzigen, kompakten Block – 72 Zeilen, keine separaten Dateien, minimale Kommentare. Der Token-Bucket-Algorithmus ist korrekt implementiert: kontinuierliches Refilling basierend auf verstrichener Zeit, nicht auf festen Zeitfenstern. Das RFC-7807-Format ist vollständig umgesetzt, inklusive des type-URI-Feldes. Das Logging ist strukturiert und enthält alle relevanten Felder.

Auffällig ist der Stil: GPT-5.5 nutzt konsequent einzeilige Arrow-Functions, kompakte Ternary-Operatoren und inline return-Statements. Der Code liest sich wie von einem erfahrenen Senior-Entwickler geschrieben, der unter Zeitdruck steht – funktional korrekt, aber ohne Rücksicht auf Lesbarkeit für Junior-Entwickler. Es gibt keine Erklärung des Token-Bucket-Algorithmus, keine Hinweise auf mögliche Produktionsprobleme (z.B. In-Memory-Bucket bei mehreren Prozessen).


Claude Opus 4.7: Der durchdachte Architekt

Claude Opus 4.7 lieferte seinen Code in 2 Minuten 14 Sekunden (Time-to-Complete), mit einer Time-to-First-Token von 0,4 Sekunden. Das Modell begann mit einer kurzen Architektur-Übersicht, bevor es den Code schrieb, und schloss mit einer detaillierten Analyse von Produktions-Caveats.

Der Code von Claude Opus 4.7

typescript
/**
 * POST /api/v1/users/:id/messages
 *
 * Sends a message on behalf of a user.
 * Auth:       JWT bearer – caller must be the target user OR an admin.
 * Rate limit: Token-bucket, 10 req/min sustained, burst up to 20.
 * Validation: Zod schema (content: 1-2000 chars, priority: low|medium|high).
 * Errors:     RFC 7807 Problem Details (application/problem+json).
 * Logging:    Structured JSON with request-id tracing.
 *
 * ⚠️  Production note: The in-memory token bucket does NOT work correctly
 *     across multiple Node.js processes or pods. Replace `TokenBucketStore`
 *     with a Redis-backed implementation before deploying to a cluster.
 */

import { Request, Response, NextFunction } from 'express';
import { z, ZodError } from 'zod';
import jwt, { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { randomUUID } from 'crypto';

// ─────────────────────────────────────────────────────────────────────────────
// Domain types
// ─────────────────────────────────────────────────────────────────────────────

type UserRole = 'user' | 'admin';

interface AuthenticatedCaller {
  sub: string;
  role: UserRole;
  iat: number;
  exp: number;
}

/** RFC 7807 Problem Details (https://www.rfc-editor.org/rfc/rfc7807) */
interface ProblemDetails {
  type: string;
  title: string;
  status: number;
  detail: string;
  instance: string;
  /** Non-standard extension: correlates logs with HTTP responses */
  requestId: string;
}

interface MessageResponse {
  id: string;
  userId: string;
  content: string;
  priority: 'low' | 'medium' | 'high';
  createdAt: string;
}

// ─────────────────────────────────────────────────────────────────────────────
// Token Bucket Rate Limiter
// ─────────────────────────────────────────────────────────────────────────────

interface BucketState {
  /** Current token count (fractional tokens are allowed during calculation) */
  tokens: number;
  lastRefillMs: number;
}

interface TokenBucketConfig {
  /** Sustained rate: tokens added per millisecond */
  ratePerMs: number;
  /** Maximum burst capacity */
  capacity: number;
}

/**
 * In-memory token bucket store.
 *
 * Algorithm: Continuous token refill (not fixed windows).
 * Each `consume()` call calculates tokens earned since the last call
 * and adds them to the bucket before attempting to consume one token.
 *
 * Time complexity: O(1) per consume call.
 * Space complexity: O(n) where n = number of unique user IDs seen.
 */
class TokenBucketStore {
  private readonly buckets = new Map<string, BucketState>();
  private readonly config: TokenBucketConfig;

  constructor(config: TokenBucketConfig) {
    this.config = config;
  }

  /**
   * Attempts to consume one token for the given key.
   * @returns `true` if the request is allowed, `false` if rate-limited.
   */
  consume(key: string): boolean {
    const now = Date.now();
    const state = this.buckets.get(key) ?? {
      tokens: this.config.capacity,
      lastRefillMs: now,
    };

    // Refill tokens based on elapsed time
    const elapsedMs = now - state.lastRefillMs;
    const newTokens = Math.min(
      this.config.capacity,
      state.tokens + elapsedMs * this.config.ratePerMs,
    );

    if (newTokens < 1) {
      // Update lastRefill even on rejection to avoid token accumulation
      this.buckets.set(key, { tokens: newTokens, lastRefillMs: now });
      return false;
    }

    this.buckets.set(key, { tokens: newTokens - 1, lastRefillMs: now });
    return true;
  }
}

// Singleton: 10 tokens/minute = 10/60000 tokens/ms, burst capacity 20
const rateLimiter = new TokenBucketStore({
  ratePerMs: 10 / 60_000,
  capacity: 20,
});

// ─────────────────────────────────────────────────────────────────────────────
// Input validation schema
// ─────────────────────────────────────────────────────────────────────────────

const MessageBodySchema = z.object({
  content: z
    .string({ required_error: 'content is required' })
    .min(1, 'content must not be empty')
    .max(2000, 'content must not exceed 2000 characters'),
  priority: z
    .enum(['low', 'medium', 'high'], {
      errorMap: () => ({ message: "priority must be 'low', 'medium', or 'high'" }),
    })
    .default('medium'),
});

type MessageBody = z.infer<typeof MessageBodySchema>;

// ─────────────────────────────────────────────────────────────────────────────
// Utilities
// ─────────────────────────────────────────────────────────────────────────────

const BASE_URL = process.env.API_BASE_URL ?? 'https://agentic-coding.ch';

function sendProblem(
  res: Response,
  status: number,
  title: string,
  detail: string,
  requestId: string,
): void {
  const body: ProblemDetails = {
    type: `${BASE_URL}/problems/${encodeURIComponent(title.toLowerCase().replace(/\s+/g, '-'))}`,
    title,
    status,
    detail,
    instance: res.req.originalUrl,
    requestId,
  };
  res
    .status(status)
    .setHeader('Content-Type', 'application/problem+json')
    .json(body);
}

function structuredLog(
  level: 'info' | 'warn' | 'error',
  message: string,
  context: Record<string, unknown>,
): void {
  // In production, replace with a proper logger (pino, winston, etc.)
  process.stdout.write(
    JSON.stringify({
      timestamp: new Date().toISOString(),
      level,
      message,
      ...context,
    }) + '\n',
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Route handler
// ─────────────────────────────────────────────────────────────────────────────

export async function postMessage(
  req: Request,
  res: Response,
  next: NextFunction,
): Promise<void> {
  const requestId = randomUUID();

  structuredLog('info', 'Incoming request', {
    requestId,
    method: req.method,
    path: req.path,
    params: req.params,
    ip: req.ip,
  });

  // ── 1. Authentication ────────────────────────────────────────────────────
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    sendProblem(res, 401, 'Unauthorized', 'Authorization header with Bearer token is required.', requestId);
    return;
  }

  const token = authHeader.slice(7);
  let caller: AuthenticatedCaller;

  try {
    caller = jwt.verify(token, process.env.JWT_SECRET!) as AuthenticatedCaller;
  } catch (err) {
    const detail =
      err instanceof TokenExpiredError
        ? 'The provided token has expired. Please re-authenticate.'
        : err instanceof JsonWebTokenError
          ? 'The provided token is invalid or malformed.'
          : 'Token verification failed.';

    structuredLog('warn', 'Authentication failed', { requestId, reason: detail });
    sendProblem(res, 401, 'Unauthorized', detail, requestId);
    return;
  }

  // ── 2. Authorization ─────────────────────────────────────────────────────
  const { id: targetUserId } = req.params;
  const isOwner = caller.sub === targetUserId;
  const isAdmin = caller.role === 'admin';

  if (!isOwner && !isAdmin) {
    structuredLog('warn', 'Authorization denied', {
      requestId, callerId: caller.sub, targetUserId,
    });
    sendProblem(
      res, 403, 'Forbidden',
      'You are not authorized to send messages on behalf of this user.',
      requestId,
    );
    return;
  }

  // ── 3. Rate limiting ─────────────────────────────────────────────────────
  if (!rateLimiter.consume(caller.sub)) {
    structuredLog('warn', 'Rate limit exceeded', { requestId, userId: caller.sub });
    res.setHeader('Retry-After', '60');
    sendProblem(
      res, 429, 'Too Many Requests',
      'You have exceeded the rate limit of 10 requests per minute. Please wait before retrying.',
      requestId,
    );
    return;
  }

  // ── 4. Input validation ──────────────────────────────────────────────────
  const parseResult = MessageBodySchema.safeParse(req.body);
  if (!parseResult.success) {
    const issues = parseResult.error.issues
      .map(issue => `${issue.path.join('.') || 'body'}: ${issue.message}`)
      .join('; ');

    structuredLog('info', 'Validation failed', { requestId, issues });
    sendProblem(res, 422, 'Unprocessable Entity', issues, requestId);
    return;
  }

  const { content, priority }: MessageBody = parseResult.data;

  // ── 5. Business logic ────────────────────────────────────────────────────
  // TODO: Persist the message to the database here.
  const messageId = randomUUID();

  structuredLog('info', 'Message created', {
    requestId,
    messageId,
    userId: targetUserId,
    priority,
    contentLength: content.length,
    sentByAdmin: isAdmin && !isOwner,
  });

  const response: MessageResponse = {
    id: messageId,
    userId: targetUserId,
    content,
    priority,
    createdAt: new Date().toISOString(),
  };

  res.status(201).json(response);
}

Analyse: Claude Opus 4.7s Herangehensweise

Claude Opus 4.7 schrieb 185 Zeilen – mehr als doppelt so viel wie GPT-5.5. Der Code ist in klar getrennte Sektionen gegliedert: Domain Types, Token Bucket, Validation Schema, Utilities, Route Handler. Jede Sektion hat einen erklärenden Kommentar-Header. Die TokenBucketStore-Klasse enthält eine vollständige JSDoc-Dokumentation inklusive Algorithmus-Beschreibung, Zeit- und Speicherkomplexität.

Besonders auffällig ist der Produktions-Caveat ganz oben im File:

⚠️ Production note: The in-memory token bucket does NOT work correctly across multiple Node.js processes or pods. Replace TokenBucketStore with a Redis-backed implementation before deploying to a cluster.

GPT-5.5 hat dieses Problem nicht erwähnt. Claude Opus 4.7 hat es nicht nur erkannt, sondern auch eine konkrete Lösung benannt. Das ist der Unterschied zwischen einem Modell, das Code generiert, und einem Modell, das Code reviewt, während es ihn schreibt.

Weitere Unterschiede: Claude differenziert zwischen TokenExpiredError und JsonWebTokenError für präzisere Fehlermeldungen. Es setzt den Retry-After-Header beim 429-Response (RFC-konform). Es loggt sentByAdmin: true wenn ein Admin im Namen eines Nutzers schreibt – ein Audit-Trail-Feature, das GPT-5.5 nicht implementiert hat.


Direktvergleich: Sieben Dimensionen

KriteriumGPT-5.5Claude Opus 4.7
Time-to-First-Token3,1 s0,4 s
Time-to-Complete47 s2 min 14 s
Codezeilen72185
Token-Bucket korrektJaJa
RFC 7807 vollständigJa (ohne Retry-After)Ja (inkl. Retry-After)
JWT-Fehlertypen differenziertNein (generisch)Ja (Expired vs. Invalid)
Produktions-Caveats dokumentiertNeinJa (Redis-Hinweis)
Audit-Log (Admin-Aktion)NeinJa
Klassen vs. FunktionenFunktionenKlasse + Funktionen
Kommentare / ErklärungenMinimalAusführlich
Lesbarkeit für Junior-DevsMittelHoch
Lesbarkeit für Senior-DevsHochHoch

Was passiert, wenn der Code fehlerhaft ist?

Um die Fehlerkorrektur-Fähigkeiten zu testen, wurde beiden Modellen anschliessend ein absichtlich fehlerhafter Code-Schnipsel gezeigt – ein Token-Bucket, der Tokens in festen Zeitfenstern statt kontinuierlich auffüllt (ein häufiger Fehler):

typescript
// Fehlerhafter Code (Fixed-Window statt Token-Bucket)
function consume(userId: string): boolean {
  const now = Date.now();
  const windowStart = Math.floor(now / 60_000) * 60_000;
  let b = buckets.get(userId) ?? { count: 0, windowStart };
  if (b.windowStart !== windowStart) { b = { count: 0, windowStart }; }
  if (b.count >= 10) return false;
  b.count++;
  buckets.set(userId, b);
  return true;
}

GPT-5.5 identifizierte den Fehler sofort: "This is a fixed-window counter, not a token bucket. It allows 10 requests in the last second of a window and 10 more in the first second of the next – a total of 20 in 2 seconds, violating your burst limit." Es lieferte direkt den korrigierten Code ohne weitere Erklärung.

Claude Opus 4.7 identifizierte denselben Fehler, erklärte ihn aber ausführlicher mit einem Zeitstrahl-Beispiel und wies zusätzlich auf einen zweiten, subtileren Bug hin: "There's also a race condition if multiple requests arrive at the exact same millisecond – Map.get and Map.set are not atomic. In a single-threaded Node.js environment this is harmless, but worth documenting." Auch hier lieferte es den korrigierten Code.


Wann welches Modell wählen?

Die Ergebnisse dieses Tests bestätigen das Muster, das sich bereits in den Benchmarks andeutet: GPT-5.5 und Claude Opus 4.7 sind keine direkten Konkurrenten, sondern zwei verschiedene Werkzeuge für verschiedene Phasen des Entwicklungsprozesses.

SituationEmpfehlungBegründung
Schnelle Implementierung, Spec ist klarGPT-5.53× schneller, kompakterer Code
Code-Review und Architektur-FeedbackClaude Opus 4.7Erkennt Produktions-Caveats, erklärt Bugs
Junior-Entwickler im TeamClaude Opus 4.7Ausführliche Kommentare, Lerneffekt
Erfahrenes Team, hoher DurchsatzGPT-5.5Weniger Token, schnellere Iteration
Sicherheitskritischer CodeClaude Opus 4.7Differenzierte Fehlertypen, Audit-Logs
Prototyping und ExplorationGPT-5.5Sofort lauffähig, minimal overhead
Produktions-Readiness-CheckClaude Opus 4.7Proaktive Caveat-Dokumentation

Fazit: Zwei Modelle, zwei Rollen

Dieser Praxis-Test zeigt etwas, das Benchmarks nicht zeigen können: den Charakter eines Modells. GPT-5.5 schreibt wie ein erfahrener Senior-Entwickler unter Zeitdruck – präzise, kompakt, funktional korrekt, aber ohne Rücksicht auf den nächsten Entwickler, der den Code lesen muss. Claude Opus 4.7 schreibt wie ein Tech-Lead, der weiss, dass Code mehr Zeit gelesen als geschrieben wird – ausführlich, selbsterklärend, mit proaktiven Warnungen vor Produktionsproblemen.

Keiner der beiden Stile ist falsch. In einem professionellen Entwicklungsprozess haben beide ihren Platz: GPT-5.5 für die schnelle Implementierungsphase, Claude Opus 4.7 für Review, Dokumentation und Architektur-Entscheidungen. Die klügste Strategie ist nicht, sich für ein Modell zu entscheiden – sondern zu wissen, welches Werkzeug für welche Phase des Entwicklungsprozesses das richtige ist.


Quellen: OpenAI – Introducing GPT-5.5 | Anthropic – Introducing Claude Opus 4.7 | Reddit r/openclaw – 24h Benchmark | RFC 7807 – Problem Details for HTTP APIs

Artikel teilen

Cookie-Einstellungen

Wir verwenden Cookies und ähnliche Technologien, um die Nutzung unserer Website zu analysieren und Ihnen ein besseres Erlebnis zu bieten. Mit Ihrer Zustimmung verwenden wir Google Analytics, um anonymisierte Statistiken über die Websitenutzung zu sammeln. Sie können Ihre Einwilligung jederzeit widerrufen.