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

相关推荐
hpoenixf19 分钟前
从工具调用到模型输入:把 Agent 的观测链路接起来
agent
10年前端老司机1 小时前
你的RAG检索正在“高效地重复废话”:一文彻底搞懂MMR算法
langchain·llm·agent
stereohomology1 小时前
大模型的观点谨:StoryTold 「Crafting Apps」四件套 · 深度总览
人工智能·llm
Csvn3 小时前
线上出问题怎么查?一套可复现的排障 SOP(O04)
人工智能·aigc·agent
浮链序5 小时前
怎么证明"你这个模型是偷我的"——把蒸馏变成取证工具
算法·安全·llm
用户938515635077 小时前
从 0 到 1 搭建企业级多模态 RAG 知识库:一个装修公司的 AI 落地实战
人工智能·langchain·llm
我跟你说7 小时前
不改权重的终身学习:AI Agent 的“可塑性”架构与技能自演进深度解构
agent
用户721746588267 小时前
把云端沙箱实例拆成三层看:控制面、运行时、计费面的一次全流程核查
agent·ai编程
用户250458106097 小时前
AI智能体正在改变软件开发方式,但距离完全自主编程还有多远?
aigc·agent
救救孩子把8 小时前
Agent面试题-记忆治理-上下文-多Agent协作与工程落地
面试·agent·多agent·agent记忆·agent面试题·上下文治理