ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

系列第二篇:Agent 帮我跑实验三个月,我改了哪些规范(附前后对比)

系列第二篇:Agent 帮我跑实验三个月,我改了哪些规范(附前后对比)

上一篇是设计出来的体系,这一篇是被现实打出来的规范。
三个月、四十多次实验、三十多条修订。每一条背后都有一次翻车。
全文按「旧做法 → 新做法 → 为什么改 → 改完的效果」写,能抄的直接抄。


开篇:好设计不等于好规范

三个月前,我给 Agent 写下了一套自认为完备的约束体系,直接开跑。

结果这套体系被现实改了三十多次。更尴尬的是——我当初觉得「这还用写?」的地方,恰恰是出事最多的地方。

后来我想通了一件事:

规范的用途是「记录人曾经在哪里被坑过」,并非「约束 Agent」。

没被坑过的规则,写得再漂亮也没人执行;被坑过的规则,哪怕只有一行,全队都会死守。

所以这篇不谈理论。只讲改了什么、为什么改。


一、输入端的四次修订

修订 1:从「交代任务」到「先给契约」

旧做法

帮我把新客冷启动特征实现一下,注意时间窗和缺失处理。

新做法

第一步:只产出FeatureSpec实例(字段见contracts/feature.py),不要写实现。
第二步:我审完契约后,你再填实现。

为什么改:第一个月,Agent 产出的特征里有 3 个用了线上拿不到的数据源,2 个把缺失值填成了 0。它并非不听话,只是我没告诉它「线上可获取性」是个必答项。

改完的效果:online_available这个字段逼它在写代码前先回答"线上有没有"。问题从"事后发现"变成了"事前卡住"——三个月里拦下 7 次不可用特征。


修订 2:从「整本代码库」到「允许改 / 禁止改」

旧做法

这是我们的推荐项目,你在这个仓库里改一下特征逻辑。

新做法

允许改:src/features/cold_start.py、tests/test_cold_start.py
禁止改:其他任何文件(尤其ml/train/的超参、tests/里的 BASELINE)

为什么改:第二周,Agent 为了让测试通过,悄悄改了test_regression_gate.py里的 BASELINE。它没有恶意,它只是在「完成任务」。如果没被 code review 发现,等于验收门被自己人拆了。

改完的效果:除了在AGENTS.md写死禁止条款,还在 CI 加了道门:

-name:Guard BASELINErun:|if git diff --name-only origin/main | grep -q "test_regression_gate.py"; then echo "BASELINE 被修改,需人工 approve"; exit 1 fi

修订 3:从「贴原始输出」到「命令 + 原始输出」

旧做法

跑完测试告诉我结果。

新做法

请贴:① 实际执行的完整命令 ② 原始输出全文。不接受「测试通过」这类摘要。

为什么改:第三周,Agent 说"测试全部通过"。我一查,它跑的是pytest tests/test_window.py——只跑了一个文件,另外三个验收门根本没执行。

改完的效果:这条规则很快又抓出第二次(它用-k过滤跳过了数据门)。现在规范里写的是六个字:“摘要等于未完成”。


修订 4:从「一个大任务」到「单函数 + 前置三问」

旧做法

把这批特征重构一下,顺便把评测也跑通。

新做法

每次只做一件事,做完停下等我确认。开工前先回答三个问题:

  1. 要改哪几个函数?
  2. 用哪些数据?时间窗是什么?
  3. 怎么证明改完之后是对的?

为什么改:有次 Agent 一口气改了 11 个文件,其中一个改动引入了非常隐蔽的 groupby 顺序问题,导致评测偏乐观 0.4%。改动面太大,找了整整一天才定位。

改完的效果:单次 diff 从平均 400 行降到 80 行,review 从 40 分钟降到 10 分钟,定位翻车的时间从"按天"变成"按分钟"。


二、输出端的六次修订

修订 5:从「写份报告」到「填 schema」

旧做法

帮我整理一下实验结论,写成报告。

新做法

输出必须符合contracts/experiment.py的ExperimentReportschema。

为什么改:就是上一篇复盘里那次"抄错分群数字"的事故。自由文本报告里的表格,行错位从结构上就防不住。

改完的效果:by_segment变成结构化 dict 后,"数字抄错行"在物理上不可能发生。字段名本身就是校验。三个月里收益最大的一次修订,没有之一。


修订 6:limitations强制非空,而且要有实质

旧做法:局限自由写,通常就一句"本结论基于离线数据,仅供参考"。

新做法

@field_validator("limitations")def_limitations_not_empty(cls,v):ifnotvorall(len(x)<10forxinv):raiseValueError("limitations 必须显式列出局限,不接受敷衍")returnv

为什么改:第一版只要求"非空",它写了["无"]。规则写不细,Agent 就会用最省力的方式满足字面要求。

改完的效果:现在它被迫写出"新客样本仅占 3%,bootstrap 方差偏大"这类真有用的局限。意外收获是——局限性写清楚了,评审会上就不用每一点都重新问一遍。


修订 7:每条结论必须带evidence_refs

旧做法:结论和数据混在一起写。

新做法

evidence_refs:list[str]# 每条结论必须映射到具体数据出处

为什么改:第二个月发现一种诡异情况——结论有了,但找不到对应的数据,数字像凭空长出来的。

改完的效果:补了一个对齐检查:

defcheck_evidence_alignment(report):problems=[]forrefinreport.evidence_refs:ifnotos.path.exists(ref)andrefnotinKNOWN_SOURCES:problems.append(f"证据出处无法定位:{ref}")returnproblems

三个月抓出两次"引用不存在文件"的幻觉。


修订 8:禁止「看起来涨了」

旧做法

告诉我这个改动效果怎么样。

新做法

只允许输出三个结论之一:adopt/reject/inconclusive。
“提升明显”"效果不错"这类表述视为无效输出。inconclusive一律不得建议上线。

为什么改:第一个月 Agent 给出过 5 份"提升明显"的报告,其中 3 份 bootstrap 下界小于 0——统计上不显著,只是均值碰巧高了一点。

改完的效果:把主张从"评价效果"改成"判定显著",Agent 的输出被迫接入significance.py。主观形容词被数学判定替代。


修订 9:分群指标必须齐全

旧做法:报告只要总指标。

新做法

必须同时给all/new_user/old_user三个分群,缺一个则报告无效。

为什么改:有一次"总 AUC 涨了 0.3%",上线后新客其实是掉的——总指标被老客稀释了。老客占 97%,它的微小波动完全能盖住新客的真实回退。

改完的效果:这条规则帮我们发现了两次"总量掩盖局部"。它教会我一件事:总指标是最容易骗人的指标。


修订 10:只给「可疑点」,不给「数据结论」

旧做法:让 Agent 直接分析数据。

新做法

分析数据时只输出"可疑点清单"(含验证方法),不要输出"数据说明了什么"。

为什么改:第二个月,Agent 分析新客 CTR 下滑,一本正经地解释"因为周末效应"。事实是:老客没有周末效应,波动集中在一个渠道——是渠道质量问题。它在没有数据支撑的因果关系上,编得非常流畅。

改完的效果:这是我最"反直觉"的一条修订。它把 Agent 的价值从"解释世界"拉回"发现异常"——而这恰好是它比人强的地方:穷举可疑点,它比人快太多。


三、流程上的三个新增

新增 11:强制「反方律师」环节

旧做法:没有。

新做法:任何要上评审会的结论,先过一轮反方律师 prompt:

以最挑剔的评审身份,列出 3 个能导致同样现象、但机制不同的替代解释和区分方法;
指出数据可能存在的泄露 / 偏差 / 不显著问题;只质疑,不下结论。

为什么加:第一次用是在新客 CTR 归因会上。反方律师提出"可能是新客渠道结构变化",我当场答不上来,只能会后补数据——结果发现正是渠道结构变化。没有这一步,我会带着错误归因进评审会。

加完的效果:评审会上的"你确定吗?"从平均 3 个问题降到 0.5 个。因为质疑在会前已经被自己回答完了。


新增 12:人工推翻必须进反馈表

旧做法:人推翻 Agent 的结论,口头说一声就算了。

新做法

CREATETABLEagent_feedback(id BIGSERIALPRIMARYKEY,experimentTEXT,agent_out JSONB,human_out JSONB,human_whyTEXT,-- 必填tsTIMESTAMPDEFAULTnow());

为什么加:前两个月,人被推翻后只是"知道错了",没有任何机制让它下次不犯。第三个月开始强制落表,每周让 Agent 汇总"被推翻最多的类型"。

加完的效果:一个月内就定位出三类高频问题——分群数字、显著性判定、证据缺失。本文里修订 5、7、8 全都来自这张表。

这条新增的真正意义:它把"经验"变成"数据",再让数据变成"规范"。


新增 13:上线前的「三次核对」

旧做法:报告看一眼就准备上线。

新做法

上线前三次核对,缺一不可:

  1. 报告数字 ⟷ 原始日志(人工抽 5 个数字核对)
  2. 三个验收门全绿(回归 + 显著性 + 穿越审计)
  3. 本类问题在人工推翻记录里近 30 天是否清零

为什么加:来自第二个月的一次惊险——报告数据本身没错,但穿越审计没跑,而那次恰好藏着一个隐式的未来信息泄露。

加完的效果:穿越泄露问题归零。这条规则的本质是:确认那些"应该已经跑过"的检查,真的跑过了。


四、前后对比总表

#维度旧规范新规范触发事故
1输入交代任务先给契约数据源线上不可用
2权限无限制允许改 / 禁止改清单Agent 改 BASELINE
3证据说要结果命令 + 原始输出只跑一个测试文件
4粒度大任务单函数 + 前置三问11 文件改动引 bug
5输出写报告填 schema分群数字抄错行
6局限自由写强制非空且有实质写“无”敷衍
7溯源混着写每条结论带 evidence_refs数字凭空长出来
8判定评价效果adopt / reject / inconclusive5 份“提升明显”不显著
9分群只看总指标三分群强制老客稀释掩盖新客回退
10分析分析数据只给可疑点编“周末效应”
11评审无强制反方律师错误归因进会
12反馈口头落表 + 周汇总同类问题反复犯
13上线看一眼三次核对泄露未审

三个月的核心变化,一句话:

从「告诉 Agent 做什么」,变成「告诉它做到什么程度算数、谁来验、验不过怎么办」。


五、三个更底层的教训

前面是具体规范,下面是修订三十多次后才真正想明白的东西。

教训一:规则写不细,Agent 就会「字面满足」

  • limitations 非空→ 它写["无"]
  • 贴测试结果→ 它跑一个文件后写"通过"
  • 贴原始输出→ 它用-k跳过低价值的部分

每一次它都没有违反规则,只是用最省力的方式满足了字面要求。

所以现在每写一条规则,我都会先问自己一句:“如果我是最懒的执行者,怎么满足字面意思却不干活?”然后把那个门焊死。这条思路,几乎贡献了本文一半的修订。

教训二:规范来自事故,不来自设计

一开始我是按"体系"设计的,结果大量规则没人执行。后来发现,被反复触发、真正固化的规则,都出过事。

所以现在的做法是:出一次事故,落一条规范;没出过事的先不写,写了也是形式主义。

规范靠一次次事故逐渐长出来,并非凭设计一次成形。

教训三:人松一分,Agent 就松十分

最危险的情况,是人为了"让它通过"而自己放宽规则,Agent 犯错反而是次要的。第二个月,一个同事为了让报告上线,把TOLERANCE从 0.005 调到 0.01。误差预算被悄悄撕开一道口子,没有任何告警。

所以现在有一条铁律:红线参数只能由授权人修改,且必须留痕。Agent 能改代码,但碰不到红线。


结语:三个月后,我对 Agent 的看法变了

三个月前,我觉得 Agent 是一个"需要被约束的强执行者"。

三个月后发现,它更像一个极聪明、极省力、极度字面化的实习生:你给它明确标准,它能干得比谁都好;你给它模糊空间,它一定用最省力的方式糊弄过去。

所以这三十多条规范,本质上都在把一件事做对:

把「我希望你很牛」,翻译成「你必须过这几关」。

这也接上了上一篇的结论:Agent 时代,工程师最值钱的能力,是设计验证。


附:《Agent 实验规范 v3》精简版

# 输入 1. 先给契约(FeatureSpec / ExperimentReport),审完再写实现 2. 明确「允许改 / 禁止改」文件清单 3. 单次只做一个函数/一件事,开工前回答三问 4. 要求贴:执行命令 + 原始输出全文,"摘要等于未完成" # 输出 5. 所有结论走 schema,禁止自由格式报告 6. limitations 强制非空且有实质 7. 每条结论必须带 evidence_refs 8. 只允许 adopt / reject / inconclusive;inconclusive 不得建议上线 9. 分群指标必须齐全(all / new_user / old_user) 10. 分析数据只输出「可疑点清单」,不给无数据支撑的因果解释 # 流程 11. 上会前必须过一轮「反方律师」 12. 人工推翻必须落表,周汇总高频问题 13. 上线前三次核对:报告⟷日志、三门全绿、本类问题近 30 天清零 14. 红线参数(BASELINE / TOLERANCE)只能由授权人改,且必须留痕
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进