夜雨聆风学习资料网

ARTICLE · 995909

OpenClaw * Langfuse 可观测性插件

OpenClaw * Langfuse 可观测性插件

重写了一个 OpenClaw × Langfuse 可观测性插件

最近,我想把 OpenClaw 的 Agent 执行过程接入 Langfuse。

最开始,我直接安装了 Langfuse 官方的 OpenClaw 插件:

openclaw-x-langfuse-plugin[1]

它确实可以把 OpenClaw 的 Diagnostics 数据发送到 Langfuse,也能生成 Turn、Generation、Tool、Retriever 和 Context 等 Observation。

但实际使用以后,我发现它的观测模型、输入输出完整性、Subagent 表达方式,以及可配置能力都不符合我的预期。

于是我参考 Langfuse 的另一个项目:

codex-observability-plugin[2]

重新实现了一个面向 OpenClaw Agent 生命周期的 Langfuse 插件:

openclaw-langfuse-observability-plugin[3]

开发过程比想象中曲折。中间 Codex 额度用完,我换成 WorkBuddy + HY3 继续修改,前后改了 5 次仍然没有完全修好。Codex 额度恢复后,我重新让 Codex + GPT-5.6-sol 全量检查,一次完整的排查—修复周期就把剩余问题全部找出并闭环。

这篇文章主要回答三个问题:

  1. Langfuse 官方插件为什么不符合我的目标;
  2. 重新实现的插件在架构和 Trace 模型上改变了什么;
  3. 为什么同一个复杂 Coding 问题,WorkBuddy + HY3 改了 5 次仍未闭环,而 Codex + GPT-5.6-sol 能一次性完成最终修复。

我真正想要的,不只是“把指标发到 Langfuse”

一次 OpenClaw 对话并不是一次简单的模型调用。它可能包含:

  • System Prompt、历史消息和本轮 Prompt;
  • LLM 输入、输出和 Token Usage;
  • 多个 Tool Call 及其参数、结果和错误;
  • 主 Agent 派生出的 Subagent;
  • Session、User、Environment、Tags 和业务 Metadata;
  • 乱序到达的完成事件和异常中断。

我希望在 Langfuse 中看到的是一棵忠实于 Agent 执行过程的 Trace 树:

OpenClaw Agent Run├── LLM Generation│   ├── Tool Call A│   └── Tool Call B└── OpenClaw Subagent Run    ├── LLM Generation    └── Tool Call C

这棵树里的 Input、Output、Usage、Session 和 Subagent 关系必须完整且稳定,而不是“有些运行能看到,有些字段偶尔为空”。

这也是我最终没有继续在官方插件上打补丁,而是选择重新设计的原因。

先客观看一下官方插件

我本地使用和分析的是 openclaw-x-langfuse-plugin 1.5.0。

它的核心入口不是 OpenClaw 会话生命周期 Hook,而是公共 Diagnostics Bus:

onInternalDiagnosticEvent((event) => engine.handle(event));

OpenClaw 会产生 run.started、run.completed、model.usage、tool.execution.*、context.assembled 等 Diagnostics 事件。官方插件使用其中的 W3C traceId,把同一 Turn 的事件聚合为:

Turn Root├── Context├── Generation├── Tool / Retriever└── Model Error

这个设计有几个明显优点:

  • 不需要直接注册原始会话 Hook;
  • 可以获取 costUsd 并映射为 Langfuse Cost;
  • 能把搜索、RAG、Memory 等工具分类为 Retriever;
  • 能记录 Prompt 长度、消息数量和 Context Budget;
  • 使用隔离的 OTel TracerProvider,不污染 OpenClaw 的全局 OTel 环境;
  • 针对 Diagnostics 事件的乱序、延迟和丢失设计了 Soft End 与 Reaper。

所以官方插件并不是一个粗糙的 Demo。作为“OpenClaw Diagnostics 到 Langfuse 的桥接器”,它的设计是成立的。

问题在于:Diagnostics Bridge 与我需要的 Agent Observability,并不是同一个目标。

官方插件为什么不符合我的预期?

1. Input 和 Output 只能从轨迹文件补齐

OpenClaw 不会把 Prompt、Response、Tool Arguments 和 Tool Result 等私有内容直接交给第三方 Diagnostics Listener。

因此,官方插件收到的 model.usage 主要是 Token、Cost 和模型元数据。为了补齐内容,它还要读取:

<stateDir>/agents/<agentId>/sessions/<sessionId>.trajectory.jsonl

完整链路实际上是:

Diagnostics Event→ 根据 traceId 重建 Turn→ 读取 trajectory.jsonl→ Best-effort 补齐 Prompt、Response 和 Tool I/O→ 导出到 Langfuse

这会带来几个问题:

  • 轨迹文件尚未写完时,内容可能不完整;
  • Tool 结束事件到达时,Tool I/O 可能还没落盘;
  • 文件不可访问时,Observation 仍然存在,但 Input/Output 为空;
  • 部分 Diagnostics 事件经过异步队列,压力下可能被丢弃;
  • 官方 README 明确说明,直接运行 openclaw agent 不会产生这组事件。

官方插件为此实现了延迟 Finalize、Transcript Resolver 和 Idle Reaper,但这些机制本质上是在补偿数据入口的限制。

2. 它描述的是 Diagnostics Turn,而不是 Agent 生命周期

官方插件把 Generation、Tool、Retriever 和 Context 平铺在 Turn Root 下面。这很适合回答:

这一轮 OpenClaw 产生了哪些诊断事件?

但我更想回答:

这个 Agent Run 是如何执行的?它调用了哪些工具,又派生了哪些 Subagent?

官方插件没有注册 llm_input、llm_output、agent_end、subagent_spawned 和 subagent_ended,因此没有显式的 Subagent 父子关联状态机。

3. Trace 级属性不符合我的使用方式

我希望每个 Observation 都稳定携带:

langfuse.trace.namelangfuse.session.idlangfuse.user.idlangfuse.trace.tags

官方插件当前源码只在 Root Span 上设置 Trace Name 和 Session,而且 Session 使用的是:

constTRACE_SESSION_ID = "session.id";

而 Langfuse v5 识别的 OTel 属性是 langfuse.session.id。

此外,BatchSpanProcessor 可能先导出 Generation 或 Tool。Trace 属性只写在最后结束的 Root 上,会让最终结果依赖导出顺序。

新插件把正确的 langfuse.* 属性直接写入 Root、Generation、Tool 和 Subagent 的底层 OTel Span。

4. 配置更像“连接器配置”

官方插件只提供 publicKey、secretKey 和 baseUrl。

而我的场景还需要:

  • Environment、User ID、Tags 和业务 Metadata;
  • 输入与输出独立采集开关;
  • 对凭据型字段进行脱敏;
  • 限制每个字符串的最大采集长度;
  • 使用插件专用环境变量,避免干扰其他 Langfuse 客户端。

这不仅是“多几个配置项”,而是让插件具备真实环境所需的数据治理能力。

为什么参考 codex-observability-plugin?

Langfuse 的 Codex 插件采用了另一种思路。Codex 每个 Turn 结束后触发 Stop Hook,插件读取完整 Rollout,重建:

Codex Turn├── LLM Generation│   ├── Tool Call│   └── Tool Call└── Codex Subagent Turn

它给我的启发主要有五点:

  1. Trace 应该围绕 Agent Turn,而不是围绕零散指标;
  2. Generation、Tool 和 Subagent 应该形成真实父子结构;
  3. Session、User、Tags、Environment 和 Metadata 是一等属性;
  4. 输入输出需要截断,导出失败必须 Fail-open;
  5. 测试要验证完整轨迹重建,而不只是检查某个函数是否返回对象。

但 OpenClaw 已经提供 Typed Lifecycle Hooks,所以我没有照搬 Codex 的事后 Transcript 解析,而是选择实时构建 Trace:

OpenClaw Typed Hooks→ TraceEngine 实时状态机→ 采集、脱敏与截断→ Langfuse Observation Tree→ 隔离的 OTel Batch Export

两个 OpenClaw 插件的核心区别

维度
Langfuse 官方插件
重新实现的插件
核心定位
Diagnostics Bridge
Agent Observability Plugin
数据入口
公共 Diagnostics Bus
Typed Lifecycle Hooks
Trace 主键
W3C Trace ID / Turn
Agent Run
输入输出
从 trajectory.jsonl Best-effort 恢复
从 LLM Hook 直接采集
Tool I/O
从 Transcript 补齐
从 Tool 前后置 Hook 成对采集
Subagent
无显式生命周期建模
使用 Spawned/Ended Hook 建立父子关系
Session 属性
Root 写入 session.id
所有 Span 写入 langfuse.session.id
用户与标签
不支持配置
支持 User、Tags、Environment、Metadata
数据治理
无独立开关与脱敏配置
支持采集开关、脱敏和截断
Cost
支持
当前未实现
Retriever
支持分类
当前统一建模为 Tool
Context 指标
有独立 Observation
当前未单独建模
权限要求
不需要原始 Conversation Hook 权限
需要 allowConversationAccess

这张表也说明,新插件不是在所有维度上都优于官方插件。

如果更关心 Cost、Retriever、Context Size,且不希望授予原始会话 Hook 权限,官方插件是合理选择。

如果更关心 Prompt、Response、Tool I/O、Subagent、Session 和业务属性能否形成一棵完整、稳定的 Agent Trace,Typed Hooks 方案更符合这个目标。

插曲:WorkBuddy + HY3 改了 5 次

在开发过程中,Codex 最先帮助定位了 Runtime Domain 与共享 Engine 的根因。

就在继续检查剩余问题时,Codex 额度用完了。为了不影响进度,我换成 WorkBuddy + HY3 继续修改代码。

前后改了 5 次,Trace 从完全没有变成了“基本能看到”,但始终还有问题:

  • Session ID 和 Trace Name 不稳定;
  • 某些 Generation 没有 Output;
  • Token Usage 会丢失;
  • Subagent 的 Session 归属不一致;
  • 单元测试通过,真实 Langfuse 页面仍不符合预期。

这些修改单独看都有一定合理性,例如把 Trace 属性传播到更多 Span,或者保留已经完成的 Run 等待迟到事件。

但它始终没有验证两个关键前提:

  1. Langfuse SDK 到底会保留哪些 Observation 参数;
  2. OpenTelemetry Span 在 end() 之后到底还能不能更新。

于是代码出现了“逻辑上已经赋值,真实导出却没有数据”的情况。

Codex 恢复后,为什么能一次性完成最终修复?

Codex 额度恢复后,我让 Codex + GPT-5.6-sol 对当前代码做了一次全量检查。

它没有继续从页面上的空字段逐个修,而是把链路完整展开:

OpenClaw Hook 调度→ Runtime Domain→ TraceEngine 状态机→ Langfuse Observation Wrapper→ OpenTelemetry Span→ BatchSpanProcessor→ Langfuse 最终展示

它直接检查 Langfuse SDK,确认额外属性会被过滤;又检查 OpenTelemetry SDK,确认 Span 结束后不能更新。两个错误前提一旦被推翻,前面反复出现的症状就能同时解释。

随后,它把问题重建成明确的状态机,并修正测试 Recorder,让 Mock 与真实 OTel 一样,在 Observation 结束后拒绝更新。

新增的回归测试覆盖了:

  1. 等待迟到的 llm_output;
  2. 缺失 llm_output 时超时完成;
  3. Subagent 结束事件先到时仍保留 Usage;
  4. 嵌套 Subagent 继承父 Trace 属性。

修复完成后,我在真实 OpenClaw 和 Langfuse 环境中重新验证,所有场景全部通过。

这次对比让我重新理解了 AI Coding 的差距

仅基于这次真实项目,我认为两组工具的差距不在于“会不会写 TypeScript”,而在于能否把复杂工程问题闭环。

维度
WorkBuddy + HY3
Codex + GPT-5.6-sol
问题入口
根据当前现象继续修改
从现象反推完整运行机制
上下文范围
更偏局部文件与状态
跨 Hook、SDK、OTel、测试和真实结果
依赖理解
容易基于 API 表面假设
下钻第三方源码验证前提
并发理解
更容易沿理想顺序推理
主动构造乱序、竞态和缺失事件
测试目标
让现有测试通过
让测试逼近真实运行时并覆盖故障
完成标准
代码已经修改
根因、实现、测试、构建和实测闭环

这不意味着所有任务都必须使用最强模型。单文件调整、样板代码和机械重构,更快、更便宜的模型完全够用。

但出现以下信号时,我会优先使用 Codex + GPT-5.6-sol:

  • Bug 横跨多个模块或抽象层;
  • 涉及并发、乱序或生命周期;
  • 需要理解第三方 SDK 的真实行为;
  • 测试通过,但真实环境失败;
  • 已连续修改两三次,问题仍从一个字段转移到另一个字段;
  • 需要一次完成代码、测试、文档与验证闭环。

强模型单次调用看起来更贵,但如果把反复试错、人工验证和上下文切换算进去,总成本未必更高。

写在最后

作为产品经理,之前写汇报材料、简单网页代码,都是通过workbuddy+hy3搞定的,尤其workbuddy+hy3用svg画架构图的能力非常惊艳,让汇报变得更加清晰。

近期在做轨迹数据产品,调研langfuse比较多,萌生了自己写plugin的想法,针对这种复杂程度的项目,codex的表现真的让我惊艳。

引用链接

[1]openclaw-x-langfuse-plugin: https://github.com/langfuse/openclaw-x-langfuse-plugin

[2]codex-observability-plugin: https://github.com/langfuse/codex-observability-plugin

[3]openclaw-langfuse-observability-plugin: https://github.com/ai-native-thinking/openclaw-langfuse-observability-plugin

相关学习资料