news 2026/10/5 11:51:31

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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。
  • Span:Agent 的每一个逻辑步骤。比如“意图识别”是一个 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( name="agent-query", user_id=user_id, metadata={"feature": "weather-assistant", "env": "prod"}, tags=["v2-prompt"], ) # 意图识别节点 intent_span = trace.span(name="intent-recognition", input={"query": query}) intent = classify_intent(query) intent_span.end(output={"intent": intent}) # 模型调用节点 generation = trace.generation( name="llm-call", model="gpt-4o-mini", input=[{"role": "user", "content": query}], ) response = call_llm(query) generation.end( output=response.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(name="process-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_URL=postgresql://postgres:password@postgres:5432/langfuse CLICKHOUSE_URL=http://clickhouse:8123 CLICKHOUSE_USER=default CLICKHOUSE_PASSWORD=password # 对象存储(存大文本) LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://minio:9000 LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minioadmin LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=minioadmin # 密钥(用于 SDK 认证) NEXTAUTH_SECRET=your-secret SALT=your-salt

启动之后,进 Web 界面创建项目,拿到public key和secret key,这两个是 SDK 认证用的。

提示:NEXTAUTH_SECRET和SALT一定要改成随机值,别用默认的。这两个是加密和会话签名用的,用默认值等于门没锁。

4.2 SDK 接入与第一个 Trace

Python 项目接入:

pip install langfuse

环境变量配置:

LANGFUSE_PUBLIC_KEY=pk-lf-xxx LANGFUSE_SECRET_KEY=sk-lf-xxx LANGFUSE_HOST=http://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_id=trace_id, name="answer-quality", value=int(score_result), comment="auto-judged by gpt-4o-mini", )

这里有个坑:评估模型本身也会产生成本,而且如果评估模型和被评估模型是同一个,会有“自己评自己”的偏差。我的做法是用一个不同厂商的便宜模型做 judge,比如主模型用 GPT-4o,judge 用 Claude Haiku 或者国产的轻量模型,交叉验证能减少偏差。

4.4 Dataset 与实验对比:换 prompt 不再靠感觉

这是 Langfuse 我觉得最被低估的功能。很多人换 prompt 的方式是:改一版,跑几个 case 看看,感觉不错就上线。这种方式的问题是没有基线,改好了不知道好多少,改差了不知道差在哪。

正确做法是建一个 Dataset,把有代表性的 case 固化下来(包括输入和期望输出),然后每次改 prompt 都跑一遍完整 Dataset,用 Score 对比。

# 创建数据集 dataset = langfuse.create_dataset(name="weather-agent-eval") # 添加测试用例 for case in test_cases: langfuse.create_dataset_item( dataset_name="weather-agent-eval", input=case["query"], expected_output=case["expected"], ) # 跑实验 for item in dataset.items: trace = langfuse.trace(name="eval-run", metadata={"promptVersion": "v3"}) output = run_agent(item.input) langfuse.score(trace_id=trace.id, name="exact-match", value=output == item.expected_output)

跑完之后在界面上能直接看到 v2 和 v3 两个版本的得分对比。Dataset 的价值在于把“感觉”变成“数字”,尤其是 Agent 这种非确定性系统,没有量化对比根本没法判断改动是正向还是负向。

5. 常见问题与排查技巧实录

5.1 Trace 丢失或不完整

这是最高频的问题。表现是:明明调用了模型,但 Langfuse 里看不到,或者只看到一半。

排查顺序我总结成一张表:

现象可能原因排查方法
完全没有 Trace环境变量没生效打印langfuse.auth_check()
只有部分 Trace进程退出前没 flush加langfuse.flush()
子节点丢失异步上下文没传递检查是否显式传 trace
Trace 延迟出现批量上报未触发正常现象,等几秒或手动 flush
报 401key 配错或 host 不对检查 public/secret key 和 host

langfuse.auth_check()这个方法是排查第一步,它会实际发一个请求验证认证,比看环境变量靠谱。

5.2 高并发下 Langfuse 成为瓶颈

Agent 系统扛并发的时候,如果埋点写得不好,Langfuse 的上报会拖慢主流程。我实测过一个反例:每个 Span 都同步等待上报完成,QPS 一上来延迟直接翻倍。

解决办法是确保上报是异步的,并且设置合理的批量参数。SDK 默认就是异步批量,但如果你在代码里手动调了flush(),就变成同步了。生产环境不要在请求路径里 flush,只在进程退出或定时任务里 flush。

另外,自托管的 Langfuse 后端也要能扛住写入量。ClickHouse 的批量写入参数可以调,max_insert_block_size和async_insert这两个参数对高并发写入影响很大。我一般会开async_insert=1,让 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 设计足够简单,自托管也不复杂,真正需要花心思的是“埋什么、怎么埋、怎么用”,这部分没有标准答案,得结合你自己的业务慢慢磨。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 11:48:23

无人共享羽毛球售卖软件源码:架构、模块与落地实战

这套源码是我在上海跑了大半年场地、改了三版架构才跑通的。先交代一下背景:羽毛球馆夜场散客买不到球、前台下班没人卖货、社恐人士不想隔着窗口喊价,这三个痛点叠加起来,就是“无人共享羽毛球售卖”最真实的商业场景。所谓“软件源码”&…

作者头像 李华
网站建设 2026/10/5 11:47:42

Spring Boot档案数字化项目管理系统全流程设计与实现

一说档案数字化项目管理,很多人第一反应就是"做个台账、管管进度",但真做过的都懂,这套系统最麻烦的从来不是CRUD,而是怎么把"扫描件、质检流程、人员绩效、批次流转"这些琐碎环节串成一条不打架的业务链。我…

作者头像 李华
网站建设 2026/10/5 11:47:39

COSCon 2025中国开源年会参会指南:从会前准备到现场逛展全攻略

从没参加过开源年会的人,第一次听到 COSCon 可能一脸懵:这是什么活动?开源跟我有什么关系?过去十年里,我参加过不少场 COSCon,从最早几百人的技术趴,到后来几千人挤满会场,亲眼看着它…

作者头像 李华
网站建设 2026/10/5 11:44:53

Java家庭理财系统源码实战:JDBC连接、业务改造与避坑全指南

简介:这是一份基于Java的家庭理财系统完整源码,面向具备一定Java基础、希望学习前后端分离项目实战或进行二次开发的开发者。系统采用B/S结构,整合微服务组件、Redis缓存、RabbitMQ消息队列与Nginx静态服务器,前端基于React与Ant …

作者头像 李华
网站建设 2026/10/5 11:44:13

Cursor插件开发:AI工作流下的沙盒化插件设计与实战

1. 项目概述:从“plugins”这个词开始,我们到底在聊什么? “plugins”不是个新词,但最近半年它在开发者圈子里的热度,几乎追平了“agent”和“TypeScript”。你刷技术社区、看GitHub trending、甚至翻国内开发群聊天记…

作者头像 李华
网站建设 2026/10/5 11:44:12

RFM客户分层模型从原理到SQL实操:用数据分析优化用户运营策略

做用户运营这些年,我最大的一个体会就是:80%的团队在做客户分层时,用的还是“按消费金额排序,取前20%”这种粗暴打法。结果就是运营资源投给了一批高客单但已经流失的“僵尸大户”,真正的绩优股反而被晾在一边。大概两…

作者头像 李华