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
bun add ai
pnpm add ai
快速开始
只需两行代码进行添加,一个参数即可修改:
export default defineEventHandler(async (event) => {
const result = streamText({
model: 'anthropic/claude-sonnet-4.6',
messages,
})
return result.toTextStreamResponse()
})
import { useLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const ai = createAILogger(log)
const result = streamText({
model: ai.wrap('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 对象。与 generateText、streamText 和 ToolLoopAgent 兼容。也适用于预包装的模型(例如来自 supermemory 的模型)。 |
captureEmbed(result) | 手动从 embed() 或 embedMany() 结果捕获 token 使用量、模型信息和维度(嵌入模型使用不同类型)。 |
中间件在提供程序级别拦截调用。它不会触及你的回调、提示或响应。捕获的数据会通过正常的 evlog 管道(采样、增强器、排水口)流动,最终进入 Axiom、Better Stack 或你指定的任何位置。
选项
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
toolInputs | boolean | ToolInputsOptions | false | 启用时,toolCalls 包含 { name, input } 对象而非纯字符串。需主动选择,因为输入可能较大且包含敏感数据。 |
cost | Record<string, ModelCost> | undefined | 成本估算的价格映射。键是模型 ID,值是 { input, output }(每 100 万 token 的美元费用)。 |
传递 true 以捕获所有输入原样,或传递选项对象以进行细粒度控制:
| 子选项 | 类型 | 描述 |
|---|---|---|
maxLength | number | 截断超过此字符长度的字符串化输入(追加 …) |
transform | (input, toolName) => unknown | 在 maxLength 应用之前执行的自定义转换,用于脱敏字段或重塑数据。 |
// 捕获所有内容
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
最常见的模式,完整可观测性的流式聊天:
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
同步生成,中间件会自动捕获结果:
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 都会在线代理循环中累积:
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。它们使用不同的模型类型,无法通过中间件包装:
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 })
{
"ai": {
"calls": 2,
"model": "claude-sonnet-4.6",
"models": ["claude-haiku-4.5", "claude-sonnet-4.6"],
"provider": "anthropic",
"inputTokens": 450,
"outputTokens": 300,
"totalTokens": 750
}
}
模型对象支持
wrap() 也接受来自提供程序 SDK 的模型对象,如果你更喜欢明确的导入:
import { anthropic } from '@ai-sdk/anthropic'
const model = ai.wrap(anthropic('claude-sonnet-4.6'))
遥测集成
对于更深入的可观测性 —— 工具执行时间、成功/失败跟踪和总生成墙时间 —— 请使用 createEvlogIntegration()。它实现了 AI SDK 的 TelemetryIntegration 接口,并捕获中间件无法看到的数据。
与中间件结合使用(推荐)
当与 AILogger 结合使用时,集成共享同一个累加器。两种路径都会写入相同的 ai.* 字段:
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 传递给它:
import { createEvlogIntegration } from 'evlog/ai'
const integration = createEvlogIntegration(log)
const result = await generateText({
model: somePreWrappedModel,
experimental_telemetry: {
isEnabled: true,
integrations: [integration],
},
})
集成捕获的数据
| 数据 | 来源 | 描述 |
|---|---|---|
ai.tools[] | onToolCallFinish | 每个工具的 name、durationMs、success 和 error(如果失败) |
ai.totalDurationMs | onStart → onFinish | 从生成开始到完成的总墙时间 |
中间件捕获 token、模型信息和流式传输指标。集成捕获工具执行时间。两者结合为你提供完整的 AI 可观测性。
捕获的数据
| 广泛事件字段 | 来源 | 描述 |
|---|---|---|
ai.calls | 调用计数 | 此请求中的 AI 调用次数 |
ai.model | response.modelId | 提供响应的模型 |
ai.models | 所有模型 ID | 使用的所有模型数组(仅当 > 1 时) |
ai.provider | model.provider | 提供商(anthropic、openai、google 等) |
ai.inputTokens | usage.inputTokens.total | 所有调用的总输入 token 数 |
ai.outputTokens | usage.outputTokens.total | 所有调用的总输出 token 数 |
ai.totalTokens | 计算 | inputTokens + outputTokens |
ai.cacheReadTokens | usage.inputTokens.cacheRead | 从提示缓存提供的 token 数 |
ai.cacheWriteTokens | usage.inputTokens.cacheWrite | 写入提示缓存的 token 数 |
ai.reasoningTokens | usage.outputTokens.reasoning | 推理 token(扩展思考) |
ai.finishReason | finishReason.unified | 生成结束原因(stop、tool-calls 等) |
ai.toolCalls | 内容 / 流式片段 | string[] 工具名称数组(当启用 toolInputs 时为 Array<{ name, input }>) |
ai.responseId | response.id | 提供程序分配响应 ID(例如 Anthropic 的 msg_...) |
ai.steps | 步骤计数 | LLM 调用次数(仅当 > 1 时) |
ai.stepsUsage | 每步累积 | 每步的 token 和工具调用分解(仅当 > 1 步时) |
ai.msToFirstChunk | 流式计时 | 第一个文本片段的时间(仅流式传输) |
ai.msToFinish | 流式计时 | 总流式持续时间(仅流式传输) |
ai.tokensPerSecond | 计算 | 每秒输出 token 数(仅流式传输) |
ai.error | 错误捕获 | 如果模型调用失败则捕获错误信息 |
ai.tools | TelemetryIntegration | 每个工具的 { name, durationMs, success, error? }(需要 createEvlogIntegration) |
ai.totalDurationMs | TelemetryIntegration | 总生成墙时间(需要 createEvlogIntegration) |
ai.embedding | captureEmbed | { model?, tokens, dimensions?, count? } — 嵌入元数据 |
ai.estimatedCost | 计算 | 估算成本(需要 cost 选项) |
组合性
ai.wrap() 与已经被其他工具包装的模型兼容。如果你使用 supermemory、guardrails 中间件或其他任何模型包装器,请将包装后的模型传递给 ai.wrap():
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 组合:
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)
import { createAILogger } from 'evlog/ai'
app.post('/api/chat', (req, res) => {
const ai = createAILogger(req.log)
// ...
})
import { createAILogger } from 'evlog/ai'
app.post('/api/chat', (c) => {
const ai = createAILogger(c.get('log'))
// ...
})
import { createAILogger } from 'evlog/ai'
app.post('/api/chat', async (request) => {
const ai = createAILogger(request.log)
// ...
})
import { useLogger } from 'evlog/nestjs'
import { createAILogger } from 'evlog/ai'
const log = useLogger()
const ai = createAILogger(log)
import { createLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'
const log = createLogger()
const ai = createAILogger(log)
// ...
log.emit()