atypica.AI 如何在不破坏千万级聊天记录的前提下迁移到 AI SDK v5
atypica.AI 是一个多 Agent 研究平台,AI Agent 协作完成用户研究。用户提出商业问题,系统编排多个专业 Agent——Study Agent 规划研究、Scout Agent 从社交媒体发现目标用户、Interview Agent 与 AI 人设进行自动化访谈、Report Agent 生成综合洞察。整个过程涉及 30+ AI 工具,并在 PostgreSQL 中存储大量聊天记录。
当 Vercel 发布 AI SDK v5,带来消息格式和工具 API 的破坏性变更时,我们面临一个挑战:迁移 200 个文件和 30+ 工具,同时保证 1000 万+ 的现有聊天记录正常访问。本文记录了我们 3 天的迁移历程。
最终数据:200 个文件变更,4,206 行新增,3,094 行删除,27 次提交。
核心破坏性变更:Message → UIMessage
v5 最根本的变化在于消息的结构方式。
v4:Message 包含 content + parts
在 v4 中,你可以使用 content(简单字符串)或 parts(结构化数组)。我们大部分代码使用 content,因为更简单。
v5:UIMessage 只有 parts
在 v5 中,parts 是唯一的数据源。content 字段从 parts 自动派生,是只读的。
迁移影响:所有访问 message.content 的地方都需要重构为遍历 message.parts。
第一天:理解范围
运行 Codemod
Codemod 处理了机械式替换:
- 类型名:
Message→UIMessage - 工具 API:
parameters→inputSchema - 添加
outputSchema占位符
但它留下了许多 FIXME 注释,因为无法理解语义变更。
第一个大问题:Content 访问
我们到处都有这样的代码:
v5 迁移方案:我们创建了一个从 parts 提取文本的工具函数:
这个模式出现在:
- 消息持久化(16 个文件)
- UI 组件(25 个文件)
- API 路由(15 个文件)
- 日志和分析(8 个文件)
工具调用结构变更
这是第二个重大破坏性变更。
v4 工具 part 结构:
v5 工具 part 结构:
关键变更:
- 扁平化结构(不再嵌套
toolInvocation对象) - 类型具体化:
"tool-${toolName}"而非通用的"tool-invocation" - 状态名:
"call"→"input-available","result"→"output-available" - 字段名:
args→input,result→output - 新增错误状态:
"output-error"带errorText
推理格式变更
字段名从 reasoning 改为 text。
第二天:千万级消息的向后兼容
数据库挑战
我们的 PostgreSQL 数据库以 v4 格式存储消息:
用户期望能够:
- 打开历史聊天对话
- 无缝继续对话
- 查看旧交互中的工具结果
我们无法迁移数据库,因为:
- 50+ 张表中有 1000 万+ 条消息
- 不可接受的停机时间
- 数据损坏风险太高
解决方案:转换层
创建 src/ai/v4.ts:
在读取时应用转换
效果:旧消息透明加载。新消息以 v5 格式存储。无需数据库迁移。
类型系统重组
我们有 5 个独立的聊天系统,使用不同的工具:
- Study:20+ 工具(推理、搜索、访谈、报告、社交媒体)
- Interview:2 个工具(endInterview、requestInteractionForm)
- Persona:1 个工具(endInterview)
- NewStudy:1 个工具(endInterview)
- Agents:3 个工具(thanks、hello、scout)
初始方案:一个全局 UIToolConfigs 类型。
问题:类型污染。Study 组件会自动补全 Interview 工具。工具类型不匹配导致运行时错误。
解决方案:领域专属的工具类型。
每个领域有自己的目录结构:
收益:每个领域内完全类型安全。无交叉污染。
第三天:UI 组件和 Model Messages
组件迁移模式
v5 之前,组件直接访问 content:
v5 之后,组件遍历 parts:
工具检测模式:
我们用这个模式迁移了 25+ 个 UI 组件。
UIMessage 与 ModelMessage 的区别
还有一个重要的细节:发送给模型的消息有不同的格式。
UIMessage — 来自模型,用于 UI:
UserModelMessage — 发给模型:
这只在为 streamText 或 generateObject 构造消息时才重要:
我们在 30+ 个工具定义和产物生成器中修复了这个问题。
useChat Hook 变更
Hook 方法重命名:
append→sendMessagereload→regenerateinitialMessages→messages
Transport 配置:
experimental_prepareRequestBody→prepareSendMessagesRequest
文件附件
字段重命名:
name→filenamecontentType→mediaTypeexperimental_attachments→parts数组的一部分
流式消息持久化
关键洞察:在 v5 中,只需检查 parts.length,无需检查 content。
v5 中 content 字段从 parts 派生,因此检查它是多余的。
关键陷阱
1. v5 中消息 Content 是只读的
2. 空 Parts 与空 Content
3. 工具状态名变更
别忘了更新所有状态检查。
4. 可选的工具调用名
在边缘情况(中止的流、错误)中,toolCall.toolName 可能是 undefined:
5. 流事件字段名
6. 消息 ID 处理
客户端消息可能没有 ID:
迁移清单
阶段 1:依赖
- 更新
ai到 v5,zod到 v4 - 运行 codemod:
npx @ai-sdk/codemod@latest upgrade - 修复
Message→UIMessage重命名导致的 TypeScript 错误
阶段 2:Content 访问
- 查找所有
message.content访问 - 替换为 parts 遍历或工具函数
- 更新标题生成、日志记录、分析
阶段 3:工具 Parts
- 更新工具检测:
"tool-invocation"→part.type.startsWith("tool-") - 更改字段访问:
args/result→input/output - 更新状态检查:
"call"/"result"→"input-available"/"output-available" - 添加
"output-error"处理
阶段 4:向后兼容
- 创建 v4 → v5 转换工具
- 在从数据库加载时应用转换
- 彻底测试旧聊天记录
阶段 5:UI 组件
- 迁移所有组件为遍历
parts - 更新工具渲染逻辑
- 更新文件附件组件
阶段 6:工具定义
- 将
parameters改为inputSchema - 添加
outputSchema - 为模型消息引入
UserModelMessage - 调用
streamText/generateObject时使用content(非parts)
阶段 7:React Hooks
- 更新
useChat:append→sendMessage,reload→regenerate - 迁移
experimental_prepareRequestBody→prepareSendMessagesRequest
阶段 8:边缘情况
- 处理缺失的消息 ID
- 处理 undefined 的
toolCall.toolName - 更新流事件处理器
- 移除
content.trim()检查
关键洞察
1. Content 已死,Parts 万岁
从 content 作为数据源到 parts 作为数据源的转变是根本性的。每个消息访问模式都需要审查。
需要为以下事项预留时间:
- 查找所有
message.content引用(使用全局搜索) - 理解每个使用上下文
- 在遍历和工具函数之间做出选择
2. Codemod 只完成了 30% 的工作
Codemod 处理了:
- 类型重命名
- 基本 API 变更
- 明显的字段重命名
它没有处理:
- 语义变更(
content访问模式) - 复杂重构(工具检测逻辑)
- 领域特定决策(类型组织)
为 codemod 后的修复预留 70% 的时间。
3. 向后兼容不可妥协
用户不关心你的迁移。他们的旧聊天记录必须正常工作。
策略:
- 读取时转换(我们的做法)
- 写入时转换(渐进式迁移)
- 双模式(复杂但零风险)
我们选择读取时转换,因为:
- 零停机
- 实现简单
- 低风险
- 透明工作
4. 类型系统组织很重要
对于多个聊天系统,合理的类型组织可以防止:
- 工具类型污染
- 使用错误工具导致的运行时错误
- 混乱的自动补全
尽早投入时间做领域特定的类型设计。
5. 增量测试
我们按复杂度顺序迁移系统:
- Agents(最简单,3 个工具)— 验证模式
- NewStudy(1 个工具)— 在生产环境测试
- Persona(1 个工具)— 建立信心
- Interview(2 个工具)— 更复杂
- Study(20+ 工具)— 最终 Boss
每个系统在广泛应用模式之前先进行验证。
结果
v5 之前:
- 不一致的访问模式(
content与parts) - 工具相关的松散类型
- 到处使用
Message类型
v5 之后:
- 单一数据源:
parts数组 - 30+ 工具的完全类型安全
- 领域特定的类型组织
- 更清晰的关注点分离
时间分解:
- 第一天:依赖、codemod、content 访问模式(8 小时)
- 第二天:向后兼容、类型系统(10 小时)
- 第三天:UI 组件、工具定义、边缘情况(6 小时)
总计:3 天内约 24 小时的专注工作。
建议
- 预算 3-5 天,而不是 1-2 天
- 从简单开始:先迁移最简单的系统
- 尽早构建向后兼容,而不是事后补救
- 创建工具函数:
getTextFromParts()、convertV4ToV5()等 - 按领域组织类型,如果你有多个系统
- 测试旧数据:加载历史聊天,验证工具结果显示
- 记录变更:团队需要理解
parts是数据源
迁移工作量很大,但通过合理规划是可控的。v5 基于 parts 的方式更灵活、更类型安全。迁移的痛苦是短期的,收益是长期的。