别只测答案——轨迹评估(Trajectory Evals)才是Agent测试的真相

现状之痛:你给Agent写测试,测的都是「最终答案对不对」——但Agent干坏事往往不在答案里:它可能调错了工具、传错了参数、循环了三次、偷偷绕过了门禁。答案全绿,过程全错。
读完你将:理解2026年评估方法论的最大转变——从测输出到测轨迹(Trajectory Evals),并拿到一个能直接复制的轨迹评估器代码。

〇、一个让你后背发凉的场景

先看一个真实案例(我的系统里发生过):

用户:查一下这个客户的报价历史

Agent 的轨迹(每一步都被记录下来):
① 调用 search_contact("上海XX物流") → 找到 3 个候选
② 调用 get_quote_history(contact_id=007) → 返回空
③ 调用 get_quote_history(contact_id=003) → 返回 12 条
④ 调用 send_email(收件人=007, 内容=003的历史报价) ← 这里错了!
⑤ 返回「已发送」

最终答案:✅ "报价历史已发送给客户"

最终答案全绿。 但轨迹第④步:把 003 的报价发给了 007——发错人了。

这不是我编的。2026年评估圈有两条共识级金句:

Anthropic(2026.01《Demystifying evals for AI agents》):
「We've seen teams with 90% benchmark scores fail in production.」
(我们看到过跑分90%的团队,在生产里翻车。)
morphllm:
「A final-answer score can be green while the trajectory looped and three turns drifted off policy.」
(最终答案打分全绿,但轨迹已经循环了三轮、偏离了策略。)

核心洞察:输出级测试对 Agent 是盲的。你测的是「它说了什么」,但 Agent 真正干的事在「它做了什么」里。


轨迹 vs 输出

*答案全绿 ≠ 轨迹正确:Agent 的价值在过程,不在结论。*


一、为什么测轨迹比测答案重要

传统测试(输出级)看什么

输入 → Agent → 最终答案
             ↑
        只测这里(对不对/好不好)

轨迹评估(过程级)看什么

输入 → ①调用工具A → ②调用工具B → ③判断 → ④输出
        ↑           ↑          ↑
    测工具选对了吗?参数对吗?循环合理吗?

Agent 的「错误」大多发生在轨迹里,不在答案里:

错误类型答案表现轨迹表现
调错工具答案可能正常工具选择错误 ✅发现
传错参数答案可能正常参数错误 ✅发现
死循环超时/空答案循环计数异常 ✅发现
越权访问答案正常访问了白名单外工具 ✅发现
绕过门禁答案正常跳过检查节点 ✅发现

一句话:答案只告诉你「结果」,轨迹告诉你「为什么」——而 Agent 的可靠性问题全在「为什么」里。


二、我的实践:error-ledger 数据飞轮

我的系统里,评估不是一次性测试,而是一个持续转动的数据飞轮:

生产环境 Agent 执行
  ↓
每一步轨迹写入审计日志(action/params/result/时间)
  ↓
发现问题 → 错误入库(error-ledger,已积累31条)
  ↓
提取教训 → 沉淀为规则/技能(writing_lessons,50行)
  ↓
门禁回归拦截(publish_gate:下次同类错误直接拦截)
  ↓
回到第一步(飞轮转动)

这就是轨迹评估的实体化:不是论文里的概念,是已经在转的生产系统。


数据飞轮

*error-ledger → lessons → publish_gate:评估飞轮持续转动。*


三、保姆级:一个可复制的 TrajectoryEvaluator

下面是我系统里实际运行的轨迹评估器(简化版,可直接复制):

TrajectoryEvaluator 三层检测

*死循环/白名单/参数合理性:三层规则给轨迹打分。*

"""TrajectoryEvaluator:记录每一步轨迹,检测异常模式"""
from dataclasses import dataclass, field
from typing import List, Dict, Any
import json
import time

@dataclass
class TrajectoryStep:
    """轨迹中的一步"""
    action: str                 # 动作:调用的工具/函数名
    params: Dict[str, Any]      # 参数:传给工具的
    result: Dict[str, Any]      # 结果:工具返回的
    timestamp: float = field(default_factory=time.time)

@dataclass
class Trajectory:
    """一次完整执行的轨迹"""
    task_id: str
    steps: List[TrajectoryStep] = field(default_factory=list)
    
    def add_step(self, action, params, result):
        self.steps.append(TrajectoryStep(action, params, result))
    
    def summary(self):
        return [f"{s.action}({json.dumps(s.params, ensure_ascii=False)[:50]})" 
                for s in self.steps]

class TrajectoryEvaluator:
    """轨迹评估器:检测异常模式"""
    
    # 规则1:同工具连续调用超过N次 = 可能死循环
    MAX_REPEAT = 3
    
    def __init__(self):
        self.whitelist = set()  # 工具白名单(关联C7最小权限)
        self.violations = []
    
    def evaluate(self, traj: Trajectory) -> Dict[str, Any]:
        """评估一条轨迹,返回 {score, violations}"""
        self.violations = []
        
        # 检查1:循环检测
        actions = [s.action for s in traj.steps]
        for i in range(len(actions) - self.MAX_REPEAT + 1):
            window = actions[i:i + self.MAX_REPEAT]
            if len(set(window)) == 1:  # 同一个工具连续调用
                self._violate(f"疑似死循环:{window[0]} 连续调用 {self.MAX_REPEAT} 次")
        
        # 检查2:白名单检测(越权)
        for s in traj.steps:
            if self.whitelist and s.action not in self.whitelist:
                self._violate(f"越权:调用了白名单外工具 {s.action}")
        
        # 检查3:参数合理性(空参数调工具)
        for s in traj.steps:
            if s.action in ("send_email", "send_quote") and not s.params.get("to"):
                self._violate(f"参数错误:{s.action} 缺少收件人")
        
        # 打分:每违规-20分,满分100
        score = max(0, 100 - len(self.violations) * 20)
        return {
            "score": score,
            "passed": score >= 80,
            "violations": self.violations,
            "trajectory": traj.summary(),
        }
    
    def _violate(self, msg):
        self.violations.append(msg)
        # 联动:写入错误日志(error-ledger 数据飞轮)
        print(f"  ⚠ VIOLATION: {msg}")

# ── 使用示例 ──
ev = TrajectoryEvaluator()
ev.whitelist = {"search_contact", "get_quote_history", "send_email"}

# 正确轨迹
good = Trajectory(task_id="T1")
good.add_step("search_contact", {"name": "上海物流"}, {"candidates": [{"id": 7}]})
good.add_step("get_quote_history", {"contact_id": 7}, {"quotes": 12})
good.add_step("send_email", {"to": 7}, {"sent": True})

# 错误轨迹(发错人:第3步用003的数据发给007)
bad = Trajectory(task_id="T2")
bad.add_step("search_contact", {"name": "上海物流"}, {"candidates": [{"id": 7}]})
bad.add_step("get_quote_history", {"contact_id": 3}, {"quotes": 12})
bad.add_step("send_email", {"to": 7, "content": "003的报价"}, {"sent": True})

print("=== 正确轨迹评估 ===")
print(json.dumps(ev.evaluate(good), ensure_ascii=False, indent=2))
print("\n=== 错误轨迹评估 ===")
print(json.dumps(ev.evaluate(bad), ensure_ascii=False, indent=2))

跑一下你就看到差异:正确轨迹 score=100 通过;错误轨迹虽然「答案全绿」,但轨迹评估器通过「参数合理性」规则抓到 send_email 参数异常——这就是轨迹评估的价值。


四、把轨迹评估接进你的系统(3步)

第1步:给Agent的每个工具调用加「轨迹记录」

不管用什么框架(LangGraph/CrewAI/自研),核心是每一步都记录 action/params/result。

第2步:定义你的「异常规则」

从我上面的三类开始,逐步加:

  • 死循环(同工具连续N次)
  • 越权(白名单外工具)
  • 参数错误(缺必填/格式错)
  • 耗时异常(单步超时)

第3步:接入门禁 + 数据飞轮

  • 低分轨迹 → 拦截输出 → 人工确认
  • 确认的错误 → 入 error-ledger → 提取规则 → 回到第2步

五、此刻的你

此刻的你,不再是那个「答案全绿就以为Agent没问题」的乐观开发者。

你正在成为那个「连轨迹都审计、每个动作都打分、每类错误都沉淀成规则」的严格工程师。

2026年评估方法论的最大共识:测轨迹,不测答案。 因为Agent的价值在过程——答案会骗人,轨迹不会。

记住:最终答案全绿,不代表Agent没干坏事。把评估从输出移到轨迹,你才真正看得见Agent在做什么。


实体:Trajectory Evals, TrajectoryEvaluator, 轨迹评估, 数据飞轮

【价值】 价值:评估准确性, 错误拦截, 可靠性提升

【认知】 认知:从测输出到测轨迹——Agent的价值在过程,不在结论


【作者】 关于作者 无记——AI / Agent / 数智化转型实践者。 只写亲手跑通的东西,不聊概念。关注我,一起把认知变现。

posted @ 2026-08-13 09:30  魏无记  阅读(26)  评论(0)    收藏  举报