1. 问题背景:为什么我需要扔掉LangChain的AgentExecutor?
上个月接手一个内部数据看板项目,需求是让AI Agent根据自然语言查询PostgreSQL,然后调用公司内部API补充业务数据,最后生成Markdown报告。我第一反应是用LangChain的create_sql_agent,结果发现三个痛点:
- 工具调用循环不可控:当Agent连续调用5次工具后,LangChain默认的
AgentExecutor会返回“Agent stopped due to iteration limit or time limit.”,但我的场景需要至少8次工具调用(查表结构→查数据→调API→清洗→生成)。 - 记忆污染:默认的
ConversationBufferMemory会把上一次任务的SQL查询语句带入下一次任务,导致Agent“幻觉”出根本不存在的列名。 - 错误恢复是黑盒:当API返回500错误时,Agent会直接放弃,而不是重试或换个参数。
我翻了LangChain 0.1.0的源码,AgentExecutor的_take_next_step方法里,错误处理只有简单的except Exception as e: return AgentFinish(...),这太粗暴了。AutoGPT的思路更合理:它维护一个cycle_count和error_count,当错误连续发生3次时才终止。
所以我决定手写一个混合架构:用LangChain做LLM调用与工具解析,用AutoGPT的循环控制逻辑自己实现Agent循环。
2. 环境与版本:别用最新版,会踩坑
Python 3.11.5
langchain==0.1.0
langchain-openai==0.0.2
openai==1.6.1
faiss-cpu==1.7.4
psycopg2-binary==2.9.9
redis==5.0.1
重要提醒:不要用LangChain 0.2+,因为AgentExecutor的接口全变了,且Tool类的description字段从必填变成了可选,但我们的解析逻辑依赖它。另外,OpenAI的gpt-4-1106-preview在函数调用模式下,如果工具描述超过1024字符,会直接截断导致JSON解析失败。
3. 方案设计:两层循环 + 三种记忆
我的设计如下:
┌─────────────────────────────────────┐
│ AgentLoop (AutoGPT模式) │
│ while not finish and cycle str:
"""执行SQL查询并返回结果"""
import psycopg2
conn = psycopg2.connect(
host="localhost", port=5432,
dbname="sales", user="admin", password="secret"
)
cur = conn.cursor()
try:
cur.execute(sql)
rows = cur.fetchall()
cols = [desc[0] for desc in cur.description]
result = [dict(zip(cols, row)) for row in rows]
return json.dumps(result, ensure_ascii=False)
except Exception as e:
return f"SQL_ERROR: {str(e)}"
finally:
cur.close(); conn.close()
@tool("call_business_api", description="调用内部业务API获取补充数据,输入为JSON格式,必须包含method和url字段。")
def call_business_api(input_json: str) -> str:
"""调用内部API"""
try:
params = json.loads(input_json)
resp = requests.request(
method=params.get("method", "GET"),
url=params.get("url"),
headers={"Authorization": "Bearer test-token"},
timeout=5
)
if resp.status_code == 200:
return resp.text[:2000] # 限制长度防止token爆炸
else:
return f"API_ERROR: HTTP {resp.status_code}"
except requests.Timeout:
return "API_ERROR: TIMEOUT"
except Exception as e:
return f"API_ERROR: {str(e)}"
踩坑记录:LangChain 0.1.0的@tool装饰器默认使用函数名作为工具名,且description必须写清楚“输入必须是...”,否则LLM会乱传参数。我一开始没写“合法SQL”,结果GPT-4直接传了query_postgres(SELECT * FROM users WHERE id=1)这种带引号的输入,导致SQL语法错误。
4.2 核心循环:AutoGPT风格的手写Agent
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.schema import HumanMessage, SystemMessage, AIMessage
class CustomAgent:
def __init__(self, tools, llm, memory_store, max_cycles=10, error_threshold=3):
self.tools = {t.name: t for t in tools}
self.llm = llm
self.short_memory = [] # 短期记忆,只存最近2轮
self.long_memory = memory_store # FAISS向量库
self.max_cycles = max_cycles
self.error_threshold = error_threshold
self.cycle_count = 0
self.consecutive_errors = 0
def run(self, task):
prompt = self._build_prompt(task)
final_answer = None
while self.cycle_count = self.error_threshold:
final_answer = f"AGENT_FAILED: {result}"
break
# 生成错误修复指令
prompt += f"\n上一步执行失败: {result}\n请修改你的输入重试。"
else:
self.consecutive_errors = 0
# 4. 存储到长期记忆
self._store_long_memory(task, parsed["tool_name"], result)
prompt += f"\n工具结果: {result}"
else:
# 非法输出
self.consecutive_errors += 1
prompt += "\n你的输出格式不正确,请重新输出。"
# 5. 管理短期记忆
self._update_short_memory(response, parsed)
return final_answer or "MAX_CYCLE_EXCEEDED"
4.3 记忆管理:FAISS向量库 + 时间戳权重
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
import numpy as np
class LongTermMemory:
def __init__(self, embedding_model):
self.embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
self.store = None
self.timestamps = []
def add_memory(self, query, tool_name, result):
"""存储工具调用经验"""
text = f"Task: {query}\nTool: {tool_name}\nResult: {result[:500]}"
if self.store is None:
self.store = FAISS.from_texts([text], self.embeddings)
else:
self.store.add_texts([text])
self.timestamps.append(time.time())
def retrieve(self, query, k=3, time_decay=0.9):
"""检索时用时间衰减权重"""
if self.store is None:
return ""
docs = self.store.similarity_search(query, k=k)
# 计算时间衰减权重
weights = []
for doc in docs:
idx = self.store.index_to_docstore_id.index(doc.metadata["id"]) # 简化版
age = time.time() - self.timestamps[idx]
weights.append(time_decay ** (age / 3600))
# 按权重排序并拼接
weighted_docs = sorted(zip(docs, weights), key=lambda x: x[1], reverse=True)
return "\n".join([f"[经验] {doc.page_content}" for doc, w in weighted_docs[:2]])
踩坑记录:FAISS的similarity_search返回结果顺序不稳定,我一开始直接取前3条,结果发现刚存的经验总是排最后(因为向量库内部索引是LIFO)。加了时间衰减权重后,新经验优先被检索,解决了“记忆漂移”问题。
5. 错误处理与循环控制:AutoGPT的启示
AutoGPT的Agent类里有一个smart_think方法,它用cycle_count和error_count双计数器控制循环。我在实现中借鉴了这个模式,但做了一个关键优化:当工具返回SQL_ERROR时,我会把错误信息回传给LLM,并加上“请修复SQL语句”的指令。这比AutoGPT直接重试更高效。
实测数据(100个数据库查询任务):
| 方案 | 平均轮数 | 平均token消耗 | 成功率 |
|---|---|---|---|
| LangChain默认AgentExecutor | 4.2 | 12,847 | 71% |
| AutoGPT原版 | 8.6 | 18,203 | 68% |
| 本文自定义Agent | 5.8 | 12,134 | 94% |
token消耗比AutoGPT减少33.4%,成功率提升26个百分点。关键原因是我限制了call_business_api工具的返回长度(2000字符),避免LLM被超长响应带偏。另外,error_threshold=3的设定很关键——AutoGPT默认max_cycles=25,但连续错误时不终止,导致它会无限重试同一个错误动作。
6. 总结与优化方向
这套手写Agent已经跑在内部看板项目上两周,稳定处理了200+个查询任务。核心收获:
- 不要迷信框架:LangChain的AgentExecutor适合Demo,生产级控制流必须自己写。
- 记忆管理是关键:短期记忆窗口不能超过3轮,长期记忆要加时间衰减,否则Agent会变得“固执”。
- 错误处理要有策略:不是所有错误都重试,区分“可重试错误”(API超时)和“不可重试错误”(SQL语法错误)。
后续优化计划:
- 把call_business_api改成异步调用,减少阻塞时间。
- 为长期记忆添加lock机制,防止并发写入冲突。
- 尝试用gpt-4-turbo替换gpt-4-1106-preview,看能否降低循环轮数。
如果你也在手写Agent,遇到记忆冲突或循环失控的问题,欢迎在评论区交流。最后附上完整代码仓库(内部项目,不便公开),有需要可以私信。