TuBrief
Subscribed Channels
Videos
Community

Vercel Eve 에이전트의 서버리스 콜드 스타트와 비용을 잡는 구조

TuBrief Editorial
July 23, 2026
0
컴퓨터/소프트웨어

Written with AI assistance from the source video. The video is the authority.

한국어EnglishEspañol中文हिन्दीDeutschFrançaisРусскийPortuguês日本語Bahasa Indonesiaالعربية

Related Video

Ship 26 NYC - 워크숍 - Eve와 함께 에이전트 구축하기: 오픈 에이전트 프레임워크39:50

Ship 26 NYC - 워크숍 - Eve와 함께 에이전트 구축하기: 오픈 에이전트 프레임워크

Vercel

More from the community

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

September 13, 2026

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

September 13, 2026

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

September 13, 2026

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

September 13, 2026

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

September 12, 2026

Apple Won the AI Race

September 12, 2026

Comments (0)

Log in to leave a comment

No posts yet

© 2026 . All rights reserved.

TuBrief
Subscribed Channels
Videos
Community
Log in

Vercel Eve 에이전트의 서버리스 콜드 스타트와 비용을 잡는 구조

Vercel Eve 기반 에이전트를 프로덕션에 올려보면 서버리스 특유의 무상태성(Statelessness)에 바로 직면한다. 요청이 끝나면 인스턴스가 닫히고 실행 상태는 날아간다. 그렇다고 세션을 복원하려고 매번 PostgreSQL 같은 DB를 치면 100ms 넘는 지연과 함께 DB 비용이 급증한다.

서버리스 한계를 넘기 위해 실제 프로덕션에서 쓰는 세 가지 구조를 정리했다.

1. Upstash Redis로 세션 복원 Latency 50ms 밑으로 낮추기

서버리스 환경에서 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, 에이전트 메모리

세션 저장 및 복원 코드는 단순하게 가져간다.

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<AgentSessionContext>(cacheKey);
  return cachedContext ?? null;
}

export async function saveSessionContext(
  sessionId: string,
  context: AgentSessionContext,
  ttlSeconds: number = 3600
): Promise<void> {
  const cacheKey = `session:context:${sessionId}`;
  await redis.set(cacheKey, JSON.stringify(context), { ex: ttlSeconds });
}

대화가 길어지면 토큰 비용이 계속 늘어난다. 최근 6개 턴만 원본으로 남기고, 이전 대화는 경량 모델로 요약해 프롬프트 상단에 배치하는 방식을 쓴다.

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 에러를 만나면 에이전트 추론 전체가 깨진다. Full Jitter를 섞은 지수 백오프와 서킷 브레이커를 깔아두어야 한다.

지수 백오프 공식은 대기 시간을 정비례로 늘리지 않고 난수를 섞어 병목을 피한다.

Tdelay=min⁡(Tmax,Tbase×2attempt)×(0.5+random(0,1.0))T_{\text{delay}} = \min\left(T_{\text{max}}, T_{\text{base}} \times 2^{\text{attempt}}\right) \times \left(0.5 + \text{random}(0, 1.0)\right)Tdelay​=min(Tmax​,Tbase​×2attempt)×(0.5+random(0,1.0))
export interface RetryConfig {
  maxRetries: number;
  baseDelayMs: number;
  maxDelayMs: number;
}

export async function executeWithExponentialBackoff<T>(
  fn: () => Promise<T>,
  config: RetryConfig = { maxRetries: 3, baseDelayMs: 200, maxDelayMs: 8000 }
): Promise<T> {
  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 →\rightarrow→ Open 지수 백오프 실행 후 실패율 임계치 달성 시 Open 재시도 후 서킷 개방
Circuit OPEN 상태 Open 외부 API 네트워크 요청 차단 (Fail-Fast) 대체 Tool 사용 또는 Fallback 메시지 출력
Cooldown 만료 후 Half-Open 단일 Probing 요청으로 외부 서비스 복구 여부 검증 성공 시 서킷 정상화, 실패 시 서킷 재차단
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<T>(requestFn: () => Promise<T>, fallbackFn: () => Promise<T>): Promise<T> {
    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) 연동

서버리스 함수는 실행 시간 제한이 존재한다. 결제나 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를 주고 실행을 멈춘 뒤 체크포인트만 남긴다.

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 } });
  },
});

사람의 승인은 웹훅 콜백으로 받아서 프로세스를 재개한다.

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 단계의 프롬프트 검증과 카나리 라우팅

프롬프트 수정 후 일어나는 환각 현상은 수동 테스트로 잡기 어렵다. DeepEval 지표를 통과해야만 PR이 병합되도록 파이프라인을 짠다.

평가 지표 수용 임계치 평가 기준
Faithfulness ≥0.85\ge 0.85≥0.85 제공된 Context 대비 팩트 왜곡 유무
Answer Relevancy ≥0.75\ge 0.75≥0.75 사용자 질문 목적과의 부합도
Hallucination Rate ≤0.10\le 0.10≤0.10 테스트 세트 중 환각 발생 비율
Tool Calling Accuracy ≥0.90\ge 0.90≥0.90 올바른 OpenAPI 스펙 도구 선택 및 타입 준수율

GitHub Actions에서 Pytest를 실행해 임계치 미달 시 빌드를 차단한다.

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%에만 신규 프롬프트를 먼저 적용한다.

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<number>('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*',
};