恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

别让 AI Agent 无限循环烧 Token!从工具调用到执行终止,设计一套可靠的智能体控制机制

  • 首页
  • 资讯中心
  • /
  • 别让 AI Agent 无限循环烧 Token!从工具调用到执行终止,设计一套可靠的智能体控制机制

相关资讯

LangGraph 实战:从单轮对话到多步骤 AI 工作流,搭建一个可追踪、可恢复的智能体 2026/10/16 23:04:01
Xshell图形化安装Oracle:X11转发原理与VcXsrv实战配置 2026/10/16 23:04:01
理工科学生AI学习路线全攻略:从数学基础到大模型实战 2026/10/16 22:59:01

最新资讯

Qwen-Image-2.1-FP8 vs INT8怎么选?LPIPS质量数据+显存对比完整选型指南
尺度均衡金字塔卷积:轻量级FPN改进提升目标检测精度
CDP7迁移评估:从CDH到Cloudera Data Platform的核心变化与踩坑记录
StreamSets实战:Origin组件核心用法全解析(文件/Kafka/数据库/HTTP)
Apache Kylin安装部署实战:从环境准备到生产调优全流程
基于Freemarker的代码自动生成器实战:摆脱重复CRUD

今日推荐

Qwen3.5-122B-A10B-singprobe幻觉检测实战:AUC 0.7806,如何实时揪出大模型一本正经的胡话?
NetSpot 实战:用信道分析与频宽优化解决 Wi-Fi 卡顿
C++ split() 字符串分割的五种实现与性能实测

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

别让 AI Agent 无限循环烧 Token!从工具调用到执行终止,设计一套可靠的智能体控制机制

发布时间:2026/10/16 23:04:01
别让 AI Agent 无限循环烧 Token!从工具调用到执行终止,设计一套可靠的智能体控制机制 引言一次失控的实验账单翻了 40 倍先讲一个真实场景。某个内部知识库问答 Agent 上线灰度任务描述是回答用户关于产品文档的问题必要时检索。上线第二天监控告警某个单会话在 3 小时内消耗了 180 万 token发起 2400 多次检索调用。日志拉出来一看Agent 的轨迹是这样的Thought: 我需要找到关于退款周期的说明 Action: search_docs(退款周期) Observation: 未找到相关文档 Thought: 检索结果不理想我换个关键词再试 Action: search_docs(退款 周期) Observation: 未找到相关文档 Thought: 还是没有再换一个说法 Action: search_docs(退款政策 时间) ...它没有崩溃没有报错只是在努力。这正是 LLM Agent 最危险的一种失效模式它不是不能工作而是不知道自己已经做完了或者做不完了。把这次事故拆开算一笔账会更直观。按主流模型每百万 input token 约 3 美元、output token 约 15 美元计价180 万 token 里即便九成是输入单会话的直接成本也在 30 美元量级——而正常会话的成本是 0.1 美元以内这就是翻了 40 倍的来源。这还没算那 2400 次检索带来的三方 API 账单、向量库的 QPS 挤占以及对同租户其他用户造成的延迟抖动。换句话说一次失控循环的成本是三重叠加的token 账单、外部系统负载、用户体验。传统软件的死循环会卡死 CPU很快被发现。Agent 的循环是付费的——每一轮都要调用一次大模型投入一次工具调用代价是 token 账单、延迟和外部系统的副作用。更糟的是很多工具写数据库、发邮件、下单不是幂等的重复执行是事故而不只是浪费。本文不谈如何让 Agent 更聪明只谈一件工程上更关键的事如何让 Agent 可靠地停下来。我们会从 ReAct 循环的控制流本质出发拆解循环的几种典型形态然后给出一套可落地的执行器设计预算控制、调用指纹去重、振荡检测、超时与取消传播最后聊聊那些只有踩过才知道的坑。无论你是用 LangChain、LlamaIndex、LangGraph还是自己写了一个while True调 API下面这套机制都是通用的——因为它约束的对象不是某个框架而是LLM 驱动的循环这个结构本身。一、Agent 循环的本质为什么它停不下来1.1 ReAct 的控制流是一台没有退出条件的状态机主流 Agent 范式是 ReAct模型输出Thought → Action运行时执行工具得到Observation再把 Observation 塞回上下文进入下一轮。终止信号只有一个模型自己决定输出 Final Answer。问题在于这个终止条件完全由模型的主观判断驱动而模型有三个固有缺陷概率性同样的上下文两次采样可能给出不同决策要不要再试一次本质是采样出来的不是算出来的。无跨轮显式状态它看不到我已经用同样参数调用过 5 次搜索这个事实除非运行时主动告诉它。目标漂移当检索失败时模型倾向于换个说法再试这个行为在有限次数内是合理的探索但它没有探索已经失败的判定能力。用控制论的话说Agent 是一个开环系统执行器LLM不知道系统的总成本也不知道自己是否在收敛。要在开环系统上做可靠性必须外挂一个反馈控制器。这里有一个认知上的关键转变终止不是模型的职责而是运行时的职责。模型负责的是在给定信息下下一步做什么这个局部决策而这个循环还该不该继续是全局决策全局信息已花多少 token、还剩多少步、外部系统是否在抖根本不在模型的上下文里它不可能做出正确判断。所以把终止权交给模型等于让一个蒙着眼睛的人判断车该不该停。正确的分工是模型提议运行时裁决。1.2 无限循环的五种典型形态实践中失控循环并不是一种而是五种需要不同的检测手段形态表现检测手段重复调用同工具同参数连续调用调用指纹去重振荡A→B→A→B 交替滑动窗口模式匹配重试风暴工具报错后高频重试错误计数 指数退避探索发散参数不断变化但无进展观察结果哈希/语义相似度上下文退化上下文溢出后模型输出混乱token 预算 强制收敛关键洞察是前四种都能用轨迹特征检测不需要理解语义。这意味着我们可以在不依赖第二个模型的前提下用确定性的代码拦住绝大多数失控。稍微展开一下每种形态的现场感方便你对号入座重复调用最典型往往发生在工具有缓存但模型不知道的情况下。它以为每次都拿到了新结果实际上拿到的是一模一样的旧结果于是再试一次。振荡通常是模型在两个候选路径之间反复横跳先search_docs觉得不够get_page看了一页又觉得该换搜索词换完又回到原来那页。它每一步都有道理整体却零进展。重试风暴的触发点往往是限流错误或 5xx。模型的默认反应是立刻重试而没有任何退避概念——它没有时间感一个循环里 0.1 秒和 10 秒对它是等价的。探索发散最隐蔽因为轨迹看起来很健康每步都在变都在调用工具都拿到了结果。但把 observation 的哈希集合拿出来一看全是一样的空结果。上下文退化是终局形态一旦发生模型输出的工具名可能都是不存在的此时任何检测逻辑都要防止它把畸形调用打到真实系统上。1.3 成本是超线性的不是线性的容易被忽略的一点Agent 每一轮的成本不是常数。ReAct 把全部历史对话重新喂给模型第 N 轮的 prompt token 大致是前 N-1 轮的总和。也就是说一个跑了 20 轮的 Agent其总成本接近 O(N²) 而非 O(N)。这就是为什么多跑几轮兜底这种想法在成本上极其危险——你以为多花 20%实际可能多花 200%。把这句话量化一下。设第 k 轮的 prompt 为 P_k历史全量回灌意味着P_k ≈ P_base Σ_{ik} (O_i Obs_i)假设每轮的输出加观察长度稳定为 L那么P_k ≈ P_base (k-1)·LK 轮的总输入成本就是Total ≈ Σ_{k1..K} P_k ≈ K·P_base L·K(K-1)/2 ≈ O(K²)代入一组实测数字某 Agent 的 base prompt 是 1200 token每轮 action 加 observation 约 800 token。那么第 1 轮 prompt 是 1200第 10 轮是 8400第 20 轮是 16400。跑满 20 轮的累计输入约 176K token而如果第 5 轮就收敛只需要约 14K。轮数翻 4 倍成本涨了 12 倍以上这个非线性放大就是不知不觉账单爆炸的数学原因。还有一个反直觉的推论长会话比长任务更危险。如果 Agent 每轮都带完整的多轮对话历史那么第 10 轮时它已经为前面 9 轮的无效探索重复付费了 9 次。这也是为什么上下文压缩和预算控制必须一起做——前者治本后者兜底。二、控制机制的核心原理2.1 三层防御执行前、执行中、执行后一套可靠的终止机制应该分层任何单点都不足以兜底执行前Pre-flight预算初始化、任务复杂度评估、工具白名单。决定这次允许花多少。具体动作包括根据任务类型选择预算档位单轮问答 3 步、多跳检索 8 步、代码修复 15 步把工具清单收敛到当前任务真正需要的子集减少模型手滑调用昂贵工具的概率对高风险工具写库、支付、发信打上requires_confirmation标记走单独闸门。执行中In-flight步数上限、token 预算、调用指纹、振荡检测、超时、取消信号。这是主战场。执行后Post-flight轨迹归档、成本归因、失败模式归类用于迭代 prompt 和阈值。三层的关系不是任一命中即可而是逐层收紧Pre-flight 决定资源池有多大In-flight 负责在池子见底前优雅收场Post-flight 负责让下一版跑得更省。缺少任何一层系统都会在某个特定场景下失守——只有执行中检测你会缺少成本归因永远不知道为什么循环只有执行前预算你拦不住预算很大但全打水漂的浪费。2.2 终止信号必须显式化不要指望模型自然输出一个漂亮的 Final Answer。要做两件事结构化终止要求模型在最后一步返回特定字段如stop_reason: final而不是靠解析自然语言里的答案如下。推荐用一个专门的终结工具比如submit_answer(answer: str, confidence: float)让终止变成一个可校验的 schema而不是一段需要正则匹配的散文。强制收敛当预算只剩最后一步时在请求中注入约束例如tool_choicenone或提示这是最后一步必须给出当前最优答案让模型优雅降级而不是被硬截断——硬截断会得到一个半成品的 Observation工具只返回了一半的 JSON或者模型的 Action 正写到一半就被掐掉最终落库的是一条残缺记录如果这一步恰好是有副作用的写操作你连判断它是否提交成功的依据都没有。优雅降级的具体做法有三种可以叠加使用软提示在每轮请求里附加一个系统消息告知你已使用 X/Y 步剩余预算 Z token。实测表明仅仅把剩余步数告诉模型就能让相当比例的循环在第 68 步自行收敛因为它终于有了我还有多少余量这个信息。硬约束在最后一轮把tools[]或tool_choicenone传进去物理上断掉工具调用的可能逼迫模型基于已有 Observation 作答。状态兜底运行时不依赖模型的输出格式自己维护一个best_effort_answer——每次拿到看起来有效的 Observation 就更新一次超限时直接返回它保证用户永远能得到点什么而不是一个报错。三、落地实现一个可控的 Agent 执行器原理讲完下面给出一套可以直接改吧改吧用在生产里的执行器。语言用 Python但设计对任何语言都成立。核心思路是把是否继续这个决策从模型手里拿走收归到一个确定性的控制器。3.1 执行器骨架importhashlibimportjsonimporttimefromdataclassesimportdataclass,fieldfromtypingimportAny,Callable,OptionaldataclassclassBudget:一次 Agent 运行的全部资源约束所有阈值都在这里集中管理。max_steps:int12# 硬步数上限max_tokens:int200_000# 输入输出 token 总量max_seconds:float120.0# 墙钟时间上限max_tool_calls:int30# 工具调用总次数含并行展开后的计数steps:int0tokens:int0tool_calls:int0started_at:floatfield(default_factorytime.monotonic)defexhausted(self)-Optional[str]:返回命中的约束名全部未命中则返回 None。ifself.stepsself.max_steps:returnmax_stepsifself.tokensself.max_tokens:returnmax_tokensifself.tool_callsself.max_tool_calls:returnmax_tool_callsiftime.monotonic()-self.started_atself.max_seconds:returntimeoutreturnNoneclassAgentRunError(Exception):预算耗尽或检测到失控时抛出携带部分结果供上层降级。def__init__(self,reason:str,partial:AnyNone):super().__init__(reason)self.reasonreason self.partialpartial注意exhausted()返回的是原因字符串而不是布尔值。原因会一路传到日志和用户提示里——因步数超限而提前结束和因检测到重复调用而终止是完全不同的两件事前者需要调大预算后者需要改 prompt 或工具描述。可观测性从数据结构的形状就开始了。3.2 调用指纹把重试和重复区分开重复调用是最好抓的一种但前提是你要先会归一化。直接用原始参数做 hash 会被这些噪音绕过参数顺序变化、query 前后多了空格、全角/半角混用、模型每次加一个无关的request_id。_VOLATILE_KEYS{request_id,trace_id,timestamp,nonce}def_normalize(value:Any)-Any:ifisinstance(value,str):# 归一化去首尾空格、压缩连续空白、统一大小写return .join(value.split()).lower()ifisinstance(value,dict):return{k:_normalize(v)fork,vinsorted(value.items())ifknotin_VOLATILE_KEYS}ifisinstance(value,(list,tuple)):return[_normalize(v)forvinvalue]returnvaluedeftool_fingerprint(name:str,args:dict)-str:稳定指纹同工具 语义等价的参数 → 同一个字符串。payloadjson.dumps({name:name,args:_normalize(args)},sort_keysTrue,ensure_asciiFalse,separators(,,:),)returnhashlib.sha256(payload.encode(utf-8)).hexdigest()[:16]有了指纹去重逻辑要区分两种场景成功过的调用再次出现→ 直接拦截。把上一次的 Observation 原样回灌给模型并附加一句该调用已执行过结果同上请勿重复调用。这比单纯拒绝更有效——模型看到结果仍在往往会转向下一策略。失败过的调用再次出现→ 允许有限次重试默认 1 次但必须走退避并且第二次失败后要把该指纹加入黑名单后续命中直接返回结构化错误。这里有个容易踩的坑不要无条件放行所有重试。如果工具返回的是未找到这种语义失败HTTP 200 但业务无结果它和网络超时完全不同——后者重试有意义前者重试一千次也一样。判断标准是错误里有没有retryable标记让工具实现方来声明而不是让控制器猜。3.3 振荡检测滑动窗口找周期振荡的检测思路是把最近 N 步压成一个符号序列工具名 参数指纹如果这个序列存在一个最小周期 p且至少重复了两个完整周期就判定为振荡。classOscillationDetector:def__init__(self,window:int8,max_period:int4,min_cycles:int2):self.windowwindow self.max_periodmax_period self.min_cyclesmin_cycles self.history:list[str][]defobserve(self,symbol:str)-Optional[int]:喂入一个符号返回检测到的周期长度未检测到则返回 None。self.history.append(symbol)iflen(self.history)self.window:self.history.pop(0)seqself.history nlen(seq)forpinrange(1,self.max_period1):ifnp*self.min_cycles:continue# 检查最后 p*min_cycles 个元素是否严格周期为 ptailseq[-p*self.min_cycles:]ifall(tail[i]tail[i-p]foriinrange(p,len(tail))):returnpreturnNoneA→B→A→B会在第 4 步命中 p2A→B→C→A→B→C在第 6 步命中 p3。检测到振荡后的处理不应是简单抛错而是注入打断在下一轮请求里显式告诉模型你正在重复 A 和 B 之间的切换二者都未产生新信息请换一个完全不同的策略或直接给出当前最优答案。绝大多数情况下模型会立刻跳出循环。3.4 进展度量识别看起来很忙的发散振荡检测抓的是结构重复但探索发散在结构上是变化的。这时候要看信息增益每轮的 observation 到底带来了多少新东西。classNoveltyTracker:def__init__(self,stall_patience:int3):self.seen:set[str]set()self.stall_patiencestall_patience self.stall_count0defobserve(self,observation:str)-bool:返回 True 表示有进展False 表示本轮无新信息。hhashlib.sha256(observation.strip().encode(utf-8)).hexdigest()ifhinself.seen:self.stall_count1returnFalseself.seen.add(h)self.stall_count0returnTruedefstalled(self)-bool:returnself.stall_countself.stall_patience生产环境里纯哈希判重有点严——模型换个标点就会绕过。更稳的做法是哈希 轻量相似度对 observation 做 token 化的 Jaccard 相似度与最近 K 条比较若最高相似度都超过 0.9就当作无新信息。这一步不需要向量库几十行代码就够。把这两个检测器接进循环就得到了执行器的核心defrun_agent(llm,tools,messages,budget:Budget):oscOscillationDetector()noveltyNoveltyTracker()best_effortNonewhileTrue:reasonbudget.exhausted()ifreason:# 优雅降级能返回什么就返回什么绝不抛裸异常给用户raiseAgentRunError(reason,partialbest_effort)# 剩余步数不足 2 时物理切断工具调用强制收敛force_finalbudget.max_steps-budget.steps2respllm.chat(messages,tools[]ifforce_finalelsetools.specs(),tool_choicenoneifforce_finalelseauto,)budget.steps1budget.tokensresp.usage.total_tokensifresp.is_final:returnresp.answerifnotresp.tool_calls:# 模型既没调工具也没给答案视作一次无效步注入提醒后继续messages.append({role:system,content:请给出答案或调用工具。})continueforcallinresp.tool_calls:fptool_fingerprint(call.name,call.args)ifosc.observe(f{call.name}:{fp})isnotNone:messages.append({role:system,content:检测到策略振荡请更换思路或直接作答。})break# ……执行工具、记录指纹、更新 novelty、回灌 observation3.5 超时与取消传播别让一个卡住的工具拖垮全局预算里的max_seconds只有在每个工具都真正响应取消时才有意义。现实中常见的情况是控制器超时了可那个正在跑的 HTTP 请求还挂在连接池里占着线程几分钟后返回然后写出一个迟到的副作用。正确做法是把取消信号做成一等公民沿着调用链传下去classCancelToken:轻量取消令牌沿调用链传递工具实现方有义务定期检查。def__init__(self):self._cancelledFalsedefcancel(self):self._cancelledTruedefraise_if_cancelled(self):ifself._cancelled:raiseAgentRunError(cancelled,partialNone)实践要点每个工具函数签名里加cancel: CancelToken在循环体分页拉取、流式读取里定期raise_if_cancelled()。对无法改造的三方调用用带超时的封装器兜底超时值取min(工具自身超时, 预算剩余时间)。取消要幂等且可重入多个检测器可能同时触发取消重复 cancel 不能报错。取消后的清理回滚事务、删除临时文件要走finally不要写在正常路径上。3.6 把成本归因做进收尾Post-flight 最容易被省略但它是唯一能让下一版更省的一环。每次运行结束时落一条结构化记录{run_id:r-9f3c,termination:max_steps,steps:12,tokens:186400,tool_calls:27,duplicate_calls:9,oscillation_hits:2,stall_rounds:3,cost_usd:0.62,final_answer_present:true}有了这些字段你就能回答我们的预算是定小了还是 prompt 写坏了如果termination以max_steps居多但duplicate_calls很低说明任务确实复杂预算要调大如果duplicate_calls和stall_rounds很高那是策略问题继续加预算只会更贵。四、常见踩坑与优化建议下面这些坑基本每一个更多硬核网安与AI工具包请扫码获取完整源码

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号