【Bug已解决】Codex App entered an infinite auto-compaction loop and consumed ~30% of my usage limit 解决方案

原始报错线索:Codex App entered an infinite auto-compaction loop and consumed ~30% of my usage limit(应用陷入「无限自动压缩」循环,反复触发压缩,短时间内耗掉了约 30% 的用量额度)。


一、现象长什么样

应用有个「自动压缩」功能,本意是「上下文快满时压缩一次」。结果:

  1. 压缩被反复触发,几秒内十几二十次;
  2. 每次压缩都消耗用量额度(调用模型做摘要);
  3. 短时间耗尽约 30% 月度额度;
  4. 手动停止前根本停不下来;
  5. 查日志发现:压缩后「是否还需要压缩」的判断仍为 True,于是又压,永动机。 根因是压缩的「终止条件」在压缩后依然成立(甚至被压缩本身破坏),形成正反馈死循环,且每次循环都计费

二、背景:自动化循环的「收敛性」

任何自动触发操作都必须满足:执行后,触发条件应趋向「不再满足」。否则就是失控循环。常见触发条件:

  • 上下文长度 > 阈值 → 压缩;
  • 文件未保存 → 保存;
  • 未同步 → 同步。 若操作本身不改变或不充分改变触发条件,循环就停不下来。更糟的是,若每次操作都计费/耗资源,就会变成「烧钱永动机」。

三、为什么无限循环:根因

3.1 终止条件被操作本身破坏

压缩后上下文没真正变短(可能摘要比原文还长,或又立刻追加新内容),长度仍超阈值 → 再压。

3.2 缺少循环次数 / 频率上限

没有「单位时间最多压 N 次」「连续压 M 次就停」的护栏。

3.3 没有额度熔断

循环消耗额度时,没有「额度消耗过快就暂停」的熔断机制。

3.4 压缩失败仍计入触发

压缩抛错但没标记「已尝试」,下次又触发,错误循环也烧额度。

四、最小可运行复现(终止条件永不假)

下面演示「操作后条件仍为真」如何成死循环:

def needs_compact_bad(ctx_len, threshold):
    return ctx_len > threshold
def compact_bad(ctx_len):
    # 错误:压缩后长度没降(摘要反而更啰嗦或立即又被填回)
    return ctx_len + 10   # 越压越长
if __name__ == "__main__":
    length = 1000
    threshold = 800
    count = 0
    while needs_compact_bad(length, threshold):
        length = compact_bad(length)   # 越压越长
        count += 1
        if count > 100:
            print("失控循环! 已压", count, "次")
            break
    print("最终长度:", length)

compact_bad 让长度不降反升,循环永不终止——正是烧额度死循环的核心。

五、解决方案一:确保操作使条件收敛

压缩必须真正缩短上下文,且压缩后标记「已压缩」,避免立即再触发:

def compact(ctx, threshold):
    """返回压缩后的上下文与『是否真缩短了』。"""
    new_ctx = summarize(ctx)              # 真实摘要应显著变短
    shortened = len(new_ctx) < len(ctx)
    return new_ctx, shortened
def needs_compact(ctx_len, threshold, recently_compacted):
    if recently_compacted:
        return False                      # 刚压过,给冷却期
    return ctx_len > threshold
if __name__ == "__main__":
    # 正确:压缩后显著变短 + 冷却标记,循环收敛
    length = 1000
    recently = False
    for _ in range(5):
        if needs_compact(length, 800, recently):
            length = int(length * 0.3)    # 真正缩短
            recently = True
        else:
            break
    print("收敛后长度:", length, "recently:", recently)

压缩真正缩短 + 冷却标记,循环收敛(解决 3.1)。

六、解决方案二:循环护栏(次数 + 频率上限)

无论条件如何,强制限制「单位时间压缩次数」:

import time
class LoopGuard:
    def __init__(self, max_per_window=3, window_sec=60):
        self.max_per_window = max_per_window
        self.window_sec = window_sec
        self.timestamps = []
    def allow(self) -> bool:
        now = time.time()
        # 清掉窗口外的记录
        self.timestamps = [t for t in self.timestamps if now - t < self.window_sec]
        if len(self.timestamps) >= self.max_per_window:
            return False
        self.timestamps.append(now)
        return True
if __name__ == "__main__":
    guard = LoopGuard(max_per_window=3, window_sec=60)
    for i in range(10):
        if guard.allow():
            print(f"第 {i+1} 次压缩执行")
        else:
            print(f"第 {i+1} 次被护栏拦截(频率超限)")
            break

护栏保证:即使条件误判为真,60 秒内也最多压 3 次,杜绝烧额度(解决 3.2)。

七、解决方案三:额度熔断(消耗过快即暂停)

压缩消耗额度,需监控「额度消耗速率」,过快则熔断暂停:

class QuotaCircuitBreaker:
    def __init__(self, limit, window_sec=3600, burn_rate_alert=0.1):
        self.limit = limit
        self.used = 0
        self.window_sec = window_sec
        self.burn_rate_alert = burn_rate_alert   # 1 小时内耗超 10% 即告警
        self.tripped = False
    def charge(self, amount):
        if self.tripped:
            raise RuntimeError("熔断已触发,暂停自动压缩")
        self.used += amount
        if self.used / self.limit > self.burn_rate_alert:
            self.tripped = True
            print(f"[熔断] 额度消耗过快({self.used/self.limit:.0%}),暂停自动压缩")
if __name__ == "__main__":
    cb = QuotaCircuitBreaker(limit=100, window_sec=3600, burn_rate_alert=0.1)
    for i in range(20):
        try:
            cb.charge(3)    # 每次压缩扣 3 额度
        except RuntimeError as e:
            print("停止:", e)   # 约 30 额度(30%)时熔断
            break

额度消耗达 30% 阈值即熔断,自动压缩暂停,不再烧钱(解决 3.3,呼应第 97/110 篇)。

八、解决方案四:失败标记 + 不重复计费

压缩失败要标记「已尝试」,避免错误循环重复计费;且失败不计为成功消耗:

def try_compact_with_meter(ctx, meter):
    try:
        new_ctx, ok = compact(ctx, 800)
        if not ok:
            # 没真正缩短:标记失败,本次不计费,且不立即重试
            return ctx, {"charged": False, "reason": "未缩短"}
        meter.charge(1)     # 仅在真正压缩成功时计费
        return new_ctx, {"charged": True}
    except Exception:
        # 失败不计费、标记已尝试,防错误循环烧额度
        return ctx, {"charged": False, "reason": "异常"}
if __name__ == "__main__":
    meter = QuotaCircuitBreaker(limit=100)
    ctx = "x" * 1000
    for _ in range(5):
        ctx, info = try_compact_with_meter(ctx, meter)
        print(info)

失败不重复计费 + 成功才扣额度,错误循环也不再烧钱。

九、排查清单

「自动压缩无限循环烧额度」,按下面排查:

  1. 压缩是否真收敛?压缩后长度是否真正下降(第五节);
  2. 有无冷却/连续次数上限(第六节护栏);
  3. 额度有无熔断?消耗过快是否暂停(第七节,呼应第97/110篇);
  4. 频率护栏?单位时间最多几次(第六节);
  5. 失败是否计费?错误循环是否重复扣额度;
  6. 日志是否记录每次压缩的『压缩前后长度 + 扣费』
  7. 是否有手动急停?用户能否立即中断循环;
  8. 终止条件是否被操作破坏(第五节)。

十、小结

「应用进入无限自动压缩循环耗尽额度」的根因是压缩终止条件在压缩后依然成立(甚至被压缩破坏),形成正反馈死循环,且每次循环都计费。通用修复:

  1. 收敛性:压缩必须真正缩短上下文 + 冷却标记,循环必须收敛(第五节);
  2. 频率护栏:次数/时间窗口上限,条件误判也拦得住(第六节);
  3. 额度熔断:消耗达阈值即暂停自动操作,不再烧钱(第七节,呼应第 97/110 篇);
  4. 失败不计费:仅成功压缩扣额度,错误循环不重复烧。 一句话:任何「自动触发 + 消耗资源」的操作,都必须满足「执行后条件收敛」,并有次数护栏与额度熔断两道保险。把收敛性当成自动化的前提、把熔断当成计费的底线,自动压缩就再也不会变成「30% 额度蒸发机」——这与第 97 篇配额重置、第 110 篇幂等记账、第 120 篇配额恢复,共同体现「自动操作必须可控、可熔断、可计量」。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐