☰
为自研 Agent 增加单次任务预算(Budget):防止模型调用无限循环消耗 Token
2026/10/20 13:07:42 网站建设 项目流程

在自主智能体(Autonomous Agent)的架构体系中,模型被赋予了根据外部环境反馈自我驱动的核心能力——通过标准的“思考-执行-观察(Think-Act-Observe)”循环,直到它认为目标已经达成为止。这种赋予智能体自主性的设计极为强大,但在工业级生产落地时,也隐藏着巨大的财务与可用性风险。

大语言模型并不是确定性的状态机。在面对未曾遇见的边界异常时(例如文件路径拼写错误、第三方 API 持续返回 403 权限拒绝、或者正则匹配陷入空循环),模型经常会陷入逻辑盲区。它会不断尝试相似的变体参数,一遍又一遍地调用工具,在死循环中狂奔。如果没有严格的熔断与预算管控,单个失控的 Agent 会在几十分钟内吃掉数百万 Token,不仅把研发团队的 API 费用余额彻底掏空,还会严重挤占后端系统的并发连接。

为了让 Agent 在真实环境中安全可控地运行,必须为其每一次任务执行建立多维度的任务预算(Task Budget)与死循环主动熔断机制。

为什么单纯限制“最大步数”远远不够

很多开源框架防止死循环的手段非常简陋,仅仅是设置一个全局计数器max_steps = 20。在实际工程中,单纯依靠最大步数限制存在三个严重漏洞:

  1. Token 维度失控:某些工具返回的内容可能极其庞大(例如模型调用抓取工具返回了一个完整的 HTML 页面,或者读取了一个几万行的日志文件)。即使只执行了 5 个步骤,由于单步上下文急剧膨胀,累计消耗的 Token 数量可能已经突破了数十万,账单开销直接突破安全红线。
  2. 多模态与推理 Token 的成本畸变:随着具备深度思考能力的推理模型普及,单次生成的思考 Token(Reasoning Tokens)单价与输入单价往往存在数倍差异。相同的步数下,纯短文本交互与深度长链推导的真实成本不可同日而语。
  3. 迟钝的停机响应:如果一个任务注定在第三步就因为逻辑悖论而卡死,非要让它机械地跑满预设的 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返回错误时,系统切忌简单粗暴地直接在终端报一段看不懂的系统异常。工业级的体验应当是优雅降级:

  1. 最后一口气的自愈陈述:当预算达到 80% 警告线时,系统会自动在下一轮输入中注入强制收敛指令。模型在明确感知到资源即将枯竭后,通常会放弃冗余的工具调用,转为输出:“由于检索范围过大,我已完成了基础框架分析,建议对特定模块进行针对性排查...”。
  2. 状态快照与断点保存:如果因为物理网络超时或硬熔断被迫终止,系统应将当前的ExecutionFrame状态机快照序列化持久化至本地或数据库中,并生成唯一的task_resume_token。当用户追加预算或指明更准确的文件路径后,智能体能够从中断的节点恢复运行,而不必从第一步重新消耗费用。

给 Agent 套上理性的缰绳,不仅保护了开发者的财务安全,更是让智能体走向严肃商业化与工业级稳定运行的必经之路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询