Skip to content
Zegging's Tech Blog
Go back

Langfuse 前端显示和实际存储不一致,让我白排查了一小时

起因

开发过程中合并了一波代码,推上去之后发现 CI 挂了——Langfuse(v3.196.0)上的测试集报告 tavily_search 工具调用失败率 100%

点进 trace 看错误详情,Pydantic 校验报错:

2 validation errors for TavilySearchInput
include_domains
  Input should be a valid list [type=list_type, input_value='[]']
exclude_domains
  Input should be a valid list [type=list_type, input_value='[]']

'[]' 是字符串,不是数组。谁传的字符串?

看 Langfuse UI,被误导了……

打开 Langfuse,找到对应的 trace,点进 tavily_search 的 generation。UI 上展示的 tool call args:

{
    "query": "5x6 bathroom ideas ...",
    "include_domains": [],
    "exclude_domains": [],
    "include_images": false,
    "time_range": "month",
    "topic": "general",
    "start_date": "2025-01-01",
    "end_date": "2026-06-24"
}

看起来完全正确。 include_domains 是空数组 []include_images 是布尔 false。参数没有问题,那为什么 Pydantic 还报错?

这次合并代码升级了几个依赖版本,Langfuse 上 tool call input 又显示无误,我自然怀疑是依赖升级导致的——Pydantic 版本不兼容?LangChain 的 tool binding 逻辑变了?……反正方向是完全跑偏了。

抓包请求

绕了一圈,通过写了单元测试单独验证 Pydantic 校验逻辑,发现没问题,和依赖升级没有关系。那就只能看 LLM 到底传了什么了。项目里本身就开发了一个 debug HTTP client,注入到 LLM chat model 里捕捉了一下 OpenAI API 的原始请求。发现实际上 LLM 的 tool call arguments 本身就有问题:

{
    "include_domains": "[]",     // ← 字符串 "[]",不是数组 []
    "exclude_domains": "[]",     // ← 字符串 "[]",不是数组 []
    "include_images": "false",   // ← 字符串 "false",不是布尔 false
    "time_range": ""             // ← 空字符串,不是 null
}

LLM 原始输出就是错的。 gpt-5.4 把 include_domainsexclude_domains 序列化成了 JSON 字符串 "[]" 而非数组 []

这个错的本质原因是公司 LLM gateway 的变更 bug 导致对于 anyOf 这样的结构不支持默认回退到了 str。模型在 5 次重试中尝试了不同变体("[]", ",", ":[],", ":[]"),但始终未正确序列化为真正的数组类型。所以工具调用的成功率变成了 0%。

Langfuse API 验证

但 Langfuse UI 上明明显示的是 [](数组)。

langfuse显示数组而非字符串

为了确认后端到底存了什么,直接用 Public API 查原始数据。API 返回的原始 tool_calls[].args

{
    "include_domains": "[]",
    "exclude_domains": "[]",
    "include_images": "false",
    "time_range": ""
}

后端存储的是字符串 "[]",和 HTTP 抓包完全一致。 Langfuse 前端在渲染时对 JSON 做了美化,把字符串 "[]" 显示成了数组 [],把 "false" 显示成了 false …… 在最开始坑了哥们一波。

Langfuse UI 显示:   include_domains: []          (看起来是数组)
Langfuse 后端存储:  include_domains: "[]"         (实际是字符串)
HTTP 原始数据:      include_domains: "[]"         (实际是字符串)
Pydantic 收到的:    include_domains: '[]'         (实际是字符串)

总结

实际上这个问题是有经验的,但是在被 langfuse 的前端页面显示误导以后先入为主觉得是依赖升级导致的。还好绕的不远,个把小时排查清楚了。类似问题请求的抓包是最直接的证据,开发过程中一个用于 debug 的 http_async_client 非常好用。


Share this post on:

Previous Post
什么是 Agent Harness?
Next Post
如何编写一个 skill