Vercel의 Eve 프레임워크를 활용한 에이전트 구축
요약
Vercel이 오픈 소스로 공개한 AI 에이전트 구축 프레임워크인 Eve를 소개합니다. Eve는 에이전트의 추론 로직과 통신 채널을 명확히 분리하여, 코드 변경 없이 다양한 플랫폼에 에이전트를 확장할 수 있는 구조를 제공합니다.
핵심 포인트
- 에이전트(추론 핵심)와 채널(통신 수단)의 명확한 관심사 분리
- 파일 시스템 우선 접근 방식으로 모델 루프 및 도구 디스패치 관리
- Slack, Discord, HTTP 등 다양한 채널을 통한 손쉬운 인터페이스 확장
- 세션 지속성(Session Persistence)을 통한 안정적인 에이전트 운영
Vercel은 최근 내구성이 있는 AI 에이전트 (AI agents)를 구축하기 위한 프레임워크인 Eve를 오픈 소스로 공개했습니다. 이 프레임워크는 의견이 반영된(opinionated), 파일 시스템 우선 (filesystem-first) 접근 방식을 취합니다. 즉, 모델 루프 (model loops), 도구 디스패치 (tool dispatch), 세션 지속성 (session persistence)을 직접 연결하는 대신, 사용자가 파일 디렉토리를 작성하면 Eve가 나머지를 처리합니다.
저는 제품 카탈로그를 검색하고, 재고를 확인하며, 가격을 비교하고, 리뷰를 읽고, 주문을 넣을 수 있는 에이전트인 쇼핑 어시스턴트를 구축하여 이를 테스트해 보았습니다. 제가 발견한 내용은 다음과 같습니다.
핵심 분리: 에이전트 (Agent) vs 채널 (Channel)
Eve는 에이전트가 _무엇인지_와 에이전트가 어떻게 통신하는지 사이에 명확한 선을 긋습니다. 에이전트는 추론 핵심 (reasoning core) — 모델 (model), 도구 (tools), 지침 (instructions)입니다. 에이전트는 사용자가 어떻게 자신에게 도달하는지 알 필요도 없고 상관하지도 않습니다. 채널 (channel)은 단순한 통신 수단입니다. 특정 플랫폼에 대한 인바운드 전송 (inbound transport), 인증 (auth), 메시지 형식 (message format) 및 전달 (delivery)을 처리합니다.
이는 동일한 에이전트가 에이전트 자체의 조건부 로직 (conditional logic) 없이도 브라우저 채팅 위젯, Slack 봇, CLI, 커스텀 웹훅 (webhook)에 동시에 서비스를 제공할 수 있음을 의미합니다. 에이전트 코드를 변경하는 것이 아니라 채널 파일 (channel files)을 추가함으로써 접점 (surfaces)을 확장할 수 있습니다.
채널: 사용자가 에이전트에 도달하는 방법
Eve에서 **채널 (channel)**은 플랫폼과 에이전트 사이의 엣지 어댑터 (edge adapter)입니다. 채널은 인바운드 메시지를 정규화하고, 대화 재개 핸들 (continuationToken)을 소유하며, 응답이 어떻게 다시 전달될지를 결정합니다.
기본 채널은 eve.ts입니다. 이는 개발자 TUI, 브라우저 클라이언트, curl이 모두 통신하는 HTTP 세션 API입니다. 하지만 채널이 HTTP에 국한되지는 않습니다. Eve는 Slack, Discord, Teams, Telegram, Twilio (SMS/voice), GitHub, Linear를 위한 통합 기능을 제공합니다. 또한 모든 접점(웹훅, WebSockets, 내부 시스템)에 대해 커스텀 채널을 작성할 수도 있습니다.
각 채널은 agent/channels/ 아래의 파일입니다:
agent/channels/
├── eve.ts # HTTP API (파일이 없어도 항상 존재)
├── slack.ts # Slack DM, 멘션, 버튼
...
핵심 통찰: 채널에 관계없이 에이전트 로직(지침 + 도구)은 동일하게 유지됩니다. 에이전트를 한 번 작성한 다음, 채널 파일들을 추가함으로써 여러 인터페이스(surfaces)를 통해 노출할 수 있습니다. 채널은 플랫폼별 관심사(인증, 메시지 형식, 전달)를 처리하는 동안, 에이전트는 추론(reasoning)을 담당합니다.
기본 Eve 채널
eve.ts 채널은 항상 존재하며, 개발자 TUI, 브라우저 클라이언트(useEveAgent), 그리고 curl이 모두 사용하는 세션 기반 HTTP API를 제공합니다. 핵심 개념은 **지속 가능한 세션 (durable sessions)**입니다. 세션을 생성하기 위해 POST를 호출하고, NDJSON을 통해 이벤트 스트림을 수신하며, continuationToken을 사용하여 세션을 이어갑니다. 세션은 서버 재시작 후에도 유지되며, 임의의 이벤트 인덱스(?startIndex=N)에서 재연결을 지원합니다. 이는 상태가 없는(stateless) 요청/응답 방식과는 근본적으로 다르며, Eve가 서버 측에서 대화 상태를 소유합니다.
세션이 기본적으로 지속 가능한 방식
Eve 세션은 단순히 "메모리에 유지"되는 것이 아니라, 워크플로우 엔진(workflow engine)에 의해 뒷받침됩니다. 내부적으로 모든 턴(turn)은 오픈 소스인 Workflow SDK를 기반으로 구축된 지속 가능한 워크플로우(durable workflow)로 실행됩니다. Eve는 각 단계 (step) 경계(하나의 모델 호출 + 해당 도구 호출들 = 하나의 단계)에서 진행 상황을 체크포인트(checkpoint)로 저장합니다. 만약 턴 중간에 프로세스가 충돌하더라도, 모든 과정을 다시 재생하는 대신 마지막으로 완료된 단계부터 재개합니다.
로컬 환경에서 이는 단순히 디스크 상의 파일입니다. 에이전트를 실행하면 .workflow-data/ 디렉토리를 찾을 수 있습니다:
.workflow-data/
├── runs/ # 세션당 하나의 JSON 파일 (워크플로우 상태)
├── steps/ # 완료된 단계별 체크포인트
...
즉, 다음과 같은 작업이 가능합니다:
- 대화를 시작하고 에이전트에게 질문하기
- 서버 종료 (
Ctrl+C) - 서버 재시작
- 동일한
sessionId와continuationToken을 사용하여 대화 이어가기
세션은 중단된 지점에서 정확히 다시 시작됩니다. 만약 충돌 전 단계가 이미 완료되었다면, 턴 중간부터 복구하는 것도 포함됩니다.
물론, 로컬 파일은 프로덕션(production) 환경에서 확장 가능하지 않습니다. Eve의 내구성(durability)은 Workflow SDK'의 "World" 추상화 계층인 스토리지/큐/스트리밍 백엔드를 통해 플러그인 방식으로 교체할 수 있습니다. 특정 world 패키지를 선택하면 Eve는 모든 세션 지속성(session persistence)을 위해 이를 사용합니다:
| World | Backend | Use case |
|---|---|---|
@workflow/world-local | Filesystem (.workflow-data/) | 로컬 개발 (기본값) |
| ... |
교체하려면 agent.ts에서 다음과 같이 설정합니다:
export default defineAgent({
model: openai("gpt-4o"),
modelContextWindowTokens: 128_000,
...
World 인터페이스는 세 가지 책임을 가집니다: Storage (추가 전용 이벤트 로그(append-only event log)를 통한 실행(runs), 단계(steps), 훅(hooks)의 지속화), Queue (최소 한 번 전달(at-least-once delivery) 보장과 함께 워크플로/단계 호출을 디스패칭), 그리고 Streams (클라이언트로의 실시간 이벤트 전달)입니다. 기존 옵션 중 적합한 것이 없다면 직접 구축할 수도 있습니다.
예시: 에이전트를 AG-UI로 노출하기
이를 구체화하기 위해, 저는 AG-UI 프로토콜 (SSE 기반이며 CopilotKit 및 유사한 프레임워크에서 사용됨)을 통해 Eve 에이전트를 노출하는 커스텀 채널을 구축했습니다. 이 채널은 Eve의 내부 이벤트 스트림을 AG-UI의 이벤트 어휘로 변환합니다:
// agent/channels/agui.ts
import { defineChannel, POST, type Session } from "eve/channels";
import { EventEncoder } from "@ag-ui/encoder";
...
이벤트 매핑은 대부분 기계적인 작업입니다 — actions.requested → TOOL_CALL_START/ARGS/END, action.result → TOOL_CALL_RESULT, message.appended → TEXT_MESSAGE_CONTENT, turn.completed → RUN_FINISHED. 하지만 몇 가지 명확하지 않은 주의 사항(gotchas)이 있었습니다:
1. Eve 세션은 지속적(durable)이지만, AG-UI 실행(runs)은 그렇지 않습니다. Eve의 이벤트 스트림은 한 턴이 끝난 후에도 닫히지 않고 다음 메시지를 기다립니다. 따라서 RUN_FINISHED를 방출한 후에는 반드시 직접 응답 스트림을 닫아야 합니다. 그렇지 않으면 클라이언트는 더 많은 데이터를 기다리며 영원히 대기 상태(hang)에 빠지게 됩니다.
2. Eve는 턴 경계(turn boundaries)에서 turn.completed와 session.waiting을 모두 방출(emit)합니다. 만약 두 경우 모두에 대해 단순히 RUN_FINISHED를 방출한다면, AG-UI 클라이언트는 다음과 같은 오류를 발생시킵니다: "Cannot send event type 'RUN_FINISHED': The run has already finished." 플래그(flag)를 사용하여 방어 로직을 구현하고 한 번만 방출하도록 하세요.
3. AG-UI는 상태가 없는(stateless) 방식이며, Eve는 상태를 유지하는(stateful) 방식입니다. 각 AG-UI 요청은 전체 메시지 기록(message history)을 포함합니다. Eve의 send()는 continuationToken을 통해 세션을 생성하거나 지속시키므로, 요청마다 새로운 토큰이 필요합니다(그렇지 않으면 Eve가 만료된 세션을 계속하려고 시도합니다). 대화 기록은 context를 통해 전달되므로 에이전트가 이전 턴을 확인할 수 있습니다.
4. Eve의 액션(actions)은 타입 유니온(typed unions)입니다. actions.requested는 tool-call, subagent-call, 또는 load-skill이 될 수 있는 RuntimeActionRequest[]를 포함합니다. action.kind === "tool-call"로 필터링한 후 action.toolName / action.callId를 사용해야 합니다(해당 타입에 존재하지 않는 .name / .id를 사용하지 마세요).
동일한 에이전트, 동일한 도구, 동일한 지침을 사용하지만, 이제 POST /agui 경로의 SSE(Server-Sent Events)를 통해 AG-UI로 통신합니다. Eve HTTP 채널, Slack 채널, 그리고 이 AG-UI 채널을 모두 동시에 실행하면서 각각 동일한 기반 에이전트와 대화하도록 구성할 수 있습니다.
개발자 경험 (The Developer Experience)
pnpm dev(또는 npx eve dev)를 실행하면 로컬에서 eve 채널 프로토콜을 사용하는 대화형 터미널 UI(TUI)가 나타납니다:
☰eve shopping-agent-orchestrator
> show me laptops under $1000
...
TUI는 다음을 보여줍니다:
- 발생하는 즉시 표시되는 도구 호출 (한 줄 요약으로 축소됨)
- 모델이 생성하는 스트리밍 텍스트
- 토큰 사용량 통계
- 슬래시 명령어 (
/model로 모델 전환,/new로 새 세션 시작)
파일 변경 시 핫 리빌드(hot rebuild)가 트리거됩니다. 도구를 수정하면 다음 메시지에서 즉시 반영됩니다.
주의 사항: 커스텀 모델은 modelContextWindowTokens가 필요합니다
게이트웨이가 아닌 모델(커스텀 baseURL, 직접 프로바이더 연결)을 사용하는 경우, Eve의 빌드는 압축 메타데이터(compaction metadata)에 관한 모호한 오류와 함께 실패합니다. 컨텍스트 윈도우(context window) 크기를 명시적으로 알려주어야 합니다:
// agent/agent.ts
import { createOpenAI } from "@ai-sdk/openai";
import { defineAgent } from "eve";
...
modelContextWindowTokens란 무엇인가요? Eve에는 긴 대화가 모델의 컨텍스트 윈도우 (context window)를 초과하는 것을 방지하는 내장된 "압축 (compaction)" 시스템이 있습니다. 대화가 윈도우의 약 90%에 도달하면, Eve는 세션이 무한히 계속될 수 있도록 이전 대화 내용들을 압축된 형태로 자동 요약합니다. 이를 수행하기 위해 Eve는 윈도우의 크기가 얼마나 큰지 알아야 합니다. 게이트웨이 모델 (예: "openai/gpt-4o")은 Vercel의 카탈로그에 이 메타데이터가 포함되어 있습니다. 하지만 createOpenAI()를 통해 사용자가 직접 프로바이더 (provider)를 가져오는 경우, Eve는 이를 조회할 방법이 없으므로 명시적으로 알려주어야 합니다.
이 필드가 없으면 런타임 (runtime)에 압축 과정을 조용히 건너뛰는 대신, 컴파일 타임 (compile time)에 빌드가 실패합니다. 에러 메시지("does not have known AI Gateway context window metadata")만으로는 해결 방법을 명확히 알기 어렵습니다.
핵심 요약 (Key Takeaways)
- 오케스트레이션 (orchestration) 코드 제로. 라우팅 (routing), 도구 디스패치 (tool dispatch), 또는 스트리밍 (streaming) 로직을 단 한 줄도 작성하지 않았습니다. 모델은 마크다운 (markdown) 지침에 따라 검색 → 재고 확인 → 주문과 같은 다단계 추론 (multi-step reasoning)을 네이티브하게 처리합니다.
- 파일 시스템 컨벤션 (filesystem convention)을 통한 보일러플레이트 (boilerplate) 제거. 새로운 기능을 추가하는 것은 단순히 "파일을 생성하는 것"입니다. 별도의 등록이나 연결을 위한 임포트 (import)가 필요 없습니다.
- 기본 제공되는 내구성 있는 세션 (Durable sessions). 다회차 대화 (multi-turn conversations), 재연결, 인간 참여형 승인 (human-in-the-loop approval) 등이 모두 내장되어 있습니다.
- 진정으로 유용한 개발용 TUI. 개발 중에 도구 호출 (tool calls)이 실행되는 것을 실시간으로 확인하며 빠른 피드백 루프를 가질 수 있습니다.
Twitter @roamingcode로 언제든 편하게 연락해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기