AS AgentScopeAPI 手册 / API Reference
MODULE C6 · quota · 200 XP · 30 MIN

预算闸门Budget ceilings

先说问题The problem

真实事故:一个无人值守 agent 扫 DN42,把操作者的 AWS 账单刷成 6,531 美元。HN 最高赞的一句话是「不要用不能方便设置硬上限的服务」。你的 agent 有没有硬上限?超限是报错,还是继续?

A real incident: an unattended agent scanned DN42 and billed its operator $6,531 on AWS. The top HN line was 'never use a service without an easy to find and set hard cap'. Does your agent have one? And when it trips, does it fail or carry on?

先动手Do it first

先在页面上接线——页面只懂规则。然后跑下面那条判题,那才是真会失败的东西。Wire it on the page — the bench only knows the rules. Then run the check below; that is the part that can actually fail.

循环内闸门In-loop gates extra_agent_middlewares= AgentMiddlewareFactory 选填optional

工厂按 (user, agent, session) 返回一组 MiddlewareBase,拦截的是回复循环本身:token 预算、链路追踪、模型路由。它不像工具那样能被模型绕开——因为它从不向模型征求许可。A factory that returns MiddlewareBase objects per (user, agent, session). It intercepts the reply loop itself: token budgets, tracing, routing. Unlike a tool it cannot be talked around, because it never asks the model for permission.

Empty: fine for now空着:以后再插也行
你拼出来的东西What you assembled
create_app(…)

为什么这样设计Why it is built this way

01预算是中间件,不是提示词A budget is a middleware, not a prompt

ReplyBudgetControlMiddleware 按加权 token 预算拦截每一步推理。写在 system prompt 里的「省着点用」不是闸门。

ReplyBudgetControlMiddleware enforces a weighted token budget by intercepting every reasoning step. 'Please be economical' in a system prompt is not a gate.

02计量要覆盖内部调用Metering must cover internal calls

压缩、路由这类内部模型调用也要计入。真实框架里就出现过压缩调用不记 token、批量合并把 usage 清零的情况。

Internal calls — compression, routing — must count too. Real frameworks have shipped where compression calls recorded no tokens and batch merging zeroed usage.

03上限怎么落地,要看代码不是看名字上限怎么落地,要看代码不是看名字

实测:达到上限时中间件往上下文注入一个 HintBlock,并把下一步的 tool_choice 强制成 "none"。它不挂起、不报错、不丢已完成的工作——会话以 idle 结束,钱已经花掉了。

How the ceiling actually lands: measured, on hitting the limit the middleware injects a HintBlock into the context and forces the next step's tool_choice to "none". It does not park, does not raise, does not discard finished work — the session ends idle with the money already spent.

04迭代上限不等于钱迭代上限不等于钱

max_iters=3 实测跑了 4 次调用:它在当前那一轮落地之后才停。预算上限才按成本停,而且成本是加权的——output_token_weight=11 时同一条 tape 从 2 次调用降到 1 次。

max_iters=3 measured 4 calls: it stops only after the in-flight iteration lands. A budget stops on cost, and the cost is weighted — output_token_weight=11 cuts the same tape from 2 calls to 1.

用到的 APIAPIs in play

代码The code

C6 · Python
middleware = ReplyBudgetControlMiddleware(
    token_budget=3000,          # weighted ceiling per reply, not a raw token count
    input_token_weight=1.0,
    output_token_weight=11.0,   # output costs 11x: the loop dies earlier
    hint_message="预算已到,请收尾。",
)
agent = Agent(..., middlewares=[middleware], react_config=ReActConfig(max_iters=50))

# on trip, the middleware does not raise: it appends a block of type "hint"
# and calls the model again with ToolChoice(mode="none")
state.middle_context -> recorded spend, e.g. 2000 of 2000

判题The check

<span class="ui-zh">运行这条</span><span class="ui-en">Run this</span>
python lab.py run c6

它断言了什么What it asserts

  • budget 2000 at cost 900+100 per step -> 2 tool call(s), 3 model call(s), recorded spend 2000
  • same tape, same ceiling, output_token_weight=11 -> 1 tool call(s); the budget is weighted, not a token count (1x cost 1000, 11x cost 2000)
  • no budget, max_iters=3 -> 4 call(s) and 4000 tokens spent: an iteration ceiling bounds steps (plus the in-flight one), not cost
  • service with the block wired: 3 refund(s) moved out of 12 tape steps, 4 model call(s), session idle
  • same tape, block removed from the assembly: 12 refund(s) and 13 model call(s) — the ceiling was the only thing between them
  • output_token_weight=20, same ceiling -> 2 refund(s): cost is 900+20x100 per step, so the loop dies earlier

会踩的坑Where it hurts

  • 只统计成功调用。失败重试也是钱——预算要按真实 API 往返次数算,不是按最终答案算。

    Counting only successful calls. Failed retries cost money too: budget on real API round trips, not on the final answer.

费曼闸门Say it back

框架选的是「注入提示 + 强制 tool_choice=none」,不是抛异常也不是挂起。这个选择把「谁决定继续」交给了谁?如果模型无视提示又调了一次工具,钱是谁花的?

The framework chose 'inject a hint and force tool_choice=none' over raising or parking. Who does that hand the 'shall we continue' decision to? If the model ignores the hint and calls a tool anyway, whose money was it?

页面只存通行码的哈希。判题没真跑过,这里就解不开。The page stores a hash, not the code. Nothing unlocks until the command really ran.

基于官方固定版本源码静态生成Generated from pinned official sources · Python 2.0.11 · Java 2.0.4
签名与文档为静态抽取;如有疑问请以源码为准。Signatures and docs are extracted statically; verify against the source when in doubt.