使用 AI SDK WorkflowAgent 构建可恢复、可断点续跑的持久化聊天 Agent:next-workflow 示例全解析
2026/9/11 20:11:11 网站建设 项目流程

使用 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/workflowWorkflowAgent搭配 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/workflowWorkflowAgent,提供容错的 Agent 执行;
  • 工具调用:天气查询(getWeather)与计算器(calculate)作为持久化步骤实现;
  • toModelOutputgetWeather工具给模型发送紧凑的一行摘要,而 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/reactaiworkflow@5.0.0-beta.42以及zod@4.4.3,其中workflow包(Workflow DevKit)提供了getWritablestart/getRuncreateWebhook等运行时能力。


二、快速开始:安装、环境变量与启动

按照 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_KEYGITHUB_TOKEN需要对你提交的仓库有读取权限,公共仓库的公共访问权限即可满足;
  • 在「Async APIs」一节,README 还额外提及WORKFLOW_LOCAL_BASE_URL:当本地无法被 FAL 回拨 webhook 时,可把它设置为一个能转发到本地服务器的公共 HTTPS 地址来启用 webhook 路径(详见后文第七节)。

主聊天页 app/page.tsx 使用@ai-sdk/reactuseChat,并配置了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工具同时演示了两件重要的事:

  1. 上下文约束execute内部检查input.path必须以toolsContext.deleteFile.rootDir(本示例为/tmp/workflow-sandbox)开头,否则直接抛错拒绝,防止模型删除任意路径——这是「沙箱化文件操作」的最小实现(agent-chat.ts);
  2. 工具审批needsApproval: true as const标记该工具必须经过用户审批后才执行,审批流在 UI 侧由addToolApprovalResponse完成(详见第六节)。

四、toModelOutput:模型视角与 UI 视角分离

这是 README 用专门一节(「TestingtoModelOutput」)讲解的重点。WorkflowAgentgenerateTextstreamTextToolLoopAgent一样,会尊重工具的可选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 给出了可复现的验证步骤:

  1. 运行应用并提问:"What's the weather in Boston?"
  2. 浏览器中渲染的工具结果展示完整 JSON 对象(来自execute的原始返回值);
  3. 在 dev server 终端中,onEnd回调会打印模型视角的工具结果,例如:
{ "type": "tool-result", "toolName": "getWeather", "output": { "type": "text", "value": "Boston: 22°C, sunny." } }

为了对照,calculate工具没有定义toModelOutput,因此其模型视角输出保持默认的json序列化。

4.1 底层实现:逐结果转换工具输出

从源码结构看,WorkflowAgentgenerateText/streamText在提示词组装上有个关键差异:它不是一次性把整段ModelMessage[]通过convertToLanguageModelPrompt整体转换,而是增量地、一次追加一个工具结果来拼装LanguageModelV4提示词。为此,@ai-sdk/workflow在 create-language-model-tool-result-output.ts 中提供了一个逐结果的等价转换 helper,其注释明确了三条处理流水线:

  1. createToolModelOutput—— 应用tool.toModelOutput(或 text/json/error 兜底);
  2. downloadAssets—— 对content类型的输出下载其中的文件/图片资源,把 URL 变成 provider 可消费的字节;
  3. 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:runtimeContexttoolsContext

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有自己的rootDirgetWeather根本拿不到它)。

prepareStep则可在每步执行前读取runtimeContext并微调设置。示例做了一个「企业版更确定性」的演示:企业套餐把采样温度调到0.2,其他套餐不改动:

prepareStep: ({ runtimeContext }) => { if (runtimeContext.plan === 'enterprise') { return { temperature: 0.2 }; } return {}; },

源码注释特别提醒:runtimeContext应视为不可变的——需要更新时应在prepareStep中返回新值。

在 app/api/chat/route.ts 中,路由层从请求头解析这些上下文(x-tenant-idx-request-idx-user-planx-unit),未提供时使用默认值(如tenant_democrypto.randomUUID()freecelsius),fileRootDir固定为/tmp/workflow-sandbox,然后调用start(chat, [messages, requestContext])启动工作流。


六、流式传输、断线重连与工具审批

6.1 流式输出链路

README 强调「实时流式响应」通过getWritable()createUIMessageStreamResponse实现。完整的链路是:

  1. agent.stream({ ..., writable: getWritable<ModelCallStreamPart>() })—— WorkflowAgent 把模型调用流部件(ModelCallStreamPart)写入由 Workflow DevKit 提供的可写流;
  2. 路由层把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 }, });
  3. 客户端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/apigetRun(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-calltexterror三种描述符),保证确定性;该 mock 模型实现了WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE静态方法,因此是可持久化/反序列化的模型——这正是「运行可恢复」对模型对象本身的要求;
  • 通过TelemetryOptions配置functionIdincludeRuntimeContext(仅挑选telemetryRunId/requestId/tenantId排除secretToken等敏感字段)和includeToolsContext(同样按工具白名单过滤)——这就是 README 提到的「上下文过滤」;
  • 注册两类遥测来源:integrations(自研的createTelemetryIntegration+ DevTools 桥接devToolsTelemetry)以及 agent 构造器/stream上的显式回调(experimental_onStartexperimental_onStepStartonToolExecutionStartonToolExecutionEndonEndonError);
  • 场景由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,工作流会:

  1. 通过 GitHub GraphQL API 查询该仓库最近 30 天合并的 pull request(分页上限MAX_GITHUB_SEARCH_PAGES = 10,每页 100 条);
  2. 统计合并者的合并次数,排序后取前 3 位维护者(MAX_MAINTAINERS = 3,同分按 login 字典序);
  3. 逐个下载头像(限制 HTTPS + 受信域名,如github.comavatars.githubusercontent.com*.githubusercontent.com,且大小不超过 10 MB);
  4. 用 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.delaysleep),同样能完成任务;
  • 若要在本地走通 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.streamworkflow/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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询