面向已经能用 LangChain/LangGraph 跑通 Agent、但开发测试"效果时好时坏、上线没底气"的开发者。
核心论点:Agent 的可靠性不是调出来的,是测出来的。 当你的 Agent 还停留在"我试了几个例子都对了"这种评估方式时,它就不该进生产。
文中代码基于 LangChain 1.0 / LangGraph 1.0 、langsmith>=0.4.29(支持组合评估器与多轮评估) 、pydantic>=2。
目录
- [为什么评估是 Agent 落地的最大短板](#为什么评估是 Agent 落地的最大短板)
- 传统测试为何失效
- 评估体系分层
- 评估维度模型
- 方法论一:可执行断言(确定性评估)
- 方法论二:LLM-as-Judge(轨迹级评估)
- 方法论三:参考轨迹对比与回归
- 测试类型
- [工具链实战:用 LangSmith 构建评估流水线](#工具链实战:用 LangSmith 构建评估流水线)
- 在线评估与持续监控闭环
- 总结
一、为什么评估是 Agent 落地的最大短板
Gartner 预测到 2027 年底超 40% 的 Agent 项目被取消,MIT 研究显示 95% 的生成式 AI 试点未带来可衡量回报。在无数失败案例背后,工程能力的缺位 里最隐蔽、也最致命的一项,就是缺乏系统化的评估与测试体系。
一个尴尬的现实:
- 你能让 Agent 在 10 个手搓的例子上正确回答;
- 你不知道它在第 11 个没见过的输入上会不会陷入死循环;
- 你无法回答"这个版本比上个版本好多少、好在哪"的问题。
没有评估,Agent 的迭代就是盲飞。而评估的难,又远超传统软件------这正是本文要解决的问题。
二、传统测试为何失效
#mermaid-svg-HUqU8IqbX4kBSI3R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HUqU8IqbX4kBSI3R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HUqU8IqbX4kBSI3R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HUqU8IqbX4kBSI3R .error-icon{fill:#552222;}#mermaid-svg-HUqU8IqbX4kBSI3R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HUqU8IqbX4kBSI3R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HUqU8IqbX4kBSI3R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HUqU8IqbX4kBSI3R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HUqU8IqbX4kBSI3R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HUqU8IqbX4kBSI3R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HUqU8IqbX4kBSI3R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HUqU8IqbX4kBSI3R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HUqU8IqbX4kBSI3R .marker.cross{stroke:#333333;}#mermaid-svg-HUqU8IqbX4kBSI3R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HUqU8IqbX4kBSI3R p{margin:0;}#mermaid-svg-HUqU8IqbX4kBSI3R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HUqU8IqbX4kBSI3R .cluster-label text{fill:#333;}#mermaid-svg-HUqU8IqbX4kBSI3R .cluster-label span{color:#333;}#mermaid-svg-HUqU8IqbX4kBSI3R .cluster-label span p{background-color:transparent;}#mermaid-svg-HUqU8IqbX4kBSI3R .label text,#mermaid-svg-HUqU8IqbX4kBSI3R span{fill:#333;color:#333;}#mermaid-svg-HUqU8IqbX4kBSI3R .node rect,#mermaid-svg-HUqU8IqbX4kBSI3R .node circle,#mermaid-svg-HUqU8IqbX4kBSI3R .node ellipse,#mermaid-svg-HUqU8IqbX4kBSI3R .node polygon,#mermaid-svg-HUqU8IqbX4kBSI3R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HUqU8IqbX4kBSI3R .rough-node .label text,#mermaid-svg-HUqU8IqbX4kBSI3R .node .label text,#mermaid-svg-HUqU8IqbX4kBSI3R .image-shape .label,#mermaid-svg-HUqU8IqbX4kBSI3R .icon-shape .label{text-anchor:middle;}#mermaid-svg-HUqU8IqbX4kBSI3R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HUqU8IqbX4kBSI3R .rough-node .label,#mermaid-svg-HUqU8IqbX4kBSI3R .node .label,#mermaid-svg-HUqU8IqbX4kBSI3R .image-shape .label,#mermaid-svg-HUqU8IqbX4kBSI3R .icon-shape .label{text-align:center;}#mermaid-svg-HUqU8IqbX4kBSI3R .node.clickable{cursor:pointer;}#mermaid-svg-HUqU8IqbX4kBSI3R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HUqU8IqbX4kBSI3R .arrowheadPath{fill:#333333;}#mermaid-svg-HUqU8IqbX4kBSI3R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HUqU8IqbX4kBSI3R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HUqU8IqbX4kBSI3R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HUqU8IqbX4kBSI3R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HUqU8IqbX4kBSI3R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HUqU8IqbX4kBSI3R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HUqU8IqbX4kBSI3R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HUqU8IqbX4kBSI3R .cluster text{fill:#333;}#mermaid-svg-HUqU8IqbX4kBSI3R .cluster span{color:#333;}#mermaid-svg-HUqU8IqbX4kBSI3R div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HUqU8IqbX4kBSI3R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HUqU8IqbX4kBSI3R rect.text{fill:none;stroke-width:0;}#mermaid-svg-HUqU8IqbX4kBSI3R .icon-shape,#mermaid-svg-HUqU8IqbX4kBSI3R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HUqU8IqbX4kBSI3R .icon-shape p,#mermaid-svg-HUqU8IqbX4kBSI3R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HUqU8IqbX4kBSI3R .icon-shape .label rect,#mermaid-svg-HUqU8IqbX4kBSI3R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HUqU8IqbX4kBSI3R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HUqU8IqbX4kBSI3R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HUqU8IqbX4kBSI3R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 传统软件测试
输入确定 → 输出确定
可穷举路径
断言相等
Agent 测试
输入相同 → 输出可能不同
路径组合爆炸
需评估'质量'而非'对错'
故障是软性的: 自信地错
- 非确定性:同一问题两次跑,推理路径、措辞、甚至工具选择都可能不同。"相等断言"失效,必须评估"分布质量"。
- 路径组合爆炸 :ReAct 循环里每一步都有分支,无法穷举所有轨迹。覆盖率思维要让位于抽样 + 代表性数据集。
- 软故障:Agent 不会"报错崩溃",而是"自信地给出一个错误的、看似合理的答案"。传统异常捕获抓不到。
- 多目标冲突 :一个回答"更准"可能"更慢更贵";"更简洁"可能"漏了关键步骤"。评估必须是多维度的权衡,而非单指标。
专家判断:Agent 评估的本质,是把"概率系统的质量"翻译成"可被工程团队比较、回归、门禁的数字"。
三、评估体系分层
一个完整的评估体系应覆盖三条时间线,缺一不可:
#mermaid-svg-uYqefMec51hbJvdC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uYqefMec51hbJvdC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uYqefMec51hbJvdC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uYqefMec51hbJvdC .error-icon{fill:#552222;}#mermaid-svg-uYqefMec51hbJvdC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uYqefMec51hbJvdC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uYqefMec51hbJvdC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uYqefMec51hbJvdC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uYqefMec51hbJvdC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uYqefMec51hbJvdC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uYqefMec51hbJvdC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uYqefMec51hbJvdC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uYqefMec51hbJvdC .marker.cross{stroke:#333333;}#mermaid-svg-uYqefMec51hbJvdC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uYqefMec51hbJvdC p{margin:0;}#mermaid-svg-uYqefMec51hbJvdC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uYqefMec51hbJvdC .cluster-label text{fill:#333;}#mermaid-svg-uYqefMec51hbJvdC .cluster-label span{color:#333;}#mermaid-svg-uYqefMec51hbJvdC .cluster-label span p{background-color:transparent;}#mermaid-svg-uYqefMec51hbJvdC .label text,#mermaid-svg-uYqefMec51hbJvdC span{fill:#333;color:#333;}#mermaid-svg-uYqefMec51hbJvdC .node rect,#mermaid-svg-uYqefMec51hbJvdC .node circle,#mermaid-svg-uYqefMec51hbJvdC .node ellipse,#mermaid-svg-uYqefMec51hbJvdC .node polygon,#mermaid-svg-uYqefMec51hbJvdC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uYqefMec51hbJvdC .rough-node .label text,#mermaid-svg-uYqefMec51hbJvdC .node .label text,#mermaid-svg-uYqefMec51hbJvdC .image-shape .label,#mermaid-svg-uYqefMec51hbJvdC .icon-shape .label{text-anchor:middle;}#mermaid-svg-uYqefMec51hbJvdC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uYqefMec51hbJvdC .rough-node .label,#mermaid-svg-uYqefMec51hbJvdC .node .label,#mermaid-svg-uYqefMec51hbJvdC .image-shape .label,#mermaid-svg-uYqefMec51hbJvdC .icon-shape .label{text-align:center;}#mermaid-svg-uYqefMec51hbJvdC .node.clickable{cursor:pointer;}#mermaid-svg-uYqefMec51hbJvdC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uYqefMec51hbJvdC .arrowheadPath{fill:#333333;}#mermaid-svg-uYqefMec51hbJvdC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uYqefMec51hbJvdC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uYqefMec51hbJvdC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uYqefMec51hbJvdC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uYqefMec51hbJvdC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uYqefMec51hbJvdC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uYqefMec51hbJvdC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uYqefMec51hbJvdC .cluster text{fill:#333;}#mermaid-svg-uYqefMec51hbJvdC .cluster span{color:#333;}#mermaid-svg-uYqefMec51hbJvdC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uYqefMec51hbJvdC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uYqefMec51hbJvdC rect.text{fill:none;stroke-width:0;}#mermaid-svg-uYqefMec51hbJvdC .icon-shape,#mermaid-svg-uYqefMec51hbJvdC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uYqefMec51hbJvdC .icon-shape p,#mermaid-svg-uYqefMec51hbJvdC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uYqefMec51hbJvdC .icon-shape .label rect,#mermaid-svg-uYqefMec51hbJvdC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uYqefMec51hbJvdC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uYqefMec51hbJvdC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uYqefMec51hbJvdC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 韧性评估 Resilience --- 定期
在线评估 Online --- 发布后
离线评估 Offline --- 发布前
单元测试 节点级
集成测试 图级
端到端 黄金集
回归测试
生产 Trace 采样
LLM-as-Judge 实时监控
用户反馈闭环
混沌测试 故障注入
对抗测试 Prompt 注入
质量门禁 CI
持续优化
鲁棒性加固
- 离线评估解决"这次改动有没有变好/变坏"。
- 在线评估解决"真实流量下到底行不行"。
- 韧性评估解决"极端/恶意情况下会不会崩"。
四、评估维度模型
不要只测"最终答案对不对"。Agent 的质量是分层的:
#mermaid-svg-cZ162mcbvEzuZGgn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cZ162mcbvEzuZGgn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cZ162mcbvEzuZGgn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cZ162mcbvEzuZGgn .error-icon{fill:#552222;}#mermaid-svg-cZ162mcbvEzuZGgn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cZ162mcbvEzuZGgn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cZ162mcbvEzuZGgn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cZ162mcbvEzuZGgn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cZ162mcbvEzuZGgn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cZ162mcbvEzuZGgn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cZ162mcbvEzuZGgn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cZ162mcbvEzuZGgn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cZ162mcbvEzuZGgn .marker.cross{stroke:#333333;}#mermaid-svg-cZ162mcbvEzuZGgn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cZ162mcbvEzuZGgn p{margin:0;}#mermaid-svg-cZ162mcbvEzuZGgn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cZ162mcbvEzuZGgn .cluster-label text{fill:#333;}#mermaid-svg-cZ162mcbvEzuZGgn .cluster-label span{color:#333;}#mermaid-svg-cZ162mcbvEzuZGgn .cluster-label span p{background-color:transparent;}#mermaid-svg-cZ162mcbvEzuZGgn .label text,#mermaid-svg-cZ162mcbvEzuZGgn span{fill:#333;color:#333;}#mermaid-svg-cZ162mcbvEzuZGgn .node rect,#mermaid-svg-cZ162mcbvEzuZGgn .node circle,#mermaid-svg-cZ162mcbvEzuZGgn .node ellipse,#mermaid-svg-cZ162mcbvEzuZGgn .node polygon,#mermaid-svg-cZ162mcbvEzuZGgn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cZ162mcbvEzuZGgn .rough-node .label text,#mermaid-svg-cZ162mcbvEzuZGgn .node .label text,#mermaid-svg-cZ162mcbvEzuZGgn .image-shape .label,#mermaid-svg-cZ162mcbvEzuZGgn .icon-shape .label{text-anchor:middle;}#mermaid-svg-cZ162mcbvEzuZGgn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cZ162mcbvEzuZGgn .rough-node .label,#mermaid-svg-cZ162mcbvEzuZGgn .node .label,#mermaid-svg-cZ162mcbvEzuZGgn .image-shape .label,#mermaid-svg-cZ162mcbvEzuZGgn .icon-shape .label{text-align:center;}#mermaid-svg-cZ162mcbvEzuZGgn .node.clickable{cursor:pointer;}#mermaid-svg-cZ162mcbvEzuZGgn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cZ162mcbvEzuZGgn .arrowheadPath{fill:#333333;}#mermaid-svg-cZ162mcbvEzuZGgn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cZ162mcbvEzuZGgn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cZ162mcbvEzuZGgn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cZ162mcbvEzuZGgn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cZ162mcbvEzuZGgn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cZ162mcbvEzuZGgn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cZ162mcbvEzuZGgn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cZ162mcbvEzuZGgn .cluster text{fill:#333;}#mermaid-svg-cZ162mcbvEzuZGgn .cluster span{color:#333;}#mermaid-svg-cZ162mcbvEzuZGgn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cZ162mcbvEzuZGgn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cZ162mcbvEzuZGgn rect.text{fill:none;stroke-width:0;}#mermaid-svg-cZ162mcbvEzuZGgn .icon-shape,#mermaid-svg-cZ162mcbvEzuZGgn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cZ162mcbvEzuZGgn .icon-shape p,#mermaid-svg-cZ162mcbvEzuZGgn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cZ162mcbvEzuZGgn .icon-shape .label rect,#mermaid-svg-cZ162mcbvEzuZGgn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cZ162mcbvEzuZGgn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cZ162mcbvEzuZGgn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cZ162mcbvEzuZGgn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 评估维度
任务层
成功率 / 目标达成 / 用户满意度
轨迹层
工具选择正确 / 步骤效率 / 无死循环
质量层
幻觉率 / 忠实度 / 合规
成本层
Token·任务 / 步数·任务 / 首字延迟
安全层
注入抵抗 / 越权拦截 / 数据泄露
关键认知:任务层(答案对不对)只是冰山一角。一个答案"看起来对"的 Agent,可能走了 23 步、调了 9 次工具、烧了 8 万 Token。
五、方法论一:可执行断言(确定性评估)
零成本、可复现、应作为第一道防线。 适用于有明确契约的场景:预期调用了哪些工具、返回了特定结构、状态满足约束。
5.0 被测 Agent 的 v1 构建方式
评估的前提是有一个"被测对象"。在 LangChain 1.0 / LangGraph 1.0 中,官方推荐用 langchain.agents.create_agent(取代已弃用的 create_react_agent)构建,模型用 init_chat_model 统一初始化:
python
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import InMemorySaver # v1 由 MemorySaver 重命名
# v1 统一模型入口:一个函数初始化任意厂商模型,格式为 "provider:model"
llm = init_chat_model("openai:gpt-5", temperature=0)
app = create_agent(
model=llm,
tools=[search, calculator, summarize],
system_prompt="你是订单分析助手,按需调用工具完成任务。",
checkpointer=InMemorySaver(), # 持久化 + 可中断(human-in-the-loop 的基础)
# middleware=[...] # v1 中间件:历史摘要 / 人工介入 / PII 脱敏可插拔
)
# 注:create_react_agent 已在 v1 弃用;AgentState 现为 langchain.agents.AgentState(不再使用 pydantic 状态)
5.1 执行并重建轨迹
python
from langchain_core.messages import AIMessage, ToolMessage, HumanMessage
from langchain_core.utils.uuid import uuid7
def run_and_capture(query: str, thread_id: str | None = None) -> dict:
"""执行 Agent 并重建轨迹,供断言与 Judge 使用。"""
thread_id = thread_id or str(uuid7())
result = app.invoke(
{"messages": [HumanMessage(content=query)]},
# v1 中 thread_id 仍走 configurable;recursion_limit 控制图级步数上限,
# 与 create_agent 内部的 remaining_steps(ManagedValue)共同防死循环
config={"configurable": {"thread_id": thread_id}, "recursion_limit": 25},
)
traj = []
for m in result["messages"]:
if isinstance(m, AIMessage) and m.tool_calls:
traj.append({"role": "ai", "tools": [t["name"] for t in m.tool_calls]})
elif isinstance(m, ToolMessage):
traj.append({"role": "tool", "id": m.tool_call_id})
elif isinstance(m, AIMessage):
traj.append({"role": "ai", "content": m.content})
return {"output": result["messages"][-1].content, "trajectory": traj}
def tool_coverage_assertion(run, example) -> dict:
"""断言:预期工具集合被完整调用。"""
expected = set(example.outputs["expected_tools"]) # 如 {"search", "calc"}
actual = {t for step in run["trajectory"]
if step["role"] == "ai" for t in step.get("tools", [])}
ok = expected.issubset(actual)
return {"key": "tool_coverage", "score": 1.0 if ok else 0.0,
"comment": f"expected {expected}, got {actual}"}
def no_deadloop_assertion(run, example) -> dict:
"""断言:步数不超过上限(防死循环)。"""
steps = len([s for s in run["trajectory"] if s["role"] in ("ai", "tool")])
ok = steps <= example.metadata.get("max_steps", 12)
return {"key": "bounded_steps", "score": 1.0 if ok else 0.0,
"comment": f"steps={steps}"}
要点 :断言类评估不花 LLM 钱、可进 CI、毫秒级,应该覆盖所有"能写成规则的"质量点。把 Judge 留给那些必须靠语义判断的部分。
六、方法论二:LLM-as-Judge(轨迹级评估)
当"对不对"需要语义理解时,用另一个(更强的)LLM 当评委。关键是结构化输出 + 明确 rubric,避免"请给个分数"这种模糊指令。
评分体系 Judge LLM 评估器 评分体系 Judge LLM 评估器 #mermaid-svg-y3h35ZdWGVeBpdyt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-y3h35ZdWGVeBpdyt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-y3h35ZdWGVeBpdyt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-y3h35ZdWGVeBpdyt .error-icon{fill:#552222;}#mermaid-svg-y3h35ZdWGVeBpdyt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-y3h35ZdWGVeBpdyt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-y3h35ZdWGVeBpdyt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-y3h35ZdWGVeBpdyt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-y3h35ZdWGVeBpdyt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-y3h35ZdWGVeBpdyt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-y3h35ZdWGVeBpdyt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-y3h35ZdWGVeBpdyt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-y3h35ZdWGVeBpdyt .marker.cross{stroke:#333333;}#mermaid-svg-y3h35ZdWGVeBpdyt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-y3h35ZdWGVeBpdyt p{margin:0;}#mermaid-svg-y3h35ZdWGVeBpdyt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-y3h35ZdWGVeBpdyt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-y3h35ZdWGVeBpdyt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-y3h35ZdWGVeBpdyt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-y3h35ZdWGVeBpdyt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-y3h35ZdWGVeBpdyt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-y3h35ZdWGVeBpdyt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-y3h35ZdWGVeBpdyt .sequenceNumber{fill:white;}#mermaid-svg-y3h35ZdWGVeBpdyt #sequencenumber{fill:#333;}#mermaid-svg-y3h35ZdWGVeBpdyt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-y3h35ZdWGVeBpdyt .messageText{fill:#333;stroke:none;}#mermaid-svg-y3h35ZdWGVeBpdyt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-y3h35ZdWGVeBpdyt .labelText,#mermaid-svg-y3h35ZdWGVeBpdyt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-y3h35ZdWGVeBpdyt .loopText,#mermaid-svg-y3h35ZdWGVeBpdyt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-y3h35ZdWGVeBpdyt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-y3h35ZdWGVeBpdyt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-y3h35ZdWGVeBpdyt .noteText,#mermaid-svg-y3h35ZdWGVeBpdyt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-y3h35ZdWGVeBpdyt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-y3h35ZdWGVeBpdyt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-y3h35ZdWGVeBpdyt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-y3h35ZdWGVeBpdyt .actorPopupMenu{position:absolute;}#mermaid-svg-y3h35ZdWGVeBpdyt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-y3h35ZdWGVeBpdyt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-y3h35ZdWGVeBpdyt .actor-man circle,#mermaid-svg-y3h35ZdWGVeBpdyt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-y3h35ZdWGVeBpdyt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 问题 + 实际轨迹 + 参考答案 + 评分标准(rubric) with_structured_output 结构化打分 TrajGrade{score, reasoning, deviation} 归一化分数 + 偏离说明
python
from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
class TrajGrade(BaseModel):
score: int = Field(ge=1, le=5, description="轨迹质量 1-5")
reasoning: str = Field(description="扣分/加分理由")
deviation: str = Field(description="与最优路径的偏离点")
hallucination: bool = Field(description="是否出现事实性错误")
# v1:Judge 同样用 init_chat_model 统一初始化,选更强的模型
llm_strong = init_chat_model("anthropic:claude-opus-4-7", temperature=0)
judge = llm_strong.with_structured_output(TrajGrade)
JUDGE_PROMPT = ChatPromptTemplate.from_messages([
("system", "你是严格的 Agent 轨迹评审专家。只依据给定信息客观打分,禁止臆测。"),
("human", """
用户问题: {question}
参考最优轨迹: {reference}
实际轨迹: {trajectory}
最终答案: {answer}
请按标准评分:
- 5: 路径最优、无冗余、答案正确忠实
- 3: 路径有冗余或绕路,但答案正确
- 1: 路径严重错误/死循环/答案错误
"""),
])
def trajectory_judge(run, example) -> dict:
grade: TrajGrade = (JUDGE_PROMPT | judge).invoke({
"question": example.inputs["question"],
"reference": example.outputs["reference_trajectory"],
"trajectory": run["trajectory"],
"answer": run["output"],
}).model_dump()
return {
"key": "trajectory_quality",
"score": grade.score / 5.0,
"comment": f"{grade.reasoning} | 偏离: {grade.deviation} | 幻觉: {grade.hallucination}",
}
Judge 防偏置三原则:
- rubric 写死:分数档位必须有明确行为锚点,不能让模型"自由发挥"。
- 参考轨迹比对:给 Judge 看"理想路径",它才判得出"有没有绕路",否则只会无脑给高分。
- 关键项独立打分:把"幻觉/忠实度/合规"拆成独立布尔/分数字段,避免被"答案通顺"带偏整体分。
专家提醒 :LLM-as-Judge 自身也会错。生产环境务必保留人工抽检 + 黄金校准集(一批你人工标过分的样本),定期量 Judge 与你人工打分的一致性(如 Cohen's κ),κ 低于 0.6 就该换 prompt 或换模型。
七、方法论三:参考轨迹对比与回归
Agent 迭代最怕"改好 A 坏 B 却不知道"。回归测试就是答案:把每个历史 bug / 边界 case 固化成数据集里的一条,每次发版自动重跑。
python
# 把一次真实故障固化为回归用例(LangSmith dataset 中一条 example)
example = {
"inputs": {"question": "帮我查一下订单 O123 的物流并总结"},
"outputs": {
"expected_tools": ["query_order", "query_logistics", "summarize"],
"reference_trajectory": ["ai→query_order", "tool", "ai→query_logistics",
"tool", "ai→summarize", "tool", "ai→final"],
},
"metadata": {"case_type": "regression", "bug_id": "AG-217", "max_steps": 12},
}
回归集的维护纪律:
- 每次线上故障 → 必加一条回归用例。
- 每条用例带
case_type:功能 / 边界 / 回归 / 安全,便于按维度看趋势。 - 版本间对比 :用
experiment_prefix区分agent-v2.1/agent-v2.2,在 LangSmith 里直接 diff 分数分布。
八、测试类型
#mermaid-svg-czaGWLND8ShaE56c{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-czaGWLND8ShaE56c .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-czaGWLND8ShaE56c .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-czaGWLND8ShaE56c .error-icon{fill:#552222;}#mermaid-svg-czaGWLND8ShaE56c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-czaGWLND8ShaE56c .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-czaGWLND8ShaE56c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-czaGWLND8ShaE56c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-czaGWLND8ShaE56c .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-czaGWLND8ShaE56c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-czaGWLND8ShaE56c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-czaGWLND8ShaE56c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-czaGWLND8ShaE56c .marker.cross{stroke:#333333;}#mermaid-svg-czaGWLND8ShaE56c svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-czaGWLND8ShaE56c p{margin:0;}#mermaid-svg-czaGWLND8ShaE56c .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-czaGWLND8ShaE56c .cluster-label text{fill:#333;}#mermaid-svg-czaGWLND8ShaE56c .cluster-label span{color:#333;}#mermaid-svg-czaGWLND8ShaE56c .cluster-label span p{background-color:transparent;}#mermaid-svg-czaGWLND8ShaE56c .label text,#mermaid-svg-czaGWLND8ShaE56c span{fill:#333;color:#333;}#mermaid-svg-czaGWLND8ShaE56c .node rect,#mermaid-svg-czaGWLND8ShaE56c .node circle,#mermaid-svg-czaGWLND8ShaE56c .node ellipse,#mermaid-svg-czaGWLND8ShaE56c .node polygon,#mermaid-svg-czaGWLND8ShaE56c .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-czaGWLND8ShaE56c .rough-node .label text,#mermaid-svg-czaGWLND8ShaE56c .node .label text,#mermaid-svg-czaGWLND8ShaE56c .image-shape .label,#mermaid-svg-czaGWLND8ShaE56c .icon-shape .label{text-anchor:middle;}#mermaid-svg-czaGWLND8ShaE56c .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-czaGWLND8ShaE56c .rough-node .label,#mermaid-svg-czaGWLND8ShaE56c .node .label,#mermaid-svg-czaGWLND8ShaE56c .image-shape .label,#mermaid-svg-czaGWLND8ShaE56c .icon-shape .label{text-align:center;}#mermaid-svg-czaGWLND8ShaE56c .node.clickable{cursor:pointer;}#mermaid-svg-czaGWLND8ShaE56c .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-czaGWLND8ShaE56c .arrowheadPath{fill:#333333;}#mermaid-svg-czaGWLND8ShaE56c .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-czaGWLND8ShaE56c .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-czaGWLND8ShaE56c .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-czaGWLND8ShaE56c .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-czaGWLND8ShaE56c .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-czaGWLND8ShaE56c .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-czaGWLND8ShaE56c .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-czaGWLND8ShaE56c .cluster text{fill:#333;}#mermaid-svg-czaGWLND8ShaE56c .cluster span{color:#333;}#mermaid-svg-czaGWLND8ShaE56c div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-czaGWLND8ShaE56c .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-czaGWLND8ShaE56c rect.text{fill:none;stroke-width:0;}#mermaid-svg-czaGWLND8ShaE56c .icon-shape,#mermaid-svg-czaGWLND8ShaE56c .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-czaGWLND8ShaE56c .icon-shape p,#mermaid-svg-czaGWLND8ShaE56c .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-czaGWLND8ShaE56c .icon-shape .label rect,#mermaid-svg-czaGWLND8ShaE56c .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-czaGWLND8ShaE56c .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-czaGWLND8ShaE56c .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-czaGWLND8ShaE56c :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 单元测试 节点级
大量·快·确定性
集成测试 图级
中量
端到端 黄金集
中量·慢
混沌 / 对抗
少量·高价值
① 单元测试(节点级) :把每个 node 当纯函数测。create_agent 的扩展机制是中间件(middleware) ------before_model / after_model / wrap_tool_call 等 Hook 把"消息裁剪、历史摘要、工具错误兜底"抽成了可独立单测的单元,这正是 v1 提升可测性的关键设计。
python
def test_router_node():
state = {"messages": [HumanMessage("查下天气")], "remaining_steps": 10}
out = route_node(state)
assert out["next"] in ("research", "action", "casual")
要点:node 里不要有外部副作用(DB 写、网络),用 mock 注入,保证单测快且可重复。
② 集成测试(图级):编译后的图跑通,验证边/条件边/降级分支。
python
def test_deadloop_fallback():
# 构造一个会触发循环上限的输入
res = app.invoke(loop_trigger_input, config={"recursion_limit": 25})
assert "人工介入" in res["messages"][-1].content # 验证降级分支
③ 端到端(黄金集) :见第九节 evaluate 全流程。
④ 混沌 / 对抗测试:主动制造失败,验证韧性。
python
import random
class FlakyTool:
"""故障注入工具:按比例随机抛错,验证 Agent 重试/回退。"""
def __init__(self, fail_rate: float = 0.4):
self.fail_rate = fail_rate
def __call__(self, query: str) -> str:
if random.random() < self.fail_rate:
raise RuntimeError("injected fault: downstream unavailable")
return real_search(query)
# 把 FlakyTool 注入图,跑黄金集,统计 task_success_rate 跌幅
# 跌幅 > 阈值 → 容错设计不合格
专家判断 :绝大多数团队只做 ①②,跳过 ④。但生产环境的工具一定会超时、限流、返回脏数据------混沌测试是唯一能在上线前暴露"容错是纸糊的"的手段。
九、工具链实战:用 LangSmith 构建评估流水线
LangSmith 是 LangChain 生态里把"数据集 + 执行 + 多评估器 + 实验对比"串起来的标准工具。下面是一套可直接落地的流水线。
python
from langsmith import Client, evaluate
client = Client()
def predict(inputs: dict) -> dict:
"""evaluate 要求的 target:输入 dataset 的 inputs,返回 outputs。"""
return run_and_capture(inputs["question"], thread_id=inputs.get("thread_id", "eval"))
# 评估器清单:确定性断言 + 语义 Judge 并行
evaluators = [tool_coverage_assertion, no_deadloop_assertion, trajectory_judge]
experiment = evaluate(
predict,
data="agent_golden_v1", # LangSmith 中的数据集名
evaluators=evaluators,
experiment_prefix="agent-v2.2", # 用于版本间对比
metadata={"model": "gpt-5", "framework": "langchain-1.0", "recursion_limit": 25},
max_concurrency=4,
)
运行后你会得到:每条用例的多维分数、整体均值/分布、与历史 experiment 的 diff。这就是回答"这版比上版好在哪"的硬证据。
接 CI 质量门禁:
python
# 在门禁里读取评估结果,低于阈值则阻断合入
stats = experiment._summary_stats # 或解析 experiment.results
if stats["trajectory_quality"]["mean"] < 0.8:
raise SystemExit("质量门禁未通过:trajectory_quality 均值 < 0.8")
if stats["tool_coverage"]["mean"] < 0.95:
raise SystemExit("工具覆盖不足")
9.1 LangSmith 的三大评估增强
LangSmith 在 v1 周期内(2025 下半年)新增了多项与 Agent 强相关的评估能力,建议直接复用:
- 多轮评估(Multi-turn Evals) :针对"端到端多轮交互"而非单轮输入输出评估。对对话型 / 多步 Agent 尤其关键------它能评估整段会话是否最终达成目标,而非只看最后一句。
- 组合评估器(Composite Evaluators,需
langsmith>=0.4.29) :把多个评估器的分数按权重合成单一分数(加权平均 / 加权和)。在 UI 中可对数据集或 Tracing Project 配置;也可在 SDK 中用自定义聚合函数把tool_coverage/bounded_steps/trajectory_quality合成一个"综合质量分"用于门禁。 - 预置评估器 & LangSmith Engine:内置 Trajectory / Security / Quality / Safety 等模板,工作区级复用;LangSmith Engine 还会自动监控生产 trace、检测问题并给出修复建议。
补充工具选型:
| 场景 | 工具 | 说明 |
|---|---|---|
| Agent/LLM 全流程 | LangSmith | 数据集 + 多评估器 + 实验对比 + 多轮评估 + 组合评估器,LangChain 生态零成本 |
| RAG 子模块质量 | Ragas | Context Precision/Recall、Faithfulness 等指标 |
| 独立 Agent 评测 | DeepEval / Giskard | 内置大量 agentic 评估指标与对抗测试 |
| 本地无平台 | 自定义 evaluate 脚本 | 用 init_chat_model(...).with_structured_output(...) 当 Judge,JSON 存结果,门槛低 |
十、在线评估与持续监控闭环
离线评估是"发布前快照",在线评估才是"真实体温"。二者必须形成闭环:
#mermaid-svg-XO71Pc186CMU98qT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XO71Pc186CMU98qT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XO71Pc186CMU98qT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XO71Pc186CMU98qT .error-icon{fill:#552222;}#mermaid-svg-XO71Pc186CMU98qT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XO71Pc186CMU98qT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XO71Pc186CMU98qT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XO71Pc186CMU98qT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XO71Pc186CMU98qT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XO71Pc186CMU98qT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XO71Pc186CMU98qT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XO71Pc186CMU98qT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XO71Pc186CMU98qT .marker.cross{stroke:#333333;}#mermaid-svg-XO71Pc186CMU98qT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XO71Pc186CMU98qT p{margin:0;}#mermaid-svg-XO71Pc186CMU98qT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XO71Pc186CMU98qT .cluster-label text{fill:#333;}#mermaid-svg-XO71Pc186CMU98qT .cluster-label span{color:#333;}#mermaid-svg-XO71Pc186CMU98qT .cluster-label span p{background-color:transparent;}#mermaid-svg-XO71Pc186CMU98qT .label text,#mermaid-svg-XO71Pc186CMU98qT span{fill:#333;color:#333;}#mermaid-svg-XO71Pc186CMU98qT .node rect,#mermaid-svg-XO71Pc186CMU98qT .node circle,#mermaid-svg-XO71Pc186CMU98qT .node ellipse,#mermaid-svg-XO71Pc186CMU98qT .node polygon,#mermaid-svg-XO71Pc186CMU98qT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XO71Pc186CMU98qT .rough-node .label text,#mermaid-svg-XO71Pc186CMU98qT .node .label text,#mermaid-svg-XO71Pc186CMU98qT .image-shape .label,#mermaid-svg-XO71Pc186CMU98qT .icon-shape .label{text-anchor:middle;}#mermaid-svg-XO71Pc186CMU98qT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XO71Pc186CMU98qT .rough-node .label,#mermaid-svg-XO71Pc186CMU98qT .node .label,#mermaid-svg-XO71Pc186CMU98qT .image-shape .label,#mermaid-svg-XO71Pc186CMU98qT .icon-shape .label{text-align:center;}#mermaid-svg-XO71Pc186CMU98qT .node.clickable{cursor:pointer;}#mermaid-svg-XO71Pc186CMU98qT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XO71Pc186CMU98qT .arrowheadPath{fill:#333333;}#mermaid-svg-XO71Pc186CMU98qT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XO71Pc186CMU98qT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XO71Pc186CMU98qT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XO71Pc186CMU98qT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XO71Pc186CMU98qT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XO71Pc186CMU98qT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XO71Pc186CMU98qT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XO71Pc186CMU98qT .cluster text{fill:#333;}#mermaid-svg-XO71Pc186CMU98qT .cluster span{color:#333;}#mermaid-svg-XO71Pc186CMU98qT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XO71Pc186CMU98qT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XO71Pc186CMU98qT rect.text{fill:none;stroke-width:0;}#mermaid-svg-XO71Pc186CMU98qT .icon-shape,#mermaid-svg-XO71Pc186CMU98qT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XO71Pc186CMU98qT .icon-shape p,#mermaid-svg-XO71Pc186CMU98qT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XO71Pc186CMU98qT .icon-shape .label rect,#mermaid-svg-XO71Pc186CMU98qT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XO71Pc186CMU98qT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XO71Pc186CMU98qT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XO71Pc186CMU98qT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生产环境
Trace 采样
LLM Judge 批量评估
异常归因
回归测试集更新
下一轮离线评估
告警 / 人工抽检
落地要点:
- 采样评估:全量 Judge 太贵,按 1%~5% 采样 + 高风险 case(长轨迹/高成本/低置信)全量评估。
- 用户信号回流:点赞/点踩、人工修正、中途退出,都是强标签,直接进回归集。
- 告警联动可观测性:当某节点成功率或轨迹质量周环比下跌超阈值,自动告警并定位到具体节点(与可观测性体系打通)。
- 流式监控节点名变更 :若用
app.stream(..., stream_mode="updates")实时观测轨迹,注意 v1 中模型节点名已由旧版的agent改为model------监控订阅与日志解析需同步更新。
总结
Agent 的可靠性是测出来的,不是调出来的。本文围绕"把概率系统的质量翻译成可比较、可回归、可门禁的数字"这一核心,给出一套分层、多维、可落地的评估与测试体系:
- 评估为什么难:非确定性、路径组合爆炸、软故障(自信地错)、多目标冲突,使传统"输入确定→断言相等"的测试彻底失效。
- 三层时间线:离线(单元/集成/端到端/回归)、在线(Trace 采样 + Judge 监控 + 用户反馈)、韧性(混沌/对抗注入),缺一不可。
- 五个维度:任务层、轨迹层、质量层、成本层、安全层------答案对不对只是冰山一角,一个"看起来对"的 Agent 可能走了 23 步、烧了 8 万 Token。
- 三类方法论:① 可执行断言(零成本确定性评估,作第一道防线);② LLM-as-Judge(结构化输出 + 写死 rubric + 参考轨迹比对,需黄金校准集防偏);③ 参考轨迹对比与回归(固化历史故障,防"改好 A 坏 B")。
- 工程闭环:以 LangSmith 串起数据集 + 多评估器 + 实验对比 + CI 质量门禁,线上线下形成"采样评估 → 用户信号回流 → 异常自动进回归集"的持续优化闭环。
落到实践:上线前用一份覆盖功能/边界/回归/安全的黄金数据集,配确定性断言兜底 + 校准过的 Judge + CI 门禁,缺一项就是风险敞口。
本文为 Agent 工程化系列之一,聚焦评估与测试,全部示例基于 LangChain 1.0 / LangGraph 1.0。数据集构建、Judge 校准、混沌工程的具体踩坑可继续深入交流。