← 全部文章
AI

我们十分钟就能搭一个 agent,为什么还要几周才能交付

所有人都说搭一个 agent 太快了,也都说 agent 上线怎么这么难——两句都对,说的不是同一件事。这篇拆开从 demo 到交付的那几周:先是发现它会怎么坏,再是定出什么叫「做对了」,最后是每改一次都得重新验一次。成本没有降下来,只是从「写」挪到了「验」,挪过去之后还更贵。

35 分钟阅读本站原文
插图:天平一头是十分钟拼起来的小机器,另一头是沉得压到桌面的一摞测试案例
目录

这一年最明显的变化,是搭一个 agent 没那么难了。一个想法,快的十分钟,慢的一下午,demo 就能跑起来。模型、工具、记忆全变成了可以插拔的东西,DeepSeek Harness 和 pi 这类框架把模块化推到了极致,连 agent loop 本身都是 plugin;MCP 当年承诺的"不必再为每个数据源维护单独的连接器",是真的做到了。

但没人只想要一个 demo,大家要的是能稳定交出去的那种 agent。于是另一批东西跟着冒出来:各式各样的 harness,各式各样的 eval,都在回答同一个问题——怎么知道它这次做得对不对。 Anthropic 8月21日发了一份 AI-Native SDLC playbook,第一节标题直接就是 "Code is no longer the bottleneck":写代码不再是瓶颈,瓶颈挪到了它左右两边那些还按人的速度跑的环节。

两头就这么分化了。一边是搭 demo 越来越快,快到几乎不计成本;另一边是"证明它能交付"的门槛越来越高——要测试集,要能复现,要知道它什么时候会坏。所以现在到处都听得见两句话,而很少有人把它们放在一起说:

所有人都说,现在搭一个 agent 太快了。

所有人也都说,agent 上线怎么这么难。

两句都对,说的不是同一件事。看起来做 agent 的成本降下来了,其实只是从一头挪到了另一头,挪过去之后还更贵。 这篇是我读了一批东西、又自己动手试了试之后的整理,只想回答一个问题:从 demo 到交付的那几周,到底在做些什么?


一、模块化到底压掉了什么#

"搭一个 agent"这个说法有个陷阱:它至少指三件不同的事,耗时差两个数量级。

三层的量级大致是这样。这是一个分层框架,不是测量结果——重点在三层之间的数量级差,不在具体数字

层次量级卡在哪
能跑的 demo十分钟~几小时基本不卡了
自己天天用的几天自己能人工兜底,容忍度高
交付给别人用的几周~几个月不能兜底:要 eval、要知道它什么时候坏

说"太快了"的人在说第一层,说"太难了"的人在说第三层。他们没有分歧,只是没在同一层对话。

我理解的 DSH 和 Pi 的模块化压掉的是最上面那层。 MCP 之前接一个工具要自己写集成、认证、分页、格式解析,现在整个工具就是一个装饰器:

# server.py —— 挂上去只要一条命令:
#   claude mcp add my-tools -- python server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP(name="my-tools")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Return the sum of a and b."""
    return a + b

函数签名自动变成 JSON Schema,docstring 变成给模型看的描述,协议、校验、传输一概不用管。

DeepSeek Harness 把同样的思路推进了 runtime 内部。它底层的 Cordis 里,一个工具就是一个插件:

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']        // 声明依赖:tools 服务就绪后才启动

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet the named person.',
    parameters: {
      name: { type: 'string', required: true, description: 'Who to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

关键是 inject 那一行。ctx.tools 是一个服务,而 ctx.llmctx.sessions同样形状的服务——这就是 "Everything is Plugin" 的字面意思:模型和会话跟工具占的是同一类插槽,注册和卸载走同一套生命周期。

这一层是真的被重构了。它压的是"几小时到十分钟",不是"几周到几天"。


二、那几周在做什么#

一个 agent 从"我自己能用"到"能交给别人用",中间那几周大致分三段,一段比一段慢:先是发现它会怎么坏,再是定出什么叫"做对了",最后是每改一次都得重新验一次。

发现它会怎么坏#

第一个撞上的,往往不是它做错了什么,是它根本没做,却告诉你做完了。我实践时手里有个项目要把实现委托出去,headless 跑,每个任务一个独立 worktree,跑完我再 review。第一批回来,进程退出码 0,输出的 JSON 里写着 "subtype": "success"。看上去挺好。

翻开同一条记录的 payload:

{"is_error": true, "api_error_status": 402,
 "result": "API Error: 402 Insufficient Balance"}

账户余额不足。一个工具调用都没跑,worktree 空空如也。

退出码 0、subtype: successis_error: true 这三样在同一条记录里同时成立,而我只看了前两样就以为任务跑完了。

这类失败的特征是:一个"什么都没发生"的信号,被读成了"发生了,而且没问题"。 换到 agent 内部就是另一副经典模样:某个工具返回了 [],agent 把它当成"确认没有结果",继续往下走,最后给出一个建立在空数据上的结论。

OK 先修复这个问题。然后我又撞上第二个:上下文接近上限,压缩策略把中段某条约束丢了。修复完再撞上第三个、第四个。根据长尾效应,这其中相当一部分很难在设计阶段穷举,只能在真实任务里逐个暴露。 撞到一定数量会意识到不能这么下去,得有个能回归的东西。

定出什么叫"做对了"#

于是坐下来把撞过的坑一条条写成案例。写到第三条就会卡住,因为每条案例都要配一个"期望输出",而期望输出这一栏经常填不出来。

工具返回 [] 那条好写,期望是"停下来问,而不是往下编",机器能判。但真实任务里的多数案例长这样:这份调研整理得全不全?这个总结抓住重点了吗?这次该追问用户还是该直接给方案?你知道哪个答案更好,但你写不出一条机器能执行的判据。

写不出判据的案例有三条出路,没有一条便宜:降级成"人工看一眼",于是它不能进 CI;换成 LLM judge,于是你多了一个同样需要评测的东西;或者干脆把它排除在案例集之外,于是你的通过率只覆盖了好判的那部分。

多数团队选了第一条。 LangChain 的《2026 State of Agent Engineering》问了 1300 多位从业者,人工评审占 59.8%,至今仍是最主流的评测方式——不是大家不想自动化,是有相当一部分东西没法自动判。

每改一次,重新验一次#

案例攒到二十个,改一句 prompt,跑一遍,五分钟。二十个里挂了三个。

问题是你不知道这三个该算谁的。可能是这句 prompt,可能是上周改的那个工具描述,也可能什么都不是——同一批案例昨天跑就挂了两个。于是控制变量再跑一遍确认,又五分钟;为了确认不是抖动,再跑三遍,十五分钟。

所以一个能拿去下结论的判断,起步价是二十分钟——五分钟一轮,得跑上三四轮才敢说这次改动确实有效。而改一行传统代码跑单元测试是两百毫秒。

几周就是这么过去的。不是没有代码要写——trace、重试、幂等、降级、权限,一样都少不了。但决定进度的已经不是"把功能接起来",而是发现失败、建立判定标准、反复验证。

怎么分成这三段是我自己的走法,行业里没有一个公认的分法。但被分的那件事本身,有两份互不相干的调查在说同一句话。

上面那份 LangChain 调查还给了一组很别扭的对比:89% 的团队装了可观测性,但只有 52.4% 跑离线评测、37.3% 跑线上评测。 整体有 29.5% 根本不做任何评测;即便在已经把 agent 放上线的团队里,这个数也还有 22.8%。工具都装上了,却没用来判断对错。今年 CHI 上有一篇论文(van der Maden 等,Results-Actionability Gap)访谈了 19 位在生产环境做 LLM 产品的从业者,横跨医疗、法律、教育、企业软件;它转引的另一项针对微软团队的研究(Nahar 等,26 次访谈 + 332 份问卷)给了两个数字——团队 76.6% 的精力花在手工测试上,只有 36.3% 有像样的评测机制。这两个数字量的是精力去向,不是上面那三周的时间分解,但它们能说明那份精力确实压在"验"这一侧,而不是"建"这一侧。


三、为什么会这样#

前两章是两句陈述:搭得越来越快,交得越来越慢。 这一章算的是这两句之间的差。

差不在"agent 更复杂"这种笼统的地方。它起于一件很具体的事:难的那一半,从"写"挪到了"验"。 挪过去之后,验证的成本从三个方向同时往上涨;涨到最后,先撑不住的是同一样东西。

实现变便宜了,验证没有#

搭 demo 难在实现——把模型、工具、循环接起来跑通。交付难在验证——知道它什么时候会坏。

传统软件里,验证能借力于代码实现结构:type、flow、error handling、if-else,这些都能帮你从代码推出相当一部分行为。分布式和并发系统当然也有大片不可预测的地方,但 code review 和一系列各代码层级的测试至少能覆盖住那块确定性的表面。

但 Agent 把这条路收窄了很多。因为其中的关键行为——它会不会选对工具、会不会在第 x 步开始编、会不会把 context 中的约束丢掉——很难从实现里直接推出来。

这个 bug 在传统程序里几乎不会发生——[]throw 是两条完全不同的控制流,编译器和类型系统会逼你分开处理。但在 agent 里,tool result 被序列化成字符串拼进消息历史。[]null{"error": "rate limited"},在模型眼里都是文本,没有类型边界,也没有异常传播。 它读到什么,就按什么往下推理。

框架降低了 agent 的实现成本,却没有同比降低"证明它可靠"的成本。

连"跑两遍一样"都不成立#

常见解释是"浮点不满足结合律 + GPU 并发调度"。但 Thinking Machines Lab 的《Defeating Nondeterminism in LLM Inference》证明这个解释是错的——重复跑同一个矩阵乘法,结果逐位相同,并发和浮点都不是元凶。

真正的原因是 kernel 缺乏批次不变性(batch invariance)。 服务端的 batch size 随并发负载动态变化,而 RMSNorm、矩阵乘、attention 的归约策略依赖 batch size。每个 kernel 单次执行是确定的,但你的请求被塞进多大的 batch 取决于此刻别人在发什么请求

他们的实测:Qwen3-235B,temperature 设为 0,同一个 prompt 发 1000 次——

  • 得到 80 个不同的补全,最高频的那个出现 78 次
  • 前 102 个 token 完全一致,在第 103 个 token 上分叉
  • 992 次输出 "Queens, New York",8 次输出 "New York City"

换上批次不变的 kernel 之后,1000 次输出逐位一致。 代价是同一批任务从 26 秒涨到 55 秒,改进 attention kernel 后回落到 42 秒。

这件事对交付意味着什么:你的回归测试挂了三个案例,可能跟你这次的改动毫无关系。 而你分不清——除非愿意让推理慢六成(示例中的 26 秒到 42 秒),或者每个结论都跑够多次。这两条都受成本和延迟的限制。

但再准确一点,agent 的不确定性至少有三层:

  1. 推理层——同样的输入产生不同的 token,就是上面这件事
  2. 控制层——一点输出差异就可能导致不同的工具选择和执行路径
  3. 环境层——搜索结果、数据库、权限、外部服务本身在变

批次不变 kernel——把规约顺序钉死、不随 batch 大小改变的那种——只解决第一层。 它让回归结果更容易复现,但不会自动收拾后两层,更不会告诉你输出是不是对的。

工具越多,失败越难归因#

加一个工具,到底会不会拉低其他工具的准确率?

这件事有人测量过。《How Many Tools Should an LLM Agent See?》在 BFCL(370 个工具)、MetaTool(199 个)、ToolBench(3251 个)三个基准上做了测试。Claude Sonnet 4.6 在 BFCL 上:自适应策略平均只展示 2.2 个候选工具,而当正确的那个在候选里时,模型选中它的比例是 93.1%;固定每次给 5 个工具,这个比例是 87.1%

少给、给准,选中率涨 6 个百分点(87.1% → 93.1%)。

但同一篇论文给了一个不该被略过的代价。在中等难度的查询上(正确工具排在第 2–5 位),固定给 5 个能保证正确工具每次都在候选里,选中率 60.9%。自适应策略只在 62.3% 的查询里把正确工具放进候选,放进去时选中率有 76.8%。

但这是两道关,得连乘:先进得了候选,再被选中。0.623 × 0.768 ≈ 48%,反而明显低于固定给 5 个的 60.9%。(这个 48% 是拿前两个数换算的,论文原文没给。)

所以这不是"越少越好"。少给提高的是选择精度,代价是可能根本没给。

而对交付来说,更要命的还不是准确率本身,是归因

一次失败摆在你面前,它可能是 prompt 写得不好,可能是工具描述写得不好,也可能是模型压根没选中该选的那个。工具越多,这三种可能越难分开——而你该修哪一种,取决于你分不分得清。

Anthropic 自己也在说这件事。他们那篇讲 MCP 用法的工程博客写道:"随着接入工具数量增长,把所有工具定义一次性加载进来、再让中间结果穿过上下文窗口,会拖慢 agent 并推高成本";接上几千个工具时,agent"得先处理几十万 token 才能开始读你的请求"。

他们的解法叫渐进披露——让模型只加载真正需要的那几个。文中那个跨 Google Drive 和 Salesforce 的例子,token 从 15 万降到 2 千。

没有断言,循环还慢#

一是 "做对了吗"经常没有断言。

这份研究报告写得好吗?这个总结抓住重点了吗?传统测试的基础是断言——一个明确的、可自动判定的期望值。agent 的正确性有很大一部分是模糊判断,要么上人工,要么上 LLM judge,而 judge 本身也不可靠。

前面那篇 CHI 论文里,19 个人有 13 个试过把传统 ML 那套自动化指标搬到生产系统上。他们自己给的评价是 "bordering on useless"

这一条有个很好的反证,就是开篇那份 playbook。它把 agent 嵌进软件开发的六个阶段,十几条 play,每一步都配了闸门、评测和可读的指标。它能写成这样,靠的是那个领域里断言是现成的。它给 eval 下的定义就是 "tests pass, lint clean, behavior unchanged, policy followed"——退出码、lint 警告数、screenshot diff、CI 绿灯,全是机器能判的。

写代码恰好是断言最不缺的少数场景之一。 换成写研究报告、做客服判断、整理调研,那一整套闸门会逐个退回人工或 LLM judge——文档还是那份文档,成本结构完全变了。所以它不是这一节的反例,是这一节的边界:它标出了"验证便宜"能走到哪儿为止。

二是 反馈循环慢三个数量级。

第二章那个"一天只推进五六个有效判断"不是效率问题,是结构问题:传统测试的反馈来自本机的确定性计算,agent 的反馈来自一次收费的、跨网络的、结果还会抖的推理。前者可以随便试,后者每试一次都在花钱。 于是"多跑几遍确认一下"这个在传统工程里近乎免费的动作,在这里成了一件要做预算的事。

于是第一个塌的是 code review#

噪声、归因、成本,三样凑齐之后,最先撑不住的是一件很多人以为不会出问题的事。

两个方向的“code review”都受到了冲击。

一个是拿 agent 加速传统交付:agent 写出来的 diff 太多,人读不过来,评审队列越积越长——那是的问题,加人、分级、抽样、让另一个 agent 先过一遍,都能缓解,Anthropic 那份 playbook 花了整整一个阶段讲的就是这件事。

另一件是这篇要说的:你在做的东西本身就是 agent。 你改的是一句 prompt、一个工具描述、一个截断阈值、一套压缩策略,diff 可能只有三行,评审人读得完,读得懂,读完也依然什么都不知道。

code review 能成立,靠的正是"行为可以从实现推导"。在 agent 上这个前提不成立:

  • 改一句 prompt,diff 三行,每个字都读得懂,但你说不出它是变好还是变坏。
  • 加一个工具,代码挑不出毛病,可它会不会拉低其他工具的选择准确率?读不出来。
  • 把截断阈值从 4000 改成 6000,盯着这一行看,你什么也得不到。

三条的共同点是:信息不在 diff 里。 它在这套配置跑完一批案例之后的结果里,而那份结果不会出现在 pr 页面上。

所以这不是"要更仔细地 review"就能解决的事。再资深的 reviewer、再详尽的 checklist,也补不上这个缺口——不是 ta 不够仔细,是 ta 手上那份材料本来就不含答案。

做 agent 的时候,把注意力放在读 diff 上,本身就是放错了地方。 该盯的是那批案例跑出来的结果、是失败案例的归因、是这次改动到底动了几个变量;而这些东西都在 pull request 之外,而人的注意力是有限的,花在 diff 上的每一分钟都是从那边挪过来的。

而只要 agent 的改动还在靠读 diff 过 review,这道关就是空的:改动照样合进去,问题到线上才冒出来,然后大家困惑于"明明 review 过了"。


四、那能怎么办#

第二章那三段都能压短,压法不一样。下面就按那个顺序讲:先让第一段少撞几次,再让第二段攒的东西不白攒,最后让第三段真能下结论。

只是得先有一样东西垫在下面,三段都要靠它。

而所有这些做法买的是同一件事:把"你需要试三百次"和"你一天能试三十次"之间的那道口子收窄一点。

先建实验基础设施,再开始调 prompt#

普遍顺序是先把 agent 做出来,做不好了再想办法测。应该反过来:在写第一版 prompt 之前,就该能用一条命令跑完一批固定案例,并输出可直接对比的结果。

  • 输入集固定
  • 一次只动一个变量,换模型的时候不要顺手改 prompt
  • 每次运行的完整配置可回溯,否则一周后你不知道当时那个好结果是怎么来的

理由就是那个三个数量级:agent 的反馈循环比传统软件慢一千倍,同样一份基础设施投入,在这里买回来的迭代次数是那边的几百倍。

能做成闸门的,别留给验证#

少撞几次最省的办法不是撞得更早,是让它压根撞不上:有些性质根本不该靠验证来保证。

同一份 playbook 里有一句分得很干净:skill 只是建议,hook 才是它背后强制的那一层——

"nothing forces a session to comply with it. A policy that must always hold needs something deterministic behind the skill... The skill makes violations rare and the hook makes them close to impossible."

写进 prompt 的约束,模型照样会违反,只是概率低一些。而"概率低一些"这件事,你只能靠跑样本才知道——它把一个本来可以确定的性质,变成了一件要花钱测量的事。

所以规则是:prompt 表达意图,出口闸保证性质。 凡是"必须永远成立"的约束,都在输出的出口再校验一遍,不成立就拦下来。闸门是确定性的,写一次,不用跑样本,也不占实验预算。

同一份文档在监控那一节把这条推得更远:σ 带的越界检测全程不过模型——"detection stays entirely deterministic, with no model involved"——越界之后才叫 agent 进来诊断。确定的部分留给代码,判断的部分才交给模型。

而最值得抄的一道闸门,管的不是 agent 的任务输出,是验证装置本身:修 bug 的 session 不许编辑测试文件。理由一句话——"an agent fixing code must not be able to weaken the check on that code."

第二部分列的那些失败模式——工具返回 []、压缩丢约束——都发生在任务这一层。这道闸门管的不是那一层。它说的是评测基础设施会被它所评测的东西改写,而这件事不会体现在通过率上:通过率只会变好看。

别把工具全摆上桌#

前面说过,工具多了不只是选得不准,是失败没法归因。落到做法上就三条:工具数量当预算管;每加一个,跑一遍加之前和加之后的对比,看其他任务有没有退化;优先渐进加载,而不是全量在场。

pi 把这条做到了极端:不支持 MCP,不要 sub-agent,不要 plan loading,不要后台 bash,不要 todo 管理,只留四个工具,system prompt 短到几个 token。它的 Agent 构造把这个决策摆在明面上:

// 注:这两个是旧包名,现已迁到 @earendil-works/pi-agent-core / pi-ai
import { Agent } from "@mariozechner/pi-agent-core";
import { getModel } from "@mariozechner/pi-ai";

const agent = new Agent({
  initialState: {
    model: getModel("anthropic", "claude-sonnet-4-20250514"),   // 文档当时的 ID
    systemPrompt: "You are a helpful coding assistant.",
    tools: [readTool, bashTool, editTool, writeTool],   // 就这四个
    thinkingLevel: "medium",
  },
  convertToLlm: (messages) => messages.filter(
    m => m.role === "user" || m.role === "assistant" || m.role === "toolResult"
  ),
});

两处值得看。

tools 就是构造函数里一个普通数组——给几个工具是一次显式决策,不是"把注册表全挂上"的默认行为。

convertToLlm 是请求发出前的最后一道闸:哪些内部消息真正进入模型上下文,由这个回调显式决定。 这里展示的是按消息角色过滤;到底能省下多少上下文,取决于一次运行里各类消息的实际占比。

多数框架把这一步藏在内部。藏起来的结果就是它变成"默认全量",然后你在 token 账单上才发现。

而 pi 的成绩:在当时公开的 Terminal-Bench 评测、指定模型和配置下,它排在前列,同时展示出明显更好的成本表现。(榜单会随版本和模型更新,引用时最好带上日期。)

让案例自动积累#

案例不可复用、只能自己攒,那唯一的出路是让它成为运行的副产品,而不是一个阶段性任务。每一次线上失败自动落成一个案例,每一次人工兜底和纠正记录成一条期望输出。

这条改变的是成本的形状:把"验"从固定成本变成边际递减的成本。这是整个交付过程里少数几件会随时间变便宜的事之一。

但只是"之一",也不是一劳永逸。案例集自己会腐烂:随着模型变强,曾经能区分好坏的案例会陆续失去区分力——前面那份 playbook 专门提醒过这件事,"cases that once discriminated stop doing so"。失去区分力的案例不会自己消失,它继续占着每一轮的运行预算,还会把通过率托高,让你以为覆盖得挺好。所以自动积累必须配一件事:定期把不再区分的案例淘汰掉。 一个案例连着几轮在所有配置下都通过,它就已经不是案例了,是背景噪音。

顺带说,这也是现有评测工具还没补上的一段。截至本文写作时,LangSmith、Braintrust、Langfuse 都已经能记录 trace、管理数据集、跑评测;但要从一次线上异常里提取输入、人工纠正和期望行为,攒成一条能直接回归的案例,通常仍然需要人确认。

判断"变好了没有",要看逐案例#

案例攒起来了,真正难的那一步才开始:这一批到底够不够下结论?

第二章那个"案例攒到二十个"确实是个陷阱——但陷阱不在数量上。

假设基线通过 18/20,改完通过 16/20,是回归了吗?

只看这两个数字,回答不了。 回归测试是在同一批案例上跑前后两轮,属于配对数据。真正携带信息的不是两个通过率,而是每个案例发生了什么变化:几个从"过"变成"挂",几个从"挂"变成"过"。

同样是 18/20 → 16/20,可以是完全不同的三件事:

过→挂挂→过前后评判不一致的 pairMcNemar 精确检验
202p = 0.500
426p = 0.688
6410p = 0.754

总分完全一样。但第一行是两个案例纯退化,第三行是六个退化、四个改善——后者说明这次改动在大范围地改变行为,而这件事从"16/20"里一个字都看不出来。

所以第一条纪律不是"攒够多少个",是保存逐案例结果。

而一旦保存了逐案例结果,需要的案例数反而比直觉少。配对设计里只有前后不一致的案例携带信息:

不一致对占比退化 : 改善所需案例数
20%纯退化21
20%9 : 148
20%3 : 1145
10%纯退化41

如果改动造成的是干净的退化,二十来个案例就够——表里那行是 21。但只要它同时制造退化和改善——多数 prompt 改动都是这样——信号会互相抵消,需要的样本数迅速上升。

这正是"只看总通过率"最危险的地方:它恰好把互相抵消的那部分信息丢干净了。

而"拿通过率当门禁"不是我为了立论编出来的做法。前面那份 SDLC playbook 在讲持续评测的一节里,给的就是这个建议:

"Gate configuration changes on the results. A skill change that drops the pass rate gets reviewed before it merges."

配置改动以通过率是否下降为门禁,同一节推荐的案例规模是 20 到 50 个。把这个规模放回上面那张表里对一下:20 个连最干净的纯退化都差一例,50 个也才刚够覆盖 9 : 1 的混合——而 9 : 1 已经是相当干净的一次改动了。

它的方向完全对:把 prompt、skill、hook 这些配置当代码看,改了就跑回归。但门禁挂在通过率上,等于挂在一个刚好把配对信息丢光的数字上。 存的是同一批运行,多存一份逐案例结果几乎不要钱,而门禁的判别力差一个量级。

(如果拿独立双比例检验去算同一组数据,会得到 p = 0.376,还会告诉你需要 199 个案例。那个数字回答的是另一个问题——它假设前后是两批不同的案例。)

至于每个案例要重复跑几次:由观察到的波动和成本决定,别定死。 有些案例每次都一样,重复纯属烧钱;有些案例本身就在两个结果之间跳,那才需要多跑几次才能判断。

但分析的时候要小心一件事:20 个案例各跑 5 次,不等于 100 个互相独立的案例。 它们仍然按案例聚集。稳妥的做法是先把每个案例汇总成一个状态(稳定通过 / 稳定失败 / 不稳定),再以案例为单位做配对比较。

汇总规则要写死在配置里,不能留给临场判断。n 次全过记为稳定通过,其余一律归进失败侧——稳定失败和不稳定都算失败。理由是不稳定的案例你不能交付,它跟稳定失败在"能不能上线"这件事上是同一类。

而 McNemar 只吃二值,所以三态必须先坍缩成二值,坍缩的方向决定了你在测什么:把不稳定归进通过侧,量的是能力上限;归进失败侧,量的是可交付性。两个都成立,但一份报告里只能选一个,并且要写出来。下面的样例输出用的是后者。

两个函数就够了:mcnemar_exact(b, c) 给出精确检验的 p 值,mcnemar_n(不一致对占比, 退化占比) 反过来算需要多少案例。上面两张表里的每一个数字都是这两个函数跑出来的,实现放在文末附录,零依赖,Python 3.9+ 直接能跑。

一次运行要留下什么#

跑一轮的部分没什么技巧,但有三件事一件都不能省。

配置指纹——把模型、温度、system prompt、工具集、截断阈值一起哈希成一个短 ID。只记模型名和 prompt 是不够的:换 provider 的时候,tokenizer、上下文窗口策略、默认采样参数都可能跟着变,而你以为自己只动了一个变量。每例重跑 n 次——默认 5 而不是 1,理由就是上面那个批次不变性,跑一次拿到的通过率,你分不清它是能力还是运气。输出区间,不输出单个数字——一个 89% 会让人安心,[81.4%, 93.7%] 才让人知道自己站在哪儿。

这三件事凑起来,对比两轮运行时输出长这样——先说你动了几个变量,再说结果差异能不能归因

配置差异:1 项
  system: 'v1' → 'v2'

案例数 20    总运行数 60(每例 3 次)
  基线  55/60 = 91.7%  95%CI [81.9%, 96.4%]
  当前  47/60 = 78.3%  95%CI [66.4%, 86.9%]

逐案例状态转移(配对数据里真正携带信息的部分):
  稳定通过 → 不稳定    8   ←
  稳定通过 → 稳定通过   7
  不稳定  → 不稳定    3
  不稳定  → 稳定通过   2   ←

退化 8,改善 2,不一致对 10
McNemar 精确检验 p = 0.109
→ 这批案例分辨不出方向。注意是「分辨不出」,不是「没有变化」

这份输出里最该看的不是那两个百分比。91.7% 掉到 78.3%,看着像一次明显的退化;但逐案例摊开是 8 个退化、2 个改善,不一致对只有 10 个,McNemar 给出 p = 0.109——这批案例分辨不出方向。

而"配置差异 1 项"那行也不是装饰。它针对的是一个特别容易发生的情况:以为自己只在换模型,实际连 tokenizer、上下文策略、默认采样参数一起换了。基于那种对比得出的任何结论都是无效的,而没有这行提示,你不会知道。

完整实现在 github.com/yuki-uix/agent-eval-harness:单文件 285 行,零依赖,含配置指纹、逐案例状态转移、配对样本量估算和接 SDK 的样例。

克隆下来 python3 eval_harness.py 直接跑,不需要 API key 就能看到上面那份输出。

review 的标准要跟着改#

既然读 diff 判断不出好坏,review 就不能只看 diff。一次 prompt 改动、一次工具集调整提交上来,评审人该要的是那批案例的前后对比,而不是"我看了一遍没问题"。

没有这个东西,review 这道关是空的——它盖了章,但没有检查任何东西。

而这条是有前提的:没有实验基础设施,你连 review 都做不了。 所以这一章如果只动一件事,动最前面那件。


收尾#

十分钟能搭出一个能跑的 agent,这在两年前不可想象——模块化实实在在做到了它承诺的事。但它承诺的是"少写重复的集成代码",那是关于实现的;而交付卡在验证上,两件事不在同一个维度。

所以那几周不会因为框架更好用而缩短。代码当然还要写,只是它不再是主要瓶颈——时间花在撞失败模式、定判定标准、攒案例、等实验跑完上。

不过整篇的地基只有一条:agent 的关键行为很难从实现直接推出来,只能靠观测,而观测很贵。

这条正在被两头撬动。批次不变 kernel 已经能让 1000 次推理逐位一致,代价是慢六成——这说明"不可复现"至少有一部分是工程选择,不是物理定律。 而确定性的出口闸把一部分性质从"要采样才知道"变回"写一次就成立",那部分根本不进验证预算。

但两头收掉的都是外围。kernel 收的是验证的噪声,闸门收的是需要验证的面。正确性标准怎么定、案例够不够覆盖、外部环境在变,一条都不会因此消失。

验证会变便宜一些,但不会消失。


附录:McNemar 的最小实现#

正文两张表的数字都出自这段代码。零依赖,Python 3.9+ 直接能跑。

import math

def mcnemar_exact(b: int, c: int) -> float:
    """配对二元数据的精确检验——回归测试该用的就是它。

    b = 前通过、后失败(退化);c = 前失败、后通过(改善)。
    只有前后不一致的案例携带信息:两轮都通过或都失败的,
    对"这次改动有没有影响"一个字都没说。
    """
    n = b + c
    if n == 0:
        return 1.0
    k = max(b, c)
    tail = sum(math.comb(n, i) for i in range(k, n + 1)) / 2 ** n
    return min(1.0, 2 * tail)

def mcnemar_n(p_discordant: float, regress_share: float) -> int:
    """配对设计下需要多少案例。

    p_discordant  前后结果不一致的案例占比
    regress_share 退化在不一致对里的占比

    注意它由不一致对决定,不由通过率决定——这也是为什么
    只存总通过率的话,这个数根本算不出来。

    双尾检验只看偏离一半的幅度,所以 0.4 和 0.6 返回同一个数:
    一边是改善占优,一边是退化占优,需要的样本量一样。
    """
    if not 0 < p_discordant <= 1:
        raise ValueError("p_discordant 要落在 (0, 1]")
    psi = 2 * min(max(regress_share, 0.001), 0.999) - 1
    if abs(psi) < 1e-9:
        raise ValueError("退化与改善各半,没有方向可检出,样本量无定义")
    za, zb = 1.959963985, 0.841621234     # α=0.05 双尾,power=0.8
    n_disc = ((za + zb * math.sqrt(1 - psi ** 2)) / abs(psi)) ** 2
    return math.ceil(n_disc / p_discordant)

参考#

访问日期均为 2026-08-27,链接与版本均已回一手来源核对。

调查与论文

  • van der Maden, Sadek, Xiao, Mottelson, Liao, Zhu. Results-Actionability Gap: Understanding How Practitioners Evaluate LLM Products in the Wild. CHI '26. arXiv:2604.16304ACM DOI 10.1145/3772318.3791069 19 位生产环境 LLM 产品从业者的半结构化访谈;"bordering on useless" 出自此文。

  • Nahar, Kästner, Butler, Parnin, Zimmermann, Bird. Beyond the Comfort Zone: Emerging Solutions to Overcome Challenges in Integrating LLMs into Software Products. ICSE-SEIP 2025. arXiv:2410.12071 26 次访谈 + 332 份问卷。正文的 76.6% / 36.3% 是经上一条 CHI 论文转引的,未回原文逐字核对。

  • LangChain. State of Agent Engineering(2026 年报告)。 www.langchain.com/state-of-agent-engineering 调查期 2025-11-18 至 2025-12-02,n = 1,340。正文五个数已回原页核对:89% 有可观测性、52.4% 跑离线评测、37.3% 跑线上评测、整体 29.5% 不做评测(已上线团队 22.8%)、人工评审 59.8%。

  • Thinking Machines Lab. Defeating Nondeterminism in LLM Inference(2025-09-10)。 thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference Qwen3-235B-A22B-Instruct-2507 / temperature 0 / 1000 次请求 / 80 个不同补全(最高频 78 次)/ 前 102 token 一致、第 103 token 分叉 / 992 次 "Queens, New York" 对 8 次 "New York City";吞吐 26 秒(vLLM 默认)→ 55 秒(未优化的确定性版)→ 42 秒(改进 attention kernel 后)。以上均已回原文逐条核对。

  • How Many Tools Should an LLM Agent See? A Chance-Corrected AnswerarXiv:2605.24660(v1 2026-05-23;v2 2026-06-07) BFCL / MetaTool / ToolBench 三基准。正文引的是论文 downstream validation 一节的 Claude Sonnet 4.6 数据,该节的 agent 用 step_cost=0.01 重训,K=2.2±0.4,与摘要里主实验的 K=7.4 不是同一组,引用时务必写清是哪一组。93.1% / 87.1% 是"正确工具在候选中时的选中率",非端到端;中等难度那组:BoR 在 62.3±2.0% 的查询上给出候选、给出时选中率 76.8±2.5%,FK=5 每次都给、选中率 60.9%。正文那个 48% 是本文用 62.3% × 76.8% 换算的端到端值,论文未给此数。

Anthropic 官方文档

  • Introducing the Model Context Protocol(2024-11-25)。 www.anthropic.com/news/model-context-protocol MCP 的问题描述与承诺,正文开篇那句引文出自此文。

  • Code execution with MCP: building more efficient AI agents(Adam Jones、Conor Kelly)。 www.anthropic.com/engineering/code-execution-with-mcp 工具全量加载的代价、渐进披露、15 万 → 2 千 token。后者是单一场景对比,不是通用基准。

  • The AI-Native SDLC playbook(Louis Claxton,2026-08-21,43 min;站内归类 Enterprise AI / Claude Code)。 claude.com/blog/the-ai-native-sdlc-playbook 正文引用的 "Code is no longer the bottleneck"、通过率门禁与 20–50 案例规模、skill 只是建议 / hook 才强制、"detection stays entirely deterministic"、禁止修 bug 的 session 编辑测试文件,均出自该文。两点须标注:它是一份纯规范性文档,全文没有任何实测数字,引它是引"官方推荐做法",不是引证据;它讲的是"用 agent 造软件",与本文"把 agent 交付成产品"是两个领域的同构现象,不是同一份论据。

代码与框架

  • 本文的 eval harness:github.com/yuki-uix/agent-eval-harness 单文件 285 行,零依赖。正文那份样例输出即 python3 eval_harness.py 的实跑结果,附录两个函数与仓库实现行为一致(已跑 15 组对照)。

  • pi(Mario Zechner)。仓库已迁至 github.com/earendil-works/pimariozechner/pi 已不存在)。正文代码块里的 @mariozechner/pi-agent-core@mariozechner/pi-ai 都是旧包名,现行包名为 @earendil-works/pi-agent-core@earendil-works/pi-ai(截至核对时 v0.84.3),源码在 packages/agentpackages/aiTerminal-Bench 成绩须标注榜单日期、benchmark 版本、模型与 harness 版本、成本口径,否则会随榜单更新失效。

  • DeepSeek Harness("Everything is a Plugin")。插件与工具注册代码取自 Cordis 教程:github.com/deepseek-ai/deepseek-harness/tree/main/docs/cordis-tutorial,正文那段对应 01-first-plugin.md。该目录同时提供中文版(*.zh.md)。

  • MCP Python SDK(FastMCP):github.com/modelcontextprotocol/python-sdk(v2.1.1,2026-08-25),@mcp.tool() 示例见 README。claude mcp add 见 Claude Code 官方文档 code.claude.com/docs/en/mcp,完整语法为 claude mcp add [options] <name> -- <command> [args...]