讲的是Langfuse里trace、span、observation这三层数据结构怎么用。你会学到它们分别记录什么(一次请求、一个步骤、一次模型调用)、怎么嵌套组织、怎么通过三级结构还原一次完整的LLM调用过程、怎么用它做性能瓶颈定位和成本归因。适合用Langfuse做LLM监控的运维和开发深入使用。读完能判断这项技术适不适合你的场景,怎么用。
需要学习Langfuse的trace/span/observation三级结构,自动化流程的部署和监控需要工程化能力。本篇涵盖MLOps/LLMOps、模型部署、CI/CD、版本管理,帮助你实现AI工程化交付。
目录
可视化演示
展示防火墙规则匹配过滤、入侵检测系统特征扫描与安全策略执行过程,动态演示数据包检查与放行/拦截决策。
本节配有交互动效(进入视口后自动加载,约 3KB)。
防火墙包过滤 检查数据包源/目的端口与协议,匹配ACL规则决定放行或拦截 默认拒绝策略:未匹配规则的数据包被拦截,确保最小权限 ACL: match(src,dst,proto) → allow|deny
Langfuse三级追踪结构概述与核心概念
Langfuse是为LLM应用设计的开源可观测性平台,其核心数据结构是trace/span/observation三级追踪模型。这个模型借鉴了分布式系统中的链路追踪思想(如OpenTelemetry),但针对LLM应用的特殊性做了定制:observation层专门记录模型调用的prompt、response、token消耗和成本,这些是传统APM工具不具备的字段。三级结构的设计目标是同时支持宏观链路分析(哪个步骤最慢)和微观调用诊断(哪次模型调用token最多)两个维度。
Trace是顶层追踪单元,对应一次用户请求的完整处理生命周期。例如在RAG应用中,用户发送一个问题到系统返回答案,这整个过程是一个trace。Trace携带session_id关联同一用户的多次请求,支持会话级追踪;携带user_id和metadata支持多维过滤分析。Trace的status字段标识请求成功、失败或超时,用于异常请求的快速筛选。
Span是中间层追踪单元,对应请求处理中的一个逻辑步骤。在RAG应用中,检索文档、重排序、生成答案可以分别是一个span。Span通过parent关系嵌套在Trace下,记录每个步骤的开始和结束时间,用于计算步骤耗时和识别性能瓶颈。Span支持嵌套——一个Span下可以包含子Span,表达复杂的多层处理流程。
Observation是最细粒度追踪单元,对应一次具体的LLM API调用或工具调用。Observation记录完整的input prompt和output response用于内容审计,usage字段记录prompt_tokens和completion_tokens用于成本归因,model字段标识调用的具体模型版本。Observation是成本分析和质量诊断的核心数据源,其丰富的元数据字段使其远超简单的日志记录。
Trace层请求级追踪与上下文管理
Trace作为追踪体系的顶层容器,其核心价值在于将分散的span和observation组织为有意义的调用链。没有trace,每次LLM调用只是孤立的日志条目,无法知道这些调用属于同一个用户请求、经历了哪些处理步骤。Trace通过唯一的trace_id将所有相关span和observation关联起来,形成完整的请求视图。
Trace的上下文管理能力体现在三个维度。第一,session_id关联同一用户的多次请求,支持会话级追踪——例如分析同一用户在一轮对话中的多次查询,观察上下文积累对token消耗的影响。第二,user_id和metadata支持多维过滤分析——可以按用户群体、请求来源、功能模块等维度筛选trace进行聚合分析。第三,tags字段支持自定义标签,如标记为“production“或”staging“区分环境,标记为”vip_user”区分用户等级。
Trace的创建方式有两种:手动创建和自动注入。手动创建通过Langfuse SDK的trace()方法显式创建,适合需要在trace上附加丰富元数据的场景。自动注入通过框架集成(如LangChain callback、OpenAI wrapper)自动创建trace,开发者无需修改业务代码即可获得追踪能力。两种方式可以混合使用——自动注入创建trace后手动添加metadata和tags。
Trace的状态管理对运维至关重要。status字段支持success、error、timeout三种状态,异常trace可以通过status快速筛选。结合error message和stack trace字段,可以在trace级别直接定位请求失败的原因,无需逐层下钻span和observation。对于超时场景,可以设置trace级别的超时阈值,超时后自动标记status为timeout并记录超时时间。
Span层步骤级追踪与执行流程分解
Span将一次用户请求的处理过程分解为可分析的逻辑步骤。在RAG应用中,典型步骤包括:查询理解(query understanding)、文档检索(retrieval)、重排序(reranking)、上下文组装(context assembly)和答案生成(generation)。每个步骤是一个span,span的start_time和end_time计算步骤耗时,通过对比各span的耗时占比识别性能瓶颈。
Span的嵌套能力是处理复杂工作流的关键。一个span下可以包含子span,形成多层级的执行树。例如Agent应用中,“推理循环”是一个span,其下每一轮推理是一个子span,每轮推理中的工具调用是更下一层的子span。这种嵌套结构精确表达了执行流程的层次关系和依赖关系。嵌套深度本身也是诊断信息——如果嵌套过深(如Agent推理循环超过10层),可能暗示架构设计问题需要告警。
Span的name和metadata字段用于标注步骤类型和记录步骤级中间结果。name标识步骤名称(如"retrieve_documents"),metadata记录步骤的输入输出摘要(如检索到的文档数量、重排序后的top-k分数)。这些中间结果不需要像observation那样记录完整的prompt和response,但足以支持步骤级别的效果分析——例如分析检索步骤返回的文档数量与最终答案质量的相关性。
Span的并行执行通过时间戳而非父子关系表达。当多个步骤并行执行时(如同时查询多个向量数据库),它们作为同一层级的兄弟span存在,通过start_time和end_time的时间重叠关系表达并发结构。Langfuse的瀑布图(waterfall view)可视化展示各span的执行时序,并行span在时间轴上重叠排列,串行span依次排列,直观呈现执行流程的并行度和瓶颈点。
Observation层模型调用追踪与详情记录
Observation记录一次具体的LLM API调用或工具调用的完整详情。与span不同,observation不仅记录时间维度信息,还记录调用的内容维度信息:完整的input prompt和output response用于内容审计和质量分析,model字段标识调用的具体模型版本(如gpt-4o-2024-05-13)支持模型维度分析,usage字段记录prompt_tokens和completion_tokens用于成本归因。这些字段使observation成为LLM应用可观测性中最丰富的数据载体。
成本归因是observation的核心价值之一。每次observation记录token消耗和对应的成本(根据模型定价计算),通过按span聚合observation的成本,可以得到每个处理步骤的成本占比。例如在RAG应用中,检索步骤的embedding调用成本和生成步骤的LLM调用成本分别统计,帮助团队理解成本结构并针对性优化——如果发现检索步骤的embedding成本过高,可以考虑换用更便宜的embedding模型或减少检索文档数量。
Observation的model字段支持模型版本追踪和A/B测试分析。当团队切换模型版本(如从gpt-4o切换到gpt-4o-mini)时,通过observation的model字段可以对比新旧模型在相同场景下的表现:延迟差异、token消耗差异、成本差异。这种基于实际调用数据的模型对比比离线benchmark更贴近生产环境的真实表现。
Observation不仅限于LLM调用,还可以记录工具调用(tool_calls)、API调用等扩展追踪范围。例如Agent应用中的代码执行、数据库查询、外部API调用都可以作为observation记录,将追踪范围从纯LLM调用扩展到完整的工具执行链路。每个observation的metadata字段可以记录工具特定的信息(如SQL查询语句、HTTP响应码),支持工具维度的分析。
三级嵌套关系与完整调用链还原
三级嵌套关系通过parent_observation_id和parent_span_id字段建立,形成树状调用链结构。Trace是根节点,Span是中间节点,Observation是叶子节点。一个trace包含多个span(作为直接子节点或嵌套子节点),一个span包含多个observation。这种树状结构精确还原了LLM应用的执行流程:从用户请求(trace)到处理步骤(span)到具体调用(observation),每一层提供更细粒度的信息。
调用链还原能力支持从宏观到微观的全链路追踪和根因定位。当用户报告响应慢时,首先在trace级别查看总耗时,然后在span级别对比各步骤耗时找到最慢的步骤(如检索占60%),最后在observation级别分析该步骤中的具体调用(如向量检索的embedding调用延迟异常)。这种逐层下钻的诊断方式比端到端延迟监控更精确,能定位到具体的根因。
并行执行的span和observation通过时间戳而非父子关系表达并发结构。当RAG应用同时查询多个数据源时,多个retrieval span作为兄弟节点并行存在,它们的start_time相近、end_time相近,在瀑布图中呈现为时间轴上的重叠条带。正确区分并行关系和嵌套关系对于解读调用链至关重要——并行span是兄弟关系(共享同一个parent),嵌套span是父子关系(一个span在另一个span内部)。
嵌套深度和广度本身是架构健康度的指标。健康的RAG应用通常有2-3层span嵌套(检索、生成),Agent应用的嵌套深度取决于推理循环次数。如果嵌套深度异常(如超过10层),可能暗示Agent陷入无限循环或架构设计过于复杂。Langfuse支持设置嵌套深度告警阈值,超过阈值时自动通知开发者检查。
性能瓶颈定位与耗时分析实践
基于三级追踪结构的性能瓶颈定位采用逐层下钻策略。首先在trace级别获取总延迟,然后在span级别对比各步骤的duration,找出耗时占比最大的步骤(瓶颈步骤),最后在observation级别分析该步骤中具体调用的延迟分布。例如某RAG应用的trace总延迟8秒,span分析显示检索步骤占4.8秒(60%),生成步骤占2.4秒(30%),其他步骤占0.8秒(10%)。进一步下钻检索步骤的observation,发现向量检索的embedding调用延迟3.5秒,定位到索引未命中导致全表扫描的问题。
P99延迟分析需基于trace而非单个observation。端到端P99延迟反映用户实际体验,包含了所有步骤的耗时和步骤间的等待时间。如果只看observation级别的P99延迟,可能忽略步骤间的序列化开销、队列等待等隐藏延迟。正确的做法是在trace级别计算P99延迟,然后在span级别分析P99延迟的构成——哪些步骤在P99场景下特别慢(可能和P50场景不同)。
耗时瀑布图(waterfall view)是性能分析的可视化工具。它将trace中所有span和observation按时间轴排列,每个条带的长度表示耗时,条带之间的间隙表示步骤间等待时间。通过瀑布图可以直观发现:并行度不足(大量串行条带)、某个步骤异常慢(特别长的条带)、步骤间等待时间过长(条带间的间隙大)。瀑布图是定位性能瓶颈最有效的可视化手段。
延迟分析还需要区分LLM推理延迟和网络传输延迟。Observation级别的延迟包含两部分:LLM模型的推理时间(取决于模型大小、prompt长度和生成长度)和网络传输时间(取决于API服务器的地理位置和网络状况)。通过对比同一模型在不同网络环境下的observation延迟,可以分离出网络延迟的影响。如果网络延迟占比高,可以考虑部署模型到更近的Region或使用API代理缓存。
成本归因与Token消耗追踪分析
成本归因基于observation的usage字段计算每次调用的token费用。每个observation记录prompt_tokens和completion_tokens,结合模型的定价(如gpt-4o的输入$0.005/1K tokens、输出$0.015/1K tokens),计算出每次调用的精确成本。按observation聚合得到span级别的成本(如检索步骤的总成本),按span聚合得到trace级别的成本(一次用户请求的总成本),按user_id聚合得到用户级别的成本统计。
成本异常检测通过历史基线对比识别token消耗突增的异常调用。系统统计每个span类型的历史平均token消耗和成本,当新trace中某个span的成本超过历史均值的3倍标准差时,触发异常告警。常见异常原因包括:prompt注入导致输入token暴增、模型陷入循环生成导致输出token暴增、检索返回过多文档导致context过长。异常检测帮助团队在成本失控前及时发现问题。
按span类型分摊成本支持精细化的成本优化决策。在RAG应用中,将总成本分摊到检索、重排序、生成等步骤,发现生成步骤占总成本的70%。优化策略可以是:减少生成步骤的prompt长度(压缩检索到的文档)、换用更便宜的模型处理简单查询、对高频查询使用缓存避免重复调用。按用户维度的成本统计还能识别高成本用户(如频繁发送长文本的用户),为定价策略提供数据支持。
成本追踪的长期价值在于支持模型选型和架构决策。通过对比不同模型在相同任务上的cost-per-query和输出质量,找到性价比最优的模型配置。例如发现gpt-4o-mini在简单查询上的质量与gpt-4o相当但成本只有1/10,将简单查询路由到gpt-4o-mini可以节省60%的总成本。这种基于生产数据的成本-质量权衡分析是离线benchmark无法提供的。
AI 应用场景
LLM应用调用链追踪与性能诊断
阶段:监控运维
解决的问题:LLM应用调用链路复杂涉及多步骤多模型调用,缺乏结构化追踪导致性能瓶颈和成本异常难以定位
具体案例:ml-engineer使用Langfuse三级追踪结构监控RAG应用,将一次用户查询的完整流程记录为一个trace,检索步骤和生成步骤分别记录为span,每次LLM API调用记录为observation包含输入输出prompt和token消耗。通过trace视图发现检索步骤耗时占总延迟的60%而LLM生成仅占30%,进一步下钻发现向量检索的observation耗时异常,定位到索引未命中导致全表扫描的问题,优化后响应时间从8秒降到3秒。
多模型协作场景的成本归因分析
阶段:监控运维
解决的问题:多模型协作场景下成本来源分散,无法精确追溯每次请求中各模型调用的成本占比,导致成本优化缺乏数据支撑
具体案例:mlops-engineer管理一个包含embedding模型、reranker和LLM生成模型的RAG流水线,每月API成本超预算30%。通过Langfuse的observation级成本追踪,发现reranker的token消耗占40%(因为每次传入50个候选文档),优化为只传入top-10后reranker成本下降75%,总成本回到预算范围内。按trace汇总的成本统计还帮助识别出5%的高成本用户贡献了30%的总成本,推动团队引入查询长度限制。
Python 演示
import timeimport uuidfrom typing import List, Dict, Optionalfrom dataclasses import dataclass, fieldfrom datetime import datetime @dataclassclass Observation: """最细粒度追踪单元:一次LLM API调用或工具调用""" id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) name: str = "" parent_span_id: Optional[str] = None model: str = "" input_prompt: str = "" output_response: str = "" prompt_tokens: int = 0 completion_tokens: int = 0 total_tokens: int = 0 cost: float = 0.0 latency_ms: float = 0.0 start_time: float = 0.0 end_time: float = 0.0 metadata: Dict = field(default_factory=dict) def to_dict(self) -> Dict: return { "id": self.id, "name": self.name, "type": "observation", "parent_span_id": self.parent_span_id, "model": self.model, "input": self.input_prompt[:50] + "...", "output": self.output_response[:50] + "...", "usage": {"prompt_tokens": self.prompt_tokens, "completion_tokens": self.completion_tokens, "total_tokens": self.total_tokens}, "cost": self.cost, "latency_ms": round(self.latency_ms, 1) } @dataclassclass Span: """中间层追踪单元:一个逻辑处理步骤""" id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) name: str = "" trace_id: str = "" parent_span_id: Optional[str] = None start_time: float = 0.0 end_time: float = 0.0 observations: List[Observation] = field(default_factory=list) child_spans: List["Span"] = field(default_factory=list) metadata: Dict = field(default_factory=dict) @property def duration_ms(self) -> float: return (self.end_time - self.start_time) * 1000 def add_observation(self, obs: Observation): obs.parent_span_id = self.id self.observations.append(obs) def add_child_span(self, child: "Span"): child.parent_span_id = self.id self.child_spans.append(child) def to_dict(self) -> Dict: return { "id": self.id, "name": self.name, "type": "span", "parent_span_id": self.parent_span_id, "duration_ms": round(self.duration_ms, 1), "observations": [o.to_dict() for o in self.observations], "child_spans": [s.to_dict() for s in self.child_spans] } @dataclassclass Trace: """顶层追踪单元:一次用户请求的完整处理生命周期""" id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) name: str = "" session_id: str = "" user_id: str = "" start_time: float = 0.0 end_time: float = 0.0 status: str = "success" spans: List[Span] = field(default_factory=list) metadata: Dict = field(default_factory=dict) total_cost: float = 0.0 @property def duration_ms(self) -> float: return (self.end_time - self.start_time) * 1000 def add_span(self, span: Span): span.trace_id = self.id self.spans.append(span) def compute_total_cost(self) -> float: total = 0.0 for span in self.spans: for obs in span.observations: total += obs.cost for child in span.child_spans: for obs in child.observations: total += obs.cost self.total_cost = total return total def to_dict(self) -> Dict: return { "id": self.id, "name": self.name, "type": "trace", "session_id": self.session_id, "user_id": self.user_id, "duration_ms": round(self.duration_ms, 1), "status": self.status, "total_cost": round(self.total_cost, 4), "spans": [s.to_dict() for s in self.spans] } class LangfuseTracer: """Langfuse三级追踪器:管理trace/span/observation的创建和关联""" def __init__(self): self.traces: List[Trace] = [] def create_trace(self, name: str, session_id: str = "", user_id: str = "") -> Trace: trace = Trace(name=name, session_id=session_id, user_id=user_id, start_time=time.time()) self.traces.append(trace) return trace def create_span(self, trace: Trace, name: str, parent: Optional[Span] = None) -> Span: span = Span(name=name, trace_id=trace.id, start_time=time.time()) if parent: parent.add_child_span(span) else: trace.add_span(span) return span def create_observation(self, span: Span, name: str, model: str, input_prompt: str, output: str, prompt_tokens: int, completion_tokens: int, cost_per_1k: float = 0.002) -> Observation: start = time.time() total_tokens = prompt_tokens + completion_tokens cost = (total_tokens / 1000.0) * cost_per_1k obs = Observation( name=name, model=model, input_prompt=input_prompt, output_response=output, prompt_tokens=prompt_tokens, completion_tokens=completion_tokens, total_tokens=total_tokens, cost=cost, start_time=start, end_time=start + 0.05 + total_tokens * 0.0001 ) span.add_observation(obs) return obs def finish_span(self, span: Span): span.end_time = time.time() def finish_trace(self, trace: Trace): trace.end_time = time.time() trace.compute_total_cost() def analyze_bottleneck(self, trace: Trace) -> Dict: """性能瓶颈分析:找出耗时最长的span""" span_durations = [] for span in trace.spans: span_durations.append({ "name": span.name, "duration_ms": round(span.duration_ms, 1), "pct": 0.0 }) for child in span.child_spans: span_durations.append({ "name": f" └─{child.name}", "duration_ms": round(child.duration_ms, 1), "pct": 0.0 }) total = max(trace.duration_ms, 0.001) for s in span_durations: s["pct"] = round(s["duration_ms"] / total * 100, 1) span_durations.sort(key=lambda x: x["duration_ms"], reverse=True) return {"total_ms": round(total, 1), "bottleneck": span_durations[0], "breakdown": span_durations} def demo_langfuse_tracing(): """演示Langfuse三级追踪结构在RAG应用中的使用""" tracer = LangfuseTracer() print("=" * 60) print("Langfuse 三级追踪结构演示:RAG应用调用链") print("=" * 60) trace = tracer.create_trace("RAG Query", session_id="sess_001", user_id="user_42") print(f"\n[Trace] {trace.name} (session={trace.session_id})") # Span 1: 检索步骤 retrieve_span = tracer.create_span(trace, "retrieve_documents") print(f" [Span] {retrieve_span.name}") obs1 = tracer.create_observation( retrieve_span, "vector_search", "text-embedding-3-small", "What is machine learning?", "[0.12, -0.34, ...]", prompt_tokens=8, completion_tokens=0, cost_per_1k=0.0001 ) print(f" [Observation] {obs1.name}: model={obs1.model}, " f"tokens={obs1.total_tokens}, cost=${obs1.cost:.6f}") obs2 = tracer.create_observation( retrieve_span, "rerank", "bge-reranker-v2", "5 candidate documents", "Top 3 documents selected", prompt_tokens=500, completion_tokens=0, cost_per_1k=0.001 ) print(f" [Observation] {obs2.name}: model={obs2.model}, " f"tokens={obs2.total_tokens}, cost=${obs2.cost:.6f}") tracer.finish_span(retrieve_span) # Span 2: 生成步骤 generate_span = tracer.create_span(trace, "generate_answer") print(f" [Span] {generate_span.name}") obs3 = tracer.create_observation( generate_span, "llm_generation", "gpt-4o", "Context: ... Question: What is ML?", "Machine learning is a subset of AI...", prompt_tokens=1200, completion_tokens=350, cost_per_1k=0.005 ) print(f" [Observation] {obs3.name}: model={obs3.model}, " f"tokens={obs3.total_tokens}, cost=${obs3.cost:.6f}") tracer.finish_span(generate_span) tracer.finish_trace(trace) # 性能分析 print(f"\n{'=' * 60}") print("性能瓶颈分析") print(f"{'=' * 60}") analysis = tracer.analyze_bottleneck(trace) print(f"总耗时: {analysis['total_ms']}ms") for item in analysis["breakdown"]: print(f" {item['name']}: {item['duration_ms']}ms ({item['pct']}%)") print(f"\n总成本: ${trace.total_cost:.6f}") print(f"Trace状态: {trace.status}") if __name__ == "__main__": demo_langfuse_tracing()
class Observation:最细粒度追踪单元,记录一次LLM API调用的完整详情:模型、输入输出、token消耗和成本class Span:中间层追踪单元,代表一个逻辑处理步骤,包含多个observation和可选的子spanclass Trace:顶层追踪单元,代表一次用户请求的完整生命周期,包含多个spandef compute_total_cost(self) -> float:遍历所有span和observation累加成本,实现trace级别的成本归因class LangfuseTracer:追踪器管理三级结构的创建和关联,提供trace/span/observation的工厂方法def create_observation(self, span: Span, name: str, model: str,创建observation并自动关联到父span,根据token消耗计算成本def analyze_bottleneck(self, trace: Trace) -> Dict:性能瓶颈分析:计算各span耗时占比,找出延迟贡献最大的步骤retrieve_span = tracer.create_span(trace, "retrieve_documents")创建检索步骤的span,嵌套在trace下,代表RAG流程的检索阶段obs1 = tracer.create_observation(在检索span下创建向量检索的observation,记录embedding模型调用详情analysis = tracer.analyze_bottleneck(trace)对完成的trace执行瓶颈分析,输出各步骤耗时占比和最大瓶颈
常见问答
Trace、Span、Observation三者是什么关系?
Observation和Span有什么区别?
如何利用三级追踪结构定位性能瓶颈?
成本归因是如何实现的?
session_id在追踪中起什么作用?
常见误区
误区认为trace、span、observation是三个独立的平行追踪类型而非嵌套关系
正解它们是严格的层级嵌套结构:一个trace包含多个span作为子节点,一个span包含多个observation作为叶子节点,形成树状调用链。理解这种嵌套关系是正确解读追踪数据和定位问题的关键
为何常见:初次接触分布式追踪概念时容易将不同层级的追踪单元理解为平行关系,特别是当文档分别介绍三种类型时容易忽视其嵌套语义
误区认为observation只记录LLM调用的输入输出文本内容
正解Observation不仅记录输入输出文本,还记录token消耗(prompt_tokens和completion_tokens)、模型名称、调用参数(temperature和max_tokens等)、延迟和计算成本等元数据,这些字段是成本归因和性能分析的细粒度数据源
为何常见:Observation的字面意思偏向观测内容,使用者容易将其理解为仅记录文本输入输出,忽视了它作为结构化数据载体的完整字段集
误区认为三级追踪结构只适用于简单的单轮请求-响应式LLM应用
正解通过span的灵活嵌套可以表达复杂的多步骤工作流:RAG的检索-重排-生成链路用三个嵌套span表示,Agent的多轮推理循环用循环嵌套的span表示,多模型协作场景用并行span表示。三级结构的表达能力远超简单的请求-响应模式
为何常见:初学者通常从最简单的单次LLM调用场景入门,容易将追踪结构与简单场景绑定,忽视了span嵌套机制对复杂工作流的表达能力
参考来源
- webpageLangfuse Tracing DocumentationLangfuse官方追踪文档,详细介绍trace/span/observation三级结构的概念、API和使用方法
- webpageLangfuse Analytics & Cost TrackingLangfuse分析和成本追踪文档,介绍基于observation的token消耗和成本归因分析
- webpageOpenTelemetry Traces - SignalsOpenTelemetry追踪概念文档,Langfuse追踪模型的设计灵感来源
- webpageLangfuse FAQLangfuse常见问题解答,包含三级结构的常见疑问和最佳实践