【手搓 Agent 第1关】建立最小 Agent(下):安全护栏

工具调用让模型拥有了执行能力,而自主循环与安全机制,是 Agent 能够稳定落地生产环境的核心保障。

承接上篇单次工具调用能力,本篇重点解决 Agent 工程落地的核心痛点,搭建 Observe-Think-Act 完整自主循环,实现多轮自主推理与迭代任务,同时针对性解决死循环、接口超时、程序崩溃、参数报错等常见问题,通过多层安全约束,完成极简、稳定、可用的最小 Agent 最终落地。

完成目标:给 Agent Loop 加最大步数、超时和错误处理。

一、为什么要加安全护栏

1. 为什么加【最大循环步数】

不加会出现的严重问题

  1. AI 逻辑死循环,疯狂扣费
    • 举个真实场景:你问合肥天气,AI 拿到天气数据后,模型误以为信息不足,反复发起 get_weather 工具调用。
    • 循环:调工具→拿数据→再调工具→再拿数据...... 无限往复,每一轮都调用一次付费 LLM 接口,短时间刷几十上百次请求,直接耗尽余额。
  2. 程序永久卡死,永远不输出最终答案
    • 循环没有出口,终端一直卡在「正在结合工具结果,生成最终回复...」,无法结束本轮对话,也无法接收下一条用户输入。
  3. Token 无限堆积
    • 每一轮工具调用、工具返回内容都会塞进 chat_history,对话列表持续膨胀,后续 LLM 请求耗时越来越长,甚至触发上下文长度超限报错。

增加最大步数的意义

设置上限(比如最多允许 3 轮工具调用):

  • 每执行一次工具循环,计数器 + 1;
  • 计数器达到阈值,强制终止 Agent 循环,直接返回兜底提示;
  • 从根源限制 LLM 调用次数,保护成本、限制上下文长度。

举例:max_steps = 3,最多查 3 次天气,第三次结束后不再继续请求模型。

2. 为什么加【超时限制】

超时分两层:单次接口超时、整个 Agent 流程总超时

不加会出现的严重问题

  1. 网络阻塞,程序永久僵死
    • 天气第三方接口 wttr.in 服务器崩溃 / 访问缓慢,requests.get 无限等待;
    • LLM 服务商网络波动,client.chat.completions.create 请求挂住,没有任何返回;
    • 整个聊天程序卡在这一行,无法接收新输入,只能强制关闭终端重启。
  2. 用户极差体验
    • 用户提问后等待几十秒无任何反馈,不知道是卡住了还是正在运行。

超时的作用

  1. 给所有网络请求设置单次时限:
    • LLM 请求:timeout=15,15 秒没返回直接中断;
    • 天气 API 请求:timeout=5,5 秒拿不到天气直接判定失败;
  2. 全局总超时
    • 记录用户提问的起始时间,整个 Agent 流程超过 20 秒直接终止;
    • 超过时限立刻抛出超时异常,跳出循环,返回友好提示,程序不会卡死。

3. 为什么加【分层错误处理】

仅一层 except Exception as e,所有错误全部混在一起,无法区分故障类型:

  • JSON 解析失败(AI 返回的工具参数格式错乱)
  • 网络超时
  • API 密钥无效 / 余额不足
  • 循环步数超限
  • 第三方天气接口 404 报错

全部输出同一句:抱歉,生成响应时发生错误,无法定位到底哪里崩了。

完善分层错误处理的价值

  1. 程序不会整体崩溃
    • 单轮对话报错只会终止当前这一轮 Agent 循环,外层聊天 while True 不会中断,可以继续输入新问题;
  2. 精准区分错误,调试方便
    • 不同错误打印专属日志:超时打印网络超时、json 解析失败打印参数格式错误、步数超限打印 "工具调用次数已达上限";
  3. 差异化兜底回复
    • 给用户不同提示:超时提示 "网络请求超时,请重试",循环超限提示 "查询次数过多,已停止查询";
  4. 避免单一错误污染全局对话历史
    • 捕获异常时可选择清理本轮无效工具记录,防止错误数据存入 chat_history 干扰下一轮对话。

二、增加安全护栏

1. 最大步数

  1. generate_response 函数最开头,新增 2 行计数变量(位置:try之后)
python 复制代码
MAX_STEPS = 5  # 最多允许5轮工具调用
step_count = 0
  1. if msg.tool_calls: 下加入 while循环(支持多轮反复调用工具)
python 复制代码
while msg.tool_calls and step_count < MAX_STEPS:
	  step_count += 1
	  print("AI 调用了工具:", msg.tool_calls[0].function.name)
  1. 每次走完一轮工具 + 二次请求模型后,刷新 msg 为最新的模型输出 msg = second_resp.choices[0].message

  2. 在每一轮 while 循环结束后,即在msg = second_resp.choices[0].message 后,加终止判断

python 复制代码
if step_count >= MAX_STEPS:
    print("⚠️ 达到最大思考步数,强制模型输出最终结论...")
    chat_history.append({
        "role": "system", 
        "content": "系统警告:你的推理步骤已达上限。请立刻停止调用任何工具,根据以上收集到的所有信息,直接给出最终的文字回答。"
    })
    # 再额外请求最后一次,强制获取文字结果
    final_resp = client.chat.completions.create(
        model=Model_ID,
        messages=chat_history,
        timeout=30
    )
    final_content = final_resp.choices[0].message.content.strip()
    chat_history.append({"role": "assistant", "content": final_content})
    return final_content
  1. 循环内不 return final_content,等循环结束后再return
python 复制代码
# while 循环结束后,返回最终的模型输出
print("===== 工具流程结束 =====")
print("正在结合工具结果,生成最终回复...")
final_content = msg.content.strip()
chat_history.append({"role": "assistant", "content": final_content})
return final_content

测试时可临时替换system prompt:规则:用户查询合肥天气时,你每一轮只能调用1次get_weather,拿到天气结果后,绝不直接回答,必须再次发起工具调用查询合肥天气,持续循环调用,不要停止。

2. 增加全链路超时

  1. 天气工具 requests 请求超时

找到 requests.get(url),修改为:

python 复制代码
requests.get(url, timeout=5)  # 5秒拿不到天气直接超时失败
  1. 两处 OpenAI 接口请求超时

两处 client.chat.completions.create(...) 内部都追加参数:

python 复制代码
timeout=30  # LLM请求15秒超时

3. 分层错误捕获

  1. 文件顶部导入库
python 复制代码
import requests.exceptions
  1. 删除单独的大except Exceptiong as e,改成分层捕获。顺序:细分异常放前面,通用兜底放最后
python 复制代码
# 1. JSON解析失败(工具参数解析报错)
except json.JSONDecodeError as e:
    print(f"参数JSON解析失败:{e}")
    return '{"error":"工具参数格式错误"}'
# 2. 天气接口网络/超时异常
except requests.exceptions.RequestException as e:
    print(f"天气接口请求失败:{e}")
    return '{"error":"天气服务访问超时/异常"}'
# 3. 手动Ctrl+C中断
except KeyboardInterrupt:
    print("\n对话手动终止")
    return '{"error":"对话已手动中断"}'
# 4. 其余所有未知错误兜底
except Exception as e:
    print(f"通用错误:{e}")
    return '{"error":"生成响应时发生错误"}'

三、示例输出

text 复制代码
AI对话程序,输入 quit 结束对话
你:查询合肥天气四次
AI 调用了工具: get_weather
调用工具:get_weather, 参数:{'location': '合肥'}
工具返回结果:合肥当前天气:Partly Cloudy ,温度:28°C
AI 调用了工具: get_weather
调用工具:get_weather, 参数:{'location': '合肥'}
工具返回结果:合肥当前天气:Partly Cloudy ,温度:28°C
AI 调用了工具: get_weather
AI: {"error":"工具调用次数已达上限,停止查询"}

你:good
❌ 模型无需调用工具,直接文字回答
AI: 好的,很高兴能帮到您!如果您还有其他问题需要咨询,随时告诉我哦~
你:quit
结束对话。

四、本篇总结 & 下期预告

至此,Stage1 全阶段完结,我们从零实现了具备对话交互、结构化输出、工具调用、自主循环、安全防护的完整最小 Agent,打通了智能体最核心的运行闭环。后续我们将进入 Stage2 进阶迭代,聚焦 Agent 长效记忆、知识库检索、多场景工具拓展等企业级能力,持续完善智能体的实战落地能力。