使用 AI SDK WorkflowAgent 构建可恢复、可断点续跑的持久化聊天 Agent:next-workflow 示例全解析
【免费下载链接】aiThe AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai
导读
本指南基于 AI SDK 仓库中的 examples/next-workflow 示例,系统讲解如何用@ai-sdk/workflow的WorkflowAgent搭配 Workflow DevKit 构建一个**持久化(durable)、可恢复(resumable)**的聊天 Agent。示例不仅覆盖了带工具调用(天气查询、计算器、文件删除)的完整聊天链路,还演示了toModelOutput双视角输出、runtimeContext/toolsContext双上下文、工具审批、流式传输与断线重连,以及基于 webhook 的异步视频生成工作流。读完本文,你将掌握WorkflowAgent从「定义工具」到「部署运行」的完整实战路径,并能直接复刻这套架构到自己的 Next.js 项目中。
一、示例定位与核心特性
examples/next-workflow是一个 Next.js 应用,演示用 AI SDK 的WorkflowAgent(来自@ai-sdk/workflow包)构建容错、可恢复的 AI Agent 执行。与普通的generateText/streamText调用不同,WorkflowAgent把一次 Agent 运行编排成一组可持久化的步骤(step),每个步骤的状态都可以被记录和恢复,因此运行中途进程重启也不会丢失进度。
该示例在 README.md 中明确列出以下核心能力:
- 持久化 Agent:基于
@ai-sdk/workflow的WorkflowAgent,提供容错的 Agent 执行; - 工具调用:天气查询(
getWeather)与计算器(calculate)作为持久化步骤实现; toModelOutput:getWeather工具给模型发送紧凑的一行摘要,而 UI 保留完整结构化结果;- 流式传输:通过
getWritable()与createUIMessageStreamResponse实现实时流式响应; - 可恢复:Workflow 运行在重启后依然存活,且可以被重新连接;
- Telemetry E2E 测试台:访问
/telemetry运行确定性的 WorkflowAgent 遥测场景,覆盖生命周期事件、工具执行、上下文过滤、审批、错误与重连; - Sandbox E2E 测试台:访问
/sandbox运行确定性的沙箱工具执行场景; - 异步视频工作流:访问
/async-apis,查找近期仓库维护者并把他们的 GitHub 头像转成 FAL 短视频,进度实时流式推送到浏览器。
从 package.json 可以看到项目的依赖组合:@ai-sdk/workflow、@ai-sdk/anthropic、@ai-sdk/fal、@ai-sdk/react、ai、workflow@5.0.0-beta.42以及zod@4.4.3,其中workflow包(Workflow DevKit)提供了getWritable、start/getRun、createWebhook等运行时能力。
二、快速开始:安装、环境变量与启动
按照 README 的「Running」一节,运行该示例只需四步:
# 1. 安装依赖(仓库使用 pnpm workspace) pnpm install # 2. 创建 .env.local 并填写目标页面所需的 API Key # ANTHROPIC_API_KEY=... # FAL_API_KEY=... # GITHUB_TOKEN=... # 3. 启动开发服务器 pnpm dev # 4. 打开浏览器 # http://localhost:3000关于环境变量的说明(README 原话要点):
- 三个 Key 按需配置:跑聊天页需要
ANTHROPIC_API_KEY;跑异步视频页需要FAL_API_KEY;GITHUB_TOKEN需要对你提交的仓库有读取权限,公共仓库的公共访问权限即可满足; - 在「Async APIs」一节,README 还额外提及
WORKFLOW_LOCAL_BASE_URL:当本地无法被 FAL 回拨 webhook 时,可把它设置为一个能转发到本地服务器的公共 HTTPS 地址来启用 webhook 路径(详见后文第七节)。
主聊天页 app/page.tsx 使用@ai-sdk/react的useChat,并配置了WorkflowChatTransport作为传输层,同时提供跳转到/telemetry、/sandbox、/async-apis三个测试台的入口。
三、核心实现:WorkflowAgent 聊天示例
聊天工作流的全部逻辑位于 workflow/agent-chat.ts,入口函数chat声明为'use workflow',其中'use step'标记的异步函数则成为可持久化的步骤:
export async function chat(messages: UIMessage[], request: ChatRequestContext) { 'use workflow'; const modelMessages = await convertToModelMessages(messages, { tools }); const agent = new WorkflowAgent({ model: anthropic('claude-sonnet-4-20250514'), instructions: 'You are a helpful assistant with access to weather, calculator, and file deletion tools. ...', tools, runtimeContext: { tenantId, requestId, plan }, toolsContext: { getWeather: {...}, deleteFile: {...} }, prepareStep: ({ runtimeContext }) => { ... }, onEnd: ({ messages }) => { ... }, }); const result = await agent.stream({ messages: modelMessages, writable: getWritable<ModelCallStreamPart>(), repairToolCall: repairToolCall as any, }); return { messages: result.messages }; }几个关键点:
- 模型:示例使用
anthropic('claude-sonnet-4-20250514'),@ai-sdk/anthropic来自仓库 workspace; - 消息转换:
convertToModelMessages(messages, { tools })会把 UI 侧的UIMessage历史转换成模型消息。注意传入tools的目的——让历史轮次中遗留的工具结果也通过各工具的toModelOutput钩子重建,与 WorkflowAgent 对新鲜工具结果应用的转换保持一致;如果不传,早前轮次的工具结果会回退到默认的json/text序列化,导致跨轮次结果不一致(源码注释对此有明确说明); - 指令:明确要求 Agent「该动手就调用工具,不要只说要做」,保持回复简洁;
- 容错:
repairToolCall回调接收ToolCallRepairFunction<typeof tools>类型,示例中直接原样返回工具调用。
3.1 工具即持久化步骤
示例定义了两个核心工具,每个工具的execute都以'use step'开头,表示这是一个可以被持久化、暂停与恢复的工作流步骤:
async function getWeather(input: { city: string }, options: { context: { defaultUnit: ... } }) { 'use step'; // 用城市名的字符码哈希生成确定性的温度与天气,便于端到端演示 ... return { city, temperature, unit, condition }; } async function calculate(input: { expression: string }) { 'use step'; const translated = input.expression.replace(/\s/g, '').replace(/\^/g, '**'); if (!/^[0-9+\-*/().]+$/.test(translated)) throw new Error(`Invalid expression: ${input.expression}`); return { expression, result: new Function(`return (${translated})`)() as number }; }工具对象通过inputSchema(zod)校验输入,通过可选的contextSchema校验每个工具的专属上下文,并通过execute绑定步骤函数:
const tools = { getWeather: { description: 'Get the current weather for a city.', inputSchema: z.object({ city: z.string().describe('The city name') }), contextSchema: z.object({ defaultUnit: z.enum(['celsius', 'fahrenheit']) }), execute: getWeather, toModelOutput: ..., }, calculate: { description: 'Evaluate a math expression.', inputSchema: z.object({ expression: z.string() }), execute: calculate, }, deleteFile: { description: 'Delete a file from the filesystem.', inputSchema: z.object({ path: z.string() }), contextSchema: z.object({ rootDir: z.string() }), execute: deleteFileStep, needsApproval: true as const, // 高风险操作需要人工审批 }, };deleteFile工具同时演示了两件重要的事:
- 上下文约束:
execute内部检查input.path必须以toolsContext.deleteFile.rootDir(本示例为/tmp/workflow-sandbox)开头,否则直接抛错拒绝,防止模型删除任意路径——这是「沙箱化文件操作」的最小实现(agent-chat.ts); - 工具审批:
needsApproval: true as const标记该工具必须经过用户审批后才执行,审批流在 UI 侧由addToolApprovalResponse完成(详见第六节)。
四、toModelOutput:模型视角与 UI 视角分离
这是 README 用专门一节(「TestingtoModelOutput」)讲解的重点。WorkflowAgent与generateText、streamText、ToolLoopAgent一样,会尊重工具的可选toModelOutput钩子:该钩子决定「模型看到的工具结果」,与「应用/UI 收到的结果」相互独立。
getWeather的演示如下:
toModelOutput: ({ output }) => ({ type: 'text' as const, value: `${output.city}: ${output.temperature}°${output.unit === 'celsius' ? 'C' : 'F'}, ${output.condition}.`, }),即:UI 收到的仍是execute返回的完整对象{ city, temperature, unit, condition },而模型收到的是压缩成一行的人类可读摘要(如Boston: 22°C, sunny.)。这样既节省模型上下文 token,又避免把结构化 JSON 塞进提示词,同时 UI 还能渲染富信息卡片。
README 给出了可复现的验证步骤:
- 运行应用并提问:"What's the weather in Boston?";
- 浏览器中渲染的工具结果展示完整 JSON 对象(来自
execute的原始返回值); - 在 dev server 终端中,
onEnd回调会打印模型视角的工具结果,例如:
{ "type": "tool-result", "toolName": "getWeather", "output": { "type": "text", "value": "Boston: 22°C, sunny." } }为了对照,calculate工具没有定义toModelOutput,因此其模型视角输出保持默认的json序列化。
4.1 底层实现:逐结果转换工具输出
从源码结构看,WorkflowAgent与generateText/streamText在提示词组装上有个关键差异:它不是一次性把整段ModelMessage[]通过convertToLanguageModelPrompt整体转换,而是增量地、一次追加一个工具结果来拼装LanguageModelV4提示词。为此,@ai-sdk/workflow在 create-language-model-tool-result-output.ts 中提供了一个逐结果的等价转换 helper,其注释明确了三条处理流水线:
createToolModelOutput—— 应用tool.toModelOutput(或 text/json/error 兜底);downloadAssets—— 对content类型的输出下载其中的文件/图片资源,把 URL 变成 provider 可消费的字节;mapToolResultOutput—— 把 AI 层的ToolResultOutput映射为 provider 层输出,并转换遗留文件类型。
这一实现细节解释了为什么toModelOutput返回{ type: 'text', value: ... }而非任意自定义对象——它最终要落为LanguageModelV4ToolResultOutput。仓库的单元测试 workflow-agent.test.ts 也验证了「本地工具结果使用toModelOutput的同时保留原始输出」,以及 provider 执行工具结果、审批后工具结果同样走该钩子的行为。
4.2 端到端可观测:onEnd
示例把toModelOutput变成可端到端观察的手段是onEnd回调——它从最终的 model messages 里筛出role === 'tool'的消息,扁平化其 content 并打印:
onEnd: ({ messages }) => { const modelFacingToolResults = messages .filter(message => message.role === 'tool') .flatMap(message => (Array.isArray(message.content) ? message.content : [])); console.log( '[WorkflowAgent] model-facing tool results (post toModelOutput):', JSON.stringify(modelFacingToolResults, null, 2), ); },这里的 tool-role 消息携带的正是模型视角的工具结果,而 UI 渲染的是原始工具输出——README 描述的两侧差异由此可被直接观测验证。
五、双上下文 API:runtimeContext与toolsContext
ChatRequestContext接口(agent-chat.ts)定义了路由层解析后传入工作流的每请求上下文,它被拆分为互补的两套 API:
runtimeContext(运行时上下文):共享的 Agent 状态,会流经prepareStep、生命周期回调和onEnd,但不会加入提示词;toolsContext(工具上下文):按工具隔离、经 schema 校验的状态。每个工具的execute只能看到属于自己那一份校验过的条目作为context。
示例演示了两者的典型用法:
runtimeContext: { tenantId: request.tenantId, requestId: request.requestId, plan: request.userPlan, }, toolsContext: { getWeather: { defaultUnit: request.preferredUnit }, deleteFile: { rootDir: request.fileRootDir }, },sensitive values like rootDir never leak across tools——源码注释强调,像rootDir这样的敏感值不会跨工具泄露(deleteFile有自己的rootDir,getWeather根本拿不到它)。
prepareStep则可在每步执行前读取runtimeContext并微调设置。示例做了一个「企业版更确定性」的演示:企业套餐把采样温度调到0.2,其他套餐不改动:
prepareStep: ({ runtimeContext }) => { if (runtimeContext.plan === 'enterprise') { return { temperature: 0.2 }; } return {}; },源码注释特别提醒:runtimeContext应视为不可变的——需要更新时应在prepareStep中返回新值。
在 app/api/chat/route.ts 中,路由层从请求头解析这些上下文(x-tenant-id、x-request-id、x-user-plan、x-unit),未提供时使用默认值(如tenant_demo、crypto.randomUUID()、free、celsius),fileRootDir固定为/tmp/workflow-sandbox,然后调用start(chat, [messages, requestContext])启动工作流。
六、流式传输、断线重连与工具审批
6.1 流式输出链路
README 强调「实时流式响应」通过getWritable()与createUIMessageStreamResponse实现。完整的链路是:
agent.stream({ ..., writable: getWritable<ModelCallStreamPart>() })—— WorkflowAgent 把模型调用流部件(ModelCallStreamPart)写入由 Workflow DevKit 提供的可写流;- 路由层把
start()返回的run.readable通过createModelCallToUIChunkTransform()转换为 UI 消息流:const run = await start(chat, [messages, requestContext]); return createUIMessageStreamResponse({ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()), headers: { 'x-workflow-run-id': run.runId, 'x-request-id': requestContext.requestId }, }); - 客户端
WorkflowChatTransport(app/page.tsx)配合useChat消费该流,maxConsecutiveErrors: 5提供连续错误上限保护。
6.2 断线重连:getRun
「Workflow 运行在重启后依然存活且可被重新连接」的关键路由是 app/api/chat/[runId]/stream/route.ts:
const run = await getRun(runId); const readable = run .getReadable({ startIndex: 0 }) .pipeThrough(createModelCallToUIChunkTransform({ uiStartIndex: startIndex })); return createUIMessageStreamResponse({ stream: readable, headers: { 'x-workflow-run-id': runId } });它通过workflow/api的getRun(runId)按运行 ID 取回持久化的运行,用getReadable({ startIndex: 0 })从存储中重放已产生的流部件,并支持startIndex查询参数从指定位置续传(校验其为非负安全整数,否则返回 400)。这正是「可恢复 + 可重连」的落地实现:客户端拿到首次响应头里的x-workflow-run-id后,可以随时重新连接到同一运行。
6.3 工具审批 UI
由于deleteFile声明了needsApproval: true,聊天 UI 需要处理审批状态机。app/page.tsx 中:
useChat配置sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,确保带审批响应的消息在合适时机自动发送;- 对
approval-requested状态的 tool part,渲染琥珀色审批卡片,展示工具名、输入 JSON,以及Approve / Deny两个按钮,分别调用addToolApprovalResponse({ id, approved: true })与addToolApprovalResponse({ id, approved: false, reason: 'User denied the operation.' }); - 对
approval-responded状态,展示绿色「approved — executing...」或红色「denied」反馈;普通工具结果则渲染output-available状态的完整 JSON。
结合 workflow-agent.test.ts 的测试用例,可以确认:审批通过后的工具结果同样走toModelOutput钩子,同时保留原始流输出。
七、三个 E2E 测试台与异步视频工作流
README 描述了三个可独立访问的页面,它们分别对应 WorkflowAgent 三个进阶能力的确定性验证。
7.1/telemetry:Telemetry E2E 测试台
打开 http://localhost:3000/telemetry 可运行确定性的 WorkflowAgent 遥测场景。测试台会记录稳定的 AI SDK 遥测集成事件,覆盖:生命周期回调、模型调用、chunk、工具执行、上下文过滤、审批恢复、错误处理、重连行为。
实现位于 workflow/telemetry-agent.ts,其要点:
- 使用
mockSequenceModel(workflow/mock-model.ts)按场景脚本化输出(tool-call、text、error三种描述符),保证确定性;该 mock 模型实现了WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE静态方法,因此是可持久化/反序列化的模型——这正是「运行可恢复」对模型对象本身的要求; - 通过
TelemetryOptions配置functionId、includeRuntimeContext(仅挑选telemetryRunId/requestId/tenantId,排除secretToken等敏感字段)和includeToolsContext(同样按工具白名单过滤)——这就是 README 提到的「上下文过滤」; - 注册两类遥测来源:
integrations(自研的createTelemetryIntegration+ DevTools 桥接devToolsTelemetry)以及 agent 构造器/stream上的显式回调(experimental_onStart、experimental_onStepStart、onToolExecutionStart、onToolExecutionEnd、onEnd、onError); - 场景由
getResponses(scenario)决定:approval(deleteFile 审批)、tool-error(failTool 抛错)、model-error(模型流抛错)、默认(天气 + 计算器双工具调用)。
7.2/sandbox:Sandbox E2E 测试台
打开 http://localhost:3000/sandbox 可运行确定性的experimental_sandbox场景,验证「传给agent.stream的沙箱会话在工具执行期间可用」。
workflow/sandbox-agent.ts 的实现要点:
createSandbox构造一个最小Experimental_SandboxSession:仅实现run(返回{ exitCode: 0, stdout: 'sandbox:<label>:<command>' }),其余文件读写、spawn 方法抛出「Only sandbox.run is implemented」;- 工具
runSandboxCommand通过tool({...})的execute第二参数解构出experimental_sandbox,为空则抛Sandbox is not available,否则调用experimental_sandbox.run({ command, workingDirectory: '/workspace', env: { SCENARIO: 'sandbox' } })并连同sandboxDescription一起返回; agent.stream({ ..., experimental_sandbox: streamSandbox })把会话注入执行环境;- 同文件还导出了
toUIMessageStreamhelper:readable.pipeThrough(createModelCallToUIChunkTransform({ uiStartIndex })),把模型调用流部件转换为 UI chunk 流,供各测试台复用。
7.3/async-apis:基于 webhook 的异步视频工作流
这是示例中「含金量」最高的部分。打开 http://localhost:3000/async-apis 并提交一个 GitHub 仓库 URL,工作流会:
- 通过 GitHub GraphQL API 查询该仓库最近 30 天合并的 pull request(分页上限
MAX_GITHUB_SEARCH_PAGES = 10,每页 100 条); - 统计合并者的合并次数,排序后取前 3 位维护者(
MAX_MAINTAINERS = 3,同分按 login 字典序); - 逐个下载头像(限制 HTTPS + 受信域名,如
github.com、avatars.githubusercontent.com、*.githubusercontent.com,且大小不超过 10 MB); - 用 FAL 的
luma-dream-machine/ray-2/image-to-video模型为每位维护者生成 5 秒、9:16 竖屏(540p)的图生视频。
全程通过writeProgress(内部getWritable<AsyncApisProgressUpdate>().getWriter())向浏览器实时推送status/maintainers/avatar/video/complete/error各阶段进度。
webhook 方案的细节(README 专段说明):工作流把新的webhook选项传给experimental_generateVideo,用 Workflow DevKit 的createWebhook()给 FAL 一个持久化的回调 URL。工作流会挂起(suspend)直到 FAL 回调该 URL,随后检查已完成的任务并把结果流式推回页面——全程无需轮询。实现要点:
using webhook = useWebhook ? createWebhook() : undefined; await generateVideo({ model: createDurableFalVideoModel({ useWebhook }), ... maxRetries: 0, poll: { delay: useWebhook ? waitWithoutSchedulingTimeout : sleep, // webhook 模式放弃竞争的 sleep timeoutMs: FAL_WEBHOOK_TIMEOUT_MS, // 10 分钟 }, webhook: webhook == null ? undefined : async () => ({ url: webhook.url, received: webhook.then(request => ({ body: null, headers: Object.fromEntries(request.headers) })), }), });几个值得注意的工程细节:
createDurableFalVideoModel把 FAL 模型的doStart/doStatus包装成'use step'的持久化步骤,并仅在启用 webhook 时附加handleWebhookOption,把webhookFactory返回的{ url, received }转成 provider 需要的{ webhookUrl, received };waitWithoutSchedulingTimeout返回一个永不 resolve 的 Promise——注释解释:可持久化的 webhook 自己拥有等待权,若同时调度一个 Workflow sleep,会在 webhook 赢得竞态时留下未提交(uncommitted)的 sleep;- 生成完成后从
result.providerMetadata.fal.videos[0].url提取视频地址,并格式化result.warnings。
本地运行的限制与应对(README 明确说明):FAL不能向私有回环地址(loopback)回调 webhook。因此:
- 当示例跑在普通
localhost上时,自动回退为异步 start/status API + 持久化轮询(poll.delay用sleep),同样能完成任务; - 若要在本地走通 webhook 路径,需要部署到 Vercel,或把
WORKFLOW_LOCAL_BASE_URL设置为一个能转发到本地服务器的公共 HTTPS 地址; canReceiveFalWebhook()的判定逻辑(async-apis.ts):未设置WORKFLOW_LOCAL_BASE_URL时看VERCEL === '1';设置了则校验协议为https:且主机名不是localhost/127.0.0.1/::1/*.localhost,否则视为不可接收 webhook。
八、总结:这套示例教会你的架构模式
综合 README 与源码,examples/next-workflow实际演示了一套可复用的「生产级 Agent 工作流」模式:
| 关注点 | 方案 | 代码位置 |
|---|---|---|
| 持久化执行 | 'use workflow'入口 +'use step'工具步骤 | workflow/agent-chat.ts |
| 可恢复运行 | start/getRun+x-workflow-run-id重连 | app/api/chat/route.ts、app/api/chat/[runId]/stream/route.ts |
| 模型/UI 输出解耦 | 工具toModelOutput钩子 | create-language-model-tool-result-output.ts |
| 上下文隔离 | runtimeContext(共享、不进提示词) +toolsContext(按工具校验隔离) | agent-chat.ts |
| 高风险操作 | needsApproval+ UI 审批流 | app/page.tsx |
| 可观测性 | telemetry集成 + 生命周期回调 | workflow/telemetry-agent.ts |
| 沙箱化工具执行 | experimental_sandbox注入agent.stream | workflow/sandbox-agent.ts |
| 异步长任务 | createWebhook挂起等待回调,回退轮询 | workflow/async-apis.ts |
| 确定性测试 | 可序列化 mock 模型 + 脚本化响应 | workflow/mock-model.ts |
如果你正在为 AI 应用设计「多步骤、长耗时、需要审批、必须抗崩溃」的 Agent 后端,这套示例是直接从仓库里可以照搬的完整蓝本:先跑通pnpm dev看三个测试台的效果,再对照本文各小节逐步拆解workflow/目录下的实现,最后把start/getRun的持久化路由接入你自己的 Next.js 项目即可。
【免费下载链接】aiThe AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考