Python Agent 踩坑实录:第一次写 Agent,我忘了加一行代码,循环跑了 200 次
摘要:本文从一次"初学者写 Agent 忘了加最大步数限制、循环跑了 200 次才停"的经历出发,分析 Agent 循环失控的三个常见原因:没有
max_steps、工具描述模糊导致模型反复试错、不检查模型是否已经给出答案。文章给出一段 30 行的完整可运行代码和三个必须加的保护措施,适合刚接触 Agent 的 Python 学习者。
文章目录
- Python Agent 踩坑实录:第一次写 Agent,我忘了加一行代码,循环跑了 200 次
- 一、问题背景:我的 Agent 跑了 200 次才停
- 二、最小复现代码:五步循环失控
- 三、原因分析:三个初学者常犯的错误
- 错误一:用 while True 而不是固定次数循环
- 错误二:工具描述写得太模糊
- 错误三:没有把模型的最终回答和工具调用区分清楚
- 四、修复方案:三个必须加的保护
- 保护一:固定循环次数
- 保护二:把工具描述写清楚
- 保护三:打印每一步,看清楚在发生什么
- 完整代码
- 五、初学者容易忽略的其他细节
- 细节一:`tool_call_id` 必须对应
- 细节二:`messages` 的顺序不能乱
- 细节三:参数 JSON 解析可能失败
- 六、排查技巧:怎么知道 Agent 哪里出了问题
- 方法一:打印每一步的消息列表
- 方法二:设置一个很小的 max_steps
- 方法三:统计工具调用次数
- 七、总结
一、问题背景:我的 Agent 跑了 200 次才停
刚学 Agent 的时候,我跟着教程写了一个能查天气的小助手。
代码很简单:
- 调用大模型;
- 如果大模型要调工具,就执行工具;
- 把工具结果返回给大模型。
跑起来后,我输入“北京天气怎么样”,然后就去倒水了。
回来一看,终端还在刷屏:
调用工具: get_weather({'city': '北京'}) 工具结果: 晴,25°C 调用工具: get_weather({'city': '北京'}) 工具结果: 晴,25°C 调用工具: get_weather({'city': '北京'}) 工具结果: 晴,25°C ...(重复 200 次)Agent 一直在调用同一个工具,传同一个参数,拿同一个结果,永远不结束。
我以为是模型的问题,换了几个模型都一样。后来才发现,问题出在我自己的代码里——我忘了一行最重要的代码。
这就是本文要讲的坑——Agent 的循环必须有人为设的上限,不能指望模型自己停下来。
二、最小复现代码:五步循环失控
先看错误版本:
importjsonfromopenaiimportOpenAI client=OpenAI()defget_weather(city:str)->str:data={"北京":"晴,25°C","上海":"多云,28°C"}returndata.get(city,f"查不到{city}")tools=[{"type":"function","function":{"name":"get_weather","description":"查天气",# 描述太模糊"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"],},},}]defrun_agent(user_input:str):messages=[{"role":"user","content":user_input}]whileTrue:# 问题在这里!没有循环上限response=client.chat.completions.create(model="gpt-4o-mini",messages=messages,tools=tools,)message=response.choices[0].messageifnotmessage.tool_calls:returnmessage.content messages.append(message)fortcinmessage.tool_calls:args=json.loads(tc.function.arguments)result=get_weather(args["city"])messages.append({"role":"tool","tool_call_id":tc.id,"content":result,})print(run_agent("北京天气怎么样?"))输出:
调用工具: get_weather({'city': '北京'}) 工具结果: 晴,25°C 调用工具: get_weather({'city': '北京'}) 工具结果: 晴,25°C ...(无限循环,直到你按 Ctrl+C)问题就在这一行:
whileTrue:# 没有退出条件三、原因分析:三个初学者常犯的错误
错误一:用 while True 而不是固定次数循环
whileTrue:# 危险:依赖模型自己停下来初学者容易觉得“模型应该知道自己什么时候完成”。实际上:
- 模型可能觉得“信息还不够”,继续调用工具;
- 模型可能重复调用同一个工具;
- 模型可能陷入某种推理循环。
永远不要用while True写 Agent 循环。
错误二:工具描述写得太模糊
"description":"查天气",# 太模糊模型不知道:
- 需要传什么参数?
- 参数是什么格式?
- 参数是必填还是选填?
模糊的描述会让模型反复试错,比如先试{"city": "北京"},再试{"location": "北京"},再试{"address": "北京"}——每次试错都是一次循环。
错误三:没有把模型的最终回答和工具调用区分清楚
ifnotmessage.tool_calls:returnmessage.content看起来没问题,但如果模型返回的content是空字符串(比如它只是想调用工具),这个判断会误判。
四、修复方案:三个必须加的保护
保护一:固定循环次数
defrun_agent(user_input:str,max_steps:int=5):# 加默认参数messages=[{"role":"user","content":user_input}]forstepinrange(max_steps):# 固定次数循环response=client.chat.completions.create(model="gpt-4o-mini",messages=messages,tools=tools,)message=response.choices[0].messageifnotmessage.tool_calls:returnmessage.content messages.append(message)fortcinmessage.tool_calls:args=json.loads(tc.function.arguments)result=get_weather(args["city"])messages.append({"role":"tool","tool_call_id":tc.id,"content":result,})returnf"超过最大步数{max_steps},任务未完成"# 超限返回提示关键点:for step in range(max_steps)保证最多循环 5 次,到点自动退出。
保护二:把工具描述写清楚
tools=[{"type":"function","function":{"name":"get_weather","description":"查询指定城市的天气。输入城市名称,返回该城市的天气信息。","parameters":{"type":"object","properties":{"city":{"type":"string","description":"城市名称,例如:北京、上海、深圳",}},"required":["city"],},},}]关键点:描述要包含三要素——做什么、传什么、格式是什么。
保护三:打印每一步,看清楚在发生什么
forstepinrange(max_steps):print(f"\n[第{step+1}步]")# ... 调用大模型 ...ifnotmessage.tool_calls:print(f"[完成]{message.content}")returnmessage.contentfortcinmessage.tool_calls:print(f"[工具]{tc.function.name}({tc.function.arguments})")# ... 执行工具 ...print(f"[结果]{result}")关键点:初学者一定要能看到每一步发生了什么,才能发现问题。
完整代码
importjsonfromopenaiimportOpenAI client=OpenAI()defget_weather(city:str)->str:data={"北京":"晴,25°C","上海":"多云,28°C"}returndata.get(city,f"查不到{city}")tools=[{"type":"function","function":{"name":"get_weather","description":"查询指定城市的天气。输入城市名称,返回该城市的天气信息。","parameters":{"type":"object","properties":{"city":{"type":"string","description":"城市名称,例如:北京、上海、深圳",}},"required":["city"],},},}]defrun_agent(user_input:str,max_steps:int=5):messages=[{"role":"user","content":user_input}]forstepinrange(max_steps):print(f"\n[第{step+1}步]")response=client.chat.completions.create(model="gpt-4o-mini",messages=messages,tools=tools,)message=response.choices[0].messageifnotmessage.tool_calls:print(f"[完成]{message.content}")returnmessage.content messages.append(message)fortcinmessage.tool_calls:args=json.loads(tc.function.arguments)print(f"[工具]{tc.function.name}({args})")result=get_weather(args["city"])print(f"[结果]{result}")messages.append({"role":"tool","tool_call_id":tc.id,"content":result,})print(f"[超限] 达到最大步数{max_steps}")returnf"超过最大步数{max_steps}"print(run_agent("北京天气怎么样?"))输出:
[第 1 步] [工具] get_weather({'city': '北京'}) [结果] 晴,25°C [第 2 步] [完成] 北京今天晴天,气温 25°C。五、初学者容易忽略的其他细节
细节一:tool_call_id必须对应
messages.append({"role":"tool","tool_call_id":tc.id,# 必须和上面工具的 id 一致"content":result,})如果tool_call_id写错了,大模型会报错或者不知道这是哪个工具的结果。
细节二:messages的顺序不能乱
正确的顺序是:
- 用户消息;
- 大模型的响应(包含工具调用);
- 工具结果。
如果顺序乱了,大模型会困惑。
细节三:参数 JSON 解析可能失败
args=json.loads(tc.function.arguments)模型偶尔会返回不合法的 JSON。初学者可以先打印看:
print(tc.function.arguments)# 看看实际返回什么args=json.loads(tc.function.arguments)六、排查技巧:怎么知道 Agent 哪里出了问题
方法一:打印每一步的消息列表
fori,msginenumerate(messages):print(f"[{i}]{msg}")看清楚大模型每一轮收到了什么、返回了什么。
方法二:设置一个很小的 max_steps
调试时把max_steps设为 2 或 3,快速看到问题,不用等 200 次循环。
方法三:统计工具调用次数
fromcollectionsimportCounter tool_counter=Counter()# 在工具调用处tool_counter[tc.function.name]+=1# 循环结束时打印print(f"工具调用统计:{dict(tool_counter)}")如果某个工具被调用了很多次,说明模型在反复试错。
七、总结
这次踩坑让我记住了一句话:
Agent 的循环必须有人为设的上限,不能指望模型自己停下来。
核心要点回顾:
- 不要用
while True写 Agent 循环,用for step in range(max_steps); max_steps设 5-10 就够了,超限要返回明确的提示;- 工具描述要写清楚:做什么、传什么、格式是什么;
- 每一步都打印出来,看清楚在发生什么;
tool_call_id必须和工具调用的 id 对应。
给初学者的三句话:
- 先把这 30 行代码跑通,比看十篇高级文章都有用;
- 出问题先打印每一步的消息列表,90% 的问题都能看出来;
- 不要急着用 LangChain,先用最基础的
openai库理解原理。
参考资料:
- OpenAI Function Calling 官方指南
- OpenAI Python SDK 快速开始