进阶 📋 6 个步骤 第 571 / 572 篇

用 Vercel eve 把 Agent 写成一个目录:工具、技能、渠道、定时与审批四档实操

Agent 框架常见的结局是一份只有作者看得懂的配置。eve 主张一个 Agent 就是一个目录:指令写 Markdown、工具写 TypeScript、能力按文件名自动发现,加渠道与定时任务各加一个文件。本教程六步跑通。

2026-10-27· 10 分钟上手· 约 3251 字· 🗂️ Vercel eve / 🧰 工具定义

大多数 Agent 框架的用法是:写一个循环,往里面注册工具、挂记忆、接渠道、配定时,最后你得到一份只有作者看得懂的配置文件。Vercel 的 eve 提了一个几乎过于简单的主张——一个 Agent 就应该是一个目录。指令写在 Markdown 里,工具写在 TypeScript 里,能力按文件名自动发现,装什么能力就加一个文件,不需要任何注册代码。这样做的好处不在优雅,而在可运维:新同事读一遍 agent/ 目录就知道这个 Agent 会干什么,改一行就是一次可评审的 diff。它由 Vercel 开源,Apache-2.0 协议,目前是公开测试版。本教程按脚手架、最小 Agent、加工具、审批四档、渠道与定时、会话与上线红线六步把它跑起来。

🗂️ 本教程适合:写 TypeScript、希望少写胶水代码、并且希望 Agent 结构能被同事读懂和被 Git 评审的开发者。你需要 Node.js 与一个终端;想部署 Web Chat 需要 Vercel 账号。文中命令以官方文档站与 README 为准;eve 处于公开测试版,框架、接口、文档与行为都可能在正式发布前变化,适合用来原型与学习,不建议今天就拿它锚定生产系统。

Step 1:一条命令生成,先看目录里都有什么

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 来读和改它的项目」的框架,是在按现实工作方式来设计。

💡 文档就在本地:让编码 Agent 去读 node_modules/eve/docs,比让它凭印象猜接口可靠得多。尤其在你升级 eve 之后——本地文档一定与装上的那个版本一致。

Step 2:两个文件的最小 Agent

2 一个模型,一份指令,就能跑

最小可运行的 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:加一个工具:一个文件,一个工具

3 文件名即工具名,schema 用 Zod

工具放在 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 的返回值:返回结构化、可判定的结果,模型才好基于它继续推理;返回一大段自然语言,等于把解析工作又扔回给模型。

💡 命名即接口。工具名会出现在模型的选择里,get_weather 这种动词开头的名字比 weather 更好派上用场;同理,同目录下堆二十个工具,不如按域拆到子目录并用更明确的名字。

Step 4:审批四档:从不用问到每次都问

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:渠道与定时:各加一个文件

5 HTTP 默认就有,Slack 是一个文件

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 可以调用工具、加载技能、读写沙箱文件、把活派给子智能体,并把生命周期事件流式回传给客户端。也就是说,它原生支持那种「发起后挂起两天等人回复再继续」的流程,而不是把每个请求都当成一次无状态调用。

💡 先用 HTTP 渠道把逻辑调通,再加 Slack。HTTP 那一步能看到完整请求与响应,排查成本最低;一旦切到即时通讯渠道,很多问题会被「消息看起来没回」这一层现象盖住。

Step 6:会话、沙箱、可观测性与上线前的红线

6 你写的只是 Agent 逻辑,外围全是默认值

模型写出来的代码在一个隔离的计算环境里跑,不在你的应用进程里:生产形态是 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/,模型拿到工具,拿不到凭据
两处文档目录列得不一样属公开测试期的正常现象;只在一处出现的项目按可能有变对待
← 返回教程中心