给一个AI任务设置“最多尝试三次”,至少要保证第4次不会再进入受控工作入口;给结果设置字节预算,也要清楚它限制的是已接纳内容,还是网络接收量。
这篇交付一个同步、串行、进程内的TaskBudget。每次工作开始前预占次数;普通失败不退款;对完整字符串按UTF-8计算累计接纳字节;超过上限拒绝整项,后续调用继续拒绝。它不是Token计量器、全局配额服务或账单封顶器。
环境与输入合同
Python 3.11及以上,标准库,无需pip、API Key或账号。建立budget-lab目录,将下面三个文件保存为task_budget.py、retry_tree.py、test_task_budget.py,进入目录运行python task_budget.py、python retry_tree.py、python -m unittest -v。
max_attempts是0—100的普通整数,max_result_bytes是0—1000000的普通整数;拒绝布尔值、浮点数和字符串。0次意味着不启动任何工作;0字节仍允许空字符串,但会使用一次次数。默认值是本例政策,不能当任何模型的参数或额度。
work为同步可调用对象,应返回普通str,不能传异步函数。返回Outcome包含status、text、dispatched、attempts_used和kept_bytes;dispatched表示本入口是否进入回调,不表示远端是否收到了网络请求。结果accepted只说明预算接纳,不保证业务完成或内容正确。
完整实现:task_budget.py
"""Trusted, serial, in-process policy; no network or billing guarantee."""
from dataclasses import dataclass
@dataclass(frozen=True)
class Outcome:
status: str
text: str
dispatched: bool
attempts_used: int
kept_bytes: int
class TaskBudget:
def __init__(self, max_attempts=3, max_result_bytes=1000):
if type(max_attempts) is not int or not 0 <= max_attempts <= 100:
raise ValueError("invalid_attempt_limit")
if type(max_result_bytes) is not int or not 0 <= max_result_bytes <= 1000000:
raise ValueError("invalid_byte_limit")
self._max_attempts = max_attempts
self._max_bytes = max_result_bytes
self._used = self._kept = 0
self._closed = ""
self._busy = False
def _outcome(self, status, text="", dispatched=False):
return Outcome(status, text, dispatched, self._used, self._kept)
def execute(self, work):
if not callable(work):
raise ValueError("invalid_work")
if self._busy:
raise RuntimeError("reentrant_use")
if self._closed:
return self._outcome(self._closed)
if self._used >= self._max_attempts:
self._closed = "attempts_exhausted"
return self._outcome(self._closed)
self._used += 1 # Reserve BEFORE dispatch; never refund after failure.
self._busy = True
try:
try:
text = work() # Synchronous callback; may itself hide other calls.
except Exception:
return self._outcome("work_failed", dispatched=True)
if type(text) is not str:
self._closed = "invalid_result"
return self._outcome(self._closed, dispatched=True)
try:
size = len(text.encode("utf-8", errors="strict"))
except UnicodeError:
self._closed = "invalid_text"
return self._outcome(self._closed, dispatched=True)
if size > self._max_bytes - self._kept:
self._closed = "result_bytes_exhausted"
return self._outcome(self._closed, dispatched=True)
self._kept += size
return self._outcome("accepted", text, True)
finally:
self._busy = False
def demo():
def fail(): raise RuntimeError("synthetic private detail")
budget = TaskBudget(max_attempts=3, max_result_bytes=6)
for work in (fail, lambda: "你好", lambda: "!", lambda: "never"):
result = budget.execute(work)
print(result.status, repr(result.text), result.dispatched,
result.attempts_used, result.kept_bytes)
if __name__ == "__main__":
demo()
_used先加1再执行work,失败和BaseException中断均不退还。普通Exception转换为固定work_failed,不泄露异常正文;它没有关闭整个预算,调用者可另作决定是否再使用剩余次数。没有自动重试。
无效返回类型、无法严格编码的文本以及累计字节超限会关闭预算,后续入口不再执行回调。超限整项拒绝,不截断,不改变之前已接纳的字节计数。调用者即使删除先前结果,也没有自动退回本任务已经使用的接纳预算。
_busy阻止同一对象在回调中重入,不是线程锁;私有属性也不是权限隔离。此对象供可信调用者串行使用,不能接收用户伪造的状态,也没有跨进程共享或重启恢复能力。
重试层级反例:retry_tree.py
"""Hypothetical maximum path, not a real SDK/network trace."""
def dispatch_slots(app_attempts, sdk_retries):
for value in (app_attempts, sdk_retries):
if type(value) is not int or not 0 <= value <= 10:
raise ValueError("invalid_policy")
return [(outer + 1, inner + 1)
for outer in range(app_attempts)
for inner in range(sdk_retries + 1)]
if __name__ == "__main__":
for retries in (0, 2):
slots = dispatch_slots(3, retries)
print("app_attempts=3 sdk_retries=", retries, "max_dispatch_slots=", len(slots))
print(slots)
这个程序只枚举假想位置,不读取SDK或真实日志。外层3次、内层最多再重试2次的最大路径有9个发送位置;实际是否走到每个位置由错误类型和策略决定。接入时下层的SDK、代理或工具可能有自己的重试,预算不自动覆盖它们。
完整测试:test_task_budget.py
import unittest
from task_budget import TaskBudget
from retry_tree import dispatch_slots
class BudgetTests(unittest.TestCase):
def test_zero_attempts_blocks_dispatch(self):
r = TaskBudget(0).execute(lambda: self.fail("must not run"))
self.assertEqual((r.status, r.dispatched, r.attempts_used), ("attempts_exhausted", False, 0))
def test_exact_attempt_limit(self):
b = TaskBudget(2)
self.assertTrue(b.execute(lambda: "a").dispatched)
self.assertTrue(b.execute(lambda: "b").dispatched)
r = b.execute(lambda: self.fail("must not run"))
self.assertEqual((r.status, r.attempts_used), ("attempts_exhausted", 2))
def test_failure_consumes_without_leaking(self):
def fail(): raise ValueError("SYNTHETIC_SECRET")
b = TaskBudget(1)
r = b.execute(fail)
self.assertEqual((r.status, r.text, r.attempts_used), ("work_failed", "", 1))
self.assertNotIn("SYNTHETIC_SECRET", repr(r))
self.assertFalse(b.execute(lambda: "x").dispatched)
def test_utf8_exact_boundary(self):
r = TaskBudget(1, 6).execute(lambda: "你好")
self.assertEqual((r.status, r.kept_bytes), ("accepted", 6))
def test_bytes_cumulative_across_attempts(self):
b = TaskBudget(3, 4)
self.assertEqual(b.execute(lambda: "abc").kept_bytes, 3)
r = b.execute(lambda: "de")
self.assertEqual((r.status, r.kept_bytes, r.text), ("result_bytes_exhausted", 3, ""))
def test_oversize_closes_without_truncation(self):
b = TaskBudget(3, 2)
self.assertEqual(b.execute(lambda: "你好").text, "")
r = b.execute(lambda: self.fail("must not run"))
self.assertEqual((r.status, r.attempts_used, r.kept_bytes), ("result_bytes_exhausted", 1, 0))
def test_empty_text_uses_attempt_not_bytes(self):
r = TaskBudget(1, 0).execute(lambda: "")
self.assertEqual((r.status, r.attempts_used, r.kept_bytes), ("accepted", 1, 0))
def test_invalid_result_closes(self):
b = TaskBudget(3)
self.assertEqual(b.execute(lambda: 123).status, "invalid_result")
self.assertFalse(b.execute(lambda: "ok").dispatched)
def test_unencodable_text_closes(self):
r = TaskBudget().execute(lambda: "\ud800")
self.assertEqual((r.status, r.kept_bytes), ("invalid_text", 0))
def test_base_exception_propagates_and_still_spends(self):
class Stop(BaseException): pass
def stop(): raise Stop()
b = TaskBudget(1)
with self.assertRaises(Stop): b.execute(stop)
self.assertEqual(b.execute(lambda: "x").status, "attempts_exhausted")
def test_reentrant_callback_cannot_get_another_slot(self):
b = TaskBudget(2)
r = b.execute(lambda: b.execute(lambda: "nested"))
self.assertEqual((r.status, r.attempts_used), ("work_failed", 1))
self.assertEqual(b.execute(lambda: "next").status, "accepted")
def test_invalid_policy(self):
for value in (True, -1, "3", 101):
with self.assertRaises(ValueError): TaskBudget(max_attempts=value)
for value in (False, -1, 1000001, "6"):
with self.assertRaises(ValueError): TaskBudget(max_result_bytes=value)
def test_noncallable_rejected_without_spending(self):
b = TaskBudget(1)
with self.assertRaises(ValueError): b.execute(None)
self.assertEqual(b.execute(lambda: "x").attempts_used, 1)
class TreeTests(unittest.TestCase):
def test_three_logical_calls_can_have_nine_slots(self):
self.assertEqual(len(dispatch_slots(3, 0)), 3)
self.assertEqual(len(dispatch_slots(3, 2)), 9)
def test_zero_and_order(self):
self.assertEqual(dispatch_slots(0, 2), [])
self.assertEqual(dispatch_slots(2, 1), [(1,1),(1,2),(2,1),(2,2)])
def test_bad_policy(self):
for value in (True, -1, 11, 1.5):
with self.assertRaises(ValueError): dispatch_slots(3, value)
if __name__ == "__main__":
unittest.main()
实际结果与边界
本次在Python 3.11.9实际运行16项离线测试通过。task_budget.py输出如下:
work_failed '' True 1 0
accepted '你好' True 2 6
result_bytes_exhausted '' True 3 6
result_bytes_exhausted '' False 3 6
第一次合成异常使用一次次数,没有接纳结果;第二次“你好”占6个UTF-8字节;第三次的“!”使累计量超限,整项拒绝并关闭预算;第四次被入口直接挡住。没有真实模型、费用或远端取消测试。
execute这种非流式入口可能先创建很大字符串,再被本地拒绝;encode也会创建临时字节对象。因此这个实现不能作为网络接收或峰值内存安全上限。若实际任务返回流,应在读取层另外限制接收量,处理完整状态和关闭资源,不要把此同步回调硬套成流式保护。
常见问题与排错
attempts_exhausted:本对象的启动次数已用完,不能失败后偷偷重建预算继续执行。result_bytes_exhausted:累计接纳额度不足,保留以前已接纳部分并停止,不把被拒绝片段作为完整成果。
work_failed:只说明回调抛出普通异常,本例不保存异常原文;结合脱敏错误码判断是否允许下一次尝试,不能无限重试。invalid_result/invalid_text:适配器返回类型或编码不符合合同,修适配器与样本,不自动跳过校验。
reentrant_use:回调再次调用了同一预算入口。例中内层在扣减前抛错,外层将其记为一次work_failed;应整理调用链,而不是关闭重入检查。
本地次数为什么不等于HTTP次数?回调内部可能重试、分发多个工具或绕开入口;应把实际发送点纳入控制并核对SDK版本。即使关闭某一层重试,也不能证明其他层没有重试。
字节限额为什么不等于费用限额?输入、输出Token、缓存和工具等可能分别计量,且远端处理状态未必已知;本例没有计算货币,也没有读取账单,不能把本地失败或取消解释为不计费。
小练习与下一篇
把max_result_bytes设为5,预测“你好”的结果,再运行本地样本核对;增加一个测试,先接纳“a”,再接纳“好”,确认累计字节为4。再为自己的预算写出“覆盖入口”和“未覆盖入口”,不接真实密钥。
下一篇设计稳定错误返回结构,区分用户提示、应用错误码与内部诊断。欢迎评论提供去掉私密原文的边界样本,讨论在哪一步停止交付。
原始依据与适用边界
Python 3.11字符串编码文档说明把字符串编码为字节;UTF-8字节不是Token、进程内存或费用。OpenAI Python官方仓库重试说明说明部分错误存在SDK自动重试,以及可配置重试。实际接入应核对自己安装的版本和调用链,不能由本文推定其他SDK或本站使用同样默认值。
本课为常青工程教学。近24—72小时候选官方公告已检索并打开;原型仓库未提供可直接确认的公告时刻,近期其他公告与此预算实现无直接关系,因此没有将本例包装成某个新模型功能。内容由AI辅助整理,程序仅本地离线执行,无模型或付费调用,也没有实际账单实验。
423



被折叠的 条评论
为什么被折叠?



