1. 为什么LLM智能体需要一台“行车记录仪”
做过智能体开发的人都有一个共同的痛:一个任务跑下来,模型调了七八次,工具调了十几次,最后输出错了,你盯着屏幕完全不知道是哪一步开始跑偏的。是检索环节召回了一堆无关内容?是工具调用参数拼错了?还是模型在多轮对话里把上下文理解拧了?没有链路级的记录,排查基本靠猜。
AgentTrace 要解决的就是这个问题。它本质上是一套面向 LLM 智能体的可观测性方案,给智能体装上一台“行车记录仪”——把每一次模型调用、每一次工具执行、每一轮推理决策完整记录下来,形成可回溯、可分析、可对比的调用链路。这个项目在社区里被反复讨论,核心原因在于它踩中了智能体从“能跑”到“跑得稳”之间最大的那道坎:可调试性。
这篇文章适合三类人看。第一类是正在做智能体开发、被线上问题折磨过的工程师;第二类是准备给团队引入可观测性体系的技术负责人;第三类是对 OpenTelemetry 这套标准感兴趣、想看看它怎么落到 LLM 场景里的开发者。我会从设计思路、核心机制、实操落地、问题排查几个角度,把这个项目拆开讲透,尽量让你看完就能在自己项目里复现一套类似的方案。
2. AgentTrace 的整体设计与核心思路拆解
2.1 智能体可观测性到底难在哪
传统后端服务的可观测性已经很成熟了:日志、指标、链路追踪三件套,OpenTelemetry 一套标准打通。但智能体这套东西和传统服务有本质区别。
传统服务的调用是确定性的:输入 A,经过固定的函数链路,输出 B。出错了,看堆栈、看日志、看耗时分布,基本能定位。智能体不一样,它的执行路径是动态生成的。同一个用户请求,模型可能决定先检索再调工具,也可能直接调工具,甚至可能多轮反思后才给出答案。执行路径不固定,意味着你没法用静态的调用图去描述它。
更麻烦的是,智能体的“中间状态”极其丰富。一次模型调用里包含了完整的 prompt、模型的推理过程(如果有思维链)、token 消耗、延迟;一次工具调用里包含了工具名、入参、返回值、执行耗时、是否报错。这些信息散落在各个 SDK 的回调里,如果不主动收集,跑完就没了。
我见过太多团队的做法是:在每个节点手动 print 日志,或者往数据库里塞记录。短期能用,但一旦智能体规模上来、节点变多、开始做多智能体编排,这套土办法立刻崩溃。日志格式不统一、链路对不上、跨进程追踪断裂,排查一个线上问题要翻好几个服务的日志文件。
AgentTrace 的思路很明确:用 OpenTelemetry 这套已经被验证过的分布式追踪标准,来承载智能体的调用链路。这个选择背后有很深的考量。
2.2 为什么选 OpenTelemetry 作为底座
先说结论:选 OpenTelemetry 不是因为它是“标准”所以政治正确,而是因为智能体的调用链路天然就是一棵树,而 OpenTelemetry 的 Span 模型天生就是为树形结构设计的。
一次智能体任务是一个根 Span,下面挂着若干子 Span:模型调用是一个 Span,工具调用是一个 Span,检索是一个 Span,如果有多智能体协作,每个子智能体的执行又是一个子 Span。这种父子嵌套关系,用 OpenTelemetry 的 trace_id 和 span_id 一挂,整条链路就串起来了。
第二个原因是生态。OpenTelemetry 的 exporter 支持把数据打到 Jaeger、Zipkin、Tempo、Prometheus 等一堆后端,你不需要自己写可视化界面。团队里如果有运维同学,他们大概率已经有一套现成的可观测性平台,AgentTrace 采集的数据直接接进去就行,不用重复造轮子。
第三个原因是跨语言、跨框架。智能体开发现在框架百花齐放,有的用 Python 写,有的用 TypeScript,有的基于 LangChain,有的自己手搓。OpenTelemetry 在各语言都有成熟的 SDK,AgentTrace 只要在框架层做适配,底层的数据模型是统一的。这一点在多智能体、多服务协作的场景下尤其重要。
提示:如果你的团队已经在用 OpenTelemetry 做后端服务的链路追踪,引入 AgentTrace 的迁移成本会非常低,因为数据模型和采集管道是复用的。
2.3 核心数据模型:一次智能体任务被拆成了什么
AgentTrace 把一次智能体执行拆成了几个层级的 Span,我按从粗到细的顺序讲。
最顶层是Agent Span,代表一次完整的智能体任务。它记录了任务的输入、最终输出、总耗时、总 token 消耗、状态(成功/失败/中断)。这个 Span 是你排查问题时的入口,先看它,判断问题是出在整体层面还是某个环节。
往下是LLM Span,代表一次模型调用。它记录的信息最丰富:完整的 prompt(包括 system message、历史消息、当前输入)、模型的原始输出、使用的模型名、temperature 等参数、prompt token 数和 completion token 数、首 token 延迟和总延迟。这里有个细节值得说:首 token 延迟和总延迟要分开记。首 token 延迟反映的是模型开始响应的速度,总延迟反映的是完整生成的时间。这两个指标在排查“用户觉得卡”的问题时指向完全不同的原因。
再往下是Tool Span,代表一次工具调用。记录工具名、入参、返回值、执行耗时、是否抛异常。工具调用是智能体最容易出问题的环节,参数拼错、超时、返回格式不符合预期,都会导致后续推理跑偏。把每次工具调用的入参和返回值完整记下来,是排查这类问题的关键。
如果涉及检索增强,还会有Retrieval Span,记录查询语句、召回的文档 ID 和片段、相似度分数、召回数量。检索质量直接决定模型能不能拿到正确上下文,这个 Span 是排查“模型答非所问”的第一现场。
这几个 Span 通过父子关系挂在一起,形成一棵完整的调用树。你在可视化界面里看到的就是:根节点是任务,展开后是若干模型调用和工具调用交替出现,每个节点点开都有完整的输入输出。
2.4 和纯日志方案的本质区别
有人会问:我打日志不也能记录这些信息吗,为什么要用 Span 这套东西?
区别在于关联性。日志是扁平的,一条一条按时间排列。当你有多个智能体并发执行、每个智能体又有几十次调用时,日志会交织在一起,你很难快速还原出“某一次任务”的完整链路。而 Span 通过 trace_id 天然做了分组,通过父子关系天然做了层级,你查一次任务就是查一棵树,不会和其他任务混淆。
另一个区别是结构化。日志是文本,你要从里面提取 token 数、耗时这些指标,得写正则解析。Span 的 attribute 是结构化的键值对,直接就能做聚合分析。比如你想统计“所有工具调用里耗时超过 5 秒的占比”,用 Span 数据一条查询就出来了,用日志得写一堆解析逻辑。
3. 核心细节解析与实操要点
3.1 Span 的埋点位置怎么选
埋点位置选得好不好,直接决定这套可观测性方案有没有用。我的经验是:在框架的抽象层埋,不要在业务代码里埋。
什么意思?如果你在每个业务函数里手动加埋点代码,一是侵入性强,二是容易漏,三是框架升级后埋点代码要跟着改。正确的做法是在智能体框架的核心执行器上做拦截。比如 LangChain 的 callback 机制、LangGraph 的节点执行器,这些都是天然的埋点位置。AgentTrace 这类项目通常会在框架层提供一个 wrapper 或者 callback handler,你只要在初始化智能体时挂上,所有调用自动被记录。
具体到实操,模型调用的埋点要包住整个请求-响应周期,包括网络传输时间。工具调用的埋点要包住工具函数的执行,包括参数序列化和结果反序列化。检索的埋点要包住向量库查询。每个埋点都要记录开始时间、结束时间、状态、以及该环节特有的属性。
注意:埋点不要记录敏感信息。prompt 里可能包含用户隐私数据,工具入参里可能有鉴权 token。AgentTrace 这类方案通常提供脱敏配置,比如对特定字段做哈希或者掩码。这一点在生产环境是硬要求,别等出了事才补。
3.2 上下文传播:跨进程、跨智能体怎么串链路
单进程单智能体的链路好串,难的是多智能体协作和跨服务调用。
多智能体场景下,主智能体调用子智能体,子智能体可能跑在另一个进程甚至另一台机器上。这时候 trace 上下文需要通过某种方式传递过去。OpenTelemetry 的标准做法是通过context propagation,把 trace_id 和 span_id 放在请求头或者消息体里传下去。子智能体收到后,用这个上下文创建自己的 Span,这样整条链路就串起来了。
实操中容易踩的坑是:很多智能体框架在调用子智能体时,是重新发起一个独立的请求,没有携带上下文。你需要手动在调用处注入上下文,或者在框架的消息传递层做统一处理。我建议在框架的“智能体间通信”这一层做统一拦截,而不是在每个调用点手动传。
跨服务调用同理。如果你的工具调用是走 HTTP 请求到另一个服务,需要在 HTTP header 里带上 trace 上下文。OpenTelemetry 的各语言 SDK 通常有自动注入的机制,但需要你正确配置 propagator。
3.3 采样策略:全量记录还是按需采样
生产环境不可能全量记录所有 Span,数据量太大,存储成本扛不住。采样策略是必须认真设计的。
常见的采样方式有三种。头部采样是在任务开始时决定采不采,简单但可能漏掉出问题的任务。尾部采样是等任务结束后,根据结果决定采不采,比如所有失败的任务全采、成功的按 1% 采。尾部采样对排查问题更友好,但实现复杂,需要先把数据缓存在内存里。
我的建议是分层采样:错误和异常的任务全量采集,慢请求(超过阈值)全量采集,正常请求按低比例采样。这样既控制了数据量,又保证了出问题时一定有数据可查。AgentTrace 这类方案通常会提供采样配置接口,你可以根据业务特点调整。
还有一个细节:采样决策要在根 Span 做,子 Span 跟随根 Span 的决策。否则会出现根 Span 没采、子 Span 采了,链路断裂的情况。
3.4 数据脱敏与合规处理
这一块单独拎出来讲,因为它是很多团队上线可观测性时最容易忽视、也最容易出事的环节。
智能体的 prompt 和工具入参里,可能包含用户手机号、身份证号、订单信息、内部接口的鉴权凭证。这些数据如果原样落到追踪系统里,一旦追踪系统的访问权限管理不严,就是数据泄露。
实操上要做几件事。第一,在埋点层做字段级脱敏,对已知的敏感字段(如 password、token、id_card)做掩码或哈希。第二,提供可配置的脱敏规则,不同业务线的敏感字段不一样,不能写死。第三,追踪系统的访问权限要收紧,不是所有人都能看全量数据。第四,保留期限要设置,追踪数据不是审计数据,不需要永久保留,一般保留 7 到 30 天足够排查问题。
提示:脱敏要在数据离开应用进程之前做,不要指望在存储端做。数据一旦落到磁盘,就多了一个泄露面。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
假设你用的是 Python 技术栈,智能体基于 LangChain 或 LangGraph 开发。先装依赖:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp pip install agenttrace如果你要把数据打到 Jaeger 做本地调试,再起一个 Jaeger 容器:
docker run -d --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ jaegertracing/all-in-one:latest16686 是 Jaeger 的 UI 端口,4317 是 OTLP 的 gRPC 接收端口。本地调试用 all-in-one 镜像最省事,生产环境要换成正式的部署方案。
4.2 初始化追踪器与导出器
初始化代码大概长这样:
from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource resource = Resource.create({ "service.name": "my-agent-service", "service.version": "1.0.0", "deployment.environment": "production", }) provider = TracerProvider(resource=resource) exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(provider)这里有几个参数值得说明。service.name是必须的,它决定了你在 Jaeger 里怎么筛选服务。BatchSpanProcessor是批量导出,比SimpleSpanProcessor性能好很多,生产环境必须用批量。批量导出的参数可以调,比如max_queue_size、schedule_delay_millis,默认值一般够用,高并发场景要调大队列。
insecure=True只在本地调试用,生产环境要走 TLS。
4.3 给智能体挂上追踪回调
以 LangChain 为例,AgentTrace 通常提供一个 callback handler:
from agenttrace.integrations.langchain import AgentTraceCallbackHandler handler = AgentTraceCallbackHandler( tracer=trace.get_tracer("agenttrace"), capture_prompt=True, capture_completion=True, redact_fields=["api_key", "password", "id_card"], ) agent = initialize_agent( tools=tools, llm=llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, callbacks=[handler], )capture_prompt和capture_completion控制是否记录完整的输入输出。生产环境如果担心数据量,可以关掉,只记录 token 数和耗时。redact_fields是脱敏字段列表,命中的字段会被替换成掩码。
挂上之后,智能体每次执行都会自动产生 Span,你不需要在业务代码里加任何东西。
4.4 手动埋点补充关键业务信息
框架自动埋的点覆盖了模型调用和工具调用,但有些业务特有的信息需要手动补。比如你想记录这次任务的用户 ID、会话 ID、业务类型,方便后续按维度分析。
tracer = trace.get_tracer("agenttrace") with tracer.start_as_current_span("agent_task") as span: span.set_attribute("user.id", user_id) span.set_attribute("session.id", session_id) span.set_attribute("task.type", "customer_support") result = agent.run(user_input) span.set_attribute("task.status", "success") span.set_attribute("task.output_length", len(result))手动埋点的原则是:只记框架记不了的、但对排查问题有用的信息。不要重复记框架已经记了的东西。
4.5 在 Jaeger 里看链路
跑完一次任务,打开 Jaeger UI,选好 service 和时间范围,就能看到这次任务的完整链路。根节点是agent_task,展开后是若干llm_call和tool_call交替出现。
排查问题的顺序我一般是这样的:先看根 Span 的总耗时和状态,判断是整体慢还是某一步慢。然后按耗时排序子 Span,找到最耗时的那个环节。如果是模型调用慢,看是首 token 慢还是生成慢;如果是工具调用慢,看是工具本身慢还是网络慢。最后看失败或异常的 Span,点开看完整的入参和返回值,基本就能定位问题。
这里有个实用技巧:给 Span 打上业务标签,比如task.type、user.tier。这样你可以在 Jaeger 里按标签筛选,比如只看“VIP 用户的失败任务”,排查效率会高很多。
5. 常见问题与排查技巧实录
5.1 链路断裂:为什么子 Span 找不到父 Span
这是最常见的坑。现象是 Jaeger 里看到一堆孤立的 Span,没有父子关系。
原因通常是上下文没有正确传播。在单进程内,OpenTelemetry 用 contextvars 自动传播,一般不会出问题。跨进程或跨线程时,上下文会丢。比如你用线程池执行工具调用,子线程里拿不到父线程的上下文。
解决办法是在提交任务到线程池时,手动把上下文传过去:
from opentelemetry import context ctx = context.get_current() executor.submit(context.attach(ctx), tool_func, args)异步场景同理,asyncio.create_task之前要确保上下文已经设置好。
5.2 数据量爆炸:追踪系统被写满
生产环境跑一段时间后,追踪系统的存储被写满,或者查询变得极慢。这是采样策略没设计好。
先检查是不是全量采集了。如果是,立刻上采样。我的经验值是:正常请求采样率 1% 到 5%,错误请求 100%,慢请求 100%。这样既能控制数据量,又不会漏掉问题。
另外检查 Span 的 attribute 是不是记了太多大字段。完整的 prompt 和 completion 可能很长,如果每个 Span 都记,数据量会很大。可以考虑只记长度和哈希,需要看详情时再去日志系统里查。
5.3 性能损耗:追踪拖慢了智能体
有人反馈加了追踪后,智能体响应变慢了。这通常是导出器配置不当。
SimpleSpanProcessor是同步导出,每个 Span 结束就发一次网络请求,性能极差。换成BatchSpanProcessor,批量异步导出,性能影响可以忽略。
另外检查 exporter 的 endpoint 是不是跨机房了。如果追踪后端和智能体不在同一个机房,网络延迟会拖慢导出。生产环境建议追踪后端和业务服务同机房部署。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决办法 |
|---|---|---|---|
| 链路断裂,Span 孤立 | 上下文未传播 | 检查跨线程/跨进程调用 | 手动传递 context |
| 追踪系统写满 | 采样率过高 | 查看采集量统计 | 调整采样策略 |
| 智能体变慢 | 同步导出 | 检查 SpanProcessor 类型 | 换 BatchSpanProcessor |
| 敏感信息泄露 | 未脱敏 | 检查 Span attribute | 配置脱敏规则 |
| Span 缺失 | 埋点未生效 | 检查 callback 是否挂上 | 确认初始化顺序 |
| 时间戳错乱 | 时钟不同步 | 检查各节点时间 | 统一 NTP 同步 |
5.5 几个我踩过的坑
第一个坑:初始化顺序。OpenTelemetry 的 TracerProvider 必须在智能体初始化之前设置好,否则 callback handler 拿到的 tracer 是空的。我一开始把追踪初始化放在智能体初始化之后,结果一个 Span 都没采到,排查了半天。
第二个坑:异步上下文丢失。LangChain 的异步接口在某些版本里会丢失上下文,导致子 Span 挂不上。解决办法是在异步入口处手动 attach 上下文,或者升级到修复了这个问题的版本。
第三个坑:采样决策不一致。根 Span 决定采样,但子 Span 在另一个进程里重新做了采样决策,导致链路不完整。解决办法是把采样决策编码进 trace 上下文,子进程读取父进程的决策,不要重新决策。
第四个坑:脱敏规则不完整。上线后发现某个工具调用的入参里带了内部系统的 session token,没在脱敏列表里。后来我们把脱敏规则做成可配置的,并且加了定期审计,确保新接入的工具都会检查敏感字段。
6. 从单智能体到多智能体的追踪扩展
单智能体的追踪跑通后,多智能体场景是下一个要面对的。多智能体的追踪复杂度主要来自两个方面:智能体之间的调用关系和并发执行。
调用关系上,主智能体调用子智能体时,子智能体的执行应该作为主智能体某个 Span 的子 Span。这要求调用时传递 trace 上下文。如果子智能体是独立部署的服务,上下文通过请求头传递;如果是同进程内的函数调用,上下文自动传播。
并发执行上,多个子智能体同时跑,它们的 Span 会并行产生。OpenTelemetry 的模型天然支持这种并行结构,你看到的就是根 Span 下面挂着多个并行的子 Span 树。排查时要注意区分是哪个子智能体出了问题。
多智能体场景下我建议额外记录几个属性:agent.name(哪个智能体)、agent.role(扮演什么角色)、parent.agent(被谁调用)。这样在链路图里能快速看出智能体之间的协作关系。
还有一个实践是给智能体间的消息传递也埋点。主智能体给子智能体发了什么指令、子智能体返回了什么结果,这些消息是排查多智能体协作问题的关键。很多时候问题不是出在单个智能体内部,而是出在智能体之间的信息传递上——主智能体给的指令有歧义,或者子智能体返回的结果格式不符合主智能体的预期。
7. 追踪数据还能怎么用
追踪数据采回来,最直接的用途是排查问题。但它的价值不止于此。
性能优化。通过分析 Span 的耗时分布,你能找到智能体执行链路上的瓶颈。是模型调用慢,还是工具调用慢,还是检索慢?数据会告诉你答案。我做过一次优化,发现某个工具调用的平均耗时是 3 秒,占了整个任务耗时的一半,后来把这个工具换成批量接口,整体耗时降了 40%。
成本分析。每个 LLM Span 都记了 token 数,按模型单价一乘,就能算出每次任务的成本。按业务维度聚合,能看出哪些业务线烧钱多。这个数据对做预算和优化很有用。
质量监控。追踪数据里包含了每次任务的输入输出,可以用来做质量分析。比如统计工具调用的失败率、模型输出的格式合规率、检索的召回率。这些指标能帮你提前发现智能体质量下降的趋势。
回归测试。把线上真实的追踪数据采样下来,作为回归测试的用例集。每次智能体迭代后,用这批数据跑一遍,对比输出有没有变化。这比手写测试用例覆盖度高得多。
A/B 实验。给不同版本的智能体打上不同的标签,通过追踪数据对比它们的表现。比如 prompt 改了一版,通过追踪数据看新版的 token 消耗、延迟、成功率有没有变化。
8. 一些实操心得
追踪方案落地,技术只是一部分,更重要的是团队的使用习惯。我见过不少团队把追踪系统搭起来了,但没人看,出了问题还是靠 print 日志。这等于白搭。
我的做法是:把追踪链接放进告警通知里。当智能体任务失败或者超时时,告警消息里直接带上这次任务的追踪链接,排查的人点一下就能看到完整链路。这样追踪系统就被自然地用起来了。
另一个做法是定期做链路复盘。每周挑几个典型的失败案例,拉上相关同学一起看链路,分析问题出在哪。这既是排查问题,也是团队学习的过程。新人通过看链路能快速理解智能体的执行逻辑。
还有一点:追踪的粒度要适中。埋得太粗,排查时信息不够;埋得太细,数据量大且噪音多。我的经验是,模型调用、工具调用、检索这三个环节必须埋,其他环节按需。不要为了“完整”而埋一堆用不上的 Span。
最后说一个容易被忽视的点:追踪系统的可用性。追踪系统本身也是服务,也会挂。如果追踪系统挂了导致智能体也挂了,那就本末倒置了。所以导出器要配置失败降级,追踪后端不可用时,数据丢弃但不影响主流程。这个在 OpenTelemetry 的 BatchSpanProcessor 里可以通过配置实现,导出失败不阻塞业务。
这套方案我在几个项目里落地过,从单智能体到多智能体协作,从本地调试到生产环境,整体下来稳定性不错。最大的感受是:可观测性不是锦上添花,而是智能体工程化的基础设施。没有它,智能体的迭代就是盲人摸象;有了它,每次迭代都有数据支撑,问题定位从小时级降到分钟级。如果你正在做智能体开发,还没上追踪,建议尽早补上这一课。