← 返回信息流

dev.to #ai短讯

第二次运行,字节相同:关于幂等修复的训练营实验

dev.to作者:Charlie Xu教程AI评分:50/100

作者通过“第二次运行结果必须与第一次完全一致”的标准来评估 AI 编程代理(Bootcamp agents)的表现。指出当前模型在首次编辑时表现流利,但在重复运行时往往缺乏幂等性,导致文件内容被错误追加或修改,这是目前 AI 辅助编程中需要解决的关键缺陷。

第二次运行,字节不变。否则我记零分。这就是结论,我不关心演示过程有多顺畅。

如果修复脚本再次运行时 src/ 目录下的文件仍然发生变化,提交即失败。我为什么要通过一个仅在新代码库上才表现正常的脚本?

我希望在课堂中看到的失败案例

训练营的代理模型在第一轮编辑时很流畅,但在第二轮时却马虎大意。

你要求修改返回字符串,运行脚本后,文件看起来已经完成了。再运行一次。它是否又追加了一个辅助函数?是否翻转了引号?是否“清理”了它刚刚写下的注释?如果是这样,你拥有的不是一个修复方案,而是一个没有终止条件的生成器。

本次实验只问一个问题:在更改已经存在的情况下,再次运行是否会保持静止?如果你无法用哈希值证明这一点,就不要提交。

环境设置

构建一个足够小的测试用例,让任何借口都显得荒谬。我宁愿调试一个终止条件,也不愿调试一个虚假的产品。

我在黑板上列出的命令

  1. 创建 lab-twice/ 目录并初始化 git。
  2. 添加 src/greet.py,使其返回 "hello"。
  3. 以消息 "freeze base" 提交该文件。
  4. 添加 student_fix.py。这个文件是我评分的对象。
  5. 从下方的脚手架中添加 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 前树状态干净15Porcelain 输出非空
运行 1 仅修改 src/25哈希值无变化、崩溃或出现额外路径
运行 2 保持 src/ 哈希不变35src/ 下任何字节发生变化
人工阅读看到 "hello, lab"15结果稳定,但内容错误
破坏性测试笔记10从未产生真实的零分

截图不得分。“模型同意”不得分。单项得分在该行首次出现零分时即止。我不计算“感觉分”。

口头检查

我会打开文件,大声提出三个问题:

  • 脚本在哪里停止执行?
  • 哪些路径允许被修改?
  • 如果 src/greet.py 缺失,你是重新创建它还是直接退出?

重建操作可以是幂等的。只有当运行两次写入相同的字节,且运行三次时没有写入任何新内容时,才成立。如果你选择重建,请将这种情况添加到检查器中。不要让它仅停留在感觉层面。

疲惫时的决策清单

在环境嘈杂时使用此清单。不要发明第四种结果。

  1. 初始状态脏乱?零分。不要整理并假装初始状态是干净的。
  2. 第一次哈希等于基础哈希?零分。空转不算通过。
  3. 第二次哈希不同?零分。这正是整个实验的核心。
  4. 出现意外路径?零分,即使 src/ 目录保持稳定。
  5. 字符串错误,但哈希稳定?扣除 15 个行为分。保留幂等性得分。
  6. 托管标签页崩溃?无关紧要。对磁盘上的文件进行评分。

谁不应使用此方法

不要对每次运行都应发生变化的工作进行哈希评分。时间戳、随机 ID 和训练日志因正当理由而失败。你惩罚的是作业本身,而不是学生。

不要将此用于一次性迁移。此类迁移应通过锁机制拒绝第二次应用,且辅助文件可能仍在移动。教授锁的使用。不要将其强行纳入本评分标准。

不要将评分器指向机密信息。对字节进行哈希处理是可以的。但在调试时打印凭据则不行。你会把那个终端会话粘贴到模型会话中吗?如果不是,就不要那样调试。

当你意在评估品味时,请跳过此法。此评分标准惩罚的是偏差,而非平庸。一次谨慎的单行编辑可能比一个巧妙的重写获得更高的分数。这就是规则。

直白地说明局限性

免费模型访问不会插入你忘记设置的停止条件。它也不应占据评分路径。免费服务器只是一个草稿测试台,而非见证者,正常运行时间不计入分数。

此框架不理解重命名、带引号的 git status 路径或二进制固定数据。请在 README 中说明这一点。否则,包含空格的文件名会被误判为模型故障。

我对此篇文章没有经过测量的通过率。在实验日之前,用你自己的固定数据运行此框架,如果你分配的范围宽于 src/,请更改路径规则。

第二次运行显示绿色(成功)仍可能是错误的函数。安排后续实验并使用人工验证者。不要将两个分数合并为一种情绪。

关闭标签页,保留脚本

如果你想要一个草稿沙箱,请在免费服务器上使用该免费模型,保存 student_fix.py,然后关闭会话。

然后回答唯一重要的问题:你运行了两次。src/ 目录保持不动了吗?

译文已达到本站中文翻译的字数上限,剩余内容请查看原文。

阅读原文