基于LangChain构建智能体:从核心原理到实战避坑指南

📅 2026/8/12 15:29:19
基于LangChain构建智能体:从核心原理到实战避坑指南
1. 从“玩具”到“生产力”为什么我们需要Agent如果你最近在折腾大语言模型大概率已经玩过各种聊天界面也试过用LangChain搭个简单的问答应用。一开始你会觉得LLM很神奇能写诗、能编程、能回答各种问题。但玩久了一个核心痛点就会浮现出来它好像什么都懂一点但又什么都做不了。你问它“帮我查一下今天上海的天气”它能给你编一段像模像样的天气报告你让它“把这份PDF总结一下发到我的Notion”它只能告诉你“我理解你的需求但我无法直接操作你的文件或访问外部应用”。这就是LLM的“大脑”与“手脚”脱节的问题。它有一个强大的、能理解自然语言、能推理规划的“大脑”但它没有感知世界、执行具体任务的“手脚”。Agent智能体技术就是为了解决这个问题而生的。它不是一个新概念但在LLM时代被赋予了全新的生命力。简单说Agent就是一个由LLM作为“决策大脑”驱动一系列“工具”Tools作为“执行手脚”的智能系统。大脑负责理解你的意图、拆解任务、制定计划、做出决策手脚则负责调用具体的API、查询数据库、操作软件把大脑的想法变成现实。这听起来有点像我们熟悉的“自动化脚本”或“RPA机器人”但底层逻辑完全不同。传统自动化是“if-else”的确定式流程而Agent是“思考-行动-观察-再思考”的自主闭环。LLM赋予了它理解模糊指令、处理异常、动态调整计划的能力。比如你给一个传统脚本下达指令“总结A文档并发邮件”如果A文档不存在脚本就卡死了。但一个Agent遇到这种情况它的LLM大脑可能会判断“用户要的是A文档的总结但A找不到。我发现有个名字相似的B文档是否需要确认后总结B或者直接告诉用户A缺失” 这种基于理解的弹性是Agent的核心价值。所以打造第一个Agent绝不仅仅是调用几个API。它是你从“用LLM聊天”迈向“用LLM解决实际问题”的关键一步。接下来我会带你从零开始用LangChain搭建一个能真正“干活”的Agent并深入每一个环节告诉你为什么这么设计以及如何避开我踩过的那些坑。2. 搭建基石理解LangChain中的核心组件在动手写代码之前我们必须把LangChain里关于Agent的几个核心概念掰扯清楚。很多人一上来就照抄官方示例结果连自己调用的每个对象是干什么的都不知道出了问题自然无从下手。2.1 LLM不只是聊天模型在Agent的语境里LLM是你的“大脑”它的核心职责是推理和规划。这意味着你选择的LLM必须有较强的逻辑思维和指令遵循能力。对于入门和实验OpenAI的GPT-3.5-turbo或GPT-4是稳妥的选择它们对LangChain的Agent格式有很好的支持。但这里有个关键点不要用纯聊天模型。很多国产或开源的模型虽然在闲聊上表现不错但在严格遵循特定输出格式比如要求它必须输出Action:和Action Input:方面可能不稳定。Agent要求LLM严格按照预设的模板进行思考输出格式错误会导致整个流程解析失败。因此在初始化LLM时我强烈建议将temperature参数设置为0或一个较低的值如0.1以减少输出的随机性确保指令遵循的稳定性。from langchain_openai import ChatOpenAI # 初始化一个适合Agent工作的LLM llm ChatOpenAI( modelgpt-3.5-turbo, temperature0, # 降低随机性让输出更稳定、更可预测 api_keyyour-api-key )2.2 Tools给Agent装上“手脚”Tools是Agent与外部世界交互的接口。一个Tool本质上就是一个函数它有着明确的名称、描述和输入参数。LLM大脑通过阅读Tool的描述来决定在什么时候调用它。起名和写描述是门艺术也是第一个大坑。名称name要简短、具象。比如search_internet就比tool_1好得多。LLM会根据名称来记忆和选择工具。描述description这是最重要的部分描述必须清晰、无歧义地说明三件事1这个工具是干什么的2它需要什么输入参数3它会返回什么。描述是LLM决定是否调用以及如何调用该工具的唯一依据。模糊的描述会导致LLM错误调用或不敢调用。一个反面教材的描述“这是一个有用的工具”。这等于什么都没说。 一个正面的描述“在互联网上搜索信息。输入应该是一个搜索查询字符串。返回的是最相关的几条网页摘要。”LangChain社区提供了大量预置工具langchain_community.tools如WikipediaQueryRun、DuckDuckGoSearchRun等。但对于我们自己的业务通常需要自定义工具。from langchain.tools import tool from typing import Optional tool def get_weather(city: str, date: Optional[str] None) - str: 获取指定城市的天气信息。 Args: city: 城市名称例如“上海”、“北京”。 date: 可选查询日期格式为‘YYYY-MM-DD’。默认为今天。 Returns: 返回该城市的天气情况描述字符串。 # 这里应该是调用真实天气API的代码 # 例如response requests.get(fhttps://weather.api?city{city}date{date}) # 为了演示我们返回一个模拟结果 if date: return f{city}在{date}的天气是晴朗25摄氏度。 else: return f{city}今天天气多云转晴22-28摄氏度。注意在实际开发中工具函数内部一定要做好异常处理try-except并返回明确的错误信息给Agent。比如return 调用天气API失败请检查网络或城市名。。这样LLM才能根据错误信息决定下一步动作如重试或向用户报告。2.3 AgentExecutor运行引擎与安全阀这是把大脑LLM和手脚Tools组装起来并让它们安全、可控运行的核心组件。你可以把它想象成Agent的“躯干”和“神经系统”。组装AgentExecutor将Agent包含LLM和工具定义和Tools列表绑定在一起。运行循环它负责执行“思考-行动-观察”的循环。LLM输出一个Action要调用哪个工具和Action Input调用参数AgentExecutor就去找对应的工具执行得到Observation工具返回的结果再把Observation连同之前的对话历史一起喂回给LLM让它进行下一轮思考。安全控制这是AgentExecutor最关键的价值。没有它Agent可能会陷入死循环或执行危险操作。主要参数有max_iterations: 最大迭代次数。必须设置我一般设为10-15防止Agent在一个简单问题上无限思考下去。early_stopping_method: 提前停止方法。“force”表示在达到最大迭代次数后强制停止“generate”则让LLM自己决定何时生成最终答案。handle_parsing_errors: 处理解析错误。当LLM的输出不符合Action/Action Input格式时是否进行重试或报错。设置为True可以增加鲁棒性。from langchain.agents import create_react_agent, AgentExecutor from langchain import hub # 从LangChain Hub拉取一个预设的Agent提示词模板例如ReAct模板 prompt hub.pull(hwchase17/react) # 创建Agent agent create_react_agent(llm, tools[get_weather], promptprompt) # 创建执行引擎并设置安全限制 agent_executor AgentExecutor( agentagent, tools[get_weather], verboseTrue, # 打印详细的执行过程调试时非常有用 handle_parsing_errorsTrue, # 优雅处理格式错误 max_iterations5, # 防止简单问题复杂化 early_stopping_methodforce )3. 选择你的“大脑”运行模式AgentType深度解析LangChain提供了多种预定义的Agent类型AgentType它们本质上是不同的“思维框架”或“提示词模板”。选对类型事半功倍选错类型事倍功半。很多人卡在第一步就是因为没搞懂这个。3.1 入门首选ZERO_SHOT_REACT_DESCRIPTION这是最常用、最通用的Agent类型基于ReActReasoning Acting框架。它的工作流非常符合直觉Thought思考LLM分析当前情况用户问题、已有信息、可用工具。Action行动决定调用哪个工具。Action Input行动输入提供调用工具所需的参数。Observation观察获得工具返回的结果。回到第1步直到LLM认为可以给出最终答案Final Answer。它的提示词模板会明确告诉LLM这个流程并列出所有可用工具及其描述。它的优点是直接、灵活适合大多数需要多步推理和工具调用的场景。比如“查一下上海今天的天气如果下雨就推荐一个室内活动并告诉我怎么去”。from langchain.agents import initialize_agent, AgentType agent initialize_agent( tools, llm, agentAgentType.ZERO_SHOT_REACT_DESCRIPTION, # 指定Agent类型 verboseTrue, handle_parsing_errorsTrue, max_iterations5 )3.2 结构化任务专家STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION这个类型是ZERO_SHOT_REACT_DESCRIPTION的升级版特别适合工具输入参数比较复杂的场景。普通Agent要求工具输入是一个简单的字符串但有些工具需要多个参数或者参数是结构化数据如列表、字典。STRUCTURED_CHAT允许LLM输出一个结构化的Action Input比如JSON格式从而更精准地调用复杂工具。如果你的工具函数需要多个参数或者你希望LLM能更规范地提供输入就选这个。# 假设我们有一个需要多个参数的工具 tool def book_flight(departure_city: str, arrival_city: str, date: str, passengers: int) - str: 预订航班。需要出发城市、到达城市、日期和乘客人数。 return f已为您搜索{date}从{departure_city}到{arrival_city}的航班共{passengers}位乘客。 # 使用STRUCTURED_CHAT类型LLM能更好地处理多参数输入 agent initialize_agent( tools[book_flight], llmllm, agentAgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION, verboseTrue )3.3 开放式对话与工具调用OPENAI_FUNCTIONS / OPENAI_TOOLS这是为OpenAI模型量身定制的类型。它利用了OpenAI API原生的“函数调用”Function Calling能力。在这种模式下你在请求LLM时不仅发送消息还会附带一个“工具列表”即函数定义。LLM会在内部判断是否需要调用函数并以一个特殊的JSON格式响应来指定要调用的函数和参数。它的最大优点是稳定性和格式正确率极高因为函数调用是模型训练的一部分。缺点则是主要绑定OpenAI模型。# 使用OpenAI专用的Agent类型 agent initialize_agent( tools, llm, agentAgentType.OPENAI_FUNCTIONS, # 或 AgentType.OPENAI_TOOLS (更新) verboseTrue )如何选择刚入门工具简单用ZERO_SHOT_REACT_DESCRIPTION。工具参数复杂或需要稳定结构化输入用STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION。使用OpenAI模型追求最稳定的工具调用用OPENAI_FUNCTIONS或OPENAI_TOOLS。4. 实战构建一个能查天气、搜网页、做计算的“全能小助手”理论说再多不如动手跑一遍。我们来构建一个具备三种能力的Agent查询天气、搜索网络信息、进行数学计算。这个例子麻雀虽小五脏俱全涵盖了自定义工具、多工具协作、以及执行流程的完整观察。4.1 步骤一准备三个核心工具首先我们定义好三个工具函数。注意看它们的描述是如何写的。from langchain.tools import tool from langchain_community.tools import DuckDuckGoSearchRun from langchain_community.utilities import WikipediaAPIWrapper import math # 工具1自定义天气工具模拟 tool def get_weather(city: str) - str: 获取指定城市的当前天气情况。输入必须是一个明确的城市名称。 Args: city: 城市名例如“北京”、“New York”。 Returns: 返回该城市的天气描述字符串。 # 模拟数据真实情况应调用API weather_db { 北京: 北京晴15-25摄氏度西北风2级。, 上海: 上海多云18-28摄氏度东南风1级。, 纽约: 纽约小雨10-15摄氏度东北风3级。 } return weather_db.get(city, f未找到{city}的天气信息。) # 工具2网络搜索工具使用社区工具 search DuckDuckGoSearchRun() # 工具3自定义计算器工具 tool def calculator(expression: str) - str: 执行一个基础数学计算或单位转换。支持加减乘除(, -, *, /)、乘方(**)、括号。 也可以处理简单的单位转换如‘100公里 to 英里’。 Args: expression: 数学表达式或转换语句例如‘(35)*2’ ‘100公里 to 英里’。 Returns: 计算或转换结果字符串。 try: # 处理单位转换的简单逻辑 if to in expression or TO in expression: parts expression.lower().split(to) if len(parts) 2: value_unit parts[0].strip().split() if len(value_unit) 2: value, from_unit float(value_unit[0]), value_unit[1] to_unit parts[1].strip() # 简单模拟几种转换 if from_unit 公里 and to_unit 英里: result value * 0.621371 return f{value} 公里 ≈ {result:.2f} 英里 elif from_unit 摄氏度 and to_unit 华氏度: result value * 9/5 32 return f{value} 摄氏度 {result:.2f} 华氏度 # 处理数学表达式使用eval需谨慎此处仅作演示 # 在生产环境中应使用更安全的库如numexpr或ast.literal_eval处理部分运算 result eval(expression, {__builtins__: {}}, math.__dict__) return f{expression} {result} except Exception as e: return f计算‘{expression}’时出错{str(e)}。请检查表达式格式。 # 将工具放入列表 tools [get_weather, search, calculator]4.2 步骤二初始化Agent与执行器我们选择ZERO_SHOT_REACT_DESCRIPTION这种通用类型来组装我们的Agent。from langchain.agents import initialize_agent, AgentType from langchain_openai import ChatOpenAI # 1. 初始化LLM llm ChatOpenAI(modelgpt-3.5-turbo, temperature0, api_keyyour-api-key-here) # 请替换为你的API Key # 2. 初始化Agent agent initialize_agent( toolstools, llmllm, agentAgentType.ZERO_SHOT_REACT_DESCRIPTION, # 使用ReAct框架 verboseTrue, # 开启详细日志方便我们看到思考过程 handle_parsing_errorsTrue, # 处理格式错误 max_iterations6 # 限制最大步数防止跑飞 )4.3 步骤三运行并观察完整的“思考-行动”闭环现在让我们问一个需要多步协作的问题。query 上海今天天气怎么样如果天气好帮我搜一下上海外滩有什么好玩的活动然后计算一下如果门票平均100元3个人要花多少钱。 result agent.invoke({input: query}) print(result[output])当verboseTrue时你会在控制台看到类似下面的详细输出。这是理解Agent工作流的绝佳材料务必仔细看 Entering new AgentExecutor chain... Thought: 用户的问题分为几个部分。首先需要查询上海的天气然后根据天气决定是否搜索活动最后计算费用。我应该按顺序执行。 Action: get_weather Action Input: 上海 Observation: 上海多云18-28摄氏度东南风1级。 Thought: 天气是多云不算坏天气可以继续搜索活动。现在需要搜索上海外滩的活动。 Action: DuckDuckGoSearchRun Action Input: 上海外滩 近期 活动 推荐 Observation: [这里会是DuckDuckGo返回的实际搜索结果摘要例如“外滩灯光秀、观光隧道、艺术展览等”] Thought: 我已经找到了活动信息。现在需要计算3个人每人平均100元的门票总花费。这是一个简单的乘法计算。 Action: calculator Action Input: 3 * 100 Observation: 3 * 100 300 Thought: 我现在有了所有信息天气情况、活动搜索结果和费用计算。可以给用户一个完整的回答了。 Final Answer: 上海今天天气多云18-28摄氏度东南风1级适合出行。根据搜索上海外滩近期有灯光秀、艺术展览等活动可供选择。如果按平均门票100元计算3个人的总费用是300元。 Finished chain.这个输出完美展示了ReAct框架的闭环Thought: Agent分析任务拆解出第一步是查天气。Action/Action Input: 它选择了正确的工具get_weather并输入了正确的参数上海。Observation: 工具返回了天气结果。新的Thought: 基于观察天气尚可它决定执行下一步搜索活动。Action/Action Input: 选择搜索工具并生成了搜索关键词。Observation: 获得搜索结果。Thought: 进行到最后一步识别出这是一个计算问题。Action/Action Input: 选择计算器工具输入3 * 100。Observation: 获得计算结果。Final Answer: 综合所有观察组织成一段完整的、人性化的答案回复给用户。这就是一个完整的、由LLM大脑驱动工具手脚的智能闭环。你可以看到LLM并非简单地串联工具而是在中间进行了关键的逻辑判断“天气是多云不算坏天气可以继续”。5. 避坑指南从“能跑”到“好用”的关键细节让一个Agent跑起来可能只需要半小时但让它稳定、可靠、高效地工作却需要填平无数个坑。下面是我从实际项目中总结出的血泪经验。5.1 工具描述的“魔鬼细节”工具描述不清是Agent“智障”行为的首要原因。除了前面说的要写清楚功能、输入、输出外还有几个高级技巧用自然语言描述输入格式如果工具期望一个日期不要说“输入是日期”而要说“输入是一个‘YYYY-MM-DD’格式的日期字符串例如‘2023-10-27’”。设定边界和约束在描述中明确说明工具的局限性。例如“该工具只能查询未来7天内的天气”或者“该计算器不支持复数运算”。使用示例在描述中嵌入示例极其有效。例如“使用示例输入‘北京’返回北京的天气输入‘巴黎’返回巴黎的天气。”一个优化后的天气工具描述可能是获取全球主要城市的当前天气状况。输入应为一个明确的英文或中文城市名称例如‘Beijing’或‘北京’。该工具返回温度、天气现象、湿度和风力信息。请注意对于非常小众的城市可能无法获取数据。5.2 迭代失控与“思考漩涡”你有没有遇到过Agent在一个简单问题上不停思考就是不输出最终答案这就是陷入了“思考漩涡”。除了设置max_iterations这个硬性限制还有更优雅的解法优化提示词Prompt在给Agent的系统提示词或用户问题中明确要求它“在获得足够信息后请直接给出最终答案不要重复思考已知信息”。你可以通过自定义prompt模板来实现。使用更好的early_stopping_method除了“force”还可以尝试“generate”。当LLM认为自己可以给出答案时它会输出一个特殊的停止令牌AgentExecutor检测到后就会结束循环。但这依赖于LLM本身的判断能力。设计工具返回“终结信号”对于一些查询类工具可以在返回结果后加上一句“【信息已完整】”。LLM在观察中看到这个信号就更倾向于结束任务。5.3 错误处理与用户体验一个成熟的Agent必须能优雅地处理失败。工具层错误处理每个工具函数内部必须有完善的try-except并返回对LLM友好的错误信息。不要抛出未处理的异常那会导致整个Agent崩溃。tool def get_stock_price(symbol: str) - str: try: # 调用API price call_finance_api(symbol) return f{symbol}的当前股价是{price}美元。 except ConnectionError: return 【网络错误】无法连接到财经数据服务请稍后再试。 except ValueError: return f【输入错误】未找到股票代码‘{symbol}’请检查代码是否正确。 except Exception as e: return f【系统错误】处理请求时发生未知问题{str(e)[:50]}... # 截断长错误信息Agent层面的解析错误设置handle_parsing_errorsTrue后当LLM输出不符合Action:格式时AgentExecutor会尝试将错误信息反馈给LLM让它重试。你还可以通过一个自定义函数来更精细地处理def parsing_error_handler(error): return f你之前的回复格式有误未能识别出有效的‘Action’。请严格按照‘Thought: ... Action: ... Action Input: ...’的格式重新思考并回答。错误详情{error} agent_executor AgentExecutor( ..., handle_parsing_errorsparsing_error_handler )5.4 上下文管理与长对话记忆默认情况下Agent是“单轮失忆”的。你问完一个问题它回答完对话历史就清空了。这对于需要多轮交互的复杂任务如一步步规划一个旅行行程是不可行的。保留对话历史将agent_executor封装在一个带有记忆功能的链中。最常用的是ConversationBufferMemory。from langchain.memory import ConversationBufferMemory memory ConversationBufferMemory(memory_keychat_history, return_messagesTrue) agent_executor AgentExecutor( agentagent, toolstools, memorymemory, # 注入记忆 verboseTrue ) # 后续调用时会自动携带历史记录 result1 agent_executor.invoke({input: 我叫小明。}) result2 agent_executor.invoke({input: 我的名字是什么}) # Agent会记得你叫小明记忆的代价与优化记忆意味着每次调用都会将越来越长的历史对话传入LLM这会增加token消耗、拖慢速度并可能触及上下文长度限制。对于长对话需要考虑使用ConversationSummaryMemory总结历史或ConversationBufferWindowMemory只保留最近N轮来优化。6. 超越基础从单Agent到多Agent与LangGraph当你熟练掌握了单个Agent的构建后自然会遇到更复杂的场景一个任务需要多个具备不同专长的Agent协作完成或者任务流程包含复杂的条件判断和循环。这时就该了解LangChain的进阶生态——LangGraph。6.1 为什么需要多Agent想象一个“旅行规划助手”的任务需要理解用户模糊的需求“我想去一个温暖的海边度假”。需要搜索和推荐目的地。需要查询目的地的天气和航班信息。需要根据预算生成一个粗略的行程单。让一个Agent干所有这些事工具列表会很长提示词会非常复杂而且容易混乱。更好的架构是一个“主管Agent”负责理解用户初始需求并将任务拆解。一个“目的地专家Agent”专门负责搜索和推荐地点拥有搜索工具和地理知识库。一个“信息查询Agent”专门负责调用天气、航班等实时API。一个“行程规划Agent”专门负责整合信息生成结构化行程。主管Agent根据任务阶段将工作分配给最专业的子Agent去执行并汇总结果。这就是多Agent系统Multi-Agent System的基本思想。6.2 LangGraph为Agent绘制工作流蓝图LangGraph是LangChain的一个库它允许你用“图”Graph的方式来定义和编排多个Agent或组件的工作流。节点Node可以是Agent、工具或任何函数边Edge定义了节点之间的流转条件。一个简单的“审批”工作流例子节点1起草Agent根据用户需求生成一份文档草案。节点2检查工具调用一个语法检查工具检查草案。条件边如果检查通过流向节点3润色Agent如果不通过则流回节点1重新起草。节点3对草案进行润色。节点4最终输出将润色后的文档返回给用户。在LangGraph中你可以清晰地定义这个包含循环和条件分支的流程这是单纯使用AgentExecutor难以实现的。from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated import operator # 1. 定义状态State即工作流中传递的数据结构 class AgentState(TypedDict): task: str draft: str checked: bool final_output: str # 2. 定义各个节点函数这里用模拟函数代替Agent def draft_node(state: AgentState): print(起草节点运行...) return {draft: f这是关于{state[task]}的草案。} def check_node(state: AgentState): print(检查节点运行...) # 模拟检查逻辑比如草案长度大于5就算通过 is_ok len(state[draft]) 5 return {checked: is_ok} def polish_node(state: AgentState): print(润色节点运行...) return {final_output: f【润色版】{state[draft]}} # 3. 构建图 workflow StateGraph(AgentState) # 添加节点 workflow.add_node(draft, draft_node) workflow.add_node(check, check_node) workflow.add_node(polish, polish_node) # 设置入口 workflow.set_entry_point(draft) # 添加边 workflow.add_edge(draft, check) # 条件边根据检查结果决定流向 def decide_next(state): if state[checked]: return polish else: return draft # 检查不通过回去重写 workflow.add_conditional_edges( check, decide_next, { polish: polish, draft: draft, } ) workflow.add_edge(polish, END) # 4. 编译并运行图 app workflow.compile() initial_state {task: 写一份周报} result app.invoke(initial_state) print(result[final_output])虽然这个例子没有用到真实的Agent但它清晰地展示了LangGraph如何编排一个带有条件判断检查是否通过和潜在循环不通过则重写的复杂流程。在实际项目中你可以把draft_node、polish_node替换成真正的Agent调用。6.3 何时该用LangGraph任务有明确的阶段和依赖关系比如“先A后BB失败了再重试A”。需要多个专业Agent协作比如前面说的旅行规划。流程中包含复杂的人工干预或审批节点比如生成内容后需要人工确认才能发布。你需要一个可视化、可调试的工作流LangGraph的图结构非常直观。对于简单的、线性的“思考-行动”循环AgentExecutor足够了。但对于真正的企业级、生产级的复杂自动化任务LangGraph提供的可控性和可扩展性是不可或缺的。走到这里你已经不再是仅仅调用API的开发者而是能够设计并实现一个具备自主推理和行动能力智能系统的工程师。从理解大脑LLM与手脚Tools的关系开始到精心设计每一个工具再到选择合适的思想框架AgentType最后通过严密的错误处理和记忆管理让系统变得稳健甚至迈向多智能体协作的广阔天地。这个过程的核心始终是理解LLM如何思考并引导它可靠地使用工具来扩展其能力边界。记住最好的Agent不是最聪明的而是最懂得在何时、以何种方式、使用何种工具来完成任务的。