关于 functions
Zavu Functions 是在 Zavu Cloud 上构建无服务器 TypeScript Agent 和工具的代码优先框架,面向需要自定义处理逻辑并通过 CLI 部署和测试的开发者。
主要功能
- 01声明式定义 Agent 和工具
- 02部署导入的 TypeScript 项目文件
- 03通过 CLI 管理函数和密钥
- 04创建模板及预置 Agent
- 05本地调用处理程序进行测试
- 06通过 HTTP 暴露函数
适用场景
- 01构建带自定义工具处理程序的 Agent
- 02让工具访问数据库或内部 API
- 03从 Git 仓库部署 Agent 配置
- 04使用模拟参数测试工具处理程序
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zavudev/ zavu-skills --skill functions安装后在 Claude Code 中输入/functions确认已加载。
详细介绍
Zavu Functions
Zavu Functions = Zavu Cloud 上的无服务器 TypeScript + 面向 AI Agent 的声明式框架。
import { defineAgent, defineTool } from "@zavudev/functions"
defineAgent({
senderId: process.env.SENDER_ID!,
name: "Bella",
provider: "zavu",
model: "openai/gpt-5.6-luna",
prompt: "You are Bella, host at the restaurant. Be brief.",
})
defineTool({
name: "check_availability",
description: "Get free reservation slots for a date.",
parameters: {
type: "object",
properties: { date: { type: "string" }, partySize: { type: "number" } },
required: ["date", "partySize"],
},
handler: async ({ date, partySize }) => {
return { available: true, slots: ["19:00", "21:00"] }
},
})
这就是一个完整的 Agent + 工具。npx zavudev deploy 会协调线上状态。
项目结构
动手编写之前就要确定项目结构,因为之后再重构会付出高昂代价。
my-agent/
├── index.ts entrypoint: defineAgent + defineTool live here
├── lib/orders.ts imported by index.ts, deployed with it
├── package.json npm dependencies, installed at build time
└── .zavu/ created by the CLI; do not edit
- 使用多个文件没有问题。
deploy从index.ts开始,并上传它通过相对导入引用到的所有文件。你可以随意拆分提示词、工具和业务逻辑。 - 只有导入的内容才会部署。 未被导入的文件会留在本地,因此测试和临时工作文件不会部署。
- npm 包不是文件。 在
package.json的dependencies中声明它们;系统会为你安装并打包。 - **限制:**最多 200 个文件,总大小不超过 900,000 字节。项目根目录以外的路径(
../shared/x.ts)、node_modules/和以__zavu开头的名称会被拒绝。若要在多个项目间共享代码,请将其发布为 npm 包。 --source <file>用于指定其他入口文件。
何时使用 Functions,何时使用命令式 AI Agent API
| 使用场景 | 使用方式 |
|---|---|
| 客户需要使用自己的语言编写代码优先的 Agent,并自定义工具处理程序 | Functions |
| 工具需要查询用户的数据库、调用内部 API,或在返回前转换数据 | Functions |
| 用户希望从 git 仓库获取可复现配置(单一事实来源) | Functions |
| 用户希望通过控制面板配置免代码方案 | 命令式 senders.agent.create API(参见 ai-agent 技能) |
用户需要事件驱动的处理程序(message.inbound、broadcast.status_changed),且不想通过控制面板进行配置 | Functions |
如果用户提到编写代码、defineAgent、defineTool、npx zavudev deploy 或“无服务器”,请使用此技能。否则请转到 ai-agent。
CLI 作为主要接口
Functions 完全通过 zavu CLI 管理,而不是通过 API 调用。安装一次即可:
# `npx zavudev@latest` needs no install and always runs the current version.
# Pin nothing: an old CLI is the most common source of "the API does not
# support this".
npx zavudev@latest --version
npx zavudev login
npx zavudev login 会打开浏览器,并将凭据保存到 ~/.zavu/credentials.json。
工厂 Agent
最快的方式是创建可用的语音或文本 Agent:将现成的 Agent 拉取到代码库中,然后部署。每个工厂 Agent 都是一个 Zavu Function,其 index.ts 使用 defineAgent 声明 Agent,并使用 defineTool 声明其技能——因此,拉取的是真实、可编辑且归你所有的代码。
npx zavudev agents catalog # list the factory agents
npx zavudev agents pull fermi --sender <senderId> # scaffold ./fermi, register it, set SENDER_ID
cd fermi
npx zavudev deploy
npx zavudev agents catalog 会列出每个 Agent 的 ID、名称、是否为 voice、工具数量和类别。npx zavudev agents pull <id> 会生成 ./<id>(可使用 --dir <path> 指定其他目录);--sender <senderId> 会设置 SENDER_ID 密钥,让 npx zavudev deploy 立即生效——如果省略该选项,请先运行 npx zavudev fn secrets set SENDER_ID <senderId>。语音 Agent 会预先填入 voice 配置块;编辑 index.ts 并重新部署即可迭代。
npx zavudev agents init 会通过一个引导式命令完成全部设置:创建 sender(可按需购买电话号码)、拉取工厂 Agent,并设置 SENDER_ID。
Agent 回复所使用的渠道由 sender 的渠道决定,每个渠道都可通过 CLI 连接。sender 拥有电话号码后,语音和 SMS 即可使用;其他富媒体渠道可分别通过一条命令连接:
npx zavudev telegram connect --sender <senderId> --token <botFatherToken>
npx zavudev email-domains add example.com # then publish DNS + `verify`
完整生命周期
1. 初始化项目
npx zavudev fn init --name order-bot --template blank
cd order-bot
可用模板:blank、restaurant-booking。运行 zavu fn init --help 查看当前列表——应以该列表为准,而非本页面。
有合适的工厂 Agent 时,优先使用它。 zavu agents catalog 会列出生产环境 Agent(客服支持、潜在客户收集、预订);zavu agents pull <id> 会生成一个完整的 Agent,包含提示词、技能,以及语音 Agent 的语音配置。
init 命令会创建 index.ts、package.json 和 .zavu/config.json,将此目录关联到用户项目中的 Function 记录。关联完成后,后续命令都会自动解析该 Function。
2. 设置密钥
密钥是加密的环境变量,会在部署时注入 Function。
npx zavudev fn secrets set SENDER_ID jx7abc123def456
npx zavudev fn secrets set DATABASE_URL "postgres://..."
npx zavudev fn secrets list
npx zavudev fn secrets unset OLD_KEY
运行 npx zavudev senders list 获取 sender ID。
3. 编写 Agent + 工具
编辑 index.ts:
import { defineAgent, defineTool, defineFunction } from "@zavudev/functions"
// Four fields are required: name, provider, model, and the prompt (under
// `prompt` or `systemPrompt`). A declaration missing any of them is SKIPPED —
// the deploy still succeeds, and you get a function with no agent. `description`
// and `instructions` are not fields.
defineAgent({
senderId: process.env.SENDER_ID!, // Optional: omit to declare it standalone
// and connect senders afterwards.
name: "Bella",
provider: "zavu", // Zavu's AI gateway (charged from project balance)
// Or "openai" / "anthropic" / "google" / "mistral" with BYOK + apiKey
model: "openai/gpt-5.6-luna", // For "zavu" provider, prefix with the underlying provider
prompt: "You are Bella…", // System prompt
channels: ["whatsapp"], // Optional: default ["*"] = all channels the sender supports
// Listing a channel the sender does NOT have does
// not add it — it just never fires. Prefer the
// default unless you are deliberately excluding one.
// apiKey: process.env.OPENAI_API_KEY // only for BYOK providers (openai / anthropic / google / mistral)
})
defineTool({
name: "lookup_order",
description: "Get current status of an order. Use when the customer asks about an order they placed.",
parameters: {
type: "object",
properties: { orderId: { type: "string" } },
required: ["orderId"],
},
handler: async (args, ctx) => {
// ctx: { projectId, functionId, slug, log, memory, contactId?, conversationId?,
// senderId?, channel?, messageId?, contactPhone?, sessionId?, callId? }
const res = await fetch(`https://pos.example.com/orders/${args.orderId}`, {
headers: { Authorization: `Bearer ${process.env.POS_API_KEY}` },
})
return await res.json()
},
})
// Optional: handle raw events (message.inbound from triggers, HTTP calls).
// NOT needed if you only declare agent + tools.
// The object form is what binds events — `on` is the subscription.
export default defineFunction({
on: ["message.inbound"],
handler: async (event, ctx) => {
ctx.log("got event", event.type)
},
})
4. 部署
npx zavudev deploy
输出:
✓ Deployed in 6.4s
Agents synced:
+ Bella
Tools synced:
+ lookup_order
| 标记 | 含义 |
|---|---|
+ name | 已创建 |
~ name | 已存在并已写入 |
= name (unchanged) | 已存在且没有任何差异 |
这些标记区分两种情况:未编辑就重新部署时会打印 =,而 ~ 表示该 Agent 或工具的某些内容确实被重写了。较旧的后端对所有已存在的内容都会打印 ~,从不输出 =,所以如果你使用的是这类后端,未编辑就重新部署时的输出会与重写提示词时完全相同。
无论哪种情况,这些标记描述的都是部署写入了什么,而不是 Agent 现在会说什么。要验证更改是否已传到模型,请询问它:
npx zavudev agents test --agent <agentId> --message "<something your edit changes>" --json
在你编辑的提示词中放入一个独特的词,然后检查它是否返回了这个词。这只需一条命令,不会产生任何费用,而且与部署摘要不同,它回答的是你真正想知道的问题。
请阅读 ✓ 上方的内容。 警告会在成功提示行之前打印,涵盖绿色部署并未如表面所示那样生效的情况:清单探测出错(没有同步任何内容,且命令以非零状态退出)、附加到 Agent 的工具所用频道永远不会调用它们,或者第二个 Agent 被部署到已有 Agent 的发送方上——此时只有第一个 Agent 会响应。
如果 Agent 最终没有任何可用频道,就不会显示 ✓:它声明的 channels 是其发送方无法接收的频道,因此它虽然已部署并启用,却无法接收任何消息。部署会打印 Agent 和不匹配信息,并以非零状态退出。你可以通过更改 Agent 的 channels 来修复,也可以将该频道连接到发送方(npx zavudev senders get <id> 会列出其实际拥有的频道)。
本地开发
这个脚手架是一个真正的 TypeScript 项目:zavu init 和 zavu agents pull 会写入 tsconfig.json 和 package.json,其中声明了 @zavudev/functions、typescript 和 @types/node 为开发依赖。
通过 HTTP 暴露函数
需要独立端点的函数——例如由你托管的 Webhook,或由你控制的工具 URL——可通过 --http 创建:
npx zavudev fn init --template blank --http
npx zavudev agents pull kepler --sender "$SENDER_ID" --http
只有在其后部署了内容后,URL 才会生效。deploy 会打印该 URL,你也可以随时查询:
npx zavudev fn info # status, and the public URL when HTTP is on
npx zavudev fn info --json # same, machine-readable
在现有函数上切换此功能也是一项 CLI 操作——它会应用于已部署的函数,无需重新部署:
npx zavudev fn http enable
npx zavudev fn http disable
HTTP 事件不是 Zavu 事件
如果想当然地处理这种事件,可能会导致你重新部署。这是唯一需要特别注意的数据形状。HTTP 调用传入的是原始 API Gateway v2 负载,而不是触发器传递的 { type, data } 事件。event.type 和 event.data 的值都是 undefined,而请求正文会以字符串形式放在 event.body 中:
{
"version": "2.0",
"rawPath": "/",
"headers": { "content-type": "application/json" },
"requestContext": { "http": { "method": "POST" } },
"body": "{\"action\":\"availability\"}",
"isBase64Encoded": false
}
因此,请按以下方式读取,并从 requestContext 检查请求方法:
export default defineFunction({
handler: async (event: any) => {
const method = event?.requestContext?.http?.method ?? "GET"
const raw = event?.isBase64Encoded
? Buffer.from(event.body ?? "", "base64").toString("utf8")
: event?.body
const payload = typeof raw === "string" && raw ? JSON.parse(raw) : {}
return { statusCode: 200, body: JSON.stringify({ ok: true, method, payload }) }
},
})
将处理程序的类型标注为 Zavu 事件可以顺利通过编译,但在生产环境中读取到的值会是 undefined——运行时包自带的类型已经对此发出警告。
在本地进行任何操作前,请运行 npm install。 在此之前,什么都无法解析:
cd <your-function-dir>
npm install
如果不运行它,zavu fn invoke 会报错 Cannot find module '@zavudev/functions',而 npx tsc 会悄悄下载一个无关的弃用软件包,而不是编译器。无论是否安装,部署都能正常工作——生产环境会通过运行时层提供该模块,你的机器不会将它打包发布——所以这只会影响本地开发流程,而你正是靠这个流程验证自己的工作。
请提交 .zavu/——它保存着 functionId,而且没有查询该 ID 的命令,因此队友克隆项目时如果没有它,就无法部署。
zavu fn invoke 使用 Bun 运行你的代码。请从 https://bun.sh 安装。其他所有命令都使用普通 Node 运行。
5. 测试
本地调用(跳过云端往返):
# Call a tool handler with synthetic args
npx zavudev fn invoke --tool lookup_order --args '{"orderId":"ORD-001"}'
# Simulate an inbound event for defineFunction
npx zavudev fn invoke --event message.inbound --data '{"from":"+14155551234","text":"hi"}'
运行处理程序前,系统会根据工具自身的 parameters 架构检查 --args,包括必填字段、类型和封闭的 enum 值。若不匹配,系统会指出确切字段和预期结构,并且绝不会调用处理程序:
✗ arguments do not match the tool's schema:
• missing required "orderId"
• "score" must be one of "hot" | "warm" | "cold", got "banana"
expected: { orderId: string, score: hot|warm|cold, notes: string? }
如果项目尚未安装依赖,此命令会停止运行,并提示你执行 npm install,而不是显示 Bun 原始的模块解析错误。
不会注入密钥。 API 会返回每个密钥的键名和末尾四个字符,但绝不会返回密钥值,因此 fn invoke 无法获取这些密钥,包括使用 --live 时。请将它们放在 index.ts 旁边的 .env 文件中(系统会自动读取),或在 shell 中导出它们。否则,读取 process.env.SOMETHING 的处理程序会进入未配置分支,并返回类似 { ok: false, reason: "not_configured" } 的合理失败结果,看起来像是代码有问题,其实并非如此。该命令会发出警告,并列出缺少的键。
--live 会实际调用 SDK,而不是模拟调用。默认情况下,所有内容都会被模拟,因此除非你主动启用,否则不会发送任何内容。
退出码值得据此分支处理:2 表示调用方式有误(未知工具、不符合架构的参数),1 表示处理程序本身抛出了异常,0 表示运行成功。因此,绿色的运行结果意味着处理程序确实使用有效输入执行了,而不只是没有崩溃。
无需发送任何内容,即可测试 Agent 的大脑:
npx zavudev agents list # your agents, with their ids
npx zavudev agents test --agent <agentId> --message "where is order ORD-001?"
此命令会打印 Agent 将会给出的回复,以及令牌数、延迟和使用的知识块数量。不会发送任何内容,也不会产生费用,因此每次修改提示词时都可以安全运行。
工具会在每个渠道上运行——包括纯文本(SMS/WhatsApp/Telegram)、语音,以及流程中的
tool步骤。收到查询信息请求的文本 Agent 会调用工具并使用真实数据作答(每次回复最多进行 5 轮工具调用)。 默认情况下,agents test是个例外:它会提供工具,报告 Agent 选择的工具及所用参数,然后就此停止。因此,你看到的回复是在谈论一次从未发生的查询——其中的任何数字都是编造的,运行结果也会对此发出警告。
如需查看真实的调用循环——模型选择工具,处理程序返回结果,模型再根据结果作答:
npx zavudev agents test --agent <agentId> --message "where is order ORD-001?" --execute-tools
处理程序会实际运行,因此它们执行的任何操作(收费、写入、通知)都会真实发生。不会向客户发送任何内容,但副作用不会被模拟。每次调用都会打印出来,并注明是否已作答:
✓ get_order_status
✗ charge_card: HTTP 500
出错的工具和正常工作的工具同样有价值:Agent 看到了错误,并据此作答,这正是客户会收到的回复。如果 Agent 完全没有调用任何工具,运行结果也会说明这一点——自信地作答却一次工具都没调用,正是这个标志要捕捉的问题。
使用 fn invoke --tool,无需调用模型即可单独测试某个处理程序。
端到端测试: 向发送方的 WhatsApp/SMS/Telegram 号码发送真实消息,或为语音 Agent 拨打电话。Agent 会运行 LLM,并通过相同渠道回复。
6. 调试
发生故障时,请从上到下逐步检查调用链:
# 1. Did the inbound reach the agent?
npx zavudev agents executions list --sender <senderId>
# 2. Detail of any failed run
npx zavudev agents executions get <executionId> --sender <senderId>
# 3. Live tool handler logs (your console.log calls)
npx zavudev fn logs --tail
executions list 命令上的 --json 标志会返回完整负载,包括 errorMessage,便于解析诊断信息。
部署本身失败时,构建输出会显示在 Build output: 下方——其中包括未解析的导入、语法错误或无法安装的软件包。上方的一行状态只是摘要,很少会指出可供你采取行动的具体问题;Build output: 下方的内容才会。相同文本也会保留在部署记录的 buildLogs 中,可通过 REST 在 GET /v1/functions/deployments/{deploymentId} 读取(npx zavudev fn versions list 可获取 ID)。
defineAgent 参考
defineAgent({
senderId: string, // Required. The sender that receives inbound + dispatches the agent.
name: string, // Required. Displayed in dashboard.
provider: "zavu" | "openai" | "anthropic" | "google" | "mistral",
model: string, // For "zavu": prefix with underlying provider e.g. "openai/gpt-5.6-luna"
prompt: string, // System prompt.
apiKey?: string, // Required for non-"zavu" providers.
channels?: string[], // Default ["*"] = whatever the sender has. See below.
messageTypes?: string[], // Default ["text"]. Filter by message type.
groups?: boolean, // Answer in group chats, on channels that carry them. Default false. See below.
temperature?: number, // 0-2.
maxTokens?: number, // Cap on output tokens.
contextWindowMessages?: number,// Past N messages included as context. Default 10.
sessionTimeoutMinutes?: number,// Reset conversation context after N minutes. Default 60.
includeContactMetadata?: boolean, // Inject contact's metadata into the system prompt. Default true.
enabled?: boolean, // Default true. Read only when the deploy creates the agent.
voice?: VoiceConfig, // Add to make the agent answer phone calls. See "Voice agents".
})
模型设置在重新部署后仍会保留。 provider、model、temperature、
maxTokens、contextWindowMessages、includeContactMetadata 和
sessionTimeoutMinutes 也可以在控制面板中更改,或通过
PATCH /v1/agents/{agentId} 更改。只有当代码自上次部署以来更改了某项设置时,
部署才会写入该设置;否则,线上值会保持不变,部署输出会列出代码声明的值。
provider + model 算作一项设置。若要强制使用代码中的模型,请在代码中更改模型并部署。
代码声明的其他所有内容(名称、提示词、渠道、语音)都会在每次部署时写入,
而 enabled 在创建后绝不会被重写。
群聊与 NO_REPLY
除非声明 groups: true,否则 Agent 只会回复一对一对话。
启用后,Agent 也会在支持群聊的渠道中回复群聊,且回复会发到群组,
而不是私下发给发消息的人。无论如何,群消息都会发送到你的 Webhook 和 Functions。
移除 groups 后,它会在下次部署时关闭。groups 需要使用当前运行时:
固定在旧运行时上的函数会在部署时忽略此字段,因此请使用
npx zavudev deploy --update-runtime 部署。
在群聊中,Agent 会收到每条消息并被询问;每行消息的格式为
Name (+number): text,如果发言者没有个人资料名称,则只显示号码。
个人资料名称由用户自行填写,因此提示词若需要判断发言者身份,应以号码为准。
群聊不支持流程或交接,并将 Agent 限制为每个群组每分钟最多回复 12 次,
因此两个 Agent 不会无限循环。
若要保持安静,Agent 必须准确回复 NO_REPLY(会忽略首尾空白)。
任何渠道都不会发送消息,且此次执行会记录为 filtered;但仍会计费。
在群聊中,Agent 会收到关于此行为的说明;请在提示词中说明何时使用它:
defineAgent({
senderId: process.env.SENDER_ID!,
name: "Host",
provider: "zavu",
model: "openai/gpt-5.6-luna",
prompt: "Answer only when someone addresses you by name. Otherwise reply NO_REPLY.",
groups: true,
})
语音 Agent
在 defineAgent 中添加 voice 块,即可让 Agent 接听电话。当该块存在且设为
enabled: true 时,Agent 会接听发送者的号码。为实现最低延迟,大模型会
与语音网络共置运行,且独立于文本 model——设置 voice.model 以选择通话模型,
或省略该项,由文本 model 推导。移除此块后,Agent 会在下次部署时恢复为纯文本模式。
defineAgent({
senderId: process.env.SENDER_ID!,
name: "Fermi",
provider: "zavu",
model: "openai/gpt-5.6-luna", // text model
channels: ["voice", "whatsapp"],
voice: {
enabled: true,
model: "openai/gpt-4o", // co-located voice model (optional; derived from `model` if omitted)
greeting: "Hi, I'm Fermi. How can I help?",
language: "en", // BCP-47; auto-detected if omitted
interruptible: true, // caller can barge in
maxCallDurationMinutes: 10,
},
prompt: "You are Fermi…",
})
voice 参考
voice: {
enabled: boolean, // Required. true = answer/place calls; removing the block reverts to text-only.
model?: string, // Co-located call model, e.g. "openai/gpt-4o". Derived from the text model if omitted.
greeting?: string, // Opening line. Max 1000 chars. If omitted, the agent waits for the caller.
greetings?: Record<string, string>, // Per-language greeting keyed by language tag: { es: "Hola…" }. Used when the caller's language differs from the one `greeting` is written in. Factory agents ship with this set.
language?: string, // BCP-47 (e.g. "en", "es", "pt-BR"). Auto-detected if omitted.
ttsVoiceId?: string, // Voice used for speech synthesis.
voiceSpeed?: number, // Speech rate 0.5–1.5. Default 1.0.
interruptible?: boolean, // Caller can barge in while the agent speaks. Default true.
maxCallDurationMinutes?: number, // Hard cap on call length. Default 15.
maxIdleSeconds?: number, // End the call after this much silence (5–300). Default 30.
voicemailAction?: "hangup" | "leave_message", // On answering-machine detection (outbound). Default "hangup".
voicemailMessage?: string, // Spoken when voicemailAction is "leave_message". Falls back to greeting.
transferPhoneNumber?: string, // E.164. Gives the agent a tool to transfer the call to a human.
}
语音功能要求发送者已分配电话号码,并且已启用语音渠道。超出范围的值会使
npx zavudev deploy 发出警告,并将 Agent 作为纯文本版本发布,而非直接失败——请修正
该值并重新部署。
在 voice 块中出现拼写错误或未知的键则是另一回事:该键会被丢弃且不会发出警告,
部署也会以 0 退出,因此 interruptable 不会带来任何效果,也不会起到任何作用。
请使用 npx zavudev agents get <agentId> 确认该块按你的写法生效,而不要只相信部署成功提示。
在代码中使用知识库(RAG)
defineAgent 会声明知识库,因此,从文档中作答的 Agent 只需一个文件和一次部署。
无需单独调用 API。
defineAgent({
senderId: process.env.SENDER_ID!,
name: "Ada",
provider: "zavu",
model: "openai/gpt-5.6-luna",
prompt: "Answer from the store policies. If they do not cover it, say so.",
knowledgeBase: {
name: "Store policies",
documents: [
{ title: "Returns", content: "# Returns\n\nUnopened: 30 days. Opened: 14 days if faulty." },
{ title: "Shipping", content: "# Shipping\n\nFree over $50, 3-5 business days." },
],
},
})
部署是事实来源:移除的文档会被删除,内容有更改的文档会重新生成嵌入,未更改的文档则不会处理,
因此重新部署不会产生费用。文档按 title 匹配,因此重命名会替换原文档。
Knowledge bases:
+ Store policies (knowledge base)
documents sync in the background; check with `agents knowledge-bases documents list`
部署会列出知识库,而不会逐个列出文档:文档会在部署返回后同步,因此如果在部署输出中逐条列出文档, 无论是否有文档发生变化,都会显示没有任何变化。
请验证检索结果,不要想当然:
npx zavudev agents test --sender "$SENDER_ID" --message "can I return an opened item?"
回复会报告使用了多少个知识块。如果 Agent 已有文档,但该数值为 0,说明回答没有以文档为依据, 而这样的回答看起来可能完全正确。
代码中的流程
defineFlow 会在与 Agent 相同的文件中声明确定性的对话。当模型不能即兴发挥时,请使用它:例如固定的问题序列、根据回答进行分支,或使用收集到的值调用工具。
import { defineAgent, defineTool, defineFlow } from "@zavudev/functions"
defineFlow({
name: "Repair intake",
trigger: { type: "keyword", keywords: ["broken", "repair"] },
steps: [
{
id: "ask_issue",
type: "collect",
config: { variable: "issue", prompt: "What is wrong with it?" },
nextStepId: "open",
},
{
id: "open",
type: "tool",
config: { toolName: "create_ticket", params: { issue: "{{issue}}" } },
nextStepId: "done",
},
{ id: "done", type: "message", config: { text: "Logged." } },
],
})
引擎会匹配 keyword、intent 和 always 触发器;manual 会被接受,但不会启动任何流程。步骤的结构请参见 ai-agent 技能。
在代码中声明流程有以下四点需要特别注意:
- 流程初始处于禁用状态。 流程会拦截真实对话,因此编写流程和启用流程是两个独立的决定。首次部署时传入
enabled: true,或运行npx zavudev agents flows update <flowId> --sender "$SENDER_ID" --enabled。 - 后续部署不会更改
enabled。 在事件期间暂停流程的状态会在重新部署后保留。 - 步骤指定的工具必须已添加到 Agent。 在同一文件中使用
defineTool即可,因为工具会先完成同步。步骤指定了不存在的工具时,系统会报告该问题并跳过该流程;部署的其余部分仍会生效。 - 有两种情况绝不会被触及: 与该函数同名、但并非由该函数创建的流程;以及联系人当前正在进行的流程。系统会报告这两种情况,而不会覆盖或删除它们。
代码中的多个发送方
senderId 是主要发送方。senderIds 列出其余发送方:
defineAgent({
senderId: process.env.SENDER_ID!,
senderIds: [process.env.SUPPORT_SENDER_ID!],
name: "Ada",
provider: "zavu",
model: "openai/gpt-5.6-luna",
prompt: "Be brief.",
})
仅连接。停止列出的发送方仍会保持连接,系统也会报告这一情况,因为断开连接会导致正在使用的号码无法接收回复。确实要断开连接时,请使用 DELETE /v1/agents/{agentId}/senders/{senderId}。
文档以内联方式存储,部署的源文件大小上限约为 900KB。若超过此上限,请改用 npx zavudev agents knowledge-bases documents add --content-file ./manual.md。
一个函数包含多个文件
将一个函数拆分到多个文件中,并在文件间导入:
// index.ts
import { formatOrder } from "./lib/orders"
npx zavudev deploy 会从入口点开始(默认为 index.ts,或使用 --source <file> 指定),并上传它通过相对导入所访问的所有文件。未被导入的文件不会部署,因此测试和临时文件会保留在本地。构建过程会解析已上传文件之间的导入关系。
拒绝的路径包括:项目根目录之外的路径(../shared/x.ts)、node_modules/,以及以 __zavu 开头的名称。限制:最多 200 个文件,总大小为 900,000 字节。
npm 依赖项需单独处理:将它们放在 package.json 的 dependencies 中,系统便会安装并打包。
若要改为在每次推送时部署整个代码库:
npx zavudev fn git link acme/my-agent --branch main
通过 REST 发送 files(路径到内容的映射),并可选提供 entrypoint;对于单文件函数,仍可使用快捷方式 sourceCode。
defineTool 参考
defineTool({
name: string, // Required. snake_case, max 64 chars.
description: string, // Required. The LLM reads this to decide WHEN to call the tool.
parameters: {
type: "object",
properties: { /* JSON Schema */ },
required?: string[],
},
handler: async (args, ctx) => any, // Required. Return any JSON-serializable value.
agent?: string, // Optional: which agent owns this tool. Defaults to the only agent in the file.
enabled?: boolean, // Default true.
})
处理程序 ctx 结构
{
projectId: string,
functionId: string,
slug: string,
awsRequestId?: string,
messageId?: string, // ID of the triggering inbound (when called by agent)
contactPhone?: string,
sessionId?: string, // Active flow session if any
channel?: string, // "whatsapp" | "sms" | "voice" | "email" | ...
callId?: string, // The voice call in progress. Voice channel only
contactId?: string, // The contact Zavu resolved, when it knows who is on the other side
conversationId?: string, // The inbox thread this conversation belongs to
senderId?: string, // The sender the conversation is running on
log: (...args) => void, // console.log proxy that appears in `npx zavudev fn logs --tail`
memory: Memory, // What the agent remembers. See below
}
ctx.memory — Agent 的记忆内容
需要 @zavudev/functions 0.3.0+。回忆信息有两种方式,使用同一套 API:
// By meaning — embedded on write, retrieved semantically
await ctx.memory.contact!.add("Prefers WhatsApp over email.")
const hits = await ctx.memory.contact!.search("how they like to be reached", { minScore: 0.3 })
// By key — JSON in, JSON out
const orders = ctx.memory.contact!.collection("orders")
await orders.set("ORD-1", { status: "shipped" })
const order = await orders.get("ORD-1") // null when absent
集合键只能包含 A-Z a-z 0-9 . _ : @ -,不能包含 +,因此 E.164 电话号码不是有效的键。请使用其数字(56940560201)或前缀(phone:56940560201)作为键,并将 E.164 号码保存在值中。
每次读取和写入都恰好发生在一个 作用域 中,各作用域彼此隔离:
| 句柄 | 作用域 | 可用时机 |
|---|---|---|
ctx.memory | 项目 — 所有对话共享 | 始终可用 |
ctx.memory.contact | 正在交谈的对象 | 在对话之外为 undefined |
ctx.memory.conversation | 当前会话 | 在对话之外为 undefined |
ctx.memory.forContact(id) / forConversation(id) | 指定对象或会话 | 始终可用 |
默认作用域是项目。 如果不带选项调用 ctx.memory.add(fact),写入的信息之后可被其他所有客户的对话读取;与个人相关的信息请使用 ctx.memory.contact。contact 和 conversation 均为可选项,因此请处理它们缺失的情况:从 cron 触发器首次运行时,假定联系人必定存在的工具会抛出 MemoryScopeError。
完整 API、作用域、限制、CLI 和 REST API:memory 技能。
defineFunction 参考(可选)
仅在以下情况使用:
- 原始 HTTP 请求(通过公共 URL 暴露的函数——创建时使用
zavu fn init --http,或对现有函数使用zavu fn http enable) - 原生事件触发器(
message.inbound、broadcast.status_changed等——通过npx zavudev fn triggers add配置)
export default defineFunction(async (event, ctx) => {
if (event.type === "message.inbound") {
// event.data: { from, text, channel, messageId, ... }
}
return { ok: true }
})
触发器(事件订阅)
要让 defineFunction 响应 Zavu 事件:
npx zavudev fn triggers list
npx zavudev fn triggers add --events message.inbound --senders <senderId>
npx zavudev fn triggers add --events broadcast.status_changed --senders any
# Schedules: event type "cron" + a 5-field UTC expression (minimum 1 minute).
# The function receives { type: "cron", data: { cron } }. Several schedules
# per function are allowed (different expressions).
npx zavudev fn triggers add --events cron --cron '*/15 * * * *'
npx zavudev fn triggers add --events cron --cron '0 9 * * 1-5' # weekdays 09:00 UTC
npx zavudev fn triggers toggle <triggerId>
npx zavudev fn triggers rm <triggerId>
npx zavudev fn triggers events # list available event types
每次推送时部署
将 GitHub 仓库关联到函数,推送到分支即可部署:
npx zavudev fn git link acme/order-bot --branch main
npx zavudev fn git link acme/monorepo --root apps/bot # monorepos
npx zavudev fn git status # link + last deploy
npx zavudev fn git deploy # deploy the branch now
npx zavudev fn git set --no-auto-deploy # keep the link, ignore pushes
npx zavudev fn git unlink
参数可使用 owner/repo、github.com URL 或 SSH 远程地址。
服务器会决定关联所使用的身份验证方式,输出中的 connection 会告知你实际使用了哪种方式。 如果账户已安装 Zavu GitHub App,该命令就完成了全部设置,私有仓库也可正常使用。如果未安装,你会得到一个 manual 关联:命令会输出一个载荷 URL 和一个密钥,你需要自行将其添加为仓库中的 webhook,且密钥只会显示一次——重新关联会生成一个新密钥。
关联时不会向 GitHub 检查仓库是否存在,因为无法进行此项检查:不存在的 owner/repo,或安装程序无法访问的仓库,都会被接受,并在首次部署时失败。首次推送后,请查看 fn git status,不要以为关联成功就代表配置可正常工作。
触发器使用带签名的内部调用(处理程序内部无需进行 HMAC 验证)。
版本与回滚
每次运行 npx zavudev deploy 都会创建一个不可变版本。
npx zavudev fn versions list # alias: npx zavudev fn history
npx zavudev fn rollback 4 # go back to version 4
仪表板中的函数元数据会跟踪当前生效的版本,也支持直接在界面中回滚。
运行时版本
每个函数在首次部署时都会固定到特定的运行时层。后续部署会沿用相同的固定版本(不可变,以确保稳定性)。
npx zavudev deploy --update-runtime # opt-in upgrade to latest runtime
仅在有安全公告或需要某项功能时才选择启用——不带此标志运行 npx zavudev deploy,始终安全。
定价模式
函数按调用单位计费,并根据内存加权:
| 内存 | 每次调用的单位数 |
|---|---|
| 128 MB | 1 |
| 256 MB | 2 |
| 512 MB | 4 |
| 1024 MB | 8 |
每个套餐都包含月度额度;超额费用会通过按量计费计入下一张 Stripe 账单。
| 套餐 | 包含的单位数 | 超额费率 |
|---|---|---|
| Free | 100k | 硬性上限(阻止调用) |
| Hobby | 1M | $5 / 1M |
| Standard | 5M | $4 / 1M |
| Growth | 10M | $3 / 1M |
创建函数时或通过仪表板设置内存。内存越低,费用越低。大多数工具处理程序使用 128 MB 即可。
常见模式
接管手动创建的 Agent
如果用户已经通过仪表板或 npx zavudev agents create 创建了 Agent,在代码中用相同的 senderId + name 声明它,就会接管该 Agent——Zavu 会为其标记 managedByFunctionId,并在仪表板中锁定手动编辑,模型设置除外。函数源码将成为事实来源。代码中未指定的模型设置(temperature、maxTokens 等)会保留手动创建的 Agent 原有值。
要恢复手动控制:删除函数(npx zavudev fn delete),Agent 即可解除绑定。
清理创建的函数
fn delete 会级联删除 Lambda、触发器、密钥、部署历史,以及该函数拥有的所有 Agent 和工具。系统会要求你重新输入 slug。
以非交互方式运行时,请预先确认 slug:
npx zavudev fn delete --confirm <slug>
这并非盲目使用 -y。你仍须指定要删除的对象,因此目录错误或 ID 过期时,命令会失败,而不会删除其他内容——这也让脚本或负责清理自身资源的 Agent 可以安全地使用此命令。
按环境区分发送方
defineAgent({
senderId: process.env.NODE_ENV === "production"
? process.env.PROD_SENDER_ID!
: process.env.DEV_SENDER_ID!,
// ...
})
然后在生产环境中运行 npx zavudev fn secrets set NODE_ENV production,在开发环境中运行 ... development。代码相同,Agent 不同。
BYOK(自带密钥)
对于 OpenAI / Anthropic / Google / Mistral,可直接传入 apiKey:
defineAgent({
senderId: process.env.SENDER_ID!,
provider: "openai",
model: "gpt-4o-mini",
apiKey: process.env.OPENAI_API_KEY,
prompt: "...",
})
运行 npx zavudev fn secrets set OPENAI_API_KEY sk-...。Agent 会直接使用该密钥——大模型调用不会消耗 Zavu 余额。
将工具固定到特定 Agent(多 Agent 函数)
当函数声明多个 defineAgent 时,工具默认关联到第一个。要显式选择:
defineTool({
name: "lookup_order",
agent: "Bella", // Match by agent's name field
// ...
})
在处理程序中调用 Zavu API
每个函数都会自动注入一个作用域受限的 ZAVU_API_KEY — 使用 SDK 回调:
import { Zavudev } from "@zavudev/sdk"
const zavu = new Zavudev({ apiKey: process.env.ZAVU_API_KEY })
defineTool({
name: "send_followup",
handler: async (args, ctx) => {
await zavu.messages.send({
to: ctx.contactPhone!,
text: "Thanks for your order!",
})
return { sent: true }
},
})
自动配置的密钥具有 messages:send、messages:read、contacts:read 作用域。如需更广泛的访问权限,请创建手动 API 密钥并将其作为密钥注入。
控制面板
https://dashboard.zavu.dev/functions/<id> 显示以下选项卡:
- 代码 — 当前草稿源代码(可在浏览器中编辑)
- 触发器 — 事件订阅
- Agent 和工具 — 此函数管理的内容,带有指向执行记录的深层链接
- 依赖项 — 此软件包使用的 npm 包
- 密钥 — 加密的环境变量(值仅可写入)
- 版本 — 部署历史记录 + 回滚
- 日志 — 最近调用的运行时 stdout/stderr
- 设置 — 内存、超时、httpEnabled、删除
参考文档
- 概述:https://docs.zavu.dev/concepts/functions
- 快速入门:https://docs.zavu.dev/guides/functions/quickstart
- CLI 参考:https://docs.zavu.dev/guides/functions/cli
- defineAgent:https://docs.zavu.dev/guides/functions/defining-agents
- defineTool:https://docs.zavu.dev/guides/functions/defining-tools
- 调试指南:https://docs.zavu.dev/guides/functions/debugging
- 示例:https://docs.zavu.dev/guides/functions/examples/restaurant
限制
- Slug:小写字母、数字和连字符,最多 50 个字符,在每个项目中唯一(重复时会返回
409 conflict)。 - 源代码包:压缩后 ≤ 900 KB。
- 环境变量总大小:所有密钥合计 4 KB。
- 密钥格式:
[A-Z_][A-Z0-9_]*。保留前缀:AWS_、LAMBDA_、_HANDLER、_X_AMZN。 - 超时:≤ 180 s(可配置,默认 30 s)。事件和 cron 调用是 异步的,因此较长的超时只会限制成本。在实时 对话中调用的工具是同步的:回复会等待,因此应将其耗时控制在远低于此值。
- 内存:128 / 256 / 512 / 1024 MB。
- 计费:按内存和时间计费。一次调用相当于 128 MB 使用 1 秒;更大或 更慢的函数会消耗多个计费单位,不足 1 秒也按 1 秒计算。 每个套餐每月包含 300,000 次调用,超出后每百万次收费 $5。
- 每个 Agent 的工具数:16。
- 每个函数的 Agent 数:没有硬性上限,但通常为 1 个。
反模式
- 不要将命令式
senders.agent.createAPI 与托管函数并行使用。如果函数声明了 Agent,该函数就拥有它 — 手动编辑会被阻止。 - 不要在源代码中硬编码
senderId。始终从process.env.SENDER_ID读取,这样相同的代码才能在不同环境中正常工作。 - 不要导入只在某一个工具中使用的重量级依赖项。每次调用都会冷启动;精简依赖项以降低延迟。
- 不要
console.log密钥。任何有项目访问权限的人都能看到函数日志。