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

相关推荐
决战灬1 分钟前
langgraph之interrupt(事例篇)
人工智能·python·agent
张申傲12 分钟前
拆解 harness9(9):Observability 可观测性
人工智能·aigc·agent·deepseek·harness
AINative软件工程1 小时前
LLM 应用的熔断降级工程实践:Circuit Breaker 不只是重试的升级版
后端·llm·ai编程
陳陈陳10 小时前
Workflow vs Agent:别再被“调包侠”忽悠了,一张图看懂AI工程的“骨架”与“大脑”
langchain·agent·workflow
新知图书11 小时前
11.3 详细实现与核心配置(作业批改智能体开发)
人工智能·agent·ai agent·智能体·扣子
wangruofeng11 小时前
opencodex 解锁 Codex 任意模型,一个本地代理打通 Claude/Kimi/GLM/DeepSeek
llm·github·openai
wangruofeng11 小时前
姚顺雨长谈:在 Anthropic 和 Gemini 训练模型,英雄主义已经过时
llm·aigc
小林ixn12 小时前
Workflow 还是 Agent?帮你一次性搞清楚这俩到底有啥不一样
agent·workflow
Darling噜啦啦12 小时前
Workflow 还是 Agent?一文搞懂 AI 工程的两大核心范式,以及它们的未来
agent·workflow
沉默王二14 小时前
腾讯一面,我霸气反问:“你说你们在做Agent项目,说说 SubAgent、Plan 模式、Skill 调用这些你们都是怎么做的?”面试官一直在擦汗。。
面试·agent·ai编程