破解代理循环
观察量化模型自我重复两周教会我的关于防护的知识
梯形图转SCL | 博途AI辅助编程文档 | AI模型价格对比 | AI工具导航 | ONNX模型库 | Vibe Coding教程 | PLC在线仿真器 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo
这里没有什么奇特的模型行为。重复是自回归解码中最古老的已知病理,每个托管API都在默默地保护你免受其害。提供商端点提供给你的是一个采样堆栈:默认温度高于零,通常有重复或频率惩罚,服务器端停止策略,以及在推理模型中对思考token的隐藏限制,无论是否收敛都会结束传递。当你自己运行GGUF时,你得到的是原始的下一个token分布和一个n_predict限制。其他一切都需要你自己构建,而在你构建之前,上面的失败模式就是你所拥有的。
量化加剧了这一问题。压缩到每个参数4位或5位的权重不仅仅损失一点基准精度。它们扰动了logit景观,而贪心解码将每个翻转argmax的扰变变成永久性承诺。如果量化模型对深思熟虑的最可能延续恰好是同一深思熟虑的开头,管道中就没有抖动来打破它,因为你没有要求任何抖动。吸引子不是模型的一种情绪。它是一个确定性函数的不动点,在下一次传递中它仍然是一个不动点。
这引出了一个显而易见的问题:为什么不提高温度就完事了?因为跨轮次的可重复性对其他一切都很重要。plank在轮次之间重用KV缓存,因此只有新后缀会被预填充,保持系统提示的指纹磁盘快照,并维护一个KV梯级阶梯,以便就地转录重写可以恢复编辑前的快照并从中向前扩展。输出取决于采样器种子的模型会将所有这些缓存从速度源转变为漂移源。更糟糕的是,它使保存的重现转储不可证伪,而当你追踪的错误需要五十分钟才能出现时,这是你无法放弃的一件事。所以确定性保持不变,循环就是为此付出的代价。
1、使循环变得困难的那个属性
固定量化权重上的贪心解码使生成传递成为其提示的纯函数。对于循环恢复来说,这就是整个问题:如果一次传递循环了,你通过附加一条消息"你在重复自己,请停止"来响应,下一次传递就会在旧提示加上一条短消息上运行。这不是实质性的改变。模型会推理回到同一个吸引子并再次循环。
plank三次学到了这一点,每条代码路径一次,因为这些路径是分别编写的。
工具调用防护首先学会了它。它检测相同的工具调用(通过名称加上其规范化参数的哈希值),在有界窗口内重复,并从第六次重复开始拒绝调用。转储repro-1788676865显示模型在被拒绝后的六分钟内每次传递都重新发出完全相同的三段诗节,直到用户按下Ctrl-C。二阶错误使情况变得更糟。被拒绝的调用被推入同一个32调用窗口,因此一旦窗口填满,每个新的拒绝就会淘汰一个相同的旧调用,每个签名的计数下降一个并上升一个,反馈文本永远说"11次"。提示在传递之间不仅是相似的,而且是逐字节相同的。修复方法是将被拒绝的调用排除在窗口之外,使用它们自己的单调计数器,并添加防护从未有过的梯级:三个完整拒绝的诗节,连续三个,然后结束轮次。

推理防护接下来学会了它,在主轮次上。转储repro-loop-1788708943和-1788709421是同一会话相隔八分钟。防护停止了循环传递,并将其错误作为工具结果反馈。下一次传递以"需要停止重复。我们已经够了。需要实现。"开头,然后产生了20KB的推理,最终以相同形状的循环结束。第三次传递以"我们需要停止重复。现在实现吧。"开头,在用户退出之前只得到了697字节。在整个会话中没有编辑任何内容。
2、识别有一个延迟下限,而这个下限会咬人
推理检测器是流式传输尾部上的精确周期查找器。它寻找最短周期,其块尾部在k次背靠背中结束,最小周期为12字节(这样空白无法触发),在8 KiB窗口中。两个周期会触发警告,状态栏渲染为一个小循环标记。四个周期停止传递。
仔细阅读,错误已经可见。看到p字节块的k个副本需要在窗口中同时驻留k * p字节,因此搜索受window / cycles限制。因此,两个梯级具有不同的周期上限,来自同一个窗口:两个周期警告时为4096字节,四个周期停止时为2048字节。
任何周期落在这些数字之间的循环对警告可见,但在结构上对停止不可见。不是延迟,不是慢。不可达。
转储repro-1788788326就是这种情况,这是有记录以来最干净的循环。一次46,692字节的推理传递,其中根本没有工具调用:3,466字节的前言,然后是一个2,434字节的块重复17次,每个增量正好是2434字节,没有漂移。占传递的百分之八十九。用户的整个错误报告是状态栏说它在循环,这是完全准确且完全无用的,因为页脚在第二个周期就锁定了,然后看着模型运行到其token上限。
项目文档中有一个表格,测量防护在每个转储中的触发及时性,表中的每个条目都停止在设计的四个周期,成本为0.6到3 KB。该表格只是在悄悄地测量防护能看到的循环。自己仪器中的选择偏差是一种特殊的焦油坑,因为数字看起来很棒,直到有人报告数字无法包含的东西。
明显的修复是更大的窗口,但这是错误的修复。加宽到32 KiB仍然需要四个完整周期来停止,在这种情况下大约10 KB的浪费推理,而且上限只是移动,所以下一个更长周期的循环会重新发现相同的发现。有效的方法是使停止独立于窗口。一旦警告梯级匹配一个块,就保留它,并在每个进一步副本到达时验证:
struct Latched {
/// 重复块,以字节为单位:周期可能拆分多字节字符,因此这永远不被视为文本。
block: Vec<u8>,
/// 最后一个确认副本之后的流偏移量。
end: usize,
/// 到目前为止确认的副本数,计算警告梯级匹配的副本。
cycles: usize,
}
这需要两个周期的窗口而不是四个,并且不设置上限。未匹配的副本,或在可以检查之前滚动出窗口的副本,会丢弃锁存器,然后检查可以自由地锁存到当前正在循环的任何内容上,因此打破并被不同循环替换的循环仍然会被捕获。诱惑是在不验证每个副本的情况下向前计数,这更便宜,并且会将打破循环后的普通散文变成错误停止。有一个测试正是为此命名的,如果你让不匹配分支增加计数器,它就会失败。
针对转储重放,修复后的防护在46,692字节中的12,925字节处停止,留下大约8,500个未生成的token。

3、然后循环变得比窗口更大
两小时后,repro-1788796284。父转录显示四次传递分别为435、1690、2768和4328字节,其中没有循环,最后是一个48字节的助手消息,工具错误显示子代理失败:中断。五十分钟,没有编辑,并且根据防护发布的所有信号,模型从未循环。
循环在一个辅助文件中。子代理的第十四次传递,189,744字节,一个字节精确的循环重复二十次,占传递的百分之九十五,运行到生成上限。而且循环不是一段话。而是一个完整的深思熟虑:退后一步考虑使用子代理,注意到我们已经是一个子代理,将32个叶文件分成三个字母批次,提出一个Python脚本,认为脚本会破坏构建器结构体,得出结论这变得复杂了,退后一步。九千零四十二字节。
两个梯级都视而不见,这归结为一个不等式。锁存器修复移除了停止梯级对窗口的依赖,但锁存需要警告梯级先匹配,而警告梯级仍然需要两个副本驻留。两个9,042字节的副本在8 KiB窗口中是18 KB。没有什么可以锁存到它上面,因此也没有什么可以从中向前计数。之前的发现关闭了窗口四分之一到一半之间的差距。这个发现 simply 是一半以上的所有内容,而关闭第一个差距的修复没有触及它。

两个教训推广到该转储之外。第一个是子代理循环在其父级中是不可见的,因为父转录保存任务字符串和最终工具结果,中间没有任何内容。只读取顶级转储并得出结论"不是循环"是失败模式。第二个更不舒服:循环长度与重复决策的大小成比例,而不是与散文成比例。每个早期转储循环一个句子或一段话,123字节到2.4 KB。这个循环了一个计划。为重复句子设计的检测器在结构上对于重复计划来说尺寸不对,而产生重复计划的请求——"使用子代理处理此32文件重构的独立部分"——正是代理存在的目的。预计周期会继续增长。任何上限是固定窗口固定倍数的检测器都会不断被超越。
4、预算什么都不识别,这就是为什么它们有效
循环检测需要两个副本才能命名一个周期。这是2p的硬延迟下限,当2p超过你的窗口时,它不再是下限而变成了一堵墙。预算两者都没有。它只是停止计数。
所以最后两个梯级根本不识别任何东西。每次传递的推理上限为16 KiB,反馈文本要求模型承诺它倾向于的任何选项,以及每次轮次上限为32 KiB生成,没有任何write、edit、bash或bash_stop调用。第二个是每次传递检查无法成为的梯级。上面的父轮次,有四次短暂且单独合理的传递和五十分钟的无事,通过了有史以来编写的每次传递检查。

大小调整是针对32个保存的转储完成的,涵盖939次生成传递,这些数字就是全部论据。7 KiB的思考预算在大约89秒后触发,捕获7个真实循环,并在923次健康传递中的27次上触发,即2.9%。在16 KiB时,它在大约204秒后触发,捕获5个,并在923次中的1次上触发。不要降低它来购买延迟。七千字节将比率从每个假阳性5个真实循环反转为每个真实循环3.9个假阳性,而对3%良好推理触发的防护是用户一天内就会关闭的防护。这里值得捕获的循环是190 KB、46 KB、45 KB。延迟是便宜的轴。假阳性是昂贵的轴。语料库在其健康尾部也很稀疏,来自两个项目,因此0.11%是一个数量级,而不是一个速率。
反馈中的两个措辞选择比预期的更重要。预算停止报告为停止了超预算传递,而不是循环,因为这是一个更弱的主张,让转储的下一个读者去寻找不存在的循环是后续发现被错误诊断的方式。而给模型的消息要求它承诺,而不是指责它重复自己,因为指责通常是错误的,然后模型会花费token来协调它。
语料库中五个循环传递仍然低于12 KB,两个梯级都无法达到,其中四个漂移:它们根本没有字节精确的周期。在干净循环形成之前,模型倾向于发出一系列共享前缀并在尾部不同的行,一个转储中有25个连续这样的行,另一个中有29个,精确匹配器只在变异消失后才唤醒。针对语料库尝试了一个模糊检测器,一个至少40字节的规范化行在窗口中看到三次,它将在52个普通传递上触发的同时为每个真实停止节省0.5到2.6 KB。如所述不是一个候选。看起来有希望的度量是重复行比率,因为语料库在它上面清晰地分离:健康传递的重复行率为零,而那四个漂移者的重复行率在15%到25%之间。
5、梯级阶梯实际的目的是什么
出现的形状不是计划好的。每个梯级的存在都是因为一个转储证明了前一个不足,每个梯级对下一个看到的东西视而不见。精确周期检测看到重复的段落,但错过重复的计划。锁存周期看到任何周期,但错过漂移循环。每次传递的字节预算看到任何长的东西,但错过由短传递组成的轮次。每次轮次的无进展预算看到没有改变任何东西的轮次,并且作为最粗的梯级,大约需要七分钟才能说出来。它们中没有一个是答案。顺序就是设计。
还有两件事伴随着每个梯级,跳过任何一个都会比检测器的价值更高。第一个是每个防护都带有自己的证据通道,因为每个停顿都无法从磁盘上的内容进行诊断:转储中的时间戳、防护触发时写入的自动转储、子代理辅助文件、父窗口上的红色防护行、页脚中的标记。你事后无法审计的防护是你将通过猜测来调整大小的防护,而语料库正是将"16 KiB感觉差不多"变成"16 KiB是0.11%,7 KiB是2.93%"的东西。
第二个是每个检测器开始时都是建议性的,必须增长一个结束轮次的梯级。这一切底部的确定性不允许更温和的补救措施,而量化本地模型会在你想要这个特定地方的确定性时将其交给你。你可以拒绝操作,注入错误,在系统提示中礼貌地请求,而之后的传递仍然非常接近之前的传递。某些东西必须实质性地改变提示,而实质性不同提示的最可靠来源是键盘前的人。
原文链接:Cracking Agent Loops
汇智网翻译整理,转载请标明出处