解决 Vercel Eve Agent 无服务器冷启动与成本问题的架构设计
将基于 Vercel Eve 的 Agent 部署到生产环境时,会立即面临无服务器(Serverless)架构特有的无状态性(Statelessness)问题。当请求结束时,实例随之关闭,运行状态也随之消失。然而,如果为了复原会话而每次都查询 PostgreSQL 等数据库,不仅会导致超过 100ms 的延迟,还会使数据库成本激增。
为了突破无服务器架构的限制,本文总结了在实际生产环境中运用的三种架构设计。
1. 通过 Upstash Redis 将会话复原延迟降低至 50ms 以下
在无服务器环境中,每次都重新建立数据库连接是破坏基础设施成本和响应速度的“捷径”。通过引入基于 HTTP REST API 通信的 Upstash Redis 作为会话缓存层,可以有效解决这一问题。
`
[User Request]
│
▼
┌──────────────┐ < 50ms (HTTP REST) ┌────────────────────────┐
│ Vercel Eve │ ────────────────────────> │ Upstash Redis │
│ Agent │ <──────────────────────── │ (Session State Storage)│
└──────────────┘ Session Context Restored└────────────────────────┘
│
│ Compress History (Sliding Window + Summary)
▼
┌──────────────┐
│ LLM Provider │
└──────────────┘
`
会话数据通过 REST API 在 50ms 内获取完毕。用缓存替代直接查询关系型数据库(RDB),可以大幅减少 Read Capacity(读取容量)的消耗。
| 评估项目 |
传统关系型数据库 (PostgreSQL) |
DynamoDB (On-Demand) |
Upstash Redis (HTTP REST) |
| 连接方式 |
TCP Socket |
AWS SDK |
HTTP/REST API |
| 平均读取延迟 |
50ms - 200ms |
10ms - 20ms |
1ms - 5ms (Edge < 50ms) |
| 无服务器适用性 |
较低 (Connection Exhaustion) |
中等 (存在连接延迟) |
极高 (支持 Scale-to-Zero) |
| 成本结构 |
按预置实例时长计费 |
按 RCU/WCU 请求单位计费 |
按 Command 请求单位计费 ($0.20/100k) |
| 主要使用目的 |
ACID 事务、原始数据存储 |
持久化数据存储与查询 |
会话缓存、Rate Limit、Agent 内存 |
会话存储与复原的代码保持简单易懂:
`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 });
}
`
随着对话增长,Token 成本也会不断上升。我们采用仅保留最近 6 轮对话的原始数据,并使用轻量模型对早期对话进行总结,然后放置于 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. 外部 API 延迟与失败防御模式
在调用外部工具时,如果遇到 429 (Rate Limit) 或 5xx 错误,可能导致整个 Agent 推理崩溃。因此需要铺设融入 Full Jitter 的指数退避(Exponential Backoff)和熔断器(Circuit Breaker)。
指数退避公式通过不按等比单纯增加等待时间,而是加入随机数,以避免峰值拥堵:
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));
}
}
}
`
当故障持续时,利用熔断器立即阻断请求(Fail-Fast),并触发 Fallback(降级)逻辑。
| 外部 API 响应状态 |
熔断器状态 |
运行机制 |
Agent 处理结果 |
| HTTP 200 OK |
Closed |
正常通过并增加成功计数器 |
将外部数据正常供给给 Agent |
| HTTP 429 / 503 |
Closed $ |
|
|
| ightarrow$ Open |
执行指数退避,达到失败率阈值时转为 Open |
重试后开启熔断器 |
|
| Circuit OPEN 状态 |
Open |
阻断外部 API 网络请求 (Fail-Fast) |
使用替代 Tool 或输出 Fallback 消息 |
| Cooldown 到期后 |
Half-Open |
通过单次 Probing 请求验证外部服务恢复情况 |
成功时恢复正常,失败时重新阻断 |
`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. 无超时限制的人工介入(Human-in-the-loop)异步审批联动
无服务器函数存在执行时间限制。如果为了等待支付或数据库删除审批而一直维持请求连接,会导致超时错误。
`
[Agent Action] ──> Eve Tool (needsApproval: true)
│
▼
[Checkpoint Saved & Instance Terminated]
│
├─> Slack Notification (Interactive Card)
│
[Human Approve] ───────>│ (Webhook POST Callback)
│
▼
[Resume Agent & Proceed Transaction]
`
在 Eve 工具中设置 needsApproval: true 并暂停执行,仅保留检查点(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 } });
},
});
`
人工审批通过 Webhook 回调接收,从而恢复执行流程。
`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. CI/CD 阶段的 Prompt 验证与金丝雀路由
修改 Prompt 后产生的幻觉现象很难通过人工测试完全捕捉。通过构建流水线,只有通过 DeepEval 指标测试的 PR 才能被合并。
| 评估指标 |
可接受阈值 |
评估标准 |
| Faithfulness |
ge0.85 |
相较于提供的 Context,是否存在事实扭曲 |
| Answer Relevancy |
ge0.75 |
与用户提问目的的契合度 |
| Hallucination Rate |
le0.10 |
测试集中发生幻觉的比例 |
| Tool Calling Accuracy |
ge0.90 |
正确选择 OpenAPI 规范工具及类型遵循率 |
在 GitHub Actions 中运行 Pytest,未达到阈值时自动阻止构建。
`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
`
部署时结合 Edge Config 和中间件,将新 Prompt 仅先应用到 10% 的流量上。
`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*',
};
`