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

这是《Agent全栈开发实战》的第 10 篇。整个系列以 catbuddy(一个本地优先的 AI 编程助手,约 3.6 万行 TypeScript)为案例拆解 harness 设计。上一篇给 Agent 循环加了一套 Hook,让外部能力可以在模型调用、工具执行等节点接进来,却不用修改核心循环。这篇看它的第一个实际用途:接入 Langfuse,把 Agent 每一轮到底做了什么看清楚。


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

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

是模型响应慢?

是 Agent 读了太多文件?

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

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

text 复制代码
用户:帮我修改配置文件

第 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 在此基础上还会关心:

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

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

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

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

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

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

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

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

四个概念放在一起就是:

text 复制代码
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 里,接入代码只有这一小段:

ts 复制代码
const hook = this._langfuseClient?.enabled
  ? new LangfuseAgentHook({
      client: this._langfuseClient,
      sessionKey: ctx.sessionKey,
      workspace: this.workspace,
      model: this.model,
      userMessage: ctx.msg.content,
    })
  : undefined

await this.runner.run({ ...spec, hook })

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

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

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

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

在 Langfuse 里怎么找到"慢"的原因

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

如果 Langfuse 展示的数据是:

text 复制代码
第 1 轮 Generation       420ms
read_file Span             8ms
第 2 轮 Generation       510ms
grep Span                 20ms
第 3 轮 Generation       380ms
exec Span               5200ms
第 4 轮 Generation       430ms

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

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

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

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

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

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

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

ts 复制代码
override async onStream(): Promise<void> {
  if (!this.firstTokenTime && this.generationStartTime) {
    this.firstTokenTime = new Date()
  }
}

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

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

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

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

Token 和成本是怎么记的

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

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

text 复制代码
inputTokens:发给模型的 token
outputTokens:模型生成的 token

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

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

ts 复制代码
const inputCost = inputTokens / 1_000_000 * price.input
const outputCost = outputTokens / 1_000_000 * price.output

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

当前实现先做数据缩减:

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

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

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

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

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

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

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

CatBuddy 怎么自托管 Langfuse

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

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

部署入口是:

bash 复制代码
pnpm deploy:langfuse

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

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

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

Langfuse 挂了,不能拖垮 Agent

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

CatBuddy 在几处做了隔离:

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

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

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

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

text 复制代码
Trace
  ├─ Generation 1
  ├─ read_file Span
  ├─ Generation 2
  └─ edit_file Span

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

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

text 复制代码
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 的眼睛。

下一篇:Langfuse 的链路里已经出现了子代理。接下来正式拆开它:SubagentManager 怎么派生子任务、限制并发、回收结果,以及主 Agent 怎么知道这些子任务正在做什么。

相关推荐
奈斯先生vector7 小时前
遗留系统不是让 AI 重写一遍:代码智能体驱动的行为保护式现代化
ai编程
MomentYY7 小时前
RAG 图检索&多跳推理:有些答案需要“顺藤摸瓜”
人工智能·agent·ai编程
爬楼的猪8 小时前
跟着AI Agent学powershell
windows·ai编程
程序员黑豆8 小时前
Java变量详解:从入门到精通
java·前端·ai编程
用户3126874877208 小时前
用 Agent 帮我买房:小区数据采集、测评图文生成全流程
ai编程
爱吃的小肥羊9 小时前
用时1.5天,Claude突破了黎曼猜想的新纪录
aigc·openai·ai编程
月弦笙音9 小时前
为什么程序员大多都拥抱 AI,而音乐人却抗拒并隔离 AI 音乐池?
ai编程
朦胧之9 小时前
AI 开发
ai编程
倾听醉梦语9 小时前
React/Vite/Next.js 前端开发工具 SpotPatch:点击页面元素精准定位 JSX/TSX 源码
javascript·react·ai编程·vite·next.js·前端开发工具
zandy101110 小时前
8款主流编程软件,五个维度深度解析——2026年AI编程工具技术选型
agent·ai编程