三次错误结论之后:Langfuse 可观测性调试实录

上周我花了大概两天时间调一个问题:Langfuse 上看不到前台 agent 的 trace。听起来很简单对吧——要么没发出去,要么发错地方了。但实际排查过程让我连续得出三个错误结论,每一个都看起来合情合理,每一个都是错的。

这篇文章记录的就是这段经历。不是教程,是一个调试故事。

第一个坑:以为是部署没 flush

最早的怀疑方向很直觉——新代码部署后 trace 没出现,那八成是 OTEL exporter 的数据还在内存里,进程退出时没来得及 flush。我甚至已经开始写修复方案了。

然后被自己打脸。仔细一查,这段代码根本没部署到生产环境过。新代码还躺在本地。第一个假说就这么废了,浪费了几个小时。

第二个坑:environment 标记全是 development

回头看 Langfuse 上已有的 trace,发现一个诡异的事:所有生产环境的 trace 都被标记成了 development。这一下子解释了很多——如果 environment 过滤器设成 production,那当然什么都看不到。

根因是 bun build 的编译期行为。我们用 Docker 多阶段构建,builder 阶段没设 NODE_ENV。bun build 在打包时会把 process.env.NODE_ENV 内联成编译期的值,而编译期这个变量是空的,最终被 fallback 成了 development。整个产物就冻结了这个错误值,运行时怎么设环境变量都没用。

修复倒是简单:在 Dockerfile 的 builder 阶段加一行 ENV NODE_ENV=production。但这只解决了 environment 标签问题,trace 丢失的核心问题还在。

第三个坑:ROOT_CONTEXT 假说

到这一步我已经有点急了。翻了一圈 OpenTelemetry 的文档,怀疑是 ROOT_CONTEXT 的使用方式导致 span 没正确关联到 trace 上。逻辑链条很完整,我甚至画了图来说明 context propagation 是怎么断的。

这时候做了一件后来证明非常有价值的事:拉了一个 review agent 来对抗性审查我的结论。这个 agent 直接把假说打回来了——ROOT_CONTEXT 只影响 span 的父子关系,不影响 span 是否被导出。span 只要 .end() 了就会进 exporter 队列,跟 context 没关系。

三振出局。

真正的根因:span 永远没有 .end()

最后发现问题出在一个很隐蔽的地方。前台的常驻 agent 使用了 endOnExit: false 的 span 配置。在正常的长驻进程里这没问题——span 会在某个时刻被手动结束。但我们的场景是 --resume 多轮模式:进程每一轮对话结束后退出,下一轮再重启。

endOnExit: false 意味着进程退出时 span 不会自动 .end()。而 OpenTelemetry 的 exporter 只在 span .end() 之后才会把它放进导出队列。进程退了,span 没结束,数据还在内存里,直接蒸发。

修复方案是改成 per-turn span:每轮对话创建一个新 span,轮次结束时显式 .end(),确保每轮数据都能被导出。

另一次丢 trace:BatchSpanProcessor 的 flush 窗口

类似的问题后来又出现过一次,但根因不同。这次 span 确实 .end() 了,但数据还是丢了。

BatchSpanProcessor 默认每 5 秒批量导出一次。如果进程在两次 flush 之间退出,已经 .end() 但还没来得及发送的 span 就没了。OTEL SDK 提供了 forceFlush() 方法,但我们的 graceful shutdown 流程里没调它。

修复是在 tracing 模块里暴露一个 flushTracing() 函数,在进程退出前 await 它。很小的改动,但不知道这个机制的话根本不会想到。

我从中学到的东西

OpenTelemetry 的心智模型其实不复杂,但有几个地方跟直觉不一样。span 的生命周期是显式的——创建不等于记录,.end() 才是触发导出的动作。BatchSpanProcessor 的名字已经说了,它是批处理的,进程退出时不会自动帮你把剩余的数据发出去。这些东西文档里都写了,但你不踩一次坑不会真的记住。

另一个收获是关于调试方法论的。我在同一个问题上连续错了三次。每一次的推理过程都是自洽的,每一次都能写出一份看起来很专业的分析报告。但自洽不等于正确。

第三次被打回来之后我开始用对抗性 review 的方式——把自己的结论交给一个专门找漏洞的 agent 去审查。这个 agent 不关心你的推理多漂亮,只关心证据链是否完整、是否有反例。说实话,被自己的 agent 打脸的感觉并不好。但它确实把我从第三个错误结论里拉了出来。

可观测性系统的调试有一个特别讨厌的性质:你用来观察问题的工具本身可能就是问题的一部分。trace 丢了,你去 Langfuse 上看——Langfuse 告诉你没有 trace。这是因为真的没产生,还是产生了但没送到?还是送到了但被过滤掉了?每一层都可能出问题,而且每一层的失败模式都是一样的:你在界面上什么都看不到。

这大概就是为什么这类问题特别容易产生错误归因。表象相同,根因不同,你很容易被第一个说得通的解释吸引住。

SayCraft 是我正在做的产品,一个用对话构建 web 应用的工具。这些调试经历都发生在它的 AI agent 可观测性建设过程中。如果你对这个项目感兴趣:saycraft.ai

感谢您的收看 祝你天天开心~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇