Eval 回答"好不好"并防变坏, Trace 回答"慢/贵在哪" — 两者共用同一套数据, badcase 回流成闭环
Agent 的输出是概率性的: 同一份代码今天跑对、明天可能跑偏——所以它没法像传统软件那样"测试过了就永远对", 只能靠持续体检。Eval 是体检报告: 用固定题库定期测, 分数跌了就拦住上线; Trace 是病历: 每次任务留下带时间的检查记录, 慢在哪贵在哪一翻便知。两者要连成一个圈: 线上翻车案例(病历)沉淀成新考题(题库), 让同样的病第二次发作前就被拦下。
vibe: "感觉还行" # → 无基线, 改坏无人知 eval: pass_rate 93% (基线 92%) # → 每次改动有账可查
unit # → 路由/解析逻辑, mock LLM 快跑 e2e # → 全任务完成率 regression # → 改动前后同题对比, CI 门禁
happy × 20 # 常规流程 hard × 20 # 歧义/超长/多约束/对抗 bad × 10 # 线上翻车复现 — 最珍贵
extract # → 精确匹配, 0 成本 codegen # → 跑测试用例 openQA # → rubric / judge
agree = judge vs human / 50 # → 0.86 上岗; 0.6 的 judge = 掷硬币
agent.run(case, temperature=0) # 错: 温度 1.0 跑 eval → 每次分数随机抖动, 无法对比
if report.pass_rate < baseline - 0.01: sys.exit(1) # → CI 红灯, 合并被拦
span.set_attribute("gen_ai.request.model", model) span.set_attribute("gen_ai.usage.input_tokens", n) # 一套约定, Jaeger/Tempo/Datadog 通吃
agent.run (3.4s)
├─ step1: llm 1.0s + tool.search 0.4s
├─ step2: llm 1.6s # → 重试 +0.9s, 瓶颈cost = (fresh*in_price + cached*in_price*0.1 + out*out_price) / 1e6 # → 月底账单能精确归因到每一次调用
thumbs_down → rebuild_case → dataset.add
# → 修复 PR 必须让新用例转绿, 否则不许合并Counter({'hallucination': 14, 'tool_error:search': 9})
# → 本周主攻幻觉: 加引用强制 + faithfulness 门禁agent 的决策逻辑(先查再答)可以用脚本化 LLM 做确定性断言, CI 里 3 秒跑完。
class FakeLLM: def __init__(self, scripted): self.script = list(scripted) # 预设响应序列 def create(self, **kw): return self.script.pop(0) def test_agent_searches_before_answer(): agent = Agent(llm=FakeLLM([tool_call("search"), text("退款 3 个工作日到账")])) out = agent.run("退款几天到账") assert out.tools_used == ["search"] # 行为: 必须先查
用例放 YAML 进仓库, runner 统一执行, 报告落 JSON 留基线。
# evals/dataset.yaml: 50+ 条(happy/难例/badcase 三源) cases = yaml.safe_load(open("evals/dataset.yaml")) async def run_eval(agent) -> Report: rows = [] for c in cases: out = await agent.run(c["task"], temperature=0) # 可复现 rows.append(judge(c, out)) # 判分器统一入口 return Report(rows) # → pass_rate / 各维度均分 / 耗时
开放式回答没法精确匹配。四个维度打分, 忠实度不达标直接判负。
JUDGE_PROMPT = """你是评审员, 按四个维度打 1-5 分并给理由: ① 事实忠实: 是否只基于给出的资料 ② 完整性: 是否覆盖问题各部分 ③ 格式: 是否符合要求的 JSON 结构 ④ 简洁: 无废话 输出 JSON: {"faith": n, "complete": n, "format": n, "brief": n}""" score = json.loads(judge_llm(JUDGE_PROMPT, answer, ctx)) assert score["faith"] >= 4, "忠实度不达标, 一票否决"
新 judge 上岗前, 拿 50 条人工标注算一致率; 换模型或改提示词后重跑。
agree = 0 for case, human in human_labeled[:50]: agree += (judge(case) == human) rate = agree / 50 assert rate >= 0.85, f"judge 一致率 {rate}, 不允许上岗" # → 0.86 通过; 之后每月抽检一次防漂移
每次 LLM 调用一个 span, 属性按社区约定命名, 任意 APM 直接可用。
with tracer.start_as_current_span("llm.call") as span: span.set_attribute("gen_ai.system", "anthropic") span.set_attribute("gen_ai.request.model", "claude-sonnet-4-5") span.set_attribute("gen_ai.usage.input_tokens", u.input_tokens) span.set_attribute("gen_ai.usage.output_tokens", u.output_tokens) span.set_attribute("gen_ai.request.temperature", 0.2) # trace_id 贯穿: 用户报障 → 一条 trace 还原全程
账单对不上? 缓存命中的输入价格不同。精确到每个任务的费用函数。
PRICES = {"claude-sonnet-4-5": {"in": 3.0, "out": 15.0}} # $/Mtok
def cost_of(model: str, usage) -> float:
p = PRICES[model]
cached = usage.cache_read_input_tokens
fresh = usage.input_tokens - cached
return (fresh * p["in"] + cached * p["in"] * 0.1
+ usage.output_tokens * p["out"]) / 1_000_000
# → 按任务/用户/agent 维度聚合, 账单精确归因
eval 接进 CI, 低于基线 1 个百分点直接退出非零, 合并被拦截。
# .github/workflows/eval-gate.yml - name: eval gate run: python -m evals.run --baseline evals/baseline.json # evals/run.py 内部: if report.pass_rate < baseline.pass_rate - 0.01: print(f"回归: {report.pass_rate:.2%} < {baseline.pass_rate:.2%}") sys.exit(1) # → CI 红灯
用户点踩后, 自动还原现场存成用例。修复 PR 必须让新用例转绿。
async def on_feedback(msg_id: str, thumbs: str): if thumbs != "down": return case = await rebuild_case(msg_id) # 输入/上下文/输出全还原 await dataset.add({"id": f"bad-{msg_id}", **case}) # → 评测集自动长大; 同类翻车的复发率是闭环的 KPI
改 prompt 靠感觉? 同一批数据集跑两版, 数字说话, 权衡延迟后灰度。
r_old = run_eval(agent, dataset, system=SYS_v14) r_new = run_eval(agent, dataset, system=SYS_v15) report.compare(r_old, r_new) # → v15: pass_rate 91→94, 但 p50 延迟 +120ms # 决策: 收益大于代价 → 灰度 10% 观察线上反馈
失败不是均匀分布的。按签名聚类, 每周主攻占比最高的失败模式。
from collections import Counter def cluster(failures) -> Counter: sig = lambda f: f"{f.type}:{f.tool or "-"}" return Counter(map(sig, failures)) # → Counter({'hallucination:-': 14, 'tool_error:search': 9}) # 本周主攻幻觉: 上引用强制 + faithfulness 门禁
# 错: 人工看 3 个例子就上线 # 对: pass_rate 93% vs 基线 92% → 数字验收
# 错: "给这个回答打 1-5 分"(默认偏爱详细) # 对: 分维度打分, brief ≤3 分即扣总分
# 错: 20 条"正常提问"全过 # 对: happy 20 + hard 20 + badcase 10
# 错: 用 gpt-x 生成又用 gpt-x 判 # 对: 生成用 A 家, judge 用 B 家 + 人工抽检
# 错: dataset 本地随便加题 # 对: dataset.yaml + judge prompt 走 PR 评审
# 错: eval 时 temperature=1.0 # 对: eval 强制 temperature=0, 差异才可信
# 错: 只测"路由对不对" # 对: e2e 用例: 10 轮工单处理全流程
# 错: 每个 service 自己起 trace_id # 对: propagate_context() + W3C traceparent 头
# 错: 月账单 $4200, 归因靠猜 # 对: group by agent → researcher 占 62% → 定点优化
# 错: eval 结果发到群里无人看 # 对: sys.exit(1) → branch protection 拦截
# 错: few-shot 例子直接来自评测集 # 对: 例子与用例分仓库管理 + 相似度巡检
# 错: judge_score > 0.6 即通过 # 对: 校准分布 → 取"误放率 <5%"的分位数做阈值
# 错: 只比 pass_rate # 对: pass_rate + p99 + cost/任务 三指标门禁
# 错: 上线后再没人看过真实输出 # 对: 周抽 50 条 → 人工打标 → 对比 judge 漂移
# 错: 10 条跑一遍, 9 比 8 就宣布新版更好 # 对: 50+ 条 + 差异显著性意识
# 错: 通用 rubric: 相关/完整/流畅 三维度 # 对: + 业务项: 引用工单号 / 金额两位小数 / 带时区
# 错: 只盯 mean=4.2 # 对: p05 与 1 分占比一并出门禁
# 错: 修完即忘 # 对: dataset.add(bad_case) + 修复 PR 必须转绿
# 错: e2e 也全 mock, 连 schema 都不对 # 对: e2e 用真实模型 + 沙箱环境
# 错: print("step 3 done, cost 0.02") # 对: log.info(json.dumps({"step": 3, "cost": 0.02, "trace": tid}))