1. 从“能跑通”到“看得见”:为什么模型调用远远不够
我最早接触 AI Agent 那会儿,判断一个系统好不好,标准特别朴素——能跑通就行。调一次模型,拿到回复,解析一下 JSON,塞进业务逻辑,完事。那时候觉得 Agent 嘛,不就是“大模型 + 工具调用 + 循环”这三板斧,能出结果就是胜利。
直到有一次线上出了个诡异问题:用户反馈 Agent 给出的答案驴唇不对马嘴,但日志里每一步都显示“成功”。模型返回了内容,工具调用也执行了,链路没有报错,可最终结果就是错的。我盯着那堆散落在不同文件里的print和logging.info看了整整一个下午,才拼凑出真相——模型在第一轮把用户意图理解偏了,后面每一步都在错误的前提上“正确地”执行。问题不在某一次调用,而在整条链路的语义漂移。
那次之后我才真正意识到:模型调用只是 Agent 系统的冰山一角,真正难的是让整条链路“可观测”。你知道它调了什么模型、传了什么参数、花了多少 token、走了哪条分支、工具返回了什么、中间状态怎么变的——这些信息如果散落在各处,排查问题就跟盲人摸象一样。
Langfuse 就是在这个背景下进入我的工具箱的。它解决的核心问题很明确:把 AI Agent 系统里每一次模型调用、每一次工具执行、每一轮对话状态,都变成结构化、可检索、可对比的追踪数据。不管你是用 LangChain、LangGraph、FastAPI 自己搭的 Agent,还是基于 Spring AI、Rust 生态的 Agent 框架,只要接入 Langfuse,就能把“黑盒”变成“玻璃盒”。
这篇文章适合谁看?如果你正在搭 AI Agent,或者已经搭好了但被线上问题折磨得够呛,又或者你只是想知道“可观测性”这三个字在 AI 工程里到底意味着什么,那接下来的内容应该能帮你少走不少弯路。我会从整体设计思路讲起,然后拆解核心细节、实操步骤、常见坑,最后聊聊怎么把它用出真正价值。
2. 整体设计思路:为什么是 Langfuse,而不是自己造轮子
2.1 自建日志系统的三个致命短板
在决定用 Langfuse 之前,我认真评估过“自己写一套日志系统”这条路。毕竟团队里有人觉得,不就是记录一下请求和响应嘛,写个中间件往数据库里塞不就完了。但真动手设计的时候,发现事情没那么简单。
第一个短板是数据结构化程度不够。Agent 的一次完整执行,不是一条平铺直叙的日志能描述的。它是一棵树:根节点是用户请求,下面挂着若干轮 LLM 调用,每轮调用下面又挂着工具执行、检索结果、状态更新。如果你用传统日志,要么把整棵树序列化成一个大 JSON 塞进一个字段,要么用 parent_id 自己维护层级关系。前者查不动,后者写起来容易出错。
第二个短板是缺少专门的 UI 和检索能力。排查问题时,你最需要的不是“把日志导出来 grep”,而是“按 trace_id 找到这次请求,展开看每一层的输入输出,对比两次执行的差异”。自建系统要做到这个程度,前端工作量不比后端小。
第三个短板是评测和指标统计几乎从零开始。Agent 系统上线后,你肯定想知道:平均延迟多少?token 消耗趋势如何?哪个工具调用失败率最高?这些指标如果靠手写 SQL 统计,每次加一个新维度都要改表结构。
2.2 Langfuse 的定位:不是日志,是“追踪 + 评测 + 指标”三合一
Langfuse 的聪明之处在于,它没有把自己定位成“AI 版日志系统”,而是定位成LLM 工程的可观测性平台。这个定位差异体现在三个层面:
- Trace 层级:一次完整请求对应一个 trace,trace 下面可以有多个 span 和 generation。span 表示一个操作单元(比如工具调用、检索步骤),generation 专门表示一次 LLM 调用。这种层级结构天然贴合 Agent 的执行模型。
- 评测能力:Langfuse 内置了 score 机制,你可以给任意 trace 或 generation 打分,支持人工标注、模型自动评分、用户反馈回传。这意味着你不仅能“看到”执行过程,还能“评价”执行质量。
- 指标面板:开箱即用的 dashboard,按时间维度展示调用量、延迟分布、token 消耗、评分趋势。不用自己写 BI 报表。
我选择 Langfuse 还有一个很实际的原因:它的接入成本极低。官方提供了 Python 和 JS/TS 的 SDK,跟 LangChain、LangGraph、OpenAI SDK 都有现成集成。如果你用的是 FastAPI + LangChain + LangGraph 这套组合,基本上加几行装饰器就能跑起来。对于 Spring AI 或 Rust 生态的 Agent,也可以通过 HTTP API 手动上报,灵活性足够。
2.3 全链路可观测的四个核心维度
在具体动手之前,先明确“全链路可观测”到底要观测什么。我总结下来是四个维度:
| 维度 | 要回答的问题 | Langfuse 对应能力 |
|---|---|---|
| 调用链 | 这次请求经过了哪些步骤?每步耗时多少? | Trace + Span 层级 |
| 输入输出 | 每步传给模型/工具的什么?返回了什么? | Generation 的 input/output 字段 |
| 成本与性能 | 花了多少 token?延迟瓶颈在哪? | Usage 统计 + Latency 面板 |
| 质量与反馈 | 结果好不好?用户满意吗? | Score 机制 + 人工标注 |
这四个维度缺一不可。只记录调用链不记录输入输出,排查问题时还是抓瞎;只记录输入输出不记录成本,优化时没有方向;只记录成本不记录质量,你不知道省下来的钱是不是以牺牲效果为代价。
3. 核心细节解析:Trace、Span、Generation 到底怎么用
3.1 Trace 的粒度设计:一次请求还是一次会话
这是我在实际使用中纠结最久的问题:trace 的粒度到底怎么定?是按“一次用户请求”建一个 trace,还是按“一个完整会话”建一个 trace?
我的结论是:按一次用户请求建 trace,会话级别的关联用 session_id 字段来维护。原因很简单,一次请求内部的执行链路是强关联的,排查问题时你需要看到完整的因果链条。而会话级别的多轮对话,轮次之间是弱关联,用 session_id 聚合就够了,不需要塞进同一个 trace 里。
Langfuse 的 trace 对象支持session_id、user_id、metadata等字段,你可以把会话 ID、用户 ID、业务场景标签都挂上去。这样在 UI 里既能按 trace 看单次执行细节,也能按 session 看多轮对话的整体表现。
注意:不要把整个会话塞进一个 trace。我试过,结果就是 trace 树深得离谱,UI 加载慢,而且每次请求的边界模糊,排查问题时反而更难定位。
3.2 Span 的划分原则:一个操作单元一个 Span
Span 是 trace 下面的执行单元。划分 Span 的原则是:一个逻辑上独立、有明确输入输出的操作,对应一个 Span。
在 Agent 系统里,典型的 Span 包括:
- 意图识别步骤
- 知识检索步骤
- 工具调用步骤(每个工具一个 Span)
- 结果后处理步骤
- 条件分支判断步骤
每个 Span 记录开始时间、结束时间、输入、输出、元数据。这样在 UI 里展开 trace 时,你能一眼看到哪一步耗时最长、哪一步输出异常。
我踩过的一个坑是:Span 划分太细。一开始我把每个函数调用都包成 Span,结果一个 trace 下面挂了上百个 Span,UI 里翻半天找不到重点。后来调整为“只包有业务意义的操作单元”,数量控制在 5 到 15 个之间,可读性大幅提升。
3.3 Generation 的特殊之处:专门为 LLM 调用设计
Generation 是 Span 的特殊类型,专门用来记录 LLM 调用。它比普通 Span 多了几个关键字段:
model:模型名称,比如 gpt-4、claude-3、qwen-maxmodel_parameters:温度、top_p、max_tokens 等参数input:发给模型的完整消息列表output:模型返回的内容usage:prompt_tokens、completion_tokens、total_tokenscompletion_start_time:首 token 返回时间(流式场景下很有用)
这些字段的价值在于:你可以按模型维度统计成本,按参数维度对比效果,按首 token 延迟评估用户体验。比如你发现某个场景下 gpt-4 和 gpt-3.5 的效果评分差不多,但成本差十倍,那就果断降级。
3.4 元数据设计:让 trace 可检索、可过滤
Langfuse 的 trace 和 span 都支持metadata字段,可以塞任意 JSON。这个字段看起来不起眼,但用好了能极大提升排查效率。
我通常会在 metadata 里放这些信息:
env:环境标识,区分 dev/staging/prodversion:Agent 版本号,方便对比不同版本的执行差异scenario:业务场景标签,比如“客服问答”“代码生成”user_tier:用户等级,用于分析不同用户群体的表现差异experiment_id:A/B 实验标识
有了这些标签,在 Langfuse UI 里就能按维度过滤 trace。比如“只看 prod 环境下 v2.3 版本在客服问答场景的失败请求”,几秒钟就能筛出来。
4. 实操过程:从零接入 Langfuse 的完整步骤
4.1 部署方式选择:云服务还是自托管
Langfuse 提供两种使用方式:官方云服务和自托管。我的建议是:
- 个人项目或小团队:直接用云服务,免费额度够用,省去运维成本。
- 企业内网或数据敏感场景:自托管,用 Docker Compose 一键拉起。
自托管的命令很简单:
git clone https://github.com/langfuse/langfuse.git cd langfuse docker compose up -d启动后访问http://localhost:3000,创建项目,拿到 public key 和 secret key。这两个 key 后面接入 SDK 时要用。
提示:自托管时记得配置持久化存储,默认的 Postgres 和 ClickHouse 数据卷要挂到宿主机上,不然容器重启数据就没了。
4.2 Python SDK 接入:装饰器方式最省事
如果你用 Python 开发 Agent,Langfuse 的 SDK 提供了装饰器方式,侵入性最小。先安装:
pip install langfuse然后配置环境变量:
export LANGFUSE_PUBLIC_KEY="pk-lf-..." export LANGFUSE_SECRET_KEY="sk-lf-..." export LANGFUSE_HOST="http://localhost:3000"接下来用@observe()装饰器包裹你的核心函数:
from langfuse.decorators import observe, langfuse_context @observe() def agent_run(user_query: str): intent = recognize_intent(user_query) result = execute_tools(intent) return generate_response(result) @observe() def recognize_intent(query: str): # LLM 调用逻辑 ... @observe() def execute_tools(intent: dict): # 工具调用逻辑 ...这样每次调用agent_run就会自动创建一个 trace,内部的recognize_intent和execute_tools会自动成为子 span。层级关系由调用栈自动推导,不用手动指定 parent。
4.3 手动上报:适合非 Python 技术栈
如果你用的是 Spring AI、Rust 或其他技术栈,可以通过 Langfuse 的 HTTP API 手动上报。核心接口有三个:
- 创建 trace:
POST /api/public/traces - 创建 span:
POST /api/public/spans - 创建 generation:
POST /api/public/generations
以创建 generation 为例,请求体大致长这样:
{ "traceId": "trace-xxx", "name": "intent-recognition", "model": "gpt-4", "input": [{"role": "user", "content": "帮我查一下订单"}], "output": {"intent": "order_query"}, "usage": {"promptTokens": 120, "completionTokens": 30}, "startTime": "2025-01-01T10:00:00Z", "endTime": "2025-01-01T10:00:02Z" }手动上报的好处是灵活,坏处是要自己维护 trace 和 span 的层级关系。我的做法是封装一个轻量的客户端类,在 Agent 框架的各个钩子点调用,避免业务代码里散落大量 HTTP 请求。
4.4 与 LangGraph 集成:节点级追踪
如果你用 LangGraph 搭建 Agent,Langfuse 有现成的 callback handler。在编译图的时候传入:
from langfuse.callback import CallbackHandler langfuse_handler = CallbackHandler() graph = builder.compile() result = graph.invoke( {"messages": [...]}, config={"callbacks": [langfuse_handler]} )这样 LangGraph 的每个节点执行都会自动上报为 span,节点之间的状态传递也会被记录。对于复杂的多节点 Agent,这个集成能省掉大量手动埋点工作。
4.5 流式响应的追踪处理
流式响应是个特殊场景。模型一边生成一边返回,你不能等全部生成完再上报。Langfuse 的处理方式是:在流开始时创建 generation,流结束时更新 output 和 usage。
Python SDK 里可以这样操作:
from langfuse import Langfuse langfuse = Langfuse() generation = langfuse.generation( trace_id=trace_id, name="streaming-response", model="gpt-4", input=messages ) full_response = "" for chunk in stream: full_response += chunk yield chunk generation.end(output=full_response, usage={...})关键是completion_start_time字段要记录首 token 到达时间,这个指标直接反映用户感知的响应速度。
5. 常见问题与排查技巧实录
5.1 Trace 丢失或不完整
现象:UI 里只能看到部分 span,或者 trace 根本没出现。
排查思路:
- 检查 SDK 是否在程序退出前 flush。Langfuse SDK 是异步批量上报的,如果进程直接退出,缓冲区里的数据可能没发出去。解决办法是在程序结束前调用
langfuse.flush()。 - 检查网络连通性。自托管场景下,确认 Agent 服务能访问到 Langfuse 的地址。
- 检查 key 配置。public key 和 secret key 要配对,host 地址不要带多余路径。
我遇到过一次 trace 丢失,排查半天发现是 Docker 容器时区不对,导致上报的时间戳超出服务端接受范围。把容器时区统一成 UTC 后问题消失。
5.2 嵌套 Span 层级错乱
现象:UI 里 span 的父子关系跟预期不符,子 span 跑到了根节点下面。
原因:手动上报时没有正确传递 parent_span_id,或者异步任务里上下文丢失。
解决:如果用装饰器方式,确保被装饰函数的调用关系是同步的。如果是异步场景,用langfuse_context手动设置当前 trace 和 span。手动上报时,每次创建 span 都要带上正确的traceId和parentSpanId。
5.3 Token 统计不准
现象:Langfuse 面板上的 token 消耗跟实际账单对不上。
原因:流式响应场景下,usage 字段可能没有正确回填;或者某些模型返回的 usage 格式跟 SDK 预期不一致。
解决:流式场景下,在流结束后手动计算 token 数并更新 generation。对于格式不一致的模型,在 SDK 外面包一层适配器,把 usage 字段转成 Langfuse 认识的格式。
5.4 高频调用下的性能影响
现象:接入 Langfuse 后,Agent 响应变慢。
原因:SDK 默认是同步上报,每次调用都等 HTTP 请求完成。
解决:确认使用的是异步上报模式。Python SDK 默认就是异步批量发送,但如果你的代码里手动调用了flush(),会强制同步等待。另外可以调整批量发送的大小和间隔,在实时性和性能之间找平衡。
| 问题类型 | 典型现象 | 排查方向 | 解决手段 |
|---|---|---|---|
| Trace 丢失 | UI 无数据 | flush、网络、key | 程序退出前 flush |
| 层级错乱 | 父子关系异常 | parent_span_id | 手动指定或检查调用栈 |
| Token 不准 | 统计对不上 | usage 回填 | 流式结束后手动更新 |
| 性能下降 | 响应变慢 | 上报模式 | 异步批量发送 |
5.5 评测数据怎么回传
Langfuse 的 score 机制支持多种回传方式:
- 人工标注:在 UI 里直接给 trace 打分
- 模型自动评分:用另一个 LLM 对输出质量打分,通过 API 回传
- 用户反馈:前端收集用户点赞/点踩,通过 API 回传
我通常的做法是:线上用用户反馈做粗筛,离线用模型评分做细评。两者结合,既能覆盖全量数据,又能保证评分质量。
6. 从可观测到可优化:Langfuse 的进阶用法
6.1 用 Trace 对比做 A/B 测试
Langfuse 支持按 metadata 过滤 trace,这天然适合做 A/B 测试。比如你同时跑两个版本的 prompt,在 metadata 里标记experiment_id: "prompt-v1"和experiment_id: "prompt-v2",然后在 UI 里分别筛选,对比两组的延迟、token 消耗、评分分布。
我做过一次 prompt 优化实验,v2 版本在评分上比 v1 高了 0.3 分,但 token 消耗多了 40%。如果没有 Langfuse 的对比视图,这种权衡很难量化。
6.2 用 Score 驱动 Prompt 迭代
Score 不只是一个数字,它是 Prompt 迭代的指南针。我的工作流是:
- 上线一版 prompt,收集一周的 score 数据
- 筛选出低分 trace,逐条分析失败原因
- 归纳失败模式,针对性修改 prompt
- 新版本上线,对比 score 变化
这个循环跑几轮之后,prompt 质量会有明显提升。关键是 Langfuse 让“筛选低分 trace”这个动作变得极其简单,点几下就能筛出来。
6.3 成本监控与告警
Langfuse 的 dashboard 可以按模型、按场景展示 token 消耗趋势。我设置了一个简单的告警规则:当日 token 消耗超过预算的 80% 时触发通知。这样能在成本失控之前及时干预。
对于多模型 Agent,还可以按模型维度分析成本结构。比如你发现某个场景下 90% 的成本花在了某个模型上,但该模型的贡献只占 30%,那就值得考虑替换或优化。
6.4 与现有监控体系打通
Langfuse 不是孤岛。我通常会把它的数据跟现有的监控体系打通:
- 把 trace_id 打到业务日志里,方便从业务日志跳转到 Langfuse
- 把关键指标(延迟、错误率、token 消耗)推送到 Prometheus,接入现有告警
- 把 score 数据同步到数据仓库,做长期趋势分析
这样 Langfuse 负责“深度追踪”,现有监控负责“广度覆盖”,两者互补。
7. 一些踩坑之后的个人体会
接入 Langfuse 这一年多,最大的体会是:可观测性不是加个 SDK 就完事,它需要你在系统设计阶段就考虑“哪些信息值得记录”。我见过不少团队接入了 Langfuse,但 trace 里只有模型输入输出,没有工具调用细节,没有状态变更记录,排查问题时还是靠猜。
另一个体会是:不要追求 100% 的追踪覆盖率。有些高频、低价值的调用,全量上报反而增加负担。我的做法是分层采样:核心链路全量上报,边缘链路按 10% 采样。这样既保证了关键路径的可观测性,又控制了成本。
最后分享一个小技巧:Langfuse 的 trace 支持添加release和version字段,每次发版时自动带上版本号。这样当线上出问题时,你可以快速筛选“最近一次发版之后的 trace”,对比发版前后的指标变化,定位问题效率极高。
这个领域还在快速演进,Langfuse 本身也在迭代。我目前关注的方向是把它跟 Agent 的自动化评测结合起来,用 LLM 做裁判,对每次执行自动打分,再根据分数自动触发 prompt 优化流程。等跑通了再找机会分享。