TanStack AI 1.0 与 AI SDK 7:机制、能力与工程选型
AI 参与说明(Agent:Codex):本文通过 Context7 定位两者文档,再对照官方原文、迁移指南和 npm 版本元数据核验;离线运行了两套工具调用示例。资料核验于 2026-10-23。执行入口为 Codex Desktop;完整模型标识与 reasoning effort 未取得任务级运行记录。
TanStack AI 与 Vercel AI SDK 都能完成模型调用、工具执行、流式聊天和前端状态管理。到了 TanStack AI 1.0 与 AI SDK 7,单凭“谁有工具调用、谁支持多个模型”已经很难选型。真正影响工程组织的是:模型能力怎样进入类型系统,工具契约怎样跨越前后端,流里传什么事件,以及中断后由谁保存和恢复执行。
TanStack AI 把共享工具声明、逐模型类型和 AG-UI 贯穿到同一条开发链路;AI SDK 以统一模型接口连接 Core、UI 与 Harnesses,并提供独立的 WorkflowAgent 集成。已有系统应先比较迁移涉及的边界;新系统则应根据目标模型、前端框架和持久执行需求做最小原型,而不只看所属品牌。
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Provider / Adapter | 提供方 / 适配器 | 把统一请求转换成模型厂商接口,并把返回结果转换回来 |
| Agent Loop | 智能体循环 | 模型提出工具请求、程序执行、结果送回模型的重复过程 |
| Tool | 工具 | 模型可以申请调用、由程序或模型厂商实际执行的能力 |
| Transport | 传输层 | 在前后端之间发送请求、接收事件的连接实现 |
| AG-UI | 保留原名(协议名称) | 描述 Agent 执行、消息、工具和状态变化的事件协议 |
| Generative UI | 生成式界面 | 由模型参与选择、组合或生成当前响应要呈现的界面 |
| UIMessage | 界面消息 | 为文本、工具进度、来源和其他展示内容组织的消息结构 |
| Interrupt | 中断 | 暂停当前执行,并等待用户或外部系统提供继续所需的输入 |
| Resumable Stream | 可恢复流 | 断线后从已记录的位置继续接收事件的机制 |
| Durable Execution | 持久执行 | 将执行进度保存到进程之外,以支持重启后的恢复 |
| Harness | 智能体运行框架 | 管理工具、上下文和执行策略的现成 Agent 运行系统 |
版本基线:不能把旧版本接口混在一起
本文锁定 @tanstack/ai@1.0.0 与 ai@7.0.150。这是核验日 npm latest 返回的版本;各自的 Provider、UI、MCP 等包有独立版本,安装时还要满足它们的依赖约束。TanStack 文档以官方仓库提交 5651e07e3181745984155ad88b65b86b8528844e 为核对基线,AI SDK 同时对照 7.0 迁移指南。
| 版本变化 | 写新代码时的影响 |
|---|---|
| TanStack AI 0.x → 1.0 | 前后端都使用 AG-UI;客户端和服务端需要同步升级;审批改用 Interrupt / resume 流程 |
| TanStack AI 1.0 的聊天身份 | 使用 threadId、runId、parentRunId,不要照搬旧 conversationId 示例 |
| AI SDK 6 → 7 | system → instructions,stepCountIs → isStepCount,fullStream → stream |
| AI SDK 7 的普通 Core / ToolLoopAgent 工具审批 | 优先用 toolApproval;这条路径的旧 needsApproval 已弃用。WorkflowAgent 仍有自己的 needsApproval 用法 |
| AI SDK 7 运行要求 | Node.js 路径要求 Node 22+,包为 ESM-only;部署到其他运行时还要核对具体集成 |
Context7 的检索结果可能包含历史迁移页和旧代码,所以需要继续打开原始来源核对版本。本文没有把检索片段中的旧审批 API 直接当作 1.0 的推荐用法。
来源:TanStack 1.0 migration、AI SDK 7 migration、TanStack npm 元数据、AI SDK npm 元数据。
先看分层:两个 useChat 背后各有什么
两者都把模型接入、执行控制和界面状态分层。安装一个 React Hook,并不会同时得到数据库、后台任务和部署平台。
flowchart TB
subgraph T["TanStack AI"]
direction TB
T1["框架绑定<br/>useChat"] --> T2["ChatClient<br/>消息与工具状态"]
T2 --> T3["Transport<br/>AG-UI 请求与事件"]
T3 --> T4["chat<br/>Agent Loop 与 Tools"]
T4 --> T5["Provider Adapter<br/>模型厂商接口"]
end
subgraph V["AI SDK"]
direction TB
V1["UI 框架绑定<br/>useChat"] --> V2["Chat<br/>UIMessage 状态"]
V2 --> V3["ChatTransport<br/>UI Message Stream"]
V3 --> V4["Core / Agent<br/>生成与工具循环"]
V4 --> V5["Provider<br/>Language Model V4"]
end
图中是常见远程聊天链路。两者也支持进程内连接,不必每一层都经 HTTP。
TanStack AI 的边界
@tanstack/ai 提供 chat()、工具和循环控制;Provider 包处理厂商差异;@tanstack/ai-client 是不依赖具体视图框架的 ChatClient,框架包再把状态接入 React、Vue 等组件。当前 ai-client 依赖 TanStack Store,不能据此把它视为 TanStack Query 的扩展。
TanStack AI does not require TanStack Query or TanStack Start. 使用 Start 的好处在于路由和服务端入口容易组合;使用其他 Web 框架时,同样可以接入 Request / Response 或自定义 Transport。它的 React UI 工具提供组合能力,仍需应用决定实际组件、布局和样式。
AI SDK 的边界
AI SDK 当前分为 Core、UI 和 Harnesses。Core 通过 Language Model V4 specification 连接 Provider;UI 管消息与客户端交互;Harnesses 把现成的 Coding Agent runtime 接入统一的调用和流式界面。ToolLoopAgent 属于自己组织模型与工具的路径,HarnessAgent 则复用现成 Harness。
AI SDK can call providers directly without Vercel hosting or AI Gateway. 显式创建 Provider 实例即可连接模型服务;字符串模型 ID 经 Gateway 解析是另一种接入方式。TanStack 也有 Vercel AI Gateway Adapter,因此“用哪个 SDK”“是否用 Gateway”“在哪里托管”需要分别决定。
来源:TanStack Overview、ai-client 依赖、AI SDK Introduction、Providers and Models。
模型抽象与类型安全:统一到哪一层
TanStack 使用 openaiText(model) 等 Adapter 工厂,把不同活动拆成明确入口;chat({ adapter, modelOptions }) 通过 Adapter 获得具体模型信息。其突出设计是保留 model literal type,再根据模型映射收窄 Provider options 和可接受的输入模态。共享配置也可以用 createChatOptions 等 helper 保留推断。
这使“这个模型允许哪些选项”尽量在编辑器里暴露出来。但模型目录类型需要维护:厂商上线新模型、修改参数或账户没有相应权限时,编译通过仍不等于远程请求必然成功。createChatOptions 本身也是保留类型的 identity function,不执行运行时验证。
AI SDK 的核心边界是 Provider specification。generateText、streamText 和 Agent 接受统一模型对象;厂商特有参数通过 providerOptions 等接口保留。它同样能从 schema 推断 Tool 参数、从 Agent 推断 UIMessage,并提供 Provider 相关类型。两者的差异在于类型组织的重心,不能概括成“一个有类型、一个没有类型”。
| 问题 | TanStack AI | AI SDK |
|---|---|---|
| 如何选择模型 | 具体活动的 Adapter 工厂 | Provider 返回符合统一 specification 的模型对象 |
| 厂商特有选项 | 模型映射收窄 modelOptions | 统一调用保留 providerOptions 扩展 |
| 工具参数类型 | toolDefinition 的 schema 与绑定实现共同传播 | tool 的 inputSchema 与工具集共同传播 |
| 前端消息类型 | 工具声明与 InferChatMessages 等类型联动 | InferAgentUIMessage 等类型联动 |
| 运行时可信度 | 仍需要 schema 验证、远程错误处理和业务鉴权 | 同样需要,TypeScript 不提供网络边界信任 |
如果主要约束是某几个模型的参数准确性,TanStack 的逐模型类型值得单独试用;如果需要接入已有 Provider 或自行实现统一模型接口,AI SDK 的 specification 是重要评估点。两者都应实际运行目标模型与工具组合,不能从“支持 OpenAI-compatible”推导出所有厂商扩展都可用。
来源:TanStack Per-model Type Safety、Typed Options、AI SDK Tools。
Agent Loop:一次请求为什么会调用模型多次
A tool call is a request from the model. The application or provider executes the tool and returns its result. 模型生成工具名称与参数,并不意味着模型已经读取数据库或修改页面。
对于在服务端本地执行的 Tool,两者的基本链路相同:
%%{init: {"sequence": {"actorMargin": 24, "width": 120, "messageMargin": 28}}}%%
sequenceDiagram
participant U as 用户界面
participant L as Agent Loop
participant M as 模型 Provider
participant T as Server Tool
U->>L: 发送问题和消息
L->>M: 第一次模型调用
M-->>L: Tool 名称与参数
L-->>U: Tool 输入与执行状态
L->>T: 验证参数后执行
T-->>L: Tool result
L-->>U: Tool 结果状态
L->>M: 第二次模型调用<br/>包含工具结果
loop 回答生成期间
M-->>L: 文本增量
L-->>U: 文本增量
end
M-->>L: 完成
L-->>U: 完成
一个模型回合可以提出多个 Tool 请求。因此限制模型回合、限制工具执行次数、限制 Token 和限制费用是不同的控制项。
| 默认行为 | TanStack AI 1.0 | AI SDK 7 |
|---|---|---|
| 常用入口 | chat() | generateText() / streamText() / ToolLoopAgent |
| 普通函数的循环 | chat() 内置循环,默认策略包含 maxIterations(5) 和对 length 的停止处理 | generateText / streamText 默认一轮;连续执行需配置 stopWhen |
| Agent 的默认上限 | 通过 agentLoopStrategy 控制 | ToolLoopAgent 默认 isStepCount(20) |
| 每轮动态控制 | 循环策略及 Middleware 等 | prepareStep、stopWhen 等 |
| 子 Agent | defineAgent()、subagents | 可组合 Agent;现成 Harness 另走 HarnessAgent |
一个常见排错场景是:AI SDK 工具已经执行,却没有最终自然语言回答。先检查是否只允许一轮,导致没有第二次模型生成。反过来,使用 TanStack chat() 或 AI SDK ToolLoopAgent 时,要主动设置符合业务预算的停止条件,不能只依赖默认值。
来源:TanStack Agentic Cycle、AI SDK Agents Overview、Building Agents。
Tool:同构声明、执行位置与审批
TanStack 的共享声明怎样工作
toolDefinition({ name, description, inputSchema, outputSchema }) 声明工具契约,再用 .server(fn) 或 .client(fn) 绑定执行位置。共享文件可以被前后端同时导入,数据库访问和密钥实现留在服务端文件中。
const getWeatherDef = toolDefinition({
name: 'get_weather',
description: 'Get the weather for a city',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ city: z.string(), sky: z.string() }),
})
const getWeather = getWeatherDef.server(async ({ city }) => ({
city,
sky: 'sunny',
}))
这是从后文完整示例抽出的契约部分。.client() 用于读取浏览器能力或改变本地界面等场景;它不会把服务端函数序列化后搬进浏览器。服务端向模型暴露 Client Tool 的声明,客户端注册实际实现,执行结果再经后续请求送回模型。同一工具集的名称必须唯一,不能把同名 server/client 实现同时当成两个 Tool 注册。
AI SDK 同样有 Client Tool
AI SDK 的 tool({ inputSchema, execute }) 可以定义服务端自动执行工具;execute 是可选的。UI 层可以通过 onToolCall 执行客户端逻辑,再用 addToolOutput 回填结果。配合 sendAutomaticallyWhen 和 lastAssistantMessageIsCompleteWithToolCalls,结果齐备后可以自动继续请求。
因此,实际差别是共享声明如何组织、类型如何传到 UI、结果如何接入各自消息状态机。迁移时需要重写这些接口,不能仅替换 import。
审批会结束当前调用,再由新输入继续
| 环节 | TanStack AI 1.0 | AI SDK 7 普通 Core / ToolLoopAgent |
|---|---|---|
| 声明审批 | Tool 的 needsApproval: true | toolApproval,可根据参数返回审批决策 |
| 等待用户 | RUN_FINISHED 携带 Interrupt | 返回 tool approval request,当前普通调用结束 |
| UI 提交决定 | interrupt.resolveInterrupt(true / false) | addToolApprovalResponse |
| 服务端继续 | 传回 parentRunId 与 resume,形成新 run | 把 approval response 纳入消息,再次生成 |
TanStack 的 1.0 路由要把 chatParamsFromRequest(request) 得到的恢复字段继续交给 chat();只转发 messages 会丢掉恢复语义。AI SDK 普通生成调用也需要应用提交后续输入,不能把审批理解成某个 HTTP 请求一直等待用户点击。
Approval is not authorization. 用户在 UI 点了同意,服务端仍要校验身份、资源权限和最终参数。Provider 自行执行的工具还属于另一条执行边界;例如 AI SDK 的本地 toolApproval 不负责拦截 Provider-executed tools。
来源:TanStack Client Tools、Tool Approval、AI SDK Chatbot Tool Usage、Tools and Tool Calling。
Streaming:SSE 相同,事件语义不同
TanStack AI 1.0 前后端使用 AG-UI。Client 发送 RunAgentInput,服务端输出 StreamChunk;事件包括 RUN_STARTED、TEXT_MESSAGE_CONTENT、TOOL_CALL_ARGS、TOOL_CALL_RESULT、RUN_FINISHED 等。TanStack 扩展信息放在 metadata.tanstack。普通服务端工具的多个模型回合可以属于同一个 run;等待审批后恢复,则是有关联的新 run。
AI SDK 的富 UI 流使用 UI Message Stream protocol,通过 SSE 承载 JSON parts。事件包括 text-start、text-delta、text-end、工具输入与输出、reasoning、source 和自定义 data 等;兼容后端需要遵守协议,并设置 x-vercel-ai-ui-message-stream: v1。它也提供纯文本协议,但纯文本不足以表达完整 Tool UI。
下面只展示文本增量事件,省略其余必要事件;这不是一条完整可重放的流:
TanStack / AG-UI:
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"m1","delta":"你好"}
AI SDK UI Message Stream:
data: {"type":"text-delta","id":"m1","delta":"你好"}
SSE defines the transport framing; the event protocol defines what each message means. 两边都用 SSE,不能让 AI SDK useChat 直接消费 TanStack 原生流,也不能反向直接互换。自定义兼容层还要转换 Tool 生命周期、错误、消息 ID、结束标记和恢复信息,单改文本事件名称不够。
Transport 则是另一个维度:TanStack 提供 SSE、NDJSON、WebSocket、直接函数和进程内 stream 等连接;AI SDK 提供默认 HTTP DefaultChatTransport、WebSocketChatTransport、DirectChatTransport 及自定义 Transport。两者都能连接非 JavaScript 后端,前提是该后端实现对应协议和请求契约。
来源:TanStack Stream Events、Transports、AI SDK Stream Protocol、Transport。
AG-UI 与 Generative UI:连接协议与界面生成方式
AG-UI 的全称是 Agent–User Interaction Protocol,是连接 Agent 后端与用户界面的开放、双向、基于事件的协议。它关心一次运行何时开始、文本如何增量到达、工具运行到哪一步、状态怎样同步、用户的审批或其他输入如何回到 Agent。标准还定义了 STATE_SNAPSHOT、STATE_DELTA 等状态事件;具体集成支持哪些事件和扩展,需要分别核对。
例如,一个“查询天气”的请求不只有最终答案:客户端可能先收到运行开始,再收到工具名称和参数、工具结果,最后收到回答文本和运行结束。AG-UI 让前端能依据这些事件更新进度、展示结果,并把后续交互送回后端。TanStack AI 采用这套协议来组织连接;AG-UI 本身是独立协议,并不专属于 TanStack。
AG-UI defines the runtime connection between an agent and an application. Generative UI describes how a model participates in choosing or producing the interface. 两者可以组合,但解决的是不同问题。
| 对比项 | AG-UI | Generative UI |
|---|---|---|
| 是什么 | 交互协议 | 一类界面生成与交付方式,包含多种实现和规格 |
| 主要回答 | 后端发生了什么,前端如何接收并继续交互 | 这次回答应该显示什么组件、内容或布局 |
| 典型产物 | 文本、工具、状态、运行生命周期等事件 | 模型选择的组件、组件属性、组件树或其他 UI 描述 |
| 是否定义页面布局 | 不要求使用某一种布局或组件语言 | 具体方案可以定义组件与组合规则 |
| 与另一方的关系 | 可以承载 Generative UI 的数据和交互 | 可以经 AG-UI、AI SDK 流协议或自定义接口交付 |
以天气卡片为例,可以按模型对界面的控制范围分两种情况:
- 预先编写组件,由模型选择何时使用。 开发者实现
WeatherCard,模型选择调用天气工具,前端把工具状态与结果映射到卡片。AG-UI 文档把这类受应用控制的组件呈现也纳入 static Generative UI;它不要求模型生成 JSX 或整个页面。 - 由模型声明组件组合。 模型进一步提出“温度卡片 + 趋势图 + 城市列表”的结构;应用验证组件名称、属性和组合规则后渲染。这需要额外的 Generative UI 规格或 Renderer,例如 A2UI 或 OpenUI 所负责的界面描述与呈现层。AG-UI 可以承载相关数据,但不会自动替应用完成组件注册、校验和渲染。
如果页面只是用事件更新一个开发者固定编排的聊天窗口,也可以完整使用 AG-UI;仅有流式输出并不足以说明模型在生成界面。反过来,AI SDK 的 Tool UI 或自定义 data 流也能支持 Generative UI,不需要先换成 AG-UI。选型时,应分别决定“怎样连接 Agent”和“允许模型怎样影响界面”。
来源:AG-UI Overview、AG-UI Events、AG-UI 与 Generative UI Specs。后者明确区分 AG-UI 交互协议与 Generative UI 规格;static / declarative 的区分见 Overview。
前端状态:messages 不只是字符串数组
两者的 UIMessage 都由 parts 组成,用于保留“正在输入 Tool 参数”“等待审批”“已取得结果”等中间状态。前端应根据 part 类型和状态渲染组件,而不只是取最后一条消息的文本。
TanStack 的 Tool part 会经过 input-streaming、input-complete、approval-requested、complete / error 等状态;ChatClient 汇总事件,框架 Hook 订阅其状态。AI SDK 同样维护工具输入、输出、审批和错误状态,并支持自定义 data 与消息 metadata。
两边都把 UI 数据和模型输入区分开来。以 AI SDK 为例,常见服务端入口用 convertToModelMessages 将 UIMessage 转换为模型消息,streamText 产生 Core stream,再用 toUIMessageStream 和 createUIMessageStreamResponse 输出 UI 协议。TanStack 则由 chatParamsFromRequest 解析输入,chat 产生事件,toServerSentEventsResponse 将事件写入 Response。
这一区分会影响数据库设计:只保存拼接后的文本,会丢失 Tool 调用、输出、审批关系及其他 UI 信息;直接把所有界面 metadata 塞给模型,又可能增加无关上下文。应按选定 SDK 的消息结构保存可恢复的对话,并在模型边界转换和筛选。
来源:TanStack Tool Architecture、AI SDK Chatbot、AI SDK 7 migration。
能力广度:两者已大量重叠,检查具体组合更有价值
下表的“提供”表示有官方 API、模块或集成路径,不表示每个 Provider / 模型都支持,也不表示实验性接口与稳定接口具有同样承诺。
| 能力 | TanStack AI 1.0 | AI SDK 7 |
|---|---|---|
| 文本与多模态输入 | chat,输入模态随 Adapter / 模型而定 | generateText / streamText,随 Provider / 模型而定 |
| Structured Output | outputSchema,支持 partial / final 与 Tool 组合 | output: Output.object(...) 等,支持 Tool 组合与部分输出 |
| Embeddings / Rerank | 对应活动入口与 Adapter | embed / embedMany、rerank API |
| 图像、视频、语音、转写 | 有对应活动,部分扩展还涵盖实时音视频与其他生成类型 | 有对应 Core API;部分接口仍带实验性标记 |
| MCP | typed client/server、codegen、MCP Apps 等文档 | @ai-sdk/mcp 的 tools/resources/prompts 等,另有 Provider 原生 MCP |
| 上下文管理 | Memory、Compaction 等模块 | 消息处理、Middleware、上下文控制及相关 Agent 集成 |
| 现成 Coding Agent | Harness、Sandbox、Code Mode 等模块 | HarnessAgent 及 Claude Code、Codex、Pi 等 Harness 接入 |
| 测试 | fakeText(),可脚本化文本、工具和错误 | ai/test 的 MockLanguageModelV4 等测试替身 |
| 观测 | Middleware、OpenTelemetry、Devtools | Model Middleware、Telemetry、OpenTelemetry 集成 |
TanStack 的官方框架绑定已覆盖 React、Vue、Solid、Svelte、Preact、Angular 等;AI SDK UI 有 React、Vue、Svelte 等集成。实际项目应逐项检查目标框架包的 API、支持状态与版本,不能把一个框架示例推定为所有绑定都完全相同。
也不宜仅根据品牌判断 Provider 覆盖。AI SDK 有官方、社区和兼容协议等接入类别;TanStack 同样通过 Adapter 扩展。选型时应列出实际需要的组合,例如“目标模型 + 图像输入 + 两个工具 + JSON 输出 + 流式返回”,跑通这条链路比统计模型品牌数量更有用。
来源:TanStack Overview 与模块导航、AI SDK Introduction 与 Provider 列表、MCP Tools。
Structured Output 的共同边界
TanStack 使用 outputSchema;AI SDK 当前推荐 generateText / streamText 的 output 选项。两者都允许先执行工具、再生成最终结构化结果,因此循环预算必须留出最终生成的机会。
A partial structured output is not a fully validated final object. 例如 { "items": [ 后续内容还没有生成时,完整 schema 当然无法通过。UI 可以展示 partial;业务写入应等待 final,并执行需要的业务校验。schema 只能验证结构和显式约束,无法保证字段里的自然语言事实正确。
来源:TanStack Structured Outputs、AI SDK Generating Structured Data。
持久化:保存消息、恢复传输、恢复执行要分别比较
“刷新后能看历史”“网络断开后继续收字”“服务进程重启后继续执行”是三项不同能力。
| 层次 | 需要保存什么 | TanStack AI | AI SDK |
|---|---|---|---|
| 对话历史 | 消息、工具结果及关联身份 | @tanstack/ai-persistence、withPersistence、reconstructChat | 应用保存消息并恢复给 UI / Agent |
| Resumable Stream | 有序事件、已消费位置、活跃流身份 | Response 的 durability + Adapter,按 offset 重放 | UI resume + 应用端流存储、恢复 endpoint 和 resumable-stream 集成 |
| Durable Execution | 执行进度、暂停点、工具结果和调度状态 | Sandbox Durable Runs / takeover 为 sandboxed Coding Agent 提供专门路径;普通 chat() 的前两层不自动等于执行恢复 | 普通 ToolLoopAgent 在内存运行;@ai-sdk/workflow 的 WorkflowAgent 提供专门路径 |
TanStack 的 Resumable Stream 先记录事件,再向客户端交付;重连时可以从 offset 继续读,不必重新请求模型。内存 Adapter 便于开发,但进程消失后不会留下持久数据。重连还可能重新进入 HTTP handler,因此需要区分首次执行与重放请求,避免在 handler 外围重复触发副作用。
TanStack 还提供 Sandbox Durable Runs:浏览器断开时可以 detach,让 sandbox 内的 Agent 继续运行;后续请求通过 sandboxRunDriver 在新宿主接管,重放 journal 并继续写入交付日志。这条路径需要共享的 RunStore、SandboxInstanceStore、durability backend、分布式 LockStore、稳定的 runId 和定时 reaper。它面向 sandboxed Coding Agent,不能推定任意普通 chat() 调用或任意 Provider 都自动具有同样的恢复保证。
AI SDK 的 UI resume 指南要求应用维护消息、活跃流和恢复路由,示例使用 Redis 与 resumable-stream。WorkflowAgent 则是另一层机制:它依赖 Workflow 5,处理持久状态、工具 step 重试和跨暂停审批。工具的 execute 必须按文档用 'use step' 声明为持久步骤;普通函数仍可运行,但不因此获得相同的持久执行保证。它目前也仍使用 Tool 的 needsApproval,不能直接套用前文普通 Core / ToolLoopAgent 的 toolApproval 迁移规则。选择这条路径还要接受相应运行时、部署和故障语义。
Replaying stream events does not guarantee exactly-once tool execution. 即使运行系统会保存步骤,支付、发信等外部副作用仍应使用业务幂等键,并明确重试策略。threadId 或 runId 是身份,不会自动创建数据库或保证进程常驻。
来源:TanStack Persistence、Resumable Streams、Sandbox Takeover、AI SDK Resume Streams、WorkflowAgent。
两个离线示例:观察同一条工具链路
下面都使用固定模型响应和固定天气数据,运行时不调用模型服务或天气接口。它们验证 SDK 的循环控制与结果传递,不测试真实模型选择工具的能力,也不构成性能对比。两份代码已在 Node.js 24.19.0 运行通过;首次安装依赖需要访问 npm。
TanStack AI:声明、绑定,再消费流
在一个空的示例工程中安装依赖,将下列代码保存为 weather.ts。Node.js 24 可直接执行这里使用的可擦除 TypeScript 语法。
npm init -y
npm pkg set type=module
npm install --save-exact @tanstack/ai@1.0.0 zod@4.2.0
node weather.ts
import assert from 'node:assert/strict'
import { chat, toolDefinition, streamToText } from '@tanstack/ai'
import { fakeText } from '@tanstack/ai/testing'
import { z } from 'zod'
let executions = 0
const getWeather = toolDefinition({
name: 'get_weather',
description: 'Get the weather for a city',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ city: z.string(), sky: z.string() }),
}).server(async ({ city }) => {
executions += 1
console.log(`tool: ${city}`)
return { city, sky: 'sunny' }
})
const model = fakeText()
model.setResponses([
{ toolCalls: [{ name: 'get_weather', input: { city: 'Oslo' } }] },
({ request }) => {
const result = request.messages.find((message) => message.role === 'tool')
assert.ok(result, 'The second model call must receive a tool result')
assert.equal(typeof result.content, 'string')
assert.deepEqual(JSON.parse(result.content as string), { city: 'Oslo', sky: 'sunny' })
return { text: 'It is sunny in Oslo.' }
},
])
const { text } = await streamToText(chat({
adapter: model,
threadId: 'weather-demo',
messages: [{ role: 'user', content: 'How is the weather in Oslo?' }],
tools: [getWeather],
}))
assert.equal(executions, 1)
assert.equal(model.state.callCount, 2)
assert.equal(text, 'It is sunny in Oslo.')
console.log(`answer: ${text}`)
console.log(`model calls: ${model.state.callCount}; tool executions: ${executions}`)
预期输出:
tool: Oslo
answer: It is sunny in Oslo.
model calls: 2; tool executions: 1
chat() 返回的流需要被消费,执行才会推进;这里的 streamToText() 完成消费并收集文本。第二个脚本响应直接检查真实工具结果有没有进入模型请求,所以示例不会只靠“最后打印了正确字符串”就宣称循环正确。
AI SDK:统一模型接口与显式循环条件
另建一个空示例工程,将代码保存为 weather.mjs:
npm init -y
npm pkg set type=module
npm install --save-exact ai@7.0.150 zod@4.3.6
node weather.mjs
import assert from 'node:assert/strict';
import { generateText, isStepCount, tool } from 'ai';
import { MockLanguageModelV4 } from 'ai/test';
import { z } from 'zod';
// No model service or weather service is contacted in this demo.
const usage = {
inputTokens: { total: 1, noCache: 1, cacheRead: undefined, cacheWrite: undefined },
outputTokens: { total: 1, text: 1, reasoning: undefined },
};
let modelCalls = 0;
let toolCalls = 0;
const weather = { city: 'Shanghai', temperatureC: 23, condition: 'sunny' };
const model = new MockLanguageModelV4({
doGenerate: async ({ prompt }) => {
modelCalls++;
if (modelCalls === 1) {
console.log('model[1]: call getWeather({ city: "Shanghai" })');
return {
content: [{
type: 'tool-call',
toolCallId: 'weather-1',
toolName: 'getWeather',
input: JSON.stringify({ city: 'Shanghai' }),
}],
finishReason: { unified: 'tool-calls', raw: undefined },
usage,
warnings: [],
};
}
// Verify that the SDK inserted the actual executed tool result into turn 2.
const part = prompt
.filter(message => message.role === 'tool')
.flatMap(message => message.content)
.find(part => part.type === 'tool-result' && part.toolCallId === 'weather-1');
assert.deepEqual(part?.output, { type: 'json', value: weather });
console.log('model[2]: received getWeather result');
return {
content: [{ type: 'text', text: 'Shanghai: sunny, 23°C.' }],
finishReason: { unified: 'stop', raw: undefined },
usage,
warnings: [],
};
},
});
const result = await generateText({
model,
prompt: 'What is the weather in Shanghai?',
tools: {
getWeather: tool({
description: 'Get the weather for a city.',
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => {
assert.equal(city, 'Shanghai');
toolCalls++;
console.log('tool: executed getWeather once');
return weather;
},
}),
},
stopWhen: isStepCount(3),
});
assert.equal(modelCalls, 2);
assert.equal(toolCalls, 1);
assert.equal(result.steps.length, 2);
assert.equal(result.text, 'Shanghai: sunny, 23°C.');
console.log(`steps=${result.steps.length}, modelCalls=${modelCalls}, toolCalls=${toolCalls}`);
console.log(result.text);
预期输出:
model[1]: call getWeather({ city: "Shanghai" })
tool: executed getWeather once
model[2]: received getWeather result
steps=2, modelCalls=2, toolCalls=1
Shanghai: sunny, 23°C.
这里的 isStepCount(3) 是最多允许推进的条件,不会强迫模型生成三轮;第二轮正常结束后就返回。换成真实 Provider 时,业务 Tool 与循环控制可以保留,测试模型则替换为目标 Provider 的模型对象。
来源:TanStack Testing、AI SDK Testing。
工程选择:按已有资产和运行约束决定
以下是根据前文机制作出的工程建议,不是两家官方的性能或市场份额结论。
| 主要条件 | 优先评估 | 为什么 |
|---|---|---|
| 新项目希望共享工具声明,并重视逐模型参数推断 | TanStack AI | toolDefinition、执行位置绑定与模型类型组织直接对应这个目标 |
| 前后端已经以 AG-UI 作为契约 | TanStack AI | 可以沿原生协议验证对接,减少自建转换层 |
| 已有 AI SDK Provider、UIMessage 数据和 Tool UI | AI SDK | 保留现有协议、消息与组件通常能减少迁移范围 |
| 明确需要跨进程恢复 Agent,并可采用 Workflow 运行系统 | AI SDK 的 WorkflowAgent 路径 | 有专门集成,但要同时评估部署、重试和费用 |
| 需要复用现成 Coding Agent | 两者的具体 Harness 集成 | 重点比较目标 Harness、凭据方式、Sandbox 和部署条件 |
| 有多个前端框架 | 逐个检查框架包 | 不从 React 示例推定其他绑定的全部行为 |
| 只需要服务端生成文本或 JSON | 两者 Core 都可 | 无需先引入聊天 UI、MCP 或持久执行模块 |
放到 TanStack Start、Cloudflare 与 Convex 中看
在 TanStack Start 中,两个 SDK 都可以接入服务端路由;Router 负责页面与路由数据,Query 管理可缓存的 Server State,聊天 SDK 管流式消息与 Tool 状态。采用 TanStack AI 可以保持工具声明的组织风格,但不要求把聊天逐 Token 更新塞进 Query cache。
部署到 Cloudflare 时,应分别验证 Provider SDK 的运行时依赖、Response streaming、连接中断和后台执行生命周期。HTTP Response 支持流式输出,不足以证明关闭页面后任务一定继续。需要持久运行时,应把 Durable Objects、Workflows 或其他宿主能力与所选 SDK 的状态恢复接口逐项对齐。
Convex 已有 Agent transcript 或持久状态时,先确定谁保存权威对话、谁运行工具、谁向 UI 输出事件。把 SDK 的浏览器 messages 再保存一份,未必等于已经正确接入既有 Agent 状态;应避免出现两个都被当作权威的历史来源。这里是职责划分建议,具体能力见文末关联阅读。
迁移时需要一起改的部分
从一方迁到另一方,至少盘点五处:Provider 配置与参数、工具定义与审批、消息数据库结构、事件协议与 Transport、前端 parts 渲染。若已使用恢复流或持久执行,还要迁移活跃执行状态及其身份关联。
一个可控做法是先迁一条完整但简单的链路:单模型、一个 Server Tool、一个 Client Tool、一次审批、一次断线重连。记录各阶段的请求与事件,再验证最终消息和副作用次数。全部通过后再接入复杂多模态或现成 Harness;不要只验证纯文本聊天就扩大迁移范围。
关联阅读
- TanStack Query v5 基础:从 Server State 到 Query、Mutation 与缓存:区分查询缓存与聊天状态。
- Convex Agent Component 与 Cloudflare Agents SDK:关页后继续生成并恢复聊天:进一步比较持久执行、传输恢复与权威消息来源。
- OpenUI:概念、使用方式与 Generative UI 方案比较:理解事件协议与界面描述协议的不同职责。
继续查阅的一手资料
- TanStack AI 官方仓库固定版本:本文引用的文档快照;与浮动的 latest 页面区分。
- TanStack AG-UI compliance:请求、事件和恢复字段的迁移细节。
- TanStack OpenTelemetry:可记录哪些执行信息、如何控制内容采集。
- AI SDK HarnessAgent:现成 Harness 与普通模型 Provider 的接入边界。
- AI SDK Telemetry:当前 Telemetry 配置及 OpenTelemetry 集成。