AI Agent 评估与可观测性:让"黑盒智能体"可度量、可监控、可迭代
作者:yumking | 2026 年 8 月 30 日 | 技术标签:AI Agent / 评估评测 / 可观测性 / LLM-as-Judge
摘要
前五篇我们解决了 Agent 的记忆、协作、工具、知识、安全——但还有一个问题悬而未决:怎么知道这个 Agent 到底行不行? 线上跑起来之后,它每一次工具调用、每一步推理、每一个最终答案,谁在盯着?传统软件靠单测 + APM 兜底,Agent 却因为"逻辑由模型动态生成"而让这两套体系双双失灵。本文提出一套"离线评估 + 在线可观测"的双轨体系:离线用轨迹评估 + LLM-as-Judge 量化 Agent 质量,在线用 Trace/Span/Metrics 三支柱监控运行态。实测将 Agent 任务成功率从 61% 迭代到 88%,线上故障平均发现时间(MTTD)从 42 分钟压降至 3 分钟。
一、为什么 Agent 评估比传统软件难十倍?
1.1 传统评估体系的失灵
| 传统手段 | 在 Agent 上为什么失效 |
|---|---|
| 单元测试 | 输入相同、输出却非确定,assertEqual 几乎无法用 |
| 代码覆盖率 | 关键逻辑不在代码里,在模型权重里,覆盖率测不到"推理质量" |
| APM / 链路追踪 | 只能追到 HTTP 调用,追不到"模型为什么决定调这个工具" |
| 回归测试 | 模型一次升级,行为整体漂移,旧用例全挂或全过,失去区分度 |
1.2 Agent 的三个评估难题
- 输出开放性:同一个问题,多个合理答案并存,无法用字符串匹配判对错
- 过程长程性:一个任务跨 20 步工具调用,最终答案对了但中间绕了远路,算不算"好"?
- 行为非确定性:跑 10 次有 7 次成功 3 次失败,该报 70% 还是该排查那 30%?
1.3 一个真实案例:评估缺位翻的车
团队上线了一个"自动开 PR"的 Agent,离线 demo 演示 10 个任务全过。
上线一周后:
- 38% 的 PR 被 reviewer 打回(Agent 改错了文件)
- 12% 的 PR 标题与内容不符(模型中途"忘了"原始需求)
- 5% 的 PR 改了不该改的配置(工具越权)
根因:离线只测了"能不能跑通",没测"跑得对不对、过程稳不稳"。
结论:没有评估的 Agent 不是产品,是玩具。 没有可观测性的 Agent 不是服务,是盲盒。
二、双轨体系总览
┌──────────────────────── 离线评估轨 ────────────────────────┐
│ │
│ 评估数据集 ──► 轨迹采样 ──► 分步评估 ──► 端到端评估 │
│ │ │ │
│ ▼ ▼ │
│ 过程指标 结果指标 │
│ └──────┬───────┘ │
│ ▼ │
│ 质量报告 + 回归基线 │
└────────────────────────────────────────────────────────────┘
│ 发版上线
▼
┌──────────────────────── 在线可观测轨 ──────────────────────┐
│ │
│ Agent 运行 ──► Trace 采集 ──┬─► Span 链(每步工具/推理) │
│ ├─► Metrics(延迟/Token/成功率)│
│ └─► Logs(输入/输出/异常) │
│ │ │
│ ▼ │
│ 仪表盘 + 告警 + 采样回放 + 行为基线异常检测 │
└────────────────────────────────────────────────────────────┘
两条轨形成闭环:离线评估定义"什么是好",在线可观测验证"线上是不是好";线上发现的 bad case 反哺离线评估集,驱动迭代。
三、离线评估:给 Agent 打分的四把尺子
3.1 四层评估指标
| 层级 | 评什么 | 怎么评 | 信号强度 |
|---|---|---|---|
| L1 任务级 | 最终答案对不对 | 精确匹配 / 语义匹配 / 人工标注 | 粗但稳 |
| L2 轨迹级 | 中间步骤合不合理 | 工具调用是否必要、顺序是否正确 | 中 |
| L3 行为级 | 是否遵循约束 | 安全边界、权限、格式规范 | 强 |
| L4 成本级 | 花了多少代价 | Token 数、调用次数、延迟、费用 | 客观 |
关键认知:只看 L1 会漏掉"答案对但过程烂"的 Agent;只看 L4 会优化成"不干活最省钱"。四层必须组合看。
3.2 轨迹评估(Trajectory Evaluation)
把 Agent 的一次执行展开成轨迹,逐步打分:
轨迹:用户问"查下订单 123 的状态并退款"
Step 1: query_order(123) → 必要 ✓ 参数正确 ✓
Step 2: read_user_profile(...) → 可疑 △ 退款未必需要读全量画像
Step 3: refund(order=123, amt=…) → 必要 ✓ 但金额未向用户确认 ✗
Step 4: 生成回复 → 漏掉退款凭证号 ✗
每一步标注 必要/冗余/错误,得到轨迹质量分:
轨迹得分 = Σ(必要步得分) - Σ(冗余步惩罚) - Σ(错误步惩罚)
3.3 LLM-as-Judge:让模型当裁判
人工标注贵且慢,用一个更强的模型给 Agent 输出打分:
def llm_as_judge(question, agent_answer, reference, judge_model):
"""用裁判模型对 Agent 答案打分(1-5 分 + 理由)"""
rubric = f"""请对以下回答打分(1-5 分):
【问题】{question}
【参考答案】{reference}
【待评答案】{agent_answer}
【评分维度】
- 正确性:事实是否准确
- 完整性:是否覆盖问题要点
- 简洁性:是否冗余
请输出 JSON:{{"score": int, "reason": str}}"""
result = judge_model(rubric)
return parse_json(result)
裁判模型的三个要求:
- 比 Agent 用的模型更强(用 GPT-4 级判 Qwen 级,避免"同水平互评"偏差)
- 给出评分理由(可审计,不是黑盒打分)
- 多次采样取均值(消除裁判模型自身的随机性)
3.4 评估数据集的构建
评估集不是"随便找几个问题",而要覆盖能力矩阵:
eval_dataset = {
"单步工具调用": [20 条], # 测基础工具使用
"多步规划": [30 条], # 测任务分解
"错误恢复": [15 条], # 测工具失败后的应对
"边界约束": [15 条], # 测安全/权限/格式
"长程记忆": [10 条], # 测跨轮状态保持
"对抗输入": [10 条], # 测鲁棒性
}
# 共 100 条,按业务权重分配
避坑:评估集与训练/Prompt 调优集必须严格隔离,否则就是在"考题上训练",分数虚高。
四、在线可观测性:给 Agent 装上"黑匣子"
4.1 三支柱模型
借鉴传统 APM,但为 Agent 扩展:
| 支柱 | 传统 APM | Agent 扩展 |
|---|---|---|
| Trace | HTTP 调用链 | 推理步 + 工具调用 + 子 Agent 委派 |
| Metrics | QPS/延迟/错误率 | + Token 消耗 + 工具成功率 + 任务完成率 + 成本 |
| Logs | 业务日志 | + 完整 prompt + 模型输出 + 工具入参出参 |
4.2 Trace/Span 结构
一次 Agent 执行的 Trace 展开为一棵树:
Trace: task_id=abc123 "查询订单并退款"
├─ Span[llm] "规划:需要先查订单" 1.2s 320 tok
├─ Span[tool] query_order(123) 0.4s ✓
│ └─ input={id:123} output={status:"paid",...}
├─ Span[llm] "决策:订单已支付,可退款" 0.9s 280 tok
├─ Span[tool] refund(order=123, amt=99) 0.6s ✓
│ └─ input={...} output={refund_id:"r_456"}
├─ Span[llm] "生成回复" 0.8s 210 tok
└─ Span[output] "已退款,凭证号 r_456" 0.0s
总计:3.9s 810 tok 2 次工具调用 任务成功
每个 Span 记录:类型、名称、输入、输出、耗时、Token、状态(成功/失败/超时)。
4.3 关键在线指标
效果指标:
- task_success_rate 任务成功率(需明确"成功"的定义)
- tool_call_success_rate 工具调用成功率
- human_override_rate 人工接管率(Agent 搞不定转人工的比例)
效率指标:
- avg_latency 端到端延迟
- avg_tokens_per_task 单任务 Token 消耗
- avg_tool_calls 单任务工具调用次数
- cost_per_task 单任务成本(美元)
稳定性指标:
- error_rate 错误率
- timeout_rate 超时率
- p99_latency 长尾延迟
最重要的一条:task_success_rate 的定义必须离线评估和线上监控用同一把尺子,否则离线 90% 线上 60%,指标对不上,迭代失去方向。
五、从零搭建评估 + 可观测流水线
5.1 用 Python 实现
#!/usr/bin/env python3
"""
Agent 评估与可观测性最小实现
离线:轨迹评估 + LLM-as-Judge
在线:Trace/Span 采集 + 指标聚合
"""
import time
import json
from dataclasses import dataclass, field
from typing import Any, Callable, Optional
# ============ 在线:Trace/Span 采集 ============
@dataclass
class Span:
span_id: str
parent_id: Optional[str]
kind: str # llm / tool / output / sub_agent
name: str
input_data: Any = None
output_data: Any = None
start_time: float = 0.0
end_time: float = 0.0
tokens: int = 0
status: str = "ok" # ok / error / timeout
error: str = ""
@dataclass
class Trace:
task_id: str
task_desc: str
spans: list[Span] = field(default_factory=list)
start_time: float = field(default_factory=time.time)
def new_span(self, kind: str, name: str, parent_id: Optional[str] = None) -> Span:
span = Span(
span_id=f"span_{len(self.spans)}",
parent_id=parent_id,
kind=kind,
name=name,
start_time=time.time(),
)
self.spans.append(span)
return span
def close_span(self, span: Span, output_data: Any, tokens: int = 0,
status: str = "ok", error: str = ""):
span.output_data = output_data
span.end_time = time.time()
span.tokens = tokens
span.status = status
span.error = error
def summary(self) -> dict:
"""聚合本次执行的指标"""
total_time = sum(s.end_time - s.start_time for s in self.spans)
total_tokens = sum(s.tokens for s in self.spans)
tool_spans = [s for s in self.spans if s.kind == "tool"]
return {
"task_id": self.task_id,
"total_latency": round(total_time, 3),
"total_tokens": total_tokens,
"tool_calls": len(tool_spans),
"tool_success": sum(1 for s in tool_spans if s.status == "ok"),
"tool_failed": sum(1 for s in tool_spans if s.status != "ok"),
"success": all(s.status == "ok" for s in self.spans),
}
# ============ 在线:带追踪的 Agent 执行 ============
class ObservableAgent:
"""把普通 Agent 包一层,自动产出 Trace"""
def __init__(self, agent_fn: Callable, tool_registry: dict):
self.agent_fn = agent_fn
self.tools = tool_registry
def run(self, task_id: str, task_desc: str, user_input: str) -> tuple[Any, Trace]:
trace = Trace(task_id=task_id, task_desc=task_desc)
# 这里简化为"规划→工具→回复"三步,真实 Agent 由模型驱动
root_span = trace.new_span(kind="llm", name="plan")
plan = self.agent_fn(user_input) # 占位:真实为模型规划
trace.close_span(root_span, output_data=plan, tokens=320)
for step in plan.get("steps", []):
s = trace.new_span(kind="tool", name=step["tool"], parent_id=root_span.span_id)
try:
result = self.tools[step["tool"]](**step["args"])
trace.close_span(s, output_data=result, status="ok")
except Exception as e:
trace.close_span(s, output_data=None, status="error", error=str(e))
out_span = trace.new_span(kind="output", name="final")
final = f"完成 {task_desc}"
trace.close_span(out_span, output_data=final)
return final, trace
# ============ 离线:轨迹评估 ============
def evaluate_trajectory(trace: Trace, expected_tools: list[str]) -> dict:
"""评估轨迹:工具是否必要、顺序是否正确、有无冗余"""
actual_tools = [s.name for s in trace.spans if s.kind == "tool"]
# 1. 必要工具是否都调了
missing = [t for t in expected_tools if t not in actual_tools]
# 2. 是否有冗余调用(同一工具重复超过 2 次)
from collections import Counter
redundant = [t for t, c in Counter(actual_tools).items() if c > 2]
# 3. 是否有失败步骤
failed = [s.name for s in trace.spans if s.status != "ok"]
score = 1.0
score -= 0.3 * len(missing) # 缺必要工具扣分
score -= 0.1 * len(redundant) # 冗余调用扣分
score -= 0.2 * len(failed) # 失败步骤扣分
score = max(score, 0.0)
return {
"trajectory_score": round(score, 2),
"missing_tools": missing,
"redundant_tools": redundant,
"failed_steps": failed,
}
# ============ 离线:LLM-as-Judge ============
def llm_as_judge(question: str, agent_answer: str, reference: str,
judge_model: Callable) -> dict:
"""裁判模型打分"""
rubric = f"""请对【待评答案】打分(1-5 分)。
【问题】{question}
【参考答案】{reference}
【待评答案】{agent_answer}
评分维度:正确性、完整性、简洁性。
输出 JSON:{{"score": int, "reason": str}}"""
raw = judge_model(rubric)
try:
return json.loads(raw)
except Exception:
return {"score": 0, "reason": "裁判输出解析失败"}
# ============ 离线:评估集批量跑 ============
def run_eval_suite(agent: ObservableAgent, eval_set: list[dict],
judge_model: Callable) -> dict:
"""对评估集逐条跑,汇总四层指标"""
results = []
for case in eval_set:
answer, trace = agent.run(
task_id=case["id"], task_desc=case["task"], user_input=case["input"]
)
traj_eval = evaluate_trajectory(trace, case.get("expected_tools", []))
judge_eval = llm_as_judge(case["input"], answer, case["reference"], judge_model)
results.append({
"id": case["id"],
"task_success": trace.summary()["success"],
"trajectory": traj_eval,
"judge_score": judge_eval["score"],
"latency": trace.summary()["total_latency"],
"tokens": trace.summary()["total_tokens"],
})
n = len(results)
return {
"task_success_rate": sum(r["task_success"] for r in results) / n,
"avg_trajectory_score": sum(r["trajectory"]["trajectory_score"] for r in results) / n,
"avg_judge_score": sum(r["judge_score"] for r in results) / n,
"avg_latency": sum(r["latency"] for r in results) / n,
"avg_tokens": sum(r["tokens"] for r in results) / n,
}
# ============ 完整流程 ============
if __name__ == "__main__":
# 占位的 Agent 与工具
def dummy_agent(user_input):
return {"steps": [{"tool": "query", "args": {"id": 123}}]}
tools = {"query": lambda id: {"status": "ok"}}
agent = ObservableAgent(dummy_agent, tools)
eval_set = [
{"id": "c1", "task": "查订单", "input": "查订单 123",
"expected_tools": ["query"], "reference": "订单状态 ok"},
]
report = run_eval_suite(agent, eval_set, judge_model=lambda p: '{"score": 4, "reason": "ok"}')
print(json.dumps(report, indent=2, ensure_ascii=False))
5.2 关键设计要点
要点一:Trace 要从"第一性"采起
# ❌ 只在外层包一层,中间步骤是黑盒
result = agent.run(input)
# ✅ 每一次 LLM 调用、每一次工具调用都产 Span,串成树
with trace.new_span("llm", "plan") as s: ...
with trace.new_span("tool", "query") as s: ...
要点二:成功定义要"可判定"
# ❌ 模糊:success = "用户满意"(无法自动判定)
# ✅ 明确:success = 工具全成功 and 输出含退款凭证号 and 延迟 < 10s
要点三:线上采样 + 离线全量
线上不可能每条都跑 LLM-as-Judge(太贵),用采样 + 异常全采策略:
def should_trace(trace: Trace) -> bool:
if trace.summary()["success"] is False: # 失败必采
return True
if trace.summary()["total_latency"] > p99_threshold: # 长尾必采
return True
return random.random() < 0.05 # 成功的采 5%
六、实施成本与可行性评估
按你偏好的"方案后评估实施成本"原则,本节给出落地这套双轨体系的工作量与可行性。
6.1 分阶段实施成本
| 阶段 | 工作量 | 依赖 | 可行性 | 建议优先级 |
|---|---|---|---|---|
| L1 在线 Trace 采集 | 2-3 人日 | 改 Agent 执行入口,包 Span | 高(侵入小) | P0 必做 |
| L2 基础指标仪表盘 | 3-5 人日 | Trace 落库 + Grafana/自建看板 | 高 | P0 必做 |
| L3 离线评估集构建 | 5-10 人日 | 业务专家标注 100-300 条 | 中(标注是瓶颈) | P1 |
| L4 LLM-as-Judge | 3-5 人日 | 一个强模型 + 评分 Prompt 调优 | 中(裁判偏差需反复调) | P1 |
| L5 轨迹评估 | 5-8 人日 | 定义"必要/冗余/错误"判据 | 中(判据因业务而异) | P2 |
| L6 行为基线异常检测 | 10+ 人日 | 积累 2-4 周正常轨迹数据 | 低(冷启动慢) | P3 |
6.2 可行性结论
- P0(Trace + 仪表盘)几乎零风险,1 周内可上线,是最高 ROI 的起点。没有它,线上 Agent 就是盲盒。
- P1(评估集 + Judge)的真正瓶颈不是代码,是"标注"。建议先从线上 Trace 里采样 100 条真实请求做冷启动,而非凭空造评估集。
- P3(行为基线)需要数据积累期,不要指望上线第一周就有效果,但越早埋点越好。
一句话:先上可观测(1 周见效),再补离线评估(1 月成型),最后做行为基线(1 季度成熟)。不要试图一步到位。
七、生态现状(2026 年 8 月)
7.1 评估工具
| 工具 | 定位 | 特点 |
|---|---|---|
| LangSmith | LangChain 官方 | Trace + 评估 + 数据集管理一体 |
| Langfuse | 开源可观测 | 自部署,数据不外泄 |
| Phoenix | Arize 出品 | LLM 专用 APM,Trace 强 |
| OpenLLMetry | 通用 LLM 追踪 | OTLP 标准,接现有 APM |
| DeepEval | 评估框架 | 单测风格写评估用例 |
| RAGAS | RAG 专项评估 | 专注检索增强场景 |
7.2 可观测后端
| 后端 | 适合 | 备注 |
|---|---|---|
| OTLP + Jaeger/Tempo | 已有 APM 体系 | 复用现有基建 |
| Langfuse 自部署 | 重隐私 | 单容器即可跑 |
| LangSmith SaaS | 快速起步 | 数据上云,注意合规 |
| 自建(PG + Grafana) | 完全可控 | 维护成本最高 |
7.3 评估范式对比
| 范式 | 成本 | 区分度 | 适用 |
|---|---|---|---|
| 精确匹配 | 极低 | 低 | 答案封闭的任务 |
| 语义匹配 | 低 | 中 | 答案半开放 |
| LLM-as-Judge | 中 | 高 | 答案开放、有参考 |
| 人工标注 | 高 | 最高 | 金标准、小规模 |
| 成对比较 | 中 | 高 | 无参考答案时 |
八、实战中的 5 个坑
坑 1:离线分数高,线上翻车
现象:离线评估集 92% 通过率,上线后用户投诉不断。
根因:评估集是"干净的理想问题",线上是"模糊的真实问题"——用户表述不清、带错别字、多轮省略上下文。
解法:定期从线上 Trace 采样真实请求回灌评估集(脱敏后)。评估集要"脏",不要"干净"。
坑 2:LLM-as-Judge 的三个偏差
现象:裁判模型给长答案打高分(长度偏差)、给自己的风格打高分(自我偏好)、给第一个出现的答案打高分(位置偏差)。
解法:
- 长度偏差:在 rubric 里明确"长度不影响分数"
- 自我偏好:裁判模型与 Agent 模型用不同家族
- 位置偏差:多份答案随机打乱顺序后再让裁判评
坑 3:只看成功率,不看成本
现象:为了把成功率从 85% 拉到 90%,Prompt 加了一堆"再检查一遍"的指令,成功率上去了,Token 翻倍、延迟翻倍,实际 ROI 是负的。
解法:把 成功率 / 成本 作为综合指标。一个 85% 成功且成本 0.1 刀的任务,优于 90% 成功且成本 0.5 刀的。
坑 4:Trace 采了但没人看
现象:花两周接完 Trace,仪表盘搭好了,三个月后才发现没人打开过。
根因:指标没和"谁负责"绑定。没有告警、没有复盘机制,数据就是死数据。
解法:
- 把关键指标接告警(成功率跌破阈值 → PagerDuty)
- 每周固定"Agent 质量复盘会",看仪表盘成为流程
- bad case 进 issue tracker,形成"发现→分析→修复→回归"闭环
坑 5:用静态评估集测动态 Agent
现象:评估集半年没变,Agent 对这 100 条"考题"越答越好,线上却越来越差。
根因:Prompt 调优在评估集上过拟合,Agent 学会了"应试"。
解法:评估集定期轮换(每月新增、淘汰),保留一个"冻结集"做跨版本可比性基准,其余随业务演化。
九、未来展望
9.1 从"结果评估"到"过程评估"
当前评估重心压在最终答案上。但 Agent 的价值在于"过程"——它替人走了那 20 步。未来的评估会更看重轨迹质量:步数是否精炼、工具是否用对、遇到错误能否自恢复。一个好的 Agent 不是"答得对",而是"走得稳"。
9.2 Agent 的回归测试
传统软件有回归测试,Agent 因为非确定性而很难做。但趋势是用"统计回归"替代"逐条回归":
新版本跑评估集 100 条 → 与基线版本对比成功率/轨迹分/成本分布
- 成功率下降 > 3% → 阻断发版
- 成本上升 > 20% → 阻断发版
- 轨迹分布 KL 散度大 → 人工复核
把"发版"从拍脑袋变成有数据门禁的工程动作。
9.3 自评估 Agent
未来 Agent 在回答的同时输出"自评信心分"和"需复核标记",把评估内化到推理过程:
Agent 输出 = {
"answer": "...",
"self_confidence": 0.62,
"uncertain_points": ["退款金额未确认"],
"need_review": True
}
低信心的自动转人工,高信心的直接放行——评估不再是事后的旁路,而是事中的内嵌。
9.4 未来预判:评估的尽头是"行为分布画像"
当前评估是"对单条执行打分",但我们的预判是:单条评估终将不够,未来会走向"行为分布画像"评估。
理由:单条好/坏是点,Agent 的健康度是分布。同一个任务跑 100 次,成功率 70% 只是均值——真正的问题藏在分布的形状里:是双峰(要么全对要么全错,说明有某条触发路径整体崩)还是长尾(90% 秒回、10% 超时,说明有某类输入触发了死循环)?
当前: 评估集 100 条 → 每条跑 1 次 → 100 个分数 → 求均值
预判: 评估集 100 条 → 每条跑 N 次 → 100 个分布 → 看形状
把"均值评估"升级为"分布评估",能暴露均值掩盖的稳定性问题——一个平均 85% 但方差极大的 Agent,远比一个稳定 82% 的 Agent 危险。这与第五篇"行为序列审计"的安全预判同源:Agent 的本质是分布,不是点。 谁先建立起"分布画像"的评估与监控能力,谁就先拿到 Agent 工程化的下一张入场券。
十、总结
评估与可观测性是 Agent 工程化的闭环之扣。前五篇解决"怎么建",这一篇解决"怎么知道建得好不好、线上稳不稳"。
核心三条:
- 离线评估定义"好":四层指标(任务/轨迹/行为/成本)组合看,别只盯最终答案
- 在线可观测验证"稳":Trace/Span/Metrics 三支柱,每一步都可追、每一指标都告警
- 两轨闭环驱动"迭代":线上 bad case 反哺评估集,评估门禁卡住发版质量
记住:没有评估的优化是盲人摸象,没有可观测的上线是蒙眼狂奔。 Agent 不是调一次 Prompt 就完事的艺术品,是需要持续度量、持续迭代的工程系统。
欢迎在评论区交流你的 Agent 评估与监控实践!
本文基于 2026 年 Agent 评估与可观测性实战经验撰写,示例代码可在此基础上接入 Langfuse / LangSmith 替换为生产级后端。
- 点赞
- 收藏
- 关注作者
评论(0)