Architektur zur Bewältigung von Serverless Cold Starts und Kosten beim Vercel Eve Agenten
TuBrief 편집팀
2026년 7월 23일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
Bringt man Vercel Eve-basierte Agenten in Produktion, wird man direkt mit der für Serverless typischen Zustandslosigkeit (Statelessness) konfrontiert. Nach Abschluss einer Anfrage wird die Instanz geschlossen und der Ausführungszustand geht verloren. Greift man jedoch zur Wiederherstellung der Sitzung jedes Mal auf eine Datenbank wie PostgreSQL zu, führt dies zu Latenzen von über 100 ms und rapide steigenden Datenbankkosten.
Um diese Serverless-Einschränkungen zu überwinden, sind hier drei Architekturen zusammengefasst, die in echten Produktionsumgebungen eingesetzt werden.
Das jedesmalige Neuaufbauen von Datenbankverbindungen in einer Serverless-Umgebung ist der schnellste Weg, sowohl Infrastrukturkosten als auch Antwortzeiten zu ruinieren. Wenn man Upstash Redis, das über eine HTTP REST API kommuniziert, als Sitzungscache-Layer einsetzt, lässt sich dieses Problem lösen.
`
[User Request]
│
▼
┌──────────────┐ < 50ms (HTTP REST) ┌────────────────────────┐
│ Vercel Eve │ ────────────────────────> │ Upstash Redis │
│ Agent │ <──────────────────────── │ (Session State Storage)│
└──────────────┘ Session Context Restored└────────────────────────┘
│
│ Compress History (Sliding Window + Summary)
▼
┌──────────────┐
│ LLM Provider │
└──────────────┘
`
Sitzungsdaten werden über die REST API in unter 50 ms abgerufen. Das Ersetzen direkter RDB-Abfragen durch Cache-Zugriffe reduziert den Verbrauch der Read Capacity erheblich.
| Bewertungskriterium | Traditionelle RDB (PostgreSQL) | DynamoDB (On-Demand) | Upstash Redis (HTTP REST) |
|---|---|---|---|
| Verbindungsart | TCP Socket | AWS SDK | HTTP/REST API |
| Durchschnittliche Lese-Latenz | 50ms - 200ms | 10ms - 20ms | 1ms - 5ms (Edge < 50ms) |
| Serverless-Eignung | Niedrig (Connection Exhaustion) | Mittel (Verbindungsverzögerung vorhanden) | Sehr hoch (Scale-to-Zero-Unterstützung) |
| Kostenstruktur | Abrechnung pro Stunde der bereitgestellten Instanz | Abrechnung pro RCU/WCU-Anfrageeinheit | Abrechnung pro Command-Anfrageeinheit ($0.20/100k) |
| Haupteinsatzzweck | ACID-Transaktionen, Originalspeicherung | Permanente Datenspeicherung und -suche | Sitzungscaching, Rate Limit, Agentenspeicher |
Der Code zum Speichern und Wiederherstellen von Sitzungen wird einfach gehalten.
`typescript
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
interface AgentSessionContext {
userId: string;
currentStep: string;
intermediateThoughts: Record<string, unknown>[];
lastActiveTimestamp: number;
}
export async function restoreSessionContext(sessionId: string): Promise<AgentSessionContext | null> {
const cacheKey = session:context:${sessionId};
const cachedContext = await redis.get(cacheKey);
return cachedContext ?? null;
}
export async function saveSessionContext(
sessionId: string,
context: AgentSessionContext,
ttlSeconds: number = 3600
): Promise {
const cacheKey = session:context:${sessionId};
await redis.set(cacheKey, JSON.stringify(context), { ex: ttlSeconds });
}
`
Wenn Gespräche länger werden, steigen die Token-Kosten kontinuierlich. Es wird ein Ansatz gewählt, bei dem nur die letzten 6 Turns im Original belassen und ältere Gespräche mit einem leichtgewichtigen Modell zusammengefasst und oben im Prompt platziert werden.
`typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
interface Message {
role: "user" | "assistant" | "system";
content: string;
}
export async function compressConversationHistory(
messages: Message[],
recentWindowSize: number = 6
): Promise<Message[]> {
if (messages.length <= recentWindowSize) return messages;
const systemMessage = messages.find((m) => m.role === "system");
const nonSystemMessages = messages.filter((m) => m.role !== "system");
const olderMessages = nonSystemMessages.slice(0, nonSystemMessages.length - recentWindowSize);
const recentMessages = nonSystemMessages.slice(nonSystemMessages.length - recentWindowSize);
const summaryResponse = await generateText({
model: openai("gpt-4o-mini"), prompt: `Fassen Sie nur die Kernfakten und Entscheidungen der folgenden Konversation in unter 200 Zeichen zusammen:
${JSON.stringify(olderMessages)}`,
});
const compressedHistory: Message[] = [];
if (systemMessage) compressedHistory.push(systemMessage);
compressedHistory.push({
role: "system",
content: [Zusammenfassung der bisherigen Konversation]: ${summaryResponse.text},
});
compressedHistory.push(...recentMessages);
return compressedHistory;
}
`
Treffen Aufrufe externer Tools auf 429- (Rate Limit) oder 5xx-Fehler, bricht die gesamte Inferenz des Agenten zusammen. Es müssen exponentieller Backoff mit Full Jitter und Circuit Breaker implementiert werden.
Die Formel für den exponentiellen Backoff erhöht die Wartezeit nicht rein proportional, sondern mischt Zufallswerte ein, um Engpässe zu vermeiden.
`typescript
export interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
}
export async function executeWithExponentialBackoff(
fn: () => Promise,
config: RetryConfig = { maxRetries: 3, baseDelayMs: 200, maxDelayMs: 8000 }
): Promise {
let attempt = 0;
while (true) {
try {
return await fn();
} catch (error: any) {
attempt++;
const statusCode = error?.status || error?.response?.status;
const isUnretryable = statusCode && statusCode >= 400 && statusCode < 500 && statusCode !== 429;
if (attempt > config.maxRetries || isUnretryable) throw error;
const calculatedDelay = Math.min(
config.maxDelayMs,
config.baseDelayMs * Math.pow(2, attempt)
);
const jitteredDelay = calculatedDelay * (0.5 + Math.random());
await new Promise((resolve) => setTimeout(resolve, jitteredDelay));
}
}
}
`
Dauern Störungen länger an, blockiert der Circuit Breaker Anfragen sofort (Fail-Fast) und führt eine Fallback-Logik aus.
| Antwortstatus der externen API | Circuit-Breaker-Status | Funktionsmechanismus | Agenten-Verarbeitungsergebnis |
|---|---|---|---|
| HTTP 200 OK | Closed | Normaler Durchlass und Erhöhung des Erfolgszählers | Normale Einspeisung externer Daten in den Agenten |
| HTTP 429 / 503 | Closed $ | ||
| ightarrow$ Open | Ausführung des exponentiellen Backoffs; bei Erreichen der Fehlerratenschwelle $ | ||
| ightarrow$ Open | Abbrechen nach Wiederholungsversuchen und Öffnen des Schaltkreises | ||
| Circuit OPEN Status | Open | Blockieren von Netzwerkanfragen an externe APIs (Fail-Fast) | Verwendung alternativer Tools oder Ausgabe einer Fallback-Nachricht |
| Nach Ablaufen des Cooldowns | Half-Open | Überprüfung der Wiederherstellung des externen Dienstes durch eine einzelne Probing-Anfrage | Bei Erfolg Normalisierung des Schaltkreises, bei Misserfolg erneutes Blockieren |
`typescript
export class CircuitBreaker {
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private failureCount = 0;
private lastStateChange = Date.now();
constructor(
private failureThreshold: number = 5,
private cooldownPeriodMs: number = 30000
) {}
async execute(requestFn: () => Promise, fallbackFn: () => Promise): Promise {
const now = Date.now();
if (this.state === 'OPEN') {
if (now - this.lastStateChange > this.cooldownPeriodMs) {
this.state = 'HALF_OPEN';
this.lastStateChange = now;
} else {
return await fallbackFn();
}
}
try {
const result = await requestFn();
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
this.failureCount = 0;
this.lastStateChange = now;
}
return result;
} catch (error) {
this.failureCount++;
if (this.failureCount >= this.failureThreshold || this.state === 'HALF_OPEN') {
this.state = 'OPEN';
this.lastStateChange = now;
}
return await fallbackFn();
}
}
}
`
Serverless-Funktionen unterliegen Ausführungszeitbegrenzungen. Läst man Anfragen offen stehen, während man auf Zahlungs- oder DB-Löschgenehmigungen wartet, kommt es zu Timeout-Fehlern.
`
[Agent Action] ──> Eve Tool (needsApproval: true)
│
▼
[Checkpoint Saved & Instance Terminated]
│
├─> Slack Notification (Interactive Card)
│
[Human Approve] ───────>│ (Webhook POST Callback)
│
▼
[Resume Agent & Proceed Transaction]
`
Dem Eve-Tool wird needsApproval: true zugewiesen, um die Ausführung zu stoppen und nur einen Checkpoint zu hinterlassen.
`typescript
import { defineTool } from "@vercel/eve";
import { z } from "zod";
export const deleteDatabaseTool = defineTool({
name: "delete_database",
description: "Löscht permanente Datenbankeinträge eines bestimmten Mandanten.",
needsApproval: true,
input: z.object({
tenantId: z.string(),
reason: z.string(),
}),
execute: async ({ tenantId }) => {
return await db.tenant.delete({ where: { id: tenantId } });
},
});
`
Menschliche Genehmigungen werden über Webhook-Callbacks empfangen, um den Prozess fortzusetzen.
`typescript
import { createWebhook } from "@vercel/workflows";
export async function handleApprovalWorkflow(event: { approvalId: string; payload: any }) {
const webhook = createWebhook();
await sendSlackApprovalCard({
approvalId: event.approvalId,
callbackUrl: webhook.url,
payload: event.payload,
});
try {
const { approved, userReason } = await webhook.timeout("12h");
if (!approved) {
await rollbackPreviousSteps(event.payload);
return { status: "REJECTED", reason: userReason };
}
return await proceedAction(event.payload);
} catch (error) {
await rollbackPreviousSteps(event.payload);
return { status: "TIMEOUT_CANCELLED" };
}
}
`
Halluzinationsphänomene, die nach Änderungen an Prompts auftreten, lassen sich durch manuelles Testen nur schwer erfassen. Die Pipeline wird so aufgebaut, dass ein PR erst gemergt werden kann, wenn die DeepEval-Metriken bestanden wurden.
| Bewertungsmetrik | Akzeptanzschwelle | Bewertungskriterium |
|---|---|---|
| Faithfulness | Vorhandensein von Faktenverzerrungen im Vergleich zum bereitgestellten Kontext | |
| Answer Relevancy | Übereinstimmung mit dem Zweck der Benutzerfrage | |
| Hallucination Rate | Anteil von Halluzinationen im Test-Set | |
| Tool Calling Accuracy | Auswahltreue korrekter OpenAPI-Spezifikations-Tools und Typeneinhaltung |
In GitHub Actions wird Pytest ausgeführt, um den Build zu blockieren, wenn die Schwellenwerte nicht erreicht werden.
`yaml
name: Eve Agent Prompt Evaluation Pipeline
on:
pull_request:
branches: [ main ]
jobs:
evaluate-agent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Evaluation Dependencies
run: |
pip install deepeval pytest
- name: Run DeepEval Regression Suite
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
pytest test_agent_evals.py --deepeval-metric-threshold=0.85
`
Bei der Bereitstellung werden Edge Config und Middleware gekoppelt, um den neuen Prompt zunächst nur auf 10 % des Traffics anzuwenden.
`typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { get } from '@vercel/edge-config';
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
let variant = req.cookies.get('agent_canary_variant')?.value;
if (!variant) {
const canaryRate = (await get('canary_traffic_rate')) || 0.10;
variant = Math.random() < canaryRate ? 'canary' : 'control';
res.cookies.set('agent_canary_variant', variant, { path: '/', httpOnly: true });
}
res.headers.set('x-agent-prompt-version', variant === 'canary' ? 'v2-canary' : 'v1-stable');
return res;
}
export const config = {
matcher: '/api/agent/:path*',
};
`