资讯详情

AI Agent停止策略:从失控到稳停的工程实践

📅 2026/10/22 5:17:41 | 华诺云谱 👁 阅读
AI Agent停止策略:从失控到稳停的工程实践
我第一次把Agent接到生产环境时遇到的最尴尬问题不是模型回答不对而是它停不下来。一个本该三步收尾的任务Agent在工具调用和错误重试之间反复横跳直到把预算全部耗尽。我翻日志同一个动作重复了七次换了个时间参数又重复了七次。那一刻我意识到在做AI Agent相关项目时跑得快是能力停得稳是工程。停止策略不是附属功能它直接决定系统会不会失控、成本会不会爆掉、用户会不会流失。这篇文章我会从失控现场讲起拆清楚大模型为什么不擅长“自己停下”再给出一套从请求级到全局级的分层停止策略附上可以直接抄的代码模板和我在真实项目里踩过的坑。适合正在搭Agent的开发者、做AI应用落地的工程师以及每一个被“一跑就停不下来”折磨过的人。1. 停止策略到底在解决什么问题从“跑起来”到“停得稳”1.1 一个失控Agent的现场回放我先描述一个非常典型的失控过程你大概率也遇过类似情况。某一次Agent拿到一个半结构化的用户请求进入工具调用循环。第一轮它调用了一个查询工具返回结果是“未找到相关信息”。按正常逻辑这时应该换一种查询词或者直接告诉用户信息不足。但模型没有这么做它把同一个查询请求原封不动又提交了一次第二次返回还是“未找到”。接着它猜测可能是参数格式不对把日期字段改了半天结果接口返回校验错误。第三轮它修好了日期却又因为没有带上排序参数返回了默认结果。模型认为“这个结果好像不够精准”于是又回到第一步重新发起相同查询。整个过程中同一个业务操作实际上执行了十几次其中至少五次是重复或无效的。最终这条Agent没有被业务逻辑停下来而是被外层硬编码的全局超时给掐断用户在界面等了四十多分钟只看到一句“任务超时请稍后重试”。这种问题的根源不是某个具体模型不行而是整个执行链路缺少了“终止条件”的显式设计。1.2 为什么大模型自己不会停很多人第一反应是模型不应该知道任务完成了吗事实上大模型在自回归生成时每一轮的任务只是预测下一个token它并不知道自己在真实环境里执行了几次工具调用、消耗了多少资源。模型能输出的“结束”通常是它在训练数据里学到的自然语言模式比如“好的我的回答完毕”但这种文本信号在Agent循环里并不可靠。可以类比一个没有结算单的工单系统工人接到任务后如果没有人告诉他“这项工作完成了交付物是什么预算用掉多少”他会一直按自己的理解继续操作。大模型作为生成模型天然缺乏“执行状态”的概念。它看不到自己已经调用过哪些工具、花了多少token、距离业务目标还有多远除非外部代码把这些信息显式写进上下文。Token预算也只能算半个停止条件。如果只在请求层设置了max_tokens模型生成到上限会被截断但截断不意味着业务结束。它可能停在一个工具调用的中间状态留给下游一堆半成品数据。所以停止策略必须从模型生成之外去考虑这正是执行器存在的意义。1.3 三种常见的错误停止姿势我见过不少项目在停止策略上踩同样的坑归纳下来有三种错误姿势。第一种只靠一个全局硬超时。设置“整个Agent最多跑60秒”到点就杀。这种方式实现最简单但粗粒度问题很大简单任务可能在5秒内就该结束硬等60秒浪费资源复杂任务可能需要120秒却在第60秒被一刀切用户拿不到结果。全局硬超时只能作为最后兜底不能作为主要停止机制。第二种靠“看结果好像对了”来启发式判定。比如某团队写了个规则只要模型最后的输出文本里包含“完成”“成功”等关键词就认为任务结束。表面上看很智能实际上模型很擅长在中间步骤里说出“我已经完成了”然后继续做下一件事。关键词误判率极高尤其当任务本身需要多阶段输出时一票否决式的终态判定很容易把正常流程打断。第三种没有任何止损漏斗。一条Agent从头跑到尾中间没有阶段预算没有循环检测没有动态注入干预直到异常被外部监控捕获。这种方式的隐患是出问题之后连定位都很难因为没有任何中间决策日志只能靠猜。这三种姿势都忽略了一个关键事实停止策略应该像交通管理一样分层而不是靠单一信号一竿子插到底。2. 分层停止策略请求、轨迹、全局三层缺一不可2.1 请求级生成参数和End Token约束请求级停止是最基础的一道闸。它管的是“模型单次生成不能无限长”对应的参数包括max_tokens、max_output_tokens以及各类结束标志。单一模型API如果允许设置终止序列我一定会设置至少两个一个是“FINISH”另一个是“|done|”之类的显式结束符。原因是模型在自然对话里喜欢输出“好的我已经完成了下面是结果”这类话但不会真正结束生成结构。给一个自定义的FINISH token并要求模型在任务真正完成时单独输出一行代码解析到之后就停止该轮生成既能省token又能拿到明确的语义信号。请求级还需要考虑结构化输出。如果Agent产生的每轮动作都是JSON字符串必须要求模型严格按照JSON格式输出并在生成到闭合大括号后立即停止。很多框架已经支持JSON mode或结构化输出这相当于给停止条件加了一个语法边界。没有这个边界模型偶尔会在工具调用后继续输出无关解释导致请求级token预算被白白消耗。这一层不解决长期循环但它能把每次模型调用的成本控制住避免“一步生成两三千token全是废话”的低级浪费。2.2 轨迹级识别重复与空转轨迹级的停止策略关注的是Agent已经走过的路径。核心任务是回答两个问题这个动作是不是刚刚做过了这个循环是不是已经反复多次了重复识别要做的不是简单的文本比较而是动作签名。每个工具调用可以抽象成一个动作三元组工具名、参数经过归一化后的签名、返回结果的摘要。时间戳、随机ID、trace_id这类无关字段必须从签名里去掉否则同一个业务操作会因为带上不同的trace_id而被误判为不同动作循环检测就失效了。归一化之后的签名可以快速做最近K步匹配。如果最近N步里同一个签名出现的次数超过阈值就能判定进入了重复空转。更复杂的做法是维护一个滑动窗口把窗口内的动作序列哈希后查重发现相同模式立刻触发干预。比如窗口长度为5如果当前第6步的动作模式与第1到5步完全一样说明循环周期已经形成。轨迹级停止还有一种场景是“进度停滞”动作没有完全重复但返回结果连续多轮都是空数据或错误码。这种也要触发分支切换而不是继续让模型重试。因为模型在重试时往往只能微调参数如果没有外部提示它可能倾向于继续原地打转。2.3 全局级预算、熔断、降级全局级策略负责整个Agent执行生命周期的资源控制。层级的衡量维度有三个总步数、总token、总墙钟时间。总步数预算是最常用的。它不是凭空拍出来的而应该根据历史成功样本统计正常任务需要多少步然后乘以一个安全系数。比如某类任务正常路径平均8步标准差2步那全局步数预算可以设置在15到20步之间。设置太紧会把长尾任务全部误杀太松又起不到止损作用。总token预算相对步数更贴近成本。同样是10步如果工具返回的文本都很大每一步可能吃掉3000 token那么10步就是3万token。在做预算时可以把“平均每步token消耗”与步数相乘再加上模型输出部分得到一条经验基线。墙钟时间预算主要照顾用户体验。用户在等一个Agent任务时心理预期通常只有几十秒。全局时间预算应该低于用户可接受的等待上限而不是等于它。否则一旦接近上限用户已经开始流失。全局级策略的最后两个动作是熔断和降级。熔断不是简单终止而是要保留现场证据当前状态、已完成步骤、最后一段上下文。降级则决定了熔断后用户看到什么——可以返回已有部分结果可以走一个兜底问答模板也可以标记为需要人工介入。没有降级方案的熔断本质上只是把问题从“Agent无限循环”变成了“用户得到一句冷冰冰的超时报错”。3. 可落地的停止策略模板参数选择与核心代码3.1 预算对象与基础检查先给一个非常朴素的预算对象它能在任何Agent主循环里直接使用。import time class StopBudget: def __init__(self, max_steps15, max_tokens30000, max_wall_time90.0): self.max_steps max_steps self.max_tokens max_tokens self.max_wall_time max_wall_time self.start_time time.time() self.steps 0 self.total_tokens 0 def add_usage(self, tokens: int) - None: self.steps 1 self.total_tokens tokens def should_stop(self) - tuple[bool, str]: if self.steps self.max_steps: return True, max_steps_reached if self.total_tokens self.max_tokens: return True, max_tokens_reached if time.time() - self.start_time self.max_wall_time: return True, max_wall_time_reached return False, def remaining_steps(self) - int: return max(0, self.max_steps - self.steps)参数怎么定我给出一个通用推算方法。max_steps先不要拍脑袋找二十条历史日志统计任务在正常情况下完成所需的步数。如果绝大多数任务集中在5到10步那max_steps设为15比较合理留出处理长尾的空间。max_tokens可以约等于平均每步消耗token数乘以max_steps再乘一个1.2的浮动系数。max_wall_time则直接参考产品对用户等待时长的要求通常是30到90秒。3.2 动作签名与重复模式检测轨迹级循环检测我一般用一个轻量实现动作签名生成加上滑动窗口哈希。import hashlib import json from collections import deque def normalize_args(args: dict) - dict: # 去掉与业务无关、每轮都会变化的噪声字段 ignored_keys {trace_id, request_id, timestamp, random_seed} return {k: v for k, v in args.items() if k not in ignored_keys} def action_signature(tool_name: str, args: dict) - str: normalized normalize_args(args) raw json.dumps( {tool: tool_name, args: normalized}, sort_keysTrue, ensure_asciiFalse ) return hashlib.sha256(raw.encode(utf-8)).hexdigest()[:16] class LoopDetector: def __init__(self, window_size5, repeat_threshold3): self.window deque(maxlenwindow_size) self.repeat_threshold repeat_threshold def record(self, tool_name: str, args: dict) - bool: sig action_signature(tool_name, args) self.window.append(sig) # 统计最近窗口内同一签名出现次数 count sum(1 for x in self.window if x sig) return count self.repeat_threshold这段代码的主要逻辑是对最近五步做滑窗统计。如果同一个动作签名在窗口内出现三次以上就认为已经进入重复循环。十五步的步数预算配合这个检测器能让很多“模型在同一个工具上反复横跳”的情况提前暴露。要注意的是这个检测器对参数归一化非常敏感。如果忽略字段没设置好每次调用因为trace_id不同而全被当成新动作检测器等于没写。反过来如果把核心业务字段也当成噪声过滤掉会导致不同查询被识别成同一个动作产生误报。判断噪声字段的原则只有一个去掉它之后两个调用是否仍然表示同一个业务行为。3.3 熔断降级和现场证据熔断不应该只是return要把现场证据组合成一个结构化对象方便排查和降级。下面是我常用的熔断出口def build_breakpoint(budget: StopBudget, trajectory: list, reason: str) - dict: return { stop_reason: reason, steps_used: budget.steps, tokens_used: budget.total_tokens, elapsed_seconds: round(time.time() - budget.start_time, 2), last_actions: trajectory[-5:], has_partial_result: False }为什么要把last_actions保留最近五条因为排查循环问题时最后几步往往比前面的状态更有价值。如果一个Agent在第十五步被熔断前面十步可能都是正常铺垫最后五步才暴露出重复模式。有了这些现场信息后续回溯时可以直接看出问题触发点。降级方案我通常用这样一个分支逻辑如果已经产生了可用的中间结果就把中间结果封装成“部分完成”返回给用户如果没有任何可用结果就返回一个兜底提示并触发人工复核队列。这里最重要的一条原则是不要把熔断当成异常隐藏它应该被记录成一种正常的终止原因纳入统计报表。4. 收敛信号设计让Agent学会“自我刹车”4.1 显式完成工具与状态字段很多Agent的失败根源是“结束”这个动作没有被设计成工具。模型的自然语言能力很强但让它靠感觉输出“我结束了”并不可靠。更好的做法是给Agent一个显式的完成工具比如finalize(summary)。无论之前走了多少步只要模型调用这个工具执行器立刻把它当作真正的终态信号停止继续规划。这种做法在行为层面对模型非常友好。工具调用是模型经过训练后比较擅长表达的结构化行为比自然语言结束语更确定。最终回答、结构化结果、任务ID都可以放进finalize的参数里。执行器解析到finalize之后再把该轮次标记为stop_reasonexplicit_finish。同样重要的是每轮输出状态字段。我在设计Agent循环时会强制要求模型在每轮生成JSON里带有should_continue字段值为true或false并附上continue_reason。即使在一次模型回答里没有调用工具只要should_continue为true执行器就再走一轮只有显式false才进入下一阶段的判定。这样做的价值是把“是否继续”从隐式的模型意图变成可解析的显式信号为后续预算预警和日志分析提供稳定的数据源。4.2 剩余预算提示词预算信息不光要写在外部代码里还要写进模型上下文。很多循环问题其实源于模型“不知道还剩多少步”。它以为可以无限试探所以试错成本很低。如果每一步告诉它当前是第7步最多15步你还有8次机会完成这个任务模型通常会收敛得多。我一般在每轮构造用户消息之前插入一段系统级提示类似当前预算状态已使用7步剩余8步。 请优先基于已有信息决策。如果无法取得进一步进展请调用 finalize 提供当前结果。这段提示不需要太长但位置要稳定放在对话上下文固定位置避免模型忽略。剩余步数较少时可以进一步强化措辞比如“剩余2步请勿继续尝试新方案尽快交付当前最优结果”。实测下来这种渐进式紧迫感能明显减少无意义分支探索。有人担心提示词里写预算会让模型“有压力乱收尾”实际并不会。模型不会因为压力而编造结果它在显式约束下反而更倾向于调用finalize来结束。当然前提是finalize工具本身可用模型知道调用了它可以安全收场。没有finalize的Agent收到“尽快结束”的指令后只能输出一段文字执行器依然很难判断是否完成等于白提示。4.3 行为级探测与夹断指令比提示词更强力的干预是“夹断指令”。当循环检测器判定重复模式后直接向上下文中注入一段信息检测到你已经连续三次尝试相同操作。不要继续重试。 请评估当前数据是否足够回答用户足够则调用 finalize不足则明确说明缺少哪些信息。这相当于在模型思考链路里强制打断重复。没有这段干预时模型会认为自己每一次微调都是“新的尝试”循环检测器即使报警主循环也只负责终止模型并不知道为什么被终止。有了夹断指令模型有机会在终止前自己扭转方向返回更有价值的最终结论。行为级探测还不只限于动作重复。连续多轮工具返回空结果、连续多轮调用同一个工具但输入参数只有时间戳变化、连续多轮产生相同错误码都值得触发不同的干预模板。每种模板都尽量做到“给出不重试的理由”和“告诉模型下一步可选方向”。只告诉模型“你错了”没太大用模型只会换个姿势继续错告诉它“不要再做A可以试着做B或交付结果”收敛效果会好很多。5. 真实调优实录三种典型Agent的停止策略修正5.1 购物助手单一阈值在多工具场景失效某电商场景里一个购物助手Agent负责根据用户描述筛选商品并形成推荐清单。初期只在全局设置了一个max_steps20结果频繁出现两类问题简单刚需商品推荐3步就能搞定却平均要跑12步复杂多条件筛选任务在15步左右被无端截断用户拿不到结果。后来我按场景拆分了预算。用户提问里包含明确商品名、价格区间、配送要求时视为“强约束任务”max_steps设为8包含模糊关键词比如“适合送长辈的实用礼物”视为“探索型任务”max_steps设为18。拆完之后强约束任务的平均步数从12降到6用户等待时间明显缩短探索型任务也保住了足够的空间不再被一刀切误杀。这里的关键不是数字本身而是“单一预算无法适配多类型任务”。只要Agent面向多意图预算就要跟随意图类型动态调整。代码里不需要复杂配置一张任务类型到预算的映射表就够用。5.2 数据处理Agent结果终局证据缺失导致误判另一个团队做一个数据处理Agent负责从非结构化文档中抽取字段并写入结果表。最初的停止条件是模型回答文本包含“完成”即停止。上线后经常出现两种情况模型说完成了结果表里关键的金额字段还是空的模型反复补抽字段流程迟迟不结束。问题本质是“完成”被定义成模型的一句话而不是结构化的终局证据。我帮他们把终止条件改成校验规则只有当结果表行数大于0、必填字段非空、数量字段能通过合法性范围检查时才允许finalize。校验不通过时系统会把缺失字段列表返回给模型让它只补缺失信息不重跑全流程。改完之后错误终态大幅减少。这个案例给我最大的启发是停止策略必须绑定“可验证的业务结果”而不是绑定“模型的主观表达”。任何不校验结果的Agent本质上都是在赌模型这次没说谎。5.3 内容聚合工作流循环检测从“人工看日志”到“半矢量识别”还有一个内容聚合类的Agent需要从多个来源抓取摘要再合并成报告。它的循环比较隐蔽动作签名并不完全相同因为模型每次传入的源URL排序不同但本质上还是在重复抓同一批来源。最初靠人看日志发现循环显然不现实。后来我把动作签名里的排序和分页参数做了归一化同时对工具返回内容的标题列表计算哈希只要来源集合没有明显变化就判定为“同一批内容的重复拉取”。检测到重复后触发分支切换让模型停止拉取直接基于已有信息生成报告。上线后重复拉取导致的空转步数下降约七成整体token消耗也明显下降。这个案例说明循环识别不能只看工具名和参数表面还要看“动作背后的目标”。同一个业务目标换参数、换排序、换载体仍然是同一个动作。签名归一化做得好不好直接决定检测器是真报警还是狼来了。6. 成本视角停止策略是资源预算的第一道闸门6.1 失控到底烧掉了什么算一笔Token账失控Agent最直接的代价是Token消耗。我经常用一个很简单的公式估算单次失控成本等于平均每轮token消耗乘以实际轮数。假设平均每轮输入输出合计3000 token正常任务12轮消耗36000 token失控循环跑到40轮就是120000 token成本翻了3倍以上。这个数字放在单个请求上好像并不多但放到生产环境的日活上可能会变成一笔明显上涨的账单。更麻烦的是成本上升和用户流失往往是同时发生的。用户等一个几十秒还完不了的任务体验分已经掉到底而系统方还在为这段失败体验烧掉几倍于正常路径的预算。后续连锁成本同样不容忽视。失控Agent会产生大量错误写入、重复通知、重复外呼脚本调用这些下游副作用比token成本更难清理。停止策略的意义就在这里它是在成本失控的第一个环节拉闸而不是等下游系统被污染后再强调错误处理。6.2 可观测性和自动告警把“停不下来”变成可度量事件停止策略要有效必须能观测。我建议每条Agent执行日志至少包含四个字段stop_reason、steps_used、tokens_used、elapsed_seconds。这四个字段看起来简单但它们能支撑起大多数成本问题的回溯。stop_reason尤其重要。它把所有终止情况切成可聚合的类别explicit_finish是正常完成max_steps_reached是步数熔断loop_detected是循环命中budget_exhausted是Token超限。有了这些标签就能按类别统计占比。如果某个Agent的loop_detected占比从2%涨到10%不需要看原始日志告警系统就能自动发现异常。自动告警的阈值也不需要复杂。我一般会同时关注三个指标单次运行步数超过预算的百分比、stop_reason中非正常终止占比、以及高Token消耗任务在总消耗里的占比。任何一个指标连续几个窗口走高就触发一次人工排查。与其事后靠用户投诉发现问题不如让告警先告诉你。6.3 小团队的低成本落地方案不是所有团队都有完整的监控基础设施。小团队或单人项目可以把成本观测做得很轻结构化日志输出到本地或简单日志平台再加一个定时任务统计聚合。比如每晚跑一个脚本统计当天所有Agent日志里的stop_reason分布和平均token消耗。哪怕没有仪表盘一张文本表格也能说明问题。第二周再看就能发现趋势。我见过不少团队一开始就想上重型监控反而被基建拖累。先把日志字段定好后面接什么平台都容易。成本治理的另一个低成本手段是“种子样本回归测试”。把之前出过循环问题的几十条请求固化成测试集每次调整停止策略后统一跑一遍观察是否出现新的误停、漏停或成本上升。这组回归样本的成本很低但能防止你修好A问题的同时改坏B场景价值非常大。7. 常见问题速查表与值得写进SOP的经验7.1 高频故障现象与排查手顺我把实际项目里反复出现的停止策略问题整理成一个速查表排查的时候可以按顺序走。现象可能根因排查顺序解决方案Agent在同一个工具上无限重试循环检测未生效参数归一化没过滤噪声字段先看日志里动作签名是否一致再看窗口命中次数完善噪声字段过滤设置重复阈值任务明明完成Agent还在继续没有显式finalize工具或模型没学会调用它检查上下文里是否定义finalize检查样例里是否有调用记录增加显式完成工具并在示例中强化调用路径简单任务也被熔断全局预算不区分任务类型按场景拆分执行耗时统计建立任务类型到预算的映射表停止时没有结果返回熔断逻辑只有终止没有降级查看熔断出口是否返回已有中间结果增加部分结果返回和人工复核队列调整阈值后正常流程频繁误停新策略没有做回归测试用历史正常请求跑一遍新策略建立种子样本回归集日志看不出为什么停止停止原因没有统一字段检查日志是否包含stop_reason统一停止原因枚举并记录预算字段排查顺序的核心原则是“先看现场再改策略”。很多问题不需要改模型提示词只需要把日志里的stop_reason和last_actions对上就能定位到具体环节。7.2 几条我踩坑后固定下来的经验第一不要只设一个全局max_steps就收工。按任务类型、用户意图、阶段进度拆分预算效果会好很多。第二循环检测的签名归一化永远是重点。宁可多花时间理清哪些参数是噪声也不要急着调阈值。归一化没做好后面所有检测结果都不可信。第三终态判定必须绑定业务校验。模型说“完成”不算完成结构化结果通过校验才算。校验规则最好独立于模型输出能被代码精确执行。第四熔断后一定要有降级方案。哪怕只是把已有结果返回并加一句“部分完成剩余内容已转人工”也比冷冰冰的超时报错强。第五停止策略每一次调整都要用种子样本回归。特别是涉及阈值和循环检测的改动很容易在看不见的长尾场景里产生误伤。第六把停止策略参数配置化、版本化。不要让阈值只藏在代码常量里配置项跟着版本走后续复盘时才知道是哪一次改动影响了线上表现。最后分享一个我固定下来的习惯任何Agent落地检查清单里一定会写“运行时能看到stop_reason、steps_used、tokens_used三件套”。线上出问题不可怕可怕的是出了问题却没有任何现场证据。停止策略不是性能优化它本质上是产品设计的一部分。用户不会关心模型内部跑了多少步但他们会记住这个系统是“一个任务三秒出结果”还是“转圈半小时还没完”。把停止机制设计得足够清晰Agent才真正从一个“能跑演示”的Demo变成“能上生产”的系统。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑