Agent不跑飞:四道防线构建智能体稳定性体系

Agent不跑飞:四道防线构建智能体稳定性体系 面试官问我“你的Agent怎么不跑飞”的时候我脑子里第一反应其实是“这个问题问得好说明对方真的被跑飞过的Agent坑过。”做过Agent项目的人都懂跑飞不是小概率事件而是默认会发生的产物。你让大模型规划任务、调用工具、自己迭代它就像个精力过剩的实习生方向感差、胆子大、还特别自信。所谓“不跑飞”本质上不是让Agent变聪明而是让它在变聪明之前先学会不作死。这篇文章我就把自己在实际项目里沉淀下来的一套“4道防线”完整拆开讲每一道防线解决什么问题、具体怎么落地、代码怎么写、上线之后踩过什么坑全给你捋一遍。不管你是正要开始搞Agent开发还是已经在生产环境里被各种异常搞到头大这4道防线都能直接拿来用。1. 先从根上聊Agent到底为什么容易跑飞要治跑飞得先搞清楚跑飞是怎么发生的。我观察下来Agent失控的根因基本逃不出下面四类而且很多时候是几类问题叠加在一起爆发的。1.1 规划层失控模型自己给自己挖坑LLM在长任务场景下做规划本质上是在做“有损推理”。任务一复杂它就容易把目标理解偏或者拆出一个奇怪的操作顺序。比如让它“整理销售数据并发送周报”它能规划成“先查询数据库再写一个Python脚本然后发现脚本报错就反复改脚本最后忘了发邮件这回事”。这就是典型的计划漂移——Agent顺着自己的推理链条越走越远离初始目标越来越远。这类跑飞在纯Prompt驱动的Agent里非常常见因为模型没有“目标锚点”它的每一步决策都是基于上一步的局部状态而不是基于全局目标。1.2 工具调用失序乱拿工具拿错工具反复拿工具工具调用是Agent跑飞的第二大重灾区。模型选错了工具、填错了参数、拿着只读工具去干了写操作这些都属于工具层失控。更令人头疼的是有些模型会重复调用同一个工具——比如因为结果不符合预期就不断重试同一个查询直接把外部API打到限流。我见过最夸张的一个case某个Agent在调试过程中对同一个HTTP接口连续调了将近40次每一次都是因为前一次结果里有个字段没解析出来它就不停重试。这种“死磕”行为放在人身上叫执着放在Agent身上叫故障。1.3 反馈信号异常环境给模型的反馈在误导它Agent是依赖环境反馈来做下一步决策的。工具返回的结果如果格式混乱、字段缺失、错误信息模糊模型就会在错误的理解上继续行动。打个比方工具返回了一个“success: false”但响应体里根本没写原因模型就只能猜猜着猜着就跑偏。还有一类问题是反馈过载工具返回了一个特别长的JSON或者几十页日志模型消化不了这么多信息会随机挑选一部分内容作为决策依据而且挑的那部分往往是无效的。1.4 记忆污染与信任偏差上下文被垃圾信息填满Agent的长上下文一旦被中间过程灌入了大量错误信息、重复输出和失败记录后期阶段的推理质量会快速下降。这和人类很像——你让一个人在一个充满噪音的环境里做精细判断他大概率也是糊涂的。所以应对跑飞不能只靠模型本身的能力提升必须从架构上做约束。这也是我把方案设计成“4道防线”的原因任何一道防线都有漏掉的可能但四道防线叠在一起能把失控概率压到可接受的范围。2. 第一道防线规划层约束让Agent没有机会“想太多”规划层约束的核心思路是不要给模型完全自由的规划空间而是把它的思维路径限制在一个有边界的安全区域内。自由度越高跑飞的概率越大。做Agent不是养宠物撒欢跑越远越危险。2.1 用状态机替代自由规划很多跑飞问题本质上是因为我们让大模型在一个本可以穷举的状态空间里做了自由探索。实际上如果业务场景相对固定直接把“规划”这件事退化成“状态路由”效果会好很多。举个例子一个客服工单Agent的处理流程其实是可以枚举的第一步识别用户意图第二步查询用户订单信息第三步根据查询结果决定“退款”还是“换货”还是“转人工”第四步执行操作并通知用户这种情况下完全没必要让模型自己规划。我们可以定义一个状态机让Agent在每一步只做“选择下一步状态”这个决策而不是“生成一整套行动计划”。蒙对路径的概率和在一个迷宫里瞎逛找到出口的概率区别是很大的。2.2 硬性限制迭代次数、时间预算、Token预算有些场景没法做状态机约束必须让模型自由探索。这个时候你需要三根“硬钉子”首先最大迭代次数。一个子任务超过5到8个循环还没有完成基本可以判断路径错了。继续跑下去只是在错误方向上加深错误。其次时间预算。给整个任务或每个子任务设置执行超时时间用asyncio.wait_for或者线程池的timeout参数都能实现。这个很关键因为有些外部API慢起来是没有下限的。第三Token预算。对大模型的输入输出做总量控制避免模型在一个子任务里狂写长文本把上下文塞满。这个在调用API的时候通过max_tokens参数直接卡死就行。2.3 决策门控先检查再行动我还习惯在任务启动之前加一个“preflight check”节点让模型在执行任何操作之前先输出一份“执行声明”内容包括我准备做什么、我会用到哪些工具、我预期得到什么结果。这份声明会通过一个校验器做检查如果声明里要用的工具不在白名单里直接拒绝执行如果声明里的操作类型与用户请求意图不匹配返回重新规划如果声明含糊不清没有明确的工具和参数要求模型补充这个门控能拦截掉相当大一部分“想太多”的情况因为模型在输出声明的时候就已经被迫把目标锚定了一遍。代码上可以这样实现class PlanningGuard: def __init__(self, allowed_tools, max_iterations6, max_plan_horizon5): self.allowed_tools allowed_tools self.max_iterations max_iterations self.max_plan_horizon max_plan_horizon def check_plan(self, plan): # 检查计划里的工具是否都在白名单内 for step in plan.get(steps, []): tool_name step.get(tool) if tool_name not in self.allowed_tools: return False, f工具 {tool_name} 不在白名单中 # 检查步骤数量是否超标 if len(plan.get(steps, [])) self.max_plan_horizon: return False, f计划步骤数 {len(plan[steps])} 超过上限 {self.max_plan_horizon} return True, ok这套约束的作用范围在“思考”阶段目标是让模型不产生危险的想法。但光堵住想法不够Agent最后还是要动手的所以第二道防线就布置在“动手”的工具层。3. 第二道防线工具层护栏Agent的手不能乱摸工具层是所有外部副作用的唯一出口。Agent不管怎么规划、怎么思考最终改变世界的动作都要通过工具来完成。所以只要把工具层卡死就等于给Agent戴上了一副“不管你怎么想但你就是做不了坏事”的手铐。3.1 工具注册表能用哪些工具代码里写死很多人会把工具列表直接当作系统提示词发给模型然后指望模型自觉遵守。这在测试环境也许没问题生产环境就不行了。模型在上下文里看到工具名不代表它真的能用更不代表它应该用。我推荐做法是“双重白名单”第一份白名单放在系统提示词里告诉模型“你有这些工具可以用”第二份白名单放在工具执行器里在执行函数调用之前做硬校验。第二份白名单才是真正的安全边界因为代码的校验比模型的自觉可靠得多。class ToolRegistry: def __init__(self): self._tools {} self._allowlist set() def register(self, name, func, allowTrue): self._tools[name] func if allow: self._allowlist.add(name) def execute(self, name, **kwargs): if name not in self._allowlist: raise PermissionError(f工具 {name} 不在执行白名单中) if name not in self._tools: raise ToolNotFoundError(f工具 {name} 未注册) return self._tools[name](**kwargs)这里有个容易踩的坑白名单集合一定要放在注册表内部不要放在外部模块里。我之前就是图省事把白名单做成了全局变量结果测试代码里有人把它改成了空集合所有工具一夜之间全部变得不可用排查了半天才找到原因。封装成类把状态藏起来能避免很多低级问题。3.2 参数Schema校验模型给的参数不一定合法模型生成工具调用参数时偶尔会生成一些“看起来合理但完全非法”的值——字符串传给数字字段、必填字段缺失、枚举值填了一个捏造的值。这些问题在业务复杂的Agent里非常常见。解决办法是给每个工具定义严格的JSON Schema在执行前做校验。Python生态里用jsonschema库就行或者用Pydantic做数据类校验后者更符合Python风格。from pydantic import BaseModel, ValidationError class SendEmailParams(BaseModel): to: str subject: str 默认主题 body: str priority: Literal[low, normal, high] normal def execute_tool_with_validation(tool_name, params): if tool_name send_email: try: validated SendEmailParams(**params) except ValidationError as e: return {success: False, error: f参数校验失败: {e}} return send_email(validated.to, validated.subject, validated.body, validated.priority) ...我在实际项目里发现加了参数校验之后工具执行错误率下降了不止一半。之前很多“工具内部报错”的日志其实根本不是工具的问题是模型传进来的参数就是错的。校验器能在执行之前拦住这些脏数据让错误信息更干净也更方便回头追查是哪一步让模型产生了错误的参数。3.3 调用频率与幂等控制防止Agent嗨起来刷接口一个失控Agent在短时间内高频调用同一个工具造成的破坏力比它调用一个错误的工具还大。哪怕每个调用都是正确的高频调用也足够把外部API打到限流甚至产生重复的订单、重复的退款、重复的邮件。我建议做两层控制频率控制和幂等控制。频率控制可以用一个简单的字典计数器在每个工具调用之前检查单位时间内的调用次数。幂等控制则要求所有有副作用的工具都接收一个request_id参数执行之前先查一下这个ID是否处理过处理过就直接返回上次的结果不做重复执行。这个方案配合起来的效果是即使Agent因为某些原因对同一个操作重复发起了10次请求最后对外部系统只产生1次副作用。这在生产环境里能帮你挡掉特别多事故。3.4 高危操作二次确认写操作、删操作、花钱的操作都要拦工具按危险等级需要分类。只读类的查询工具可以直接放行。写操作、删除操作、涉及支付的操作、调用费用很高的操作必须走“二次确认”流程。这个确认流程可以是让Agent先输出“确认请求”再由另一个独立模型或者人工审批。我通常用一个简单的分级策略危险等级操作类型策略L1查询、读取、搜索自动放行L2写入、更新、发送模型二次确认 上下文校验L3删除、支付、批量操作人工审批或延迟执行二次确认在代码里的实现可以做成一个装饰器拦截器逻辑统一处理开发新工具时只需要声明危险等级不用单独写审批逻辑维护成本低很多。4. 第三道防线执行层监督在循环里安插检查点工具层卡住了“手”但Agent大脑里的推理过程还是有可能逐渐偏离方向。执行层监督负责的就是在每个ReAct循环里安插检查节点让Agent在迈出下一步之前先被“照一下镜子”。4.1 观察验证器模型对工具结果的解读需要被检查模型每执行完一次工具调用会拿到一个“Observation”观察结果然后基于这个观察生成下一步的Thought和Action。问题在于如果观察结果本身不健康模型的下一步推理就会被带偏。所以我在执行循环里强行插入了一个观察验证步骤工具返回结果之后不能直接交给模型必须先用代码做一次基础健康检查。def validate_observation(tool_name, result): if result is None: return False, 工具返回了空结果 if isinstance(result, dict) and result.get(error): return False, f工具报错: {result[error]} if isinstance(result, str) and len(result.strip()) 0: return False, 工具返回了空字符串 if isinstance(result, (list, dict)) and len(result) 0: return False, 工具返回了空集合 return True, ok这一步检查看起来很简单但能拦截掉大量后续噪音。模型在看到一个空列表的时候可能会生成“没有数据我要换个查询条件再查一次”的决策然后陷入查询死循环。如果你在把结果交给模型之前就拦截住让工具重新执行或者返回一个更明确的错误提示模型的决策质量会高很多。4.2 自省节点让模型周期性“停下来想想”自省节点是很多Agent框架里没有的机制。ReAct循环通常就是Thought-Action-Observation无限流转模型根本没有“复盘”的环节。但人类做复杂任务时是需要定期停下来重新审视整体目标的。我的做法是每经过两个完整循环就强制插入一个self-reflection节点。在这个节点里模型不能调用工具只能输出一段结构化思考回答三个问题当前已经完成了哪些子目标这些子目标与最终目标的偏差是多少是否应该调整执行策略这个自省输出同样会经过一个校验器校验模型是否真的在反思而不是敷衍。如果模型连续两次自省都说“方向正确”但执行结果持续异常监督器会直接判定为计划漂移终止当前路径并回退到安全状态。自省节点的代价是增加了一部分模型调用开销。实测下来大概增加10%到15%的token消耗但换来的是任务成功率的明显提升这笔账我觉得非常划算。4.3 沙箱隔离能产生副作用的操作在隔离环境里跑工具里如果涉及动态代码执行比如模型自己写一段Python代码来处理数据这段代码绝对不能直接在本机跑。正确做法是放到沙箱环境里比如Docker容器或者云函数里执行网络策略默认拒绝一切外联只允许访问白名单内的API。这个属于老生常谈了但我还是要强调生产环境里模型生成的代码执行权限一定要最小化。别觉得自己的模型“不会写恶意代码”它不需要恶意只是正常代码里带一个os.system(rm -rf /tmp/xxx)就够让人头疼了。沙箱兜底出了事还能恢复。4.4 预算跟踪Token、费用、调用次数全部实时累计执行层监督的另一项重要能力是预算跟踪。每个Agent任务在启动时分配一个预算对象里面包含最大Token消耗输入输出累计最大工具调用次数最大外部API费用最大执行时长每次执行一步就从预算里扣减。预算耗尽时Agent不能继续运行必须转入汇报状态。这个机制的价值在于即使前面三道防线全部失效预算也会成为最后一道硬性物理限制保证Agent不会无限消耗资源。class BudgetTracker: def __init__(self, token_budget, api_call_budget, cost_budget): self.token_budget token_budget self.api_call_budget api_call_budget self.cost_budget cost_budget self.used {tokens: 0, api_calls: 0, cost: 0.0} def consume(self, tokens0, api_callFalse, cost0.0): self.used[tokens] tokens self.used[api_calls] 1 if api_call else 0 self.used[cost] cost return self.is_exceeded() def is_exceeded(self): return (self.used[tokens] self.token_budget or self.used[api_calls] self.api_call_budget or self.used[cost] self.cost_budget)有些读者可能觉得预算跟踪没什么了不起。但在我负责的一个数据分析Agent项目里第一版上线后大概两周就是靠费用预算拦住了Agent重复调用付费OCR接口的问题直接省了一大笔账单。预算不是功能是保险。5. 第四道防线反馈与熔断飞出去了也能拉回来前三道防线管的是“不让它飞出去”第四道防线管的是“万一飞出去了怎么快速发现并拉回来”。没有这道兜底前面的防线就只能应对已知风险面对长尾的、不可预见的异常组合时依然无力。5.1 异常检测系统要能自动发现Agent在失控我先说一个数据人工实时盯着Agent日志来发现跑飞平均响应时间大概是几十秒到几分钟中间Agent可能已经执行了十几个错误操作。自动检测要好得多。异常检测需要在Agent运行过程中持续采集心跳和状态信号包括当前循环次数、最近动作序列、最近工具调用结果。然后通过规则引擎做判断以下几条命中任意一条就触发告警连续三轮Thought内容高度相似用哈希或向量距离判断说明在重复循环。工具调用序列里出现超过3次相同操作且中间没有其他有效动作。Observation里连续出现error或者exception关键词。自省节点的输出连续两次为“继续执行”但执行结果没有任何进展。检测逻辑用一段简单的状态追踪就能实现不必上复杂模型。class LoopDetector: def __init__(self, threshold3): self.thought_hashes [] self.threshold threshold def record(self, thought): h hash(normalize(thought)) self.thought_hashes.append(h) if len(self.thought_hashes) self.threshold: recent self.thought_hashes[-self.threshold:] if len(set(recent)) 1: return True # 检测到重复循环 return False5.2 自动降级从自主模式切到受限模式一发现异常不是直接杀进程就结束了。更优雅的做法是让Agent自动降级。降级策略按严重程度分几级一级降级禁止调用L2和L3危险等级的工具只允许只读操作。 二级降级暂停工具调用能力Agent只能输出文本解释当前情况和计划形成半成品报告。 三级降级终止Agent执行将当前状态打包成上下文转交给人工。降级和熔断的区别在于熔断是完全停止降级是“先限制能力再观察”。如果Agent在降级模式下能自己恢复正常产出一个合理报告那这次任务就不算完全失败。如果降级模式下还在持续输出垃圾结果那就直接熔断。5.3 熔断与人工审批给人类插手的机会熔断器就是一道纯物理开关。触发条件包括检测到重复循环、预算耗尽、连续降级后仍无好转。熔断一旦触发所有工具调用入口直接关闭只有人工确认之后才能恢复。这里有个关键点熔断状态的恢复必须走人工不能由Agent自己恢复否则熔断的意义就没了。对于L3级别的高危操作我会在代码里加一个人工审批的hook。比如“删除用户数据”这样的操作即使Agent规划了、调用了、参数校验也通过了如果没有人点一下“确认执行”这个操作就只会停留在待执行列表里。这个审批逻辑实现起来很简单但对于生产环境的稳定性帮助巨大。人工审批的过程会在前面加一层“慢就是快”的逻辑操作对应的业务影响越大审批流程越严格。没必要对每个工具调用都加人工审批否则Agent就失去了自动化意义但高风险操作必须有人兜底。5.4 经验回放与灰度演练把昨天的故障变成今天的防线第四道防线里我最想强调的一件事是每次线上故障处理完毕后一定要把这次的失败案例保存下来然后定期“回放”一遍。回放的方式很简单把历史会话中导致跑飞的用户请求重新喂给Agent跑一遍验证当前的防线能不能拦住。这个机制能防止防线退化。模型升级、Prompt调整、工具数量增加都可能导致防线的有效性下降。如果每次变更之后都跑一圈回归测试就能提前发现哪些防线开始失效。我在项目里维护了一个“跑飞案例库”每一条都记录了触发条件、当时的错误链、现在的防线是否拦截。目前这个案例库已经积累了上百条每一次新增案例都是系统抗风险能力的一次增强。6. 面试场上这个问题应该这样答回到标题里的场景。面试官问“你的Agent怎么不跑飞”很多人第一反应是讲技术细节一上来就说用LangGraph还是用ReAct还是用Plan-and-Execute。但面试官想听的往往不只是框架选型。我建议按“问题定义、分层防御、实际效果、思考延伸”四个层次来组织回答。首先把“跑飞”定义清楚。跑飞不是一个单一现象它可能是指计划漂移、工具误用、循环死锁、预算超支、上下文污染也可以是以上几种的叠加。先把定义讲清楚能向面试官展示你有结构化拆解问题的能力。然后沿着四道防线的思路依次讲规划层怎么做约束工具层怎么做白名单和校验执行层怎么做监督和检查点最后兜底怎么做熔断和人工审批。接着一定要带上真实数据。哪怕数据是自己项目里的也很有说服力。比如“接入四道防线之后我们Agent任务的失败率从15%降到了3%左右”、“单次任务的平均工具调用次数从9次降到5次左右”、“高危操作的人为审批拦截率达到100%”。有数据支撑的回答和纯理论背诵的差距面试官一耳朵就能听出来。最后可以谈一点延伸思考。比如Agent跑飞问题的终极解法不是靠更多的防线而是靠更好的模型推理能力、更好的工具设计让工具本身就具备强约束以及更好的评估体系来提前发现风险。这种回答会让面试官觉得你不只是在背方案而是有深度的系统思考。7. 生产环境里我踩过的那些坑与排查技巧前面讲的都是框架性内容最后这部分我挑几个真实发生过的线上问题讲讲排查思路和解决办法。这些坑在文档里很难看到但碰到一次就够让人长记性的。7.1 案例一Agent陷入了“工具结果纠结症”现象是Agent在拿到工具返回之后反复重新调用同一个工具每次调用的参数略有不同但始终没有得到“满意”的结果于是无限循环。排查过程是翻日志发现循环模式思考-调用-校验失败-再思考-再调用。真正的原因不是工具调用错误而是工具返回的数据结构里有大量空字段模型把空字段理解成“数据还没准备好”因此不断重试。解决思路是在观察验证器里加了“数据完整性评分”空字段比例超过阈值时直接给模型返回“这部分数据缺失继续尝试不会改变结果请基于现有字段做判断”。这就从源头掐断了重复循环的诱因。7.2 案例二模型对“黑盒工具”的过度信任有个Agent依赖一个内部推荐引擎的输出做决策但推荐引擎偶尔会返回低质量的默认推荐。模型没有判断能力就会把这些默认推荐当真一路执行下去。排查下来发现是工具返回的置信度字段是浮点数但模型经常忽略这个字段。解决方法是把工具返回的报告用结构化的方式重新组装把置信度直接嵌入到Recommendation的摘要里并且告诉模型“置信度低于0.4的推荐结果默认视为无效”。这类问题的本质是模型不知道工具结果里哪些信息重要。工具返回的信息应该经过预处理做了降噪之后再进入Agent的上下文。7.3 案例三参数Schema校验救了我一命有一次某个Agent在测试环境里持续报“内部错误”我以为是代码bug排查半天才发现是有个模型在生成参数时把一个非法的枚举值传了进来。那个枚举值从字面上看非常合理比如把pending写成了pendings人类很难一眼看出来但Pydantic校验器一秒钟就识别出来了。从那以后我对工具参数的Schema校验就特别上心。凡是能用类型系统表达的约束绝不只靠Prompt来描述。Prompt容易被模型忽略代码校验不会。7.4 常见问题速查表症状可能原因排查方向任务反复执行但无进展计划漂移、目标锚点丢失检查自省节点输出检查Thought序列哈希是否重复工具调用频率异常升高模型对观察结果不满意导致重试检查观察验证器是否过滤了无效结果参数错误频繁模型生成的参数结构不可靠检查Schema校验规则覆盖是否充分任务耗时突然变长模型生成了过长回复Token预算吃紧检查Token预算跟踪和执行超时时间高危操作被多次触发幂等控制失效检查request_id是否贯穿整个执行链路降级后仍无法恢复熔断条件触发不正确检查降级状态机的状态转换逻辑模型“一意孤行”不听提示上下文被垃圾信息污染检查记忆模块的内容过滤规则写在最后的一些经验说了这么多其实我做Agent项目最大的体会是所谓的“不跑飞”靠的从来不是某个单一模型的高智商而是一整套工程约束的协作结果。规划层管住思想、工具层管住手脚、执行层管住过程、熔断层管住底线。任何单独一层都不完美但叠在一起整个系统的失控概率就会降到极低。我建议每个做Agent相关工作的朋友都把自己项目里遇到的跑飞案例收集起来做成一套自己的“防线测试集”。这个测试集会随着你的项目一起成长比任何理论方案都更有价值。你也不需要一次就把四道防线全部做完整。可以从最迫切的一道开始先解决线上最疼的问题再逐步叠加其他能力。等你的Agent真正做到“大规模生产可用而不跑飞”的时候你大概会对这个问题有一种新的理解跑飞不可怕可怕的是你不知道它在哪一层飞出去的。