
1. 为什么“能跑通”的 Agent 离“能上线”还差一整套观测体系我最早做 AI Agent 项目的时候判断一个系统能不能交付标准特别朴素本地跑一遍工具调用没报错最终答案看起来像那么回事就认为可以了。直到有一次线上出了个诡异问题——用户问“帮我查一下上周的订单状态”Agent 连续调了三次订单查询工具每次参数都不一样最后返回了一个完全无关的答案。日志里只有一行“tool call success”没有任何上下文我盯着屏幕看了两个小时愣是不知道它为什么这么决策。那次之后我才真正意识到Agent 系统的复杂度不在“能不能调通模型”而在“调通之后你根本不知道它内部发生了什么”。传统后端服务出问题你可以看请求链路、看数据库慢查询、看线程堆栈但 Agent 系统里一次用户请求可能触发多轮 LLM 推理、多次工具调用、若干次上下文压缩和路由决策每一层都是黑盒。你看到的只是输入和输出中间那几十个决策节点全是盲区。这就是可观测性要解决的问题。而 Langfuse 之所以在 AI Agent 工程圈里被反复提起不是因为它是个“日志工具”而是因为它把LLM 调用、工具执行、会话追踪、成本统计、质量评估这几件事串成了一条完整的链路。你可以把它理解成 Agent 系统的“行车记录仪 仪表盘 故障诊断仪”三合一。这篇文章适合两类人看一类是已经用 LangChain、LangGraph、Spring AI 或者自己手写 Agent 循环但每次排查问题都靠print大法的开发者另一类是正准备把 Agent 从 Demo 推向生产环境需要一套可落地观测方案的工程负责人。我会从实际项目出发把 Langfuse 的接入逻辑、核心概念、踩坑经验和评估方法讲透不堆概念只讲我真正用过、验证过的东西。2. Langfuse 的核心数据模型Trace、Span、Generation 到底怎么对应 Agent 的执行过程很多人第一次看 Langfuse 文档会被 Trace、Span、Generation、Score、Session 这一堆概念绕晕。其实你只要把 Agent 的一次完整请求想象成一次“出差行程”这些概念就全通了。2.1 用“出差行程”类比理解三层结构一次用户请求进来比如“帮我分析这份销售报表并生成摘要”这就是一次Trace相当于一整趟出差。这趟出差里有很多环节坐飞机、住酒店、开会、吃饭每个环节就是一个Span代表一个有开始和结束的操作单元。而其中“开会”这个环节如果涉及 LLM 推理那它同时也是一个Generation因为 Generation 是 Span 的特殊类型专门记录模型调用的输入输出、token 消耗、模型名称、温度参数这些信息。在 Agent 场景里典型的映射关系是这样的Agent 执行环节Langfuse 对应类型记录的关键信息用户发起一次完整对话Trace用户 ID、会话 ID、输入输出、总耗时Agent 规划下一步动作Generation提示词、模型返回的决策、token 数调用外部工具搜索、数据库、APISpan工具名、入参、返回值、耗时、是否报错多轮对话中的一轮Trace通过 Session 关联轮次编号、上下文长度、历史消息对回答质量打分Score评分值、评分来源人工/自动、评论这个结构的好处是你既能从宏观上看一次请求的整体链路也能下钻到某一个具体的模型调用看它当时收到的完整提示词是什么、返回了什么、花了多少 token。2.2 为什么 Span 的嵌套关系对 Agent 特别重要普通 Web 服务的调用链通常是线性的请求进来查缓存查数据库返回。但 Agent 的执行是树状嵌套的。一个典型的 ReAct 循环是这样的Agent 先思考Generation决定调用工具 ASpan工具 A 返回结果后Agent 再思考Generation决定调用工具 BSpan工具 B 报错了Agent 重新思考Generation换了个参数再调工具 BSpan最后生成答案Generation。如果你不记录嵌套关系只记平铺的日志那你看到的就是一堆孤立的模型调用和工具调用根本还原不出“它为什么在第三次才换参数”这个决策过程。Langfuse 的 Span 支持父子嵌套你可以把整个 Agent 循环包在一个父 Span 里每一轮思考和工具调用作为子 Span这样在 UI 上就能看到一棵完整的执行树。实操建议在 LangGraph 里每个节点天然就是一个 Span 边界在自研 Agent 循环里建议把“一轮完整的 think-act-observe”包成一个 Span而不是把每次 LLM 调用单独平铺否则链路会碎得没法看。2.3 Session 和 User 维度多轮对话场景下不可省略的关联字段单轮问答场景Trace 就够了。但 Agent 往往是多轮对话用户可能先问“帮我查订单”再问“那帮我退掉”这两次请求在业务上是一个会话。Langfuse 的Session就是用来把多个 Trace 串成一个会话的。你只需要在每次创建 Trace 时传入相同的sessionId就能在 UI 里按会话维度查看完整对话历史。User维度则用于区分不同用户的行为模式。我实际项目中遇到过一个问题某个用户的 Agent 调用成本特别高排查后发现是他习惯一次性粘贴超长文本导致上下文膨胀。如果没有 User 维度你只能看到整体成本上升根本定位不到具体是谁。这两个字段看起来简单但很多团队接入时只传了 Trace 的基本信息忘了传 sessionId 和 userId等到想做用户级分析时才发现数据缺失只能回头补埋点。这个坑我踩过建议一开始就加上。3. 接入 Langfuse 的三种姿势SDK 埋点、框架集成、代理转发怎么选Langfuse 的接入方式比很多人想象的灵活不是只有“装个 SDK 到处写代码”这一条路。我按侵入性从高到低梳理三种方式你可以根据项目阶段选。3.1 SDK 手动埋点控制力最强但别到处撒Python SDK 的核心用法就是langfuse.trace()创建 Trace然后用trace.generation()和trace.span()创建子节点。看起来简单但实际写起来有个原则埋点要跟着业务语义走不要跟着函数调用走。我见过有团队在每个工具函数入口都加一个 Span结果一个简单查询产生了二十几个 SpanUI 上密密麻麻根本看不清。正确的做法是只在有决策意义的节点埋点。比如“Agent 决定调用哪个工具”这个决策点值得记“工具内部执行了一次 HTTP 请求”就不一定要单独记除非这个请求本身可能成为瓶颈。from langfuse import Langfuse langfuse Langfuse( public_keypk-xxx, secret_keysk-xxx, hosthttps://your-langfuse-host ) trace langfuse.trace( nameorder-query-agent, user_iduser_123, session_idsession_456, input{query: 查一下上周订单} ) # Agent 第一轮思考 generation trace.generation( nameplanning-step-1, modelgpt-4o, input[{role: user, content: 查一下上周订单}], model_parameters{temperature: 0.2} ) # ... 调用模型 ... generation.end(output需要调用订单查询工具, usage{input: 120, output: 30}) # 工具调用 span trace.span(nametool-order-query, input{time_range: last_week}) # ... 执行工具 ... span.end(output{orders: [...]}, levelDEFAULT) trace.update(output{answer: 上周共有 3 笔订单...})这段代码里有个细节值得说generation.end()里的usage字段。如果你用的是 OpenAI 兼容接口很多 SDK 会自动带上 token 统计但如果你用的是自部署模型或者某些国产模型 APItoken 数可能拿不到这时候要么手动估算要么在模型返回里解析。成本统计的准确性直接取决于这个字段别偷懒。3.2 框架原生集成LangChain 和 LangGraph 用户的首选如果你用的是 LangChain 或 LangGraphLangfuse 提供了 CallbackHandler接入成本极低。你只需要在调用链里传入 callback剩下的嵌套关系、模型参数、token 统计它自动帮你处理。from langfuse.callback import CallbackHandler langfuse_handler CallbackHandler( public_keypk-xxx, secret_keysk-xxx, hosthttps://your-langfuse-host, session_idsession_456, user_iduser_123 ) # LangChain 链式调用 result chain.invoke( {input: 查一下上周订单}, config{callbacks: [langfuse_handler]} )LangGraph 场景下更简单因为 LangGraph 本身就是基于 LangChain 的callback 会自动贯穿整个图执行过程。每个节点、每次模型调用、每次工具执行都会自动生成对应的 Span 和 Generation。但这里有个容易忽略的坑自动集成虽然省事但 Span 的命名往往是框架默认的比如ChatOpenAI、ToolExecutor这种UI 上一眼看不出业务含义。建议在关键节点用run_name参数覆盖默认名称比如把ChatOpenAI改成agent-planning这样排查时能快速定位。3.3 代理转发模式不改代码的“兜底方案”有些团队用的是自研 Agent 框架或者模型调用封装得很深改代码成本高。这时候可以用 Langfuse 的代理模式把模型 API 的 base_url 指向 Langfuse 的代理地址它在转发请求的同时自动记录调用信息。这种方式的优点是零代码侵入缺点是只能记录模型调用层面的信息工具调用、Agent 决策这些业务逻辑它看不到。所以我的建议是代理模式适合快速验证和成本监控但要做全链路可观测最终还是得回到 SDK 或框架集成。接入方式侵入性记录粒度适用阶段SDK 手动埋点高最细可自定义自研框架、需要精细控制框架集成低较细自动嵌套LangChain/LangGraph 项目代理转发极低仅模型调用层快速验证、成本监控4. 把 Langfuse 接进真实 Agent 项目从环境搭建到第一条完整链路跑通光讲概念没意思我拿一个实际场景走一遍一个基于 FastAPI LangGraph 的订单查询 Agent用户输入自然语言Agent 决定调用哪个工具最后返回结果。我会把 Langfuse 从零接进去包括环境准备、代码改造、验证链路。4.1 自部署还是用云服务先算一笔账Langfuse 提供云服务和自部署两种模式。云服务有免费额度适合小团队快速起步自部署用 Docker Compose 就能跑起来数据完全自己掌控。我选的是自部署原因有两个一是项目涉及订单数据合规上要求调用日志不能出内网二是自部署版本功能完整没有阉割。部署命令很简单git clone https://github.com/langfuse/langfuse.git cd langfuse docker compose up -d默认会启动 Postgres、ClickHouse、Redis 和 Langfuse 服务。注意 ClickHouse 是必须的Langfuse 的追踪数据存在 ClickHouse 里不是 Postgres。我第一次部署时只起了 Postgres结果 UI 能打开但数据写不进去排查了半天才发现少起了 ClickHouse。部署完成后访问http://localhost:3000创建项目拿到 public_key 和 secret_key。这两个 key 要放到环境变量里别硬编码。4.2 在 LangGraph 节点里埋点的正确位置LangGraph 的图结构天然适合 Langfuse 的嵌套模型。我的做法是在图的入口创建一个 Trace然后每个节点内部用 callback 自动生成 Span。from fastapi import FastAPI from langgraph.graph import StateGraph from langfuse.callback import CallbackHandler app FastAPI() def build_agent_graph(): graph StateGraph(AgentState) graph.add_node(plan, plan_node) graph.add_node(execute_tool, tool_node) graph.add_node(generate_answer, answer_node) # ... 添加边 ... return graph.compile() agent build_agent_graph() app.post(/chat) async def chat(request: ChatRequest): handler CallbackHandler( session_idrequest.session_id, user_idrequest.user_id, trace_nameorder-agent-request ) result await agent.ainvoke( {messages: [{role: user, content: request.query}]}, config{callbacks: [handler]} ) return {answer: result[answer]}这里的关键点是CallbackHandler 在每次请求时新建而不是全局单例。因为 session_id 和 user_id 是请求级别的全局单例会导致所有请求的会话信息串在一起。这个坑我在早期版本踩过UI 上所有用户的消息混在一个 session 里完全没法分析。4.3 验证链路是否完整看三个地方接入完成后别急着上线先验证链路是否完整。我通常看三个地方第一Trace 列表里能不能看到刚才的请求输入输出是否完整。第二点进 Trace 详情执行树是否呈现了正确的嵌套关系plan、tool、answer 三个节点是否都在。第三Generation 详情里 token 统计是否有值模型名称和参数是否正确。如果执行树是平铺的说明嵌套关系没建立通常是 callback 没有正确传递到子节点。如果 token 统计为空检查模型返回里是否有 usage 字段或者是不是用了不兼容的模型接口。实测经验LangGraph 的ainvoke和invoke在 callback 传递上行为一致但如果你在节点内部又手动创建了新的 chain 或 agent需要把 callback 继续往下传否则子链路的追踪会断掉。5. 并发场景下的追踪数据不串号session 隔离与批量写入的实战处理Agent 系统一旦上生产并发是绕不开的。我见过不少团队在单请求测试时一切正常一上并发就出现追踪数据串号、Span 归属错误、甚至数据丢失。这一节专门讲并发场景下的坑和处理方式。5.1 为什么全局单例的 CallbackHandler 会串号前面提过 CallbackHandler 要按请求新建但很多人会想那我能不能创建一个全局的然后每次请求动态改 session_id答案是不行。因为 Langfuse 的 SDK 内部维护了 trace 的上下文状态多个并发请求同时修改同一个 handler 的 session_id会导致 A 请求的 Span 被挂到 B 请求的 Trace 上。正确的做法是每个请求独立创建 handler或者使用上下文变量contextvars来隔离。在 FastAPI 这种异步框架里每个请求本身就在独立的协程上下文里只要 handler 是在请求处理函数内部创建的就不会串。5.2 高并发下的写入压力批量上报与采样策略Langfuse SDK 默认是异步批量上报的不是每次调用都同步写。这个设计在高并发下很重要但有几个参数需要关注参数默认值作用调整建议flush_at15攒够多少条上报高并发可调到 50-100flush_interval1.0多久强制上报一次保持 1-2 秒避免数据延迟max_retries3上报失败重试次数内网部署可保持默认timeout10单次上报超时跨网络部署可适当调大如果你的 Agent 每秒处理几十个请求每个请求产生十几个 Span那写入量是相当可观的。我实际项目中遇到过 ClickHouse 写入成为瓶颈的情况后来通过两个手段缓解一是把flush_at调大减少网络往返二是对低价值的 Span 做采样比如工具调用的成功记录只保留 10%但报错记录 100% 保留。采样这个事要谨慎报错和异常绝对不能采样否则你排查问题时发现数据缺失那才是真的抓瞎。5.3 异步 Agent 的追踪完整性别让后台任务丢了链路有些 Agent 会把耗时操作放到后台任务里比如“生成报告”这种可能跑几分钟的任务。如果后台任务没有继承请求的 callback 上下文那这部分执行就不会被追踪到。在 Python 里可以用contextvars.copy_context()把上下文复制到后台任务中。或者更简单的方式在后台任务开始时用之前记录的 trace_id 重新关联。Langfuse 支持通过 trace_id 获取已有 Trace 并继续添加 Span这个能力在异步场景下很实用。6. 用 Langfuse 做 Agent 质量评估从人工打分到自动化评测的落地路径可观测性解决的是“看得见”的问题但看得见不等于做得好。Langfuse 的 Score 功能就是用来回答“这个回答到底好不好”的。我把它分成三个层次来讲从最简单的人工打分到自动化评测。6.1 人工打分别小看这个最笨的方法Langfuse UI 上可以直接对某个 Trace 打分支持数值、分类、布尔三种类型。我建议在项目早期一定要做人工打分哪怕每天只打 20 条。因为只有你自己看过足够多的真实输出才知道 Agent 到底在哪些场景下会翻车。打分的时候有个技巧不要只打总分要拆维度。比如“答案准确性”“工具选择合理性”“回复格式规范性”分开打。这样你后续分析时能看出到底是模型能力问题还是提示词问题。6.2 自动评分用 LLM 当裁判的注意事项当数据量上来后人工打分不现实可以用 LLM 做自动评分。Langfuse 支持通过 API 写入 Score你可以写一个评估脚本定期拉取未评分的 Trace用另一个模型打分后写回。但这里有个非常容易踩的坑用同一个模型既做 Agent 又做裁判会存在自我偏好偏差。我实测下来用 GPT-4o 做 Agent、用 Claude 做裁判评分结果比同模型自评更客观。另外裁判模型的提示词要写得非常具体不能只说“请打分”要给出明确的评分标准和示例。def evaluate_trace(trace_id, query, answer): prompt f请根据以下标准对回答打分1-5分 1分完全无关或错误 3分部分正确但有明显遗漏 5分准确完整格式规范 用户问题{query} Agent回答{answer} 只输出分数数字。 score judge_model.invoke(prompt) langfuse.score( trace_idtrace_id, nameanswer-quality, valuefloat(score), commentauto-evaluated by judge model )6.3 把评估结果反哺到提示词优化评分数据最大的价值不是看个热闹而是指导优化。我的做法是每周拉一次低分 Trace按问题类型归类是工具选错了还是参数提取错了还是回答格式不对。然后针对性地改提示词或工具描述。举个例子之前有个 Agent 总是把“查订单”和“查物流”搞混看了一周的低分记录后发现两个工具的描述太相似了。把工具描述改得更具体后错误率直接降了一半。这种优化如果没有评分数据支撑你根本不知道问题出在哪。7. 成本与性能的平衡Langfuse 数据保留策略和查询优化Langfuse 用起来爽但数据量大了之后存储成本和查询性能都是问题。这一节讲几个我在生产环境验证过的优化手段。7.1 数据保留策略不是所有 Trace 都值得存一年Langfuse 支持配置数据保留天数。我的建议是分层保留报错和低分 Trace 保留 90 天正常 Trace 保留 30 天高频重复的成功调用保留 7 天。这样既保证了问题排查有数据又不会让 ClickHouse 无限膨胀。具体配置在 Langfuse 的环境变量里LANGFUSE_RETENTION_DAYS可以设置全局保留天数。如果需要更细粒度的策略可以通过定时任务手动清理。7.2 查询优化按需拉取别在 UI 上翻全量数据Langfuse UI 的查询在数据量大时会变慢尤其是按时间范围拉取大量 Trace 的时候。我的经验是日常排查用精确过滤条件比如指定 session_id 或 user_id而不是直接拉最近 7 天全部数据。如果要做批量分析走 API 导出到本地再处理比在 UI 上翻页效率高得多。另外ClickHouse 的查询性能对索引很敏感。Langfuse 默认已经建了常用索引但如果你有自定义的分析需求建议在导出数据后用 Pandas 或 DuckDB 处理别直接压 ClickHouse。7.3 和现有监控体系的对接Langfuse 不是孤立的它应该和你的现有监控体系打通。我的做法是把 Langfuse 的 API 封装一层关键指标如每小时调用量、平均延迟、错误率、token 消耗推送到 Prometheus然后在 Grafana 上做统一看板。这样运维团队看整体健康度算法团队看 Agent 质量各取所需。注意Langfuse 的 API 有速率限制做指标推送时要注意控制频率建议用定时任务每分钟拉一次聚合数据而不是实时拉取。8. 几个我踩过的坑和对应的解法最后分享几个实际踩过的坑都是文档里不会写、但一踩一个准的问题。第一个坑Trace 名称重复导致无法区分。早期我给所有 Trace 都叫 “agent-request”结果 UI 上一眼望去全是同名记录排查时根本找不到目标。后来改成{业务场景}-{用户ID后四位}-{时间戳}的格式定位效率提升明显。第二个坑工具报错没有记录 level。Langfuse 的 Span 支持设置 levelDEBUG/DEFAULT/WARNING/ERROR但默认是 DEFAULT。如果工具报错了你没设 ERRORUI 上不会高亮很容易漏掉。建议在工具执行的异常捕获里统一设置 level。第三个坑长文本输入导致 UI 卡顿。有些用户会粘贴几千字的文本Langfuse UI 渲染大文本时会卡。解法是在写入前对超长输入做截断只保留前 2000 字符完整内容存到自己的对象存储里Trace 里放个引用链接。第四个坑忘记设置环境区分。开发、测试、生产环境的 Trace 混在一起根本分不清哪些是真实用户数据。Langfuse 支持通过environment字段区分建议在初始化时就根据部署环境设置好。第五个坑评估脚本和 Agent 用同一个 API Key。这会导致评估调用也产生 Trace污染 Agent 的统计数据。正确做法是评估脚本用独立的项目或至少独立的 tag 标记。这些坑单看都不复杂但组合在一起就是“为什么我的 Langfuse 用起来这么乱”的根源。我的建议是接入初期就把命名规范、环境区分、错误标记这三件事定好后面会省很多事。实际用下来Langfuse 给我最大的价值不是某个具体功能而是它逼着我把 Agent 的每个决策节点都想清楚这一步到底在做什么、输入是什么、输出是什么、可能出什么错。这个思考过程本身比工具带来的便利更重要。如果你现在还在用 print 调试 Agent真的建议花半天时间把 Langfuse 接进去后面排查问题的时间能省回来十倍。