资讯动态

为自研 Agent 增加单次任务预算(Budget):防止模型调用无限循环消耗 Token

发布时间:2026/10/21 21:37:44 来源:尧图企业网站定制
在自主智能体Autonomous Agent的架构体系中模型被赋予了根据外部环境反馈自我驱动的核心能力——通过标准的“思考-执行-观察Think-Act-Observe”循环直到它认为目标已经达成为止。这种赋予智能体自主性的设计极为强大但在工业级生产落地时也隐藏着巨大的财务与可用性风险。大语言模型并不是确定性的状态机。在面对未曾遇见的边界异常时例如文件路径拼写错误、第三方 API 持续返回 403 权限拒绝、或者正则匹配陷入空循环模型经常会陷入逻辑盲区。它会不断尝试相似的变体参数一遍又一遍地调用工具在死循环中狂奔。如果没有严格的熔断与预算管控单个失控的 Agent 会在几十分钟内吃掉数百万 Token不仅把研发团队的 API 费用余额彻底掏空还会严重挤占后端系统的并发连接。为了让 Agent 在真实环境中安全可控地运行必须为其每一次任务执行建立多维度的任务预算Task Budget与死循环主动熔断机制。为什么单纯限制“最大步数”远远不够很多开源框架防止死循环的手段非常简陋仅仅是设置一个全局计数器max_steps 20。在实际工程中单纯依靠最大步数限制存在三个严重漏洞Token 维度失控某些工具返回的内容可能极其庞大例如模型调用抓取工具返回了一个完整的 HTML 页面或者读取了一个几万行的日志文件。即使只执行了 5 个步骤由于单步上下文急剧膨胀累计消耗的 Token 数量可能已经突破了数十万账单开销直接突破安全红线。多模态与推理 Token 的成本畸变随着具备深度思考能力的推理模型普及单次生成的思考 TokenReasoning Tokens单价与输入单价往往存在数倍差异。相同的步数下纯短文本交互与深度长链推导的真实成本不可同日而语。迟钝的停机响应如果一个任务注定在第三步就因为逻辑悖论而卡死非要让它机械地跑满预设的 20 步才停机不仅白白浪费了后 17 步的费用也严重拉长了最终用户的等待时间。因此工业级的预算控制器必须综合**物理时间、Token 总量、真实成本Cost以及动作重复度Stagnation Detection**进行立体监控。预算控制器架构设计与核心实现我们在自研 Agent 内核中设计了独立于业务循环的BudgetController。在每一步模型推理发起前与工具执行返回后控制器都会对当前消耗进行原子化扣减与健康度评估。一旦累计指标触碰预警线例如消耗达到 80%控制器会向模型的输入流中注入紧急提示Budget Warning促使模型放弃边缘尝试、立即收敛若指标触碰熔断线则通过标准 Gocontext.Context立即截断下游执行链。以下是使用 Go 1.27.1 实现的生产级任务预算控制器package runtime import ( context crypto/sha256 encoding/hex errors fmt sync time ) var ( ErrBudgetExceeded errors.New(任务超出预设资源预算) ErrLoopDetected errors.New(检测到模型陷入重复调用死循环) ErrTimeoutExceeded errors.New(任务执行超出最大时钟上限) ) type TokenUsage struct { PromptTokens int64 CompletionTokens int64 ReasoningTokens int64 } // BudgetPolicy 定义任务预算上限规则 type BudgetPolicy struct { MaxSteps int // 最大执行步数 MaxTotalTokens int64 // 最大 Token 上限 MaxCostUSD float64 // 最大允许折合美金上限 Timeout time.Duration // 最大允许物理时间 MaxConsecutiveStagnant int // 连续相同工具调用的阈值 } // ModelPricing 定义每千 Token 的单价 type ModelPricing struct { PromptPricePerK float64 CompletionPricePerK float64 ReasoningPricePerK float64 } type BudgetController struct { mu sync.Mutex policy BudgetPolicy pricing ModelPricing startTime time.Time currentSteps int totalUsage TokenUsage recentHashes []string // 记录最近动作的指纹用于识别死循环 } func NewBudgetController(policy BudgetPolicy, pricing ModelPricing) *BudgetController { return BudgetController{ policy: policy, pricing: pricing, startTime: time.Now(), recentHashes: make([]string, 0), } } // TrackModelUsage 记录大模型交互产生的 Token 与折算成本 func (bc *BudgetController) TrackModelUsage(u TokenUsage) error { bc.mu.Lock() defer bc.mu.Unlock() bc.totalUsage.PromptTokens u.PromptTokens bc.totalUsage.CompletionTokens u.CompletionTokens bc.totalUsage.ReasoningTokens u.ReasoningTokens bc.currentSteps // 1. 检查物理超时 if bc.policy.Timeout 0 time.Since(bc.startTime) bc.policy.Timeout { return ErrTimeoutExceeded } // 2. 检查最大步数 if bc.policy.MaxSteps 0 bc.currentSteps bc.policy.MaxSteps { return fmt.Errorf(%w: 步数超出上限 (%d/%d), ErrBudgetExceeded, bc.currentSteps, bc.policy.MaxSteps) } // 3. 检查 Token 总量 totalTokens : bc.totalUsage.PromptTokens bc.totalUsage.CompletionTokens bc.totalUsage.ReasoningTokens if bc.policy.MaxTotalTokens 0 totalTokens bc.policy.MaxTotalTokens { return fmt.Errorf(%w: Token 总数超出限制 (%d/%d), ErrBudgetExceeded, totalTokens, bc.policy.MaxTotalTokens) } // 4. 检查折算金额 cost : (float64(bc.totalUsage.PromptTokens)/1000.0)*bc.pricing.PromptPricePerK (float64(bc.totalUsage.CompletionTokens)/1000.0)*bc.pricing.CompletionPricePerK (float64(bc.totalUsage.ReasoningTokens)/1000.0)*bc.pricing.ReasoningPricePerK if bc.policy.MaxCostUSD 0 cost bc.policy.MaxCostUSD { return fmt.Errorf(%w: 费用超出配额 (已消耗 $%.4f, 上限 $%.4f), ErrBudgetExceeded, cost, bc.policy.MaxCostUSD) } return nil } // TrackActionFingerprint 检查动作指纹防范相同工具与参数的重复死循环 func (bc *BudgetController) TrackActionFingerprint(toolName string, args string) error { bc.mu.Lock() defer bc.mu.Unlock() h : sha256.New() h.Write([]byte(toolName)) h.Write([]byte(:)) h.Write([]byte(args)) fp : hex.EncodeToString(h.Sum(nil)) bc.recentHashes append(bc.recentHashes, fp) threshold : bc.policy.MaxConsecutiveStagnant if threshold 0 { threshold 3 } // 检查最近的 N 次操作是否完全一致 if len(bc.recentHashes) threshold { allSame : true for i : len(bc.recentHashes) - threshold; i len(bc.recentHashes); i { if bc.recentHashes[i] ! fp { allSame false break } } if allSame { return fmt.Errorf(%w: 工具 [%s] 连续以相同参数被触发 %d 次, ErrLoopDetected, toolName, threshold) } } return nil } // IsApproachingLimit 判断是否需要向模型下发收敛警告 func (bc *BudgetController) IsApproachingLimit() (bool, string) { bc.mu.Lock() defer bc.mu.Unlock() totalTokens : bc.totalUsage.PromptTokens bc.totalUsage.CompletionTokens bc.totalUsage.ReasoningTokens if bc.policy.MaxTotalTokens 0 float64(totalTokens)/float64(bc.policy.MaxTotalTokens) 0.8 { return true, 当前任务预算已消耗超过 80%请停止探索新的工具分支基于现有信息立即产出最终结论。 } if bc.policy.MaxSteps 0 float64(bc.currentSteps)/float64(bc.policy.MaxSteps) 0.8 { return true, 当前任务步数接近上限请立即总结并回答用户。 } return false, }熔断后的优雅降级Graceful Degradation当BudgetController返回错误时系统切忌简单粗暴地直接在终端报一段看不懂的系统异常。工业级的体验应当是优雅降级最后一口气的自愈陈述当预算达到 80% 警告线时系统会自动在下一轮输入中注入强制收敛指令。模型在明确感知到资源即将枯竭后通常会放弃冗余的工具调用转为输出“由于检索范围过大我已完成了基础框架分析建议对特定模块进行针对性排查...”。状态快照与断点保存如果因为物理网络超时或硬熔断被迫终止系统应将当前的ExecutionFrame状态机快照序列化持久化至本地或数据库中并生成唯一的task_resume_token。当用户追加预算或指明更准确的文件路径后智能体能够从中断的节点恢复运行而不必从第一步重新消耗费用。给 Agent 套上理性的缰绳不仅保护了开发者的财务安全更是让智能体走向严肃商业化与工业级稳定运行的必经之路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑