为什么智能体需要专属的可观测性?
传统软件的可观测性聚焦于 CPU、内存、延迟等系统指标。但 AI 智能体引入了全新的可观测维度:
- Token 消耗:每次 LLM 调用都有成本,需要精确追踪
- 推理链路:Agent 可能经历多轮 Thought → Action → Observation 循环,需要完整 Trace
- 工具调用质量:工具是否被正确调用?返回结果是否有效?
- Prompt 效果:不同 Prompt 版本对输出质量的影响如何?
- 幻觉检测:模型输出是否与事实相符?
缺乏可观测性的智能体就像一个黑盒——你不知道它在想什么,也不知道它为什么出错。本文将带你从零搭建一套生产级的智能体可观测性平台。
可观测性三支柱在 AI 场景下的重构
传统可观测性的三支柱是 Metrics、Logs、Traces。在智能体场景下,我们需要将其扩展为五支柱:
| 支柱 | 传统场景 | 智能体场景 |
|---|---|---|
| Traces | 请求链路追踪 | Thought-Action-Observation 链路 |
| Metrics | QPS、延迟、错误率 | Token 用量、工具调用成功率、幻觉率 |
| Logs | 结构化日志 | Prompt/Completion 完整记录 |
| Evaluations | N/A | 输出质量自动评估 |
| Cost | N/A | Token 成本与预算控制 |
架构设计
┌────────────────────────────────────────────────────────┐
│ Agent Application │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │ LLM Call │ │ Tool Call │ │ Observation Callback │ │
│ └─────┬─────┘ └─────┬────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Observability SDK │ │
│ │ (OpenTelemetry + Custom Spans + Token Counter) │ │
│ └──────────────────────┬──────────────────────────┘ │
└─────────────────────────┼──────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌───────────┐
│ Jaeger │ │ Prometheus │ │ Postgres │
│ (Traces) │ │ (Metrics) │ │ (Logs) │
└─────────────┘ └─────────────┘ └───────────┘
│ │ │
└───────────────┼───────────────┘
▼
┌─────────────┐
│ Grafana │
│ (Dashboard) │
└─────────────┘
核心组件实现
1. Trace 追踪:基于 OpenTelemetry 扩展
智能体的 Trace 与传统微服务不同,需要记录 LLM 特有的 Span 属性:
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
from dataclasses import dataclass
import json
import time
# 初始化 Tracer
provider = TracerProvider()
exporter = OTLPSpanExporter(endpoint="localhost:4317")
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
@dataclass
class LLMSpanAttributes:
"""LLM 专有的 Span 属性"""
model: str
prompt_tokens: int
completion_tokens: int
total_tokens: int
temperature: float
system_prompt_hash: str
tool_calls: list
latency_ms: float
class AgentTracer:
"""智能体 Trace 追踪器"""
def __init__(self, agent_name: str):
self.agent_name = agent_name
self.tracer = trace.get_tracer(agent_name)
def trace_llm_call(self, model: str, messages: list,
response: dict, latency_ms: float):
"""记录一次 LLM 调用"""
with self.tracer.start_as_current_span(
f"llm.{model}",
attributes={
"llm.model": model,
"llm.messages_count": len(messages),
"llm.prompt_tokens": response.get("usage", {}).get("prompt_tokens", 0),
"llm.completion_tokens": response.get("usage", {}).get("completion_tokens", 0),
"llm.total_tokens": response.get("usage", {}).get("total_tokens", 0),
"llm.latency_ms": latency_ms,
"llm.finish_reason": response.get("choices", [{}])[0].get("finish_reason", ""),
}
) as span:
# 记录完整的 Prompt 和 Completion(注意脱敏)
span.set_attribute("llm.prompt", json.dumps(
self._redact_sensitive(messages), ensure_ascii=False
))
span.set_attribute("llm.completion", json.dumps(
response.get("choices", [{}])[0].get("message", {}),
ensure_ascii=False
))
def trace_tool_call(self, tool_name: str, arguments: dict,
result: str, latency_ms: float, success: bool):
"""记录一次工具调用"""
with self.tracer.start_as_current_span(
f"tool.{tool_name}",
attributes={
"tool.name": tool_name,
"tool.arguments": json.dumps(arguments, ensure_ascii=False),
"tool.result_length": len(result) if result else 0,
"tool.latency_ms": latency_ms,
"tool.success": success,
}
) as span:
if not success:
span.set_attribute("tool.error", result)
span.set_status(trace.Status(trace.StatusCode.ERROR))
def trace_agent_step(self, step_type: str, step_content: str):
"""记录智能体推理步骤"""
with self.tracer.start_as_current_span(
f"agent.{step_type}",
attributes={
"agent.step_type": step_type, # thought / action / observation
"agent.step_content": step_content,
}
):
pass
def _redact_sensitive(self, messages: list) -> list:
"""脱敏处理"""
redacted = []
for msg in messages:
content = msg.get("content", "")
if isinstance(content, str):
# 脱敏手机号、邮箱等
import re
content = re.sub(r'\b1[3-9]\d{9}\b', '[PHONE]', content)
content = re.sub(r'\b[\w.+-]+@[\w-]+\.[\w.-]+\b', '[EMAIL]', content)
redacted.append({**msg, "content": content})
return redacted
2. Token 监控与成本追踪
from prometheus_client import Counter, Histogram, Gauge, start_http_server
from dataclasses import dataclass, field
from collections import defaultdict
import time
# Prometheus 指标定义
TOKEN_USAGE = Counter(
"agent_token_total",
"Token 使用总量",
["agent", "model", "token_type"] # token_type: prompt/completion
)
TOOL_CALL_TOTAL = Counter(
"agent_tool_calls_total",
"工具调用次数",
["agent", "tool", "status"] # status: success/failure
)
AGENT_LATENCY = Histogram(
"agent_request_duration_seconds",
"智能体请求延迟",
["agent", "phase"], # phase: llm/tool/total
buckets=[0.5, 1, 2, 5, 10, 30, 60, 120, 300]
)
ACTIVE_AGENTS = Gauge(
"agent_active_instances",
"活跃智能体实例数",
["agent"]
)
# Token 定价表(美元/百万 Token)
PRICING = {
"gpt-4o": {"prompt": 2.50, "completion": 10.00},
"gpt-4o-mini": {"prompt": 0.15, "completion": 0.60},
"claude-sonnet-4": {"prompt": 3.00, "completion": 15.00},
"claude-haiku-3.5": {"prompt": 0.80, "completion": 4.00},
}
@dataclass
class CostTracker:
"""成本追踪器"""
agent_name: str
total_cost: float = 0.0
costs_by_model: dict = field(default_factory=lambda: defaultdict(float))
daily_costs: dict = field(default_factory=lambda: defaultdict(float))
def record(self, model: str, prompt_tokens: int, completion_tokens: int):
"""记录一次调用的成本"""
if model not in PRICING:
return
cost = (
prompt_tokens / 1_000_000 * PRICING[model]["prompt"] +
completion_tokens / 1_000_000 * PRICING[model]["completion"]
)
self.total_cost += cost
self.costs_by_model[model] += cost
today = time.strftime("%Y-%m-%d")
self.daily_costs[today] += cost
# 更新 Prometheus 指标
TOKEN_USAGE.labels(
agent=self.agent_name, model=model, token_type="prompt"
).inc(prompt_tokens)
TOKEN_USAGE.labels(
agent=self.agent_name, model=model, token_type="completion"
).inc(completion_tokens)
return cost
def get_daily_report(self) -> dict:
"""获取每日成本报告"""
today = time.strftime("%Y-%m-%d")
return {
"date": today,
"daily_cost": self.daily_costs[today],
"total_cost": self.total_cost,
"by_model": dict(self.costs_by_model),
}
def check_budget(self, daily_budget: float = 50.0) -> bool:
"""检查是否超出每日预算"""
today = time.strftime("%Y-%m-%d")
return self.daily_costs[today] < daily_budget
3. 输出质量自动评估
from pydantic import BaseModel
from typing import Optional
import openai
class EvaluationResult(BaseModel):
"""评估结果"""
score: float # 0-1
dimension: str # 评估维度
reasoning: str # 评估理由
suggestions: str # 改进建议
class AgentEvaluator:
"""智能体输出质量评估器"""
def __init__(self, eval_model: str = "gpt-4o-mini"):
self.eval_model = eval_model
async def evaluate(
self,
query: str,
response: str,
expected: Optional[str] = None,
tools_used: list = None
) -> list[EvaluationResult]:
"""多维度评估智能体输出"""
results = []
# 维度1:任务完成度
results.append(await self._eval_completion(query, response, expected))
# 维度2:事实准确性
results.append(await self._eval_accuracy(query, response))
# 维度3:工具使用效率
if tools_used:
results.append(await self._eval_tool_efficiency(query, tools_used, response))
# 维度4:安全性
results.append(await self._eval_safety(response))
return results
async def _eval_completion(self, query, response, expected):
prompt = f"""
评估以下智能体回答的任务完成度:
用户问题:{query}
智能体回答:{response}
{'期望回答:' + expected if expected else ''}
评分标准(0-1):
- 1.0:完全满足用户需求
- 0.7:基本满足,有小瑕疵
- 0.4:部分满足
- 0.0:完全未回答
返回 JSON:{{"score": float, "reasoning": str, "suggestions": str}}
"""
result = await self._call_eval(prompt)
return EvaluationResult(dimension="task_completion", **result)
async def _eval_accuracy(self, query, response):
prompt = f"""
评估以下回答的事实准确性:
问题:{query}
回答:{response}
检查是否有事实错误、误导性陈述或幻觉。
返回 JSON:{{"score": float, "reasoning": str, "suggestions": str}}
"""
result = await self._call_eval(prompt)
return EvaluationResult(dimension="factual_accuracy", **result)
async def _eval_tool_efficiency(self, query, tools_used, response):
prompt = f"""
评估工具使用效率:
任务:{query}
使用的工具调用序列:{tools_used}
最终回答:{response}
评估是否有冗余调用、是否缺少必要调用。
返回 JSON:{{"score": float, "reasoning": str, "suggestions": str}}
"""
result = await self._call_eval(prompt)
return EvaluationResult(dimension="tool_efficiency", **result)
async def _eval_safety(self, response):
prompt = f"""
评估以下回答的安全性:
回答:{response}
检查是否包含有害内容、隐私泄露、不当建议。
返回 JSON:{{"score": float, "reasoning": str, "suggestions": str}}
"""
result = await self._call_eval(prompt)
return EvaluationResult(dimension="safety", **result)
async def _call_eval(self, prompt: str) -> dict:
client = openai.AsyncOpenAI()
resp = await client.chat.completions.create(
model=self.eval_model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0
)
import json
return json.loads(resp.choices[0].message.content)
4. 告警体系
from dataclasses import dataclass
from enum import Enum
import smtplib
import requests
class AlertLevel(Enum):
INFO = "info"
WARNING = "warning"
CRITICAL = "critical"
@dataclass
class AlertRule:
"""告警规则"""
name: str
condition: callable
level: AlertLevel
message: str
cooldown_seconds: int = 300
class AlertManager:
"""告警管理器"""
def __init__(self):
self.rules: list[AlertRule] = []
self.last_triggered: dict[str, float] = {}
def add_rule(self, rule: AlertRule):
self.rules.append(rule)
def check(self, metrics: dict) -> list[dict]:
"""检查所有告警规则"""
alerts = []
for rule in self.rules:
if rule.condition(metrics):
# 冷却期检查
last = self.last_triggered.get(rule.name, 0)
if time.time() - last < rule.cooldown_seconds:
continue
self.last_triggered[rule.name] = time.time()
alert = {
"rule": rule.name,
"level": rule.level.value,
"message": rule.message,
"timestamp": time.time(),
"metrics": metrics
}
alerts.append(alert)
self._send_notification(alert)
return alerts
def _send_notification(self, alert: dict):
"""发送告警通知(Webhook + 邮件)"""
# Webhook 通知
try:
requests.post(
"https://hooks.slack.com/services/xxx",
json={
"text": f"[{alert['level'].upper()}] {alert['message']}",
"attachments": [{"text": json.dumps(alert["metrics"], indent=2)}]
},
timeout=5
)
except:
pass
# 预定义告警规则
alert_manager = AlertManager()
alert_manager.add_rule(AlertRule(
name="token_cost_spike",
condition=lambda m: m.get("hourly_cost", 0) > 20,
level=AlertLevel.WARNING,
message="Token 成本激增:每小时消耗超过 $20"
))
alert_manager.add_rule(AlertRule(
name="tool_failure_rate_high",
condition=lambda m: m.get("tool_failure_rate", 0) > 0.3,
level=AlertLevel.CRITICAL,
message="工具调用失败率超过 30%"
))
alert_manager.add_rule(AlertRule(
name="latency_high",
condition=lambda m: m.get("p99_latency_ms", 0) > 30000,
level=AlertLevel.WARNING,
message="P99 延迟超过 30 秒"
))
alert_manager.add_rule(AlertRule(
name="quality_drop",
condition=lambda m: m.get("avg_quality_score", 1.0) < 0.6,
level=AlertLevel.CRITICAL,
message="输出质量评分降至 0.6 以下"
))
与 LangSmith 集成
如果你使用 LangChain 生态,可以直接集成 LangSmith 获得开箱即用的可观测性:
import os
from langsmith import Client
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
# 配置 LangSmith
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "ls__xxx"
os.environ["LANGCHAIN_PROJECT"] = "production-agent"
# 所有 LLM 调用和工具调用会自动被追踪
llm = ChatOpenAI(model="gpt-4o", temperature=0)
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# 执行并自动记录到 LangSmith
result = executor.invoke({"input": "分析最近一周的销售数据"})
# 在 LangSmith Dashboard 中可以看到:
# - 完整的 Thought-Action-Observation 链路
# - 每一步的 Token 消耗
# - 工具调用的输入输出
# - 总延迟和成本
Grafana Dashboard 配置
将 Prometheus 指标可视化,核心 Panel 包括:
- Token 消耗趋势:按模型、按 Agent 分组的折线图
- 工具调用成功率:按工具分类的统计面板
- 请求延迟分布:P50/P95/P99 延迟曲线
- 成本热力图:按小时×日期的成本热力图
- 质量评分趋势:各维度评分随时间的变化
- 告警面板:当前活跃告警列表
总结
智能体的可观测性不是一个可选项,而是生产化部署的必选项。一套好的可观测性平台能帮你回答三个核心问题:
- Agent 在做什么? —— Trace 追踪告诉你执行链路
- Agent 做得好不好? —— 质量评估告诉你输出水平
- Agent 花了多少钱? —— 成本追踪告诉你经济可行性
从 OpenTelemetry 扩展的 Span 体系,到基于 LLM-as-Judge 的自动评估,再到 Prometheus + Grafana 的监控大盘,这套技术栈已经足够支撑中小规模的智能体生产环境。随着你的 Agent 生态壮大,可以逐步引入更专业的工具如 LangSmith、Arize Phoenix、Weave 等,但核心思路不变:让黑盒变白盒,让不可见变可见。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
