Token、时间与费用预算:调用前预留,返回后结算
正在读取学习记录…
1. 三种预算有不同单位
Agent 每多走一轮,就可能多发一次模型或工具请求。Token表示模型处理文本的用量,时间表示用户等了多久,费用表示按计价规则产生的金额。输入和输出都会占用Token;具体费用还可能区分缓存输入等档位。
这像出门办事要检查电量、钱包和时间:每一项都要够。程序把三种单位分别记录,例如100个Token、50分钱、10秒,分别比较。
预留是在发送前占住预计额度,结算是在返回后修正余额。这样下一次调用能在出发前判断额度是否足够。下面数字用于手算,真实金额可依据 供应商计价说明和返回用量计算。
2. 手算后再运行
Budget(tokens=100, cents=50, seconds=10) 设置任务的初始额度:100个Token、50分钱和10秒总时限。调用 reserve 时,tokens 和 cents 是本次想预留的用量,estimated_seconds 是本次预计耗时,elapsed 是任务已经用掉的时间。例如预计再用3秒、已经用了3秒,就拿6秒与10秒总时限比较。实际调用时还要通过请求超时控制等待时间。
先在 PowerShell 创建并进入学习目录:
mkdir "F:\学习\l3-budget" -Force
cd "F:\学习\l3-budget"保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。
在新文件夹创建 budget.py,写入完整代码:
先在 PowerShell 创建并进入学习目录:
mkdir "F:\学习\l3-budget" -Force
cd "F:\学习\l3-budget"保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。
"""用三个独立额度控制调用,并在返回用量后退回预留差额。"""
import json
import math
def amounts(tokens, cents):
# 数量使用非负整数,便于按统一单位结算。
if any(type(value) is not int or value < 0 for value in (tokens, cents)):
raise ValueError("Token与金额须为非负整数")
def duration(seconds):
# 秒数可有小数,但须为有限的非负数。
if type(seconds) not in (int, float) or not math.isfinite(seconds) or seconds < 0:
raise ValueError("秒数须为有限的非负数")
class Budget:
def __init__(self, tokens, cents, seconds):
amounts(tokens, cents)
duration(seconds)
self.tokens = tokens
self.cents = cents
self.seconds = seconds
def reserve(self, tokens, cents, estimated_seconds, elapsed=0):
amounts(tokens, cents)
duration(estimated_seconds)
duration(elapsed)
# 各单位分别比较;一次调用须同时满足三个条件。
if tokens > self.tokens or cents > self.cents:
return False
if elapsed + estimated_seconds > self.seconds:
return False
self.tokens -= tokens
self.cents -= cents
return True
def settle(self, reserved_tokens, reserved_cents, used_tokens, used_cents):
amounts(reserved_tokens, reserved_cents)
amounts(used_tokens, used_cents)
# 差额回到余额。实际用量超过预留时,余额也会相应减少。
self.tokens += reserved_tokens - used_tokens
self.cents += reserved_cents - used_cents
if __name__ == "__main__":
budget = Budget(tokens=100, cents=50, seconds=10)
# 为第一次调用预留60个Token、20分钱和预计3秒。
print("第一次预留:", budget.reserve(tokens=60, cents=20, estimated_seconds=3))
budget.settle(60, 20, 40, 12)
print(json.dumps({"tokens_left": budget.tokens, "cents_left": budget.cents}))
# 任务已用3秒;检查第二次新调用能否再预留70个Token、20分钱和预计3秒。
print("第二次预留:", budget.reserve(tokens=70, cents=20, estimated_seconds=3, elapsed=3))ls
python budget.py输出:
第一次预留: True
{"tokens_left": 60, "cents_left": 38}
第二次预留: False第一次预留60Token和20分,余额变40和30。实际用了40和12,退回20Token和8分,余额为60和38。第二次想用70Token,超过60的余额,因此返回 False。
Token和金额使用非负整数,秒数使用有限的非负数。amounts 与 duration 先检查这些条件,发现负数或类型错误就返回清晰错误,余额保持原值。
reserve 先比较,全部通过再扣除。elapsed 是任务已用时间,与本次预计时间相加判断。结算加回“预留-实际”,如果实际更多,余额就进一步减少。
3. 分别改动用量和时间
找到主程序里打印“第二次预留”的一行,将 tokens=70 改成 tokens=50,其他参数保持原样。保存后重新运行整个文件:每次运行都会从100个Token、50分钱重新开始。第一次预留和结算后余额仍为60个Token、38分钱;第二次预留50个Token、20分钱成功,余额变为10个Token、18分钱。这是第二次新调用占住额度,上一次已经在 settle 中退回了差额。
接着单独观察时间限制。将文件末尾从 if __name__ == "__main__": 开始的主程序全部替换为下面代码,保留前面的 Budget 类与函数。这里重新设置初始额度,再设置已用8秒、预计再用3秒。8+3超过10秒总时限,所以不会进入结算分支:
if __name__ == "__main__":
budget = Budget(tokens=100, cents=50, seconds=10)
# 任务已用8秒,本次预计还需3秒,超过10秒总时限。
if budget.reserve(tokens=60, cents=20, estimated_seconds=3, elapsed=8):
budget.settle(60, 20, 40, 12)
else:
print("时间预算不足,保留当前结果")
print("剩余Token和金额:", budget.tokens, budget.cents)运行 python budget.py,看到“时间预算不足,保留当前结果”和“剩余Token和金额:100 50”。拒绝预留时,余额保持原值。观察结束后恢复第2节的主程序。
当前例子由你传入耗时,实际服务可用 time.monotonic读取时间间隔,并设置请求超时。
4. 保留检查用例
同一目录创建 test_budget.py:
下面的测试检查预留、结算、额度不足和非法输入,核对每次操作后的余额。
测试代码:test_budget.py
"""检查budget示例的正常路径和边界结果。"""
import unittest
from budget import Budget
class Tests(unittest.TestCase):
def test_reserve_settle(self):
b=Budget(100,50,10)
self.assertTrue(b.reserve(60,20,3)); b.settle(60,20,40,12)
self.assertEqual((b.tokens,b.cents),(60,38))
def test_reject(self):
b=Budget(100,50,10); self.assertFalse(b.reserve(101,20,3))
self.assertEqual(b.tokens,100); self.assertFalse(b.reserve(60,20,11))
def test_invalid_values_leave_balance(self):
b=Budget(100,50,10)
for values in [(-60,20,3),(60,-20,3),(60,20,-3),(True,20,3),(60,20,float("nan"))]:
with self.assertRaises(ValueError): b.reserve(*values)
self.assertEqual((b.tokens,b.cents),(100,50))
def test_invalid_settlement(self):
b=Budget(100,50,10)
with self.assertRaises(ValueError): b.settle(60,20,-40,12)
if __name__ == "__main__":
unittest.main()python -m unittest test_budget -v测试同时检查正常路径和改变条件后的结果。遇到导入错误时,先核对两个文件是否在同一目录;遇到断言失败时,比较预期值和实际值,再回到相应分支。
5. 未知用量与并发预留怎样处理
| 发生的情况 | 处理方式 | 原因 |
|---|---|---|
| Token余额不足 | 减少上下文或输出长度 | 降低下一次用量 |
| 费用余额不足 | 暂停或选择更低成本方案 | 限制后续支出 |
| 预计时间超过截止时间 | 返回已有部分 | 避免继续等待 |
| 请求超时,用量未知 | 保留预留并标记待对账 | 远端可能已处理 |
| 实际用量超过预留 | 扣除差额并检查估算 | 后续余额要反映消耗 |
网络超时后,服务可能已经处理并计费。这笔预留适合保留到用量或账单可查询时再结算。立刻退回全部额度,可能让下一轮继续花同一笔钱。
当前对象在一个进程内顺序使用。两个任务若同时看到余额60,各自想用50,就需要把“检查并扣除”作为不可分割操作。数据库事务或原子计数可以把并发预留集中处理。每次预留还可关联请求编号,结算时确认是否已经结过,避免重复退款。
预估也有误差。可以限制输出长度、模型种类和并发数,再根据返回用量对账。余额变负是一条暂停后续调用、核对预估的信号。金额换算也应选择一致单位;微小费用可使用更细的整数单位或十进制数。
逐项核对这次预算的加减
| 阶段 | Token余额 | 金额余额 | 发生的计算 |
|---|---|---|---|
| 初始化 | 100 | 50 | 读取配置额度 |
| 预留 | 40 | 30 | 100−60,50−20 |
| 实际返回 | 40 | 30 | 获得用量40与金额12 |
| 结算 | 60 | 38 | 40+(60−40),30+(20−12) |
| 再申请70Token | 60 | 38 | 超过余额,保持原值 |
预留与结算的输入需要配对。示例手工传入四个数,方便看公式;应用可为每次预留保存请求编号和预留值,结算时读取它,再标记该请求已结算。这样响应重发时也能识别同一笔用量。
时间预算采用截止时间更方便:任务开始记录start,总时限10秒,每轮读取now,elapsed等于now−start。下一请求的超时取工具上限与剩余时间中较小者,等待结束后再读取实际耗时。预估帮助决定是否出发,实际时间帮助决定下一轮。
不同模型的输入、输出与缓存计价可能分别计算。把用量计算和预算判断拆成两个函数,先得到统一金额,再检查余额。文章里的分钱仅为手算单位,细小金额可以选择更细的整数单位,避免浮点累计误差。
回顾与下一步
分别比较,先预留,按实际结算,未知结果待对账。下一篇 任务状态把这些运行结果组织成调用方容易处理的表示。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“Token、时间与费用预算”,哪种做法更稳妥?
对应知识点:L3-budget
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L3-budget
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L3-budget
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L3-budget
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。