← 返回首页

LangChain Agent踩坑记:从Demo到生产的血泪教训

2023-03-28 | LangChain Agent LLM
LangChain的Agent在Demo里跑得很顺,但上线后问题一堆:无限循环、工具选错、输出解析失败、响应超时...这篇文章记录我把一个Agent从Demo打磨到生产可用的全过程。

项目背景

2023年3月,我们要做一个"智能数据分析助手",用户用自然语言提问,Agent自动选择工具(查数据库、画图表、做计算)来回答。听起来很美好,现实很骨感。

第一版:天真的Demo

from langchain.agents import Tool, initialize_agent, AgentType
from langchain.chat_models import ChatOpenAI

# 定义工具
tools = [
    Tool(
        name="SQL查询",
        func=run_sql,
        description="用于查询数据库"
    ),
    Tool(
        name="画图",
        func=plot_chart,
        description="用于生成图表"
    ),
    Tool(
        name="计算器",
        func=calculator,
        description="用于数学计算"
    )
]

llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True
)

# 测试
agent.run("帮我查一下上个月的销售额是多少")

Demo跑起来了!然后我信心满满地上线了...

问题1:无限循环

现象

Agent陷入循环,反复调用同一个工具,直到token耗尽或超时。

# 用户问题: "上个月销售额比上上个月多多少?"

# Agent的思考过程:
# Thought: 我需要查询上个月销售额
# Action: SQL查询
# Observation: 上个月销售额是100万

# Thought: 我还需要查询上上个月销售额  
# Action: SQL查询
# Observation: 上上个月销售额是80万

# Thought: 我需要计算差值
# Action: SQL查询  <- 又选了SQL!
# ... 无限循环

原因

Tool的description写得太模糊,模型分不清什么时候该用SQL,什么时候该用计算器。

解决方案

# 优化后的Tool定义
tools = [
    Tool(
        name="SQL查询",
        func=run_sql,
        description="""用于从数据库查询原始数据。
输入应该是一个具体的数据查询需求,如"查询2023年3月的销售额"。
注意:这个工具只能查询数据,不能做计算。如果需要计算(如求和、比较、百分比),请使用计算器工具。"""
    ),
    Tool(
        name="计算器",
        func=calculator,
        description="""用于数学计算,如加减乘除、百分比、比较大小。
输入应该是一个数学表达式,如"100-80"或"(100-80)/80*100"。
注意:这个工具需要具体的数字,如果数字未知,请先用SQL查询工具获取。"""
    ),
]

# 加上最大迭代次数
agent = initialize_agent(
    tools, llm,
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    max_iterations=5,  # 最多5次工具调用
    early_stopping_method="generate"  # 超过次数时生成最终答案
)
⚠️ 核心教训: Tool的description是Agent的"说明书",必须写清楚:
1. 这个工具能做什么
2. 输入应该是什么格式
3. 什么情况下不该用这个工具

问题2:输出解析失败

现象

# 报错
OutputParserException: Could not parse LLM output: 
"我已经查询到数据了,上个月销售额是100万元。"

原因

ReAct Agent期望LLM输出固定格式(Thought/Action/Observation),但GPT有时候会"忘记"格式,直接输出答案。

解决方案

from langchain.agents import AgentOutputParser
from langchain.schema import AgentAction, AgentFinish
import re

class RobustOutputParser(AgentOutputParser):
    """容错性更强的输出解析器"""
    
    def parse(self, text: str):
        # 尝试标准解析
        action_match = re.search(
            r"Action:\s*(.+?)\nAction Input:\s*(.+)", 
            text, 
            re.DOTALL
        )
        
        if action_match:
            action = action_match.group(1).strip()
            action_input = action_match.group(2).strip()
            return AgentAction(action, action_input, text)
        
        # 检查是否是最终答案
        if "Final Answer:" in text:
            answer = text.split("Final Answer:")[-1].strip()
            return AgentFinish({"output": answer}, text)
        
        # 兜底:如果LLM直接输出答案,当作Final Answer处理
        if not any(keyword in text for keyword in ["Action:", "Thought:"]):
            return AgentFinish({"output": text.strip()}, text)
        
        # 实在解析不了,抛异常
        raise OutputParserException(f"Could not parse: {text}")

# 使用自定义解析器
from langchain.agents import LLMSingleActionAgent

agent = LLMSingleActionAgent(
    llm_chain=llm_chain,
    output_parser=RobustOutputParser(),
    stop=["\nObservation:"],
    allowed_tools=tool_names
)

问题3:工具调用失败处理

现象

SQL执行报错时,Agent不知道怎么办,要么卡死,要么瞎编。

解决方案

def safe_tool_wrapper(func, tool_name: str):
    """包装工具函数,处理异常"""
    def wrapped(input_str: str) -> str:
        try:
            result = func(input_str)
            return str(result)
        except Exception as e:
            error_msg = f"工具'{tool_name}'执行失败: {str(e)}"
            # 返回错误信息,让Agent知道发生了什么
            return f"[错误] {error_msg}。请检查输入或尝试其他方法。"
    return wrapped

# 使用
tools = [
    Tool(
        name="SQL查询",
        func=safe_tool_wrapper(run_sql, "SQL查询"),
        description="..."
    )
]

问题4:响应太慢

现象

一个简单问题,Agent要思考10秒+,用户等不了。

分析

阶段 耗时
第1次LLM调用(决定用什么工具) 2.5s
工具执行(SQL查询) 0.5s
第2次LLM调用(处理结果) 2.5s
第3次LLM调用(生成答案) 2.5s
总计 8s+

优化方案

# 方案1:用更快的模型做规划
from langchain.chat_models import ChatOpenAI

fast_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
smart_llm = ChatOpenAI(model="gpt-4", temperature=0)

# 用3.5做工具选择(快),用4做最终回答(准)
planning_agent = initialize_agent(tools, fast_llm, ...)

# 方案2:简单问题直接回答,不走Agent
def route_query(query: str):
    """判断是否需要Agent"""
    simple_patterns = ["你好", "谢谢", "什么是"]
    if any(p in query for p in simple_patterns):
        return llm.predict(query)  # 直接回答
    return agent.run(query)  # 走Agent

# 方案3:流式输出,边思考边展示
async def stream_agent_response(query: str):
    """流式输出Agent的思考过程"""
    async for event in agent.astream_events(query, version="v1"):
        kind = event["event"]
        if kind == "on_llm_stream":
            yield event["data"]["chunk"].content
        elif kind == "on_tool_end":
            yield f"\n[工具结果: {event['data']['output'][:100]}...]\n"

问题5:上下文丢失

现象

多轮对话中,Agent会忘记之前说过的话。

# 用户: 查一下北京的销售额
# Agent: 北京销售额是500万

# 用户: 那上海呢?
# Agent: 请问您想查询什么?  <- 忘了上下文!

解决方案

from langchain.memory import ConversationBufferWindowMemory

# 使用带记忆的Agent
memory = ConversationBufferWindowMemory(
    memory_key="chat_history",
    k=5,  # 保留最近5轮对话
    return_messages=True
)

agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
    memory=memory,
    verbose=True
)

# 或者手动管理上下文
class ConversationAgent:
    def __init__(self):
        self.history = []
        self.agent = create_agent()
    
    def chat(self, user_input: str) -> str:
        # 构建带上下文的输入
        context = "\n".join([
            f"用户: {h['user']}\n助手: {h['assistant']}" 
            for h in self.history[-3:]
        ])
        
        full_input = f"对话历史:\n{context}\n\n当前问题: {user_input}"
        
        response = self.agent.run(full_input)
        
        self.history.append({
            "user": user_input,
            "assistant": response
        })
        
        return response

最终架构

from typing import Optional
import asyncio

class ProductionAgent:
    """生产级Agent"""
    
    def __init__(self):
        self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
        self.tools = self._create_tools()
        self.agent = self._create_agent()
        self.history = []
    
    def _create_tools(self):
        return [
            Tool(
                name="数据查询",
                func=safe_tool_wrapper(self.query_data, "数据查询"),
                description="""查询业务数据。
输入格式: {"metric": "销售额", "dimension": "城市", "time": "上个月"}
只能查询,不能计算。"""
            ),
            Tool(
                name="数据计算",
                func=safe_tool_wrapper(self.calculate, "数据计算"),
                description="""对数字进行计算。
输入格式: 数学表达式,如 "100-80" 或 "(100-80)/80*100"
需要先用数据查询获取数字。"""
            ),
        ]
    
    def _create_agent(self):
        return initialize_agent(
            self.tools,
            self.llm,
            agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
            max_iterations=5,
            early_stopping_method="generate",
            handle_parsing_errors=True,  # 自动处理解析错误
            verbose=True
        )
    
    async def chat(self, user_input: str, timeout: int = 30) -> str:
        """带超时的对话"""
        try:
            response = await asyncio.wait_for(
                self._run_agent(user_input),
                timeout=timeout
            )
            return response
        except asyncio.TimeoutError:
            return "抱歉,处理超时,请稍后重试。"
        except Exception as e:
            return f"抱歉,出现错误: {str(e)}"
    
    async def _run_agent(self, user_input: str) -> str:
        # 简单问题不走Agent
        if self._is_simple_query(user_input):
            return await self.llm.apredict(user_input)
        
        # 构建带历史的输入
        full_input = self._build_input_with_history(user_input)
        
        # 执行Agent
        response = await self.agent.arun(full_input)
        
        # 更新历史
        self.history.append({"user": user_input, "assistant": response})
        
        return response

性能对比

指标 初版 优化后
平均响应时间 12s 4s
成功率 65% 92%
无限循环率 15% 0.5%
解析失败率 20% 2%

经验总结

✅ 关键要点:
  1. Tool描述要详细:告诉LLM什么时候用、怎么用、什么时候不用
  2. 限制迭代次数:防止无限循环,5次通常够了
  3. 处理解析失败:LLM输出不可控,要有容错
  4. 工具要有错误处理:失败时返回有意义的错误信息
  5. 设置超时:Agent可能卡住,必须有超时机制

LangChain的问题

说实话,LangChain在生产环境用起来挺痛苦的:

后来我们逐渐把核心逻辑迁移到了自己的实现,只保留LangChain的一些工具类。

更新记录:
2023-03-28: 初版发布
2023-08-15: 更新LangChain 0.0.300兼容性
2024-01-10: 补充自定义Agent实现