一、问题背景:为什么我不直接用现成的AgentExecutor?
我上一篇文章介绍过用langchain.agents快速搭一个带搜索引擎的Agent。但生产环境跑了两周,遇到两个致命问题:
- 记忆无限膨胀:默认的
ConversationBufferMemory会把所有历史消息塞进Prompt,单轮对话超过5个工具调用后,Token直接冲破8K,报ContextWindowExceededError。 - 错误处理黑洞:当我的自定义工具(比如查MySQL)因为网络抖动抛出
TimeoutException时,AgentExecutor默认行为是直接终止会话,而不是重试或换一个工具。
AutoGPT的思想给了我启发:Agent循环应该像一个状态机,需要显式控制思考→行动→观察的循环,并且每一步都要有失败回退。所以这次我决定手写一个极简版Agent循环,只依赖LangChain的BaseTool和模型封装,不依赖AgentExecutor。
二、环境与版本
- Python 3.10.13
- langchain 0.1.0(注意:0.2.x API有变动,请锁定版本)
- langchain-openai 0.0.8
- openai 1.12.0
- 模型:
gpt-4-0125-preview(temperature=0.2,避免过度发散)
不要用langchain-experimental,里面那个AutoGPT类太重,调试起来想砸键盘。
三、方案设计:一个最小可运行的Agent循环
我的设计参考了AutoGPT的Agent类,但砍掉了文件操作和网页浏览,只保留核心骨架:
while steps str:
# 安全计算,只允许数字和运算符
allowed = set("0123456789+-*/(). ")
if not set(expression).issubset(allowed):
raise ValueError("非法字符")
return str(eval(expression)) # 生产环境请用ast.literal_eval
class WeatherTool(BaseTool):
name = "weather"
description = "查询城市天气。输入城市名拼音。"
args_schema = CalculatorInput # 复用结构,实际应为WeatherInput
def _run(self, city: str) -> str:
import random
if random.random() = max_retries:
result = f"重试{max_retries}次后失败: {e}"
else:
logging.warning(f"工具 {action} 超时,第{retries}次重试")
time.sleep(1 * retries) # 指数退避
except Exception as e:
result = f"工具执行出错: {e}, 请检查输入格式"
break
# 4. 更新记忆
memory.add_assistant(f"[工具观察] {action}返回: {result}")
logging.info(f"第{steps+1}步: action={action}, result={result[:50]}...")
steps += 1
return "达到最大步数,强制结束。"
关键细节:
- max_steps=6,限制死循环。AutoGPT默认100步,对于一般任务太浪费。
- 重试只针对TimeoutError,其他异常(比如计算器输入非法)直接返回错误字符串,让LLM自己纠正。
- 每步之后,我把工具观察结果作为一条assistant消息加入记忆,但不加user消息,防止LLM认为用户在重复提问。
六、踩坑与优化:三个血泪教训
坑1:JSON解析失败。GPT-4偶尔会输出带Markdown代码块的JSON,或者多一个逗号。我用了两重保险:strip()去掉首尾空白,再替换掉``json标签。但还是有5%的概率失败,所以我加了一个continue`,让LLM重新生成。
坑2:记忆窗口太大会让模型忽略工具观察。最初我用窗口大小=10,发现第3步之后模型开始“忘”了之前工具返回的结果。分析原因:窗口满了之后,早期工具观察被挤出。改成window_size=5,且每次只取最后4条消息(留一条给当前输出),效果显著提升。
坑3:tool.run() vs tool._run()。LangChain的run()方法内部会捕获异常并返回一个AgentAction错误对象,这导致我无法判断到底是超时还是非法输入。后来查源码发现_run()才是原始方法,异常会直接抛出。所有需要自定义错误处理的场景,请绕过run()。
优化后的效果:在30个混合任务测试集上(包含数学计算、天气查询、多步推理),平均每次任务调用LLM 4.2次,Token消耗约5.2K(对比AutoGPT官方实现8.3K)。错误恢复率(第一次工具失败但最终完成任务)为92%,未出现无限循环或上下文溢出。
七、总结与后续改进
这个手写循环虽然简陋,但胜在透明、可控、可插拔。如果你想加一个新的工具,只需要继承BaseTool并实现_run()即可,不需要改循环逻辑。目前记忆还是基于消息窗口,下一步我计划引入向量数据库做长期记忆,让Agent记住跨会话的工具使用偏好。
最终再强调一次:不要迷信AutoGPT的复杂实现,核心循环只有几十行代码。理解了这个循环,你就能在LangChain之上构建任何Agent。
代码已上传至GitHub(仓库名:mini-agent-loop),欢迎Star。有问题评论区见。