跳转到内容

输入关键词开始搜索文档。

可观测性

最后更新 查看 Markdown 源文件

Flue 将其智能体所做的一切——模型轮次、工具调用、结构化日志、压缩和结算——作为类型化的运行时事件发出,你的应用可以在进程中观察它们。这个接口与聊天 UI 读取的按对话消息流是分开的,后者属于路由和 Flue 智能体 SDK。本指南涵盖两个接口及何时使用哪个、使用 observe() 订阅、事件流包含的内容、模型轮次的 token 使用量和提供商诊断、工具活动和日志、将遥测导出到 Sentry、Braintrust 和 OpenTelemetry,以及智能体活动如何在 Cloudflare 的平台可观测性中呈现。

两个事件接口

Flue 在两个不同的接口上暴露智能体活动:

  • 对话流是产品接口:一个对话的持久的、可渲染的消息、数据部分和结算,通过 HTTP 使用 createFlueClient(...) 的 observe() / history() 消费。路由涵盖它。
  • 运行时事件流是操作接口:进程中每个智能体的实时活动——模型请求、工具执行、日志、token 计数、失败——在进程中使用 @flue/runtime 的 observe() 消费。该流是本指南的主题。

两个 API 共享名称但不共享形状:SDK 客户端的 observe() 维护一个对话的具体化消息状态,而运行时的 observe() 传递原始活动事件。遥测、计量和错误报告属于运行时流。接口共享关联标识符——对话消息的 submissionId 匹配其提交产生的运行时事件。

使用 observe() 订阅

来自 @flue/runtime 的 observe() 为当前进程中所有智能体活动注册一个全局订阅者。在启动时注册一次,在 app.ts(或 app.ts 导入的模块)的模块顶层:

src/app.ts
import { observe } from '@flue/runtime';

observe((event) => {
  if (event.type === 'submission_settled' && event.outcome === 'failed') {
    console.error(
      `[${event.agentName}] submission ${event.submissionId} failed:`,
      event.error?.message,
    );
  }
});

订阅者接收每个智能体的每个事件——直接提示、调度的工作、子智能体任务和工具活动。observe() 返回一个取消订阅函数,但遥测订阅者通常注册一次且永不移除自己;返回值用于测试和动态连接。

订阅者的三条规则:

  • 保持轻量。 订阅者在事件发出路径上同步运行。根据 event.type 分支,对于不消费的活动立即返回,将大量异步工作排队而不是阻塞发出。
  • 将事件视为只读。 每次传递都是一个分离的、冻结的观察;订阅者永远无法改变其他订阅者或运行时看到的内容。
  • 故障被隔离。 抛出异常的订阅者被记录并跳过——它永远不会停止智能体或其他订阅者。返回的 promise 被观察是否有拒绝但不会被等待。

订阅是隔离范围且仅实时的:它从注册时刻起看到当前进程中发出的活动,没有持久回放,没有跨进程聚合。在 Node.js 上一个进程托管所有智能体,因此一次注册看到一切。在 Cloudflare 上,每个智能体对话在自己的 Durable Object 隔离中运行——从 app.ts 注册的订阅者在每个隔离中运行并仅看到该隔离的活动。一个与 setProvider() 共享的放置注意事项:flue run 仅加载智能体模块,永不加载 app.ts——当订阅者也必须在 CLI 下运行时,在智能体模块中注册。

流包含的内容

每个事件携带事件格式版本(v: 3)、按上下文的 eventIndex、timestamp,以及适用于它的关联字段,包括 agentName、conversationId、instanceId、submissionId、operationId、turnId 和 taskId。事件族:

事件活动
agent_start、agent_end、idle智能体循环生命周期。
submission_settled持久提交达到 completed、failed 或 aborted——可靠的终结信号。
operation_start、operation提示、技能、任务、shell 和紧凑操作边界,包含持续时间和汇总使用量。
turn_start、turn_request、turn、turn_messages模型轮次(见下文)。
message_*、text_delta、thinking_*实时消息和推理进度。
tool_start、tool工具执行,通过 toolCallId 关联。
task_start、task子智能体任务委托,包含结果、错误状态和持续时间。
compaction_start、compaction上下文压缩,包含消息计数和使用量。
log你的工具和钩子写入的结构化日志(见下文)。

流式增量是实时进度信号,不是权威消息状态;助手 message_end 事件携带完成的消息。嵌套错误不一定会使包含它们的工作失败——智能体可以从失败的轮次或工具调用中恢复——因此对 submission_settled 结果发出告警,并将嵌套的 isError 事件读取为诊断上下文。

实时流的两个属性超出了持久记录的范围:

  • 实时观察携带额外细节。 observe() 将每个事件作为 FlueObservation 传递——事件加上仅实时字段,如规范化的工具参数、有效结果和分类的 errorInfo(包括抛出点堆栈)。这些面向导出器的字段永不被持久化或回放。
  • turn_request 仅限进程内。 它包含完整的模型可见请求——提供商标识、设置、系统提示、消息和工具——并传递给 observe() 订阅者,但永不被持久化或通过 HTTP 提供。

事件参考记录了每个事件的字段和哪些负载是稳定契约。

Token 使用量

每个完成的模型调用发出一个 turn 事件,其 request 总结了发送的内容(提供商、请求的模型、API、设置),其 response 携带结果——输出、完成原因和 usage:

usage 字段含义
input、output发送给模型的 token 和模型生成的 token。
cacheRead、cacheWrite读取和写入的提示缓存 token。
totalTokens所有组件的总计。
cost来自模型目录费率的估计成本,按组件加上 total。

每个智能体的 token 计量是一个单一观察者:

src/app.ts
import { observe } from '@flue/runtime';
import { metrics } from './shared/metrics.ts';

observe((event) => {
  if (event.type !== 'turn' || !event.response.usage) return;
  const { usage } = event.response;
  metrics.increment('llm.tokens', usage.totalTokens, {
    agent: event.agentName,
    model: event.request.requestedModel,
    purpose: event.purpose, // 'agent' | 'compaction' | 'compaction_prefix'
  });
  metrics.increment('llm.cost', usage.cost.total, { agent: event.agentName });
});

使用量也在更粗粒度的边界上汇总:operation 和 compaction 事件携带它们所绑定工作的聚合使用量。跨事件求和使用量时,仅求和一个级别——turn 值是叶子,汇总已经包含它们。不同级别的持续时间值以相同方式重叠,不应相加。在智能体内部,useResponseFinish() 接收整个响应的聚合使用量——这是为客户端在响应元数据上标记 token 计数的正确位置;参见事件钩子。

提供商诊断

turn 事件的 response 是规范化的——finishReason 和 error 使用 Flue 的词汇表,无论提供商。在它们旁边,当提供商附加时,响应携带白名单的原始提供商元数据:

  • providerFinishReason ——提供商在规范化之前的确切完成值(例如,Workers AI 的 tool_calls 在规范化后是 toolUse)。
  • gatewayLogId ——响应自身的 Cloudflare AI Gateway 日志 id(cf-aig-log-id),用于将特定轮次与其在网关仪表板中的条目关联。

两者仅用于遥测——它们永远不会影响执行或回放——并且仅在提供商记录它们时存在。Workers AI 提供商今天两者都附加。失败轮次的诊断观察者直接从事件中读取它们:

src/app.ts
import { observe } from '@flue/runtime';

observe((event) => {
  if (event.type !== 'turn' || !event.isError) return;
  console.error('model turn failed', {
    provider: event.request.providerName,
    model: event.request.requestedModel,
    finishReason: event.response.finishReason,
    providerFinishReason: event.response.providerFinishReason,
    gatewayLogId: event.response.gatewayLogId,
    error: event.response.error?.message,
  });
});

request.providerId 是模型说明符中的注册键;request.providerName 是语义提供商标识,当网关或自定义注册前置模型时会不同。

工具活动和日志

工具执行发出 tool_start 和 tool 事件,携带工具名称、toolCallId、持续时间、错误状态和结果——对于模型驱动的调用和编程 shell 活动都是如此。实时观察添加了规范化参数和有效结果。

对于长时间运行的工具内部的进度,工具的 run 上下文提供一个日志记录器(参见工具);生命周期钩子上下文如 useAgentStart 携带相同的 log 接口。每次调用发出一个带有级别、消息和你的属性的 log 事件——工具日志额外标记 tool 和 toolCallId,钩子日志标记写入它们的钩子。模型永远看不到日志行;它们为你的应用而存在:

src/tools/sync-crm.ts (excerpt)
async run({ data, log }) {
  log.info('sync started', { records: data.ids.length });
  const failed = await crm.sync(data.ids);
  if (failed.length > 0) log.error('sync incomplete', { failed: failed.length });
  return { synced: data.ids.length - failed.length };
}
src/app.ts
import { observe } from '@flue/runtime';
import { logger } from './shared/logger.ts';

observe((event) => {
  if (event.type !== 'log') return;
  logger.log(event.level, event.message, {
    ...event.attributes,
    conversation: event.conversationId,
  });
});

日志行是运行时事件,不是对话内容:它们永远不会出现在客户端渲染的消息中,它们只到达进程内订阅者——从观察者将它们转发到你的日志后端,或通过下面的集成之一。

选择可观测性提供商

对于生产遥测,Flue 提供与三个生态系统的集成,而不是一个内置仪表板:

  • Sentry——终结性失败作为问题,每个日志在 Sentry Logs 中,可选的 AI 追踪(默认关闭内容)。通过 flue add tooling sentry 添加。
  • Braintrust——LLM 追踪:操作作为追踪,包含模型、工具、任务和压缩 span 以及使用量。通过 flue add tooling braintrust 添加。
  • OpenTelemetry——基于标准的 GenAI span、指标和日志,适用于任何 OTel 兼容后端。将 @flue/opentelemetry 添加到你的 OTel SDK 设置。

Sentry 和 Braintrust 蓝图生成一个 app.ts 导入的源码根模块——一个像上面那样的事件桥加上提供商初始化。产生 span 的集成通过 instrument(...) 注册,它将观察者与执行拦截器配对,使 span 包装实时智能体、模型、工具和任务执行:

src/app.ts (abridged)
import { createOpenTelemetryInstrumentation } from '@flue/opentelemetry';
import { instrument } from '@flue/runtime';

instrument(createOpenTelemetryInstrumentation());

当你想要在现有应用监控器中获取失败、日志和追踪时选择 Sentry;当你想要用于检查和评估的带内容 LLM 追踪时选择 Braintrust;当你的组织标准化于 OTel 后端时选择 OpenTelemetry。它们可以组合——错误报告器和追踪器可以并行订阅。在 Cloudflare 上,每个集成按隔离导出,最终刷新是尽力而为;每个工具页面记录其目标特定行为。

Cloudflare

在 Cloudflare 目标上,智能体工作对平台自身的可观测性产品也可见,无需 Flue 端的连接。每个智能体响应作为一个平台工作单元运行——接纳立即回答,然后响应从开始到结算作为平台可以看到和测量的单次调用执行。Workers Logs 将工具和钩子日志归因于写入它们的响应,Workers Traces 每个响应捕获一个追踪——模型调用和其他子请求作为其中的 span 出现。两者都在 wrangler.jsonc 中启用;参见在 Cloudflare 上部署了解配置,Cloudflare 目标指南了解归因背后的执行模型。

智能体形状的 span 是内置的——启用追踪后,每个响应的追踪携带它们,无需连接。要自定义适配器(内容策略、编辑),在 app.ts 模块作用域自己安装一次,这会替代默认值:

src/app.ts
import { instrument } from '@flue/runtime';
import { createCloudflareTracing } from '@flue/runtime/cloudflare';

instrument(createCloudflareTracing({ content: false }));

在 flue.config.ts 中设置 tracing: false 以从构建中移除智能体追踪。

每个响应的追踪携带一个包装运行的 invoke_agent span、每个模型轮次带 token 使用量的 chat span 和每个工具调用的 execute_tool span,使用与 Cloudflare 自身智能体追踪发出的相同 OpenTelemetry GenAI 命名——Flue 智能体在 Traces 仪表板中原生可读。span 默认携带对话:输入和输出消息、系统指令,以及工具定义、参数和结果,因此你可以直接从追踪中读取智能体实际说了和做了什么。原始错误消息和堆栈追踪是例外——它们在这个后端上永不发出,失败仅记录低基数的 error.type。传递 content: false 获取无内容的 span,或传递 transform 在代码中编辑或丢弃内容;参见 createCloudflareTracing() 了解属性表面、截断契约和行为细节。

平台视图和运行时事件流是互补的,而非冗余的。Workers Observability 显示操作形状——调用、持续时间、结果、子请求,以及使用适配器时的智能体标识、使用量和对话内容;运行时事件保持全保真流:结算结果、错误详情、追踪属性无法容纳的一切。使用平台产品获取集群健康、延迟和读取对话,使用运行时流(或上面的集成)获取需要完整记录的任何内容。

保护敏感内容

运行时事件可能包含提示、系统指令、推理、工具参数和结果以及错误详情——两个追踪适配器默认都捕获对话内容。对于每个离开 Cloudflare 的导出器,使用 instrument(...) 安装一个插桩即表示同意:仅部署不会发出任何内容,但一旦该行存在,提示和工具负载就流向接收后端。Cloudflare 自身的 Workers Traces 是例外——智能体追踪内置于目标中,因此那里的开关是在账户上启用追踪,内容保留在其中。每个适配器接受相同的两个控制:content: false 完全关闭捕获,content: { transform } 是策略钩子——编辑、按 scope.contentType 丢弃,或用 truncateContent 收紧字节预算。

两个保护是无条件的:turn_request 事件永不离开进程,图像内容块永不携带原始字节(它们的 data 被替换为 IMAGE_DATA_OMITTED 哨兵值)。除此之外,每个导出器有自己的姿态:Cloudflare 适配器永不发出原始错误消息或堆栈,OpenTelemetry 适配器通过相同的内容门传递异常消息和堆栈追踪,Sentry 集成在记录标志选择加入之前将模型和工具内容排除在追踪之外,Braintrust 是带内容的并有掩码钩子。审查接收你追踪的任何内容的保留和访问控制,并明确做出数据处理决策。

后续步骤