可观测性
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 导入的模块)的模块顶层:
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 计量是一个单一观察者:
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 提供商今天两者都附加。失败轮次的诊断观察者直接从事件中读取它们:
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,钩子日志标记写入它们的钩子。模型永远看不到日志行;它们为你的应用而存在:
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 };
}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 包装实时智能体、模型、工具和任务执行:
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 模块作用域自己安装一次,这会替代默认值:
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 是带内容的并有掩码钩子。审查接收你追踪的任何内容的保留和访问控制,并明确做出数据处理决策。
后续步骤
- 事件参考——完整的事件词汇表、信封字段和
observe()契约。 - 路由和智能体 SDK——你的 UI 消费的对话流。
- 智能体钩子——从智能体内部读取使用量并标记响应元数据。
- Sentry、Braintrust 和 OpenTelemetry——每个集成的设置和内容策略。
- 评估——将观察到的行为转化为评分的回归检查。