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 全量检查,一次完整的排查—修复周期就把剩余问题全部找出并闭环。
这篇文章主要回答三个问题:
Langfuse 官方插件为什么不符合我的目标; 重新实现的插件在架构和 Trace 模型上改变了什么; 为什么同一个复杂 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它给我的启发主要有五点:
Trace 应该围绕 Agent Turn,而不是围绕零散指标; Generation、Tool 和 Subagent 应该形成真实父子结构; Session、User、Tags、Environment 和 Metadata 是一等属性; 输入输出需要截断,导出失败必须 Fail-open; 测试要验证完整轨迹重建,而不只是检查某个函数是否返回对象。
但 OpenClaw 已经提供 Typed Lifecycle Hooks,所以我没有照搬 Codex 的事后 Transcript 解析,而是选择实时构建 Trace:
OpenClaw Typed Hooks→ TraceEngine 实时状态机→ 采集、脱敏与截断→ Langfuse Observation Tree→ 隔离的 OTel Batch Export两个 OpenClaw 插件的核心区别
trajectory.jsonl Best-effort 恢复 | ||
session.id | langfuse.session.id | |
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 等待迟到事件。
但它始终没有验证两个关键前提:
Langfuse SDK 到底会保留哪些 Observation 参数; 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 结束后拒绝更新。
新增的回归测试覆盖了:
等待迟到的 llm_output;缺失 llm_output时超时完成;Subagent 结束事件先到时仍保留 Usage; 嵌套 Subagent 继承父 Trace 属性。
修复完成后,我在真实 OpenClaw 和 Langfuse 环境中重新验证,所有场景全部通过。
这次对比让我重新理解了 AI Coding 的差距
仅基于这次真实项目,我认为两组工具的差距不在于“会不会写 TypeScript”,而在于能否把复杂工程问题闭环。
这不意味着所有任务都必须使用最强模型。单文件调整、样板代码和机械重构,更快、更便宜的模型完全够用。
但出现以下信号时,我会优先使用 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