智能体试运行时怎样设置暂停条件

告警群里的 API 扣费通知在半小时内响了上千次。单次调用费用从几分钱突然飙升到单小时数百美金。

当时线上正在运行一个处理客服多模态工单的 AI Agent 系统。该 Agent 负责读取用户上传的故障截图与文本描述,通过 Tool Calling 调用内部查询接口并自动回复。然而某个边缘场景下,图像识别返回了一个格式微小的错误,Agent 在理解这个报错时陷入了自推导死循环:不断尝试调用查询工具、收到报错、再次修正参数、再次发起调用。

当系统脱离了明确的流程边界,非确定性的 Agent 就会像一个不知疲倦且不断吞噬 API 预算的盲目轮询程序。

1. 凌晨 3 点的账单预警:单小时调用费用暴涨 18倍

排查日志时,发现这起事故的起因非常琐碎。

一位用户提交了一张尺寸异常的透明 PNG 格式报错截图。多模态大模型在读取图片时,OCR 节点提取出了空字符串,随后将控制权交给了 ReAct(Reasoning + Acting)Agent 循环。

Agent 在思考步骤中给出了这样的推理:
"当前无法获取错误码,尝试调用 query_order_status(order_id='') 补充信息。"

后端 API 拦截到空参数后返回了标准的 400 Bad Request。Agent 收到 400 状态码后,没有选择放弃,而是判定“参数可能格式不对”,接着构造了 query_order_status(order_id='null') 重新发包。

这个死循环在 40 分钟内重复了 1400 次。因为多模态模型的输入包含高清图像 Token,单次 Tool Calling 携带的上下文历史层层叠加,Token 消耗呈立方级爆缩。

2. 追踪 Agent 轨迹日志:Tool Calling 陷入循环补全死结

分析 Agent 的推理轨迹(Execution Trace),发现传统的超时机制(Timeout)在这里失效了。

因为单次 HTTP API 调用都是在 200ms 内成功完成的,服务端认为请求完全健康。失效发生在 Agent 的“语义决策环”上。

Agent Trace 节点记录:
Step 1: Vision LLM 解析图片 ➔ 返回空文本 ➔ 选择 Tool [query_db]
Step 2: Tool [query_db] 报错 400 ➔ 拼接报错信息至 Context ➔ 选择 Tool [query_db]
Step 3: Tool [query_db] 报错 400 ➔ 拼接更长上下文 ➔ 选择 Tool [query_db]
...
Step 82: Context 长度增加至 112K Tokens ➔ 每次单步调用耗时 8 秒 ➔ 费用暴增

LLM 的补全倾向在这种场景下成了副作用。在缺乏显式限制的情况下,模型会不断试图通过调整参数来“讨好”返回报错的工具,直到达到上下文窗口极限或被外部强行杀掉掉。

3. 状态机分层熔断与 Token 预算控制链路

要在运营过程中及时止损,就不能把控制权全盘托管给 Agent 的内部思考逻辑。必须在框架层施加确定性的物理闸门。

我们构建了一套三层止损防御体系:

这套架构把止损拆为三个维度:
第一层是硬性的 Token 消耗与金额预算上限;
第二层是相同工具调用的重复失败阈值;
第三层是上下文历史的滑动窗口裁减,防止历史报错无休止地拉大上下文体积。

4. 面向生产环境的多模态 Agent 耗速限制与状态熔断器实现

下面是在 Agent 框架核心调度环中集成的熔断与止损控制逻辑。

import time
from typing import List, Dict, Any, Callable

class AgentBudgetExceededException(Exception):
    """单次会话预算超限异常"""
    pass

class ToolLoopDetectedException(Exception):
    """工具调用死循环异常"""
    pass

class AgentGuardrailController:
    def __init__(
        self,
        max_cost_usd: float = 0.50,
        max_tool_calls: int = 10,
        max_same_tool_failures: int = 3,
        input_token_cost_per_k: float = 0.003,
        output_token_cost_per_k: float = 0.015
    ):
        self.max_cost = max_cost_usd
        self.max_tool_calls = max_tool_calls
        self.max_same_tool_failures = max_same_tool_failures
        self.input_cost_k = input_token_cost_per_k
        self.output_cost_k = output_token_cost_per_k
        
        # 运行时状态变量
        self.accumulated_cost = 0.0
        self.total_tool_calls = 0
        self.tool_failure_tracker: Dict[str, int] = {}

    def track_token_usage(self, prompt_tokens: int, completion_tokens: int):
        """实时计算 Token 计费"""
        cost = (prompt_tokens / 1000.0) * self.input_cost_k + \
               (completion_tokens / 1000.0) * self.output_cost_k
        self.accumulated_cost += cost
        
        if self.accumulated_cost > self.max_cost:
            raise AgentBudgetExceededException(
                f"会话预算达到上限!已消耗 ${self.accumulated_cost:.4f},超过阈值 ${self.max_cost}"
            )

    def validate_tool_execution(self, tool_name: str):
        """校验工具调用频次上限"""
        self.total_tool_calls += 1
        if self.total_tool_calls > self.max_tool_calls:
            raise ToolLoopDetectedException(f"单次 Agent 执行工具总次数超过上限: {self.max_tool_calls}")

    def record_tool_result(self, tool_name: str, is_success: bool):
        """记录工具执行状态,发现连续失败立即熔断"""
        if is_success:
            self.tool_failure_tracker[tool_name] = 0
        else:
            current_failures = self.tool_failure_tracker.get(tool_name, 0) + 1
            self.tool_failure_tracker[tool_name] = current_failures
            if current_failures >= self.max_same_tool_failures:
                raise ToolLoopDetectedException(
                    f"工具 [{tool_name}] 已连续失败 {current_failures} 次,触发链路熔断!"
                )

class SafeAgentRunner:
    def __init__(self, agent_engine, guardrail: AgentGuardrailController):
        self.agent = agent_engine
        self.guardrail = guardrail

    def run_multimodal_task(self, image_path: str, user_text: str) -> str:
        context = self._init_context(image_path, user_text)
        
        while True:
            try:
                # 调用大模型决策下一步
                step_output = self.agent.predict_next_step(context)
                self.guardrail.track_token_usage(
                    step_output.usage.prompt_tokens,
                    step_output.usage.completion_tokens
                )
                
                if step_output.is_final_answer:
                    return step_output.final_response
                
                # 尝试执行 Tool
                tool_name = step_output.tool_to_call
                tool_args = step_output.tool_args
                
                self.guardrail.validate_tool_execution(tool_name)
                
                tool_success, result_data = self._execute_tool_safely(tool_name, tool_args)
                self.guardrail.record_tool_result(tool_name, tool_success)
                
                # 裁减上下文长度,只保留最近 5 轮 Tool 对话
                context.append_tool_result(tool_name, result_data)
                context.truncate_history(max_rounds=5)
                
            except (AgentBudgetExceededException, ToolLoopDetectedException) as e:
                # 记录兜底日志并触发业务止损转接
                self._log_emergency(str(e))
                return "很抱歉,当前工单处理遇到异常,已自动为您转接人工客服处理。"

    def _execute_tool_safely(self, name: str, args: dict) -> tuple[bool, Any]:
        try:
            res = self.agent.call_tool(name, args)
            return True, res
        except Exception as err:
            return False, str(err)

    def _log_emergency(self, msg: str):
        print(f"[AGENT 熔断拦截点]: {msg}")
    def _init_context(self, img: str, txt: str):
        return self.agent.build_multimodal_context(img, txt)

这段实现确保了 Agent 在底层模型产生逻辑死循环时,能够在 3 次工具失败或消费达到 0.5 美元时终止执行,避免把资源浪费在无意义的尝试上。

5. 止损之后的防线:别把业务控制权交给模型

多模态 Agent 展现了极强的自主分析能力,但系统设计的核心永远是可控性。

在生产环境中落地 Agent 业务,必须确立三条安全红线。

第一,所有的外部写操作(如退款、下单、修改数据库)必须经过显式确认授权。Agent 可以决定读操作,但在执行有副作用的写操作时,必须生成确定性的 API 参数并提交给审批闸门。

第二,Token 消费必须做租户级与会话级双重 Rate Limiting。不设置上限的 Agent 调度器,本质上是一个开放的资金消耗漏洞。

第三,建立监控告警指标而非只看 Agent 成功率。平均工具调用次数、工具连续失败率、会话 Token 增长梯度,这些指标比单一的最终成功率更容易提前暴露系统的潜在风险。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐