☰
从 OpenTelemetry 到 Langfuse:企业级 Agent 可观测性实践
2026/10/1 21:23:06 网站建设 项目流程

1.1 OTel 和 Langfuse 的关系

一句话概括:OTel 负责「怎么标准化记录」,Langfuse 负责「怎么把记录理解成 Agent 能分析的样子」。

OpenTelemetry → Agent 执行记录成通用 Trace / SpanLangfuse → 在它的基础上补 Agent 语义,并做分析

而且 Langfuse 本身就是基于 OTel 实现的。

1.2 Langfuse 的定位

Langfuse 是一个面向 LLM / Agent 应用的可观测性与分析平台。它把 OTel 的通用 span 转换成带 Agent 语义的 observation(LLM 调用、工具调用、Agent 决策),并在 trace 上挂上 session、user、score 这些分析维度。

回到一次具体的 Agent 执行。OTel 能告诉你哪一步慢、哪一步报错、调用层级是什么。但对一个 LLM / Agent 应用,真正想知道的往往是另一批信息:

第一次 LLM 的输入是什么、输出是什么为什么决定调用这个 ToolTool 的参数和返回分别是什么每一步用了多少 Token、花了多少钱这次会话整体效果怎么样、属于哪个用户

OTel 的 span 知道「这是一次调用、耗时 50ms」,但不知道「这是一次 LLM 调用,用了 n 个 token,成本 m 」。这些字段属于 Agent 语义,要么自己定义,要么交给上层平台。

Langfuse 的核心作用就是把这层补齐:

  • OTel 原生:能直接接收 OTel 的 OTLP 数据,已有的埋点可以复用;
  • Agent 视角开箱即用:generation / tool / agent 分类型展示,token、成本、会话面板自动渲染;
  • 开源可自建:支持私有化部署,数据不出内网;
  • 不止 tracing:还涵盖评估(Evaluation)、Prompt 管理、数据集与实验。

二、核心原理

2.1 Langfuse

Langfuse 不是另一套 Trace 系统,底层核心还是基于 OpenTelemetry 实现的。

具体表现为三点:

  1. 一次请求在 Langfuse 里的 Trace ID,和 OTel 的trace_id是同一个值;
  2. Langfuse 里的一个Observation,就是 OTel 的一个 span 在 Langfuse 侧的表示;
  3. Langfuse 还能直接作为 OTel 后端使用——把 exporter 指向它的 OTLP 端点,原生 OTel 的 span 也能进 Langfuse。

2.2 Observation Type

OTel 的 span 只有通用类型;Langfuse 给它补上了类型标签,也就是as_type:

span 通用步骤(非 LLM 操作)generation LLM 调用,额外带 model / token / costtool 工具调用,带输入输出agent 能自主决策、调用工具的执行单元chain 多个步骤之间的衔接embedding 生成向量guardrail 内容防护、越狱检测

generation是其中最关键的一类。它不是「多了一种 span」,而是在 span 上固定了一组 LLM 专属字段:

model 用的哪个模型input / output 发给模型的和模型返回的usage_details 输入 / 输出 token 数cost_details 本次调用的成本

有了这组字段,token 统计、成本计算、模型对比这些,Langfuse 的 Dashboard 能自动渲染。

2.3 Trace 级维度

Agent 的效果往往要跨多次请求看,所以 Langfuse 在 trace 上挂了几个聚合维度:

session_id 把同一个会话的多轮请求归到一起user_id 关联到具体用户tags 自定义分类标签score 人工或自动评估的结果

前面的 Observation Type 解决「单次执行看到了什么」,这几个维度解决「多次执行放在一起怎么比较」。

2.4 数据结构

把同一个 Agent 的数据分别送进 Jaeger 和 Langfuse,形状是一样的都是树状,能回答的问题不同。

Jaeger 面向系统:

哪里慢、哪里报错、调用链是什么、服务之间怎么调用

Langfuse 面向 Agent:

模型到底说了什么、为什么调用这个 Tool、Tool 返回了什么用了多少 Token、这次请求花了多少钱不同 Model / Prompt 的效果对比、某个 Session 的整体体验

两者的关注点可以概括成:

OpenTelemetry → 系统发生了什么Langfuse → Agent 做了什么、效果怎么样

三、编码实现

下面用一个真实的LLM → Tool → LLM链路对比,不用单次函数调用,因为多步结构才能体现差异。

3.1 Agent 本身

原生没有可观测性的 Agent:

class ProductAgent: name = "product-agent" def __init__(self, llm, search_tool): self.llm = llm self.search_tool = search_tool def run(self, query): # 第一次 LLM:理解需求,决定是否调用 Tool response = self.llm.chat( messages=[ {"role": "system", "content": "你是电商商品助手,需要时调用商品搜索工具。"}, {"role": "user", "content": query}, ], tools=[self.search_tool], ) if response.tool_calls: tool_call = response.tool_calls[0] # 执行 Tool products = self.search_tool(**tool_call.arguments) # 第二次 LLM:根据 Tool 结果生成最终回答 return self.llm.chat( messages=[ {"role": "user", "content": query}, {"role": "assistant", "tool_calls": [tool_call]}, {"role": "tool", "content": products}, ] ) return response

执行链路:

User → LLM → Tool → LLM → Answer

3.2 原生 OpenTelemetry

OTel 只提供通用 span,所以 LLM、Tool 的语义和字段都要自己定义:

with tracer.start_as_current_span("product-agent"): with tracer.start_as_current_span("llm") as span: response = llm.chat(...) span.set_attribute("gen_ai.request.model", "claude-haiku-4-5") span.set_attribute("gen_ai.usage.input_tokens", response.usage.input_tokens) span.set_attribute("output.value", response.content) with tracer.start_as_current_span("search-product") as span: products = search_tool(**tool_call.arguments) span.set_attribute("tool.name", "search_product") span.set_attribute("tool.arguments", str(tool_call.arguments)) span.set_attribute("output.value", str(products)) with tracer.start_as_current_span("llm") as span: final_response = llm.chat(...) span.set_attribute("output.value", final_response.content)

得到的树:

product-agent├── llm├── search-product└── llm

结构没问题,耗时和父子关系都能看。但有一批东西需要应用自己建设:

这是 Agent 还是普通 span?这是 LLM 调用还是普通 span?Token 怎么统一统计?Cost 怎么算?Session、User 怎么关联?Evaluation 挂在哪?最终这些字段怎么渲染成面板?

OTel 提供的是最标准和最基础能力,这些 Agent 专项能力需要自己编码实现。

3.3 Langfuse

同一个 Agent,用 Langfuse SDK 写:

from langfuse import get_clientlangfuse = get_client()with langfuse.start_as_current_observation( as_type="agent", name="product-agent"): with langfuse.start_as_current_observation( as_type="generation", name="understand-query", model="claude-haiku-4-5" ) as generation: response = llm.chat(...) generation.update( input=query, output=response.content, usage_details={ "input": response.usage.input_tokens, "output": response.usage.output_tokens, }, ) with langfuse.start_as_current_observation( as_type="tool", name="search-product" ) as tool: products = search_tool(**tool_call.arguments) tool.update(input=tool_call.arguments, output=products) with langfuse.start_as_current_observation( as_type="generation", name="generate-answer", model="claude-haiku-4-5" ) as generation: final_response = llm.chat(...) generation.update( input=products, output=final_response.content, usage_details={ "input": final_response.usage.input_tokens, "output": final_response.usage.output_tokens, }, )

代码结构和 OTel 几乎一样,仍然是「开 observation → 做事 → 更新字段」。差别在于:

  • as_type直接声明这是agent/generation/tool,平台能识别并按类型渲染;
  • generation自带model、usage_details,token 和成本自动算;
  • input/output是固定字段,模型到底说了什么一眼可见。

如果还想把多轮会话和用户关联起来,加一层 trace 级属性即可:

from langfuse import propagate_attributeswith propagate_attributes(user_id="u_123", session_id="s_abc"): agent.run(query)

同一个会话的多轮请求会自动归到s_abc下,面板上就能按 session 看整体效果。


四、实践落地

4.1 痛点问题

单个 Agent 直接写with langfuse...没问题。但企业里业务场景一般都需要 Multi-Agent 架构:

MasterAgent├── ProductAgent├── OrderAgent├── RecommendAgent├── QAAgent└── AfterSaleAgent

如果每个 Agent 都手写 Langfuse,业务代码会变得繁琐冗余:

业务逻辑 + Langfuse + 异常处理 + Trace 管理 + Metrics + Logging

一旦要改字段规范、换后端、加采样,就要修改每个 Agent 代码。

4.2 定义 BaseAgent 基类

第三章的埋点直接写在执行流程里,业务和可观测性混在一起。第一反应是抽一个基类,把 agent 级的 observation 提到父类:

class BaseAgent: name = "base-agent" def run(self, query): with langfuse.start_as_current_observation( as_type="agent", name=self.name ): return self.execute(query)class ProductAgent(BaseAgent): name = "product-agent" def __init__(self, llm, search_tool): self.llm = llm self.search_tool = search_tool def execute(self, query): # 第一次 LLM:理解需求,决定是否调用 Tool response = self.llm.chat( messages=[ {"role": "system", "content": "你是电商商品助手,需要时调用商品搜索工具。"}, {"role": "user", "content": query}, ], tools=[self.search_tool], ) if response.tool_calls: tool_call = response.tool_calls[0] # 执行 Tool products = self.search_tool(**tool_call.arguments) # 第二次 LLM:根据 Tool 结果生成最终回答 return self.llm.chat( messages=[ {"role": "user", "content": query}, {"role": "assistant", "tool_calls": [tool_call]}, {"role": "tool", "content": products}, ] ) return response

业务 Agent 里已经看不到 langfuse,剩下的全是业务。agent 级的 observation 由基类统一开;内部的 generation / tool observation 由 LLM client 和工具包装统一产生——包装一次,所有 Agent 共用。

但基类的问题也很直接。它只管住了 agent 这一层,其他横切能力如果也往基类里塞:

BaseAgent├── Langfuse├── Logging├── Retry├── Timeout├── Metrics├── Guardrail└── Cache

一个 Agent 想加只属于自己的能力,就得改基类,影响所有子类。

4.3 Middleware & Hook

更合适的做法是不靠继承,用一个 Runner 在运行时给 Agent 包一层 Middleware:

Agent 只负责业务,Middleware 负责横切能力。

ProductAgent 不再继承 BaseAgent:

class ProductAgent: name = "product-agent" def __init__(self, llm, search_tool): self.llm = llm self.search_tool = search_tool def run(self, query): response = self.llm.chat( messages=[ {"role": "system", "content": "你是电商商品助手,需要时调用商品搜索工具。"}, {"role": "user", "content": query}, ], tools=[self.search_tool], ) if response.tool_calls: tool_call = response.tool_calls[0] products = self.search_tool(**tool_call.arguments) return self.llm.chat( messages=[ {"role": "user", "content": query}, {"role": "assistant", "tool_calls": [tool_call]}, {"role": "tool", "content": products}, ] ) return response

Middleware 只做一件事,包一层 observation:

class LangfuseMiddleware: def __init__(self, langfuse): self.langfuse = langfuse def __call__(self, agent, query, next): with self.langfuse.start_as_current_observation( as_type="agent", name=agent.name ): return next()

Runner 在运行时把中间件串起来:

runner = AgentRunner(middlewares=[LangfuseMiddleware(langfuse)])runner.run(ProductAgent(llm, search_tool), query)

执行过程:

以后新增一个 OrderAgent,只要写好run、接进 Runner 就自动带上可观测性,不用继承任何基类,也不用写一行可观测性代码。

Hook 是同一个思路的另一种写法,在 Agent 生命周期的关键节点回调:

class LangfuseHook: def before_run(self, agent, query): ... def after_run(self, agent, query, result): ... def on_error(self, agent, query, error): ... ``````plaintext Hook → 偏生命周期事件监听Middleware → 偏执行链包装与控制

只做 Trace、Logging、Metrics,两者都够用。如果要把 Tracing、Retry、Timeout、Guardrail、Fallback、Cache 统一承载,Middleware 更适合作为 Agent Runtime 的统一扩展机制。

4.4 最终形态

整个方案从「给一个 Agent 加埋点」演进成「Runtime 自带可观测性」:

到这一步,Langfuse 不再只是某个 Agent 的监控工具,而是整个 Agent Runtime 的一项基础能力。


五、总结

把 OTel 和 Langfuse 放在一起,是一条连续的路:

OpenTelemetry:给 Agent 建可观测性底座Langfuse:让这套底座真正服务于 LLM / Agent

落到 Multi-Agent 生产环境,接入方式的演进是:

手动埋点 → BaseAgent → Middleware / Hook → Agent Runtime

核心结论只有一句:从单个 Agent 到几十个 Agent,业务代码里不应该到处出现with langfuse...,可观测性应该是 Agent Runtime 的基础能力,而不是每个 Agent 自己负责的一段业务代码。

学AI大模型的正确顺序,千万不要搞错了

🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!

有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!

就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇

学习路线:

✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经

以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!

我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询