mcpField
F
functions

functions

github.com/zavudevAgent 工作流1174 次安装今天更新
代码仓库

关于 functions

Zavu Functions 是在 Zavu Cloud 上构建无服务器 TypeScript Agent 和工具的代码优先框架,面向需要自定义处理逻辑并通过 CLI 部署和测试的开发者。

主要功能

  1. 01声明式定义 Agent 和工具
  2. 02部署导入的 TypeScript 项目文件
  3. 03通过 CLI 管理函数和密钥
  4. 04创建模板及预置 Agent
  5. 05本地调用处理程序进行测试
  6. 06通过 HTTP 暴露函数

适用场景

  1. 01构建带自定义工具处理程序的 Agent
  2. 02让工具访问数据库或内部 API
  3. 03从 Git 仓库部署 Agent 配置
  4. 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 MB1
256 MB2
512 MB4
1024 MB8

每个套餐都包含月度额度;超额费用会通过按量计费计入下一张 Stripe 账单。

套餐包含的单位数超额费率
Free100k硬性上限(阻止调用)
Hobby1M$5 / 1M
Standard5M$4 / 1M
Growth10M$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、删除

参考文档

限制

  • 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.create API 与托管函数并行使用。如果函数声明了 Agent,该函数就拥有它 — 手动编辑会被阻止。
  • 不要在源代码中硬编码 senderId。始终从 process.env.SENDER_ID 读取,这样相同的代码才能在不同环境中正常工作。
  • 不要导入只在某一个工具中使用的重量级依赖项。每次调用都会冷启动;精简依赖项以降低延迟。
  • 不要 console.log 密钥。任何有项目访问权限的人都能看到函数日志。

评论

支持 Markdown