日志记录

AI SDK 集成

从 Vercel AI SDK 捕获 token 使用量、工具调用、模型信息和流式传输指标到广泛事件中。为你的模型添加中间件和可选的遥测集成,实现完整的 AI 可观测性。

evlog/ai 通过为你的模型添加中间件和可选的遥测集成,提供完整的 AI 可观测性。Token 使用量、工具调用、工具执行时间、流式传输性能、缓存命中、推理 token、成本估算 —— 所有这些都会自动捕获到广泛事件中。

提示
使用 evlog 为我的应用添加 AI 可观测性。

- 安装 AI SDK:pnpm add ai
- 从 'evlog/ai' 导入 createAILogger 和 createEvlogIntegration
- 使用 createAILogger(log) 创建 AI logger,其中 log 是你的请求 logger
- 使用 ai.wrap('anthropic/claude-sonnet-4.6') 包装你的模型,并将其传递给 generateText、streamText 等
- Token 使用量、工具调用、流式传输指标和错误会自动捕获到广泛事件
- 若要更深入的可观测性(工具执行时间、总生成墙时间),请在 experimental_telemetry.integrations 中添加 createEvlogIntegration(ai)
- 对于嵌入调用,在 embed() 或 embedMany() 后使用 ai.captureEmbed({ usage, model, dimensions, count })
- 若要进行成本估算,请传递成本映射:createAILogger(log, { cost: { 'claude-sonnet-4.6': { input: 3, output: 15 } } })
- 适用于所有框架:Nuxt、Express、Hono、Fastify、NestJS、Elysia、独立运行

文档:https://www.evlog.dev/logging/ai-sdk
适配器:https://www.evlog.dev/adapters

安装

将 AI SDK 作为依赖项添加:

npm install ai

快速开始

只需两行代码进行添加,一个参数即可修改:

export default defineEventHandler(async (event) => {
  const result = streamText({
    model: 'anthropic/claude-sonnet-4.6',
    messages,
  })
  return result.toTextStreamResponse()
})

你的广泛事件现在包含:

广泛事件
{
  "method": "POST",
  "path": "/api/chat",
  "status": 200,
  "duration": "4.5s",
  "ai": {
    "calls": 1,
    "model": "claude-sonnet-4.6",
    "provider": "anthropic",
    "inputTokens": 3312,
    "outputTokens": 814,
    "totalTokens": 4126,
    "reasoningTokens": 225,
    "finishReason": "stop",
    "msToFirstChunk": 234,
    "msToFinish": 4500,
    "tokensPerSecond": 180
  }
}

工作原理

createAILogger(log, options?) 返回一个带有两个方法的 AILogger

方法描述
wrap(model)使用中间件包装语言模型。接受模型字符串(例如 'anthropic/claude-sonnet-4.6')或 LanguageModelV3 对象。与 generateTextstreamTextToolLoopAgent 兼容。也适用于预包装的模型(例如来自 supermemory 的模型)。
captureEmbed(result)手动从 embed()embedMany() 结果捕获 token 使用量、模型信息和维度(嵌入模型使用不同类型)。

中间件在提供程序级别拦截调用。它不会触及你的回调、提示或响应。捕获的数据会通过正常的 evlog 管道(采样、增强器、排水口)流动,最终进入 Axiom、Better Stack 或你指定的任何位置。

选项

选项类型默认值描述
toolInputsboolean | ToolInputsOptionsfalse启用时,toolCalls 包含 { name, input } 对象而非纯字符串。需主动选择,因为输入可能较大且包含敏感数据。
costRecord<string, ModelCost>undefined成本估算的价格映射。键是模型 ID,值是 { input, output }(每 100 万 token 的美元费用)。

传递 true 以捕获所有输入原样,或传递选项对象以进行细粒度控制:

子选项类型描述
maxLengthnumber截断超过此字符长度的字符串化输入(追加
transform(input, toolName) => unknownmaxLength 应用之前执行的自定义转换,用于脱敏字段或重塑数据。
server/api/chat.post.ts
// 捕获所有内容
const ai = createAILogger(log, { toolInputs: true })

// 截断长输入(例如 SQL 查询)
const ai = createAILogger(log, { toolInputs: { maxLength: 200 } })

// 脱敏敏感的工具输入
const ai = createAILogger(log, {
  toolInputs: {
    maxLength: 500,
    transform: (input, toolName) => {
      if (toolName === 'queryDB') return { sql: '***' }
      return input
    },
  },
})

// 成本估算
const ai = createAILogger(log, {
  cost: {
    'claude-sonnet-4.6': { input: 3, output: 15 },
    'gpt-4o': { input: 2.5, output: 10 },
  },
})

使用模式

streamText

最常见的模式,完整可观测性的流式聊天:

server/api/chat.post.ts
import { streamText } from 'ai'
import { createAILogger } from 'evlog/ai'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const ai = createAILogger(log)
  const { messages } = await readBody(event)

  log.set({ action: 'chat', messagesCount: messages.length })

  const result = streamText({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    messages,
    onFinish: ({ text }) => {
      // 你的代码,不会与 evlog 冲突
      saveConversation(text)
    },
  })

  return result.toTextStreamResponse()
})

generateText

同步生成,中间件会自动捕获结果:

server/api/summarize.post.ts
import { generateText } from 'ai'
import { createAILogger } from 'evlog/ai'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const ai = createAILogger(log)

  const result = await generateText({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    prompt: 'Summarize this document',
  })

  return { text: result.text }
})

多步骤代理

中间件会自动触发每一步。步骤、工具调用和 token 都会在线代理循环中累积:

server/api/agent.post.ts
import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from 'ai'
import { useLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const { messages } = await readBody(event)
  const ai = createAILogger(log, {
    toolInputs: { maxLength: 500 },
  })

  const agent = new ToolLoopAgent({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    tools: { searchWeb, queryDatabase },
    stopWhen: stepCountIs(5),
  })

  return createAgentUIStreamResponse({
    agent,
    uiMessages: messages,
  })
})

多步骤代理运行后的广泛事件:

广泛事件
{
  "ai": {
    "calls": 3,
    "steps": 3,
    "model": "claude-sonnet-4.6",
    "provider": "anthropic",
    "inputTokens": 4500,
    "outputTokens": 1200,
    "totalTokens": 5700,
    "finishReason": "stop",
    "toolCalls": [
      { "name": "searchWeb", "input": { "query": "TypeScript 6.0 features" } },
      { "name": "queryDatabase", "input": { "sql": "SELECT * FROM docs WHERE topic = 'typescript'" } },
      { "name": "searchWeb", "input": { "query": "TypeScript 6.0 release date" } }
    ],
    "responseId": "msg_01XFDUDYJgAACzvnptvVoYEL",
    "stepsUsage": [
      { "model": "claude-sonnet-4.6", "inputTokens": 1200, "outputTokens": 300, "toolCalls": ["searchWeb"] },
      { "model": "claude-sonnet-4.6", "inputTokens": 1500, "outputTokens": 400, "toolCalls": ["queryDatabase", "searchWeb"] },
      { "model": "claude-sonnet-4.6", "inputTokens": 1800, "outputTokens": 500 }
    ],
    "msToFirstChunk": 312,
    "msToFinish": 8200,
    "tokensPerSecond": 146
  }
}

RAG(嵌入 + 生成)

对于嵌入调用,请使用 captureEmbed。它们使用不同的模型类型,无法通过中间件包装:

server/api/rag.post.ts
import { embed, generateText } from 'ai'
import { useLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const ai = createAILogger(log)

  const { embedding, usage } = await embed({
    model: openai.embedding('text-embedding-3-small'),
    value: query,
  })
  ai.captureEmbed({
    usage,
    model: 'text-embedding-3-small',
    dimensions: 1536,
  })

  const docs = await findSimilar(embedding)

  const result = await generateText({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    prompt: buildPrompt(docs),
  })

  return { text: result.text }
})

对于 embedMany,请传递批次计数:

const { embeddings, usage } = await embedMany({
  model: openai.embedding('text-embedding-3-small'),
  values: documents,
})
ai.captureEmbed({ usage, model: 'text-embedding-3-small', count: documents.length })

多模型

分别包装每个模型,它们共享同一个累加器。当使用多个模型时,广泛事件会同时包含 model(最后一个模型)和 models(所有唯一模型):

const ai = createAILogger(log)

const fast = ai.wrap('anthropic/claude-haiku-4.5')
const smart = ai.wrap('anthropic/claude-sonnet-4.6')

const classification = await generateText({ model: fast, prompt: classifyPrompt })
const response = await generateText({ model: smart, prompt: detailedPrompt })

模型对象支持

wrap() 也接受来自提供程序 SDK 的模型对象,如果你更喜欢明确的导入:

server/api/chat.post.ts
import { anthropic } from '@ai-sdk/anthropic'

const model = ai.wrap(anthropic('claude-sonnet-4.6'))

遥测集成

对于更深入的可观测性 —— 工具执行时间、成功/失败跟踪和总生成墙时间 —— 请使用 createEvlogIntegration()。它实现了 AI SDK 的 TelemetryIntegration 接口,并捕获中间件无法看到的数据。

与中间件结合使用(推荐)

当与 AILogger 结合使用时,集成共享同一个累加器。两种路径都会写入相同的 ai.* 字段:

server/api/agent.post.ts
import { generateText } from 'ai'
import { createAILogger, createEvlogIntegration } from 'evlog/ai'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const ai = createAILogger(log)

  const result = await generateText({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    tools: { getWeather, searchDB },
    experimental_telemetry: {
      isEnabled: true,
      integrations: [createEvlogIntegration(ai)],
    },
  })

  return { text: result.text }
})

你的广泛事件现在包含工具执行细节:

广泛事件
{
  "ai": {
    "calls": 2,
    "steps": 2,
    "model": "claude-sonnet-4.6",
    "provider": "anthropic",
    "inputTokens": 3500,
    "outputTokens": 800,
    "totalTokens": 4300,
    "toolCalls": ["getWeather", "searchDB"],
    "tools": [
      { "name": "getWeather", "durationMs": 150, "success": true },
      { "name": "searchDB", "durationMs": 45, "success": true }
    ],
    "totalDurationMs": 2340,
    "msToFirstChunk": 180,
    "msToFinish": 2100,
    "tokensPerSecond": 380
  }
}

独立使用(无中间件)

如果你的模型已经被包装(例如由其他中间件包装),直接将请求 logger 传递给它:

server/api/chat.post.ts
import { createEvlogIntegration } from 'evlog/ai'

const integration = createEvlogIntegration(log)

const result = await generateText({
  model: somePreWrappedModel,
  experimental_telemetry: {
    isEnabled: true,
    integrations: [integration],
  },
})

集成捕获的数据

数据来源描述
ai.tools[]onToolCallFinish每个工具的 namedurationMssuccesserror(如果失败)
ai.totalDurationMsonStartonFinish从生成开始到完成的总墙时间

中间件捕获 token、模型信息和流式传输指标。集成捕获工具执行时间。两者结合为你提供完整的 AI 可观测性。

捕获的数据

广泛事件字段来源描述
ai.calls调用计数此请求中的 AI 调用次数
ai.modelresponse.modelId提供响应的模型
ai.models所有模型 ID使用的所有模型数组(仅当 > 1 时)
ai.providermodel.provider提供商(anthropicopenaigoogle 等)
ai.inputTokensusage.inputTokens.total所有调用的总输入 token 数
ai.outputTokensusage.outputTokens.total所有调用的总输出 token 数
ai.totalTokens计算inputTokens + outputTokens
ai.cacheReadTokensusage.inputTokens.cacheRead从提示缓存提供的 token 数
ai.cacheWriteTokensusage.inputTokens.cacheWrite写入提示缓存的 token 数
ai.reasoningTokensusage.outputTokens.reasoning推理 token(扩展思考)
ai.finishReasonfinishReason.unified生成结束原因(stoptool-calls 等)
ai.toolCalls内容 / 流式片段string[] 工具名称数组(当启用 toolInputs 时为 Array<{ name, input }>
ai.responseIdresponse.id提供程序分配响应 ID(例如 Anthropic 的 msg_...
ai.steps步骤计数LLM 调用次数(仅当 > 1 时)
ai.stepsUsage每步累积每步的 token 和工具调用分解(仅当 > 1 步时)
ai.msToFirstChunk流式计时第一个文本片段的时间(仅流式传输)
ai.msToFinish流式计时总流式持续时间(仅流式传输)
ai.tokensPerSecond计算每秒输出 token 数(仅流式传输)
ai.error错误捕获如果模型调用失败则捕获错误信息
ai.toolsTelemetryIntegration每个工具的 { name, durationMs, success, error? }(需要 createEvlogIntegration
ai.totalDurationMsTelemetryIntegration总生成墙时间(需要 createEvlogIntegration
ai.embeddingcaptureEmbed{ model?, tokens, dimensions?, count? } — 嵌入元数据
ai.estimatedCost计算估算成本(需要 cost 选项)

组合性

ai.wrap() 与已经被其他工具包装的模型兼容。如果你使用 supermemory、guardrails 中间件或其他任何模型包装器,请将包装后的模型传递给 ai.wrap()

server/api/chat.post.ts
import { createAILogger } from 'evlog/ai'
import { withSupermemory } from '@supermemory/tools/ai-sdk'
import { createGateway } from 'ai'

const gateway = createGateway({ ... })
const ai = createAILogger(log)
const base = gateway('anthropic/claude-sonnet-4.6')
const model = ai.wrap(withSupermemory(base, 'your-org-id', { mode: 'full' }))

如需显式的中间件组合,请使用 createAIMiddleware 获取原始中间件,并自行通过 wrapLanguageModel 组合:

server/api/chat.post.ts
import { createAIMiddleware } from 'evlog/ai'
import { wrapLanguageModel } from 'ai'

const model = wrapLanguageModel({
  model: base,
  middleware: [createAIMiddleware(log, { toolInputs: true }), otherMiddleware],
})

createAIMiddleware 返回与 createAILogger 内部使用的相同中间件。区别在于:createAIMiddleware 不包含 captureEmbed(嵌入模型不使用中间件)。使用 createAILogger 获取完整 API,createAIMiddleware 用于需要显式中间件排序的场景。

错误处理

如果模型调用失败,中间件会在重新抛出错误之前将错误捕获到广泛事件中:

广泛事件
{
  "ai": {
    "calls": 1,
    "model": "claude-sonnet-4.6",
    "provider": "anthropic",
    "finishReason": "error",
    "error": "API rate limit exceeded"
  }
}

流式错误(例如内容过滤器)也会从流的错误片段中捕获。

适用于所有框架

evlog/ai 适用于 evlog 支持的任何框架:

import { useLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'

const log = useLogger(event)
const ai = createAILogger(log)