Agent 可靠性工程实战(三):给目标做指纹,防止重试把“修正确”偷换成“变绿色”
本篇读取上一篇追加了tool_allowed的runs/demo-001/events.jsonl,复用首条run_started.payload.goal和授权事件中的规范化路径,生成runs/demo-001/contract.json。上一篇的事件账本是本篇唯一输入;本篇输出的契约文件将成为后续每次检查与转向提示的不可变基线。
一、目标漂移为何比提示词写差更隐蔽
第一次指令可能是“让charge(10000)按九折返回 9000,不得修改测试”。测试失败后,如果宿主只反馈“让测试通过”,下一轮目标已经从业务行为变成绿灯。模型修改断言也能完成新目标。问题不完全在模型,而在循环丢掉了原始目标、保护范围与验收条件。
契约把三类信息分开:goal描述期望行为,protected列出不可由执行器修改的裁判材料,checks给出确定性命令。序列化时必须稳定排序和固定分隔符,再计算 SHA-256;直接对 Python 字典的展示文本求摘要会受格式与实现变化影响。
from__future__importannotationsimporthashlibimportjsonfrompathlibimportPathfromtypingimportAnyfromledgerimportload_eventsdefcanonical(data:Any)->bytes:returnjson.dumps(data,ensure_ascii=False,sort_keys=True,separators=(",",":"),).encode("utf-8")defbuild_contract(events:list[dict])->dict:started=next(eventforeventineventsifevent["kind"]=="run_started")allowed=[eventforeventineventsifevent["kind"]=="tool_allowed"]contract={"version":1,"goal":started["payload"]["goal"],"allowed_paths":sorted(event["payload"]["path"]foreventinallowedif"path"inevent["payload"]),"protected":["workspace/tests","runs/demo-001/contract.json"],"checks":[["python","-m","unittest","discover","-s","workspace/tests"]],}contract["sha256"]=hashlib.sha256(canonical(contract)).hexdigest()returncontractdefmain()->int:events=load_events(Path("runs/demo-001/events.jsonl"))contract=build_contract(events)target=Path("runs/demo-001/contract.json")target.write_text(json.dumps(contract,ensure_ascii=False,indent=2),encoding="utf-8")print(f"goal={contract['goal']}sha256={contract['sha256'][:12]}")return0if__name__=="__main__":raiseSystemExit(main())运行输出:
goal=修复折扣计算 sha256=7a1a4a54d861二、摘要链保护的到底是什么
摘要不是加密,也不阻止有权限的人改文件。它把一份具体字节内容变成短标识,使后续步骤能发现“当前契约已不是启动时那份”。如果攻击者能同时改契约和所有保存摘要的位置,普通摘要无能为力;真实部署可把根摘要写入只读控制面或使用带密钥的签名。
还有一个常见坑:把sha256字段自身也放进摘要输入会形成无法满足的自引用。代码先对不含摘要的契约求值,再添加字段;验证时必须复制对象并删除sha256。记忆点是:指纹证明的是同一份契约,不证明契约本身正确。错误但未变化的目标仍会稳定通过,所以签名前要由人或上游系统确认内容。
importhashlibimportjsonfrompathlibimportPathdefverify_contract(path:Path)->bool:data=json.loads(path.read_text(encoding="utf-8"))claimed=data.pop("sha256")actual=hashlib.sha256(json.dumps(data,ensure_ascii=False,sort_keys=True,separators=(",",":")).encode("utf-8")).hexdigest()returnclaimed==actual path=Path("runs/demo-001/contract.json")ok=verify_contract(path)print(f"contract_ok={ok}protected=2")运行输出:
contract_ok=True protected=2三、保护范围为什么必须显式
“不要改测试”只是一句软约束,执行器仍可能开放写权限。protected是宿主强制规则:任何补丁触及这些路径都在应用前拒绝。除了测试,还应保护契约、工具清单、预算和裁判脚本。否则 Agent 可以不修代码,只把允许重试次数改成无限,或把检查命令替换成恒定成功。
保护范围也不能粗暴到整个仓库只读。任务确实需要修改生产代码,过宽保护会让循环没有可行解。工程上先列出“谁给分、谁定目标、谁管权限”,把这些控制面与被修复的数据面分离。控制面只允许人工或独立流水线更新。
四、契约版本与变更流程
客户中途改变需求时,不应就地编辑contract.json。旧任务以contract_changed终止,新建任务编号与新契约,并记录父任务。这样旧尝试不会被新标准重新解释,成本与结论都有明确边界。版本字段负责解析兼容性,摘要负责内容身份,两者不可互换。
验收时先正常验证,再改一个空格、交换数组顺序和修改目标。对象键顺序变化不应影响摘要,数组顺序是否敏感取决于语义;本例对允许路径主动排序,对检查顺序保留,因为先静态检查再测试可能是契约的一部分。
本篇产物runs/demo-001/contract.json将成为下一篇输入。下一篇会读取其中的protected和checks,复制裁判文件到只读快照并生成grader_snapshot.json,让 Agent 即使改了工作区测试,也无法改变真正给分的材料。
参考来源
- Python 文档|hashlib — Secure hashes and message digests
- Dev.to|Loop Engineering: How to Stop Your Agent Reward-Hacking Its Own Checks
👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于《Agent可靠性工程实战》系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。