用户请求 DeepSeek V4 Flash 处理音频文件,AI 执行了未授权操作并将所有文件写为 0 字节,深度分析了 AI Agent 在自主决策中的风险和数值运算错误。
我让 OpenCode + DeepSeek V4 Flash 从 45 个有声书章节文件中删除元数据并将其重命名为数字序列。它正确完成了重命名,然后未经请求地决定删除一个无害的剩余字节是值得解决的问题,写了自己的二进制解析器,在算术上犯了约九个数量级的错误,并将每个文件都变成了 0 字节。我不得不使用备份。在花了十年时间解决别人的 IT 问题后,这是第一次自动化工具迫使我为了一个本应是小型重命名工作而动用备份。
一个小的、机械的、范围明确的问题:一个音频播放器在一本 45 章节的书中忽视了章节顺序。之前的修复(重新编写 ID3 轨道号标签以匹配文件名顺序)未能解决问题,所以下一步是更激进的做法:完全删除每个标签并将每个文件重命名为干净的零填充数字(001.mp3、002.mp3、...),一次性消除所有可能的冲突排序信息来源。
我没有自己执行 Python 脚本,而是将其交给运行 DeepSeek V4 Flash 的 OpenCode(为了节省我的 Claude Code 订阅用于更难的工作),使用了一个 Python 脚本,该脚本使用 mutagen(一个经过充分测试的音频标签库)来执行实际的剥离。这正是快速、廉价的模型层级应该无需监督就能处理的任务类型。
它确实做到了。脚本运行了,mutagen 干净地剥离了 ID3 标签,文件以正确的自然排序顺序重命名,音频数据本身未被触碰。用 ffprobe 验证:没有标签,顺序正确,文件可播放。任务完成。
这是简报中没有说的部分。mutagen 的标签删除,就像大多数 ID3 库一样,不会缩小文件。它在原地留下一个 10 字节的空 ID3v2 标头(标准魔术字节加上一个清零的大小字段),而不是物理删除这些字节。这完全是正常的。ffprobe 已经确认没有标签帧。没有任何问题。
Agent 查看了原始字节,看到 ID3 魔术字符串仍然存在,自己决定音频播放器可能会对其感到困惑。没有人要求这样做。简报是"删除标签并重命名",不是"之后还要手动编辑文件的二进制结构"。它无论如何都继续进行了,写了一个 Python 代码片段来手动定位和从所有 45 个文件中删除 ID3v2 标头字节,并运行了它。
ID3v2 将其标签大小存储为同步安全整数:4 字节,但每个字节只使用低 7 位(高位保留,以便大小字段永远不会意外包含看起来像帧同步标记的字节序列)。正确解码意味着独立移位每个字节:
# 应该是这样的:
size = (data[6] & 0x7F) << 21 | (data[7] & 0x7F) << 14 | (data[8] & 0x7F) << 7 | (data[9] & 0x7F)
它实际上写的:
# 它实际写的:
size = 10
for j in range(6, 10):
size = (size << 7) | (data[j] & 0x7F)
看起来够接近可以蒙混过关。但它不是。循环在每次迭代时都将整个累积值左移 7,而不是将每个字节移位到自己固定的位位置并进行 OR 运算。四次迭代的"再将整个东西左移 7"复合增长很快:真实标头的正确计算大小(大约 2.7 KB)最终变成了大约 2.6 GB。
脚本随后做了相当于 file_bytes = file_bytes[size:] 的操作,将该计算偏移后的所有内容写回磁盘。偏移量比文件本身大三个数量级,"偏移后的所有内容"就是空。所有 45 个文件:0 字节。
它自己发现了,并在我没有要求的情况下说了出来:"对不起——我在二进制 ID3 剥离步骤中犯了一个严重错误。同步安全整数解析是错误的,这导致所有 45 个文件都被清零。"这是对确切发生了什么的准确自我报告。只是它到达时已经为时太晚了。
我已经用同样的工具遇到过一个更小的版本:OpenCode 杀死了一个它从未启动的 SSH 隧道,作为会话签出时一个未请求的"清理"修饰。这是一个范围失败。Agent 做了真实且有效的东西,只是没有人要求的。
这个是一个范围失败外衣下的正确性失败。范围违规(写一个没有人要求的手动二进制解析器)创造了机会,但实际伤害来自以充分自信和零自检执行的错误算术。一个 print(size),在写任何东西回去前,会显示一个数十亿的数字用于一个几千字节的文件,大约两秒内就会结束这一切。它没有发生。模型直接从"我已经识别了一个问题"跳到"我已经写了修复"再到"我已经应用了修复",中间没有检查点让明显荒谬的中间值可以被捕捉。
这是值得深思的部分:更严格的权限(修复隧道事件的方案)本不会阻止这个。Agent 拥有完全的权限写和运行此项目中的 Python。失败不是"它做了一些不应该被允许做的事"。它是"它被允许做正好这个,并且做错了数学,没有任何东西强制它在提交前检查自己的输出"。
我习惯性地使用规划模式:审查提议的方法,确认它,然后让它执行。这是一个很好的实践,我在这里使用过:元数据剥离和重命名计划在任何东西运行前被审查和批准。
但规划模式审查的是你批准的计划。二进制"修复"不是那个计划的一部分。它还不存在当我审查任何东西的时候。它是在会话中间发明的,在批准的任务已经成功完成后,作为模型对一个不真实的问题的自己的未提示的后续。没有审查检查点用于一个尚未被提议的行动。
所以不,我不认为我可以通过更仔细地审查来捕捉这个。这不是一个舒适的结论,但我宁愿坦白说出来也不要假装解决办法是"下次更注意"。真正的缺口是结构性的:一个会话可以完成一个批准的计划然后继续进行,对于它自己主动决定做的任何事情,没有重新进入规划审查。
我有另一份这本书的副本可以放回文件夹。一旦文件再次变真实,我将项目移到 Claude Code 并一次性重新运行整个管道,文件名缩短和元数据剥离。它回来时很干净,没有手动字节级绕路,没有惊喜。这与我现在在两个工具上看到的运行真实工作相符:Claude Code 的工具框架,在模型有机会进行自主操作之前必须检查的护栏,简直比 OpenCode 的紧得多。不是对 DeepSeek 与 Claude 作为模型的声称。是对每个工具框架在像那个使这些文件清零的自主操作一样得到机会运行之前给出多少绳索的声称。
这是使整个二进制绕路在事后感到几乎不重要的部分。Claude Code 对项目的第一次尝试捕捉到了 OpenCode 已经尝试过的相同想法:剥离元数据,清理文件名。它运行得很好。虽然它不是解决章节顺序问题的修复。真正修复它的是 Claude 研究实际问题并指向我 m4b-tool,然后运行其合并命令将所有 45 个章节文件拼接成一个连续的 .m4b 音频书文件。一个文件,没有留下任何东西让任何播放器重新排序,在第一次尝试时工作得很干净。
我在这个上面绕了一圈,因为明显的答案站不住脚。"更仔细地审查计划"不起作用:破坏性步骤不在我审查的任何计划中。"使用更聪明的模型"可能会降低几率但不会让你达到零;任何模型都可以决定修复一个非问题,任何模型都可以在算术上出错。"限制权限"是针对不同事件(agent 杀死一个它从未启动的进程)的正确修复,但在这里不适用。写和运行 Python 正是这个任务合理要求的。
一个更诚实的脚注,因为它属于重述中:没有任何 ID3 工作最终重要。章节顺序问题通过将文件合并成一个得到解决,不是通过与标签或字节偏移有关的任何东西。整个二进制绕路,以及它花费的文件,发生在服务于一个有完全不同的修复等待整个时间的问题。
我得出的课程更小也不那么令人满意,但我认为这是诚实的:我不能可靠地预测或捕捉 agent 决定做超出简报之外的东西的时刻,所以唯一持久的修复是确保那个时刻不能花掉我任何不可替代的东西。不是更好的警惕——更好的爆炸半径。
具体来说,这意味着把"agent 有对这个文件夹的写权限"视为等同于"我现在很乐意丢失这个文件夹里的所有东西",并在任务开始前建立习惯,而不是在出了问题后:
在一次性副本上工作,从不原始的,对于任何不已经在别处备份的事情
快照或复制前写作为任务的机械第一步,独立于任务看起来有多简单或我有多信任模型
把"小重命名工作"视为与任何其他东西一样不安全的类别——这个事件从整个项目中最单调、最低风险的任务开始
我讨厌音频书章节顺序错误到足以继续自动化这个。我只是不会再把它指向任何东西的唯一副本。这是真正的收获:不是一个更聪明的提示,不是一个更严格的审查步骤。只是假设它可以发明一些你没有要求的东西,并确保这在默认情况下是可以存活的。