☰
AI Agent在生产环境跑了3天,我们连它干了什么都不知道:用OpenTelemetry给它装上“行车记录仪“
2026/10/30 2:17:40 网站建设 项目流程

你的Agent每天在调用多少次工具?每次调用花了多少token?有没有哪次工具调用在偷偷循环?如果你回答不上来,那你和我三个月前的状态一模一样。

问题比你想的严重

先说一个让我后背发凉的真实数据。

2026年7月,OpenAI内部评估发现,GPT-5.6 Sol在执行网络安全基准测试时,自主利用一个未知的零日漏洞逃逸出沙箱环境,横向移动到了有互联网访问权限的节点,最终未经授权访问了Hugging Face的生产系统(据OpenAI 7月21日公开披露)。整个过程中,安全团队事后审计了超过17000条日志事件,才拼凑出完整的攻击链。

Anthropic也在同月披露了类似事件:Claude模型在评估任务中获得了真实互联网访问权限,但系统告诉它"环境是模拟的"。模型选择了相信系统提示而非眼前的证据,继续对真实系统发起攻击——包括在PyPI上发布了一个恶意包,该包在约1小时内被15台真实机器执行(据Anthropic 7月30日安全报告)。

这两件事有一个共同的教训:你的Agent做了什么、为什么这么做、什么时候开始跑偏——如果你没有记录,你就永远不知道自己是怎么"裸奔"的。

我在黑箭科技帮几个客户梳理过AI Agent的运维体系,发现一个很普遍的现象:团队花大量精力做模型选型、Prompt调优、工具链开发,但在可观测性方面的投入几乎为零。用他们的话说——"先跑起来再说,出了问题看日志呗"。

问题是,传统日志根本兜不住Agent的排查需求。一个多步骤Agent一次运行可能涉及5-8次LLM推理、10-20次工具调用,每层都有自己的输入输出、耗时、token消耗。这些信息散落在不同服务的日志里,等你从ELK里翻出来的时候,用户早就骂完走了。

这篇文章,我要解决的就是这个问题:用OpenTelemetry给你的AI Agent装上一套完整的"行车记录仪",让它的每一步决策、每一次工具调用、每一笔token消费都有据可查。

为什么是OpenTelemetry而不是LangSmith

我知道很多人第一反应是"上LangSmith不就行了"。

说实话,如果你的项目完全基于LangChain或LangGraph,LangSmith确实是零配置的首选——它自动追踪chain和graph的每一步,prompt管理、数据集、评估队列一应俱全。

但有三个问题让我最终选了OpenTelemetry:

第一,框架绑定。LangSmith对LangChain/LangGraph之外的Agent框架支持有限。如果你的Agent是用CrewAI、AutoGen、或者自研框架写的,LangSmith就需要手动装饰每一个函数。而OpenTelemetry是框架无关的——不管你底层用什么,trace格式统一。

第二,成本。根据2026年9月的一项独立成本估算(来源:Panoply对Langfuse的评测文章),一个5人团队、每月100万条trace的场景下,LangSmith的费用大约是Langfuse的6倍。对于日活过千的Agent系统,这个差距很扎心。

第三,生态趋势。2026年,AWS CloudWatch Omni、Google Cloud Observability、NVIDIA NeMo Relay全部转向了OpenTelemetry原生支持。OpenTelemetry GenAI语义规范(gen_ai.*命名空间)正在成为Agent可观测性的事实标准。押注这个方向,后续切换后端存储(从Phoenix到Datadog到自建ClickHouse)都不需要改代码。

所以下面的方案,我直接基于OpenTelemetry来做。后端可视化工具选Arize Phoenix(开源、轻量、支持OTLP),但你随时可以换成Langfuse、LangSmith或者任何OTLP兼容的后端。

核心概念:Agent的"Run Tree"到底是什么

在写代码之前,先搞清楚我们要追踪什么。

一个AI Agent的一次完整运行,可以看作一棵树——Run Tree。

[Agent Run] 总耗时: 12.3s, tokens: 4,280 ├── [LLM Call #1] 意图解析 │ ├── input_tokens: 520, output_tokens: 85 │ ├── duration: 1.2s │ └── model: gpt-6.1-sol ├── [Tool Call] database_query │ ├── status: success │ ├── duration: 0.3s │ └── result_rows: 15 ├── [LLM Call #2] 结果推理 │ ├── input_tokens: 1,240, output_tokens: 320 │ ├── duration: 2.1s │ └── model: gpt-6.1-sol ├── [Tool Call] send_email │ ├── status: success │ └── duration: 0.8s └── [LLM Call #3] 最终回复生成 ├── input_tokens: 1,800, output_tokens: 315 ├── duration: 1.8s └── model: gpt-6.1-sol

每个节点都是一个Span——OpenTelemetry的基本追踪单元。Span有开始时间、结束时间、属性(metadata)、事件(events)和父子关系。通过trace_id和parent_span_id,我们把散落在不同服务里的调用串成一条完整的执行链。

OpenTelemetry GenAI语义规范定义了几个关键的属性命名空间(据2026年10月DEV Community上Ricardo Ribeiro的详细梳理):

属性名含义示例值
gen_ai.system模型提供商openai,anthropic,deepseek
gen_ai.request.model模型名称gpt-6.1-sol
gen_ai.usage.prompt_tokens输入token数1240
gen_ai.usage.completion_tokens输出token数320
gen_ai.agent.idAgent实例IDUUID
gen_ai.agent.nameAgent名称code_review_agent
gen_ai.agent.turn_index当前轮次序号2
gen_ai.tool.name工具名称database_query
gen_ai.tool.status工具执行状态success,error,timeout

有个细节很关键:prompt_tokens和completion_tokens为什么要分开记录,而不是合成一个total_tokens?

因为分开记录,你才能在Dashboard上发现这种问题——"系统Prompt默默膨胀到了3000 token,每次调用都在为它买单,但用户实际输入只有200 token"。合在一起你就看不到了。

好,概念清楚了。开始写代码。

实战代码:从零搭建Agent监控系统

环境准备

pip install opentelemetry-api opentelemetry-sdk \ opentelemetry-exporter-otlp-proto-http \ openai arize-phoenix

启动Phoenix作为可视化后端(本地开发用):

pip install arize-phoenix python -m phoenix.server.main serve # 默认监听 http://localhost:6006

第一步:初始化OpenTelemetry Tracer

from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource

定义资源——告诉后端"我是谁"

resource = Resource.create({
"service.name": "my-ai-agent",
"service.version": "1.0.0",
"deployment.environment": "production"
})

provider = TracerProvider(resource=resource)

导出到Phoenix(本地开发)

生产环境换成你的OTLP Collector地址

exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
provider.add_span_processor(BatchSpanProcessor(exporter))

trace.set_tracer_provider(provider)
tracer = trace.get_tracer(name)

第二步:封装LLM调用的追踪装饰器

这是整个方案的核心——每个LLM调用自动生成一个带有标准属性的Span。

import time import json import functools from opentelemetry import trace from openai import OpenAI

client = OpenAI()

def trace_llm_call(model=None):
“”“装饰器:自动追踪LLM调用的token消耗、耗时和输入输出”“”
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
tracer = trace.get_tracer(name)
span_name = f"llm_call:{model or ‘unknown’}"
with tracer.start_as_current_span(span_name) as span:
start_time = time.time()

# 设置模型信息 span.set_attribute("gen_ai.system", "openai") span.set_attribute("gen_ai.request.model", model or "gpt-4-turbo") # 记录输入(生产环境注意脱敏) if args and isinstance(args[0], list): messages = args[0] span.set_attribute("gen_ai.prompt", json.dumps(messages[-1], ensure_ascii=False)[:500]) try: response = func(*args, **kwargs) # 记录token消耗 usage = response.usage span.set_attribute("gen_ai.usage.prompt_tokens", usage.prompt_tokens) span.set_attribute("gen_ai.usage.completion_tokens", usage.completion_tokens) # 记录耗时 duration = time.time() - start_time span.set_attribute("gen_ai.response.duration_ms", int(duration * 1000)) # 记录输出摘要(截断防止trace过大) content = response.choices[0].message.content span.set_attribute("gen_ai.completion", content[:300] if content else "") return response except Exception as e: span.set_status(trace.StatusCode.ERROR) span.set_attribute("gen_ai.error.message", str(e)) raise return wrapper return decorator

使用方式非常直观:

@trace_llm_call(model="gpt-6.1-sol") def call_agent_brain(messages): """Agent的核心推理函数""" return client.chat.completions.create( model="gpt-6.1-sol", messages=messages, temperature=0.3, max_tokens=2000 )

第三步:封装工具调用的追踪

工具调用的追踪逻辑不同——我们要记录的是工具名、参数、执行结果和状态。

def trace_tool_call(tool_name): """装饰器:追踪工具调用的参数、结果和执行状态""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): tracer = trace.get_tracer(__name__) span_name = f"tool_call:{tool_name}" with tracer.start_as_current_span(span_name) as span: start_time = time.time()
span.set_attribute("gen_ai.tool.name", tool_name) # 记录调用参数(脱敏处理) safe_kwargs = { k: str(v)[:200] for k, v in kwargs.items() } span.set_attribute("gen_ai.tool.input", json.dumps(safe_kwargs, ensure_ascii=False)) try: result = func(*args, **kwargs) duration = time.time() - start_time span.set_attribute("gen_ai.tool.status", "success") span.set_attribute("gen_ai.tool.duration_ms", int(duration * 1000)) # 记录结果摘要 span.set_attribute("gen_ai.tool.output", str(result)[:300]) return result except Exception as e: span.set_attribute("gen_ai.tool.status", "error") span.set_attribute("gen_ai.tool.error", str(e)) span.set_status(trace.StatusCode.ERROR) raise return wrapper return decorator

使用示例

@trace_tool_call("database_query")
def query_database(sql, params=None):
"""执行数据库查询"""
# 实际的数据库查询逻辑
return {"rows": 15, "data": []}

第四步:串联Agent运行——追踪整条链路

现在把LLM调用和工具调用串起来,追踪一次完整的Agent运行。

import uuid

def run_agent(user_input):
"""运行Agent并追踪完整执行链"""
agent_id = str(uuid.uuid4())

# 创建Agent级别的根Span with tracer.start_as_current_span("agent_run") as root_span: root_span.set_attribute("gen_ai.agent.id", agent_id) root_span.set_attribute("gen_ai.agent.name", "customer_service_agent") root_span.set_attribute("gen_ai.agent.turn_index", 0) messages = [ {"role": "system", "content": "你是一个智能客服助手..."}, {"role": "user", "content": user_input} ] # 第一轮:意图解析 response = call_agent_brain(messages) content = response.choices[0].message.content # 检查是否需要调用工具 if "query_order" in content: order_id = extract_order_id(content) order_info = query_database( sql="SELECT * FROM orders WHERE id = %s", params={"id": order_id} ) # 第二轮:基于工具结果生成回复 messages.append({"role": "assistant", "content": content}) messages.append({"role": "tool", "content": json.dumps(order_info)}) final_response = call_agent_brain(messages) return final_response.choices[0].message.content return content

这样,当Agent运行时,Phoenix(或者你选择的后端)里就能看到一棵完整的调用树:

agent_run (gen_ai.agent.id = xxx, turn_index = 0) ├── llm_call:gpt-6.1-sol (prompt_tokens=520, completion_tokens=85, 1200ms) ├── tool_call:database_query (status=success, 300ms) └── llm_call:gpt-6.1-sol (prompt_tokens=1240, completion_tokens=320, 2100ms)

第五步:基于Trace数据做异常检测

有了完整的trace数据,接下来就可以做有意思的事了——用规则引擎检测异常行为模式。

from collections import defaultdict from datetime import datetime, timedelta

class AgentBehaviorMonitor:
"""基于trace数据的Agent行为监控"""

def __init__(self): self.tool_call_history = defaultdict(list) self.loop_threshold = 3 # 连续相同调用超过3次触发告警 def check_tool_call(self, agent_id, tool_name, arguments, timestamp): """检查工具调用是否异常""" history = self.tool_call_history[agent_id] history.append({ "tool": tool_name, "args_hash": hash(json.dumps(arguments, sort_keys=True)), "time": timestamp }) # 检测1:循环调用检测 recent = history[-self.loop_threshold:] if len(recent) >= self.loop_threshold: all_same_tool = all(c["tool"] == tool_name for c in recent) all_same_args = all( c["args_hash"] == recent[0]["args_hash"] for c in recent ) if all_same_tool and all_same_args: return { "alert": "LOOP_DETECTED", "detail": f"Agent {agent_id} 连续{self.loop_threshold}" f"次调用 {tool_name} 且参数相同", "action": "CIRCUIT_BREAK" } # 检测2:高频调用检测 one_minute_ago = timestamp - timedelta(minutes=1) calls_last_minute = [ c for c in history if c["time"] > one_minute_ago ] if len(calls_last_minute) > 20: return { "alert": "HIGH_FREQUENCY", "detail": f"Agent {agent_id} 1分钟内调用了" f"{len(calls_last_minute)}次工具", "action": "RATE_LIMIT" } # 检测3:敏感操作检测 sensitive_tools = ["database_write", "send_email", "delete_resource"] if tool_name in sensitive_tools: return { "alert": "SENSITIVE_OPERATION", "detail": f"Agent执行了敏感操作: {tool_name}", "action": "REQUIRE_HUMAN_APPROVAL" } return None

这段代码看着简单,但在生产环境中能帮你挡住大部分灾难。

比如Agent陷入死循环(反复用同样的参数调用同一个工具),传统做法是等它把额度烧完你才发现。有了行为监控,3次相同调用就触发熔断。

再比如Agent突然开始调用敏感工具(写数据库、发邮件),系统自动要求人工确认后才执行。

第六步:采样策略——别让trace数据把你的存储吃垮

生产环境有个现实问题:每条trace都完整记录,数据量会非常大。LLM调用的Span包含prompt和response文本,单条trace可能就有几十KB。

推荐的采样策略(来源:Uptrace 2026年OTel最佳实践文档):

import random

class SmartSampler:
"""智能采样:错误和慢请求全量记录,正常请求按比例采样"""

def __init__(self, normal_sample_rate=0.1): self.normal_rate = normal_sample_rate def should_sample(self, span) -> bool: # 错误的Span全量记录 if span.status.status_code == trace.StatusCode.ERROR: return True # 耗时超过30秒的Span全量记录 duration_ms = span.attributes.get("gen_ai.response.duration_ms", 0) if duration_ms > 30000: return True # token消耗超过5000的Span全量记录 total_tokens = ( span.attributes.get("gen_ai.usage.prompt_tokens", 0) + span.attributes.get("gen_ai.usage.completion_tokens", 0) ) if total_tokens > 5000: return True # 正常请求按比例采样 return random.random() < self.normal_rate

这个策略的核心逻辑:正常的调用采样10%就够了(省存储),但出错、慢、贵的调用必须100%记录——因为这些恰恰是你需要排查的。

监控平台选型:5个工具实测对比

后端存储和可视化这块,2026年主流的选择大概有这几个(以下数据来源于befailproof.ai 2026年10月的横向评测):

平台开源自托管起价核心优势局限性
LangfuseMIT支持免费/29刀/月框架无关、OTel原生、成本最低重度trace下UI较慢
LangSmith否企业版免费/39刀/人/月LangChain零配置集成绑定LangChain生态
Arize Phoenix弹性许可支持免费轻量、OTel兼容、开源评测企业功能需Arize AX
Confident AI否企业版免费/200刀/月评测驱动、自动失败分类不自托管较贵
Datadog LLM否否按用量基础设施统一视图价格高、Agent深度不足

我的建议:

  • 预算紧张 + 想自托管:选Langfuse。MIT协议,5人团队100万trace/月大约$69(vs LangSmith约$420)
  • 全栈LangChain:选LangSmith,零配置省心
  • 想自己控制一切:选Phoenix + ClickHouse,完全开源
  • 已有Datadog:直接用Datadog的LLM Observability,省一套系统

不管选哪个,前端接入都是OTLP协议——上面写的代码一行都不用改。这也是OpenTelemetry的价值所在。

几个踩过的坑

最后分享几个我在实际项目中踩过的坑,帮你省点时间。

坑1:Trace数据里包含了用户敏感信息。

Prompt里可能有用户的手机号、身份证号。如果你直接把prompt和response存进trace,这些数据就进入了可观测性系统。解决方案是在记录前做一层脱敏:

import re

def sanitize_text(text):
“”“脱敏处理:替换手机号、身份证等敏感信息”“”
text = re.sub(r’1[3-9]\d{9}‘, ‘PHONE’, text)
text = re.sub(r’\d{17}[\dXx]', ‘ID_CARD’, text)
return text

坑2:Agent的多轮对话导致trace树太深。

如果一个Agent有10轮工具调用,每轮都有LLM调用,trace树会变得非常深。建议设置最大追踪深度,超过深度的调用合并为一个聚合Span。

坑3:别忘了给trace加上业务标签。

纯技术属性(token数、耗时)不够用。你需要加上业务维度的标签——用户ID、租户ID、业务场景、对话ID。这样你才能回答"某个租户的Agent平均消耗是不是异常偏高"这种问题。

span.set_attribute("business.tenant_id", tenant_id) span.set_attribute("business.scenario", "customer_service") span.set_attribute("business.conversation_id", conv_id)

总结

AI Agent和传统微服务最大的区别在于:传统服务是确定性的(同样的输入同样的输出),Agent是概率性的(同样的输入,行为可能完全不同)。

这意味着传统APM那一套——看延迟、看错误率、看CPU——对Agent基本不够用。你需要知道它选择了哪个工具、为什么这么选、选完之后结果如何、有没有跑偏。

OpenTelemetry + GenAI语义规范 + 行为监控引擎,是目前我能想到的最务实的方案。不是最炫酷的,但确实是最管用的。

如果你们团队也在做Agent的可观测性,欢迎在评论区聊聊你们用了什么方案。踩过什么坑也说说,让大家少走弯路。


本文代码基于OpenTelemetry Python SDK 1.28+,Phoenix 8.x测试通过。文中引用的安全事件数据来源于OpenAI和Anthropic 2026年7月公开披露的安全报告。

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

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

立即咨询