Erpilot 系列第 5 篇 · 2026-10-20 发布 · 实现与验收截至 2026-10-19
上一篇:幂等与崩溃对账。本篇讨论项目现有程序化评测、质量门槛和历史失败,不把固定用例结果外推为通用准确率。
本文引用的源码与报告保存在随文资料清单中,包含当时尚未推送到项目仓库的工作区内容,文件版本由 SHA-256 校验值标识。代码文本与复现脚本为局部摘录,运行仍需 Erpilot 工作区依赖。
模型调用了 adjust_stock,最后说“库存已经调整”。这能证明任务成功吗?还需要检查审批有没有放行、工具有没有返回业务错误、实际数量改了多少,以及其他记录有没有意外变化。
Agent 评测的困难在这里:一段看起来合理的回答,只是执行结果的一部分。对于 ERP 任务,工具请求、执行结果、业务状态和用户可见文本需要一起检查。
Erpilot 从工具名与子串断言起步,逐步补上状态核验、调用请求证据、报告完整性和计量检查。与这些实现同样需要保留的,是评分器修正前的失败和当时的实验条件。
一、先把“做对了”拆成可观察事实
一条评测用例先说明用户问题和考点,再声明可核对条件。项目用 EvalCase 保存这些约定,运行结果进入 CaseResult。
| 检查层 | 回答的问题 | 现有字段或检查 |
|---|---|---|
| 工具选择 | 是否取得所需业务事实 | expect_tools_all、expect_tools_any |
| 执行顺序 | 是否在依赖成立后推进 | tools_in_order 及专项顺序检查 |
| 成功结果 | 工具是否真正完成目标操作 | expect_successful_tools、successful_tool_counts |
| 业务状态 | 修改量、状态与其他记录是否符合要求 | state 与套件状态摘要 |
| 可见答复 | 是否回应问题、说明限制、避免编造 | 文本断言、结构化字段与逐条复核 |
| 安全边界 | 是否提出禁止调用或改变业务 | tool_attempts、禁止工具与零变更检查 |
不应给所有场景强制同一条调用路线。用户请求一个不存在的信息时,先澄清与查找后说明未找到,都可能符合任务要求。只有当调用本身是考点,例如必须先取得当前草稿再修改,才固定相应步骤。
这些程序规则也有能力边界。回答里出现正确金额字符串,不代表所有数字和条件都准确;有合法引用,不代表附近结论得到支持。写不出可靠程序判据的部分,需要保留人工或开发过程复核的范围说明。
二、“工具结束”不等于业务成功
早期评测发现过一个协议缺陷:某些聚合工具声明返回对象,实际返回列表,FastMCP 结构化结果处理失败;原检查只看工具名,仍可能判为通过。这个问题推动评测开始保存工具参数、返回内容和执行状态。
即使 ToolCallFinished.ok 为真,也还要解释业务正文。工具可能正常返回 {"error": ...},或者审批正常完成但结果为拒绝。这些都不是批准执行成功的证据。
当前成功检查因此进一步排除业务错误与 approval=denied;批准路径还要求正确成功次数,以及指定业务变化。拒绝路径则检查未执行说明与业务表不变。
写用例使用独立临时种子库,防止上一条修改污染下一条。早期基线核对订单、明细、商品和库存,后续草稿与政策套件增加了相关表的全表摘要。不同套件检查的表范围需要随报告说明,不能统一写成“所有表永远完全相同”。
实现与历史缺陷入口见写入可靠性复盘,判据定义见评测标注指南。
三、危险调用在被拒绝之后,仍然是一次尝试
假设政策查询任务不允许建单,模型提出了写调用,但审批门将它拒绝。数据库保持不变,说明门起了作用;模型是否遵守了“只读任务”的要求,则需要另外判断。
如果只检查最终提交记录,就会漏掉这次越界请求。如果只检查已完成工具列表,中途被中断的调用也可能缺失。
因此,新报告在 ToolCallStarted 时记录 tool_attempts,同时保留完成顺序的 tool_calls 与完整工具结果。质量门槛检查禁止工具时,会综合这些证据。旧报告的 tool_attempts=None 表示当时没有记录,不能解释为没有尝试。
审批测试也需要同时跑放行与拒绝。只用 AutoDeny 门,能验证拒绝后的表达,却到不了真实写 handler;项目曾有“发现工具时开放写入、调用时却关闭”的桥接缺陷,正是在脚本放行路径暴露的。
对于错误恢复,还要证明模型确实收到目标错误。先读到零库存并停止写入,是前置判断;批准后得到 insufficient_stock 再只读复核,才属于错误后的处理。受控注入能稳定提供这个错误入口,但需要注明 handler 被测试夹具替换,不能冒充自然触发的业务失败。
四、评分器也要允许被检查
2026-10-08 的一次完整基线执行了固定 35 条,32 条通过。逐条复核后,部分失败来自评分器遗漏等价表达或有效工具路径:空搜索结果可以说“没搜到”;报价可以经过 compare_quotes;拒绝表达也不只一种固定话术。
修正规则后,v2.3 定点报告实际是 2/3,随后 v2.4 对剩余一例单独运行并通过。它们不能与旧完整报告相加,写成一次全量通过。对应原始数据分别是完整运行、v2.3 定点和v2.4 定点。
规则修改需要有可判别的正反例。例如接受同义拒绝措辞后,仍要拒绝“先说不能透露、随后给出号码”的回答。否则扩展措辞只是在放宽条件,没有修正判断能力。
注入实验中还出现过格式误伤:回答“当前库存为 0 件”,强调符号阻断了旧子串匹配。v2.6 修正格式归一化后,对原 trace 离线复判:control 保持 3/4,treatment 从原 2/4 变为 4/4。原分数、原请求与费用保留,没有重新调用模型。复判说明记录了逐项变化。
这里的提高来自重新解释同一份证据,不能描述成模型在新一次运行中变得更强;有限攻击载荷的通过,也不证明抵抗任意提示注入。
五、一次评测要连同条件保存
只有成功率,难以判断两份报告能否比较。项目报告还保存 case 集和指纹、scorer 版本、相关源码指纹、种子、固定时间、工具面、重试、超时和实际执行范围。RAG 等专项另外绑定语料、索引与夹具。
源码身份尤其不能只用 Git HEAD:工作区可能已有未提交改动。内容哈希用于标识实际参与实验的源码,提交号保留作历史定位。运行期间源码变化也需要拒绝验收。
计划数量、执行数量和通过数量分别保存。预算跳过与依赖失败后未执行的轮次,既不是通过,也不能因为结果列表里存在占位行就算执行完成。
项目曾发现 runner 的 complete 标记可能把预算跳过项算进去,旧 strict 又过度信任该标记。当前 strict 独立核对覆盖、唯一性、实际执行、子轮次、源码稳定性和未知计量。原有错误报告保留,不能直接修一个元字段就让旧证据获得新的完整性。
趋势聚合因此按 scorer、suite、prompt 和 case 等指纹分组,缺关键元数据的旧报告单独保留。不能把不同题目与判分规则的百分比连起来,宣称项目获得连续提升曲线。
六、可比性与验收分别回答
evals.compare --strict 用于同源码、相同实验条件的对照,检查证据能否比较。它允许两份报告都存在真实失败;通过可比性检查,不等于通过业务质量门槛。
evals.gate 则读取已有报告和版本化策略,判断固定范围是否完整、结果是否一致、计量是否已知,以及成功率和禁止调用等要求是否满足。它也支持同模型跨版本回退检查:即使总通过数没有变化,原先通过的某一例变成失败,也会单列。
下面命令只读历史报告,不调用模型,也不写回原报告:
uv run python -m evals.gate reports/evals/20261017-150138-f81491.json --policy configs/evals/b4-reference.json这份旧 B4 指代报告在当前策略下预期返回退出码 1。它缺少完整轮次,并有未知计量及旧版请求证据缺失;该拒绝不等于本次重新运行了模型,也不改写旧 scorer 的成绩。
质量策略、输出与返回码见质量门槛说明。其中 trace 完整性检查主要核对引用字段,不保证对应文件实际存在或内容正确;报告里的模型名也不能独立证明端点内部实现。
七、费用未知时,让未知保留下来
一次超时可能已经消耗供应商额度,即使客户端没拿到 usage。项目把已返回 usage 的所有尝试累计,同时单列缺失用量或价格的请求;未知不能按零计费。
CNY 和 USD 估价独立列示,不能比较金额数字大小后就认定某模型更便宜。估价还依赖配置费率,不是供应商账单。请求前累计预算检查是熔断条件,最后一次响应仍可能超过余额,因此也不是账单硬上限。
多轮任务费用汇总需要避免父结果与子轮次重复相加。完整计量、可比性、业务通过与费用审计是不同结论,报告应分别给出。
这些约定让失败更容易归因:模型任务失败、工具实现失败、评分器缺陷、上游中断与计量缺失可以各自保留。下一篇政策 RAG 的检索与回答使用这套分层方式,讨论为什么检索命中之后仍然可能答错。