AI Agent 成本工程实战:从 OpenAI Codex 的 8 个“烧 Token“Bug 学起

前言

8 月 30 日,OpenAI Codex 工程负责人 Tibo(Thibault Sottiaux)发帖宣布:团队逐条排查了数千份用户报告,修复了 8 类会"额外消耗额度"的问题,并为所有 Codex 和 ChatGPT Work 付费用户重置了使用额度------按使用方式不同,同等额度预计可多支撑 10%~50%。

额度重置是安抚措施,真正有价值的是那份 8 类问题清单。它等于 OpenAI 公开承认:一个月活 2000 万的 Agent 产品,在控制面(谁在花钱、花在哪、怎么停)上踩遍了几乎所有典型的坑。

这份清单是一份免费的故障模式教材。本文把它拆成四族故障、四条设计原则、一套回归测试,最后给一个可以直接移植到你项目里的"Agent 预算守卫"。


一、事件回顾:一次罕见的"官方自曝"

时间线:

量化数字(官方与多方信源一致):个别异常的 /goal 任务曾消耗一周额度的 15%~70% ;旧版 Computer History 在部分场景每周最多吃掉 约五分之一 额度;后台记忆进程曾因停止条件永远无法满足而运行多达 15000 次


二、为什么"少发消息"救不了你的额度

很多人对 Agent 额度的直觉是"我发了多少条消息"。OpenAI 官方帮助文档明确:用量不只取决于消息数或时长,而取决于 token 量、模型选择、任务/上下文规模、本地或云端执行、Fast mode、上下文压缩、工具调用,以及后台任务(reviews、automations、subagents、terminals、memories)。

用户侧证据(社区报告,个案仅供参考):有 Pro 用户分析本地日志,发现 codex-auto-review 约占其记录用量的 75%------消耗发生在用户根本看不见的地方。

三个常见误区:

| 误区 | 真相 | | --- | --- | | "我少发几条消息就行" | 后台任务(记忆/自动化/评审)不经过你的消息计数 | | "上下文压缩是省 token 的" | 压缩本身是一次 LLM 调用;压缩策略有 Bug 时反而反复计费 | | "子任务用便宜模型更省钱" | 子 Agent 若意外升级到高成本模型(Bug ⑤),成本会反向爆炸 |


三、8 个 Bug 的分类学:四族故障模式

把 8 个 Bug 平铺罗列没有信息量,归类之后规律立刻浮现。

A 族 · 上下文管理(Bug ①⑥⑧) :压缩残留旧图导致反复压缩、Computer History 重复总结、MCP 结果重复编码或截断后重取。共同点:同一信息被多次编码、多次计费

B 族 · 后台任务(Bug ②④⑦) :Memory Worker 停止条件永不满足(跑了 15000 次)、自动化运行频率高于配置、普通对话意外触发后台请求。共同点:后台执行体缺少硬终止条件或频率上限

C 族 · 执行控制(Bug ③)/goal 任务越过终止条件继续运行或反复重试,个别消耗周额度 15%~70%。共同点:自主循环没有预算熔断

D 族 · 模型选择(Bug ⑤) :子 Agent 异常升级调用高成本模型。共同点:模型路由缺乏显式声明与约束

这个分类的意义在于:修好 8 个具体 Bug 只能救 OpenAI 的用户,理解 4 族模式才能救你自己的 Agent 项目。


四、四条设计原则:从别人的事故里学架构

原则一:幂等压缩(治 A 族)

对被压缩/总结/编码的内容做内容寻址(hash 去重):同一段内容在会话全生命周期只总结一次,总结结果缓存复用。压缩后清理旧载荷时,必须原子性地"删除旧内容 + 记录已总结标记",两者缺一就会出现 Codex 的 Bug ①(旧图残留 → 下一轮再压缩一次)。

原则二:后台任务两阶段终止(治 B 族)

后台任务的停止条件必须是可满足的状态,而不是"永不触发"的循环判断(Bug ② 的 15000 次就是这么来的)。两阶段终止:第一阶段停止接受新工作,第二阶段等待在途任务收尾;外加一个看门狗(watchdog)------超过最大存活时间强制回收。automations 的调度频率必须以用户配置为硬上限,配置变更时旧任务要重新校准。

原则三:预算熔断器(治 C 族)

任何自主循环(goal、重试、探索)在启动前必须声明 token 预算;运行中累计消耗逼近阈值时先降级(缩小搜索空间/降低频率),越过阈值即硬熔断并汇报。熔断检查必须发生在"发出下一次请求之前"------等响应回来再检查,钱已经花出去了。这正是 Bug ③ 消耗 70% 周额度的根源:终止条件检查在循环边界上失效。

原则四:模型路由白名单(治 D 族)

子 Agent 的模型选择必须显式声明在任务描述符里,且只能从白名单中选;任何"自动选择更强模型"的逻辑默认关闭。D 族 Bug 的本质是隐式授权:主 Agent 调子 Agent 时没有传递成本约束,子 Agent 的"聪明选择"变成了账单炸弹。


五、把事故清单变成回归测试

社区(zeronoise)给出的建议值得直接采纳:把 8 类故障模式当作回归测试套件,在任何无人值守的 Agent 任务上线前跑一遍。

|

| 测试场景 | 断言 | | --- | --- | --- | | 1 | 图片密集会话触发多次压缩 | 同一图片只被编码一次;token 增量有上界 | | 2 | 带 /goal 的任务设置一个必然可达的停止条件 | 任务在条件满足后 N 步内终止 | | 3 | 后台记忆任务运行到最大存活时间 | 看门狗强制回收,无僵尸进程 | | 4 | 自动化任务以自定义频率调度 | 实际触发频率 ≤ 配置频率 | | 5 | 子 Agent 任务描述符 | 模型选择 ∈ 白名单;无隐式升级 | | 6 | 返回超大结果的 MCP 工具 | 结果只编码一次;截断后有标记,不重取 | | 7 | 普通对话轮次 | 无计划外后台请求产生 | | 8 | 记录每次请求的模型与 token 数 | 账单可按任务/来源归因 |

度量三件套:token 增量(分请求记录)、模型选择日志、任务是否按预期终止。没有这三个度量,上面的断言无从谈起------这就引出实战项目。


六、实战项目:写一个 Agent 预算守卫

四个原则浓缩成一个约百行的可移植中间件。接口只做三件事:计量(meter)、限流(enforce)、汇报(report)

bash 复制代码
# budget_guard.py --- Agent 预算守卫(可直接移植的最小实现)
from dataclasses import dataclass, field
from enum import Enum
import time

class GuardAction(Enum):
    ALLOW = "allow"          # 放行
    DOWNGRADE = "downgrade"  # 逼近阈值:降级(提示缩小上下文/换便宜模型)
    BLOCK = "block"          # 熔断:禁止发起新请求

# 模型路由白名单:治 D 族(隐式升级)
MODEL_WHITELIST = {
    "planner":  ["cheap-model"],          # 规划类子任务只允许便宜模型
    "worker":   ["cheap-model", "pro-model"],
    "reviewer": ["cheap-model"],
}
MODEL_COST_PER_1K = {"cheap-model": 0.002, "pro-model": 0.02}

@dataclass
class BudgetGuard:
    token_budget: int                      # 任务级 token 总预算(治 C 族)
    usd_budget: float                      # 任务级美元预算
    watchdog_timeout_s: float = 600        # 看门狗:治 B 族(后台失控)
    used_tokens: int = 0
    used_usd: float = 0.0
    ledger: list = field(default_factory=list)   # 流水:治"账本不透明"
    started_at: float = field(default_factory=time.time)

    def check_model(self, role: str, model: str) -> str:
        """发请求前校验模型白名单,非法请求自动降级到白名单最便宜项。"""
        allowed = MODEL_WHITELIST.get(role)
        if allowed is None:
            raise ValueError(f"未注册的任务角色: {role}")
        if model not in allowed:
            return allowed[0]   # 拒绝隐式升级,显式降级
        return model

    def precheck(self, est_tokens: int, model: str) -> GuardAction:
        """关键点:检查发生在发请求之前,而不是收响应之后。"""
        ratio = (self.used_tokens + est_tokens) / self.token_budget
        cost = est_tokens / 1000 * MODEL_COST_PER_1K[model]
        if time.time() - self.started_at > self.watchdog_timeout_s:
            return GuardAction.BLOCK            # 看门狗超时,硬熔断
        if self.used_usd + cost > self.usd_budget:
            return GuardAction.BLOCK            # 美元预算熔断
        if ratio >= 1.0:
            return GuardAction.BLOCK            # token 预算熔断
        if ratio >= 0.8:
            return GuardAction.DOWNGRADE        # 80% 水位:降级运行
        return GuardAction.ALLOW

    def record(self, role: str, model: str, tokens: int) -> None:
        """每次请求后记账------流水是透明计费的地基。"""
        self.used_tokens += tokens
        self.used_usd += tokens / 1000 * MODEL_COST_PER_1K[model]
        self.ledger.append({
            "ts": time.time(), "role": role, "model": model,
            "tokens": tokens, "cum_tokens": self.used_tokens,
            "cum_usd": round(self.used_usd, 4),
        })

    def report(self) -> str:
        """按角色/模型归因的账单摘要------治 A/B 族的'看不见的消耗'。"""
        by_role: dict = {}
        for e in self.ledger:
            by_role.setdefault(e["role"], 0)
            by_role[e["role"]] += e["tokens"]
        lines = [f"总消耗: {self.used_tokens} tokens / ${self.used_usd:.4f}"]
        for role, tk in by_role.items():
            lines.append(f"  {role}: {tk} tokens ({tk/max(self.used_tokens,1):.0%})")
        return "\n".join(lines)

6.1 集成演示:接进一个最小 Agent 循环

bash 复制代码
def agent_loop(guard: BudgetGuard, goal: str, max_iters: int = 50):
    for i in range(max_iters):
        model = "pro-model"
        model = guard.check_model("worker", model)      # 白名单校验
        est = 2000                                       # 粗估本次请求 token
        action = guard.precheck(est, model)              # 发请求前检查
        if action == GuardAction.BLOCK:
            print(f"[熔断] 第{i}轮:预算耗尽或看门狗超时,终止")
            break
        if action == GuardAction.DOWNGRADE:
            model, est = "cheap-model", est // 2         # 降级运行

        # response = call_llm(model, goal, ...)          # 你的 LLM 调用
        response_tokens = 1800                           # 演示用假数据
        guard.record("worker", model, response_tokens)
        # ... 处理响应、判断 goal 是否达成(终止条件必须可达!)
    else:
        print("[警告] 达到最大迭代仍未终止------检查终止条件是否可满足")
    print(guard.report())

6.2 项目讲解:三个设计决策

  1. 为什么 precheck 在发请求前? 对应原则三。收到响应再检查等于"先花钱再对账",对失控循环毫无约束力。Codex 的 /goal Bug 正是循环边界上的检查失效

  2. 为什么要按角色/模型记账(ledger)? 对应"账本透明"。用户社区最集中的诉求是"按任务/来源归因"------report() 的输出就是你排查"额度去哪了"的第一手证据

  3. 为什么 check_model 返回降级模型而不是抛异常? 对应原则四的工程化表述:白名单违规应当显式降级并记录,而不是让整条任务链失败------成本控制和可用性要同时成立


七、观点:额度透明度是下一个产品分水岭

OpenAI 在公告中承诺后续"在应用中进一步展示额度去向"。我认为这个承诺比额度重置更重要:

  1. Agent 产品的竞争正从"能力"转向"能力 × 成本 × 可控性"三角。 单点跑分的天花板效应越来越明显,而"敢把账本给用户看"会成为企业采购的硬指标------企业要的是可预算、可审计、可熔断

  2. 成本工程不是附加功能,是控制面的一等公民。 Codex 这 8 个 Bug 全部出在控制面(压缩/后台/循环/路由),没有一个出在"代码写得不对"上。这说明 Agent 可靠性工程的主战场已经转移

  3. 给开发者的三条行动清单:① 无人值守任务上线前,跑一遍第五节的 8 条回归测试;② 任何自主循环都接预算熔断(本文 BudgetGuard 可以直接抄);③ 从今天起记录每次 LLM 请求的角色/模型/token 流水------没有流水,一切成本优化都是盲人摸象

一句话收束:这次事件最大的启示不是"OpenAI 也会犯错",而是------当 Agent 开始自主花钱,控制面设计就和中枢能力同等重要。8 个 Bug 是别人交过的学费,四条原则和一套测试是你能省下的学费。


参考资料

  • Tibo(@thsottiaux)官方公告,2026-08-29/30(经多方信源转述)

  • 爱范儿:《Codex 修复 8 类额度异常,付费用户同等额度预计可多用 10%~50%》,2026-08-31

  • zeronoise.ai: Codex's Usage Reset Exposes Unbounded Agent Loops, 2026-08-31

  • OpenAI 社区:Codex Rate Limits Discussion Thread(用户侧证据,社区报告)

  • OpenAI 帮助文档:《在你的 ChatGPT 套餐中使用 Codex》

相关推荐
一点一木2 小时前
🚀 2026 年 8 月 GitHub 十大热门项目排行榜 🔥
人工智能·github
ssshooter2 小时前
现在网页都能提供 MCP 了?!
前端·人工智能·程序员
京东云开发者2 小时前
Claude Code vs Codex 记忆体系深度对比
人工智能
律宏阔3 小时前
微信小程序使用 Orval + OpenAPI 自动生成接口:Axios 兼容踩坑记录
前端·微信小程序
律宏阔3 小时前
微信小程序集成 TDesign 完整记录:解决 NPM packages not found
前端·微信小程序
律宏阔3 小时前
微信小程序 ECharts 瘦身实战:分包异步化 + componentPlaceholder 避开主包 2MB 限制
前端·微信小程序
不加辣椒4 小时前
第 4 章 什么是 Harness Engineering
人工智能
桃西西呀4 小时前
上下文窗口都卷到 100 万了,大模型为什么还在为"位置"发愁?
人工智能·llm·ai编程
阿拉斯攀登4 小时前
SpringBoot整合MQTT:后端订阅设备数据、解析传感器报文
人工智能