Vercel Eve エージェントのサーバーレスコールドスタートとコストを抑える構成
TuBrief Editorial
July 23, 2026
0
Computing/SoftwareWritten with AI assistance from the source video. The video is the authority.
More from the community
Comments (0)
Log in to leave a comment
No posts yet
Written with AI assistance from the source video. The video is the authority.
Log in to leave a comment
No posts yet
Vercel Eve ベースのエージェントをプロダクションに投入すると、サーバーレス特有の無状態性(Statelessness)にすぐさま直面する。リクエストが終わるとインスタンスが終了し、実行状態は消失する。だからといって、セッションを復元するために毎回 PostgreSQL のような DB にアクセスすると、100ms を超えるレイテンシとともに DB コストが急増してしまう。
サーバーレスの限界を超えるために、実際のプロダクションで採用されている3つの構成をまとめた。
サーバーレス環境で DB 接続を毎回新しく確立する行為は、インフラコストと応答速度の両方を悪化させる近道だ。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 の消費を大幅に削減できる。
| 評価項目 | 従来の RDB (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、エージェントメモリ |
セッションの保存および復元コードはシンプルに保持する。
`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 });
}
`
会話が長くなるとトークンコストが膨らみ続ける。直近の 6 ターンのみを原文のまま残し、それ以前の会話は軽量モデルで要約してプロンプトの先頭に配置する手法をとる。
`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;
}
`
外部ツールを呼び出す際に 429 (Rate Limit) や 5xx エラーに遭遇すると、エージェントの推論全体が破損してしまう。Full Jitter を組み込んだ指数バックオフとサーキットブレーカーを導入しておく必要がある。
指数バックオフの計算式は、待機時間を単純な正比例で増やすのではなく乱数を混ぜることでボトルネックを回避する。
`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 応答状態 | サーキットブレーカー状態 | 動作メカニズム | エージェント処理結果 |
|---|---|---|---|
| HTTP 200 OK | Closed | 正常通過および成功カウンター増加 | 外部データを正常にエージェントへ供給 |
| 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();
}
}
}
`
サーバーレス関数には実行時間の制限が存在する。決済や DB 削除の承認を待つためにリクエストを開いたまま保持すると、タイムアウトエラーが発生してしまう。
`
[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 を設定して実行を中断し、チェックポイントのみを保存する。
`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" };
}
}
`
プロンプトの変更後に生じるハルシネーション(幻覚)現象は、手動テストで検出するのが困難だ。DeepEval の指標をクリアしなければ PR がマージされないようパイプラインを構築する。
| 評価指標 | 許容閾値 | 評価基準 |
|---|---|---|
| Faithfulness | 提供された Context に対するファクトの歪みの有無 | |
| Answer Relevancy | ユーザーの質問目的との適合度 | |
| Hallucination Rate | テストセットにおけるハルシネーション発生率 | |
| Tool Calling Accuracy | 正しい 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 とミドルウェアを連携させ、トラフィックの 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*',
};
`