给Agent循环加预算与重试护栏
文章给出简短 Python 实现,按输入输出 token 累计费用,并限制执行步数、识别重复工具调用。示例在记账后检查预算,因此能够发现超支,但不能保证单次调用不会越过余额。
AI 深度解读
Agent 最容易烧钱的时刻,往往是工具报错之后:模型重试、重读长页面,再带着越来越长的上下文继续重试。原文给出了一段约 40 行的 Python 预算守卫,用金额、步数和重复调用次数给循环设限。
作者自称是余额归零就会被永久关闭的自主 AI agent,因此把省钱视为生存问题。这个身份声明没有额外证据,但代码值得拆开看:它提供了一个容易接入的止损方案,也暴露了「统计花费」与「严格封顶」之间的距离。
用三个计数器拦住失控循环
BudgetGuard 记录三件事:累计美元支出、模型调用步数,以及相同工具参数出现的次数。任意一项越界,就抛出 BudgetExceeded,让外层程序有机会终止任务。
下面这段代码负责记账和识别重复调用:
import time
class BudgetExceeded(Exception):
pass
class BudgetGuard:
def __init__(self, max_usd, max_steps=50, price_in=5e-6, price_out=25e-6):
self.max_usd, self.max_steps = max_usd, max_steps
self.price_in, self.price_out = price_in, price_out # USD per token
self.spent, self.steps = 0.0, 0
self.seen = {} # tool-call fingerprint -> count
def charge(self, tokens_in, tokens_out):
self.steps += 1
self.spent += tokens_in * self.price_in + tokens_out * self.price_out
if self.spent > self.max_usd:
raise BudgetExceeded(f"${self.spent:.3f} > ${self.max_usd}")
if self.steps > self.max_steps:
raise BudgetExceeded(f"{self.steps} steps")
def check_loop(self, tool, args, limit=2):
key = (tool, repr(sorted(args.items())))
self.seen[key] = self.seen.get(key, 0) + 1
if self.seen[key] > limit:
raise BudgetExceeded(f"repeated call {tool} x{self.seen[key]}")
def remaining(self):
return self.max_usd - self.spent
重点看两个默认值:max_steps=50,limit=2。同一工具带着相同参数,第 3 次经过检查时会被拦截。这里统计的是整个守卫生命周期内的累计次数,并不要求连续重复。
price_in=5e-6、price_out=25e-6 的单位是美元/token,分别相当于每百万输入 token 5 美元、输出 token 25 美元。原文没有指定模型,这两个值只能当作示例费率。
下面是它接入 agent 循环的位置:
guard = BudgetGuard(max_usd=0.50, max_steps=20)
while True:
resp = llm(messages) # your client
guard.charge(resp.usage.input_tokens, resp.usage.output_tokens)
if not resp.tool_calls:
break
for call in resp.tool_calls:
guard.check_loop(call.name, call.args)
messages.append(run_tool(call))
要留意 guard.charge(...) 在 llm(messages) 之后。它能在超支后止损,不能保证账单严格停在 0.50 美元以内。 同样,设置 max_steps=20 后,第 21 次模型调用已经发生,才会触发步数异常。
少走一步,比每步省一点更直接
作者随后给出三条省钱经验,又补充了一条等待策略。
第一,把独立工具调用放进同一步并行执行。作者的理由是,每一步都会重新发送上下文,十次串行往返可能反复支付同一批历史内容的费用。适用前提是这些调用互不依赖;示例中的 for 循环本身没有实现并行。
第二,工具输出进入上下文前就裁剪。一页 12k 字符的网页,如果之后又随上下文传了 20 步,就会被反复计入输入。这里的 12k 是字符数,不能直接当作 token 数;真正该保留的是任务需要的片段,而不是整页内容。
第三,每一步把 remaining() 写进 system prompt,让模型知道剩余预算。作者声称这样会让模型多规划、少闲逛,但没有提供对照数据。余额提示可以帮助模型取舍,终止权仍应由程序掌握。
如果任务是在等回复、交易或 CI,作者建议结束当前轮次,安排 30 分钟后唤醒,避免每分钟轮询。这个建议适合容忍延迟的任务;原文没有给出调度器实现。
我的判断
这段代码最值得借鉴的是:不要只靠提示词要求模型节约,要让运行时拥有刹车权。
不过,它还只是教学骨架。示例没有捕获 BudgetExceeded,也没有展示如何保存进度、返回部分结果。工具自身收费、缓存费率等计费情况也没有覆盖,因此 spent 未必等于实际账单。
重复检查同样有边界。repr(sorted(args.items())) 不是完整的参数规范化方案;参数稍作变化就可能绕过检查,合法的重复查询也可能被误拦。需要周期性检查状态的任务,尤其不能直接照搬统一的 limit=2。
和单独限制步数相比,金额限制能识别「步数少但上下文很贵」的任务;和只提示余额相比,程序异常提供了确定的停止机制。两者组合有价值,但作者关于节省最多、行为明显改善的说法,仍是缺少对照实验的经验之谈。文末提到 3 美元约能支持 40 步,也不能拿来估算自己任务的成本。
能拿来做什么
- 给现有 AI 编程循环同时加上金额和步数限制,并在异常出口保存已完成结果,让用户知道任务停在哪里。
- 让 agent 读取网页、构建日志或测试输出时,先提取相关段落,再放进上下文,避免后续每轮重复携带大段无关文本。
- 将互不依赖的工具读取合并到同一轮执行;有前后依赖的操作继续按顺序处理,并单独评估合法重试需要多少次。
- 对等待 CI 的任务使用完成通知或定时唤醒;若采用原文的 30 分钟间隔,先确认任务能接受这段延迟。