🚀 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 应用就拥有了专业的元数据处理能力,不再是简单的"黑盒"对话了!

相关推荐
云道轩13 小时前
告诉 Claude Code 在项目中遵循特定的编程模式/设计模式和技术栈约束
设计模式·ai·agent·claude code
后端小肥肠14 小时前
OpenClaw多Agent实战|手把手教你用一只小龙虾接入多个飞书Bot
人工智能·aigc·agent
x-cmd14 小时前
[x-cmd] 一切 Web、桌面应用和本地工具皆可 CLI -opencli
前端·ai·github·agent·cli·x-cmd
码农三叔14 小时前
(11-4-02)感知-运动耦合与行为理解:人形机器人沉浸式感知运动协同系统(2)人形机器人运动控制
人工智能·机器人·agent·人形机器人
swipe15 小时前
向量数据库实战:为什么 AI Agent 离不开 Milvus
前端·面试·agent
Nelson82012515 小时前
不只是 Copilot:一个完整 AI 软件交付团队的实践 - iforgeAI - 用更少的Tokens,办大事
agent
码森林20 小时前
别卷模型了!OpenAI 工程师都在偷偷用的"Harness Engineering",才是 AI 编程的终极杀器
agent·ai编程·全栈
米小虾20 小时前
从对话到行动:AI Agent 架构演进与工程实践指南
人工智能·langchain·agent
NikoAI编程21 小时前
Claude Code宝藏命令: /insights 报告
agent·ai编程·claude
嘉伟咯21 小时前
动手做一个AIAgent - SKILLS
人工智能·agent