用例

eve

源代码
每个 eve agent 轮次产生一个 evlog 宽事件:令牌使用量、工具执行和业务上下文,通过你自己的 drain 和尾部采样实现。

eve 内置了可观测性:Vercel 上的 Agent Runs,以及通过 agent/instrumentation.ts 提供的可选 OpenTelemetry spans。evlog/eve 增加了第三层:每个轮次都生成可导出的宽事件,并接入你完整的 evlog 管道(drains、enrichers、尾部采样、审计)。

evlog/eve 需要 eve 0.30 或更高版本。eve 仍处于 beta 阶段,流事件结构可能会在正式发布前发生变化,因此请在生产环境中的 agent 内固定 eveevlog 的版本。

为我的 eve agent 添加 evlog 宽事件

何时使用什么

需求使用
在 Vercel 中调试会话eve Agent Runs(自动)
在 Datadog / Honeycomb 中查看跨度级追踪eve agent/instrumentation.ts + OTel 导出器
将宽事件发送到 Axiom / Better Stack / FS,用于计费、审计和尾部采样evlog/eve
在跨度与其宽事件之间相互跳转defineEvlogInstrumentation()见下文

快速开始

1. 安装

pnpm add evlog eve

2. 添加 hook

创建 agent/hooks/evlog.ts

agent/hooks/evlog.ts
import { defineEvlogHook } from 'evlog/eve'
import { createAxiomDrain } from 'evlog/axiom'

export default defineEvlogHook({
  init: { env: { service: 'my-agent' } },
  drain: createAxiomDrain(),
  enrich: (ctx) => {
    ctx.event.region = process.env.VERCEL_REGION
  },
})

eve 会自动发现 agent/hooks/ 下的 hook 文件。不需要 HTTP 中间件,因为工作单元是 agent 轮次,而不是请求。

3. 从工具中记录业务上下文

defineEvlogHook() 会在 turn.started 上通过 AsyncLocalStorage 绑定轮次 logger。在工具的 execute() 处理器内,不带参数调用 useLogger(),其使用方式与 Nuxt 或 Hono 中的 useLogger(event) 相同。

示例 agent 中,支持工具会在 agent 处理退款时附加客户和订单上下文:

agent/tools/lookup_order.ts
import { defineTool } from 'eve/tools'
import { useLogger } from 'evlog/eve'
import { z } from 'zod'

export default defineTool({
  description: '按 ID 查找订单。',
  inputSchema: z.object({ orderId: z.string() }),
  async execute({ orderId }) {
    const order = await fetchOrder(orderId)
    const log = useLogger()
    log.set({ order: { id: order.id, amount: order.amount, status: order.status } })
    return order
  },
})
**解决方案:**当 AsyncLocalStorage 无法传播时(例如 hook 和工具使用了不同的 bundle),传入 eve 工具的 ctxuseLogger(ctx)。使用标准 eve agent 布局并注册了 agent/hooks/evlog.ts 后,通常不需要传入它。useLogger() 从不抛出异常:当它无法访问轮次 logger 时,会发出一次警告,并返回一个接受所有调用但不产生任何输出的 logger。在步骤边界之后于另一个进程中恢复轮次是最常见的情况,丢失 enrich 信息绝不能导致正在进行 enrich 的工具失败。轮次会继续运行;只有其额外字段会被丢弃。

4. Next.js Web 聊天(可选)

使用来自 eve/nextwithEve() 包装 next.config.ts,并在应用中使用来自 eve/reactuseEveAgent()。eve 会与 next dev 一起启动,并在同一源上代理 /eve/v1/*,因此工具调用、审批和 ask_question 都可以直接使用。请参阅示例 agent

宽事件形态

每个已完成的轮次都会发出一个事件:

宽事件 — 轮次等待审批
{
  "method": "EVE",
  "path": "/sessions/sess_abc/turns/turn_0",
  "status": 200,
  "duration": "7.9s",
  "durationMs": 7903,
  "service": "clearbill-support-agent",
  "eve": {
    "sessionId": "sess_abc",
    "turnId": "turn_0",
    "turnSequence": 0,
    "phase": "awaiting-approval",
    "sessionTurns": 1
  },
  "customer": { "slug": "acme-corp", "plan": "enterprise" },
  "order": { "id": "4821", "amount": 890, "currency": "USD" },
  "approval": { "status": "pending", "tool": "issue_refund" },
  "ai": {
    "model": "deepseek/deepseek-v4-flash",
    "provider": "deepseek",
    "calls": 2,
    "steps": 2,
    "inputTokens": 11724,
    "outputTokens": 282,
    "finishReason": "tool-calls",
    "tools": [
      { "name": "lookup_customer", "durationMs": 26, "success": true, "inputTokens": 8900 },
      { "name": "lookup_order", "durationMs": 13, "success": true }
    ]
  }
}

在审批通过后,下一个轮次会携带结果:

宽事件 — 退款已完成
{
  "path": "/sessions/sess_abc/turns/turn_1",
  "eve": {
    "sessionId": "sess_abc",
    "turnId": "turn_1",
    "turnSequence": 1,
    "sessionTurns": 2
  },
  "refund": { "orderId": "4821", "amount": 890, "status": "refunded" },
  "audit": { "action": "refund.issued", "target": { "type": "order", "id": "4821" } },
  "approval": { "status": "approved", "tool": "issue_refund" },
  "ai": {
    "tools": [{ "name": "issue_refund", "durationMs": 993, "success": true }],
    "finishReason": "stop"
  }
}

Token 使用量和工具执行情况会从 eve 流事件(step.completedactions.requestedaction.result)中累积。通过 useLogger() 设置的业务字段会在同一会话的各轮次之间延续。在分析中使用 eve.sessionId + eve.turnSequence 关联各轮次,并且每个事件都保持自包含。eve.phase 仅会在非例行结束时设置:awaiting-approvalawaiting-authorizationrejectedcancelledfailed

轮次可以报告的所有信息

除了上述标识符之外,轮次还会记录 eve 报告的相关信息。每个字段都是可选的,只有在轮次产生该字段时才会出现。

字段含义
eve.runtimeeve 版本、agent ID、模型,以及部署的 gitSha / gitBranch / deployedAt
eve.caller触发轮次的调用方:principalIdprincipalTypeauthenticator。在多用户频道中,你可以据此对成本和数量进行分组。绝不会记录 subjectattributes — 频道可能会将姓名或电子邮件放入其中
eve.parent子 agent 运行的父会话 ID 和根会话 ID — 使用 rootSessionId 重建委派树
eve.authorizations连接登录信息,包括其 outcomereason 和持续时间
eve.compaction执行了多少次压缩、使用了哪个模型,以及 inputTokensAtTrigger — 第一次压缩触发时上下文有多满
eve.stepFailures / eve.failedSteps失败的模型调用,包括随后通过重试成功的轮次中的失败调用
eve.subagents委派信息,包括其状态和持续时间
eve.reasoningblockschars — 模型思考的大小,但绝不会记录其内容
eve.result结构化结果,适用于具有输出 schema 的 agent
eve.contextCleared持久化历史记录在此轮次期间被清除
message.responseCharsagent 响应的长度,无论使用哪种 message 模式都会记录
ai.costUsdeve 报告的成本。ai.estimatedCost 是根据 cost 计算出的备用值
ai.model / ai.provider为轮次提供服务的网关 slug(deepseek/deepseek-v4-flash)和解析后的 provider(deepseek)。ai.provider 是从 eve 在 session.started 上报告的模型 ID 中拆分出来的
ai.tools[].inputTokens工具结果为下一次模型调用增加的输入 token 数:调用工具的步骤与使用其结果的步骤之间的增量。如果轮次在结果被使用前结束,或者压缩重置了上下文,则不设置该字段

关联宽事件与追踪

一个宽事件和一个 Agent Runs span 描述的是同一轮交互,但它们不会自动关联。defineEvlogInstrumentation() 会将 evlog 的轮次标识写入 eve 的 AI SDK spans:

agent/instrumentation.ts
import { defineEvlogInstrumentation } from 'evlog/eve'

export default defineEvlogInstrumentation()

随后,每个模型调用 span 及其子 span 都会携带 evlog.request_idevlog.session_id,其值与宽事件报告的 requestIdeve.sessionId 相同。你可以从 Braintrust、Datadog 或 Agent Runs 中的追踪直接跳转到 drain 中的事件,也可以反向跳转。

不使用 setup 时,OpenTelemetry 导出不会受到影响:eve 仍会写入本地追踪,可通过 eve traces 查看。传入 setup 即可将其导出到其他位置:

agent/instrumentation.ts
import { defineEvlogInstrumentation } from 'evlog/eve'
import { registerOTel } from '@vercel/otel'

export default defineEvlogInstrumentation({
  setup: ({ agentName }) => registerOTel({ serviceName: agentName }),
})

functionIdrecordInputsrecordOutputstraceChannelRequests 会直接传递给 eve 的 defineInstrumentation

与其他可观测性后端并用

defineEvlogInstrumentation() 是仅使用 evlog 进行 agent instrumentation 时的快捷方式。它拥有该文件,而一个 agent 恰好只有一个 agent/instrumentation.ts,因此 eve 注册表中的每个可观测性项目都会写入同一个路径。这也是为什么在 eve add instrumentation/posthog 之后执行 eve add instrumentation/sentry 会拒绝执行,而不是覆盖前者。

一旦启用了其他后端,请使用 eve 自己的 defineInstrumentation 手动编写该文件,并将 evlogRuntimeContext 展开到运行时上下文中。evlog 会为你的 instrumentation 提供属性,而不是对其进行包装:

agent/instrumentation.ts
import { BatchSpanProcessor, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { PostHogTraceExporter } from '@posthog/ai/otel'
import { OTLPHttpProtoTraceExporter, registerOTel } from '@vercel/otel'
import { defineInstrumentation } from 'eve/instrumentation'
import { evlogRuntimeContext } from 'evlog/eve'

export default defineInstrumentation({
  // 每个后端使用一个 provider 和一个 span processor。生成
  // `traceExporter` 的项目(Sentry、Datadog、Arize、Jaeger、Braintrust、Honeycomb)
  // 会成为此数组中的一项。只调用一次 registerOTel,不要为每个后端各调用一次。
  setup: ({ agentName }) => registerOTel({
    serviceName: agentName,
    spanProcessors: [
      new SimpleSpanProcessor(new PostHogTraceExporter({
        projectToken: process.env.POSTHOG_PROJECT_TOKEN!,
      })),
      new BatchSpanProcessor(new OTLPHttpProtoTraceExporter({
        url: process.env.SENTRY_OTLP_TRACES_ENDPOINT!,
        headers: { 'x-sentry-auth': `sentry sentry_key=${process.env.SENTRY_PUBLIC_KEY}` },
      })),
    ],
  }),
  events: {
    'step.started': (input) => {
      const principalId = input.session.auth.initiator?.principalId
        ?? input.session.auth.current?.principalId
      return {
        runtimeContext: {
          ...evlogRuntimeContext(input),
          // 省略而不是留空:空属性会显示为空 ID。
          ...(principalId ? { posthog_distinct_id: principalId } : {}),
        },
      }
    },
  },
})

evlogRuntimeContext 在受追踪轮次之外会返回 undefined,展开它不会添加任何内容。PostHog 是唯一一个同时使用 events 的注册表项目;其余项目只需要它们的 span processor。在合并时,请保持每个生成的文件处于打开状态:环境变量和导出器选项是值得原样复制的部分。

跨度和宽事件通过属性而不是 trace ID 保持关联:agent turn 没有可继承的入站 traceparent,因此由 evlog.request_id 负责传递关联信息。

**PostHog 会丢弃 evlog.*。**它的导出器会转发完整的 span,但服务端将其转换为 $ai_generation / $ai_span 事件时,只保留以 posthog_ 为前缀的属性,并去掉该前缀。要在 PostHog 中进行关联,请在能够保留下来的名称下重复这些 ID,因为 posthog_evlog_request_id 会作为 evlog_request_id 到达。其他 OTLP 后端会原样接收 evlog.*

eve 只会将运行时上下文应用到步骤 span,因此其下的生成 span 不会继承任何内容。在 PostHog 中,这意味着成本会出现在没有身份和环境信息的事件上。需要使用一个 span processor,将当前 turn 的 posthog_* 属性复制到每个子 span 上,才能弥合这一缺口。

要反向完成关联,请将 agent 自身的宽事件发送到 PostHog Logs,并将 drain 指向同一个主体:

agent/hooks/evlog.ts
import { createPostHogDrain } from 'evlog/posthog'
import { defineEvlogHook } from 'evlog/eve'

export default defineEvlogHook({
  drain: createPostHogDrain({ distinctIdField: 'eve.caller.principalId' }),
})

这样,轮次就会进入与其生成的 LLM 追踪相同的 PostHog 用户。请参阅 PostHog 适配器

生产环境

长时间运行的 eve agent 应关闭终端美化打印,并使用非阻塞的 drain:

agent/hooks/evlog.ts
import { defineEvlogHook } from 'evlog/eve'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const drain = createDrainPipeline({ batch: { size: 50, intervalMs: 5000 } })(
  createAxiomDrain(),
)

export default defineEvlogHook({
  init: {
    env: { service: 'my-agent', environment: 'production' },
    pretty: false,
    sampling: { rates: { info: 10 } },
  },
  drain,
  maxSessions: 256,
})
关注点建议
终端输出init.pretty: false — 美化打印仅适用于本地开发
Drain 延迟批处理或异步 HTTP drains;绝不要因为 I/O 阻塞 turn
Head 采样init.sampling.rates — eve 每个 turn 只发出一个事件,而不是每个 token
内存maxSessions(默认 256)会驱逐最久未使用的空闲会话状态
负载Hook 处理器对每个流事件的复杂度为 O(1);成本主要取决于你的 drain

选项

选项描述
init在首次调用钩子时传递给 initLogger()
drain / enrich / keep / plugins与 HTTP 集成相同(插件
message'omit'(默认)、'preview''full' — 记录多少用户消息和代理响应内容
messagePreviewLength'preview' 模式下保留的字符数(默认值为 500
sessionEvent设为 true 时,每个会话额外生成一个事件,汇总该会话的各轮交互
cost / model备用令牌定价(来自 evlog/aiModelCost)→ ai.estimatedCost,仅在 eve 未报告成本时使用
maxSessions用于上下文传递的内存中会话上限(默认值为 256
include / exclude对轮次路径(/sessions/*/turns/*)应用类似路由的过滤器

尾部采样

使用 keep 强制保留带有失败工具或高 token 使用量的轮次:

agent/hooks/evlog.ts
export default defineEvlogHook({
  drain: createAxiomDrain(),
  keep: (ctx) => {
    const ai = ctx.context.ai as {
      inputTokens?: number
      outputTokens?: number
      tools?: Array<{ success: boolean }>
    } | undefined
    const totalTokens = (ai?.inputTokens ?? 0) + (ai?.outputTokens ?? 0)
    if (totalTokens > 10_000) ctx.shouldKeep = true
    if (ai?.tools?.some(t => !t.success)) ctx.shouldKeep = true
  },
})

审计日志

结合 审计日志:通过 init.plugins 或全局插件注册 auditEnricher(),并在人类审批门触发时在工具内调用 log.audit()。工具拒绝会在 action.result 中显示,status: "rejected"

本地运行

git clone https://github.com/evloghq/evlog
cd evlog
pnpm install
pnpm example eve

examples/eve 项目是一个客服退款助手(Clearbill SaaS):查询客户 → 查询订单 → 发起退款,当金额超过 100 美元时需要 eve 审批。运行 pnpm example eve,打开 **

接下来阅读什么