上一篇我们用「查天气并总结」把 Chain 和 Graph 左右对照完了——能跑、能升级、能加记忆。但说句扎心的:你改了一行 Prompt、换了个模型、升级了 LangGraph,行为可能悄悄退化,而你还在靠「手动试几条」自我安慰。

传统测试写 expect(add(1, 2)).toBe(3),结果确定。Agent 不是:同一输入,temperature > 0 时说法会变;ReAct 环里工具调用次数、顺序也可能飘。没有系统化评估,边缘 case 的退化进不了 CI,合并后再救火成本翻倍。

这一篇把评估体系立起来:golden set、两种判分方式、本地 eval runner,以及怎么把通过率当成合并门槛。

老规矩,本文以官网最新文档核对过(How to evaluate agentsAgent Evals)。Agent 入口继续用博客 9 的 createAgent——别再抄 createReactAgent。本篇主路径是本地轻量 runner(适合 Ollama、零 LangSmith 账号也能跑);LangSmith / agentevals 作升级选项点到为止。

在这里插入图片描述

一、为什么 Agent 需要评估

挑战 说明
非确定性 同一 prompt,temperature > 0 时输出可能不同
多步推理 ReAct 环里 tool 调用顺序、次数可能变化
回归风险 改 prompt、换模型、升级依赖后,行为可能静默退化
成本与延迟 除了「对不对」,还要盯 token 与端到端耗时

单元测试测的是确定性逻辑(解析器、路由函数、工具本体)。Eval 测的是 Agent 端到端行为:该不该调工具、参数对不对、最终答案合不合理。两者互补,谁也替不了谁。

改Prompt或换模型

跑golden set

通过率达标?

CI失败挡住合并

合并

二、评估什么

生产里常见维度:

维度 衡量什么 典型指标
正确性 最终回答是否符合预期 pass rate、关键词命中、语义相似
工具调用 是否调对 tool、参数是否靠谱 tool name match、args 校验
延迟 端到端响应时间 p50 / p95 latency
Token 成本 输入 + 输出用量 avg tokens / query
鲁棒性 空输入、超长输入、异常工具返回 不崩溃、有友好错误
安全性 注入、越权 tool 拒绝率(后文安全篇再展开)

起步建议:

  • ReAct / 工具 Agent:至少同时看「最终答案」+「是否调用了期望工具」——只看文本,模型可能蒙对答案却根本没走 tool。
  • RAG:再加检索相关性、引用是否瞎编(本篇不展开检索评测细节)。

三、数据集:先有 golden set

评估质量 ≈ 数据集质量。三种来源:

来源 特点
Golden set 人工精选,覆盖核心场景 + 已知边缘 case;起步首选
合成数据 LLM 批量变体,扩面快;要人工抽检,防分布漂移
生产抽样 最贴真实;成本高,适合后期

建议起步:先建 10~20 条 golden set——happy path + 3~5 条边缘 case(空输入、不会的工具、超长问句),再逐步加。

单条 case 建议字段:

/** 单条评估用例 */
export interface EvalCase {
  id: string;
  input: string;
  /** 期望回答包含的关键词(部分匹配即可 pass) */
  expectedKeywords?: string[];
  /** 期望调用过的 tool 名称 */
  expectedTools?: string[];
  /** 分类标签,便于按场景统计 */
  tags: string[];
}

export const goldenSet: EvalCase[] = [
  {
    id: "calc-01",
    input: "3 加 5 等于多少?",
    expectedKeywords: ["8"],
    expectedTools: ["calculator"],
    tags: ["calculator", "react"],
  },
  {
    id: "calc-02",
    input: "100 加 25 是多少?",
    expectedKeywords: ["125"],
    expectedTools: ["calculator"],
    tags: ["calculator", "react"],
  },
  {
    id: "chat-01",
    input: "你好,介绍一下自己",
    expectedKeywords: ["助手", "帮助"],
    tags: ["chat"],
  },
];

四、两种判分:确定 vs 裁判

官网把 Agent 评估大致分成两路(见 Agent Evals):

方式 何时用 代价
确定性检查 你知道期望工具 / 关键词 / 参考轨迹 快、稳、几乎零成本
LLM-as-judge 开放题、要评「是否合理」「轨迹是否高效」 多一次 LLM 调用,有偏差

确定性这边:本篇用关键词 + expectedTools;更严的 trajectory match(严格顺序 / 无序 / subset / superset)可用 agentevalscreateTrajectoryMatchEvaluator——知道参考轨迹时优先它。

开放题这边:给 judge 一张 rubric,固定模型、temperature: 0,关键 case 保留人工复核。

Rubric 示例

请根据以下标准对 Agent 回答评分(1~5 分):
5 分:完全正确,信息准确,表述清晰
4 分:基本正确,有小瑕疵
3 分:部分正确,有遗漏或轻微错误
2 分:大部分错误
1 分:完全错误或无关
只输出一个整数分数。

极简 judge 骨架(本地 Ollama)

import { ChatOllama } from "@langchain/ollama";
import * as z from "zod";

const judgeLlm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
const scorer = judgeLlm.withStructuredOutput(
  z.object({ score: z.number().min(1).max(5), reason: z.string() })
);

async function judgeAnswer(question: string, answer: string) {
  return scorer.invoke(
    `问题:${question}\n回答:${answer}\n\n按 1~5 分打分,并给一句理由。`
  );
}

还有 pairwise:同一输入比 Agent A / B 谁更好,适合 Prompt 或模型 A/B。judge 本身也会偏——别把分数神化。

五、本地 Eval Runner(主路径)

不接 LangSmith 也能先跑起来:读 golden set → 逐条 invoke → 记 pass/fail、延迟 → 打汇总。

import { createAgent } from "langchain";
import { ChatOllama } from "@langchain/ollama";
import { tool } from "@langchain/core/tools";
import * as z from "zod";
import type { EvalCase } from "./dataset";
import { goldenSet } from "./dataset";

const calculator = tool(
  async ({ a, b }: { a: number; b: number }) => String(a + b),
  {
    name: "calculator",
    description: "两数相加",
    schema: z.object({ a: z.number(), b: z.number() }),
  }
);

const llm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
const agent = createAgent({
  model: llm,
  tools: [calculator],
  systemPrompt: "需要计算时必须调用 calculator 工具。",
});

function checkKeywords(content: string, keywords?: string[]): boolean {
  if (!keywords?.length) return true;
  const lower = content.toLowerCase();
  return keywords.every((k) => lower.includes(k.toLowerCase()));
}

function extractToolNames(
  messages: { tool_calls?: { name: string }[] }[]
): string[] {
  const names: string[] = [];
  for (const msg of messages) {
    for (const call of msg.tool_calls ?? []) {
      names.push(call.name);
    }
  }
  return names;
}

async function runSingleEval(caseItem: EvalCase) {
  const start = Date.now();
  const result = await agent.invoke({
    messages: [{ role: "user", content: caseItem.input }],
  });
  const latencyMs = Date.now() - start;

  const lastContent = String(result.messages.at(-1)?.content ?? "");
  const keywordOk = checkKeywords(lastContent, caseItem.expectedKeywords);
  const actualTools = extractToolNames(result.messages);
  const toolOk =
    !caseItem.expectedTools?.length ||
    caseItem.expectedTools.every((t) => actualTools.includes(t));

  const pass = keywordOk && toolOk;
  return {
    id: caseItem.id,
    pass,
    latencyMs,
    reason: pass ? undefined : `keywordOk=${keywordOk}, toolOk=${toolOk}`,
  };
}

async function runEval(passRateThreshold = 0.9) {
  const results = [];
  for (const c of goldenSet) {
    // 串行更省本地 Ollama;要速度可改成有限并发
    results.push(await runSingleEval(c));
  }

  const passed = results.filter((r) => r.pass).length;
  const rate = passed / results.length;
  const avgLatency =
    results.reduce((s, r) => s + r.latencyMs, 0) / results.length;

  console.log(
    `通过率: ${passed}/${results.length} (${(rate * 100).toFixed(1)}%)`
  );
  console.log(`平均延迟: ${avgLatency.toFixed(0)} ms`);
  for (const r of results.filter((x) => !x.pass)) {
    console.log(`  FAIL ${r.id}: ${r.reason}`);
  }

  if (rate < passRateThreshold) {
    process.exitCode = 1; // CI gate
  }
}

// await runEval();

跑「3 加 5」这类 case 时,你真正在验的是:既说对了 8,又真的调了 calculator——这比只 includes("8") 靠谱得多。

六、升级:LangSmith 与轨迹评估(短)

本地 runner 够你起步。需要实验对比、历史趋势、团队共享仪表盘时,再上 LangSmith

  1. 建 dataset(每条 example 有 inputs / 可选 reference outputs)
  2. 写 target:(inputs) => agent.invoke(inputs)
  3. 挂 evaluators,调用 evaluate(JS 里自带异步)

示意:

import { evaluate } from "langsmith/evaluation";
import {
  createTrajectoryLLMAsJudge,
  TRAJECTORY_ACCURACY_PROMPT,
} from "agentevals";

const trajectoryEvaluator = createTrajectoryLLMAsJudge({
  model: "openai:o3-mini", // 生产 judge 常用强模型;本地可换
  prompt: TRAJECTORY_ACCURACY_PROMPT,
});

await evaluate(
  async (inputs) => {
    const result = await agent.invoke(inputs);
    return result.messages;
  },
  {
    data: "your_dataset_name",
    evaluators: [trajectoryEvaluator],
  }
);

agentevals 还提供 trajectory matchstrict / unordered / subset / superset):有参考工具序列时,优先确定性匹配,又快又稳。细节见官网,本篇不展开成第二教程。

七、回归测试与 CI

把 eval 脚本挂进 CI(PR 或夜间定时):

# 示例:跑完通过率不达标则 exit 1
pnpm eval
做法 作用
通过率阈值(如 90%) 低于阈值 → job 失败,挡住合并
记录延迟 / 失败 case 看是变慢还是变笨
与单元测试分工 pnpm test 测纯逻辑;eval 测 Agent 行为

别指望 eval 替代单测:工具函数、State reducer、路由纯函数该单测还是单测。

常见坑

  1. 手动试几条就当过关:盖不住边缘 case,Prompt 小改会静默退化。
  2. golden set 全是 happy path:缺空输入、工具失败、超长输入 → 通过率虚高。
  3. LLM-as-judge 无 rubric / temperature 乱飘:分数不可比;固定 rubric + temperature: 0
  4. 只评最终文本、不评工具调用:模型可能蒙对答案却没走 tool,上线就露馅。
  5. eval 不进 CI:退化合并后才发现,修复成本高。
  6. 一上来就绑死 LangSmith:本地 runner 先通;仪表盘是加速器,不是起步前置。
  7. 还在用 createReactAgent 写 eval target:主路径换成 createAgent
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐