当前位置: 首页 > news >正文

给 Agent 加上防爆闸:Tool Calling 异常循环的防护设计

给 Agent 加上防爆闸:Tool Calling 异常循环的防护设计

把大模型接入业务系统做 Tool Calling(工具调用)时,很多团队都会踩到一个坑——Agent 突然卡死在某一个工具调用上,陷入无限死循环。

其实模型并不是“故意不停”。常见的情况有三种:一是工具返回的 JSON 格式不符合模型的预期,模型误以为是自己参数填错了,下一轮修改一下参数继续调;二是系统本身已经执行成功了,但响应在网络传输中丢了,模型不知道已经成功,盲目发起重试;三是 Prompt 里根本没有写明白什么情况下应该结束任务。

只靠设置“最大调用轮数(Max Iterations)”只能在最后强行关闭任务,根本没法阻止中间已经发生的重复扣费、重复写数据库或者误发邮件这些严重后果。要解决这个问题,必须用确定性的后端代码去约束不确定的模型输出。


大模型 Tool Calling 死循环的工程根因

要设计防爆机制,先看看大模型在 Tool Calling 时发生死循环的三种常见情况:

flowchart TD A[LLM 发起 Tool Call 请求] --> B[后端执行器调用微服务 API] B --> C{API 返回结果形态} C -->|类型 1: 格式不相符/未加结构化界定| D[模型误判为参数未提供 ➔ 再次产生相同调用] C -->|类型 2: 网络超时但后端已成功| E[模型未感知幂等 ➔ 重复发起写操作调用] C -->|类型 3: 缺少收敛判定条件| F[模型在相似工具间反复交替切换] D --> G[陷入无限循环死锁] E --> G F --> G
  1. 响应格式不对导致模型一直在“猜参数”
    如果 API 返回了一大堆带有堆栈信息的错误文本,或者不是标准的 JSON,LLM 分析上下文时很容易误判,觉得是自己上一次传入的参数不对。于是它会在下一轮稍微改改参数再试一次,陷入“失败 ➔ 猜参数 ➔ 再次失败”的怪圈。
  2. 网络超时引发的重复写入
    在微服务环境下,网络超时经常发生在 API 已经处理完、正在回传响应的阶段。由于没有全局 Task ID 和幂等 Key 的限制,当编排层把超时错误扔给模型时,模型会直接重试,导致原本不能重复执行的操作(比如扣款、下单)被执行了多次。
  3. 目标不明确导致任务收敛不了
    如果 Prompt 给的目标太模糊(比如“帮我分析并优化这份数据”),又没有给明确的停止触发条件,LLM 就会在好几个功能差不多的查询工具之间来回调用,自己不知道什么时候该输出最终答案。

把循环拆成可审计的状态机体系

为了打破死循环,服务端绝对不能直接拿大模型的输出去调工具。必须在编排层引入一个有限状态机(FSM)

在整个任务的生命周期里,任务只能处于以下几种明确的状态,而且每次状态变更都要记日志:

  • Planning:大模型正在分析上下文,生成下一步的行动计划。
  • Validating:防爆闸正在检查模型想调用的工具名称、参数格式、权限、预算和幂等 Key。
  • Executing:后端正在安全地执行这个工具。
  • NeedsReview:碰到了敏感操作或者规则校验异常,暂停自动执行,转给人工确认。
  • Completed/Failed:任务正常完成或者因为不可恢复的错误退出。
stateDiagram-v2 [*] --> Planning Planning --> Validating: 模型返回 Tool Call 请求 state Validating { [*] --> CheckBudget CheckBudget --> CheckPermission: 预算正常 CheckPermission --> CheckIdempotency: 鉴权通过 CheckIdempotency --> ValidateSchema: 无幂等冲突 } Validating --> Executing: 防爆闸全量校验通过 Validating --> NeedsReview: 触发高风险操作或规则校验异常 Validating --> Failed: 超出硬预算 limit Executing --> Planning: 工具执行成功,结果写入 Context NeedsReview --> Executing: 人工批准执行 NeedsReview --> Failed: 人工拒绝或超时 Planning --> Completed: 模型产出最终 Answer Completed --> [*] Failed --> [*]

有了状态机之后,每次工具调用就不再是随意的 Prompt 拼接,而是变成可以监控、审计和人工接管的确定性流程。


确定性防护防线:硬限制、语义幂等 Key 与业务审批隔离

防爆闸的设计应该采用三层拦截机制:

第一层:全局硬配额限制

这是保底的熔断机制。对单个 Task 必须在服务端强制配置以下阈值:

  • 最大工具调用轮数(比如最多 15 轮);
  • 单 Task 最大 Token 消耗上限
  • 任务总超时时间(比如最多 180 秒);
  • 单个工具连续重试次数(同一个工具不能连续尝试超过 3 次)。

只要触发了任何一条硬限制,状态机立刻终止任务,切到Failed或者NeedsReview状态。

第二层:基于参数 Hash 的语义幂等 Key

为了防止模型连续发起完全相同的无效调用,服务端必须对模型给出的工具参数做标准化处理:

  1. 把 JSON 参数的 Key 按字母顺序升序排列;
  2. 过滤掉无意义的空格、换行符和默认空值;
  3. TaskID + ToolName + NormalizedArgsHash拼成全局唯一的幂等 Key。

如果服务端检查发现这个幂等 Key 在当前任务里已经成功执行过了,拦截器就会直接拦截这次调用,把上一次执行的结果直接填进 Context,并提示模型:“该操作已经完成,请直接读取已有结果进行下一步。”

第三层:高风险写操作的人工审批

对于扣款、删除数据、修改权限这些不可逆的操作,防爆闸会把对应的工具标记为RequiresApproval。不管模型的推理看起来多么合理,都必须把任务挂起到NeedsReview状态,发送通知给人工审批界面,拿到明确的批准信号后才由后端继续执行。


生产级 Go 语言 Agent Guardrail 防爆中间件实现

下面是一段集成了预算检查、JSON 参数 Hash 计算、幂等拦截和审批控制的 Go 代码实现:

package guardrail import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "sort" "sync" "time" ) var ( ErrBudgetExceeded = errors.New("task budget or max iterations exceeded") ErrDuplicateCall = errors.New("duplicate tool call with identical arguments") ErrApprovalRequired = errors.New("action requires manual human approval") ) type ToolCall struct { ID string `json:"id"` Name string `json:"name"` Arguments map[string]interface{} `json:"arguments"` } type TaskContext struct { TaskID string ToolCalls int MaxToolCalls int Deadline time.Time Approved map[string]bool mu sync.RWMutex } type Guardrail struct { sensitiveTools map[string]bool completedKeys map[string]string mu sync.Mutex } func NewGuardrail(sensitiveTools []string) *Guardrail { st := make(map[string]bool) for _, tool := range sensitiveTools { st[tool] = true } return &Guardrail{ sensitiveTools: st, completedKeys: make(map[string]string), } } func (g *Guardrail) Allow(ctx *TaskContext, call ToolCall) (string, error) { ctx.mu.Lock() defer ctx.mu.Unlock() // 1. 硬限制配额检查 ctx.ToolCalls++ if ctx.ToolCalls > ctx.MaxToolCalls || time.Now().After(ctx.Deadline) { return "", ErrBudgetExceeded } // 2. 参数标准化并生成 Hash argHash, err := g.computeNormalizedHash(call.Name, call.Arguments) if err != nil { return "", fmt.Errorf("failed to normalize arguments: %w", err) } idempotencyKey := fmt.Sprintf("%s:%s:%s", ctx.TaskID, call.Name, argHash) // 3. 幂等性检查 g.mu.Lock() cachedResult, exists := g.completedKeys[idempotencyKey] g.mu.Unlock() if exists { // 返回上一次的结果,阻止重复调用 return cachedResult, ErrDuplicateCall } // 4. 敏感写操作人工审批检查 if g.sensitiveTools[call.Name] { if !ctx.Approved[call.ID] { return "", ErrApprovalRequired } } return idempotencyKey, nil } func (g *Guardrail) RecordSuccess(idempotencyKey string, resultStr string) { g.mu.Lock() defer g.mu.Unlock() g.completedKeys[idempotencyKey] = resultStr } func (g *Guardrail) computeNormalizedHash(toolName string, args map[string]interface{}) (string, error) { keys := make([]string, 0, len(args)) for k := range args { keys = append(keys, k) } sort.Strings(keys) sortedMap := make([]string, 0, len(args)) for _, k := range keys { valBytes, err := json.Marshal(args[k]) if err != nil { return "", err } sortedMap = append(sortedMap, fmt.Sprintf("%q:%s", k, string(valBytes))) } rawPayload := fmt.Sprintf("%s|{%s}", toolName, joinStrings(sortedMap, ",")) hash := sha256.Sum256([]byte(rawPayload)) return hex.EncodeToString(hash[:]), nil } func joinStrings(elems []string, sep string) string { if len(elems) == 0 { return "" } res := elems[0] for _, s := range elems[1:] { res += sep + s } return res }

故障注入测试与实际权衡

故障注入自动化测试

防爆系统不能只用正常流程做测试,必须做针对编排层的故障注入测试

  1. 模拟 API 返回损坏的 JSON:让测试 API 返回格式错误的文本,验证防爆闸能不能在达到次数限制前正常拦截,而不是直接崩溃。
  2. 模拟丢包与延迟:在测试 API 里注入超时,模拟“后端写成功了但响应丢了”的场景,看看幂等 Key 机制能不能挡住二次重复写入。
  3. 模拟高自信度的违规调用:构造带有误导性的 Prompt 诱导模型调用敏感工具,验证审批拦截是否有效。

权衡分析

设计 Agent 防控时,要找准安全和灵活之间的平衡点:

评估维度完全放任 (无 Guardrail)适度工程防线 (推荐)过度保守
安全性与控费很差,极易死循环拉爆账单或者误删数据。好,硬限制配幂等 Key 能挡住绝大多数异常。很好,但复杂任务很容易被打断。
复杂任务成功率表面看起来高,实际上很多靠运气。,能提供明确反馈引导模型收敛。低,正常的长链任务容易被误杀。
人工介入成本事后救火成本极高。低,只有敏感写操作需要点一下确认。极高,每一步都要确认,失去了自动化的意义。

总结

大模型的优势在于灵活的推理能力,但后端系统要求的是确定性的安全与稳定。

靠修改 Prompt 很难彻底避免死循环。正确的做法是把推理和执行分开:大模型只负责给出下一步想做什么,至于这一步能不能做,由后端的防爆闸中间件说了算。


参考资料

  • OWASP Top 10 for LLM Applications
  • NIST AI Risk Management Framework
  • Building Effective Agents - Anthropic
http://www.jsqmd.com/news/1310402/

相关文章:

  • 基于STM32与USB协议的自制便携显示器:从图像压缩到驱动开发全解析
  • 基于ESP32与I2S的嵌入式音频流媒体系统:UDP实时传输实战
  • CCS铁魄EVA二号机二式开箱测评:合金骨架与极致造型的深度解析
  • MH迈汇:从公开信息出发,归纳运营连贯性与市场覆盖
  • PCB设计必备:Altium与Allegro封装库路径设置与高效管理指南
  • Spark大数据平台在气象数据分析中的架构设计与工程实践
  • Godot VR动作系统平滑优化实战:从输入滤波到性能调优
  • ESP32-S3触摸屏开发板:集成LCD与触摸的物联网交互核心方案
  • 算法优化的内存亲和性与NUMA架构分析
  • FT232 USB转串口芯片:硬件开发的稳定之选与实战应用
  • 广州中小微企业主经济犯罪律师哪个专业:【法纳刑辩】胜诉无忧 - 松梢月冷
  • DIY USB便携显示器:从CH552G到Windows客户端的完整实现
  • 文案生成与排版自动化:从 Markdown 到出版级画册的工程实践
  • 如何快速解决Windows 10上PL-2303旧芯片的驱动兼容性问题:终极完整指南
  • WeWe RSS:用微信读书接口打造你的专属微信公众号聚合中心
  • 在Windows系统上部署和使用iperf3网络性能测试工具的完整指南
  • 2.7英寸电子纸HAT驱动全解析:从SPI接口到低功耗项目实战
  • Qt6 C++开发指南:从环境配置到项目实战的完整迁移手册
  • Rust AI 生态全景横评:Candle、Burn、Tract 与 ONNX Runtime Rust 绑定深度对比
  • 【AI云边协同架构落地指南】:20年架构师亲授5大避坑法则与3个高并发实战案例
  • 广州中小微企业主经济犯罪律师哪个优秀:【法纳刑辩】团队精锐 - 秋山寄远
  • 解决maven unresolved plugin 以及 如何控制maven plugin 的插件版本
  • Linux(CentOS)系统管理入门笔记(第二十一期)——防火墙管理(Firewalld)——zone、服务端口、富规则与端口转发
  • 如何轻松下载B站视频:大会员4K高清与充电专属内容一键保存
  • Iceberg 小文件合并与治理:从写放大到读优化的全链路
  • 很多人都搞混了:三层交换机和路由器到底有什么区别,这篇终于讲明白了
  • Terraria 源代码终极指南:如何快速掌握游戏开发核心架构
  • 音乐解锁终极指南:3分钟解决加密音乐播放难题
  • 树结构在索引优化中的存储机制分析
  • 5分钟免费激活Windows和Office:告别系统弹窗烦恼