ARTICLE DETAIL

资讯详情

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

AI Agent 可观测性实战:Langfuse 链路追踪与工程化落地

AI Agent 可观测性实战:Langfuse 链路追踪与工程化落地 1. 从“能跑通”到“敢上线”AI Agent 工程化卡在哪我最早接触 AI Agent 是在一个内部知识库问答项目上。当时用 LangChain 把 LLM、向量检索和几个工具函数串起来本地跑得挺顺Demo 演示也拿得出手。但真正推到测试环境让业务方试用之后问题就来了用户说“答得不对”我打开日志一看只有一行HTTP 200中间检索到了什么文档、模型收到了什么 prompt、工具调用了几次、每次返回了什么全是黑盒。排查一次问题要复现、加 print、重新部署来回折腾大半天。这个场景我相信做过 AI Agent 的人都不陌生。Agent 和传统后端服务最大的区别在于它的执行路径是非确定性的。同一个输入模型可能走完全不同的工具调用链检索结果也可能因为 embedding 的细微差异而不同。传统 APM 工具比如看 QPS、看 P99 延迟、看错误率能告诉你“服务挂了”但告诉不了你“Agent 为什么做了这个决策”。这就是可观测性Observability在 AI Agent 场景下必须单独拿出来讲的原因。Langfuse 就是在这个背景下进入我视野的。它是一个开源的 LLM 工程平台核心能力可以概括成三块Trace链路追踪、Prompt 管理、评测Evaluation。Trace 解决的是“每一步发生了什么”Prompt 管理解决的是“版本怎么控”评测解决的是“改完之后到底变好还是变坏”。这三块合起来才构成从“模型调用”到“全链路可观测”的完整闭环。这篇文章适合两类人看一类是已经把 Agent 跑起来、但被线上问题折磨得够呛的工程师另一类是正准备搭 Agent、想一开始就把可观测性设计进去的开发者。我会按“为什么需要—怎么接入—怎么用起来—怎么避坑”的顺序把我在实际项目里踩过的坑和总结的方法讲清楚。文中涉及的具体参数和配置我会说明取值理由方便你按自己的场景调整。2. Langfuse 到底补的是哪块拼图2.1 传统日志在 Agent 场景下的三个盲区先说清楚 Langfuse 解决的不是“有没有日志”的问题而是“日志够不够用”的问题。我在项目里对比过纯日志方案和 Langfuse 方案差距主要体现在三个地方。第一个盲区是层级关系丢失。一个 Agent 请求内部可能嵌套了多次 LLM 调用、多次工具调用、多次检索它们之间有父子关系。用普通日志打出来是一堆平铺的行你得靠时间戳和 request_id 自己脑补调用树。Langfuse 的 Trace 天然是树形结构一次用户请求是一个 Trace下面挂若干 Span每个 Span 有类型generation、retrieval、tool、chain 等层级一目了然。第二个盲区是Token 和成本不可见。LLM 调用是按 Token 计费的一个复杂 Agent 一次请求可能消耗几万 Token。如果不做统计月底账单出来你都不知道钱花在哪条链路上。Langfuse 会自动记录每次 generation 的 input/output Token 数并按你配置的模型单价算出成本可以按用户、按会话、按版本聚合。第三个盲区是输入输出不可回溯。传统日志出于隐私和体积考虑通常不会把完整的 prompt 和 response 打出来。但 Agent 调试恰恰最需要看这两样东西。Langfuse 把每次调用的完整输入输出都存下来配合 UI 可以直接对比不同版本的 prompt 效果。2.2 Trace、Span、Generation 三个核心概念理解 Langfuse 的数据模型是接入的前提。我用一个实际例子来说明。假设用户问“帮我查一下上个月华东区的销售冠军是谁”Agent 的执行过程是先调用 LLM 判断意图然后调用数据库查询工具拿到结果后再调用 LLM 生成自然语言回答。这个过程在 Langfuse 里会呈现为一个Trace代表这次完整的用户请求有 trace_id、user_id、session_id、tags 等元数据。三个Span分别对应“意图判断”“数据库查询”“答案生成”三个步骤。Span 是通用的操作单元可以嵌套。两个Generation对应两次 LLM 调用。Generation 是 Span 的特殊类型额外记录 model、prompt、completion、token 用量、成本等 LLM 专属信息。这个层级设计的好处是你既可以从顶层 Trace 看整体耗时和成本也可以下钻到某个 Generation 看具体的 prompt 内容。排查“为什么答错了”的时候通常的路径是先看 Trace 整体定位到可疑的 Span再展开 Generation 看模型到底收到了什么。2.3 和 LangSmith、Phoenix 的取舍市面上同类工具不少我选 Langfuse 主要基于三点考虑这里如实说一下不是拉踩。开源自托管是首要因素。Langfuse 可以完全部署在自己的服务器上数据不出内网。对于有数据合规要求的团队这一点几乎是决定性的。LangSmith 是 SaaS 为主Phoenix 是 Arize 开源的但更偏评测。框架无关是第二点。Langfuse 提供 Python 和 JS/TS 的 SDK也有 OpenTelemetry 的接入方式不绑定 LangChain。我有个项目用的是自研的 Agent 调度逻辑照样能接入。如果你的技术栈是 FastAPI LangChain LangGraph 这套组合Langfuse 有现成的 callback handler接入成本很低。评测能力内置是第三点。很多可观测工具只管“看”不管“评”。Langfuse 内置了数据集、评分score、LLM-as-a-judge 这些评测原语改完 prompt 之后可以直接跑回归测试不用再搭一套评测系统。提示如果你的团队完全没有自托管运维能力且数据敏感度不高SaaS 版本上手更快。但只要涉及用户隐私数据或内部业务数据我建议优先考虑自托管。3. 接入实战从零把 Trace 跑通3.1 自托管部署的最小可用配置Langfuse 官方推荐用 Docker Compose 部署。最小可用配置需要这几个组件Langfuse ServerWeb API、Postgres存元数据和 Trace、Clickhouse存大规模 Trace 数据、Redis队列和缓存、MinIO存原始 payload。我实际部署时用的是官方仓库里的docker-compose.yml但做了几处调整。第一处是给 Clickhouse 单独挂了数据卷因为 Trace 数据增长很快容器重建时不能丢。第二处是调整了 Postgres 的连接池大小默认值在并发稍高时会不够用。第三处是给 Server 配了LANGFUSE_INIT_ORG_ID等初始化环境变量避免每次重建都要手动建组织和项目。部署命令大致是这样git clone https://github.com/langfuse/langfuse.git cd langfuse # 修改 docker-compose.yml 中的数据卷和资源限制 docker compose up -d启动后访问 3000 端口用初始化时设置的管理员账号登录创建一个组织和一个项目拿到public_key和secret_key。这两个 key 就是后续 SDK 接入的凭证。注意自托管版本默认没有开启认证如果部署在公网务必在前面加一层反向代理并配置访问控制。我见过有人直接把 3000 端口暴露出去结果 Trace 数据被人爬走的情况。3.2 Python SDK 的初始化与第一个 TraceSDK 接入有两种方式装饰器observe和上下文管理器langfuse.trace()。我推荐新手从装饰器开始侵入性最小。先装依赖pip install langfuse然后配置环境变量这是最推荐的方式避免 key 硬编码在代码里export LANGFUSE_PUBLIC_KEYpk-lf-xxx export LANGFUSE_SECRET_KEYsk-lf-xxx export LANGFUSE_HOSThttp://your-langfuse-host:3000初始化客户端并写第一个 Tracefrom langfuse import Langfuse from langfuse.decorators import observe langfuse Langfuse() observe() def my_agent_query(user_input: str): # 这里放你的 Agent 逻辑 result call_llm(user_input) return result observe(as_typegeneration) def call_llm(prompt: str): # 实际调用 LLM response llm_client.chat(prompt) # 手动上报 token 用量和模型信息 langfuse.update_current_generation( modelgpt-4o-mini, usage{input: 120, output: 80}, ) return responseobserve()装饰的函数会自动创建一个 Spanas_typegeneration则创建 Generation。函数之间的调用关系会自动形成父子层级。这个设计很巧妙你几乎不用改业务逻辑加个装饰器就有 Trace 了。3.3 在 LangChain / LangGraph 里挂 Callback如果你的 Agent 是基于 LangChain 或 LangGraph 搭的接入更简单直接用官方 callback handlerfrom langfuse.callback import CallbackHandler langfuse_handler CallbackHandler( user_iduser_123, session_idsession_abc, tags[production, v2], ) # LangChain 场景 chain.invoke({input: ...}, config{callbacks: [langfuse_handler]}) # LangGraph 场景 graph.invoke(state, config{callbacks: [langfuse_handler]})这里有个细节值得说user_id和session_id一定要传。user_id让你能按用户维度看成本和问题分布session_id让你能把多轮对话串起来看。我一开始图省事没传后来想分析“哪个用户的问题最多”时发现数据没法关联只能重新埋点。LangGraph 场景下还有个进阶用法把langfuse_handler放进config的callbacks里LangGraph 的每个节点执行都会自动生成对应的 Span节点之间的边也会体现在 Trace 结构里。这样你看到的 Trace 就和你的图结构一一对应排查哪个节点慢、哪个节点报错非常直观。3.4 手动埋点补齐框架覆盖不到的地方框架自动埋点覆盖的是 LLM 调用和链式调用但 Agent 里往往有些自定义逻辑比如权限校验、缓存查询、结果后处理。这些地方需要手动埋点。手动埋点用上下文管理器with langfuse.start_as_current_span(namepermission_check) as span: allowed check_permission(user_id, resource) span.update(output{allowed: allowed}) if not allowed: span.update(levelWARNING, status_message权限不足)level字段可以设成DEBUG、DEFAULT、WARNING、ERROR在 UI 里会按颜色区分。我习惯把业务上“预期内的失败”比如权限不足、参数校验不通过标成 WARNING把真正的异常标成 ERROR。这样在 Trace 列表里一眼就能区分“正常拒绝”和“系统故障”。4. 让 Trace 真正产生价值四个落地场景4.1 定位“答非所问”的完整排查链路这是 Langfuse 最高频的使用场景。我拿一个真实案例走一遍排查过程。用户反馈“问它退款政策它答的是发货时间。”我打开 Langfuse按user_id和大致时间范围筛出这条 Trace。整体看下来耗时正常没有报错。展开 Trace 树看到三个 Span意图识别、知识检索、答案生成。先看意图识别这个 Generationprompt 里用户问题是“退款要几天到账”模型输出的意图是query_shipping。问题定位到了意图识别错了。再看这个 Generation 的 prompt发现意图分类的候选列表里query_refund和query_shipping的描述写得很接近模型混淆了。修复方案就很明确了把两个意图的描述改得更区分或者在 prompt 里加几个 few-shot 例子。改完之后我把这条 Trace 的输入存进 Langfuse 的数据集作为回归测试用例。下次再改意图识别 prompt 时跑一遍数据集就能确认没有引入新的错误。这个链路的关键在于没有 Trace你只能看到“答错了”这个结果有了 Trace你能看到“在哪一步错的、为什么错的”。排查时间从半天缩短到十分钟。4.2 用 Session 视图分析多轮对话的上下文漂移单轮问答的排查相对简单多轮对话才是真正的难点。用户可能在第三轮突然说“那它呢”这个“它”指代什么只有结合前两轮才能理解。Langfuse 的 Session 视图把同一个session_id下的所有 Trace 按时间排列你可以像看聊天记录一样看整个对话过程。我遇到过一个典型问题用户在第一轮问了 A 产品的价格第二轮问了 B 产品的功能第三轮问“哪个更划算”。Agent 在第三轮只检索了 B 产品的信息因为它把“哪个”理解成了只指 B。通过 Session 视图我发现问题出在对话历史的截断策略上。为了控制 Token代码里只保留了最近两轮对话导致第一轮的 A 产品信息丢失了。修复方案是改成基于 Token 数的动态截断而不是固定轮数。这个问题的根因只有把多轮 Trace 串起来看才能发现。4.3 成本归因找出最烧钱的链路上线一个月后我发现 LLM 账单比预期高不少。用 Langfuse 的成本分析功能按 Trace 聚合 Token 用量很快找到了三个“烧钱大户”。第一个是某个检索增强的链路每次都会把检索到的 10 篇文档全文塞进 prompt单次消耗上万 Token。优化方案是只保留最相关的 3 篇并对文档做摘要压缩Token 用量直接降了 60%。第二个是重试逻辑没有上限。某些情况下模型返回格式不对代码会重试但重试时把完整对话历史又发了一遍导致成本翻倍。加了最大重试次数和退避策略后解决。第三个是开发环境的调试流量混进了生产统计。后来通过tags区分环境和用途成本报表才准确。这里的关键经验是成本优化不能靠猜要靠数据。Langfuse 的按维度聚合能力让你能快速定位到“哪条链路、哪个用户、哪个版本”最烧钱。4.4 用 Score 和数据集做 Prompt 回归测试Prompt 改动的风险在于修好了 A 场景可能弄坏了 B 场景。没有回归测试你只能靠人工抽查覆盖不全。Langfuse 的评测流程是这样的先把一批有代表性的输入存成数据集Dataset每个条目可以带期望输出或评分标准。然后每次改完 prompt用数据集跑一遍给每个输出打分。打分可以是人工的也可以配置 LLM-as-a-judge 自动打分。我实际用下来LLM-as-a-judge 适合做粗筛比如判断“回答是否包含关键信息”“是否偏离主题”。精细的质量判断还是需要人工。但即便是粗筛也能挡住大部分明显的回归问题。数据集条目的来源我一般从两个地方收集一是线上出过问题的 Trace直接存进数据集二是主动构造的边界用例。前者保证“不再犯同样的错”后者保证“覆盖到没出过但可能出问题的场景”。5. 踩过的坑与性能优化5.1 Trace 上报拖慢主流程怎么办Langfuse SDK 默认是异步批量上报的正常情况下对主流程影响很小。但我遇到过一次线上延迟抖动排查发现是 Trace 上报阻塞了。原因是 SDK 的默认队列满了之后会阻塞等待而不是丢弃。当 Langfuse Server 响应慢或者网络抖动时队列积压主流程就被拖住了。解决方案是调整 SDK 的配置langfuse Langfuse( flush_at50, # 攒够 50 条就发 flush_interval1.0, # 或者每 1 秒发一次 max_retries2, # 最多重试 2 次 timeout5, # 单次请求超时 5 秒 )更关键的是我在业务代码里给 Trace 上报加了 try-except 兜底确保上报失败绝不影响主流程。可观测性工具本身不应该成为故障源这个原则要守住。5.2 敏感数据脱敏的三种做法Trace 里会记录完整的 prompt 和 response如果用户输入包含手机号、身份证号、内部业务数据直接存进去是有合规风险的。我实践下来有三种脱敏做法按侵入性从低到高排列。第一种是在 SDK 层做 mask。Langfuse 支持配置mask函数在数据上报前对指定字段做处理def mask_sensitive(data): # 对手机号、邮箱等做正则替换 return re.sub(r\d{11}, ***, data) langfuse Langfuse(maskmask_sensitive)第二种是在业务层做脱敏。在把用户输入传给 Agent 之前先过一遍脱敏逻辑Trace 记录的就是脱敏后的数据。这种做法最彻底但需要业务代码配合。第三种是只记录元数据不记录内容。对于特别敏感的场景可以配置只记录 Token 数、耗时、模型名不记录 prompt 和 response 原文。代价是排查问题时看不到具体内容需要权衡。我的建议是用户输入类数据用第一种内部业务数据用第二种涉及密钥、凭证的用第三种。5.3 高并发下的采样策略Agent 服务的 QPS 可能很高如果每条请求都完整记录 Trace存储成本和写入压力都很大。Langfuse 支持采样可以按比例记录。采样策略我一般分两层。第一层是按比例采样比如生产环境只记录 10% 的正常请求但 100% 记录报错请求。Langfuse 的 SDK 支持通过sample_rate参数控制。第二层是按重要性采样。给不同的请求打不同的tags比如 VIP 用户的请求全量记录普通用户按比例记录。这样既控制了成本又保证了关键链路的可观测性。注意采样率一旦设定历史数据的统计口径会受影响。比如你按 10% 采样那成本报表里的数字要乘以 10 才是真实值。建议在报表层面做换算而不是靠记忆。5.4 自托管版本的存储规划自托管 Langfuse 的存储增长比想象中快。我统计过一个中等规模的 Agent 服务每天几千次请求每次请求平均 5 个 SpanTrace 数据加上原始 payload一天大概增长几百 MB。一个月就是几十 GB。Clickhouse 的数据要定期做 TTL 清理Langfuse 支持配置数据保留天数。我的做法是原始 payload 保留 30 天聚合后的指标数据保留 1 年。这样既能满足近期排查需求又能控制存储成本。MinIO 存的是原始 payload是存储增长的主要来源。如果对历史 payload 没有强需求可以配置更短的保留期或者定期归档到冷存储。6. 把可观测性设计进 Agent 的骨架里6.1 从“事后加日志”到“事前设计 Trace 结构”我最大的体会是可观测性不应该是在出问题之后才补的东西而应该在设计 Agent 架构时就考虑进去。具体怎么做在设计 Agent 的每个节点时就问自己三个问题这个节点的输入是什么、输出是什么、可能出什么错。这三个问题的答案就是埋点的内容。比如一个检索节点输入是 query输出是文档列表和相关性分数可能的错误是检索为空或超时。把这些都记录到 Span 里排查时就不用猜。另一个实践是统一 Trace 的命名规范。我给 Span 命名时遵循模块名.操作名的格式比如retrieval.vector_search、llm.intent_classify、tool.db_query。这样在 UI 里筛选和聚合时非常方便也能一眼看出是哪个模块的问题。6.2 和现有监控体系的衔接Langfuse 不是要替代你现有的监控体系而是补充。我的做法是业务指标QPS、错误率、延迟继续用 Prometheus Grafana 看Agent 内部的决策链路用 Langfuse 看。两者通过trace_id关联。具体实现上我在请求入口生成一个trace_id同时写进日志和 Langfuse。当 Grafana 告警说错误率上升时我可以从日志里拿到trace_id直接跳到 Langfuse 看这条链路的详情。这个衔接让“发现异常”和“定位原因”之间的路径缩短了很多。6.3 团队协作中的 Prompt 版本管理Langfuse 的 Prompt 管理功能解决的是“谁在什么时候改了什么 prompt效果如何”的问题。它把 prompt 从代码里抽出来做成带版本号的独立实体。实际用法是在 Langfuse UI 里创建 prompt每次修改生成新版本可以给版本打标签比如production、staging。代码里通过名称和标签拉取 promptprompt langfuse.get_prompt(intent_classifier, labelproduction) compiled prompt.compile(user_inputuser_input)这样做的好处是改 prompt 不用改代码、不用重新部署运营和产品同学也能参与 prompt 优化。同时每次 Generation 都会关联到具体的 prompt 版本效果对比有据可查。不过要提醒一点Prompt 管理虽然方便但也要建立变更流程。我见过有人直接在 UI 上改了生产环境的 prompt导致线上效果波动。建议至少做到生产标签的变更需要 review变更后跑一遍回归数据集。7. 一些零散但重要的经验关于 Langfuse 的使用还有几个零散的点值得单独说。Trace 的 user_id 和 session_id 要尽早规划。这两个字段一旦开始用后期补数据的成本很高。建议在项目初期就确定好 user_id 的生成规则和 session_id 的传递方式。不要把所有东西都塞进 Trace。Trace 是给排查问题用的不是给业务分析用的。业务分析该用数据仓库就用数据仓库Trace 里只放和 Agent 决策相关的信息。我见过把用户完整行为日志都塞进 Trace 的结果 UI 卡得没法用。定期清理测试数据。开发调试产生的 Trace 会污染生产统计。用tags区分环境定期清理测试数据保证报表干净。关注 Langfuse 的版本升级。这个项目迭代很快新版本经常带来性能改进和新功能。但升级前一定要看 changelog特别是数据模型有变更的版本需要按官方指引做迁移。评测数据集要持续维护。数据集不是建一次就完事要随着业务变化不断补充新用例。我一般每个月 review 一次线上出问题的 Trace把有代表性的加进数据集。最后说一个我踩过的坑一开始我追求 Trace 的“全量记录”什么信息都往里塞结果 UI 加载慢、存储成本高、真正重要的信息反而被淹没。后来我做了减法只记录和决策相关的关键信息可观测性反而更好了。可观测性的价值不在于记录了多少而在于需要的时候能不能快速找到答案。这个认知转变是我用 Langfuse 最大的收获。
返回列表