☰
LLM智能体可观测性实战:基于OpenTelemetry的AgentTrace追踪与脱敏方案
2026/9/26 23:45:46 网站建设 项目流程

1. 为什么LLM智能体需要一台"行车记录仪"

1.1 从一次线上事故说起

去年年底我帮一个团队排查他们客服智能体的"胡言乱语"问题。现象很典型:用户问退款政策,智能体先查了订单库,又调了知识库检索,最后给出的答案里混进了一条三个月前的旧政策,还附带了一个根本不存在的退款单号。团队五个人围着日志看了两天,最后发现是工具调用返回的JSON里有个字段被上游接口改了类型,模型拿到null之后自己"脑补"了一个值。

问题本身不复杂,复杂的是定位过程。他们的日志散在三个地方:应用层的print日志、模型API的请求响应日志、工具函数的执行日志。三份日志时间戳精度不一样,请求ID对不上,中间还隔着异步任务。等他们把链路拼出来,故障已经复现了十几次。

这件事让我彻底意识到一个事实:LLM智能体的可观测性,和传统后端服务的可观测性,根本不是一回事。传统服务你打个日志、埋个trace,调用链是确定的、代码路径是可枚举的。但智能体不一样——它的执行路径是模型在运行时"决定"的,同一个输入两次运行可能走完全不同的工具组合,中间还夹着自然语言的推理过程。你没法靠读代码预判它会干什么。

这就是AgentTrace这类工具存在的意义。说白了,它就是给智能体装的一台"行车记录仪":记录它每一步想了什么、调了什么、拿到了什么、最后输出了什么,出事的时候能倒带回放,平时还能用来分析它的行为模式。

1.2 AgentTrace到底解决什么问题

先把概念理清楚。AgentTrace不是一个具体的开源库,而是一类面向LLM智能体的追踪与可观测性方案的统称,核心思路是把智能体的一次完整执行拆解成结构化的Span(跨度)树,每个Span对应一个语义单元——一次模型调用、一次工具执行、一次检索、一次规划决策。

它要解决的核心痛点有这么几个:

  • 黑盒问题:模型为什么这么答?中间用了哪些信息?传统日志只能看到输入输出,看不到"思考过程"。
  • 链路断裂:多轮对话、多工具、多智能体协作时,一次用户请求可能触发几十次内部调用,没有统一Trace ID根本串不起来。
  • 成本归因:Token花了多少、花在哪个环节、哪个工具最烧钱,没有细粒度数据就没法优化。
  • 评测困难:想对比两个Prompt版本的效果,没有标准化的执行记录,只能靠人肉看case。
  • 合规审计:金融、医疗这类场景要求可追溯,智能体说了什么、依据什么说的,必须留痕。

热词里提到的OpenTelemetry是关键。AgentTrace这类方案基本都建立在OTel的语义约定之上,把智能体的Span映射成标准的OTel Trace,这样就能直接复用现有的Jaeger、Tempo、Langfuse等后端,不用自己造一套观测体系。这个选择非常务实——可观测性这件事,生态比功能重要。

1.3 谁该认真看这篇内容

如果你属于下面任何一类,这篇值得花时间:

  • 正在做智能体开发,被"线上出问题查不到原因"折磨过的工程师;
  • 用LangChain、LangGraph、Dify这类框架搭过智能体,但只会看框架自带日志的开发者;
  • 需要给智能体做效果评测、成本分析的技术负责人;
  • 对LLM应用的可观测性感兴趣,想搞清楚Trace、Span、OTel这套东西怎么落到智能体场景的人。

不需要你精通分布式追踪,但至少得写过能调用工具的智能体,知道什么是Function Calling。下面我会从设计思路讲到实操落地,尽量把每个"为什么"都讲透。

2. 智能体追踪的核心设计思路拆解

2.1 为什么不能直接套用传统APM

很多人第一反应是:我有APM啊,SkyWalking、Pinpoint不都能看调用链吗,为什么还要专门搞智能体追踪?

我拿实际数据说话。一个典型的智能体处理一次用户请求,内部可能发生这些事:1次规划调用(模型决定下一步)、3次工具调用(每次工具内部可能还有HTTP请求)、2次检索、1次最终生成。传统APM能看到的,是这些HTTP请求的耗时和状态码。但你看不到的是:模型在规划那一步到底输出了什么、它为什么选了工具A而不是工具B、工具返回的内容有没有被截断、最终答案是基于哪几条检索结果生成的。

更麻烦的是语义层面的东西。传统APM的Span是"技术单元"——一个函数、一次RPC。智能体的Span是"语义单元"——一次推理、一次决策。这两者的粒度和含义完全不同。你硬套APM,最后得到的就是一堆没有语义的HTTP调用,排查问题时还是抓瞎。

所以AgentTrace的设计第一原则就是:Span的划分必须贴合智能体的语义结构,而不是技术调用结构。

2.2 Span树应该怎么设计

这是整个方案里最需要想清楚的部分。我踩过的坑是:一开始把Span切得太细,每次模型调用、每次字符串处理都建一个Span,结果一次请求产生几百个Span,看都看不过来。后来又把Span切得太粗,整个智能体执行就一个Span,等于没追踪。

经过几轮调整,我总结出一个比较合理的Span层级结构,你可以直接参考:

层级Span类型对应语义关键属性
L0agent.run一次完整的智能体执行用户输入、最终输出、总耗时、总Token
L1agent.step一轮"思考-行动"循环步骤序号、当前状态
L2llm.call一次模型调用模型名、Prompt、Completion、Token数
L2tool.call一次工具执行工具名、入参、出参、耗时、是否报错
L2retrieval一次检索查询语句、命中文档ID、相似度分数
L3http.request工具内部的网络请求URL、状态码、耗时

这个结构的核心逻辑是:L0到L2是语义层,L3是技术层。排查业务问题时看L0-L2就够了,需要深挖性能瓶颈时再下钻到L3。父子关系用Trace ID和Parent Span ID串起来,和OTel的标准完全兼容。

提示:L2的llm.call一定要记录完整的Prompt和Completion,但要注意脱敏。热词里"使用llm时如何防止密钥等鉴权信息泄露"是个高频问题,追踪系统本身就是泄露重灾区——Prompt里经常夹着API Key、用户隐私数据。后面第4节我会专门讲脱敏方案。

2.3 为什么选OpenTelemetry作为底座

自己造一套追踪协议不是不行,但代价太大。选OTel的理由很实在:

  • 生态现成:Jaeger、Tempo、Grafana、Langfuse、Phoenix全都支持OTel协议,数据采集完直接能可视化,不用自己写UI。
  • 语义约定成熟:OTel有GenAI语义约定(Semantic Conventions for GenAI),虽然还在演进,但基本的gen_ai.system、gen_ai.request.model、gen_ai.usage.input_tokens这些字段已经标准化了,社区工具都认。
  • 采样和导出机制完善:批量导出、采样策略、背压处理这些工程细节,OTel的SDK都帮你搞定了,自己写容易出问题。
  • 跨语言:Python、Java、Go、JS都有成熟SDK,多语言智能体系统也能统一。

我实测下来,用OTel做底座,从零到能看Trace,一个熟练工程师半天就能跑通。自己造轮子,光是把数据可视化做出来就得一周。

2.4 采样策略:全量还是抽样

这是实操中必须做决策的点。全量记录所有Trace,存储成本会爆炸——一次智能体请求动辄几十KB的Trace数据,日活一万的系统一天就是几百GB。

我的建议是分层采样:

  • 错误全采:任何Span报错的Trace,100%保留。这是排查问题的核心数据。
  • 慢请求全采:超过P95耗时的Trace,100%保留。
  • 正常请求采样:按1%-10%的比例采样,用于统计分析和效果评测。
  • 关键用户全采:VIP用户或特定测试账号的请求全量保留。

OTel支持基于Trace ID的采样决策,可以在Span创建时就决定采不采,避免"采了一半"的尴尬。具体配置后面实操部分会给代码。

3. 核心细节解析与实操要点

3.1 一次智能体执行的完整追踪流程

我把一次典型的智能体执行拆成下面这些追踪点,你可以对照自己的代码看哪些地方还没埋点:

  1. 入口埋点:用户请求进来,创建根Span(agent.run),生成Trace ID,注入到上下文。
  2. 规划阶段:模型决定下一步做什么,创建llm.call Span,记录Prompt和输出。
  3. 工具选择:如果模型决定调工具,创建tool.call Span,记录工具名和参数。
  4. 工具执行:工具内部如果有网络请求,创建http.request Span作为子Span。
  5. 结果回填:工具返回结果,更新tool.call Span的属性和状态。
  6. 循环:重复2-5,直到模型决定输出最终答案。
  7. 收尾:记录最终输出、总Token、总耗时,结束根Span。

关键点在于上下文的传递。Python里用contextvars,异步场景下要确保Span的父子关系正确。我见过太多人因为异步任务里丢了上下文,导致Span全变成平级的,Trace树直接废掉。

3.2 Prompt和Completion的记录与脱敏

这是最敏感的部分。追踪系统要记录Prompt才能排查问题,但Prompt里可能包含:

  • API Key、Token等鉴权信息
  • 用户手机号、身份证、地址等PII
  • 内部业务数据、价格策略

我的做法是双层处理:

第一层,在埋点时就做正则脱敏。常见的Key格式(sk-开头、Bearer后面、长随机字符串)直接替换成[REDACTED]。手机号、身份证用正则匹配替换。

第二层,存储时加密。敏感字段用AES加密后再落库,只有有权限的人能解密查看。

import re SENSITIVE_PATTERNS = [ (r'sk-[a-zA-Z0-9]{20,}', '[REDACTED_API_KEY]'), (r'Bearer\s+[a-zA-Z0-9\-._~+/]+=*', 'Bearer [REDACTED]'), (r'1[3-9]\d{9}', '[REDACTED_PHONE]'), (r'\d{17}[\dXx]', '[REDACTED_ID]'), ] def sanitize(text: str) -> str: for pattern, replacement in SENSITIVE_PATTERNS: text = re.sub(pattern, replacement, text) return text

注意:脱敏一定要在Span创建时就做,不要等到导出时再做。因为Span对象在内存里可能被其他代码读到,导出前脱敏等于没脱敏。

3.3 Token计数的准确性

Token计数直接影响成本归因,但很多人这里会踩坑。模型API返回的usage字段有时候不准,尤其是流式响应。我的经验是:

  • 优先用API返回的usage,这是最权威的。
  • 流式响应要自己累加,很多SDK在流式模式下不返回usage,得用tiktoken之类的库自己算。
  • 区分输入输出,输入Token和输出Token价格不一样,混在一起算成本会错。
  • 记录模型版本,不同版本的计费规则可能不同。

在Span属性里,我一般记这几个字段:gen_ai.usage.input_tokens、gen_ai.usage.output_tokens、gen_ai.usage.total_tokens、gen_ai.response.model。

3.4 工具调用的错误捕获

工具调用是智能体最容易出问题的环节。追踪系统必须能区分几种错误:

  • 工具本身报错:函数抛异常,Span状态设为ERROR,记录异常信息。
  • 工具返回错误码:函数正常返回但内容是错误,比如{"error": "not found"},这种情况Span状态是OK,但要在属性里标记tool.result.status=error。
  • 工具超时:单独标记,方便统计超时率。
  • 模型解析工具结果失败:这是最隐蔽的,工具返回正常,但模型理解错了。这种情况只能在llm.call的Span里通过对比输入输出发现。

我一般会在tool.call Span上加一个tool.result.valid属性,标记返回结果是否符合预期schema。这个属性对排查"模型胡言乱语"特别有用。

4. 实操过程与核心环节实现

4.1 环境准备与依赖安装

先明确技术栈。我用Python举例,因为智能体生态Python最成熟。需要装这些:

pip install opentelemetry-api \ opentelemetry-sdk \ opentelemetry-exporter-otlp \ opentelemetry-instrumentation \ tiktoken

后端我选Jaeger做演示,因为它部署简单、UI直观。生产环境可以用Tempo或Langfuse,看团队习惯。

docker run -d --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ jaegertracing/all-in-one:latest

16686是UI端口,4317是OTLP gRPC端口,4318是OTLP HTTP端口。跑起来后访问http://localhost:16686就能看到Jaeger界面。

4.2 初始化TracerProvider

这是所有追踪的基础,配置错了后面全白搭。我给出一个生产可用的配置:

from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.resources import Resource from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio resource = Resource.create({ "service.name": "my-agent-service", "service.version": "1.0.0", "deployment.environment": "production", }) # 采样策略:基于Trace ID的比例采样,父Span决定子Span sampler = ParentBasedTraceIdRatio(0.1) # 10%采样率 provider = TracerProvider(resource=resource, sampler=sampler) exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) provider.add_span_processor(BatchSpanProcessor( exporter, max_queue_size=2048, schedule_delay_millis=5000, max_export_batch_size=512, )) trace.set_tracer_provider(provider) tracer = trace.get_tracer(__name__)

几个关键参数解释一下:

  • ParentBasedTraceIdRatio(0.1):根Span按10%概率采样,子Span跟随父Span的决策。这样保证一个Trace要么全采要么全不采,不会出现半截Trace。
  • BatchSpanProcessor:批量导出,减少网络开销。schedule_delay_millis=5000表示最多攒5秒就发一次。
  • insecure=True:本地开发用,生产环境要配TLS。

4.3 给智能体主流程埋点

下面是一个简化但完整的智能体执行函数,带完整埋点:

import time from opentelemetry.trace import Status, StatusCode def run_agent(user_input: str, session_id: str): with tracer.start_as_current_span("agent.run") as root_span: root_span.set_attribute("agent.session_id", session_id) root_span.set_attribute("agent.input", sanitize(user_input)) total_input_tokens = 0 total_output_tokens = 0 step = 0 messages = [{"role": "user", "content": user_input}] try: while step < 10: # 防止死循环 step += 1 with tracer.start_as_current_span(f"agent.step") as step_span: step_span.set_attribute("agent.step.index", step) # 模型调用 llm_result = call_llm(messages) total_input_tokens += llm_result["input_tokens"] total_output_tokens += llm_result["output_tokens"] # 判断是否要调工具 if llm_result.get("tool_calls"): for tool_call in llm_result["tool_calls"]: tool_result = execute_tool(tool_call) messages.append({ "role": "tool", "content": tool_result, "tool_call_id": tool_call["id"] }) else: # 没有工具调用,输出最终答案 final_answer = llm_result["content"] root_span.set_attribute("agent.output", sanitize(final_answer)) break root_span.set_attribute("agent.total_input_tokens", total_input_tokens) root_span.set_attribute("agent.total_output_tokens", total_output_tokens) root_span.set_attribute("agent.step_count", step) root_span.set_status(Status(StatusCode.OK)) return final_answer except Exception as e: root_span.set_status(Status(StatusCode.ERROR, str(e))) root_span.record_exception(e) raise

这段代码的核心是用with语句管理Span生命周期,确保异常时Span也能正确关闭。start_as_current_span会自动处理上下文传递,子Span自动挂到当前Span下。

4.4 模型调用Span的详细实现

模型调用是最需要详细记录的部分:

import tiktoken def call_llm(messages: list) -> dict: with tracer.start_as_current_span("llm.call") as span: model_name = "gpt-4o-mini" span.set_attribute("gen_ai.system", "openai") span.set_attribute("gen_ai.request.model", model_name) span.set_attribute("gen_ai.request.temperature", 0.7) # 记录脱敏后的Prompt prompt_text = "\n".join([m["content"] for m in messages]) span.set_attribute("gen_ai.prompt", sanitize(prompt_text)) start = time.time() try: response = openai_client.chat.completions.create( model=model_name, messages=messages, temperature=0.7, ) latency = time.time() - start content = response.choices[0].message.content or "" tool_calls = response.choices[0].message.tool_calls span.set_attribute("gen_ai.completion", sanitize(content)) span.set_attribute("gen_ai.response.finish_reason", response.choices[0].finish_reason) span.set_attribute("llm.latency_ms", int(latency * 1000)) # Token统计 usage = response.usage span.set_attribute("gen_ai.usage.input_tokens", usage.prompt_tokens) span.set_attribute("gen_ai.usage.output_tokens", usage.completion_tokens) span.set_attribute("gen_ai.usage.total_tokens", usage.total_tokens) # 成本估算(以gpt-4o-mini为例,实际价格按官方调整) cost = usage.prompt_tokens * 0.15 / 1_000_000 + \ usage.completion_tokens * 0.6 / 1_000_000 span.set_attribute("llm.cost_usd", cost) span.set_status(Status(StatusCode.OK)) return { "content": content, "tool_calls": [ {"id": tc.id, "name": tc.function.name, "arguments": tc.function.arguments} for tc in (tool_calls or []) ], "input_tokens": usage.prompt_tokens, "output_tokens": usage.completion_tokens, } except Exception as e: span.set_status(Status(StatusCode.ERROR, str(e))) span.record_exception(e) raise

这里有几个细节值得说:

  • 成本估算要单独记,不要等到事后算。因为模型价格会变,事后算容易用错价格。
  • finish_reason要记,length表示被截断了,tool_calls表示要调工具,stop表示正常结束。这个字段对排查问题很有用。
  • latency单独记,虽然Span本身有duration,但显式记录方便做聚合分析。

4.5 工具调用Span的实现

工具调用的埋点重点是记录入参出参和错误:

import json def execute_tool(tool_call: dict) -> str: tool_name = tool_call["name"] try: arguments = json.loads(tool_call["arguments"]) except json.JSONDecodeError: arguments = {} with tracer.start_as_current_span("tool.call") as span: span.set_attribute("tool.name", tool_name) span.set_attribute("tool.arguments", sanitize(json.dumps(arguments))) start = time.time() try: result = TOOL_REGISTRY[tool_name](**arguments) latency = time.time() - start result_str = json.dumps(result, ensure_ascii=False) \ if not isinstance(result, str) else result span.set_attribute("tool.result", sanitize(result_str[:2000])) span.set_attribute("tool.latency_ms", int(latency * 1000)) span.set_attribute("tool.result.valid", True) span.set_status(Status(StatusCode.OK)) return result_str except Exception as e: latency = time.time() - start span.set_attribute("tool.latency_ms", int(latency * 1000)) span.set_attribute("tool.result.valid", False) span.set_status(Status(StatusCode.ERROR, str(e))) span.record_exception(e) return json.dumps({"error": str(e)})

注意tool.result我截断到2000字符。工具返回大JSON时,全量记录会让Trace体积爆炸。截断后保留头部信息,一般够排查问题了。如果确实需要完整结果,可以单独存到对象存储,Span里只放引用ID。

4.6 多智能体协作的追踪

热词里"多智能体如何配置"是高频问题。多智能体场景下,追踪的复杂度会上升一个量级。核心思路是用Span Link而不是父子关系。

比如一个主管智能体(Supervisor)分派任务给三个工人智能体(Worker),这三个Worker是并行执行的,它们之间没有父子关系,但都属于同一次用户请求。这时候:

  • Supervisor的Span是根Span。
  • 每个Worker的Span是根Span的子Span。
  • Worker之间如果需要通信,用Span Link关联。
from opentelemetry.trace import Link def dispatch_to_worker(task: str, parent_context): # 创建Link,关联到父上下文 link = Link(parent_context.get_span_context()) with tracer.start_as_current_span( "agent.worker", links=[link] ) as span: span.set_attribute("worker.task", task) # ... 执行任务

Span Link的好处是,即使Worker在完全独立的进程或服务里执行,也能通过Link关联回原始请求。这在分布式多智能体系统里是必须的。

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

5.1 Trace断链:为什么我的Span全是平级的

这是最高频的问题。现象是Jaeger里看到一堆没有父子关系的Span,Trace树完全散架。

根因:上下文丢失。常见于三种场景:

  1. 异步任务:asyncio.create_task创建的任务不会自动继承上下文,需要手动传递。
  2. 线程池:ThreadPoolExecutor提交的任务同样不继承上下文。
  3. 跨服务调用:HTTP请求没带traceparent头。

解决方案:

import asyncio from opentelemetry import context as otel_context # 异步任务:手动传递上下文 async def main(): ctx = otel_context.get_current() task = asyncio.create_task(worker(), context=ctx) await task # 线程池:用contextvars.copy_context import contextvars from concurrent.futures import ThreadPoolExecutor ctx = contextvars.copy_context() with ThreadPoolExecutor() as executor: future = executor.submit(ctx.run, worker_func, arg)

跨服务调用的话,用OTel的自动instrumentation,它会自动注入和提取traceparent头。手动实现的话,就是在HTTP header里加traceparent字段。

5.2 数据量太大:Trace存储成本失控

我见过一个团队,上线追踪系统一周,存储费用涨了十倍。原因是全量记录了所有Prompt和Completion,而且没做采样。

优化方案(按优先级):

优化手段效果实施难度
开启采样降低50%-90%数据量低
Prompt/Completion截断降低30%-60%低
敏感字段脱敏后不存原文降低10%-20%中
冷热分离,老数据归档降低长期成本中
只记录关键Span降低20%-40%高

我的建议是采样+截断组合拳,一般能降80%以上。截断策略:Prompt保留前2000字符+后500字符,Completion保留前1000字符。中间用...[truncated N chars]...标记。

5.3 性能影响:追踪拖慢了智能体

追踪本身有开销,主要是Span创建、属性设置、数据导出。我实测下来,合理的配置下开销在5%以内,但如果配置不当,可能到30%。

性能优化清单:

  • 用BatchSpanProcessor而不是SimpleSpanProcessor,后者每个Span都同步导出,性能灾难。
  • 属性值不要太大,单个属性超过1KB就要考虑截断。
  • 避免在热路径上做复杂计算,比如Token计数用缓存。
  • 采样率不要设太高,10%对大多数场景够用。
  • 导出用gRPC而不是HTTP,前者性能更好。

提示:如果发现追踪开销异常,先用SimpleSpanProcessor换成BatchSpanProcessor试试,这是最常见的性能问题来源。

5.4 排查速查表

我把常见问题和排查思路整理成表,方便你对照:

现象可能原因排查方法
Trace树散架上下文丢失检查异步/线程池的上下文传递
Span缺失采样决策不一致检查采样器配置,确保父子一致
数据延迟大BatchSpanProcessor队列满调大max_queue_size或减小schedule_delay
Token数不准流式响应未累加检查流式模式的usage处理
成本归因错误模型价格配置过时定期更新价格表
敏感信息泄露脱敏未覆盖审查脱敏正则,补充新格式
Jaeger看不到数据端口或协议不匹配检查OTLP endpoint和协议

5.5 几个我踩过的坑

坑一:Span属性类型不一致。OTel的属性值类型必须一致,同一个key不能一会儿是string一会儿是int。我一开始把tool.latency_ms有时写成int有时写成float,导致后端解析报错。统一用int,毫秒级精度够了。

坑二:忘了关闭Span。用start_span而不是start_as_current_span时,必须手动end()。我有个同事忘了end,Span一直不导出,排查了半天。

坑三:采样率改了但没生效。采样器是在TracerProvider初始化时设置的,运行中改不了。要改采样率得重启服务,或者用动态采样器。

坑四:脱敏正则太激进。我一开始用[a-zA-Z0-9]{32,}匹配长字符串,结果把正常的业务ID也脱敏了,排查问题时看不到关键信息。脱敏规则要精准,宁可漏脱也别误脱。

坑五:多进程环境下Trace ID冲突。用gunicorn多worker部署时,每个worker的TracerProvider是独立的,Trace ID可能重复。解决方案是用OTel的TraceIdRatioBased采样器配合全局唯一的service instance id。

6. 追踪数据的进阶用法

6.1 用Trace数据做效果评测

追踪系统积累的Trace数据,是评测智能体效果的天然数据集。我一般这么用:

  • 构建回归测试集:从生产Trace里挑出典型case,标注期望输出,形成回归测试集。每次改Prompt或换模型,跑一遍看有没有退化。
  • 对比分析:同一个case,用不同Prompt版本跑,对比Trace里的Token数、工具调用次数、最终输出质量。
  • 失败案例挖掘:筛选出Span状态为ERROR或用户反馈差的Trace,人工分析根因。

热词里"evaluation智能体添加方法论"说的就是这个方向。追踪是评测的基础设施,没有Trace数据,评测就是空中楼阁。

6.2 成本优化:从Trace里找省钱机会

我帮一个团队做过成本优化,从Trace数据里发现三个问题:

  1. Prompt冗余:系统Prompt有3000 Token,但其中一半是重复的示例,可以精简。
  2. 工具调用过多:平均每次请求调5次工具,但分析发现其中2次是冗余的,模型在重复查同样的信息。
  3. 模型选型不当:简单任务用了大模型,换成小模型效果差不多,成本降了80%。

这三个优化加起来,月成本从2万降到6千。没有Trace数据,这些问题根本发现不了。

6.3 告警:让追踪系统主动发现问题

追踪数据不光能事后排查,还能实时告警。我配置的告警规则:

  • 错误率:5分钟内tool.call错误率超过5%,告警。
  • P99延迟:agent.run的P99超过30秒,告警。
  • Token异常:单次请求Token超过10万,告警(可能是死循环)。
  • 成本异常:小时成本超过阈值,告警。

这些告警用Prometheus从Trace数据里聚合出来,配合Alertmanager发通知。实测下来,能在用户投诉之前发现问题。

7. 我个人的一些实践体会

追踪系统这东西,上线容易,用好难。我见过太多团队装了个Jaeger,埋了点,然后就没然后了——数据在那儿躺着,没人看。

真正让追踪产生价值的,是把它嵌入到日常工作流里。我的做法是:

  • 每次线上问题,先看Trace再改代码。养成习惯后,排查效率提升非常明显。
  • 每周review一次Trace数据,看有没有异常模式,比如某个工具突然变慢、某个Prompt突然变贵。
  • 把Trace链接放进工单系统,处理问题时直接点开看,不用来回找日志。

还有一点,不要追求大而全。一开始就埋几十个属性,最后没人看得过来。先把核心的埋好——模型调用、工具调用、错误信息,这三个覆盖了80%的排查场景。等有需求了再逐步加。

最后分享一个我觉得特别有用的小技巧:在Span里加一个agent.version属性,标记当前智能体的版本号。这样对比不同版本的Trace时,一眼就能看出差异。我们团队靠这个属性,定位过一次Prompt改版导致的性能退化——新版本平均多调了2次工具,Token成本涨了40%,但输出质量没提升。没有版本标记,这种问题很难发现。

追踪系统的价值,不在于技术多先进,而在于它能不能让你在出问题时少熬几个夜。从这个角度看,AgentTrace这类方案,值得每个认真做智能体的团队投入。

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

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

立即咨询