Agent可观测性自建方案:零成本搭建智能体全链路追踪系统(Python 3.10+ / OpenTelemetry)
⚠️ 版本说明:本文基于 Python 3.10+、OpenTelemetry SDK(opentelemetry-api==1.27.0、opentelemetry-sdk==1.27.0)。代码可降级至 Python 3.8+(需调整 match 语法)。自建方案零额外成本,生产环境建议配合 Prometheus+Grafana。
导读:你的Agent在生产环境出问题了,你怎么知道"它到底做了什么"?当用户投诉"Agent给了错误答案",你能在5分钟内定位是LLM幻觉、工具调用参数错误、还是检索到了错误的记忆吗?2026年,Agent从Demo走向生产,可观测性(Observability)不是"可选项"而是"必选项"——没有它,你就是在黑盒里运行Agent。但CSDN上关于Agent可观测的文章,全是LangSmith的入门教程或Langfuse的部署指南,没有人告诉你"怎么从零自建一套Agent可观测体系"。本文用OpenTelemetry + Python,用零额外成本搭建Agent的日志、指标、追踪三支柱,附与LangSmith的对比决策表和GB/Z 185审计合规的映射方案。如果你已经读过我的《GB/Z 185合规的Agent Loop设计》,这篇文章就是7.1可审计性条款的"工程实现"。
文章目录
- Agent可观测性自建方案:零成本搭建智能体全链路追踪系统(Python 3.10+ / OpenTelemetry)
-
- 一、为什么Agent可观测性不是"可选",是"必需"
-
- 1.1 Agent的可观测性比传统应用更复杂
- 1.2 生产环境的真实痛点
- 1.3 与GB/Z 185的关联:可观测性=合规要求
- 二、Agent可观测性的三支柱模型
-
- 2.1 Logs:记录"发生了什么"
- 2.2 Metrics:量化"有多少/多快/多贵"
- 2.3 Traces:追踪"请求走了哪条路"
- 三、Python实现:OpenTelemetry + 自定义Agent追踪器
-
- 3.1 环境准备
- 3.2 核心实现:AgentOpenTelemetryTracer
- 四、与GB/Z 185的映射:7.1可审计性的工程实现
- 五、自建 vs 使用SaaS(LangSmith/Langfuse等):选型决策表
- 六、生产级Dashboard:用Prometheus + Grafana可视化
-
- 6.1 核心Dashboard设计
- 6.2 告警规则(Alertmanager)
- 七、总结
- 八、适用边界与限制条件
一、为什么Agent可观测性不是"可选",是"必需"
1.1 Agent的可观测性比传统应用更复杂
传统Web应用的可观测性关注:请求入口→业务逻辑→数据库→响应。但Agent的运行轨迹完全不同:
#mermaid-svg-dDMKMwWa7QEQ2aTu{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dDMKMwWa7QEQ2aTu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dDMKMwWa7QEQ2aTu .error-icon{fill:#552222;}#mermaid-svg-dDMKMwWa7QEQ2aTu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dDMKMwWa7QEQ2aTu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dDMKMwWa7QEQ2aTu .marker.cross{stroke:#333333;}#mermaid-svg-dDMKMwWa7QEQ2aTu svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dDMKMwWa7QEQ2aTu p{margin:0;}#mermaid-svg-dDMKMwWa7QEQ2aTu .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-dDMKMwWa7QEQ2aTu .cluster-label text{fill:#333;}#mermaid-svg-dDMKMwWa7QEQ2aTu .cluster-label span{color:#333;}#mermaid-svg-dDMKMwWa7QEQ2aTu .cluster-label span p{background-color:transparent;}#mermaid-svg-dDMKMwWa7QEQ2aTu .label text,#mermaid-svg-dDMKMwWa7QEQ2aTu span{fill:#333;color:#333;}#mermaid-svg-dDMKMwWa7QEQ2aTu .node rect,#mermaid-svg-dDMKMwWa7QEQ2aTu .node circle,#mermaid-svg-dDMKMwWa7QEQ2aTu .node ellipse,#mermaid-svg-dDMKMwWa7QEQ2aTu .node polygon,#mermaid-svg-dDMKMwWa7QEQ2aTu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dDMKMwWa7QEQ2aTu .rough-node .label text,#mermaid-svg-dDMKMwWa7QEQ2aTu .node .label text,#mermaid-svg-dDMKMwWa7QEQ2aTu .image-shape .label,#mermaid-svg-dDMKMwWa7QEQ2aTu .icon-shape .label{text-anchor:middle;}#mermaid-svg-dDMKMwWa7QEQ2aTu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dDMKMwWa7QEQ2aTu .rough-node .label,#mermaid-svg-dDMKMwWa7QEQ2aTu .node .label,#mermaid-svg-dDMKMwWa7QEQ2aTu .image-shape .label,#mermaid-svg-dDMKMwWa7QEQ2aTu .icon-shape .label{text-align:center;}#mermaid-svg-dDMKMwWa7QEQ2aTu .node.clickable{cursor:pointer;}#mermaid-svg-dDMKMwWa7QEQ2aTu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dDMKMwWa7QEQ2aTu .arrowheadPath{fill:#333333;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dDMKMwWa7QEQ2aTu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dDMKMwWa7QEQ2aTu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dDMKMwWa7QEQ2aTu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dDMKMwWa7QEQ2aTu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dDMKMwWa7QEQ2aTu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dDMKMwWa7QEQ2aTu .cluster text{fill:#333;}#mermaid-svg-dDMKMwWa7QEQ2aTu .cluster span{color:#333;}#mermaid-svg-dDMKMwWa7QEQ2aTu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dDMKMwWa7QEQ2aTu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dDMKMwWa7QEQ2aTu rect.text{fill:none;stroke-width:0;}#mermaid-svg-dDMKMwWa7QEQ2aTu .icon-shape,#mermaid-svg-dDMKMwWa7QEQ2aTu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dDMKMwWa7QEQ2aTu .icon-shape p,#mermaid-svg-dDMKMwWa7QEQ2aTu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dDMKMwWa7QEQ2aTu .icon-shape .label rect,#mermaid-svg-dDMKMwWa7QEQ2aTu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dDMKMwWa7QEQ2aTu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dDMKMwWa7QEQ2aTu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dDMKMwWa7QEQ2aTu :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
用户输入
LLM意图识别
工具调用?
调用MCP Server
工具返回结果
LLM再思考
最终输出
记忆检索
向量数据库
这个循环中,任何一个环节都可能出问题:
- LLM意图识别:用户问"查一下数据",LLM理解为"查天气"→后面的工具调用全错
- 工具调用参数:参数幻觉,传了{"city": "北京"}给数据库查询工具→报错
- 记忆检索:检索到了去年的旧数据,当作当前数据使用→决策偏差
- 循环发散:Agent在"搜索→没找到→再搜索"里循环了10轮→token烧光
没有可观测性,你只知道"输出错了",但不知道"错在哪一步"。
1.2 生产环境的真实痛点
| 响应慢 | “Agent回复太慢了” | 不知道慢在哪 | 追踪显示:工具调用耗时3s,LLM推理耗时1s→优化工具端 |
| 结果错 | “Agent给的数据是错的” | 不知道错在哪 | 追踪显示:检索到了旧记忆,向量数据库中数据已过时→更新记忆 |
| 费用高 | “这个Agent怎么这么贵” | 不知道token花在哪 | 指标显示:平均每次调用消耗5000 tokens,循环迭代4次→优化Prompt缩短循环 |
| 不稳定 | “有时对有时错” | 无法复现问题 | 追踪显示:工具A有50%概率超时→增加重试机制或替换工具 |
| 安全事件 | “Agent泄露了敏感信息” | 无法追溯 | 日志显示:Agent在循环第3轮通过read_file读取了/etc/passwd→加固权限 |
1.3 与GB/Z 185的关联:可观测性=合规要求
GB/Z 185 7.1条款明确要求:
“全链路操作可追溯、可回放。”
这不是"建议",是强制要求。如果你的Agent在受监管的行业(金融、政务、医疗)使用,没有可观测性就是不合规。
二、Agent可观测性的三支柱模型
可观测性不是"打个日志"那么简单,而是三个支柱的协同:
#mermaid-svg-xjys12PLCr29OEqd{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xjys12PLCr29OEqd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xjys12PLCr29OEqd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xjys12PLCr29OEqd .error-icon{fill:#552222;}#mermaid-svg-xjys12PLCr29OEqd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xjys12PLCr29OEqd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xjys12PLCr29OEqd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xjys12PLCr29OEqd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xjys12PLCr29OEqd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xjys12PLCr29OEqd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xjys12PLCr29OEqd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xjys12PLCr29OEqd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xjys12PLCr29OEqd .marker.cross{stroke:#333333;}#mermaid-svg-xjys12PLCr29OEqd svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xjys12PLCr29OEqd p{margin:0;}#mermaid-svg-xjys12PLCr29OEqd .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-xjys12PLCr29OEqd .cluster-label text{fill:#333;}#mermaid-svg-xjys12PLCr29OEqd .cluster-label span{color:#333;}#mermaid-svg-xjys12PLCr29OEqd .cluster-label span p{background-color:transparent;}#mermaid-svg-xjys12PLCr29OEqd .label text,#mermaid-svg-xjys12PLCr29OEqd span{fill:#333;color:#333;}#mermaid-svg-xjys12PLCr29OEqd .node rect,#mermaid-svg-xjys12PLCr29OEqd .node circle,#mermaid-svg-xjys12PLCr29OEqd .node ellipse,#mermaid-svg-xjys12PLCr29OEqd .node polygon,#mermaid-svg-xjys12PLCr29OEqd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xjys12PLCr29OEqd .rough-node .label text,#mermaid-svg-xjys12PLCr29OEqd .node .label text,#mermaid-svg-xjys12PLCr29OEqd .image-shape .label,#mermaid-svg-xjys12PLCr29OEqd .icon-shape .label{text-anchor:middle;}#mermaid-svg-xjys12PLCr29OEqd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xjys12PLCr29OEqd .rough-node .label,#mermaid-svg-xjys12PLCr29OEqd .node .label,#mermaid-svg-xjys12PLCr29OEqd .image-shape .label,#mermaid-svg-xjys12PLCr29OEqd .icon-shape .label{text-align:center;}#mermaid-svg-xjys12PLCr29OEqd .node.clickable{cursor:pointer;}#mermaid-svg-xjys12PLCr29OEqd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xjys12PLCr29OEqd .arrowheadPath{fill:#333333;}#mermaid-svg-xjys12PLCr29OEqd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xjys12PLCr29OEqd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xjys12PLCr29OEqd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xjys12PLCr29OEqd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xjys12PLCr29OEqd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xjys12PLCr29OEqd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xjys12PLCr29OEqd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xjys12PLCr29OEqd .cluster text{fill:#333;}#mermaid-svg-xjys12PLCr29OEqd .cluster span{color:#333;}#mermaid-svg-xjys12PLCr29OEqd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xjys12PLCr29OEqd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xjys12PLCr29OEqd rect.text{fill:none;stroke-width:0;}#mermaid-svg-xjys12PLCr29OEqd .icon-shape,#mermaid-svg-xjys12PLCr29OEqd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xjys12PLCr29OEqd .icon-shape p,#mermaid-svg-xjys12PLCr29OEqd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xjys12PLCr29OEqd .icon-shape .label rect,#mermaid-svg-xjys12PLCr29OEqd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xjys12PLCr29OEqd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xjys12PLCr29OEqd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xjys12PLCr29OEqd :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Agent可观测性三支柱
Logs 日志记录发生了什么
Metrics 指标有多少/多快/多贵
Traces 追踪请求经过的路径
LLM调用日志
工具调用日志
记忆操作日志
安全事件日志
请求QPS
Token消耗/次
循环迭代次数
错误率
请求链路图
循环迭代路径
工具调用依赖
跨Agent协作路径
2.1 Logs:记录"发生了什么"
Agent的日志不是传统的"INFO/ERROR",而是结构化的事件日志:
| llm_decision | LLM的输入、输出、模型版本、耗时 | “gpt-4o, prompt=1024tokens, output=256tokens, 1450ms” |
| tool_call | 工具名称、参数、返回结果、耗时 | “get_weather, params={city:‘北京’}, result=晴, 320ms” |
| memory_retrieval | 查询query、返回结果、相似度分数 | “query=‘用户偏好’, top3=[{id:1, score:0.92}]” |
| loop_iteration | 循环轮次、当前状态、是否终止 | “iteration=3, state=working, is_complete=false” |
| security_event | 事件类型、风险等级、触发条件 | “path_traversal_detected, level=high, path=‘/etc/passwd’” |
| error | 异常类型、堆栈、恢复策略 | “ToolTimeout, retry=2, fallback=cache” |
2.2 Metrics:量化"有多少/多快/多贵"
Agent特有的指标:
| agent_request_qps | Counter | 每秒请求数 | >100 |
| agent_request_latency | Histogram | 请求响应时间(P50/P90/P99) | P99>5s |
| agent_token_usage | Counter | 单次请求消耗的token数 | >8000 |
| agent_loop_iterations | Histogram | 循环迭代次数 | >10 |
| agent_tool_call_errors | Counter | 工具调用失败次数 | 错误率>5% |
| agent_memory_retrieval_relevance | Gauge | 检索结果平均相关度 | <0.7 |
| agent_llm_cost | Counter | 单次请求的LLM成本($) | >$0.5 |
2.3 Traces:追踪"请求走了哪条路"
Trace是Agent可观测性的核心——它记录了一个请求从入口到出口的完整路径,每个环节的时间戳和上下文。
Trace: request_abc123
├── Span: user_input (0ms)
│ └── 标签: intent="sales_query"
├── Span: llm_decision_1 (50ms – 1200ms)
│ ├── 标签: model="gpt-4o", prompt_tokens=512
│ └── 事件: tool_call_decided="get_sales_data"
├── Span: tool_call_get_sales (1200ms – 3200ms)
│ ├── 标签: tool="get_sales_data", params="{quarter:'Q2'}"
│ └── 事件: result_size=1024
├── Span: memory_retrieval (3200ms – 3400ms)
│ ├── 标签: query="用户偏好", top_k=3
│ └── 事件: results=[{id:1, score:0.92}, …]
├── Span: llm_decision_2 (3400ms – 4200ms)
│ └── 标签: is_complete=true
└── Span: final_output (4200ms – 4250ms)
└── 标签: output_tokens=256, total_cost=$0.023
三、Python实现:OpenTelemetry + 自定义Agent追踪器
3.1 环境准备
⚠️ 以下依赖锁定版本号,确保可复现。
# 安装核心依赖(锁定版本)
pip install opentelemetry-api==1.27.0 opentelemetry-sdk==1.27.0
pip install opentelemetry-exporter-otlp==1.27.0
# 验证安装
python -c "import opentelemetry; print(opentelemetry.__version__)"
# 预期输出:1.27.0
3.2 核心实现:AgentOpenTelemetryTracer
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader, ConsoleMetricExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from contextlib import contextmanager
from typing import Dict, Any, Optional, List
from datetime import datetime
import json
import time
import uuid
class AgentOpenTelemetryTracer:
"""
Agent可观测性追踪器:基于OpenTelemetry的Agent全链路追踪。
功能:
– Trace:请求链路追踪(Span嵌套)
– Metrics:Agent特有指标采集
– Logs:结构化事件日志
"""
def __init__(self, service_name: str = "agent_service",
export_to_console: bool = True,
export_to_otlp: bool = False,
otlp_endpoint: str = "http://localhost:4317"):
self.service_name = service_name
# 1. 初始化Trace Provider
self.trace_provider = TracerProvider()
if export_to_console:
self.trace_provider.add_span_processor(
BatchSpanProcessor(ConsoleSpanExporter())
)
if export_to_otlp:
otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
self.trace_provider.add_span_processor(
BatchSpanProcessor(otlp_exporter)
)
trace.set_tracer_provider(self.trace_provider)
self.tracer = trace.get_tracer(service_name)
# 2. 初始化Metrics Provider
metric_readers = []
if export_to_console:
metric_readers.append(PeriodicExportingMetricReader(ConsoleMetricExporter()))
self.metrics_provider = MeterProvider(metric_readers=metric_readers)
metrics.set_meter_provider(self.metrics_provider)
self.meter = metrics.get_meter(service_name)
# 3. 定义Agent特有指标
self._init_metrics()
def _init_metrics(self):
"""初始化Agent特有指标。"""
# 请求计数器
self.request_counter = self.meter.create_counter(
"agent.request.count",
description="Agent请求总数"
)
# Token使用量
self.token_counter = self.meter.create_counter(
"agent.token.usage",
description="Agent Token使用量",
unit="token"
)
# 请求耗时直方图
self.request_latency = self.meter.create_histogram(
"agent.request.latency",
description="Agent请求耗时",
unit="ms"
)
# 循环迭代次数
self.loop_iterations = self.meter.create_histogram(
"agent.loop.iterations",
description="Agent循环迭代次数"
)
# 工具调用错误率
self.tool_error_counter = self.meter.create_counter(
"agent.tool.error",
description="Agent工具调用失败次数"
)
# 检索相关度
self.retrieval_relevance = self.meter.create_histogram(
"agent.memory.retrieval_relevance",
description="记忆检索结果平均相关度"
)
@contextmanager
def trace_request(self, user_input: str, request_id: Optional[str] = None):
"""
追踪一次完整的Agent请求(Trace根Span)。
使用方式:
with tracer.trace_request("查北京天气") as ctx:
# Agent执行逻辑…
pass
"""
req_id = request_id or str(uuid.uuid4())[:8]
with self.tracer.start_as_current_span(
name="agent_request",
attributes={
"agent.request_id": req_id,
"agent.user_input": user_input[:100],
"agent.input_length": len(user_input),
}
) as span:
start_time = time.time()
try:
yield {
"request_id": req_id,
"span": span,
"start_time": start_time
}
# 请求成功
span.set_attribute("agent.status", "success")
self.request_counter.add(1, {"status": "success"})
except Exception as e:
# 请求失败
span.set_attribute("agent.status", "error")
span.set_attribute("agent.error", str(e)[:200])
span.record_exception(e)
self.request_counter.add(1, {"status": "error"})
raise
finally:
duration = (time.time() – start_time) * 1000
self.request_latency.record(duration)
span.set_attribute("agent.duration_ms", duration)
@contextmanager
def trace_llm_call(self, model: str, prompt_length: int):
"""追踪一次LLM调用(子Span)。"""
with self.tracer.start_as_current_span(
name="llm_call",
attributes={
"llm.model": model,
"llm.prompt_length": prompt_length,
}
) as span:
start_time = time.time()
try:
yield span
finally:
duration = (time.time() – start_time) * 1000
span.set_attribute("llm.duration_ms", duration)
def record_llm_output(self, span, output_tokens: int, output_text: str):
"""记录LLM输出结果。"""
span.set_attribute("llm.output_tokens", output_tokens)
span.set_attribute("llm.output_length", len(output_text))
self.token_counter.add(output_tokens, {"component": "llm_output"})
@contextmanager
def trace_tool_call(self, tool_name: str, params: Dict):
"""追踪一次工具调用(子Span)。"""
with self.tracer.start_as_current_span(
name="tool_call",
attributes={
"tool.name": tool_name,
"tool.params": json.dumps(params, ensure_ascii=False)[:500],
}
) as span:
start_time = time.time()
try:
yield span
span.set_attribute("tool.status", "success")
except Exception as e:
span.set_attribute("tool.status", "error")
span.set_attribute("tool.error", str(e)[:200])
self.tool_error_counter.add(1, {"tool": tool_name})
raise
finally:
duration = (time.time() – start_time) * 1000
span.set_attribute("tool.duration_ms", duration)
def record_tool_result(self, span, result: Any, result_size: int = 0):
"""记录工具返回结果。"""
result_preview = str(result)[:200] if result else "None"
span.set_attribute("tool.result_preview", result_preview)
span.set_attribute("tool.result_size", result_size)
def record_memory_retrieval(self, query: str, results: List[Dict]):
"""记录记忆检索操作。"""
with self.tracer.start_as_current_span("memory_retrieval") as span:
span.set_attribute("memory.query", query[:100])
span.set_attribute("memory.result_count", len(results))
if results:
avg_score = sum(r.get("score", 0) for r in results) / len(results)
span.set_attribute("memory.avg_relevance", avg_score)
self.retrieval_relevance.record(avg_score)
def record_loop_iteration(self, iteration: int, state: str, is_complete: bool):
"""记录循环迭代状态。"""
with self.tracer.start_as_current_span("loop_iteration") as span:
span.set_attribute("loop.iteration", iteration)
span.set_attribute("loop.state", state)
span.set_attribute("loop.is_complete", is_complete)
if is_complete:
self.loop_iterations.record(iteration)
def record_security_event(self, event_type: str, risk_level: str, details: str):
"""记录安全事件。"""
with self.tracer.start_as_current_span("security_event") as span:
span.set_attribute("security.event_type", event_type)
span.set_attribute("security.risk_level", risk_level)
span.set_attribute("security.details", details[:200])
span.set_status(trace.Status(trace.StatusCode.ERROR, f"Security: {event_type}"))
def get_trace_context(self) –> Dict[str, str]:
"""获取当前Trace上下文(用于跨Agent传递)。"""
current_span = trace.get_current_span()
trace_id = current_span.get_span_context().trace_id
span_id = current_span.get_span_context().span_id
return {
"trace_id": format(trace_id, '032x'),
"span_id": format(span_id, '016x'),
}
# ========== 使用示例:与Agent Loop整合 ==========
class ObservableAgent:
"""集成了可观测性的Agent Loop。"""
def __init__(self, llm, tools, tracer: AgentOpenTelemetryTracer):
self.llm = llm
self.tools = tools
self.tracer = tracer
def run(self, user_input: str) –> str:
"""运行Agent,全链路被追踪。"""
with self.tracer.trace_request(user_input) as ctx:
request_id = ctx["request_id"]
print(f"[Request {request_id}] 开始处理: {user_input[:50]}…")
# 1. LLM决策
with self.tracer.trace_llm_call("gpt-4o", len(user_input)) as llm_span:
# 模拟LLM调用
time.sleep(0.1)
decision = "调用get_weather工具"
self.tracer.record_llm_output(llm_span, output_tokens=50, output_text=decision)
# 2. 工具调用
with self.tracer.trace_tool_call("get_weather", {"city": "北京"}) as tool_span:
time.sleep(0.2)
result = {"temperature": 28, "condition": "晴"}
self.tracer.record_tool_result(tool_span, result, result_size=256)
# 3. 记忆检索(可选)
self.tracer.record_memory_retrieval(
query="用户偏好",
results=[{"id": 1, "score": 0.92, "content": "喜欢深色模式"}]
)
# 4. 循环迭代记录
self.tracer.record_loop_iteration(
iteration=1, state="completed", is_complete=True
)
final_output = "北京今天晴,气温28°C。"
print(f"[Request {request_id}] 完成: {final_output}")
return final_output
# ========== 演示 ==========
if __name__ == "__main__":
# 创建追踪器(输出到控制台,便于演示)
tracer = AgentOpenTelemetryTracer(
service_name="weather_agent",
export_to_console=True,
export_to_otlp=False
)
# 创建Agent
agent = ObservableAgent(llm=None, tools=[], tracer=tracer)
# 运行请求
result = agent.run("帮我查一下北京天气")
print(f"\\n最终输出: {result}")
四、与GB/Z 185的映射:7.1可审计性的工程实现
GB/Z 185 7.1条款要求"全链路操作可追溯"。本文的可观测性体系直接对应:
| 操作可追溯 | Logs | record_security_event + trace_tool_call | 每条操作都有时间戳、操作者、结果 |
| 操作可回放 | Traces | trace_request的完整Span链 | 给定request_id,可以回放完整执行路径 |
| 异常可定位 | Metrics + Traces | tool_error_counter + span.record_exception | 错误率和错误类型实时可见 |
| 责任可认定 | Logs | trace_context传递Agent ID | 跨Agent协作时,可以追溯每个环节的责任方 |
| 数据可审计 | Logs | record_memory_retrieval + 敏感操作日志 | 记忆检索、工具调用、LLM决策都有记录 |
五、自建 vs 使用SaaS(LangSmith/Langfuse等):选型决策表
| 成本 | 零额外成本(OpenTelemetry免费) | 按量付费($0.005/trace起) | 自托管免费,云服务按量 |
| 数据隐私 | 数据完全在本地 | 数据上传到第三方服务器 | 自托管数据在本地 |
| 定制化 | 完全可定制(Agent特有指标) | 有限定制(用他们定义的指标) | 中等定制 |
| 集成复杂度 | 中(需自己写代码) | 低(SDK集成) | 低-中(SDK集成) |
| Dashboard | 需自己搭建(Prometheus+Grafana) | 开箱即用 | 开箱即用 |
| 告警能力 | 强(Prometheus Alertmanager) | 中(内置告警) | 中(内置告警) |
| 学习曲线 | 陡峭(需理解OpenTelemetry) | 平缓 | 平缓 |
| 适用场景 | 数据敏感、深度定制、成本敏感 | 快速启动、不想运维 | 数据敏感但不想自建全栈 |
建议:
- 快速验证:先用LangSmith SDK跑通,验证可观测性的价值
- 生产落地:数据敏感或成本敏感时,迁移到本文的自建方案
- 混合方案:LangSmith做快速排查,自建方案做深度分析和合规审计
六、生产级Dashboard:用Prometheus + Grafana可视化
6.1 核心Dashboard设计
# 如果使用Prometheus导出,可以这样定义Dashboard面板
AGENT_DASHBOARD_PANELS = [
{
"title": "Agent请求QPS",
"query": "rate(agent_request_count[1m])",
"type": "graph",
"alert": "QPS > 100"
},
{
"title": "请求延迟P99",
"query": "histogram_quantile(0.99, rate(agent_request_latency_bucket[5m]))",
"type": "graph",
"alert": "P99 > 5000ms"
},
{
"title": "Token消耗/请求",
"query": "rate(agent_token_usage[5m]) / rate(agent_request_count[5m])",
"type": "stat",
"alert": "> 8000"
},
{
"title": "工具调用错误率",
"query": "rate(agent_tool_error[5m]) / rate(agent_tool_call_count[5m])",
"type": "graph",
"alert": "> 0.05"
},
{
"title": "记忆检索相关度",
"query": "avg(agent_memory_retrieval_relevance)",
"type": "gauge",
"alert": "< 0.7"
},
{
"title": "循环迭代次数",
"query": "avg(agent_loop_iterations)",
"type": "graph",
"alert": "> 10"
}
]
6.2 告警规则(Alertmanager)
# agent_alerts.yml
groups:
– name: agent_alerts
rules:
– alert: AgentHighLatency
expr: histogram_quantile(0.99, rate(agent_request_latency_bucket[5m])) > 5000
for: 5m
labels:
severity: warning
annotations:
summary: "Agent请求延迟过高"
description: "P99延迟超过5秒,当前值: {{ $value }}ms"
– alert: AgentHighErrorRate
expr: rate(agent_tool_error[5m]) / rate(agent_tool_call_count[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "Agent工具调用错误率过高"
description: "错误率超过5%,当前值: {{ $value }}"
– alert: AgentHighTokenUsage
expr: rate(agent_token_usage[5m]) / rate(agent_request_count[5m]) > 8000
for: 10m
labels:
severity: warning
annotations:
summary: "Agent Token消耗过高"
description: "平均每次请求消耗超过8000 tokens,当前值: {{ $value }}"
七、总结
本文是一篇从零自建Agent可观测性的实战文章,核心交付:
| AgentOpenTelemetryTracer | 基于OpenTelemetry的Agent追踪器,三支柱全覆盖 |
| trace_request | 请求级Trace(根Span) |
| trace_llm_call | LLM调用追踪 |
| trace_tool_call | 工具调用追踪 |
| record_memory_retrieval | 记忆检索追踪 |
| record_loop_iteration | 循环迭代追踪 |
| record_security_event | 安全事件追踪 |
| Dashboard面板定义 | 6个核心面板(QPS/延迟/Token/错误率/相关度/迭代次数) |
| 告警规则 | 3个核心告警(延迟/错误率/Token消耗) |
核心结论:
八、适用边界与限制条件
自建可观测性方案并非适合所有场景:
| 单Agent调试 | 完整的Tracing过于重量级 | 只使用 trace_request + trace_llm_call,跳过Metrics和Dashboard |
| 无运维能力团队 | Prometheus+Grafana维护成本高 | 先用LangSmith快速验证,再迁移自建方案 |
| 需要实时告警 | 自建方案告警规则需手动配置 | 使用Prometheus Alertmanager + 飞书/钉钉Webhook |
| 跨Agent追踪 | 需要手动传递Trace Context | 参考 get_trace_context() 方法在A2A协议中传递trace_id |
| 已有APM系统(Datadog等) | 不需要再自建一套 | 将OpenTelemetry Exporter指向已有APM系统 |
⚠️ OpenTelemetry版本兼容性:opentelemetry-api==1.27.0 与 opentelemetry-sdk==1.27.0 版本必须匹配。不同大版本之间API不兼容,升级时注意同时更新所有子包。
相关阅读:
- GB/Z 185合规的Agent Loop设计(标准合规——7.1可审计性的条款解读)
- Loop Engineering四层架构(循环控制——循环迭代次数是可观测性指标之一)
- MCP Server安全加固(安全层——安全事件是可观测性的重要组成部分)
- Agent评估体系自建指南(评估体系——可观测性数据是评估的输入来源)
你的Agent现在有可观测性吗? 是打日志、用LangSmith、还是完全没管?评论区说说你的现状——如果还在"裸奔",打算怎么开始?我针对高频场景整理一份"Agent可观测性5分钟快速启动脚本",复制粘贴就能跑。
收藏这篇Agent可观测性自建指南,上线前把可观测性补上,出了问题才能"有据可查"。觉得有用的话点赞+收藏,让更多开发者看到这篇Agent生产化的基础设施指南。
📅 更新日志:
| 2026-07 | 初始发布(基于Python 3.10+ / OpenTelemetry 1.27.0) |
⚠️ 版本变更提示:本文基于OpenTelemetry 1.27.0。如果SDK大版本升级(如2.0.0),TracerProvider和MeterProvider的初始化方式可能变化。OpenTelemetry的三大子包(api/sdk/exporter)版本必须保持一致。Prometheus+Grafana集成需分别安装。




