eve 中文教程 · 第 2 章 第一个 agent
一条命令建项目,一个 Markdown 文件定义人格,一个
npm run dev开始对话。本章结束时你会有一个能回答问题的值班助手。
2.1 前置条件
- Node.js 24 或更新(
node -v确认) - npm(Node 自带)
- 一个模型凭据:AI Gateway、OpenAI / Anthropic API key,或本地 Ollama
Node 版本是硬要求,package.json 里的 engines 会检查。用 22 会在启动时报错。
2.2 建项目
npx eve@latest init opsbot
cd opsbot
这条命令做五件事:写脚手架、装依赖(eve、ai、zod)、初始化 Git、写好内置 HTTP 通道 agent/channels/eve.ts、打开终端 UI 让你登录模型。
不想进交互界面(CI、脚本里)加 -n:
npx eve@latest init opsbot --non-interactive --model anthropic/claude-sonnet-5 --reasoning high
给已有项目加 agent,在项目根目录跑:
npx eve@latest init .
它只补 agent/ 和缺的依赖,不动你已有的文件。注意:目录里已经有 agent/ 时会拒绝,这是保护。
2.3 先看看它生成了什么
opsbot/
├── agent/
│ ├── agent.ts # 模型配置(可选,不写就用默认模型)
│ ├── instructions.md # 系统提示词
│ └── channels/
│ └── eve.ts # 内置 HTTP API 的鉴权配置
├── package.json
└── tsconfig.json
agent.ts 长这样:
// agent/agent.ts
import { defineAgent } from "eve";
export default defineAgent({
model: "anthropic/claude-sonnet-5",
});
defineAgent 的字段(全部可选,除了 model):
| 字段 | 类型 | 说明 |
|---|---|---|
model |
模型 ID 字符串、provider 模型对象,或 defineDynamic(...) |
每轮用的模型。字符串形式走 AI Gateway |
description |
string |
给人看的说明。子代理必填,根 agent 可选 |
reasoning |
"low" \| "medium" \| "high" \| … |
推理强度,具体档位取决于模型 |
compaction |
{ thresholdPercent?, model?, modelContextWindowTokens? } |
压缩策略,默认窗口用到 90% 时触发 |
limits |
{ sessionTimeoutMs?, maxInputTokensPerSession?, maxOutputTokensPerSession?, maxTokenCostUsdPerSession? } |
运行时预算,见第 6 章 |
defaultTools |
boolean,默认 true |
是否加 eve 的可选默认工具(bash、read_file 等) |
tool |
true \| "deferred" \| false,默认 true |
这个 agent 作为子代理时怎么暴露给父模型 |
modelContextWindowTokens |
number |
覆盖模型上下文窗口,用于未收录的模型 |
modelOptions |
provider 选项对象 | 原样传给 AI SDK 的 provider |
build |
{ externalDependencies?: string[] } |
打包控制,让敏感包保持 external |
experimental |
{ workflow?: … } |
实验特性,见第 10、18 章 |
子字段的含义在第 6 章(压缩、预算)和第 10 章(workflow)展开。
modelContextWindowTokens 和 modelOptions 只和静态模型一起用。model 换成 defineDynamic(...) 时这两个字段的类型是 never,窗口和 provider 选项要跟着每次选择一起返回。
接国产模型(DeepSeek、Qwen、GLM、Kimi)
model 收两种东西:Gateway 模型 ID 字符串,或一个 AI SDK 的 LanguageModel 对象。国产模型两条路都能走。
路线一:Gateway 字符串。 Gateway 目录里有对应条目时最省事:
export default defineAgent({
model: "deepseek/deepseek-chat",
});
需要 AI_GATEWAY_API_KEY,请求经 Gateway 转发。国内网络到 Gateway 可能不稳,长 turn 更容易断——这是很多人最后改成直连的原因。
路线二:直连厂商。 官方有 AI SDK provider 包时用它:
npm install @ai-sdk/deepseek
// agent/agent.ts
import { defineAgent } from "eve";
import { createDeepSeek } from "@ai-sdk/deepseek";
const deepseek = createDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY! });
export default defineAgent({
model: deepseek("deepseek-v4-pro"),
modelContextWindowTokens: 128_000, // 直连模型不在 Gateway 目录里,必须手填
});
包里也导出了一个配好的默认实例 deepSeek(注意大写 S),读 DEEPSEEK_API_KEY:
import { deepSeek } from "@ai-sdk/deepseek";
model: deepSeek("deepseek-v4-flash");
路线三:OpenAI 兼容端点。 只提供 /v1/chat/completions 兼容接口的厂商(通义、GLM、Kimi、豆包、混元)用 @ai-sdk/openai-compatible:
npm install @ai-sdk/openai-compatible
import { defineAgent } from "eve";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
const qwen = createOpenAICompatible({
name: "qwen",
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
apiKey: process.env.DASHSCOPE_API_KEY!,
includeUsage: true, // 流式响应里带上 usage,否则预算算不出来
});
export default defineAgent({
model: qwen("qwen-plus"),
modelContextWindowTokens: 128_000,
});
常见厂商的对照:
| 厂商 | 包 | baseURL |
环境变量 | 模型 ID 示例 |
|---|---|---|---|---|
| DeepSeek | @ai-sdk/deepseek |
内置 | DEEPSEEK_API_KEY |
deepseek-v4-pro、deepseek-v4-flash |
| 通义千问 | @ai-sdk/openai-compatible |
https://dashscope.aliyuncs.com/compatible-mode/v1 |
DASHSCOPE_API_KEY |
qwen-plus、qwen-max |
| 智谱 GLM | 同上 | https://open.bigmodel.cn/api/paas/v4 |
ZHIPU_API_KEY |
glm-5.2(以控制台为准) |
| Kimi | 同上 | https://api.moonshot.cn/v1 |
MOONSHOT_API_KEY |
kimi-k2(以控制台为准) |
| 豆包 | 同上 | https://ark.cn-beijing.volces.com/api/v3 |
ARK_API_KEY |
以控制台为准 |
| 混元 | 同上 | https://api.hunyuan.cloud.tencent.com/v1 |
HUNYUAN_API_KEY |
hunyuan-turbos-latest |
模型 ID 会随版本变,上表只是常见别名,接之前先去控制台确认一次。
两条路的请求和密钥走向不同:
flowchart LR
A["agent.ts"] -->|"字符串 + AI_GATEWAY_API_KEY"| G["AI Gateway"]
G --> P["DeepSeek / Qwen / GLM …"]
A -->|"provider 对象 + 厂商自己的 key"| P
走 Gateway 时,厂商密钥由 Gateway 管;直连时,厂商密钥只在你的应用里。后者少一跳,也少一个外部依赖。
四条必须知道的差异:
modelContextWindowTokens要手填。 字符串模型 eve 从 Gateway 目录查窗口;直连的查不到,不填会让压缩阈值按错误的基准算(第 6 章)。按厂商文档的上下文长度填。- 动态选择在 session / turn 上只能返回字符串。
LanguageModel对象不可序列化,过不了停泊,只能从step.started返回(第 18 章)。 - 工具调用是硬门槛。 eve 的一切都靠 function calling。DeepSeek 的
deepseek-v4-pro/deepseek-v4-flash支持;推理特化的模型或预览版可能不稳,接完先用第 16 章的 smoke 评测跑一遍。 reasoning的档位未必认得。 国产模型大多没有"low"/"high"这套映射,厂商特有参数走modelOptions.providerOptions,键是 provider 名:
export default defineAgent({
model: deepseek("deepseek-v4-pro"),
modelContextWindowTokens: 128_000,
modelOptions: {
providerOptions: {
deepseek: {
thinking: { type: "disabled" }, // 关掉思维链
// reasoningEffort: "high", // 或者保留思维链,只调强度
},
},
},
});
createOpenAICompatible 的 name 就是这里的键(上面例子是 qwen)。OpenAI 兼容端点还有两个开关值得留意:includeUsage: true 让流式响应带上 usage(不然 limits 的 token 预算没法统计),supportsStructuredOutputs: true 在厂商支持时启用严格 JSON schema。
密钥一律走环境变量,部署前在目标环境配好(第 17 章)。注意 eve dev 会自动读 .env.local,但 eve build 不读——哪些命令读、编译期为什么会把值定死、沙箱里为什么读不到,全在附录 C。
接本地模型(Ollama、LM Studio、vLLM)
在自己机器上跑模型调试时,把 AI SDK 的 OpenAI 兼容 provider 指向本地服务即可。任何实现了 OpenAI Chat Completions 的服务都能用:
npm install @ai-sdk/openai-compatible
ollama pull gpt-oss:20b
OLLAMA_CONTEXT_LENGTH=64000 ollama serve
// agent/agent.ts
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { defineAgent } from "eve";
const ollama = createOpenAICompatible({
name: "ollama",
baseURL: "http://localhost:11434/v1",
includeUsage: true, // 流式响应里带上 token 用量
});
export default defineAgent({
model: ollama("gpt-oss:20b"),
modelContextWindowTokens: 64_000,
});
四条与云端不同的注意点:
- 上下文窗口按服务实际分配的填,不是模型的理论上限。 AI Gateway 没有本地模型的元数据,
modelContextWindowTokens是压缩阈值的唯一依据。Ollama 默认只给 4K,agent 场景至少要 64K,用OLLAMA_CONTEXT_LENGTH调。 - 必须有原生工具调用能力。 eve 几乎每一轮都要调工具。
gpt-oss:20b、qwen3、gemma4是 Ollama 上比较可靠的起点。 includeUsage: true才统计得到 token。 本地模型不报成本,所以maxTokenCostUsdPerSession对它们不起作用,只能用 token 上限。- 部署环境连不到你机器上的服务。 上线必须换成 Gateway 或托管 provider。
会话亲和(只对 AI Gateway 生效)
Gateway 会用会话 ID 做会话亲和:同一会话的请求优先打到上次服务它的那个 provider,好让那边命中 prompt 缓存。它可能把你的 order 顺序挤开。要严格按 order 走就关掉:
export default defineAgent({
model: "anthropic/claude-opus-5.5",
modelOptions: {
providerOptions: {
gateway: { order: ["bedrock", "anthropic"], sessionAffinity: false },
},
},
});
直连 provider 的调用拿不到这个选项。会话 ID 超过 256 字符时 Gateway 会先做哈希;自己写的 gateway.sessionId 会被逐字记录,别往里塞个人信息。
channels/eve.ts 是生产环境的占位鉴权:
// agent/channels/eve.ts
import { eveChannel } from "eve/channels/eve";
import { localDev, placeholderAuth, vercelOidc } from "eve/channels/auth";
export default eveChannel({
auth: [vercelOidc(), localDev(), placeholderAuth()],
});
这三条是顺序执行的,第一个返回非 null 的生效:
vercelOidc():认 Vercel 签发的 OIDC token(本地 CLI 访问已部署的 agent 靠它)localDev():只在eve dev/vercel dev进程里放行,生产环境什么都不认placeholderAuth():占位,生产环境一律 401
eveChannel 还有 audience、uploadPolicy、fetchFile、cors、turnPolicy、onMessage、events、trustedForwarders 这些字段,完整的表和默认值在第 14 章。
所以开发环境能用,不是 placeholderAuth() 的功劳——是 localDev()。上线前必须把 placeholderAuth() 换成真的鉴权,否则生产流量全被拒(好的一面)或你为了图省事换成 none()(坏的一面,等于公开)。第 14 章讲怎么换。
2.4 写人格
instructions.md 是每轮都会发给模型的系统提示。它应该只放稳定的身份和规则,不是流程手册。
替换成下面这份:
<!-- agent/instructions.md -->
你是「值班助手」,服务一个做在线支付的后端团队。
## 你的职责
- 回答关于服务状态、错误日志、部署历史的问题。
- 排查故障时先给结论,再给证据,最后给建议动作。
- 拿不准就说不确定,并说明需要什么信息才能确定。
## 硬性规则
- 不要编造日志内容、指标数值或时间戳。没查到就说没查到。
- 任何会改变生产状态的操作(重启、回滚、改配置)都必须先说明影响范围,
并等待人工确认。你不能自己决定执行。
- 回答用中文。代码、日志、命令保持原文。
- 涉及金额时保留两位小数并注明币种。
## 输出风格
先一句话结论,然后最多三条证据,最后一行是建议的下一步。
不要写「希望对你有帮助」这类客套话。
三个要点:
- 写规则,不写流程。 「重启前要确认」是规则;「第一步查 A,第二步查 B」是流程,属于技能(第 7 章)。
- 写否定约束。 模型最容易犯的错是编造,明确写「不要编造」比写「要准确」有效。
- 别写太长。 这段文字每轮都发,一千字就是每轮一千字的成本。超过两百行就该拆技能了。
2.5 跑起来
npm run dev
终端 UI 打开。先问一个不需要工具、靠常识就能答的问题:
支付接口的 p99 延迟突然从 80ms 涨到 900ms,一般先查什么?
你会得到一个有结构、有优先级的回答——它遵循了人格设定,但完全没碰你的系统。这是对的:现在它只有一个提示词,没有任何工具。
试试故意违反规则:
帮我把 payment-api 重启一下。
它应该拒绝自己执行,转而说明需要人工确认。如果它假装执行了,说明你的提示词约束不够硬——回到 2.4 加强「硬性规则」。
2.6 改一句看效果
编辑 agent/instructions.md,把「回答用中文」改成「回答用英文」,保存。不用重启,dev server 会热加载。再问一句,回答变成英文了。
这个即时反馈循环是后面所有章节的工作方式:改文件 → 发消息 → 看行为。
2.7 看看框架眼里的你
npx eve info
输出是编译后的 agent 快照:发现了哪些工具、技能、通道、定时任务,哪些默认值被覆盖了,有没有诊断警告。养成习惯,改完目录结构先跑一次。
同类命令:
npx eve set model anthropic/claude-opus-5.5 --reasoning high # 改模型
npx eve logs list # 本地诊断日志
npx eve traces list # 本地 trace
2.8 直接用 HTTP 说话
dev server 起在 http://localhost:2000。不通过 TUI 也能聊:
# 建会话并发第一条消息
curl -X POST http://localhost:2000/eve/v1/session \
-H 'content-type: application/json' \
-d '{"message":"现在有哪些服务在报警?"}'
# {"ok":true,"sessionId":"wrun_A","status":"accepted"}
# 看事件流(NDJSON,一行一个事件)
curl http://localhost:2000/eve/v1/session/wrun_A/stream
# 追问
curl -X POST http://localhost:2000/eve/v1/session/wrun_A \
-H 'content-type: application/json' \
-d '{"message":"按严重程度排序"}'
sessionId 是唯一入口,后续所有消息、取消、清空都带着它。第 9 章把这条协议讲全。
2.9 常见坑
坑:eve init 之后 npm run dev 报模型鉴权失败。
脚手架没有配置凭据。在 TUI 里输入 /model 走登录流程,或把 OPENAI_API_KEY / ANTHROPIC_API_KEY 写进 .env。用 Vercel AI Gateway 的模型还需要 AI_GATEWAY_API_KEY。
坑:改了 agent.ts 没生效。
模型、build.externalDependencies 这类配置在启动时读取,改完要重启 dev server。instructions.md 和工具文件是热加载的,两者不一样。
坑:在 agent/ 里放业务代码。
agent/lib/ 只放 agent 自己用的辅助代码,它不会被复制进沙箱。前端代码、数据库迁移、普通后端接口都放 agent/ 外面。
坑:以为 instructions.md 能当流程文档用。
写两百行排障步骤进去,结果是每轮都付这份 token,而且模型经常忽略中间的条目。长流程放技能。
2.10 练习
- 把
instructions.md改成你自己领域的助手(客服、代码审查、数据分析都行),写三条硬性规则。 - 故意让它做一件越界的事,确认它拒绝,并记录是哪条规则起了作用。
- 用
curl建一个会话,发两条消息,观察stream里出现了哪些事件类型(不用看懂,先有印象)。
延伸阅读
上一章:01 认识 eve · 下一章:03 agent 目录解剖