模块五的收官篇看过了前沿形态:Deep Research 一跑几十分钟,Computer Use 直接上手操作浏览器。形态越炫,一个朴素的追问越响——它凭什么进生产?demo 的标准是"当着你的面成功一次",生产的标准是"没人盯着也天天对"。从 demo 到生产,中间隔着三道质检:调试靠 trace,质量靠 evals,成本靠监控。
本篇是《AI Agent 开发》系列第 27 篇,第六模块"生产化"开篇,工具主线仍是第 2 篇开通过的 LangSmith(LangChain 官方 SaaS,2026-10 上线了新的自助定价),代码基于 langsmith SDK 与 openevals、agentevals 两个官方评估包(2026-10 基线)。全文按"为什么观测 → 深读 trace → 数据集与回归 → 评估器选型 → 判官的坑 → CI 门禁 → 成本监控"推进,依赖一条命令装齐:uv add langsmith openevals agentevals。
1、Agent 为何更依赖观测
传统软件是确定性的:同样的输入走同样的分支,单测覆盖住路径,bug 就能复现、定位、回归。Agent 破坏了这个前提——模型输出是概率性的,同一个问题问三遍可能得到三种工具选择。更麻烦的是三个变量随时在动:模型供应商悄悄更新权重、你改了一行提示词、某个工具的返回格式变了,任何一项都足以让上周稳定的 Agent 这周开始抽风。
所以 Agent 的"测试"要换思路:单次执行的输赢说明不了什么,要看统计意义上的质量;而统计的前提,是每一次执行都留下完整现场。观测由此分成三层:
| 层 | 回答的问题 | 手段 |
|---|---|---|
| trace | 这一次执行到底发生了什么 | LangSmith 追踪树 |
| 数据集统计 | 改动之后质量涨了还是跌了 | evals 回归实验 |
| 成本监控 | 一次会话、一天烧掉多少钱 | usage 与成本聚合 |
trace 解释个案,evals 证明趋势,成本决定这个 Agent 能不能开着跑。 三层缺一不可:没有 trace,evals 分数下跌了你不知道为什么;没有 evals,你只能靠用户投诉发现质量滑坡;没有成本监控,一个失控循环一夜能烧掉一个月预算。
不想把数据送 SaaS 的团队,开源侧有 Langfuse 与 Arize Phoenix 两个成熟的自托管替代,本文的概念与流程全部平移,下文以 LangSmith 为例。
2、深读 trace 树形结构
第 2 篇配好 LANGSMITH_TRACING=true 后,每次执行自动上报成一条 trace。会开开关不等于会读现场,先讲树形结构的读法。LangGraph Agent 的 trace 分四层:最外层是图 run(一次完整执行的根节点),往下是节点 run(agent 节点、tools 节点),节点里挂着模型 run 与工具 run,用了子图或多 Agent 还会再嵌套一层。每层都带输入输出、耗时、token 消耗与错误信息,点的层级越深,细节越多。
定位"哪一步出错",推荐一套固定流程:先看根节点的最终输出,把错误归类,再按类下钻:
| 症状 | 下钻位置 | 常见根因 |
|---|---|---|
| 没调工具,答案靠编 | agent 节点里模型 run 的输入 | prompt 被长对话稀释、工具描述含糊 |
| 调错工具或参数错 | 模型 run 的 tool_calls |
docstring 没写清边界 |
| 工具调对了答案还错 | ToolMessage 与下一轮输入 |
工具返回异常或上下文被截断 |
这套流程的本质,是把"Agent 出错了"翻译成"某一层 run 的哪个字段不对"。翻译完成,修复方向自然浮出。
trace 除了看,还能打标。给坏 trace 打上标记,就是后面数据集的进货渠道,SDK 一行搞定:
from langsmith import Client
client = Client()
# run_id 从 trace 详情页的 URL 里复制
client.create_feedback(
run_id="0cf9d8a1-7f2b-4b1e-9c3a-5e8f1a2b3c4d",
key="human-review",
score=0, # 0 差评 / 1 好评
comment="检索回来的文档和问题无关,答案疑似编造",
)
打完标的 trace 在 UI 里按 feedback 筛选,攒下的差评现场就是下一节评测集的第一批原料。量大了可以用 Annotation Queue 把待标注的 trace 排队分给团队成员,标注流程就产品化了。
3、数据集与实验回归
单条 trace 是个案。要知道一次改动是改良还是劣化,得把真实 case 固化成数据集,改动前后各跑一遍对比分数——这套流程就是 evals,本质是给 Agent 写回归测试。数据集由若干 example 组成,每条 example 是两个 dict:inputs 是给 Agent 的输入,outputs 是参考答案(reference outputs),供评估器对比用。
用第 2 节攒下的坏 case 建集,核心两步:
from langsmith import Client
client = Client()
dataset = client.create_dataset(dataset_name="客服 Agent 回归集")
client.create_examples(
dataset_id=dataset.id,
examples=[
{
"inputs": {"question": "我的订单 8823 到哪了"},
"outputs": {"tools": ["get_order_status"], "answer": "报出物流状态"},
},
{
"inputs": {"question": "你好,在吗"},
"outputs": {"tools": [], "answer": "直接寒暄,不调任何工具"},
},
],
)
字段名完全自定义:这里的 tools 写期望的工具调用序列,answer 写参考答案,评估器按同名字段取值。批量写法需要 langsmith 0.3.13 以上,装最新版即可。
数据集就位,写一个目标函数把被测 Agent 包进去,交给 evaluate 跑实验:
def agent_target(inputs: dict) -> dict:
result = agent.invoke({"messages": [("user", inputs["question"])]})
called = [
c["name"]
for m in result["messages"]
if getattr(m, "tool_calls", None)
for c in m.tool_calls
]
return {"answer": result["messages"][-1].content, "tools": called}
experiment = client.evaluate(
agent_target, # 被测系统:进 inputs,出 outputs
data="客服 Agent 回归集", # 数据集名或 id
evaluators=[tool_match, answer_quality], # 评估器,下一节展开
experiment_prefix="gpt-5-mini",
max_concurrency=4, # 并行跑例,加速回归
)
agent_target 里那段推导把轨迹里实际调过的工具名抽出来,与参考答案同字段对齐——评估比的是结构化字段,不是整段自由文本,字段对不齐,评估器再准也没用。跑完 LangSmith 生成一条 experiment 记录,改一版提示词再跑一条,UI 里两条实验并排,分数与成本全部可视。旧教程里的 run_on_dataset 是这个方法的历史名字,新代码直接用 evaluate,参数细节见 LangSmith 评估指南。
4、评估器选型
评估器决定"什么算对",是 evals 的核心。三类按成本与可靠度排开:确定性判断(精确匹配、包含判断)便宜且完全可复现;LLM 判官灵活但有方差;轨迹评估评判工具调用序列,是 Agent 特有的维度。
4.1、精确与包含判断
评估器就是普通函数,签名固定收三个 dict,返回布尔值:
def tool_match(inputs: dict, outputs: dict, reference_outputs: dict) -> bool:
return outputs["tools"] == reference_outputs["tools"]
def answer_contains(inputs: dict, outputs: dict, reference_outputs: dict) -> bool:
return reference_outputs["answer"] in outputs["answer"]
inputs 是这条 case 的输入,outputs 是 Agent 实际输出,reference_outputs 是数据集里的参考答案。这两个评估器零 token 成本、结果完全确定——只要能写成明确规则(工具白名单、答案必含某关键词),就优先用它。
4.2、LLM 判官
答案开放、没法写规则时,请一个模型当裁判。openevals 仓库提供工厂函数:
from openevals.llm import create_llm_as_judge
from openevals.prompts import CORRECTNESS_PROMPT
answer_quality = create_llm_as_judge(
prompt=CORRECTNESS_PROMPT, # 内置评分标准,可换自定义 rubric
feedback_key="correctness", # 实验结果里的分数列名
model="openai:gpt-5-mini", # 判官模型
)
工厂返回的评估器与上一节的自定义函数同构,直接塞进 evaluators 列表即可。判官拿到的同样是 inputs、outputs、reference_outputs 三段,按 prompt 里的标准打分。判官模型选便宜的就行,但有一条禁忌:别用被测模型自评,原因下一节讲。
4.3、轨迹评估
最终答案对了,不代表过程对:Agent 可能绕了十步冤枉路,也可能调用了不该碰的写工具。这类问题只有轨迹评估抓得住——被评对象不是答案,是整条消息轨迹(含 tool_calls)。agentevals 仓库提供判官版与确定性版两条路,先看判官版:
from agentevals.trajectory.llm import (
create_trajectory_llm_as_judge,
TRAJECTORY_ACCURACY_PROMPT,
)
trajectory_judge = create_trajectory_llm_as_judge(
prompt=TRAJECTORY_ACCURACY_PROMPT,
model="openai:gpt-5-mini",
)
# outputs 传实际消息轨迹,reference_outputs 传期望轨迹(可省略)
verdict = trajectory_judge(outputs=messages, reference_outputs=expected)
判官版按"工具用得对不对、顺序合不合理"给轨迹打分,不带参考轨迹也能判"合理但不完美"这类相对结论。要零成本就换 create_trajectory_match_evaluator,四种模式 strict、unordered、subset、superset 确定性比对工具调用序列:subset 要求实际调用是参考序列的子集,少调可以、名单外的工具一个不许碰;superset 相反,参考工具必须全调到、多调不究。模式细节见轨迹评估指南。
三类对比一张表收口:
| 评估器 | token 成本 | 可复现性 | 适用场景 |
|---|---|---|---|
| 精确/包含 | 零 | 完全确定 | 有白名单或标准答案 |
| LLM 判官 | 每例一次调用 | 有方差 | 开放式回答质量 |
| 轨迹评估 | 零或一次调用 | 分模式 | 过程对不对、路绕不绕 |
5、LLM 判官的偏差与缓解
LLM 判官把打分外包给了模型,模型自带的偏差也一起进场。三个最常见:
- 位置偏差:比较两个候选答案时,偏向先出现的那个
- 自我偏好:判官偏爱与自己风格一致的输出,同模型自评显著偏高
- 评分漂移:同一个答案今天 4 分明天 3 分,温度与模型版本更新都会移动标尺
判官不是免费的午餐:它把"评估输出"的问题,换成了"评估判官"的问题。
对应的缓解手段,按性价比排:
- 换位打分:A/B 两个顺序各判一次,结论矛盾就判平局,位置偏差即被中和
- 锚定评分标准:rubric 里给最低分与最高分各配一个具体示例,分数贴着锚点走
- 固定判官:锁模型版本、
temperature=0,判官不随被测系统升级 - 分数粗档化:五分制漂移明显,改 0/1 两档或 0/0.5/1 三档
还有终极一招:定期抽 20 条人工标注,算判官与人的一致率,一致率跌破阈值就修 rubric 或换判官。判官这把尺子,本身也需要校准——不然你就是拿一支没人校过的尺子在量所有改动。
6、evals 进 CI 门禁
回归集建好、评估器选定,最后一步是把 evals 挂进 CI:提示词或模型配置的改动提 PR,流水线自动跑回归,分数跌破阈值就挡合并。质量保障从"上线后救火"前移成"合并前把关"。
LangSmith 提供了 pytest 集成:@pytest.mark.langsmith 把测试执行自动上报成 trace,t.log_outputs 把断言现场挂上去。
import pytest
from agent import agent # 第 2 篇 create_agent 的产物
from langsmith.testing import t
CASES = [
("我的订单 8823 到哪了", ["get_order_status"]),
("你好,在吗", []),
]
@pytest.mark.langsmith
@pytest.mark.parametrize("question,expected_tools", CASES)
def test_tool_selection(question, expected_tools):
result = agent.invoke({"messages": [("user", question)]})
called = [
c["name"] for m in result["messages"]
if getattr(m, "tool_calls", None) for c in m.tool_calls
]
t.log_outputs({"answer": result["messages"][-1].content, "tools": called})
assert called == expected_tools
断言就是门禁,但阈值要分类设:确定性判断(如工具选择)要求 100% 通过,零容忍;LLM 判官分数有方差,设 0.85 这类下限留余量;轨迹判官建议先只记录不阻断,观察两周稳定后再收紧。测试失败时,t.log_outputs 挂的结果在 LangSmith 里点开就是完整现场,不用去服务器翻日志。
7、成本监控
第三道质检管钱。Agent 的成本结构天然带放大器:每次工具回传都把全量历史重发模型,轮次一多,token 消耗超线性增长——第 2 篇提过的失控循环账单,就是这台放大器失控的样子。
监控闭环至少三个口径。单 trace 成本:模型 run 上记着 usage,输入输出 token 各多少,LangSmith 按模型单价折算成成本,点开 trace 就能看到这次执行花了多少钱,给它设上限告警防失控。日均与环比:Dashboard 按天、按模型聚合 token 与美元,看趋势比看绝对值重要。单位任务成本:每千次请求的美元数,是与业务规模挂钩的核心指标,QPS 翻倍它不该跟着翻倍。
监控的终局是给优化提供证据。最常见的降本动作是模型路由:主干任务用小模型,难题升级到大模型。这个决策别拍脑袋,跑实验拿数据——同一数据集、两版模型各跑一次 evaluate,metadata 里写上模型名(UI 会填进实验对比列),分数与成本并排出现。小模型分数不掉、成本降七成,这才是换的依据;分数掉两个点省三成成本,多半是亏的。evals 不只守质量,也是每一笔降本决策的裁判。
8、总结
三道质检到此闭环:trace 把"Agent 出错了"翻译成"某一层 run 的哪个字段不对";evals 用真实 case 的回归实验证明每次改动的方向;成本监控守住单 trace、日均、单位任务三条线。从这一篇起,改任何提示词、换任何模型,都应该有"跑一遍回归再合并"的肌肉记忆。
留一个练习:给第 17 篇的 Agentic RAG 建一个 10 条 case 的评测集——5 条该检索(含一条需要查询改写的)、3 条不该检索的闲聊与常识题、2 条多跳问题;outputs 同时写期望工具序列与参考答案,分别配 subset 轨迹比对与 LLM 判官,跑完记下单例平均成本。这份评测集会一直陪你到第 30 篇的收官实战。
下一篇讲安全与护栏:Prompt Injection 怎么防、工具权限怎么最小化。对于正在把 Agent 推向生产的读者,建议本周就动手:先把 trace 常开,再攒够十条真实差评 case——评测集是长出来的,不是憋出来的。


