详情

首页手游攻略 10|Langfuse 全链路追踪:如何看清一个 Agent 到底在做什么

10|Langfuse 全链路追踪:如何看清一个 Agent 到底在做什么

佚名 2026-08-24 09:51:56

有一次,我让 CatBuddy 改一个配置文件。

文件不大,改动也不复杂,但界面转了十几秒才回来。最后文件改对了,回答也正常。问题是:这十几秒到底花在哪?

是模型响应慢?

是 Agent 读了太多文件?

还是某条 Shell 命令一直没有结束?

如果是普通接口,我大概会先看请求耗时和错误日志。但 Agent 不是“一次请求模型,一次返回结果”。它可能在模型和工具之间来回很多轮:

用户:帮我修改配置文件第 1 轮 LLM:我得先看看文件→ read_file第 2 轮 LLM:还要确认这个配置在哪里使用→ grep第 3 轮 LLM:可以修改了→ edit_file第 4 轮 LLM:检查结果,回复用户

用户只发了一条消息,内部却跑了四次模型、三个工具。最终回答里不会告诉你每一步用了多久,也不会告诉你哪一轮消耗了最多 token。

这就是 Agent 可观测性要解决的问题:不是只知道它成功或失败,而是能把它刚才的思考和行动过程重新展开。

CatBuddy 选择用 Langfuse 记录这条过程。它会把一次用户请求拆成模型调用、工具执行、token、成本和质量指标,让原本藏在循环里的行为变成一条可以展开、筛选和比较的链路。

Langfuse 到底是什么

如果你用过后端链路追踪,可以把 Langfuse 理解成一套专门面向 LLM 应用的 tracing 系统。

普通链路追踪会记录:接口 A 调了服务 B,服务 B 又查了数据库 C,每一步用了多久。

Langfuse 在此基础上还会关心:

  1. 调用了哪个模型;
  2. 给模型传了什么;
  3. 模型返回了什么;
  4. 输入和输出用了多少 token;
  5. 这一轮调用了哪些工具;
  6. 从发起请求到第一个字返回用了多久;
  7. 整个任务大概花了多少钱;
  8. 最终结果好不好。

它不是模型,也不会替 Agent 做决策。它只是站在旁边,把 Agent 的执行过程记录下来,再提供查询、筛选和统计界面。

第一次看 Langfuse 时,最容易被 Trace、Generation、Span 这些词绕晕。先别背定义。继续看前面那次“修改配置文件”的请求,它在 Langfuse 里大致会长成这样:

现在再解释术语就简单了。

Trace 是用户的一次完整请求。从消息进入 CatBuddy,到最终回答结束,都属于同一条 Trace。

Generation 是一次真正的 LLM 调用。Agent 跑了四轮,就会有四个 Generation。每一个都可以记录模型、输入、输出、token 和耗时。

Span 是链路中的普通步骤。read_filegrepedit_file 不是模型调用,所以用 Span 记录它们的参数、结果、状态和耗时。

还有一个 Session,表示一段连续对话。用户先让 CatBuddy 分析代码,接着让它修改,最后让它跑测试,这三条 Trace 可以放进同一个 Session。

四个概念放在一起就是:

Session:一段对话└─ Trace:用户的一次请求 ├─ Generation:一次 LLM 调用 └─ Span:一次工具或普通步骤

Langfuse 官方的数据模型也是按 Session、Trace 和 Observation 组织的;Generation 与 Span 都属于 Observation,也就是一条 Trace 中可以被观察的具体步骤。Langfuse 数据模型

CatBuddy 怎么把这条链路记下来

上一章讲的 Hook 在这里真正派上了用场。

AgentRunner 每跑一轮,都会经过几个固定节点:调用模型前、收到流式内容时、执行工具前、这一轮结束后。LangfuseAgentHook 在这些节点收集数据,再写进 Langfuse。

Agent 运行到这里LangfuseHook 做什么
一轮开始前创建一个 Generation
收到第一个流式字符记录首字到达时间
模型要求调用工具记录工具名和本轮 token
工具执行完成为每个工具创建 Span
最终回答完成汇总整条 Trace

loop.ts 里,接入代码只有这一小段:

const hook = this._langfuseClient?.enabled? newLangfuseAgentHook({client: this._langfuseClient,sessionKey: ctx.sessionKey,workspace: this.workspace,model: this.model,userMessage: ctx.msg.content,}): undefinedawaitthis.runner.run({ ...spec, hook })

Langfuse 开启,就创建 LangfuseAgentHook;没开启,就传 undefined

AgentRunner 不知道背后接的是 Langfuse。它只知道在生命周期节点通知 Hook。这样哪天换追踪平台,或者某个用户不允许上传追踪数据,都不用改模型与工具循环。

这个设计还有一个实际好处:子袋里也能复用。

CatBuddy 的子袋里既要把状态同步给 UI,又要写 Langfuse。CompositeHook 会把状态 Hook 和 Langfuse Hook 组合起来。一轮执行结束,两个 Hook 分别处理自己的事情,谁也不用塞进 AgentRunner

在 Langfuse 里怎么找到“慢”的原因

回到那条用了七秒的请求。

如果 Langfuse 展示的数据是:

第 1 轮 Generation 420msread_file Span 8ms第 2 轮 Generation 510msgrep Span 20ms第 3 轮 Generation 380msexec Span 5200ms第 4 轮 Generation 430ms

问题就很清楚了:不是模型慢,而是 exec 执行了五秒多。

这比一个 totalTime=7s 有用得多。

不过,Generation 的总耗时仍然不够。流式模型还有一个很影响体感的指标:TTFT,Time to First Token,也就是首字延迟。

假设两个请求都用了八秒:

请求 A:300ms 后开始输出,持续生成到第 8 秒请求 B:7 秒没有动静,最后 1 秒突然全部输出

总耗时一样,用户感受完全不同。

CatBuddy 在 onStream() 第一次收到内容时记下时间:

overrideasynconStream(): Promise<void> {if (!this.firstTokenTime && this.generationStartTime) {this.firstTokenTime = newDate()}}

Generation 结束后,用首字时间减去开始时间,就得到 ttftMs

于是一次慢请求可以继续拆成:

Trace 总耗时高 整个任务慢Generation 耗时高某轮模型慢TTFT 高模型排队、网络或超长输入Tool Span 耗时高 某个工具慢Generation 数量多Agent 绕了太多轮

这才是“可观测”的意义:不是收集一个数字,而是能沿着数字找到下一层原因。

Token 和成本是怎么记的

Agent 的成本不是一条模型调用的成本,而是整条 Trace 中所有 Generation 的成本总和。

CatBuddy 会读取 Provider 返回的真实 usage:

inputTokens:发给模型的 tokenoutputTokens:模型生成的 token

每一轮单独记录,最后再汇总到 Trace。这样可以看到某次任务到底是输入上下文太大,还是模型输出太长。

美元成本则根据模型价目表估算:

const inputCost = inputTokens / 1_000_000 * price.inputconst outputCost = outputTokens / 1_000_000 * price.output

如果模型名能在本地价目表里找到,就按对应价格计算;找不到,就用默认单价粗估,并标记为 estimated

这里有个容易误解的地方:token 是 Provider 返回的使用量,美元成本只是按当前价目表换算出来的估值。 模型厂商会调价,缓存 token、图片 token、批量折扣也可能有不同计费规则,所以 Langfuse 里的成本适合做趋势和异常排查,不应该直接当财务账单。

有了这些数据后,可以回答以前很难回答的问题:

  1. 为什么这个会话特别贵;
  2. 哪个模型平均每个任务成本更高;
  3. 改了 system prompt 后,输入 token 增加了多少;
  4. 子袋里并发缩短了时间,但成本增加了多少;
  5. 哪些请求属于明显的成本异常点。

Langfuse 本身也支持对 Generation 记录不同类型的 usage 和 cost,不只局限于输入、输出两类 token。Langfuse Token 与成本追踪

看见过程之后,还要判断结果好不好

到这里,Langfuse 已经能告诉我们 Agent 做了什么、用了多久、花了多少钱。

但它还不能回答一个更重要的问题:任务做对了吗?

所有工具都执行成功,不代表修改就是正确的。Agent 只跑了两轮,也不代表它比跑四轮的 Agent 更好。这个时候要用到 Langfuse 的 Score。

Score 可以理解成挂在 Trace 上的一张成绩单。

CatBuddy 当前会上报三个运行指标:

tool-success-rate工具成功率iterations Agent 迭代次数response-latency 整轮响应时间

这些指标能发现执行异常,但还不是真正的任务质量。下一步可以继续加入:

user-feedback用户点赞或点踩tests-passed 修改后测试是否通过typecheck-passed TypeScript 检查是否通过task-completed 任务是否真的完成

前两个运行命令就能得到,不需要再调一个模型。像回答是否相关、解释是否完整这类主观问题,才适合让另一个 LLM 按明确规则评分,也就是常说的 LLM-as-a-Judge。

Langfuse 的 Score 可以来自程序、用户反馈、人工标注或 LLM 评估,并且能在 Dashboard 中按模型、版本、Prompt 继续比较。Langfuse Scores

走到这一步,Langfuse 就不只是“出问题时看一眼”的调试工具了。它开始回答模型选型和版本迭代问题:

模型 B 便宜了 30%,任务成功率有没有下降?新 Prompt 让迭代次数减少了,用户反馈有没有变好?加了子袋里以后速度快了,工具失败率是否升高?

把对话和代码上传到追踪平台,安全吗

这是 CatBuddy 接 Langfuse 时最不能绕过去的问题。

CatBuddy 的特点是代码和文件操作都在本地。可如果为了观察 Agent,把完整对话、工具参数和文件内容原样传到云端,那“本地优先”就只剩一句口号了。

当前实现先做数据缩减:

  1. Generation 只保留最近 20 条消息的摘要;
  2. 单条消息最多保留 500 个字符;
  3. 最终输出、reasoning、工具参数和结果都有限长;
  4. 观测需要的是排障线索,不是复制一份完整会话。

缩减之后,还要经过 maskSensitiveData() 脱敏:

对象里名为 secretpasswordtokencredential 等字段会直接替换;字符串中长得像 API Key、JWT、GitHub token 或 Bearer token 的内容也会打码。

但正则脱敏不是万能的。它能识别已知格式,识别不了任意源码中的商业秘密,也不能保证覆盖每种内部凭证。

所以敏感项目还有两个选择。

一个是只上传耗时、token、模型和状态,不上传输入输出。另一个是自托管 Langfuse,把追踪数据留在自己的服务器里。

Langfuse 官方同样建议:如果敏感数据不能离开应用边界,应当在客户端发送前完成脱敏;服务端脱敏只能作为第二道防线。Langfuse 数据脱敏

CatBuddy 怎么自托管 Langfuse

仓库里的 langfuse-docker 已经准备了一套自托管环境:

Langfuse Web 页面和 APILangfuse Worker异步处理追踪数据PostgreSQL 元数据ClickHouse Trace 与统计数据Redis队列和缓存MinIO对象存储

部署入口是:

pnpm deploy:langfuse

应用侧配置服务地址和密钥:

配置 / 环境变量作用
LANGFUSE_ENABLED是否启用
LANGFUSE_PUBLIC_KEY项目公钥
LANGFUSE_SECRET_KEY项目密钥
LANGFUSE_BASE_URLCloud 或自托管地址

自托管的好处是数据位置可控,但不代表部署完就安全了。访问权限、HTTPS、备份、数据保留时间和版本升级仍然要自己负责。Langfuse 自托管文档

Langfuse 挂了,不能拖垮 Agent

可观测性是旁路能力。它可以失效,但不能让用户的任务跟着失败。

CatBuddy 在几处做了隔离:

  1. 没启用或没有密钥时,不创建 Langfuse Hook;
  2. 创建 Trace、Generation 或 Span 失败,只记录 warning;
  3. Score 异步上报,失败不阻塞回答;
  4. SDK 按批次发送,减少主流程等待;
  5. 应用退出时 flush,尽量把缓冲区里的数据发完。

这也是上一篇为什么先讲 Hook,再讲 Langfuse。只有可观测能力和核心循环真正解耦,追踪平台出问题时,Agent 才能继续工作。

当前实现还有一个可以继续改的地方

CatBuddy 现在直接在 Trace 下创建 Generation 和工具 Span:

Trace├─ Generation 1├─ read_file Span├─ Generation 2└─ edit_file Span

靠名称和时间顺序可以看懂,但层级还不够清楚。

更理想的结构是给每一轮加一个 agent-step

Trace├─ Agent Step 1│├─ Generation│└─ read_file Span└─ Agent Step 2 ├─ Generation └─ edit_file Span

这样能直接看出“哪一轮模型决定调用哪个工具”,也方便以后按 Agent Step 统计耗时和质量。Langfuse 的 tracing best practices 也建议把 Generation 和它触发的工具调用放在同一个编排 Span 下,而不是全部平铺在 Trace 根节点。Langfuse Tracing 最佳实践

这不是当前功能的阻塞问题,但它是从“能看到数据”走向“Trace 结构准确”的下一步。

回到开头那次用了十几秒的请求。

打开 Langfuse 中对应的 Trace 后,我看到几轮模型调用都不慢,真正耗时的是一条 exec Span。问题不在 Provider,也不在上下文,而是命令本身。

如果只看最终回答,这次任务没有任何异常;只有把 Trace 展开,才会发现时间究竟消失在哪一步。Langfuse 的价值就在这里:它把 Agent 内部的模型和工具循环,从一个黑盒变成一条可以解释的工程链路。

这篇最需要记住的不是那些英文名词,而是这条关系:一次对话属于 Session,一次用户请求是一条 Trace,每次模型调用是 Generation,每个工具步骤是 Span,结果好不好再用 Score 衡量。

当这些数据通过 Hook 从 Agent 生命周期里自然产生,我们才真正拥有了一双能看清 Agent 的眼睛。

点击查看更多
推荐专题
热门阅读