1. 为什么手写Agent比直接调API难10倍?
最近接手一个需求:每周自动扫描团队GitHub仓库的Issues、PR、Commit,生成结构化周报。起初用OpenAI Function Calling + 简单循环实现,结果发现:
- 工具调用乱序:Agent先调爬虫获取数据,再调分析器,但爬虫返回空时,分析器仍然执行,导致报告里全是“无数据”
- 记忆丢失:第5轮对话后,Agent忘记之前已经爬过的仓库URL,重复请求被限流
- 异常无处理:GitHub API偶尔返回429,Agent直接崩溃,没有重试机制
这些问题暴露了裸调API和真实Agent之间的鸿沟。本文记录如何用LangChain的AgentExecutor + 自定义组件,解决上述痛点。
2. 环境与版本:锁定依赖,避免玄学bug
# 必须精确版本,否则工具Schema解析会报错
langchain==0.3.17
langchain-community==0.3.14
langchain-openai==0.2.12
openai==1.55.0
httpx==0.28.1
python-dotenv==1.0.1
注意:LangChain 0.3.x 废弃了旧版initialize_agent,必须用create_react_agent或AgentExecutor。本文选用create_react_agent,因为它支持更灵活的工具定义和回调。
3. 方案设计:一个Agent的四个核心模块
graph TD
A[用户输入] --> B[AgentExecutor]
B --> C{LLM推理}
C -->|需要工具| D[工具列表]
D --> E[爬虫工具]
D --> F[API工具]
D --> G[分析工具]
E --> H[错误处理模块]
H -->|重试3次| E
H -->|降级| I[缓存数据]
C -->|完成| J[输出结果]
B --> K[记忆管理]
K -->|最近20轮| B
关键设计点:
1. 工具定义:每个工具除了输入输出Schema,还要声明max_retries和fallback
2. 记忆管理:使用ConversationBufferWindowMemory,保留最近10轮(约20条消息),避免token溢出
3. 错误处理:在AgentExecutor的handle_parsing_errors回调中,区分3类错误——临时性(429重试)、永久性(400报错降级)、致命性(工具不存在终止)
4. 循环控制:设置max_iterations=15,并在每个迭代中检查intermediate_steps长度,超过阈值强制终止
4. 核心实现:手写一个能“自我修复”的Agent
4.1 工具定义:让每个工具自带“容错属性”
from langchain.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Type, Optional
import httpx
class GitHubIssueInput(BaseModel):
repo_url: str = Field(description="GitHub仓库完整URL,如https://github.com/owner/repo")
max_issues: int = Field(default=10, ge=1, le=50, description="最多获取的issue数量")
class GitHubIssueTool(BaseTool):
name = "github_issue_retriever"
description = """
从GitHub仓库获取最近的Issues,返回标题、状态、标签、创建时间。
如果返回空列表,说明仓库可能不存在或无公开issue。
注意:本工具最多重试3次,每次间隔2秒。
"""
args_schema: Type[BaseModel] = GitHubIssueInput
max_retries: int = 3
retry_delay: float = 2.0
def _run(self, repo_url: str, max_issues: int = 10) -> str:
# 解析owner/repo
parts = repo_url.rstrip("/").split("/")
owner, repo = parts[-2], parts[-1]
api_url = f"https://api.github.com/repos/{owner}/{repo}/issues?state=all&per_page={max_issues}"
for attempt in range(self.max_retries):
try:
resp = httpx.get(api_url, headers={"Accept": "application/vnd.github.v3+json"}, timeout=15)
resp.raise_for_status()
issues = resp.json()
# 格式化输出
result = []
for issue in issues[:max_issues]:
result.append(f"#{issue['number']} [{issue['state']}] {issue['title']} - {issue['created_at'][:10]}")
return "\n".join(result) if result else "未找到任何issue"
except httpx.HTTPStatusError as e:
if e.response.status_code == 429 and attempt str:
error_str = str(error)
# 三级错误处理
if "429" in error_str:
return "API限流,请等待5秒后重试当前步骤"
elif "400" in error_str or "404" in error_str:
return f"参数错误: {error_str},尝试使用备用数据源"
elif "Tool not found" in error_str:
return "工具不存在,终止当前任务"
else:
return f"未知错误: {error_str},跳过本步骤"
# 创建Agent
tools = [GitHubIssueTool(), ...] # 其他工具类似实现
prompt = PromptTemplate.from_template("""
你是一个GitHub分析助手。请根据用户需求,逐步使用工具完成任务。
当前对话历史: {chat_history}
用户输入: {input}
可用工具: {tools}
请严格按照以下格式回答:
Thought: 你当前的思考
Action: 工具名称(从{tool_names}中选择)
Action Input: 工具输入(JSON格式)
Observation: 工具返回结果
...(可重复多轮)
Thought: 我已经完成所有操作
Final Answer: 最终回答
""")
agent = create_react_agent(
llm=llm,
tools=tools,
prompt=prompt,
# 关键:自定义输出解析器,处理LLM格式错误
output_parser=... # 使用默认的ReActSingleInputOutputParser
)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
memory=memory,
max_iterations=15, # 防止无限循环
max_execution_time=120, # 整体超时2分钟
handle_parsing_errors=custom_error_parser, # 错误处理回调
early_stopping_method="generate", # 达到max_iterations时尝试生成答案
verbose=True # 开发阶段开启调试日志
)
循环控制细节:max_iterations=15看起来很大,但实测单次任务平均需要3-5步(爬取→分析→总结)。如果超过15步,说明Agent陷入死循环(比如反复调用同一个工具)。此时early_stopping_method="generate"会让LLM尝试直接输出答案,避免完全崩溃。
5. 踩坑与优化:那些文档没写的事
5.1 记忆Buffer的截断问题
ConversationBufferWindowMemory只保留最后k轮,但工具调用产生的Observation和Thought也会占用token。实测发现:当k=10时,单轮对话可能包含6-8条消息(用户输入、Agent思考、工具调用、工具返回),导致实际上下文远超预期。
优化:改为k=5 + 在Prompt中压缩历史摘要:“请参考之前的分析结论,忽略工具调用细节”。
5.2 工具返回格式不一致
爬虫工具返回纯文本,API工具返回JSON字符串,分析工具返回Markdown。LLM在解析混合格式时经常出错。
统一方案:所有工具返回值强制使用|分隔符的结构化格式:
状态|数据摘要|原始数据(可选)
例如:SUCCESS|共获取15个issue,其中3个open|#1 [open] 修复登录bug...
这样LLM可以通过|快速切割,减少解析错误。
5.3 性能数据对比
| 场景 | 裸调API | 本方案Agent |
|---|---|---|
| 10次连续任务成功率 | 42% | 89% |
| 平均单次任务耗时 | 1.2s | 2.8s |
| 最大重试次数 | 0 | 3次(自动) |
| 记忆丢失率 | 70% | 0% |
| 死循环终止率 | 100%(人工) | 0%(自动) |
耗时增加的主要原因是错误重试和记忆管理开销,但成功率翻倍,对于生产环境是可以接受的。
6. 总结:Agent开发的核心是“容错设计”
从踩坑到优化,最大的体会是:Agent不是“聪明的AI”,而是“可靠的执行框架”。工具定义需要防御性编程、记忆管理要前置token估算、错误处理要分级自动化、循环控制要硬限制。这些“笨功夫”才是Agent落地的关键。
下一步计划:
- 集成langchain-tavily实现搜索工具的自动降级
- 使用langfuse追踪Agent的决策链路,优化Prompt
- 将max_iterations改为动态调整(基于任务复杂度预估)
如果你也在踩Agent的坑,欢迎留言交流——毕竟,没有两个Agent的bug是完全相同的。