ToWow / 工程笔记

把成功提交放到身份校验之后

一个子项认错了任务,整批结果却已被记为成功。这次故障修的是提交顺序。

批量评审中的一项返回,把 case_id 从 gold-pair-078 改成了 gold-pair-079。它的任务编号还指向原题。按失效记录重建的身份字段如下:

task_id:      gold-pair-078::gold-pair-078-candidate-127
candidate_id: gold-pair-078-candidate-127
case_id:      gold-pair-079

这项返回属于一个包含 20 个候选的批次。JSON 可以解析,结构检查也通过了。逐子项校验随后发现错误,但批次回执已经写上 status: ok。原始失效记录与代码定位

结果没有形成完整的正式评审;它却留下了一个恢复隐患:旧文件里的“成功”与后来的失败记录相互冲突。只按成功回执导入的恢复程序,会再次接纳这个批次。

修复点很明确。成功状态必须放在全部子项身份校验之后。 请求属于哪道题,是调用方已经知道的事实,应当由程序对照,不能交给模型自行保证。

验收整批,再写成功

只规定类型和结构的 Schema,允许两个不同的字符串同时合法。动态 Schema 可以进一步限定身份值;本系统还在返回后直接核对实际请求,完成批次验收。

当前实现依次检查顶层 case_id、任务覆盖、子项结构和身份。任务覆盖同时比较数量与集合,用来发现遗漏、重复和额外任务;身份检查再核对 task_id、case_id、candidate_id 和候选类型。

下面是核心顺序的教学简写。生产实现另有阶段 Schema、回执与重试处理。

expected = {task["task_id"]: task for task in requests}
if len(expected) != len(requests):
    raise InvalidBatch("请求自身包含重复任务")

ids = [item["task_id"] for item in response["items"]]
if len(ids) != len(expected) or set(ids) != set(expected):
    raise InvalidBatch("任务缺失、重复或新增")

for item in response["items"]:
    task = expected[item["task_id"]]
    for key in ("task_id", "case_id", "candidate_id"):
        if item[key] != task[key]:
            raise InvalidBatch(f"身份字段不一致:{key}")

commit_success(response)

按编号关联,返回顺序可以变化。对照冗余字段,则避免同一个对象在不同下游眼里属于不同任务:一个模块按 task_id 找到078,另一个按 case_id 分组到了079。

程序没有把079直接改回078。仅凭错位,无法判断模型是抄错编号,还是理解了另一道题。整批判失败并保留原始返回,保住了调查这两种情况的机会。

整批验收有成本:其余看似正常的19项也不能复用。这个取舍换来一条简单的提交边界。要支持部分接受,就得另外定义每个子项的独立验证、缺项补齐和重复提交处理;那是另一套恢复设计。

旧的成功文件重新过关

新调用修好之后,历史回执仍然存在。导入代码因此也重新检查每个子项。发现与旧失败日志对应的身份错误时,整个批次作为诊断材料保留,任何子项都不进入可继续使用的检查点。

已经发生的调用成本仍然记账。一次重启可以更换运行目录,不能抹掉此前的尝试。否则系统最终报出的“每份可用结果成本”,只剩最后成功的那一段。

批次大小应该按这个总成本选择。大批次能摊薄公共输入;整批重试又会放大单项错误的影响。项目后续只对指定的、已经耗尽重试的批次开放确定性拆分,并让单项失败仍能结束运行。拆分是有界恢复手段,不能变成无限重跑的理由。

回归测试故意损坏第一次返回的子项身份,检查三件事:这次尝试写失败回执,成功检查点路径为空,随后正确返回才写成功。它测的就是这次故障穿过的那条缝。

批处理的完成计数应从这里开始增长:返回已与请求对上,整批验收已结束,成功提交才发生。

下一步

如果你的团队正在把 agent 放进真实业务,想了解 Flowness,或想聊企业 AI 落地怎么做,请在“联系我们”页写下行业和想解决的问题,我们按这些内容回复。 联系我们