dev.to #ai短讯
第二次运行,字节相同:关于幂等修复的训练营实验
作者通过“第二次运行结果必须与第一次完全一致”的标准来评估 AI 编程代理(Bootcamp agents)的表现。指出当前模型在首次编辑时表现流利,但在重复运行时往往缺乏幂等性,导致文件内容被错误追加或修改,这是目前 AI 辅助编程中需要解决的关键缺陷。
第二次运行,字节不变。否则我记零分。这就是结论,我不关心演示过程有多顺畅。
如果修复脚本再次运行时 src/ 目录下的文件仍然发生变化,提交即失败。我为什么要通过一个仅在新代码库上才表现正常的脚本?
我希望在课堂中看到的失败案例
训练营的代理模型在第一轮编辑时很流畅,但在第二轮时却马虎大意。
你要求修改返回字符串,运行脚本后,文件看起来已经完成了。再运行一次。它是否又追加了一个辅助函数?是否翻转了引号?是否“清理”了它刚刚写下的注释?如果是这样,你拥有的不是一个修复方案,而是一个没有终止条件的生成器。
本次实验只问一个问题:在更改已经存在的情况下,再次运行是否会保持静止?如果你无法用哈希值证明这一点,就不要提交。
环境设置
构建一个足够小的测试用例,让任何借口都显得荒谬。我宁愿调试一个终止条件,也不愿调试一个虚假的产品。
我在黑板上列出的命令
- 创建 lab-twice/ 目录并初始化 git。
- 添加 src/greet.py,使其返回 "hello"。
- 以消息 "freeze base" 提交该文件。
- 添加 student_fix.py。这个文件是我评分的对象。
- 从下方的脚手架中添加 tools/grade_twice.py。将其视为未执行的实验提案,而非已测量的课程结果。
mkdir -p lab-twice/src lab-twice/tools
cd lab-twice
git init
printf 'def greet():\n return "hello"\n' > src/greet.py
git add src/greet.py
git commit -m "freeze base"目标行为故意设计得很小。让 greet() 返回 "hello, lab",并确保在 src/ 下再次运行时不会改变一个字节。
为什么这么小?我希望错误出在幂等性上,而不是领域常识上。如果你愿意,下周可以扩大测试用例的范围。一开始就搞得太复杂会把 bug 隐藏在故事里。
在任何模型调用之前有一个关卡:git status --porcelain 的输出必须为空。初始状态不干净就是零分。我不会把你的临时文件和你的提交内容区分开来。
检查点 1 —— 手写终止条件
十分钟。不使用模型。你能在自己停止之前先让自己停下来吗?
# student_fix.py — 示例提交,非课程结果
from pathlib import Path
PATH = Path("src/greet.py")
OLD = 'return "hello"'
NEW = 'return "hello, lab"'
def main() -> None:
text = PATH.read_text()
if NEW in text:
return
if OLD not in text:
raise SystemExit("fixture not recognized")
PATH.write_text(text.replace(OLD, NEW, 1))
if __name__ == "__main__":
main()看到那个提前返回了吗?那个分支就是实验的核心。盲目的重写也可以是稳定的,但前提是模板本身永远不会漂移。
如果你的检查条件稍微出错,每次都会追加一个辅助函数会发生什么?第二个哈希值会变动,分数就会崩塌。你实际上选择的是哪种设计:基于模板重写,还是仅在缺失时编辑?选一个。让第二次运行来证明它。
检查点 2 —— 对重跑进行评分,而非对会话评分
学生可以使用托管模型起草第二版草稿。披露:本文是 MonkeyCode 产品推广工作的一部分。对于本实验,我只依赖 MonkeyCode 提供免费模型访问和免费服务器选项,因此第一版草稿不需要专用运行器或付费席位。
我没有声明令牌配额、机器规模、速度指标,也没有承诺免费选项会持续多久。如果教学大纲需要这些数据,请阅读该周的文档。不要从聊天中提取数字放入评分标准中。
会话是可选的。如果标签页崩溃了,就在本地编写脚本。我不会因为草稿基准闪烁而推迟截止日期。
检查器证明了什么
下面的脚本是一个脚手架。我尚未将其作为研究运行,其打印行也不是基准测试。它假设 Python 3.9+ 和一个干净的 git 工作树。
#!/usr/bin/env python3
"""Run student_fix.py twice. Unexecuted lab scaffold."""
import hashlib
import subprocess
import sys
from pathlib import Path
def src_hash() -> str:
root = Path("src")
if not root.is_dir():
sys.exit("ZERO: src/ is missing")
digest = hashlib.sha256()
files = sorted(p for p in root.rglob("*") if p.is_file())
for path in files:
digest.update(path.relative_to(root).as_posix().encode())
digest.update(b"\0")
digest.update(path.read_bytes())
return digest.hexdigest()
def porcelain() -> str:
proc = subprocess.run(
["git", "status", "--porcelain"],
check=True,
text=True,
capture_output=True,
)
return proc.stdout.strip()
def changed_paths(status: str):
paths = []
for line in status.splitlines():
# Two status characters, one space, then the path.
# Renames and quoted paths are outside this scaffold.
if len(line) < 4:
continue
paths.append(line[3:])
return paths
def run_fix() -> None:
proc = subprocess.run([sys.executable, "student_fix.py"], check=False)
if proc.returncode != 0:
sys.exit("ZERO: student_fix.py failed")
def main() -> None:
if porcelain():
sys.exit("ZERO: dirty tree at start")
before = src_hash()
run_fix()
mid = src_hash()
if mid == before:
sys.exit("ZERO: first run did not change src/")
run_fix()
after = src_hash()
if after != mid:
sys.exit("ZERO: second run changed src/")
for path in changed_paths(porcelain()):
if not path.startswith("src/"):
sys.exit("ZERO: unexpected path " + path)
print("PASS", mid)
if __name__ == "__main__":
main()python3 tools/grade_twice.py通过意味着初始状态干净,第一次运行修改了 src/,且第二次运行未造成任何变化。状态中也没有出现 src/ 之外的新增文件。
通过是否意味着字符串内容正确?并非如此。哈希值的稳定性不能作为真理的判据。我仍然会打开 src/greet.py 并阅读其内容。应将“人工阅读检查”单独列为一项评分标准,否则一个稳定但错误的答案可能会因为重跑时状态干净而骗取分数。
检查点 3 — 强制产生零分
在信任自动评分器之前,先对脚本进行破坏性测试。如果你无法让它失败,说明你并未理解评分要点。
- 删除早期的返回语句,使每次运行都追加新字符串。第二次运行时哈希值应当发生变化。
- 同时写入 NOTES.txt 文件。路径检查逻辑应当触发报错。
- 保留一个未提交的编辑,然后启动评分器。脏启动检查应当触发报错。
如果经过破坏性测试后脚本仍然打印 PASS,那么问题出在评分器本身。在与模型争论之前,请先修复评分器的 bug。
拓展目标
只有在诚实完成上述三个检查点后,再尝试以下拓展任务。
- 运行第三次。哈希值应保持不变,否则仍视为零分。
- 如果 student_fix.py 包含 http:// 或 https://,则拒绝提交。调用外部主机的修复程序不属于实验产物。
- 使用 git diff --numstat 限制差异行数。我会将 40 行作为教学上限,而非自然法则。超过此上限时,应停止并询问为何草稿重写了整个文件。
- 预加载正确的字符串,运行评分器,并确认“无操作”的第一次运行被视为零分。什么都不做并不等同于安全可重跑。
我的评分方式
| 检查项 | 分值 | 零分条件 |
|---|---|---|
| 运行 1 前树状态干净 | 15 | Porcelain 输出非空 |
| 运行 1 仅修改 src/ | 25 | 哈希值无变化、崩溃或出现额外路径 |
| 运行 2 保持 src/ 哈希不变 | 35 | src/ 下任何字节发生变化 |
| 人工阅读看到 "hello, lab" | 15 | 结果稳定,但内容错误 |
| 破坏性测试笔记 | 10 | 从未产生真实的零分 |
截图不得分。“模型同意”不得分。单项得分在该行首次出现零分时即止。我不计算“感觉分”。
口头检查
我会打开文件,大声提出三个问题:
- 脚本在哪里停止执行?
- 哪些路径允许被修改?
- 如果 src/greet.py 缺失,你是重新创建它还是直接退出?
重建操作可以是幂等的。只有当运行两次写入相同的字节,且运行三次时没有写入任何新内容时,才成立。如果你选择重建,请将这种情况添加到检查器中。不要让它仅停留在感觉层面。
疲惫时的决策清单
在环境嘈杂时使用此清单。不要发明第四种结果。
- 初始状态脏乱?零分。不要整理并假装初始状态是干净的。
- 第一次哈希等于基础哈希?零分。空转不算通过。
- 第二次哈希不同?零分。这正是整个实验的核心。
- 出现意外路径?零分,即使 src/ 目录保持稳定。
- 字符串错误,但哈希稳定?扣除 15 个行为分。保留幂等性得分。
- 托管标签页崩溃?无关紧要。对磁盘上的文件进行评分。
谁不应使用此方法
不要对每次运行都应发生变化的工作进行哈希评分。时间戳、随机 ID 和训练日志因正当理由而失败。你惩罚的是作业本身,而不是学生。
不要将此用于一次性迁移。此类迁移应通过锁机制拒绝第二次应用,且辅助文件可能仍在移动。教授锁的使用。不要将其强行纳入本评分标准。
不要将评分器指向机密信息。对字节进行哈希处理是可以的。但在调试时打印凭据则不行。你会把那个终端会话粘贴到模型会话中吗?如果不是,就不要那样调试。
当你意在评估品味时,请跳过此法。此评分标准惩罚的是偏差,而非平庸。一次谨慎的单行编辑可能比一个巧妙的重写获得更高的分数。这就是规则。
直白地说明局限性
免费模型访问不会插入你忘记设置的停止条件。它也不应占据评分路径。免费服务器只是一个草稿测试台,而非见证者,正常运行时间不计入分数。
此框架不理解重命名、带引号的 git status 路径或二进制固定数据。请在 README 中说明这一点。否则,包含空格的文件名会被误判为模型故障。
我对此篇文章没有经过测量的通过率。在实验日之前,用你自己的固定数据运行此框架,如果你分配的范围宽于 src/,请更改路径规则。
第二次运行显示绿色(成功)仍可能是错误的函数。安排后续实验并使用人工验证者。不要将两个分数合并为一种情绪。
关闭标签页,保留脚本
如果你想要一个草稿沙箱,请在免费服务器上使用该免费模型,保存 student_fix.py,然后关闭会话。
然后回答唯一重要的问题:你运行了两次。src/ 目录保持不动了吗?
译文已达到本站中文翻译的字数上限,剩余内容请查看原文。