给 AI Agent 装上「黑匣子」:用 OpenTelemetry + OpenInference 手写可观测性(附 Python 实战)
📖 摘要:只有约 5% 的企业级 Agent 能真正上生产,根因往往不是模型不行,而是「看不见」——它选了哪个工具、为什么跑偏、烧了多少 token,全靠猜。本文用 OpenTelemetry + OpenInference 语义约定,从零手写一个最小 Agent Tracer,覆盖 Span/Trace/Thread 层级模型、LLM 与工具调用埋点、OTLP 导出对接 Langfuse/Phoenix,并落地「轨迹/质量/安全/成本」四大监控支柱与三条生产避坑清单。读完你能给自己的 Agent 装上可 replay 的黑匣子。
🏷️ 关键词:AI Agent,可观测性,OpenTelemetry,OpenInference,Tracing
目录
- 一、为什么 Agent 需要「黑匣子」
- 二、核心概念:Span / Trace / Thread 与 OpenInference
- 三、实战:手写一个最小 Agent Tracer
- 四、从「日志」到「可观测」:四个支柱落地
- 五、生产落地避坑清单
- 六、总结与选型建议
一、为什么 Agent 需要「黑匣子」
1.1 传统 APM 为什么看不见 Agent
传统 APM(如 Datadog、New Relic)擅长看「HTTP 状态码、CPU、延迟」,但面对 Agent 时会失明。原因有三:
- 输入是无限的:同样的请求,Agent 这次走工具 A、下次走工具 B,行为非确定性。
- 质量藏在对话里:返回了「一段文字」≠「完成了目标」,APM 看不出语义层面的失败。
- 失败模式是全新的:幻觉、无限循环、prompt 注入、工具误调用——这些在传统软件里不存在。
💡 一句话:APM 告诉你「服务挂没挂」,可观测性告诉你「Agent 这次到底怎么想的、为什么错」。
1.2 只有 5% 的 Agent 能上生产
多个 2026 年的行业统计指向同一个数字:企业级 Agent 真正上生产的比例约 5%。为什么?当一个请求会扇出几十条模型调用、检索、工具调用时,一旦出错、变慢或悄悄烧预算,你必须有能力回放完整链路,看清「哪一步失败、为什么、花了多少」。没有这层能力,就只能靠人工逐条 review——而人工上限约 50~100 条 trace/小时,日请求过千就要 10~20 小时/天,不可持续。
1.3 可观测性的四个支柱
把监控维度拆成四根柱子,缺一根都会「看得见却救不了」:
| 支柱 | 衡量什么 | 关键指标 |
|---|---|---|
| 轨迹 / 工具使用 | 选对工具了吗?是否跑题、回退、死循环? | 推理深度、工具错误率、loop 次数 |
| 质量 / 幻觉 | 回答满足需求吗?有依据吗? | 任务完成率、忠实度、幻觉率 |
| 安全 / 策略 | 越权、被 jailbreak、数据外泄? | 注入尝试、越权访问、策略违规 |
| 成本 / 延迟 | 每步花多少 token 和时间? | 单次成本、step 耗时、重试次数 |
二、核心概念:Span / Trace / Thread 与 OpenInference
2.1 三层层级模型
Agent 的每次运行不是一条线,而是三层嵌套:
- Span(跨度):一次原子操作——一次 LLM 调用、一次工具调用、一次检索。
- Trace(链路):一次 Agent 交互里所有 Span 的集合,从请求到响应完整回放。
- Thread(会话):一组 Trace 组成的多轮对话,保留跨轮上下文。
理解这三层,你就明白为什么「在日志里 print 一下」救不了 Agent:print 是扁平的,而 Agent 的真相在嵌套的因果树里。
2.2 OpenTelemetry 与 OpenInference 是什么(以及边界)
- OpenTelemetry(OTel):厂商中立的遥测标准,一次埋点可发往不同后端(Langfuse、Phoenix、Datadog、Grafana),本质是「日志/指标/链路」的通用协议,不绑定任何 Agent 框架。
- OpenInference:基于 OTel 的 GenAI 语义约定层,它定义了「LLM 调用」「工具调用」该带哪些标准属性(模型名、prompt、completion、token 数),让后端能自动识别并可视化 agent 链路。本质是 OTel 的「AI 方言扩展」,不是另一个独立体系。
⚠️ 边界澄清:OpenInference 不是可观测性平台本身,它只定义属性规范;真正存数据、画链路图的是 Langfuse/Phoenix 这类后端。它也不是评估框架——评估要靠下文第四支柱另外接。
2.3 GenAI 语义约定长什么样
OpenInference 把一次 LLM Span 应携带的属性标准化了,常见键:
llm.model.name、llm.input_messages、llm.output_messagesllm.token_count.prompt、llm.token_count.completion、llm.token_count.totaltool.name、tool.args、tool.result
后端拿到这些标准键,就能自动渲染出「对话气泡 + token 消耗」的漂亮视图,而不用你写一堆自定义解析。
三、实战:手写一个最小 Agent Tracer
3.1 环境准备
安装 OTel SDK 与 OpenInference 语义约定包(示例数据,仅作演示):
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
pip install openinference-semantic-conventions
3.2 用 OTel SDK 手动埋点
我们用标准 OTel API 创建 TracerProvider,并挂两个导出器:控制台(调试)和 OTLP(对接后端)。
3.2.1 关键步骤
第 1 步:初始化 Provider 与导出器
# agent_tracer.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
# 初始化全局 TracerProvider(一个进程一个即可)
provider = TracerProvider()
# 控制台导出:本地调试时直接看 span 树
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
# OTLP 导出:发往 127.0.0.1:4318(Langfuse/Phoenix 的 OTel 接收端点)
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://127.0.0.1:4318/v1/traces"))
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("demo.agent.tracer")
第 2 步:用 OpenInference 标准属性包裹 LLM 调用
from opentelemetry.trace import SpanKind
from openinference.semconv.trace import SpanAttributes # OpenInference 语义约定
def traced_llm_call(prompt: str, model: str = "example-model") -> str:
with tracer.start_as_current_span("llm.call", kind=SpanKind.CLIENT) as span:
# 注入标准属性:后端一看就知道这是一次 LLM 调用
span.set_attribute(SpanAttributes.LLM_MODEL_NAME, model)
span.set_attribute(SpanAttributes.LLM_INPUT_MESSAGES,
[{"role": "user", "content": prompt}])
span.set_attribute(SpanAttributes.LLM_INVOCATION_PARAMETERS, '{"temperature": 0.2}')
# —— 此处替换为你的真实模型调用;示例用占位返回 ——
completion = f"[示例] 针对「{prompt[:20]}…」的回复"
span.set_attribute(SpanAttributes.LLM_OUTPUT_MESSAGES,
[{"role": "assistant", "content": completion}])
span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_PROMPT, 128)
span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_COMPLETION, 64)
span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_TOTAL, 192)
return completion
第 3 步:包裹工具调用
def traced_tool_call(tool_name: str, args: dict) -> str:
with tracer.start_as_current_span(f"tool.{tool_name}") as span:
span.set_attribute(SpanAttributes.TOOL_NAME, tool_name)
span.set_attribute(SpanAttributes.TOOL_ARGS, str(args))
# 示例:真实场景替换为 search_orders 等工具的返回值
result = f"[示例] {tool_name} 返回结果"
span.set_attribute(SpanAttributes.TOOL_RESULT, result)
return result
3.3 导出到控制台 / OTLP
上面的 ConsoleSpanExporter 会把 span 直接打印到终端,适合本地联调;OTLPSpanExporter 则用 HTTP 把数据推到 127.0.0.1:4318/v1/traces。该端点同时被 Langfuse(self-hosted) 与 Arize Phoenix 兼容——也就是说,你写一遍埋点,就能在两者里看到同一棵链路树,彻底避免厂商锁定。
3.4 一个 ReAct Agent 的完整链路追踪
把上面两个包裹器串起来,就得到一个「思考 → 选工具 → 调工具 → 合成」的多步 Trace:
def run_react_agent(question: str):
with tracer.start_as_current_span("agent.run") as root:
root.set_attribute("agent.question", question) # 顶层 Thread/Trace 上下文
thought = traced_llm_call(question) # Span: llm.call
tool_out = traced_tool_call("search_orders", # Span: tool.search_orders
{"user_id": "u_1001"})
answer = traced_llm_call(f"基于工具结果回答:{tool_out}") # Span: llm.call
root.set_attribute("agent.answer", answer)
return answer
if __name__ == "__main__":
run_react_agent("帮我查 u_1001 最近的订单状态") # 示例问题,仅作演示
运行后,你会在控制台看到三个嵌套 Span;接上 Langfuse/Phoenix 后则是一张可点击回放的瀑布图——哪次 LLM 调用最慢、哪个工具报错、总共烧了多少 token,一目了然。
四、从「日志」到「可观测」:四个支柱落地
4.1 成本归因:把 token 花到模型/用户/功能
在 LLM Span 上记录 llm.token_count.total,并把 user_id、feature 作为 span 属性。后端就能按「模型 × 用户 × 功能」聚合成本,快速定位「是哪个功能的哪段 prompt 在烧钱」。
4.2 质量评估:在线 eval + LLM-as-judge
可观测性离开评估就是「昂贵的日志」。在 Trace 上挂在线评估:抽 10~20% 的线上流量,用 LLM-as-judge 或数据集打分,跟踪「任务完成率、忠实度、幻觉率」随时间变化。Score 下滑自动告警,才能把「猜测它行」变成「证明它行」。
4.3 安全信号:prompt injection 与越权
在工具 Span 记录 tool.name 与入参,对「未授权工具调用」「疑似注入的 prompt 片段」做规则/模型检测。越权或 jailbreak 尝试会像普通调用一样出现在链路里——关键是你要对这些 Span 打 security.flag 并触发告警。
4.4 延迟瓶颈:span 级耗时定位
每个 Span 自带起止时间。把慢检索、冗余模型调用、过多重试的耗时叠加,就能定位「总响应 8 秒里,6 秒卡在检索」。没有 span 级计时,这个 6 秒会被埋没在一次不透明的请求里。
五、生产落地避坑清单
5.1 采样率:别用 100%
全量记录既贵又没必要。线上评估通常取 10~20% 的 trace 做抽样即可,既覆盖异常分布,又把存储与成本压住。关键路径可全采,长尾流量抽样。
5.2 隐私:trace 里别打 PII
链路会原样记录 prompt / 工具入参,极易夹带手机号、token、密钥、用户隐私。生产环境务必在导出前做脱敏(敏感字段替换为 ***),或只对非敏感功能开启全量记录。示例里的 u_1001 也是虚构占位,真实场景要评估合规。
5.3 别把可观测性当「昂贵的日志」
这是最常见的误区:接了 tracing 却从不接 eval,等于只花钱存了一堆看不懂的树。tracing 揭示「发生了什么」,evaluation 回答「好不好」——两者必须闭环,否则可观测性只是更贵的日志系统。
六、总结与选型建议
本文用约 60 行 Python,基于 OpenTelemetry + OpenInference 给 Agent 装上了一个可 replay 的「黑匣子」:从 Provider 初始化、LLM/工具 Span 埋点、OTLP 导出,到四支柱落地与生产避坑。核心结论三条:
- Agent 上不了生产,多半是「看不见」,不是「模型弱」——先补可观测性再谈优化。
- OTel 一次埋点、多后端通用,OpenInference 让 AI 语义被标准识别,避免厂商锁定。
- tracing 必须接 eval 才闭环,否则只是昂贵的日志。
💡 延展阅读:本系列前文已覆盖 Agent 记忆层、并行多 Agent 协作、MCP/A2A 协议、Skills 加载机制;可观测性正好是「上线运营」这一环,串起来就是一条完整的 Agent 工程知识链。
如果觉得有用,欢迎点赞收藏,评论区聊聊你用哪套后端做 Agent 可观测~
&spm=1001.2101.3001.5002&articleId=163927011&d=1&t=3&u=6af5947de5724a4b8769a55b8e639cc7)
4943

被折叠的 条评论
为什么被折叠?



