大多数 Agent 框架的用法是:写一个循环,往里面注册工具、挂记忆、接渠道、配定时,最后你得到一份只有作者看得懂的配置文件。Vercel 的 eve 提了一个几乎过于简单的主张——一个 Agent 就应该是一个目录。指令写在 Markdown 里,工具写在 TypeScript 里,能力按文件名自动发现,装什么能力就加一个文件,不需要任何注册代码。这样做的好处不在优雅,而在可运维:新同事读一遍 agent/ 目录就知道这个 Agent 会干什么,改一行就是一次可评审的 diff。它由 Vercel 开源,Apache-2.0 协议,目前是公开测试版。本教程按脚手架、最小 Agent、加工具、审批四档、渠道与定时、会话与上线红线六步把它跑起来。
Step 1:一条命令生成,先看目录里都有什么
脚手架会创建项目、安装依赖、初始化 Git,并启动交互式终端界面或开发服务器。生成出来的东西全在一个 agent/ 目录里:instructions.md 是常驻的系统提示,agent.ts 是用 defineAgent 写的运行时与模型配置,tools/ 放带类型的工具,skills/ 放按需加载的 Markdown 流程,subagents/ 放子智能体,connections/ 放对外部服务的类型化集成,channels/ 放平台入口,sandbox/ 是隔离计算环境,schedules/ 放周期任务,instrumentation.ts 是可选的 OpenTelemetry 配置。
npx eve@latest init my-agent
cd my-agent
eve dev # 开发服务器
eve build # 编译
eve start # 提供服务
发现规则值得单独记住:目录与文件名本身就是注册。加一个工具、技能、渠道或定时任务,就是加一个文件;eve 在构建时发现并校验它们,编译出清单,不需要任何样板代码去登记。还有一处细节能看出它是为谁设计的——npm 包把完整文档放在 node_modules/eve/docs 里,于是在你仓库里干活的编码 Agent 可以直接本地读 eve 自己的文档。一个预设「会有别的 Agent 来读和改它的项目」的框架,是在按现实工作方式来设计。
Step 2:两个文件的最小 Agent
最小可运行的 Agent 只有两个文件:agent.ts 里用 defineAgent 声明跑哪个模型与运行时选项,instructions.md 里写它是谁、该怎么干活。模型 ID 默认走 Vercel AI Gateway,某个厂商不可用时可以做 provider 回退;底层站在 Vercel AI SDK 上,所以 AI SDK 支持的模型它都能跑。Markdown 一侧是给人读的描述,TypeScript 一侧是有类型、可执行的部分——这条分界线贯穿整个框架。
// agent/agent.ts —— 模型与运行时配置
import { defineAgent } from 'eve/agent';
export default defineAgent({
model: 'anthropic/claude-sonnet-4.6',
});
# agent/instructions.md —— 常驻系统提示
你是一个发布助手。回答只用中文,结论先行。
涉及删除数据、改动权限或对外发送消息的操作,一律先说明再等待批准。
不确定的地方直接说不确定,不要猜。
指令文件是「常驻」的,意思是每一轮都在上下文里,所以它适合放身份、边界与表达规范,不适合放几十条流程。那些只在特定任务才用得上的流程,应该放进 skills/ 让模型按需加载——这也是这类框架与「把所有说明塞进一个巨型系统提示」最实际的分野:常驻内容越短,每一轮的固定开销越低。
指令会一直占着上下文:instructions.md 是常驻系统提示,写得越长,每轮固定成本越高。把「身份与边界」留在这里,把「具体流程」拆到 skills/ 按需加载;流程超过十几条就该拆了。
Step 3:加一个工具:一个文件,一个工具
工具放在 agent/tools/ 下,一个文件一个工具,文件名就是运行时暴露给模型的工具名,所以命名要用 snake_case ASCII。官方示例是一个查天气的工具,四个要素缺一不可:写给模型看的 description、必填的 Zod inputSchema、真正干活的 execute,以及符合规范的文件名。哪怕工具不需要任何入参,也要写 z.object({}),不能省。
// agent/tools/get_weather.ts —— 文件名即工具名,模型看到的是 get_weather
import { defineTool } from 'eve/tools';
import { z } from 'zod';
export default defineTool({
description: 'Get the current weather for a city.',
inputSchema: z.object({
city: z.string(),
}),
async execute(input) {
return { city: input.city, condition: 'Sunny', temperatureF: 72 };
},
});
这里有两个习惯值得带进你自己的工具。一个是 description 的读者是模型:写清楚「什么时候该用它」,而不是复述函数名;描述含糊的工具,模型要么不用,要么在错的场合用。另一个是 execute 的返回值:返回结构化、可判定的结果,模型才好基于它继续推理;返回一大段自然语言,等于把解析工作又扔回给模型。
Step 4:审批四档:从不用问到每次都问
有副作用的工具要挂审批。辅助函数从 eve/tools/approval 导入,一共四档:never() 是默认值,不需要批准;once() 是同一会话里该工具跑之前需要批准一次;always() 是每次调用前都要批准;auto() 由一个评估模型来判断。还有一种更实用的写法——用一个谓词,让是否审批取决于入参本身。
import { defineTool } from 'eve/tools';
import { z } from 'zod';
import { always, once, auto, never } from 'eve/tools/approval';
// 固定档位
export default defineTool({
description: 'Issue a refund to a customer.',
inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
approval: always(),
async execute(input) { /* ... */ },
});
// 按入参决定:超过 1000 才需要人点头
approval: ({ toolInput }) => (toolInput?.amount ?? 0) > 1000
按入参判断这一档在日常里最好用:小额自动走、大额才拦人,既不会把人淹没在确认框里,也不会让大额静默通过。机制之外还有一层:概念文档里提到一个叫 Jev 的工具,它按审批标准复核被提议的工具调用,判定为 clear 的自动执行,判定为 caution 的暂停等人。挂在 Slack 这类渠道上时,等待会以按钮的形式出现在对话里,而等待期间不消耗计算资源——这一点对那些「等人等两天」的长流程很关键。
auto() 把判断权交给了一个模型:它方便,但也意味着「是否需要人批准」这件事本身是概率性的。涉及资金、权限、对外发送或不可逆删除的工具,用 always() 或显式的入参谓词;把 auto() 留给那些判断错了代价也很低的场合。
Step 5:渠道与定时:各加一个文件
Agent 的入口是渠道。HTTP 默认开启,Slack、Discord、Microsoft Teams、Telegram、Twilio、GitHub、Linear 这些平台各加一个文件即可;一个会话可以在一个渠道开始,在另一个渠道继续。周期任务放在 schedules/ 里,一个 cron 表达式配一个处理函数,部署时落成平台上的定时任务。本地验证最短路径是起开发服务器,然后给会话端点发一条消息。
# 本地起一个会话:单位是 session,不是 request
curl -X POST http://127.0.0.1:3000/eve/v1/session \
-H 'content-type: application/json' \
-d '{"message":"What is the weather in Brooklyn?"}'
# agent/schedules/ 下放周期任务,部署后按 cron 触发
# 例如每周工作日早上跑一次依赖体检,结果发回指定渠道
把「会话」这个概念搞清楚,很多行为就解释得通了。会话是由渠道或 HTTP 请求发起的一段持久对话或任务,每条用户消息或外部事件构成一个回合;一个回合之内,Agent 可以调用工具、加载技能、读写沙箱文件、把活派给子智能体,并把生命周期事件流式回传给客户端。也就是说,它原生支持那种「发起后挂起两天等人回复再继续」的流程,而不是把每个请求都当成一次无状态调用。
Step 6:会话、沙箱、可观测性与上线前的红线
模型写出来的代码在一个隔离的计算环境里跑,不在你的应用进程里:生产形态是 Vercel Sandbox,本地用 Docker 或类似机制。对外部服务的集成放在 connections/,指向一个 MCP 服务器或一份 OpenAPI 描述——模型拿到的是工具,拿不到凭据。可观测性是内置的,每个回合产出 OpenTelemetry span,可以导出到你已经在用的后端;需要自定义就写 agent/instrumentation.ts。还有评估:测试文件发一条消息,然后断言调用了哪些工具、返回了什么。
my-agent/
└── agent/
├── agent.ts # 模型与运行时配置
├── instructions.md # 常驻系统提示
├── tools/ # 带类型的工具,文件名即工具名
├── skills/ # 按需加载的 Markdown 流程
├── subagents/ # 子智能体,各有自己的工具与沙箱
├── connections/ # MCP 服务器与 OpenAPI 后端
├── channels/ # Slack、Discord、Teams、HTTP
├── sandbox/ # 隔离计算环境
├── schedules/ # cron 周期任务
└── instrumentation.ts # 可选 OpenTelemetry 配置
最后把边界说完整。它站在 Vercel 自家的基元上:持久会话跑在 Vercel Workflow 上,沙箱跑在 Vercel Sandbox 上,模型经 AI Gateway 路由,对 Slack 与 Salesforce 的 OAuth 走 Vercel Connect,追踪落在 Vercel Observability。这和 Next.js 的商业模型是同一套,采用之前值得知道。另外两份文档对目录的列举并不完全一致——概念页没列 schedules,README 的目录树没列 subagents、connections 与 sandbox,只出现在其中一份里的项目,建议按「可能有变」对待。typed 工具与 Zod schema 对 TypeScript 团队很自然,对希望留在 Python 里的团队则相当陌生。
公开测试版,别拿它锚定生产:项目自己写明了框架、接口、文档与行为都可能在正式发布前变化。合理的用法是现在用来搭原型、理清结构;真要上生产,等正式发布并把版本钉死,同时为「接口变了要改一层」预留时间。
常见问题速查
| 现象 | 原因与处理 |
|---|---|
| 脚手架跑完不知道从哪看起 | 先读 agent/instructions.md 与 agent/agent.ts,这两个文件就定义了一个能跑的 Agent |
| 加了工具但模型不用 | 检查文件名是否 snake_case ASCII,description 有没有写清「什么时候用」 |
| 工具没有入参却报错 | inputSchema 必填,无入参也要写 z.object({}) |
| 想让大额操作才拦人 | 用谓词形式,按 toolInput 里的字段判断是否超过阈值 |
| 每次都被确认框打断 | 默认是 never();只在敏感工具上用 once() 或 always() |
| auto() 有时不拦 | 判断权在评估模型,是概率性的;关键操作改用 always() 或显式谓词 |
| curl 起会话没反应 | 确认 eve dev 已起,端点为 127.0.0.1:3000/eve/v1/session |
| 会话看起来「断了」 | 单位是 session 不是 request,会话可跨渠道继续并可长时间挂起 |
| 想接 Slack 或 Discord | 在 channels/ 下加一个文件;HTTP 是默认开启的 |
| 定时任务没触发 | 放在 agent/schedules/ 下,部署后才会落成平台的 cron 任务 |
| 担心凭据泄漏给模型 | 外部集成放 connections/,模型拿到工具,拿不到凭据 |
| 两处文档目录列得不一样 | 属公开测试期的正常现象;只在一处出现的项目按可能有变对待 |