Arquitetura para resolver o Cold Start sem servidor e reduzir custos no Vercel Eve Agent
Ao colocar um agente baseado no Vercel Eve em produção, você se depara imediatamente com a ausência de estado (Statelessness) típica de ambientes serverless. Quando a requisição termina, a instância é fechada e o estado de execução é perdido. Por outro lado, consultar um banco de dados como o PostgreSQL a cada execução para restaurar a sessão gera um atraso superior a 100ms e dispara os custos de DB.
Para superar as limitações do serverless, resumi três estruturas usadas na prática em ambientes de produção.
1. Reduzindo a latência de restauração de sessão para menos de 50ms com Upstash Redis
Em ambientes serverless, criar uma nova conexão com o banco de dados a cada requisição é o caminho mais rápido para arruinar tanto os custos de infraestrutura quanto o tempo de resposta. Colocar o Upstash Redis, que se comunica via HTTP REST API, como uma camada de cache de sessão resolve esse problema.
[User Request] │ ▼ ┌──────────────┐ < 50ms (HTTP REST) ┌────────────────────────┐ │ Vercel Eve │ ────────────────────────> │ Upstash Redis │ │ Agent │ <──────────────────────── │ (Session State Storage)│ └──────────────┘ Session Context Restored└────────────────────────┘ │ │ Compress History (Sliding Window + Summary) ▼ ┌──────────────┐ │ LLM Provider │ └──────────────┘
Os dados da sessão são recuperados em menos de 50ms por meio de uma REST API. Substituir consultas diretas ao RDB pelo cache reduz drasticamente o consumo de Read Capacity.
| Item de Avaliação |
RDB Tradicional (PostgreSQL) |
DynamoDB (On-Demand) |
Upstash Redis (HTTP REST) |
| Tipo de Conexão |
TCP Socket |
AWS SDK |
HTTP/REST API |
| Latência Média de Leitura |
50ms - 200ms |
10ms - 20ms |
1ms - 5ms (Edge < 50ms) |
| Adequação para Serverless |
Baixa (Exaustão de Conexões) |
Média (Existe latência de conexão) |
Muito Alta (Suporta Scale-to-Zero) |
| Estrutura de Custo |
Cobrança por hora da instância provisionada |
Cobrança por unidade de requisição RCU/WCU |
Cobrança por unidade de requisição de comando ($0,20/100k) |
| Propósito Principal |
Transações ACID, armazenamento primário |
Armazenamento e busca de dados permanentes |
Cache de sessão, Rate Limit, memória do agente |
O código para salvar e restaurar a sessão é mantido de forma simples.
`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 });
}
`
Conforme a conversa se estende, o custo de tokens continua aumentando. Utilizamos a abordagem de manter apenas os últimos 6 turnos em seu formato original e resumir as conversas anteriores com um modelo leve, posicionando o resumo no topo do prompt.
`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: 다음 대화의 핵심 사실과 결정 사항만 200자 이내로 요약하세요:\n\n${JSON.stringify(olderMessages)},
});
const compressedHistory: Message[] = [];
if (systemMessage) compressedHistory.push(systemMessage);
compressedHistory.push({
role: "system",
content: [이전 대화 요약]: ${summaryResponse.text},
});
compressedHistory.push(...recentMessages);
return compressedHistory;
}
`
2. Padrões de proteção contra latência e falhas de APIs externas
Ao chamar ferramentas externas, encontrar erros 429 (Rate Limit) ou 5xx quebra toda a inferência do agente. É essencial implementar Backoff Exponencial com Full Jitter e um Circuit Breaker.
A fórmula do Backoff Exponencial evita gargalos ao misturar números aleatórios em vez de aumentar o tempo de espera de forma estritamente proporcional.
Textdelay=minleft(Textmax,Textbaseimes2extattemptight)imesleft(0.5+extrandom(0,1.0)ight)`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));
}
}
}
`
Se a falha persistir, o Circuit Breaker bloqueia imediatamente as requisições (Fail-Fast) e executa a lógica de Fallback.
| Status da Resposta da API Externa |
Estado do Circuit Breaker |
Mecanismo de Ação |
Resultado do Processamento no Agente |
| HTTP 200 OK |
Closed |
Fluxo normal e incremento no contador de sucessos |
Fornece dados externos normalmente ao agente |
| HTTP 429 / 503 |
Closed $ |
|
|
| ightarrow$ Open |
Executa Backoff Exponencial e muda para Open ao atingir o limite de taxa de falha |
Interrompe tentativas e abre o circuito |
|
| Estado Circuit OPEN |
Open |
Bloqueia requisições de rede para a API externa (Fail-Fast) |
Usa Tool alternativa ou exibe mensagem de Fallback |
| Após expiração do Cooldown |
Half-Open |
Valida a recuperação do serviço externo com uma única requisição de Probing |
Sucesso normaliza o circuito; falha bloqueia novamente |
`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();
}
}
}
`
3. Integração assíncrona para aprovação humana (Human-in-the-loop) sem Timeout
Funções serverless possuem limites de tempo de execução. Manter uma requisição aberta aguardando aprovação para pagamentos ou exclusão de banco de dados resultará em um erro de timeout.
[Agent Action] ──> Eve Tool (needsApproval: true) │ ▼ [Checkpoint Saved & Instance Terminated] │ ├─> Slack Notification (Interactive Card) │ [Human Approve] ───────>│ (Webhook POST Callback) │ ▼ [Resume Agent & Proceed Transaction]
Definimos needsApproval: true na ferramenta do Eve, pausamos a execução e salvamos apenas o checkpoint.
`typescript
import { defineTool } from "@vercel/eve";
import { z } from "zod";
export const deleteDatabaseTool = defineTool({
name: "delete_database",
description: "특정 테넌트의 영구 데이터베이스 레코드를 삭제합니다.",
needsApproval: true,
input: z.object({
tenantId: z.string(),
reason: z.string(),
}),
execute: async ({ tenantId }) => {
return await db.tenant.delete({ where: { id: tenantId } });
},
});
`
A aprovação humana é recebida via callback de webhook para retomar o processo.
`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" };
}
}
`
4. Validação de prompts e roteamento Canary na etapa de CI/CD
Alucinações que ocorrem após alterar um prompt são difíceis de capturar com testes manuais. Configuramos o pipeline para que o PR só seja mesclado se passar nas métricas do DeepEval.
| Métrica de Avaliação |
Limite Aceitável |
Critério de Avaliação |
| Faithfulness |
ge0.85 |
Presença ou ausência de distorção de fatos em relação ao Contexto fornecido |
| Answer Relevancy |
ge0.75 |
Grau de conformidade com a intenção da pergunta do usuário |
| Hallucination Rate |
le0.10 |
Proporção de alucinações no conjunto de testes |
| Tool Calling Accuracy |
ge0.90 |
Seleção correta da ferramenta da especificação OpenAPI e taxa de conformidade com tipos |
Executamos o Pytest no GitHub Actions para bloquear a build caso os limites não sejam atingidos.
`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
`
No momento do deploy, integramos o Edge Config ao middleware para aplicar o novo prompt inicialmente a apenas 10% do tráfego.
`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*',
};
`