TuBrief
Subscribed Channels
Videos
Community

Архитектура агентов Vercel Eve: борьба с холодным стартом и оптимизация затрат в Serverless

TuBrief Editorial
July 23, 2026
0
Computing/Software

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

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

Related Video

Ship 26 NYC — Воркшоп — Создание агента с помощью Eve: The Open Agent Framework39:50

Ship 26 NYC — Воркшоп — Создание агента с помощью Eve: The Open Agent Framework

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: борьба с холодным стартом и оптимизация затрат в Serverless

При выводе агентов на базе Vercel Eve в продакшен сразу же сталкиваешься со специфической для serverless-среды проблемой — отсутствием состояния (Statelessness). Как только запрос завершается, инстанс закрывается, и состояние выполнения теряется. При этом обращение к СУБД вроде PostgreSQL при каждом запросе для восстановления сессии приводит к задержкам более 100 мс и резкому росту расходов на базу данных.

Ниже описаны три архитектурных подхода, используемых в реальном продакшене для преодоления ограничений serverless.

1. Снижение Latency восстановления сессии ниже 50 мс с помощью Upstash Redis

В serverless-среде создание нового соединения с БД при каждом запросе — кратчайший путь к ухудшению времени отклика и увеличению затрат на инфраструктуру. Размещение Upstash Redis, взаимодействующего через HTTP REST API, в качестве кэш-слоя сессий позволяет решить эту проблему.

`
[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 менее чем за 50 мс. Замена прямых выборок из реляционной БД кэшированием существенно снижает потребление 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)
Пригодность для Serverless Низкая (Connection Exhaustion) Средняя (Задержки подключения) Очень высокая (Поддержка Scale-to-Zero)
Структура затрат Почасовая оплата за зарезервированный инстанс Оплата за единицы запросов RCU/WCU Оплата за количество команд ($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 шагов (turns), а предыдущая часть диалога суммаризируется с помощью облегченной модели и помещается в начало промпта.

`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 нарушают весь процесс рассуждения агента. Необходимо внедрить экспоненциальную задержку (Exponential Backoff) с добавлением Full Jitter, а также паттерн Circuit Breaker.

Формула экспоненциальной задержки не просто увеличивает время ожидания пропорционально, а добавляет случайную величину для предотвращения узких мест:

Textdelay=minleft(Textmax,Textbaseimes2extattemptight)imesleft(0.5+extrandom(0,1.0)ight)T_{ ext{delay}} = minleft(T_{ ext{max}}, T_{ ext{base}} imes 2^{ ext{attempt}} ight) imes left(0.5 + ext{random}(0, 1.0) ight)Textdelay​=minleft(Textmax​,Textbase​imes2extattemptight)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));
}

}
}

`

При длительных сбоях Circuit Breaker мгновенно блокирует запросы (Fail-Fast) и направляет поток выполнения в логику Fallback.

Статус ответа внешнего API Состояние Circuit Breaker Механизм работы Результат обработки агентом
HTTP 200 OK Closed Нормальный пропуск и увеличение счетчика успехов Штатная подача внешних данных агенту
HTTP 429 / 503 Closed $
ightarrow$ Open Выполнение экспоненциальной задержки; при достижении порога ошибок $
ightarrow$ 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) без таймаутов

Serverless-функции имеют ограничение по времени выполнения. Удерживать запрос открытым в ожидании подтверждения оплаты или удаления БД приведет к ошибке таймаута.

`
[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 Callback, после чего процесс возобновляется.

`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 и канареечный маршрутинг

Галлюцинации, возникающие после изменения промптов, трудно отловить вручную. Настраивается пайплайн, при котором PR сливается только после прохождения метрик DeepEval.

Метрика оценки Пороговое значение Критерий оценки
Faithfulness ge0.85ge 0.85ge0.85 Отсутствие искажения фактов относительно предоставленного контекста
Answer Relevancy ge0.75ge 0.75ge0.75 Соответствие ответа цели вопроса пользователя
Hallucination Rate le0.10le 0.10le0.10 Доля возникновения галлюцинаций в тестовом наборе
Tool Calling Accuracy ge0.90ge 0.90ge0.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 и Middleware, благодаря чему новый промпт сначала применяется только к 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*',
};

`