Skip to content

评估与可观测性 ​

前面八篇,你已经能把一个 Agent 搭起来、接上工具、配上记忆。但有个问题一直悬着:你怎么知道它干得好不好? 普通程序跑一遍,结果对不对一目了然;Agent 不是——同样的问题问它两遍,答案、路径、调用的工具都可能不一样。

这一篇回答一个核心问题:Agent 这种非确定性系统,用什么手段才能「看清它每一步在干什么」,又「量化它干得好不好」。 前者叫可观测性,后者叫评估。读完你会发现,没有这两样,Agent 上线就是开盲盒。


1. 开飞机不能凭感觉 ​

1.1 确定性 vs 非确定性:一道分水岭 ​

先看清问题的本质。普通程序是确定性的:

python
def add(a: int, b: int) -> int:
    return a + b

add(2, 3)  # 永远是 5,跑一万次也是 5

同样的输入,永远同样的输出。错了,复现它,一步步 debug,一定能定位。

Agent 是非确定性的。表面上看它也是「输入 → 输出」,但中间隔着一个大模型的采样过程:模型的输出是从一个概率分布里「抽」出来的。同一个 prompt,temperature、随机种子、甚至只是换了台机器,都可能抽出不同的 token。于是:

  • 同样的「深圳今天几度」,一次它直接答,一次它去调 get_weather,一次它反问「你要哪天的」。
  • 同一个任务,一次 3 步搞定,一次绕了 20 步还调错工具。

所以最危险的一句话是:「我看它这次回答得挺对的,应该没问题了吧。」 这句话的荒谬,等同于「我今天开车没出事,所以这辈子都不用系安全带」。

1.2 两个词的分工 ​

要对付非确定性,需要两样东西,缺一不可:

  • 可观测性(Observability)= 看清 Agent 每一步在干什么。 一次运行里,它思考了什么、调了哪个工具、传了什么参数、拿到了什么结果、最终输出了什么——全程留痕。
  • 评估(Evaluation)= 量化「干得好不好」。 不是「我感觉不错」,而是一套可重复、可比对的指标和评测集,让你能回答「改了这行 prompt 之后,成功率从 82% 涨到 87%,还是跌到 70%」。

一句话心智模型:可观测性给你「飞行记录仪」,评估给你「仪表盘和验收标准」。两者合起来,把 Agent 从「盲飞」变成「有仪表盘地飞」。

打个比方:开飞机不能只靠「感觉飞得挺稳」。你得看高度、速度、航向,出事了得查黑匣子。Agent 就是那架飞机,Trace(轨迹)就是黑匣子,指标就是仪表盘,评测集就是民航局的适航标准。


2. 为什么必须每步都能看见、能量化 ​

这一节不堆概念,讲清楚「为什么」——你只有想通了这三层为什么,才会心甘情愿去搭那套观测评估体系,而不是敷衍了事。

2.1 为什么 Agent 必须「可观测」 ​

三个原因,缺一条都站不住:

第一,非确定性 → 不复现 = 无法修。 用户报了个 bug:「它刚才回答完全跑偏了。」如果你没有轨迹,你连它当时输入是什么、走了哪条路都不知道。普通程序有堆栈、有日志;Agent 的「堆栈」就是它的完整轨迹。没有轨迹,Bug 报告等于一句「它坏了」。

第二,多步 + 工具副作用 → 不看过程 = 不知哪一步错。 一个客服 Agent 处理退款:它先查订单(对)、再调 check_policy(对)、然后调 execute_refund(错,金额写反了)。如果你只看最终输出「退款已完成」,你会以为一切正常;只有看到轨迹里 execute_refund 那个节点传错了参数,才能定位到根因。

第三,成本与责任 → 不记录 = 无法追责、无法审计、无法优化。 Agent 会烧钱(每一步都是 Token 计费),会越权(调了不该调的接口)。金融、医疗这类场景,出事要能回溯「谁在什么时间点做了哪个动作」。没有记录,责任就是一笔糊涂账。

2.2 为什么「人工看几个 case」不够 ​

这是最容易被低估的一层。很多人评估 Agent 的方式是:自己拿 10 个问题试一遍,觉得「嗯,回答得都不错」,于是上线。然后真实用户来了 1000 个,跑出 30% 的失败。

问题出在哪?人工印象是主观的、不可复现的、有偏的。

  • 主观:你「觉得不错」,另一个同事「觉得一般」,没有统一标尺。
  • 不可复现:你改了一行 prompt,想确认「变好了」,但上次那 10 个 case 的结果你已经记不清了,没法精确对比。
  • 有偏:你挑的 10 个 case,往往是自己熟悉的、模型也容易答对的——这叫幸存者偏差,那些模型容易翻车的边角 case,你压根没测到。

只有「评测集 + 指标」才能给你一把可重复、可量化的标尺。 评测集是一组固定的「问题 → 期望结果」,每次改动都跑一遍,指标自动对比前后变化。这样「改了一行 prompt 之后到底变好还是变差」,不再是感觉,是一个能查证的数字。

2.3 做一个诚实的取舍 ​

观测与评估不是免费的:要埋点、要写评测集、要维护指标、要养一套平台。它不直接产生业务价值,反而先吃掉一批工程时间。

但这是一笔「买确定性的钱」:

不花这笔钱花这笔钱
上线靠感觉,出事靠用户报障上线前有标尺,出事能回溯到具体步骤
改一行 prompt,不知道是变好还是变坏每次改动都有指标变化的证据
Bug 报过来,只能「我复现不了」Bug 报过来,点开那次轨迹就知道根因

取舍的结论很硬:Agent 上线前不搭观测评估,等于开飞机不装仪表盘。飞得越高,摔得越惨。 这钱不是「要不要花」的问题,是「早花一步还是摔了再花」的问题。


3. 怎么做好观测:Trace / Metric / Log 三件套 ​

观测有公认的三板斧,各自回答不同层次的问题。先建立这个框架,再往下落到工程。

3.1 三件套各看什么 ​

件看什么回答的问题
Trace(轨迹)一次运行每一步:输入、思考、工具调用、中间结果、输出「这一次到底发生了什么」
Metric(指标)聚合统计:成功率、延迟、成本、工具调用次数、循环次数「整体干得怎么样」
Log(日志)离散事件、异常、审计记录「某个时间点发生了什么」

三者的关系可以用一句话串起来:Trace 是单次的「录像」,Metric 是把很多次录像汇总成的「统计报表」,Log 是散落在各处的「随手记的便条」。 查单次失败看 Trace,看整体健康看 Metric,查具体异常事件翻 Log。

3.2 Trace 的结构:一个嵌套的调用树 ​

这是可观测性的核心数据结构,值得讲透。一次 Agent 运行,本质是一棵嵌套的调用树:

txt
root: "帮用户查一下深圳天气并给出穿衣建议"        ← 整次运行(根 Span)
├── span: LLM call(第一次思考)
│     ├── input: messages=[...用户问题...]
│     └── output: 决定调用 get_weather(city="深圳")
├── span: tool: get_weather(city="深圳")          ← 工具调用(子 Span)
│     ├── input: {"city": "深圳"}
│     └── output: "深圳当前 26°C,多云"
└── span: LLM call(第二次思考,综合结果)
      ├── input: [..., tool_result, ...]
      └── output: "深圳 26°C,建议穿薄外套"

这个结构叫 Span(一段有起止时间、带输入输出的执行单元),根和子 Span 的嵌套关系就叫 Trace。它的价值在于:任何一个节点崩了,你都能精确地「点开」它,看到它当时的输入、输出、耗时、报错。 这就是黑匣子的数据模型。

3.3 最小可跑的可观测实现 ​

先用标准库最朴素的方式,把「给 Agent 留痕」这件事讲清楚(不依赖任何平台,让你看到本质):

python
import time
import json
from dataclasses import dataclass, field

@dataclass
class Span:
    name: str
    kind: str                      # "llm" | "tool"
    input: dict = field(default_factory=dict)
    output: dict = field(default_factory=dict)
    error: str | None = None
    start: float = field(default_factory=time.time)
    duration_ms: float = 0.0
    children: list = field(default_factory=list)

class Tracer:
    def __init__(self):
        self.root: Span | None = None
        self._stack: list[Span] = []          # 用栈维护嵌套关系

    def start(self, name: str, kind: str, **inputs) -> Span:
        span = Span(name=name, kind=kind, input=inputs)
        if self.root is None:
            self.root = span                   # 第一个 Span 即根
        else:
            self._stack[-1].children.append(span)  # 挂到父节点
        self._stack.append(span)
        return span

    def end(self, span: Span, **outputs):
        span.output = outputs
        span.duration_ms = (time.time() - span.start) * 1000
        self._stack.pop()

用起来就是这样——把 Agent 的每一步包在 start/end 之间:

python
tracer = Tracer()

def run_agent(question: str):
    # 第一次思考
    s1 = tracer.start("llm:think-1", "llm", messages=[question])
    decision = {"tool": "get_weather", "args": {"city": "深圳"}}  # 模型决定调工具
    tracer.end(s1, decision=decision)

    # 工具调用
    s2 = tracer.start("tool:get_weather", "tool", args=decision["args"])
    result = "深圳当前 26°C,多云"
    tracer.end(s2, result=result)

    # 第二次思考,综合结果
    s3 = tracer.start("llm:think-2", "llm", messages=[question, result])
    answer = "深圳 26°C,建议穿薄外套"
    tracer.end(s3, answer=answer)

    return answer

run_agent("深圳今天穿什么?")
print(json.dumps({"root": trace_to_dict(tracer.root)}, ensure_ascii=False, indent=2))

这段代码的关键点,不是语法,而是那个栈:self._stack 用「后进先出」天然维护了 Span 的父子嵌套关系。你在别的观测平台(LangSmith、Langfuse、OpenTelemetry 等)看到的 Trace 树,本质都是这个数据结构——它们比你手写的版本多的,只是自动埋点、UI 可视化、聚合存储和分布式关联,而不是这个数据结构本身。

工具层面一句话定位:Python 生态用 LangSmith / Langfuse 这类平台(或自建 OpenTelemetry + 后端的组合)做轨迹追踪。它们自动给 LangChain/LangGraph 的每个节点、每次 LLM 调用打 Span,你无需手写上面那套 Tracer。核心价值不变:点开一次失败运行,看它走到哪一步崩了。


4. 怎么做好评估:量化「干得好不好」 ​

观测解决「看得见」,评估解决「判得出」。这一节讲评估的三样工具,以及它们背后的原理和坑。

4.1 评测集:非确定性系统的「回归测试」 ​

普通软件有单元测试,Agent 的对应物是评测集(Evaluation Set)——一组固定的「输入 → 期望结果」样本:

python
eval_set = [
    {"question": "北京今天几度?", "expected": "工具 get_weather(city=北京) 被正确调用并返回真实温度"},
    {"question": "帮我查订单 12345 的状态", "expected": "调用 get_order,返回已发货"},
    {"question": "讲个冷笑话", "expected": "不需要调工具,直接回答"},
    # ... 真实场景 50~500 条
]

它的用法是一句硬规则:每次改 prompt、改工具、换模型、调参数,都跑一遍评测集,对比前后指标。 这就是 Agent 版的「回归测试」——防止你「修好一个 bug,又带出三个新的」。

这里有个常常被忽视的原理:评测集的价值上限,取决于它对真实输入的覆盖率。 如果你的评测集 50 条全是「查天气、查订单」这类模型闭眼都会的,那 100% 的通过率是虚假的安全感。真正有用的评测集,必须包含模型容易翻车的边角 case:歧义问题、多义词、越权请求、超长上下文、带噪音的输入。评测集的质量,是评估质量的天花板,而这个天花板经常被「图省事只塞简单题」压得很低。

4.2 两类打分:有标准答案 vs 无标准答案 ​

评估任务分两种,打分方式完全不同,这是很多人混淆的点:

第一类:有客观标准答案。 比如「查天气要调 get_weather 并传对 city」、「订单状态要返回『已发货』」。这类可以直接断言——检查工具是否被调用、参数对不对、结果字段对不对。用代码就能判定,不需要模型裁判。

python
def check_weather_case(run) -> bool:
    # 判据:调用了 get_weather,且 city 参数是"北京",且返回了温度数字
    tool_calls = [s for s in run.spans if s.kind == "tool"]
    return (
        any(s.name == "get_weather" and s.input.get("city") == "北京" for s in tool_calls)
        and any(any(ch.isdigit() for ch in s.output.get("result", "")) for s in tool_calls)
    )

能断言的,绝不用模型裁判。 这是第一条原则——可判定的就写代码判定,又快又便宜又可复现。

第二类:没有标准答案,只有「好坏」。 比如「这个总结写得好不好」「这段文案有没有说服力」。这类任务引入了 LLM-as-judge(用另一个模型当裁判):

python
JUDGE_PROMPT = """你是一名严格的裁判。请按 1-5 分给下面的回答打分,并说明理由。

原问题:{question}
待评估回答:{answer}

评分维度:
- 准确性(回答是否与事实/工具返回一致)
- 相关性(是否直接回答了问题,有无跑题)
- 简洁(有无冗余废话)

只输出:分数 + 一行理由。"""

def judge(question: str, answer: str) -> tuple[int, str]:
    resp = llm.invoke(JUDGE_PROMPT.format(question=question, answer=answer))
    # 解析出分数和理由 ...
    return score, reason

4.3 为什么 LLM-as-judge 会「系统性有偏」——必须想透的一层 ​

这是评估里最深、也最容易踩的机理层,值得专门说清楚。

裁判和选手是同一个底层模型(甚至同一个模型实例),共享同一套训练数据带来的偏见。 选手犯的错,往往正是裁判「也一样会犯」的错——于是裁判给高分,不是因为它对,而是因为它和裁判「错到一块去了」。

举个具体场景:选手模型对长文本的理解偏弱,把一段 10 页合同的「违约责任」总结漏了关键条款;你让同一个模型当裁判去评这个总结,它大概率也「注意不到」那个漏掉的条款,于是打个高分。结果就是:缺陷被系统性放过了。

更深一层是所有 LLM 共有的两个倾向:

  1. 位置偏见(position bias):把两个候选答案排序比较时,裁判倾向于高估「排在前面/后面」的那个,而不是真正更优的那个。所以 A/B 对比评测里,要交换顺序各评一次,取平均。
  2. 长度偏见(verbosity bias):裁判本能地觉得「写得长的」比「写短的」更认真、更权威,哪怕长的只是注水。所以对比评测要尽量控制长度变量,或明确告诉裁判「简洁是加分项」。
  3. 自我偏好(self-preference / 同源偏置):同一个模型的输出,裁判天然更容易认可,因为它们的「审美」一致。

对策不是「不用裁判」,而是「不迷信裁判」:

  • 裁判只做初筛:先扫出低分样本,缩小人工抽查范围;
  • 关键判断人工兜底:抽 10% 的高分样本人工复核,尤其是那些「涉及安全、合规、金钱」的结论;
  • 换一个不同来源的模型当裁判:选手用 A,裁判用 B(不同厂商/不同代际),能显著降低同源偏置。

一句话:LLM-as-judge 能扩展、能标准化,但它的分数是把「有偏的尺子」,不是「绝对真理」。

4.4 别只看终点,要看过程指标 ​

这是评估里最容易犯的错:只盯「最终答案对不对」,不看「走到答案的代价」。

同一个「答对了」的结果,背后可能是天壤之别:

Agent最终答案步数工具调用单次成本质量
A对31¥0.03优
B对4012(还调错了 4 次)¥1.40差

B 虽然答对了,但它绕远路、乱调工具、烧了 40 多倍的 Token。「答案对了但过程烂」的 Agent,和「答案对且过程干净」的 Agent,在生产上完全是两个物种——前者会在高并发下拖垮延迟、烧穿成本、甚至因为误调工具产生副作用。

所以评估指标要分两套,都要盯:

  • 效果指标(outcome):最终答案对不对、用户满意度、任务完成率。
  • 过程指标(process):走了几步、调了几次工具、有没有死循环、单次 Token 成本、端到端延迟。

过程指标常常比效果指标更早暴露问题。 一个正在退化的 Agent,往往先表现为「步数变多、成本上升」,然后才表现为「答案开始出错」。盯住过程指标,等于提前看到故障的预警信号。


5. 黄金法则:评估要「采样」,别「全量」 ​

每次改动都全量跑评测集,又慢又贵——500 条评测集、每条几十个 Token、还带多轮工具调用,全量跑一遍可能就要几块钱加十几分钟。于是有了采样策略:

小改动先采样,确认方向对了再全量。 改了一个 prompt 措辞,先随机抽 20~30 条跑一遍,看指标趋势是「变好」还是「变坏」;方向对了,再全量验证拿最终数字。这背后的原理是统计学里的抽样估计——当样本是随机的且足够大(一般 ≥20~30),样本指标已经能给出相当可信的趋势判断,而成本只是全量的零头。

两条配套的铁律:

  1. 采样要随机,不能「专门挑简单的」。挑简单的跑,那叫「自我安慰」,不叫评估。
  2. 线上一定要接 Trace——评测集覆盖不了真实世界的全部输入,真正的疑难杂症,只能从线上轨迹里捞。评测集是「体检」,线上 Trace 是「急诊室里的心电监护」,两者各管一段,缺一不可。

6. 容易踩的几个坑 ​

  1. 上线前从不建评测集 —— 改 prompt 全靠「感觉不错」。根因:觉得写评测集麻烦。正确姿势:第一批就建一个小评测集,哪怕只有 50 条,也是你的安全网;之后边跑边补,重点补「翻车的边角 case」。
  2. 只看「答案对不对」,不看「过程」 —— 结果对了就放行,结果对了但绕远路、乱调工具、烧穿成本却不知道。根因:指标只看终点。正确姿势:同时盯步数、工具调用数、成本、有无死循环。
  3. 迷信 LLM-as-judge —— 裁判打高分就以为没问题,其实裁判和选手有同源偏见。根因:过度信任自动化。正确姿势:裁判只做初筛,关键判断人工抽查,尽量换不同源模型当裁判。
  4. 线性比较位置偏见 —— A/B 对比时,同一个答案排前面总比排后面得分高。正确姿势:交换顺序各评一次取平均。
  5. 线上不接 Trace —— 出了问题只能靠用户报障,查都无从查起。根因:只做线下评估。正确姿势:线上第一优先级接 Trace,出事能回溯到具体 Span。

7. 记住这一句 ​

Agent 不是「看起来对」就能上线的,它是非确定系统——必须用 Trace 看清每一步、用评测集量化好坏、用指标盯过程、用采样控成本。没有仪表盘的飞行,飞得越高摔得越惨。

下一篇预告:你终于能「看清」Agent 在干什么了。但看清只是第一步——看清之后,还得防它「干蠢事、干坏事」。这就到了 10 · 安全护栏。

Released under the MIT License.