🚀 Vercel AI SDK 使用指南: 消息元数据 (Message Metadata)

在构建 AI 聊天应用时,我们经常需要传递一些不属于消息内容本身的额外信息。例如:

  • 🕒 时间戳:消息生成的时间。
  • 🤖 模型信息:使用的是 GPT-4 还是 Claude 3.5。
  • 💰 Token 用量:当前对话消耗了多少 Token。
  • 🆔 用户上下文:Session ID 或用户 ID。

Vercel AI SDK 提供了 Message Metadata(消息元数据) 功能来解决这个问题。它允许我们在消息级别(Message Level)附加自定义数据,这些数据不会作为 prompt 的一部分发送给大模型,而是专门用于 UI 展示或逻辑处理。

本文将带你通过三个步骤,实现一个带有时间戳和 Token 统计功能的聊天应用。


1. 定义类型 (Type Safety)

为了在前后端获得完整的 TypeScript 类型提示,我们首先需要定义元数据的 Schema。这里使用 zod 来定义结构。

新建 app/types.ts

TypeScript

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>;

2. 服务端实现 (Server Side)

在服务端,我们需要在流式响应中注入元数据。Vercel AI SDK 的 streamText 返回的 result 对象提供了一个 toUIMessageStreamResponse 方法,专门用于处理这种情况。

我们可以利用 messageMetadata 回调函数,在流的 开始 (start)结束 (finish) 阶段注入不同的数据。

新建或修改 app/api/chat/route.ts

TypeScript

javascript 复制代码
// 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,
        };
      }
    },
  });
}

注意toUIMessageStreamResponse 是处理 UI 消息流的标准方式,它能确保元数据与文本流正确合并,而不会破坏流式响应的格式。


3. 客户端实现 (Client Side)

在客户端,我们使用 useChat hook,并传入我们定义的 MyUIMessage 泛型。这样 message.metadata 就会有自动补全提示了。

修改 app/page.tsx

TypeScript

javascript 复制代码
// 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>
  );
}

💡 总结与最佳实践

  1. 区分场景:不要将所有数据都塞进 Metadata。

    • Metadata:适合"关于消息的数据"(如时间、来源、成本)。
    • Data Parts:适合"消息的内容"(如生成的图表数据、工具调用结果)。
  2. 类型安全 :始终使用 zod 定义 Schema 并共享给前后端,这能避免很多拼写错误带来的 bug。

  3. 按需发送 :利用 part.type ('start' 或 'finish'),只在正确的时机发送必要的数据。例如 Token 统计只有在生成结束后 (finish) 才能获取到。

通过以上配置,你的 AI 应用就拥有了专业的元数据处理能力,不再是简单的"黑盒"对话了!

相关推荐
想要成为糕糕手19 小时前
深入理解AI Agent工具调用:从原理到代码实现
llm·agent
yLDeveloper20 小时前
从矩阵乘法到多模态大模型 - LLM 篇
llm·nlp
Sokach101520 小时前
Windows使用hermes桌面端个人出现的问题
agent
leeyi20 小时前
Agent Transfer:让 AI 把任务交给更合适的 AI
aigc·agent·ai编程
后端小肥肠20 小时前
Codex + Obsidian 做人生副本视频:输入主题文案,直通剪映草稿
人工智能·aigc·agent
花椒技术21 小时前
Agent 不只会聊天:我们如何用 CLI 整理业务能力入口
agent·ai编程·mcp
DigitalOcean1 天前
在云端运行 Codex —— DigitalOcean Codex 插件正式推出
agent
FanetheDivine1 天前
学习Agent开发6 langgraph速览
agent·ai编程
前端君1 天前
Claude Code 如何配置本地Ollama模型或别的模型(Deepseek等)
llm·agent·claude
程序员小假1 天前
RAG文档存储与切割策略详解:从基础到进阶
agent