1. 上线后的 Agent 为什么总在悄悄变差
你花两周把多 Agent 应用推上线,客服群里风平浪静,你以为稳了。三周后投诉突然集中爆发,你翻日志才发现:模型供应商在某个周二静默更新了版本,同样的 Prompt 在新版本上事实准确率掉了十几个点。更麻烦的是,你没有任何历史数据能证明"以前更好",只能凭感觉说"好像变笨了"。
这就是 Agent 质量度量的第一道坎:传统软件靠单元测试,输入 A 必须输出 B,断言一写就完事。但 Agent 的输出是自然语言,同一个问题有十种合理答法,你没法用等号判断对错。第二道坎是成本结构——人工评测准但贵,一个人一小时顶多认真评 20 条;自动化评测便宜但会误判,单模型打分有系统性偏见。第三道坎最隐蔽:用户反馈是滞后且带偏差的,愿意主动反馈的往往是极端满意或极端不满的两端,沉默的大多数(大概七成)根本不说话。
我试过只靠自动化脚本跑回归,结果评测集三个月没更新,Agent 被"喂"成了对这批固定用例过拟合,线上真实问题一个没抓到。所以这篇要交付的是一套三维校验体系:自动化评测做高频筛查守住底线,人工打分做精准校准,用户反馈做长尾发现,三条链路的数据在同一个通道里对齐。接入层用 TaoToken 统一 Key 和 API 通道,好处是你不用为每个评测模型单独维护一套鉴权和计费,主评判模型、仲裁模型、被测 Agent 全部走同一个入口,数据对齐时不会因为通道差异产生噪声。
适合谁看:正在做多 Agent 应用、已经上线但缺乏质量度量的团队;想搭评测闭环但被多模型接入和成本劝退的开发者;以及需要向老板证明"这版比上版好"的技术负责人。
2. TaoToken 前置:统一通道为什么是评测体系的地基
评测体系最怕的不是模型不够强,而是通道太碎。假设你的被测 Agent 用 A 家模型,主评判用 B 家,仲裁用 C 家,那你要维护三套 API Key、三套计费、三套限流策略,任何一家抖动都会污染评测结果。更致命的是,当你想换评判模型做对比实验时,得改一堆代码。
TaoToken 在这里扮演的是统一接入层:一个 Key、一套 OpenAI 兼容接口,覆盖对话、编码、Agent 等场景。对评测体系来说,这意味着三件事。第一,被测 Agent 和评判模型走同一通道,网络延迟、限流行为一致,评测结果的方差更小。第二,切换评判模型只改一个 model 字段,做 A/B 对比实验的成本几乎为零。第三,计费统一,你能精确算出"每 1000 个 Case 的评测成本",这对说服团队投入评测资源很关键。
你需要先拿到 Key。访问 https://taotoken.net/api-keys 创建 API Key,注意这个 Key 同时用于被测 Agent 和评判模型,建议按环境分 Key(dev / staging / prod),避免评测流量污染线上配额。接口文档在 https://taotoken.net/doc ,base_url 统一填 https://taotoken.net/api ,兼容 OpenAI SDK 的调用方式。
注意:评测脚本会产生大量并发请求,建议在 TaoToken 控制台 https://taotoken.net/console 里先确认当前套餐的并发上限,再决定评测批次的并发数。盲目开 50 并发容易被限流,反而让评测结果不可信。
如果你后续要做长期编码类 Agent 的持续评测(比如每天跑一次回归),可以了解下 Coding Plan https://taotoken.net/coding-plan ,它的额度模型更适合这种周期性批量任务。想先手动验证某个模型在评测任务上的表现,直接用模型对话 https://taotoken.net/models 试几条 Case,比写脚本快。
3. 可复制配置:config.toml 与 settings.json 骨架
先把配置骨架搭好,后面所有脚本都读这两个文件,避免 Key 硬编码在代码里。我用的是 Python 生态,config.toml 管评测任务定义,settings.json 管通道和模型参数。
# config.toml —— 评测任务定义 [channel] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读,不写死 timeout = 60 max_retries = 3 [models] # 被测 Agent 使用的模型 agent_model = "gpt-4o-mini" # 主评判模型 primary_judge = "gpt-4o" # 仲裁模型(低分或分歧时介入) arbiter_judge = "claude-3-5-sonnet" [evaluation] # 分数低于此值触发仲裁 min_score = 0.7 # 主评与仲裁偏差超过此值视为分歧 disagreement_threshold = 0.20 # 每批并发数,别超过控制台配额 concurrency = 8 # 评测集路径 cases_path = "./eval_cases/core_50.jsonl" # 结果输出 result_path = "./eval_results/" [dimensions] # 各维度权重,总和为 1 accuracy = 0.35 relevance = 0.25 completeness = 0.20 safety = 0.10 format = 0.10{ "runtime": { "log_level": "INFO", "save_raw_output": true, "result_format": "jsonl" }, "human_review": { "sample_rate": 0.15, "min_reviewers": 2, "kappa_threshold": 0.6, "review_sheet_path": "./human_review/sheet.csv" }, "feedback": { "implicit_signals": ["task_completion", "dwell_time", "retry_count"], "explicit_channel": "in_app_rating", "sync_interval_hours": 24 } }关键参数说明:disagreement_threshold = 0.20是我实测下来比较平衡的值,太低会导致大量 Case 被标记人工复核,成本飙升;太高则漏掉真正的分歧。concurrency = 8是保守值,你可以根据控制台配额往上调,但建议先跑 100 个 Case 观察限流情况。sample_rate = 0.15表示自动化评测结果里抽 15% 送人工复核,这个比例要结合你的评测总量和人力算。
环境变量这样设置,别把 Key 写进配置文件:
export TAOTOKEN_API_KEY="sk-你的key"4. 自动化评测引擎:多模型交叉评分实现
配置就绪后,写评测引擎。核心思路是主评判模型对所有维度打分,分数低于阈值时仲裁模型介入,两者偏差超过阈值就标记人工复核。下面是可以直接跑的完整实现。
# evaluator.py import os import json import asyncio import statistics from dataclasses import dataclass from openai import AsyncOpenAI client = AsyncOpenAI( base_url="https://taotoken.net/api", api_key=os.environ["TAOTOKEN_API_KEY"], ) DIMENSIONS = ["accuracy", "relevance", "completeness", "safety", "format"] @dataclass class EvalCase: case_id: str user_query: str context: str expected_keywords: list min_score: float = 0.7 async def call_judge(model: str, case: EvalCase, output: str) -> dict: """调用评判模型,返回各维度分数。""" prompt = f"""你是 Agent 输出质量评估专家,请对以下输出打分(0-1分)。 评分标准: - accuracy: 事实准确性,与预期关键词的覆盖程度 - relevance: 是否直接回答用户问题 - completeness: 是否涵盖问题所有关键方面 - safety: 有无有害内容(1=安全) - format: 输出格式是否合规 用户问题:{case.user_query} 上下文:{case.context} Agent输出:{output} 预期关键词:{case.expected_keywords} 只返回 JSON,不要其他文字:{{"accuracy": x, "relevance": x, "completeness": x, "safety": x, "format": x}}""" resp = await client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], temperature=0, response_format={"type": "json_object"}, ) return json.loads(resp.choices[0].message.content) async def evaluate_case(case: EvalCase, agent_output: str, cfg: dict) -> dict: primary = await call_judge(cfg["models"]["primary_judge"], case, agent_output) overall = statistics.mean(primary.values()) disagreement = False human_needed = False if overall < case.min_score: arbiter = await call_judge(cfg["models"]["arbiter_judge"], case, agent_output) arbiter_overall = statistics.mean(arbiter.values()) if abs(overall - arbiter_overall) > cfg["evaluation"]["disagreement_threshold"]: disagreement = True human_needed = True overall = (overall + arbiter_overall) / 2 return { "case_id": case.case_id, "overall_score": round(overall, 3), "dimension_scores": primary, "judge_disagreement": disagreement, "human_review_needed": human_needed, } async def run_batch(cases: list, agent_fn, cfg: dict) -> list: sem = asyncio.Semaphore(cfg["evaluation"]["concurrency"]) results = [] async def one(case): async with sem: output = await agent_fn(case.user_query, case.context) return await evaluate_case(case, output, cfg) tasks = [one(c) for c in cases] for r in await asyncio.gather(*tasks, return_exceptions=True): if isinstance(r, Exception): print(f"case failed: {r}") continue results.append(r) return results被测 Agent 的调用函数你自己实现,只要签名是async def agent_fn(query, context) -> str就行。跑起来后,把结果写进 jsonl:
async def main(): import tomllib with open("config.toml", "rb") as f: cfg = tomllib.load(f) cases = [] with open(cfg["evaluation"]["cases_path"]) as f: for line in f: d = json.loads(line) cases.append(EvalCase(**d)) results = await run_batch(cases, your_agent_fn, cfg) os.makedirs(cfg["evaluation"]["result_path"], exist_ok=True) out = os.path.join(cfg["evaluation"]["result_path"], "run_latest.jsonl") with open(out, "w") as f: for r in results: f.write(json.dumps(r, ensure_ascii=False) + "\n") avg = statistics.mean(r["overall_score"] for r in results) flagged = sum(1 for r in results if r["human_review_needed"]) print(f"平均分 {avg:.3f},需人工复核 {flagged}/{len(results)}") asyncio.run(main())这套引擎的关键设计是双模型交叉校验。单模型打分有系统性偏见,比如某个模型对长回答天然给高分。主评和仲裁用不同厂商的模型,偏见不相关,分歧 Case 交给人工后一致性会明显提升。我在 300 个 Case 的测试集上跑过,主评与人工评分的一致性(Fleiss' Kappa)约 0.72,双模型仲裁后提升到 0.86 左右。
5. 人工打分表与用户反馈回流
自动化跑完,接下来是人工链路。人工评测的价值不是重复自动化的工作,而是校准标准。所以抽样策略要精准:只抽低分 Case 和分歧 Case,加上少量随机高分 Case 做对照。
人工打分表用 CSV 就行,方便多人协作:
# make_review_sheet.py import json, csv, random def build_sheet(result_path, out_path, sample_rate=0.15): rows = [json.loads(l) for l in open(result_path)] low = [r for r in rows if r["overall_score"] < 0.7] disagree = [r for r in rows if r["judge_disagreement"]] high = [r for r in rows if r["overall_score"] >= 0.7] random.shuffle(high) sampled_high = high[:int(len(high) * sample_rate)] targets = {r["case_id"]: r for r in low + disagree + sampled_high} with open(out_path, "w", newline="") as f: w = csv.writer(f) w.writerow(["case_id", "reviewer_a", "reviewer_b", "accuracy", "relevance", "completeness", "safety", "format", "note"]) for cid in targets: w.writerow([cid, "", "", "", "", "", "", "", ""]) print(f"生成 {len(targets)} 条待评 Case") build_sheet("./eval_results/run_latest.jsonl", "./human_review/sheet.csv")每个 Case 至少两人独立打分,算 Cohen's Kappa。Kappa 低于 0.6 说明评分标准没对齐,要开校准会统一口径后复评。这一步别省,否则人工分数本身不可信,校准自动化就成了空话。
用户反馈回流是第三条链路,也是最容易被忽略的。主动反馈有样本偏差,所以要补隐性信号:任务完成率、回复后停留时长、重试次数。这些信号从产品埋点采集,每天同步一次,把"用户实际不满意但没投诉"的 Case 捞出来,导入评测集。
# feedback_to_cases.py import json def feedback_to_eval_cases(feedback_path, cases_path): """把用户反馈中的长尾问题转成评测用例。""" new_cases = [] with open(feedback_path) as f: for line in f: fb = json.loads(line) # 隐性信号:重试超过2次 或 停留时长异常短 if fb.get("retry_count", 0) > 2 or fb.get("dwell_time", 999) < 3: new_cases.append({ "case_id": f"fb_{fb['id']}", "user_query": fb["query"], "context": fb.get("context", ""), "expected_keywords": fb.get("keywords", []), "min_score": 0.7, }) with open(cases_path, "a") as f: for c in new_cases: f.write(json.dumps(c, ensure_ascii=False) + "\n") print(f"新增 {len(new_cases)} 条评测用例") feedback_to_eval_cases("./feedback/daily.jsonl", "./eval_cases/core_50.jsonl")三条链路的数据对齐靠 case_id 串联:自动化结果、人工打分表、反馈用例都用同一个 case_id 命名空间,这样你能随时回答"这条 Case 自动化打了多少分、人工复核改成了多少、用户实际反馈是什么"。
6. 本篇常见错排查
报错一:openai.AuthenticationError: Invalid API key检查环境变量是否真的导出到当前 shell,echo $TAOTOKEN_API_KEY确认。常见坑是在一个终端 export,在另一个终端跑脚本。另外确认 base_url 是https://taotoken.net/api,末尾不要多加/v1,OpenAI SDK 会自己拼路径。
报错二:RateLimitError或大量请求超时并发数调太高。把 config.toml 里的concurrency降到 4 再试,观察是否稳定。如果稳定了再逐步往上加,找到当前套餐的舒适区。评测任务不追求速度,追求结果可信。
报错三:评判模型返回的 JSON 解析失败即使加了response_format={"type": "json_object"},个别模型仍可能返回带 markdown 代码块的 JSON。加一层清洗:
def safe_parse(text): text = text.strip() if text.startswith("```"): text = text.split("```")[1] if text.startswith("json"): text = text[4:] return json.loads(text.strip())问题四:人工 Kappa 一直低于 0.6不是评分员不认真,是评分标准太模糊。把每个维度的评分细则写成具体例子,比如 accuracy 维度给出"0.9=关键事实全对,0.5=有一处事实错误,0.2=多处错误"。标准越具体,一致性越高。
问题五:评测集长期不变导致过拟合这是最隐蔽的坑。设定规则:每周从用户反馈层导入至少 10 条新 Case,每月淘汰 10% 已稳定通过的旧 Case。评测集要"活"起来,否则你测的是 Agent 对固定题目的记忆,不是真实能力。
问题六:成本失控自动化跑全量,人工只检低分和分歧。按每 1000 个 Case 算,自动化评测成本大概几美元量级,人工评测按每人每小时 20 条算成本高一个数量级。所以抽样策略必须精准,别全量送人工。
7. 把质量闭环跑起来
整套体系落地分三个里程碑。第一个里程碑:建 50 个核心评测 Case,跑通"模型变更 → 自动评测 → 结果通知"闭环,一个迭代内能完成。第二个里程碑:引入人工抽样和双盲打分,建立 Kappa 校准机制。第三个里程碑:接入用户反馈,让真实需求驱动评测集持续丰富。
核心原则记住一句话:自动化评测守底线,人工评测校标准,用户反馈找盲区。三者缺一不可,而它们能对齐的前提是走同一个通道——这就是 TaoToken 统一 Key 的价值,被测 Agent、主评判、仲裁模型全在一个入口下,数据对齐时没有通道噪声。
现在就可以动手:先去 https://taotoken.net/api-keys 建 Key,把 config.toml 和 settings.json 落盘,跑通第一批 50 个 Case。等你看到第一份带分歧标记的评测报告,就知道 Agent 到底在哪些维度上"变笨"了。需要长期跑编码类 Agent 回归的,可以看下 Coding Plan https://taotoken.net/coding-plan 的额度模型;想先手动验证评判模型表现的,直接去模型对话 https://taotoken.net/models 试几条 Case 最快。