Agent可观测性:你无法优化你看不见的东西

传统软件的可观测性已经相当成熟——我们有Prometheus做指标、ELK做日志、Jaeger做追踪。但Agent系统引入了新的可观测性挑战:非确定性的执行路径、不可预测的token消耗、LLM输出的质量评估、多Agent协作的链路追踪。2026年,Agent可观测性已经形成了一套独立的最佳实践。

Agent可观测性的三大支柱

┌───────────────────────────────────────────────────┐
│            Agent 可观测性三大支柱                   │
├─────────────┬─────────────┬───────────────────────┤
│             │             │                       │
│   追踪      │   日志      │       指标            │
│  (Traces)   │  (Logs)     │    (Metrics)         │
│             │             │                       │
│ Agent执行   │ 结构化事件  │ 性能计数器            │
│ 链路追踪    │ 决策记录    │ 资源消耗              │
│ 跨Agent关联 │ 上下文快照  │ 质量评估              │
│             │             │                       │
└─────────────┴─────────────┴───────────────────────┘

1. 分布式追踪

Agent Trace模型

Agent系统的追踪比传统微服务更复杂,因为一个"请求"可能涉及多轮LLM调用、多次工具调用、跨多个Agent。

from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Optional
import uuid

@dataclass
class AgentSpan:
    """Agent追踪的基本单元"""
    span_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    parent_id: Optional[str] = None
    trace_id: str = ""
    
    # 基本信息
    name: str = ""                    # span名称
    span_type: str = ""               # llm_call / tool_call / agent_step / sub_agent
    agent_name: str = ""              # 哪个Agent
    start_time: datetime = field(default_factory=datetime.now)
    end_time: Optional[datetime] = None
    
    # Agent特有信息
    input_data: Any = None
    output_data: Any = None
    model: str = ""                   # 使用的LLM模型
    prompt_tokens: int = 0
    completion_tokens: int = 0
    cost: float = 0.0
    
    # 状态
    status: str = "ok"                # ok / error / timeout
    error_message: Optional[str] = None
    
    # 元数据
    attributes: dict = field(default_factory=dict)
    
    def finish(self, output=None, status="ok", error=None):
        self.end_time = datetime.now()
        self.output_data = output
        self.status = status
        self.error_message = error
    
    @property
    def duration_ms(self) -> float:
        if self.end_time:
            return (self.end_time - self.start_time).total_seconds() * 1000
        return 0

追踪实现

class AgentTracer:
    """Agent追踪器"""
    
    def __init__(self, service_name="agent-service"):
        self.service_name = service_name
        self.spans: list[AgentSpan] = []
        self.current_span_stack: list[AgentSpan] = []
    
    def start_trace(self, name: str, agent_name: str) -> AgentSpan:
        """开始一个新的追踪(根span)"""
        trace_id = str(uuid.uuid4())
        span = AgentSpan(
            trace_id=trace_id,
            name=name,
            span_type="agent_step",
            agent_name=agent_name
        )
        self.spans.append(span)
        self.current_span_stack.append(span)
        return span
    
    def start_span(
        self, 
        name: str, 
        span_type: str,
        agent_name: str = "",
        input_data: Any = None
    ) -> AgentSpan:
        """开始一个子span"""
        parent = self.current_span_stack[-1] if self.current_span_stack else None
        
        span = AgentSpan(
            trace_id=parent.trace_id if parent else str(uuid.uuid4()),
            parent_id=parent.span_id if parent else None,
            name=name,
            span_type=span_type,
            agent_name=agent_name,
            input_data=input_data
        )
        self.spans.append(span)
        self.current_span_stack.append(span)
        return span
    
    def end_span(self, span: AgentSpan, output=None, status="ok", error=None):
        """结束一个span"""
        span.finish(output, status, error)
        if self.current_span_stack and self.current_span_stack[-1].span_id == span.span_id:
            self.current_span_stack.pop()
    
    def get_trace_tree(self, trace_id: str) -> dict:
        """获取追踪树"""
        trace_spans = [s for s in self.spans if s.trace_id == trace_id]
        return self._build_tree(trace_spans, parent_id=None)
    
    def _build_tree(self, spans: list[AgentSpan], parent_id: str | None) -> dict:
        children = [s for s in spans if s.parent_id == parent_id]
        return [
            {
                "span_id": s.span_id,
                "name": s.name,
                "type": s.span_type,
                "agent": s.agent_name,
                "duration_ms": s.duration_ms,
                "tokens": s.prompt_tokens + s.completion_tokens,
                "cost": s.cost,
                "status": s.status,
                "children": self._build_tree(spans, s.span_id)
            }
            for s in children
        ]

使用示例

tracer = AgentTracer()

# 开始追踪
root = tracer.start_trace("用户咨询", "router_agent")

# Agent执行LLM调用
llm_span = tracer.start_span("LLM调用", "llm_call", "router_agent", input_data="用户问题")
response = await llm.complete("...")
tracer.end_span(llm_span, output=response.text, 
                status="ok")
llm_span.prompt_tokens = response.usage.prompt_tokens
llm_span.completion_tokens = response.usage.completion_tokens
llm_span.cost = calculate_cost(response.usage, "gpt-4o")

# Agent调用工具
tool_span = tracer.start_span("搜索知识库", "tool_call", "router_agent")
results = await knowledge_base.search("query")
tracer.end_span(tool_span, output=results)

# 路由到子Agent
sub_agent_span = tracer.start_span("专家Agent处理", "sub_agent", "expert_agent")
# ... 子Agent内部会有自己的span ...
tracer.end_span(sub_agent_span, output="最终回答")

# 结束追踪
tracer.end_span(root, output="最终回答")

# 查看追踪树
trace_tree = tracer.get_trace_tree(root.trace_id)

追踪可视化

追踪树可以渲染为瀑布图:

[router_agent] 用户咨询                          2850ms  1520 tok  ¥0.08
├── [router_agent] LLM调用 (意图识别)             320ms   180 tok  ¥0.01
├── [router_agent] 搜索知识库                      85ms      0 tok  ¥0.00
├── [expert_agent] 专家Agent处理                 2100ms  1240 tok  ¥0.06
│   ├── [expert_agent] LLM调用 (分析)             450ms   320 tok  ¥0.02
│   ├── [expert_agent] 工具调用 (数据查询)         180ms      0 tok  ¥0.00
│   ├── [expert_agent] LLM调用 (生成回答)          680ms   540 tok  ¥0.03
│   └── [expert_agent] LLM调用 (审查)              350ms   380 tok  ¥0.02
└── [router_agent] LLM调用 (格式化输出)            280ms   200 tok  ¥0.01

2. 结构化日志

日志格式

import json
import logging

class AgentLogger:
    """Agent专用结构化日志"""
    
    def __init__(self, agent_name: str):
        self.agent_name = agent_name
        self.logger = logging.getLogger(agent_name)
        
    def log(self, level: str, event: str, **kwargs):
        """结构化日志输出"""
        log_entry = {
            "timestamp": datetime.now().isoformat(),
            "level": level,
            "agent": self.agent_name,
            "event": event,
            **kwargs
        }
        self.logger.log(
            getattr(logging, level.upper()),
            json.dumps(log_entry, ensure_ascii=False, default=str)
        )
    
    def log_llm_call(self, model: str, prompt: str, response: str, 
                     tokens: dict, latency: float):
        self.log("INFO", "llm_call",
            model=model,
            prompt_length=len(prompt),
            response_length=len(response),
            prompt_tokens=tokens.get("prompt", 0),
            completion_tokens=tokens.get("completion", 0),
            latency_ms=latency
        )
    
    def log_tool_call(self, tool_name: str, args: dict, result: Any,
                      latency: float, success: bool):
        self.log("INFO", "tool_call",
            tool=tool_name,
            args=args,
            result_preview=str(result)[:200],
            latency_ms=latency,
            success=success
        )
    
    def log_decision(self, decision: str, reasoning: str, 
                     alternatives: list, confidence: float):
        self.log("INFO", "decision",
            decision=decision,
            reasoning=reasoning,
            alternatives=alternatives,
            confidence=confidence
        )
    
    def log_error(self, error_type: str, message: str, context: dict):
        self.log("ERROR", "error",
            error_type=error_type,
            message=message,
            context=context
        )

3. 指标监控

核心指标定义

from dataclasses import dataclass
from collections import defaultdict
import time

@dataclass
class AgentMetrics:
    """Agent核心指标"""
    
    # 性能指标
    request_count: int = 0
    success_count: int = 0
    error_count: int = 0
    timeout_count: int = 0
    
    # 延迟指标
    latencies: list[float] = None  # ms
    
    # Token指标
    total_prompt_tokens: int = 0
    total_completion_tokens: int = 0
    
    # 成本指标
    total_cost: float = 0.0
    
    # 质量指标
    user_feedback_scores: list[float] = None
    
    # Agent特有指标
    avg_iterations: float = 0.0       # 平均迭代次数
    tool_call_count: int = 0           # 工具调用总次数
    tool_success_rate: float = 0.0     # 工具成功率
    escalation_count: int = 0          # 升级人工次数
    
    def summary(self) -> dict:
        return {
            "total_requests": self.request_count,
            "success_rate": self.success_count / max(1, self.request_count),
            "error_rate": self.error_count / max(1, self.request_count),
            "p50_latency_ms": self._percentile(self.latencies, 50),
            "p95_latency_ms": self._percentile(self.latencies, 95),
            "p99_latency_ms": self._percentile(self.latencies, 99),
            "avg_tokens_per_request": (
                self.total_prompt_tokens + self.total_completion_tokens
            ) / max(1, self.request_count),
            "avg_cost_per_request": self.total_cost / max(1, self.request_count),
            "avg_user_score": sum(self.user_feedback_scores) / max(1, len(self.user_feedback_scores)),
            "tool_success_rate": self.tool_success_rate,
            "escalation_rate": self.escalation_count / max(1, self.request_count)
        }
    
    @staticmethod
    def _percentile(data: list[float], p: int) -> float:
        if not data:
            return 0
        sorted_data = sorted(data)
        idx = int(len(sorted_data) * p / 100)
        return sorted_data[min(idx, len(sorted_data) - 1)]

关键指标仪表盘

┌─────────────────────────────────────────────────────┐
│                 Agent 监控仪表盘                      │
├─────────────────┬─────────────────┬─────────────────┤
│  请求总数       │  成功率          │  P95延迟        │
│   12,847        │   94.2%         │   2.1s          │
│  ↑ 8.3% vs昨   │  ↑ 1.2% vs昨   │  ↓ 15% vs昨   │
├─────────────────┼─────────────────┼─────────────────┤
│  Token/请求     │  成本/请求       │  用户评分       │
│   18,200        │   ¥0.42         │   4.3/5.0      │
│  ↓ 5% vs昨     │  ↓ 7% vs昨     │  ↑ 0.2 vs昨   │
├─────────────────┼─────────────────┼─────────────────┤
│  工具成功率     │  升级人工率      │  错误率         │
│   91.5%         │   2.7%          │   3.1%          │
│  → 持平         │  ↓ 0.3% vs昨   │  → 持平         │
└─────────────────┴─────────────────┴─────────────────┘

异常检测规则

class AnomalyDetector:
    """Agent异常检测"""
    
    RULES = [
        # 延迟异常
        {"metric": "latency_p95", "threshold": 5000, "window": "5m", 
         "action": "alert", "message": "P95延迟超过5秒"},
        
        # 错误率飙升
        {"metric": "error_rate", "threshold": 0.10, "window": "5m",
         "action": "alert", "message": "错误率超过10%"},
        
        # Token消耗异常
        {"metric": "avg_tokens", "threshold": 50000, "window": "1h",
         "action": "alert", "message": "平均token消耗异常偏高"},
        
        # 成本异常
        {"metric": "cost_per_hour", "threshold": 100, "window": "1h",
         "action": "alert", "message": "每小时成本超过¥100"},
        
        # 成功率下降
        {"metric": "success_rate", "threshold": 0.85, "window": "15m",
         "action": "alert", "message": "成功率低于85%"},
        
        # 工具调用失败率
        {"metric": "tool_error_rate", "threshold": 0.15, "window": "10m",
         "action": "warn", "message": "工具调用错误率超过15%"},
    ]
    
    def check(self, metrics: dict) -> list[dict]:
        alerts = []
        for rule in self.RULES:
            value = metrics.get(rule["metric"], 0)
            threshold = rule["threshold"]
            
            if rule["metric"] in ["success_rate", "tool_success_rate"]:
                # 这些指标是越低越异常
                if value < threshold:
                    alerts.append(rule)
            else:
                # 这些指标是越高越异常
                if value > threshold:
                    alerts.append(rule)
        
        return alerts

4. OpenTelemetry集成

2026年,OpenTelemetry已经成为Agent可观测性的事实标准:

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

# 配置OpenTelemetry
provider = TracerProvider()
provider.add_span_processor(
    BatchSpanProcessor(
        OTLPSpanExporter(endpoint="http://otel-collector:4317")
    )
)
trace.set_tracer_provider(provider)

tracer = trace.get_tracer("agent-service")

# 在Agent代码中使用
class TracedAgent:
    def __init__(self, name: str):
        self.name = name
        self.tracer = trace.get_tracer(f"agent.{name}")
    
    async def run(self, task: str) -> str:
        with self.tracer.start_as_current_span(f"agent.{self.name}.run") as span:
            span.set_attribute("agent.name", self.name)
            span.set_attribute("agent.task", task)
            
            # LLM调用
            with self.tracer.start_as_current_span("llm.call"):
                response = await self.llm.complete(task)
                span.set_attribute("llm.tokens", response.usage.total_tokens)
                span.set_attribute("llm.model", self.model)
            
            # 工具调用
            with self.tracer.start_as_current_span("tool.search"):
                results = await self.tool.search(task)
                span.set_attribute("tool.results_count", len(results))
            
            return response

5. 质量评估

除了技术指标,Agent系统的输出质量也需要监控:

class QualityMonitor:
    """Agent输出质量监控"""
    
    async def evaluate_response(self, query: str, response: str) -> dict:
        """使用LLM评估响应质量"""
        eval_prompt = f"""评估以下AI响应的质量,给出1-5分:
        
        用户问题: {query}
        AI回答: {response}
        
        评分维度:
        1. 准确性: 回答是否正确?
        2. 完整性: 是否完整回答了问题?
        3. 相关性: 回答是否切题?
        4. 清晰度: 表达是否清晰?
        5. 安全性: 是否包含不当内容?
        """
        result = await self.eval_llm.complete(eval_prompt)
        return self._parse_scores(result.text)
    
    async def detect_hallucination(self, response: str, sources: list[str]) -> float:
        """检测幻觉概率"""
        # 检查response中的事实声明是否有sources支撑
        prompt = f"""判断以下回答中的事实是否有来源支撑:
        回答: {response}
        来源: {sources}
        
        返回幻觉概率(0-1),1表示完全虚构。
        """
        result = await self.eval_llm.complete(prompt)
        return float(result.text.strip())

结论

Agent可观测性不是锦上添花,而是生产部署的必要条件。一个不可观测的Agent系统就像一个黑箱——出了问题不知道为什么,性能下降不知道哪里瓶颈,成本飙升不知道哪个环节。

核心建议:

  1. 追踪先行:先实现完整追踪,再考虑日志和指标
  2. 结构化日志:所有日志都是JSON格式,可机器解析
  3. 关键指标看板:成功率、延迟、token消耗、成本是四大核心指标
  4. 异常告警:设置合理的告警阈值,避免告警疲劳
  5. 质量监控:技术指标之外,也要监控输出质量

可观测性的投入回报率极高——在100次故障中,有可观测性系统的平均故障恢复时间(MTTR)是没有的1/5。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。