ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent 可观测性实战:Langfuse 全链路追踪与质量评估落地指南

AI Agent 可观测性实战:Langfuse 全链路追踪与质量评估落地指南 1. 为什么“能跑通”和“能上线”之间隔着一整套可观测体系我最早做 AI Agent 项目的时候和大多数人一样注意力全在“怎么把链路串起来”上模型能调通、工具能触发、多轮对话不崩就觉得这事成了。直到有一次线上环境里一个本该三步走完的任务用户反馈“等了半分钟只回了一句话”我翻遍日志才发现中间有一次工具调用超时被静默吞掉了模型拿着空结果硬编了一个答案。那一刻我才真正意识到Agent 系统的复杂度不在“能不能跑”而在“跑的时候到底发生了什么”。这就是可观测性要解决的问题。传统后端服务的可观测性核心是日志、指标、链路追踪三件套关注的是请求耗时、错误率、资源占用。但 AI Agent 系统完全是另一回事——它的执行路径是非确定性的同一个输入模型可能走三条不同的工具调用链它的成本是按 token 计费的一次失控的循环可能烧掉几十块钱它的失败是语义级的HTTP 200 不代表任务成功模型可能一本正经地胡说八道。Langfuse 就是冲着这些痛点来的。它是一个开源的 LLM 工程可观测平台核心能力可以概括成四块Trace全链路追踪、Span/Generation细粒度节点记录、Score质量评估打分、Dataset数据集与实验对比。你可以把它理解成“给 Agent 装了一个行车记录仪 黑匣子 体检报告生成器”。它解决的核心问题是让每一次模型调用、每一次工具执行、每一轮对话的输入输出、耗时、成本、评分全部可追溯、可对比、可复盘。这篇文章适合谁看如果你正在用 LangChain、LangGraph、Spring AI、或者自己手搓的 Agent 框架做项目已经过了“Hello World”阶段开始被线上问题、成本失控、效果不稳定折磨那这篇就是写给你的。我会从整体设计思路讲到具体落地细节包括 Trace 怎么埋、评测怎么做、并发怎么扛、踩过哪些坑尽量把“工程实践”这四个字落到实处。2. 整体设计思路可观测性不是加个 SDK 就完事2.1 先想清楚要观测什么再谈用什么工具很多人接 Langfuse 的方式是看到官方文档说pip install langfuse然后加两行环境变量跑起来看到 Trace 列表里冒出来几条记录就觉得接完了。这种做法的问题在于你观测到的是框架默认给你的东西而不是你真正需要的东西。我在动手之前会先列一张“观测需求清单”按优先级排观测维度具体问题对应 Langfuse 能力链路完整性一次用户请求经过了哪些节点哪一步慢了Trace Span 树模型调用细节用了哪个模型输入输出是什么token 多少Generation成本归因哪个用户/哪个功能/哪个 Agent 最烧钱Metadata Tags质量评估这次回答好不好有没有幻觉Score版本对比换了 prompt 之后效果变好还是变差Dataset Experiment异常定位工具调用失败了几次错误堆栈在哪Level StatusMessage这张表决定了你的埋点策略。比如你关心成本归因那每个 Trace 就必须带上userId、sessionId、feature这类 metadata你关心版本对比那 prompt 版本号就得作为 tag 打进去。埋点信息是在设计阶段定的不是事后补的因为很多上下文一旦出了函数作用域就丢了。2.2 自托管还是云服务一个绕不开的选型Langfuse 提供两种部署形态官方云服务和自托管。这个选择直接影响后面的架构。云服务的优点是开箱即用不用管数据库、不用管扩容适合快速验证和小团队。但它有两个现实问题一是数据要出你的网络边界如果你的业务涉及敏感数据合规上过不去二是量大之后成本会上去尤其是高频 Agent 场景Trace 数量是普通 Web 请求的好几倍。自托管的话Langfuse 官方提供 Docker Compose 方案核心组件是Postgres存元数据 ClickHouse存 Trace 和 Observation量大时的主力 Redis队列 S3/MinIO存大对象比如长文本输入输出。这套组合的选型逻辑很清晰Trace 数据是典型的“写多读少、按时间范围查、需要聚合”的时序型数据ClickHouse 的列式存储和压缩比在这种场景下比 Postgres 强一个数量级而元数据用户、项目、评分定义是关系型的放 Postgres 更合适。我自己的项目最后选了自托管原因是数据敏感 量大。实测下来单机 8C16G 跑这套组合日均百万级 Observation 写入没什么压力前提是 ClickHouse 的磁盘要用 SSD机械盘在批量写入时会成为瓶颈。提示自托管部署时LANGFUSE_S3_*这一组配置千万别漏。默认情况下长文本会直接进 ClickHouse字段膨胀很快把大对象外置到对象存储能显著降低存储成本。2.3 埋点粒度Trace、Span、Generation 到底怎么分这是最容易搞混的地方。我用一个具体例子说明。假设用户问“帮我查一下北京明天的天气如果下雨就提醒我带伞”。Trace整个用户请求从接收到返回一个 Trace。SpanAgent 的每一个逻辑步骤。比如“意图识别”是一个 Span“调用天气工具”是一个 Span“生成最终回复”是一个 Span。Generation专门指模型调用是 Span 的一种特化。它比普通 Span 多记录了 model、token 用量、prompt/completion 内容、温度参数等。划分原则我总结成一句话凡是调用模型的地方用 Generation凡是执行逻辑的地方用 Span一次完整请求用 Trace 包起来。工具调用本身不是模型调用所以是 Span但如果工具内部又调了一次模型比如用模型做参数抽取那这个内部调用又是一个 Generation嵌套在工具 Span 下面。这样分的好处是你在 Langfuse 的树形视图里能一眼看出“时间花在哪、钱花在哪”。如果某个 Span 特别长但下面没有 Generation说明是工具或 IO 慢如果某个 Generation token 特别多说明是 prompt 或上下文膨胀。3. 核心细节解析Trace 埋点的几个关键决策3.1 用装饰器还是手动埋点Langfuse 的 Python SDK 提供了observe()装饰器用起来很爽加一行就能自动记录函数入参、出参、耗时。但我实际用下来装饰器适合快速验证生产环境更推荐手动埋点原因有三个。第一装饰器会把函数的所有参数都序列化进 Trace如果参数里有大对象比如整个对话历史、大段文档Trace 体积会爆炸而且可能把不该记录的敏感字段也带进去。第二装饰器的嵌套关系依赖调用栈异步场景下比如asyncio.gather并发调用父子关系容易乱。第三手动埋点虽然多写几行但你能精确控制记录什么、不记录什么以及 metadata 怎么打。手动埋点的典型写法是这样from langfuse import Langfuse langfuse Langfuse() def handle_user_query(user_id: str, query: str): trace langfuse.trace( nameagent-query, user_iduser_id, metadata{feature: weather-assistant, env: prod}, tags[v2-prompt], ) # 意图识别节点 intent_span trace.span(nameintent-recognition, input{query: query}) intent classify_intent(query) intent_span.end(output{intent: intent}) # 模型调用节点 generation trace.generation( namellm-call, modelgpt-4o-mini, input[{role: user, content: query}], ) response call_llm(query) generation.end( outputresponse.content, usage{input: response.usage.prompt_tokens, output: response.usage.completion_tokens}, ) trace.update(output{reply: response.content}) return response.content注意trace.update()这一步它把最终输出补到 Trace 上。很多人忘了这步结果 Trace 列表里只有输入没有输出复盘的时候还得点进去一层层看很费劲。3.2 metadata 怎么设计才不浪费metadata 是 Langfuse 里最灵活也最容易被滥用的字段。我的经验是metadata 只放“你未来会用来筛选或聚合的维度”不要什么都往里塞。必放的几个userId成本归因、sessionId多轮对话串联、feature或agentName功能维度、env区分测试和生产、promptVersion版本对比。这几个字段配合 Langfuse 的筛选器基本能覆盖 80% 的排查场景。不要放的完整的对话历史放 input 里、大段文档内容放对象存储Trace 里只存引用、任何密钥或个人信息。Langfuse 虽然支持数据脱敏但最稳妥的做法是在埋点层就不记录敏感数据。注意sessionId这个字段特别重要。多轮对话场景下如果每轮都生成新的 Trace 而不带 sessionId你就没法把一次完整会话串起来看。我见过有人排查“为什么第三轮回答突然变差”结果发现前两轮的上下文根本没传进去就是因为没有 session 视图。3.3 异步和并发场景下的埋点陷阱Agent 系统大量使用异步和并发这是埋点最容易出问题的地方。核心陷阱是上下文传递。Langfuse 的 SDK 依赖 contextvars 来维护当前 Trace 的上下文。在同步代码里这没问题但在asyncio.create_task或者线程池里contextvars 不会自动传递导致子任务的 Span 挂不到父 Trace 上变成孤立的 Trace。解决办法是显式传递 trace 对象而不是依赖隐式的上下文async def process_batch(trace, items): tasks [process_one(trace, item) for item in items] await asyncio.gather(*tasks) async def process_one(trace, item): span trace.span(nameprocess-item, input{item: item}) # ... 处理逻辑 span.end(output{result: result})这样每个子任务都显式持有父 trace 的引用无论怎么调度父子关系都不会丢。代价是代码里多传一个参数但比起排查“Trace 断链”的痛苦这点成本完全值得。4. 实操过程从零搭一套 Agent 可观测链路4.1 环境准备与自托管部署自托管我用的是官方 Docker Compose但做了几处调整。先拉官方仓库git clone https://github.com/langfuse/langfuse.git cd langfuse官方 compose 文件里默认用 Postgres 存所有数据量大的话要改成 ClickHouse 模式。关键环境变量# 数据库 DATABASE_URLpostgresql://postgres:passwordpostgres:5432/langfuse CLICKHOUSE_URLhttp://clickhouse:8123 CLICKHOUSE_USERdefault CLICKHOUSE_PASSWORDpassword # 对象存储存大文本 LANGFUSE_S3_EVENT_UPLOAD_BUCKETlangfuse LANGFUSE_S3_EVENT_UPLOAD_ENDPOINThttp://minio:9000 LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_IDminioadmin LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEYminioadmin # 密钥用于 SDK 认证 NEXTAUTH_SECRETyour-secret SALTyour-salt启动之后进 Web 界面创建项目拿到public key和secret key这两个是 SDK 认证用的。提示NEXTAUTH_SECRET和SALT一定要改成随机值别用默认的。这两个是加密和会话签名用的用默认值等于门没锁。4.2 SDK 接入与第一个 TracePython 项目接入pip install langfuse环境变量配置LANGFUSE_PUBLIC_KEYpk-lf-xxx LANGFUSE_SECRET_KEYsk-lf-xxx LANGFUSE_HOSThttp://your-langfuse-host:3000然后就是前面 3.1 节那段埋点代码。跑一次之后去 Langfuse 界面应该能看到一条 Trace点进去是树形结构能看到 intent-recognition 和 llm-call 两个节点以及各自的耗时和 token。这里有个细节SDK 是异步批量上报的默认每 0.5 秒或攒够一定数量才发一次。所以程序跑完立刻去界面看可能看不到要么等几秒要么在程序结束前调langfuse.flush()。生产环境里进程常驻这个问题不明显但脚本类任务一定要记得 flush否则最后几条 Trace 会丢。4.3 用 Score 做质量评估Trace 记录的是“发生了什么”Score 记录的是“发生得好不好”。Langfuse 支持三种打分方式人工打分、模型打分LLM-as-a-Judge、代码规则打分。人工打分适合小批量抽检在界面上点一下就行。模型打分适合大批量自动化比如用一个便宜的模型给每次回答打 1-5 分。代码规则打分适合有明确标准的场景比如“回答里必须包含订单号”。模型打分的实现思路是把 Trace 的输入输出取出来喂给一个评估模型让它按 rubric 打分然后把分数写回对应的 Trace。def score_trace(trace_id: str, query: str, answer: str): judge_prompt f请给下面这个回答打分1-5分只输出数字。 用户问题{query} 模型回答{answer} 评分标准准确性、完整性、是否有幻觉。 score_result call_judge_model(judge_prompt) langfuse.score( trace_idtrace_id, nameanswer-quality, valueint(score_result), commentauto-judged by gpt-4o-mini, )这里有个坑评估模型本身也会产生成本而且如果评估模型和被评估模型是同一个会有“自己评自己”的偏差。我的做法是用一个不同厂商的便宜模型做 judge比如主模型用 GPT-4ojudge 用 Claude Haiku 或者国产的轻量模型交叉验证能减少偏差。4.4 Dataset 与实验对比换 prompt 不再靠感觉这是 Langfuse 我觉得最被低估的功能。很多人换 prompt 的方式是改一版跑几个 case 看看感觉不错就上线。这种方式的问题是没有基线改好了不知道好多少改差了不知道差在哪。正确做法是建一个 Dataset把有代表性的 case 固化下来包括输入和期望输出然后每次改 prompt 都跑一遍完整 Dataset用 Score 对比。# 创建数据集 dataset langfuse.create_dataset(nameweather-agent-eval) # 添加测试用例 for case in test_cases: langfuse.create_dataset_item( dataset_nameweather-agent-eval, inputcase[query], expected_outputcase[expected], ) # 跑实验 for item in dataset.items: trace langfuse.trace(nameeval-run, metadata{promptVersion: v3}) output run_agent(item.input) langfuse.score(trace_idtrace.id, nameexact-match, valueoutput item.expected_output)跑完之后在界面上能直接看到 v2 和 v3 两个版本的得分对比。Dataset 的价值在于把“感觉”变成“数字”尤其是 Agent 这种非确定性系统没有量化对比根本没法判断改动是正向还是负向。5. 常见问题与排查技巧实录5.1 Trace 丢失或不完整这是最高频的问题。表现是明明调用了模型但 Langfuse 里看不到或者只看到一半。排查顺序我总结成一张表现象可能原因排查方法完全没有 Trace环境变量没生效打印langfuse.auth_check()只有部分 Trace进程退出前没 flush加langfuse.flush()子节点丢失异步上下文没传递检查是否显式传 traceTrace 延迟出现批量上报未触发正常现象等几秒或手动 flush报 401key 配错或 host 不对检查 public/secret key 和 hostlangfuse.auth_check()这个方法是排查第一步它会实际发一个请求验证认证比看环境变量靠谱。5.2 高并发下 Langfuse 成为瓶颈Agent 系统扛并发的时候如果埋点写得不好Langfuse 的上报会拖慢主流程。我实测过一个反例每个 Span 都同步等待上报完成QPS 一上来延迟直接翻倍。解决办法是确保上报是异步的并且设置合理的批量参数。SDK 默认就是异步批量但如果你在代码里手动调了flush()就变成同步了。生产环境不要在请求路径里 flush只在进程退出或定时任务里 flush。另外自托管的 Langfuse 后端也要能扛住写入量。ClickHouse 的批量写入参数可以调max_insert_block_size和async_insert这两个参数对高并发写入影响很大。我一般会开async_insert1让 ClickHouse 自己攒批写入吞吐能提升好几倍。5.3 成本失控的定位方法Agent 烧钱通常有三个原因循环调用、上下文膨胀、模型选型不当。用 Langfuse 定位的思路是先按userId或feature聚合看总成本找到异常高的维度然后进到具体 Trace看 Generation 的 token 分布。如果单个 Generation 的 input token 特别大是上下文膨胀如果 Generation 数量特别多是循环调用如果 token 正常但单价高是模型选型问题。我遇到过一次典型的循环调用Agent 在工具返回空结果时会重新规划再试但重试逻辑没有上限导致某些 case 下循环了十几次。Langfuse 的 Trace 树里能清楚看到十几个结构相同的 Span 串在一起一眼就能定位。提示给 Agent 设置最大迭代次数是基本功但光有上限不够还要在 Langfuse 里对“迭代次数”打点这样才能发现“哪些 query 总是触发多次迭代”从 prompt 层面优化。5.4 评测结果不稳定怎么办LLM-as-a-Judge 的评分波动是常见问题。同一个回答跑两次可能一个 4 分一个 5 分。这不是 Langfuse 的问题是模型本身的不确定性。缓解办法有三个一是把 temperature 设成 0减少随机性二是用多个 judge 取平均比如跑三次取中位数三是把评分标准写得更具体减少模型的自由发挥空间。我现在的做法是 rubric 写得非常细比如“回答中包含具体温度数值得 1 分包含穿衣建议得 1 分”把主观判断拆成可验证的客观项。6. 一些踩坑之后的个人体会Langfuse 这类工具最大的价值不是让你“看到”数据而是逼着你在写代码之前就想清楚系统的关键路径和失败模式。我接完 Langfuse 之后最大的收获其实是重新审视了一遍 Agent 的架构哪些节点是必须的、哪些状态是应该传递的、哪些失败是应该重试的。这些问题在没有可观测性的时候都是靠猜有了之后才有据可依。如果让我给正在做 Agent 项目的人一个建议那就是别等到出问题才接可观测性从第一个版本就接上。前期多花的这点时间会在第一次线上事故的时候连本带利还给你。至于 Langfuse 本身它的 API 设计足够简单自托管也不复杂真正需要花心思的是“埋什么、怎么埋、怎么用”这部分没有标准答案得结合你自己的业务慢慢磨。
返回列表