云上建始
60.80M · 2026-02-19
在构建 AI 聊天应用时,我们经常需要传递一些不属于消息内容本身的额外信息。例如:
Vercel AI SDK 提供了 Message Metadata(消息元数据) 功能来解决这个问题。它允许我们在消息级别(Message Level)附加自定义数据,这些数据不会作为 prompt 的一部分发送给大模型,而是专门用于 UI 展示或逻辑处理。
本文将带你通过三个步骤,实现一个带有时间戳和 Token 统计功能的聊天应用。
为了在前后端获得完整的 TypeScript 类型提示,我们首先需要定义元数据的 Schema。这里使用 zod 来定义结构。
新建 app/types.ts:
TypeScript
// app/types.ts
import { UIMessage } from 'ai';
import { z } from 'zod';
// 1. 定义元数据 Schema
// 这里我们定义了创建时间、模型名称和 Token 用量
export const messageMetadataSchema = z.object({
createdAt: z.number().optional(),
model: z.string().optional(),
totalTokens: z.number().optional(),
});
// 2. 导出 Metadata 类型
export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
// 3. 创建带有元数据的 UIMessage 类型
// 在客户端,我们将使用这个类型来替代默认的 Message
export type MyUIMessage = UIMessage<MessageMetadata>;
在服务端,我们需要在流式响应中注入元数据。Vercel AI SDK 的 streamText 返回的 result 对象提供了一个 toUIMessageStreamResponse 方法,专门用于处理这种情况。
我们可以利用 messageMetadata 回调函数,在流的 开始 (start) 和 结束 (finish) 阶段注入不同的数据。
新建或修改 app/api/chat/route.ts:
TypeScript
// app/api/chat/route.ts
import { convertToModelMessages, streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic'; // 或者 @ai-sdk/openai
import type { MyUIMessage } from '@/app/types'; // 引入我们定义的类型
export async function POST(req: Request) {
// 显式指定 messages 的类型为 MyUIMessage[]
const { messages }: { messages: MyUIMessage[] } = await req.json();
const result = streamText({
model: anthropic('claude-3-5-sonnet-20240620'), // 这里替换为你使用的模型
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
// 传入原始 messages 以确保返回对象的类型安全
originalMessages: messages,
messageMetadata: ({ part }) => {
// 阶段 1: 流开始时,注入创建时间和模型信息
if (part.type === 'start') {
return {
createdAt: Date.now(),
model: 'claude-3-5-sonnet',
};
}
// 阶段 2: 流结束时,注入 Token 用量统计
if (part.type === 'finish') {
return {
totalTokens: part.totalUsage.totalTokens,
};
}
},
});
}
在客户端,我们使用 useChat hook,并传入我们定义的 MyUIMessage 泛型。这样 message.metadata 就会有自动补全提示了。
修改 app/page.tsx:
TypeScript
// app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import type { MyUIMessage } from '@/app/types';
export default function Chat() {
// 1. 使用泛型 MyUIMessage 初始化 useChat
// 2. 配置 transport 以匹配服务端 API 路径
const { messages, input, handleInputChange, handleSubmit } = useChat<MyUIMessage>({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
});
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map(message => (
<div key={message.id} className="mb-4 whitespace-pre-wrap">
<div className="font-bold flex items-center gap-2">
{message.role === 'user' ? 'User' : 'AI'}
{/* --- 展示元数据:时间戳 --- */}
{message.metadata?.createdAt && (
<span className="text-xs text-gray-400 font-normal">
{new Date(message.metadata.createdAt).toLocaleTimeString()}
</span>
)}
</div>
{/* 渲染消息内容 */}
<div className="mt-1">
{message.content}
</div>
{/* --- 展示元数据:Token 统计 (仅在流结束后显示) --- */}
{message.metadata?.totalTokens && (
<div className="text-xs text-gray-400 mt-1 bg-gray-100 p-1 rounded inline-block">
Token消耗: {message.metadata.totalTokens}
</div>
)}
</div>
))}
<form onSubmit={handleSubmit} className="fixed bottom-0 w-full max-w-md p-2 bg-white border-t">
<input
className="w-full p-2 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={handleInputChange}
/>
</form>
</div>
);
}
区分场景:不要将所有数据都塞进 Metadata。
类型安全:始终使用 zod 定义 Schema 并共享给前后端,这能避免很多拼写错误带来的 bug。
按需发送:利用 part.type ('start' 或 'finish'),只在正确的时机发送必要的数据。例如 Token 统计只有在生成结束后 (finish) 才能获取到。
通过以上配置,你的 AI 应用就拥有了专业的元数据处理能力,不再是简单的“黑盒”对话了!