三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从LangChain入门AI Agent:手把手实现ReAct智能体与核心原理剖析

从LangChain入门AI Agent:手把手实现ReAct智能体与核心原理剖析

1. 项目缘起:为什么从LangChain开始你的AI Agent之旅?

最近和不少刚入行的朋友聊天,发现大家对“AI Agent”这个概念既兴奋又迷茫。兴奋的是,它听起来像是能自动完成复杂任务的“数字员工”;迷茫的是,一搜教程,满屏的框架、工具和抽象概念,比如LangGraph、Dify、Coze,让人不知从何下手。我的建议是,别急着追新框架,先把地基打牢。这个地基,就是LangChain

你可能听过这样的说法:“LangChain太重了”、“LangGraph才是未来”、“直接用Dify搭智能体更快”。这些观点都有道理,但如果你目标是真正理解AI Agent是如何“思考”和“行动”的,而不是仅仅快速拼凑出一个演示Demo,那么从LangChain入手,亲手实现一个最基础的智能体,是性价比最高的学习路径。它就像学编程先学C语言,虽然写Web应用可能直接用Spring Boot更快,但C语言能让你理解内存、指针和底层逻辑,这些知识是通用的。

LangChain本质上是一个编排框架,它不提供大模型(LLM),也不提供具体的工具(比如搜索、计算),但它定义了一套清晰的“乐谱”,告诉大模型、工具、记忆等组件如何协同工作,来完成一个任务。通过实现一个基础的LangChain Agent,你将透彻理解几个核心问题:智能体是如何根据用户指令决定下一步行动的?它如何选择和使用工具?它的“思考过程”(ReAct模式)是怎样的?这些理解,是你未来评估LangGraph、Dify、Coze,甚至自研框架的基石。

所以,这篇内容不是又一个简单的“Hello World”示例。我会带你从零开始,搭建一个能解决实际问题的、具备基础推理能力的LangChain智能体。我们会聚焦于最经典、最核心的ReAct Agent的实现,并在这个过程中,拆解每一个组件的职责,分析每一步的决策逻辑。当你完成这个项目,你不仅会得到一个可运行的代码,更会获得一套理解任何Agent框架的“元认知”。

2. 环境准备与核心组件拆解:不只是安装包

在写第一行代码之前,我们需要把“舞台”搭好。这个舞台包括运行环境、关键“演员”(组件)以及对它们角色的清晰认知。

2.1 基础环境搭建与依赖选择

我强烈建议使用Python 3.10或以上版本,并且创建一个独立的虚拟环境。这能避免未来各种依赖冲突的噩梦。

# 创建并激活虚拟环境(以conda为例) conda create -n langchain-agent python=3.10 conda activate langchain-agent # 安装核心依赖 pip install langchain langchain-openai

这里有两个关键包:

  • langchain: LangChain框架的核心。
  • langchain-openai: 这是LangChain官方维护的OpenAI模型集成包。在较新的版本中,LangChain将不同厂商的模型集成拆分为独立的包(如langchain-anthropic,langchain-google-genai),这样更清晰,也便于维护。我们使用OpenAI的模型作为我们智能体的“大脑”。

注意:你需要准备一个有效的OpenAI API Key,并设置到环境变量中。我习惯在项目根目录创建一个.env文件来管理,使用python-dotenv加载,而不是在代码里硬编码。

# .env 文件 OPENAI_API_KEY=your_api_key_here
# 在代码开头加载 from dotenv import load_dotenv load_dotenv() # 之后在初始化OpenAI模型时,它会自动从环境变量读取OPENAI_API_KEY

2.2 深入理解LangChain Agent的核心“演员表”

一个最简单的LangChain ReAct Agent,通常由以下四个核心组件构成,理解它们的关系至关重要:

  1. 大语言模型 (LLM):智能体的“大脑”。负责理解指令、进行推理、生成下一步的行动计划或最终答案。我们选用gpt-3.5-turbo作为起点,它成本效益高,能力足够完成我们的实验。

  2. 工具 (Tools):智能体的“手和脚”。LLM本身无法直接操作外部世界(如执行计算、搜索网络、查询数据库)。工具就是赋予它这些能力的函数。例如,一个计算器工具、一个搜索引擎工具。智能体的核心能力,很大程度上取决于你为它装备了哪些工具。

  3. 智能体类型 (AgentType):智能体的“行为范式”或“决策算法”。它定义了大脑(LLM)如何与工具交互的流程。ZERO_SHOT_REACT_DESCRIPTION是我们即将使用的类型,它是一种最经典的ReAct范式:对于每个步骤,LLM会生成一个“Thought”(思考)、“Action”(选择哪个工具)、“Action Input”(工具的输入)的格式化文本,然后框架执行工具,得到“Observation”(观察结果),再喂回给LLM进行下一轮思考,直到它认为可以给出“Final Answer”。

  4. 代理执行器 (AgentExecutor):智能体的“舞台导演”或“流程控制器”。它封装了运行智能体的复杂循环逻辑:调用LLM、解析输出、运行工具、处理错误、管理交互历史(记忆)等。我们不需要自己写while循环和复杂的解析逻辑,AgentExecutor帮我们搞定了一切。

它们之间的关系可以用一个简单的比喻:LLM是公司CEO,负责战略思考;Tools是各个部门的专家(财务部、市场部);AgentType是公司的决策流程(例如,CEO提出问题,各部门提供方案,CEO综合决策);AgentExecutor是CEO的助理,确保这个流程每一步都正确执行,并记录会议纪要。

3. 实战:构建一个能查天气和计算的智能体

现在,让我们把这些组件组装起来。我们的目标是创建一个智能体,它能理解“北京现在的天气怎么样?”或者“计算一下356乘以128等于多少?”这类问题,并自动调用正确的工具来解答。

3.1 第一步:打造智能体的“工具箱”

我们首先创建两个最基础的工具:一个模拟的天气查询工具和一个真实的数学计算工具。

from langchain.agents import Tool from langchain_community.utilities import SerpAPIWrapper # 为了示例,这里用SerpAPI作为搜索工具示例,但实际我们会先模拟 from langchain.chains import LLMMathChain from langchain_openai import ChatOpenAI import requests # 初始化LLM,这是所有链和工具共享的“大脑” llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) # temperature=0 使输出更确定,减少随机性,对于工具调用这类任务很重要。 # 工具1:模拟天气查询工具 def get_weather(location: str) -> str: """根据城市名查询模拟天气信息。在实际应用中,这里应调用真实的天气API,如OpenWeatherMap。""" # 模拟数据 - 实际项目请替换为API调用 weather_data = { "北京": "北京当前天气:晴,气温 25°C,湿度 40%,东南风2级。", "上海": "上海当前天气:多云,气温 28°C,湿度 65%,微风。", "广州": "广州当前天气:阵雨,气温 30°C,湿度 85%,南风3级。", } return weather_data.get(location, f"抱歉,未找到{city}的天气信息。") # 将函数封装成LangChain Tool对象 weather_tool = Tool( name="GetWeather", func=get_weather, description="当用户询问某个城市的当前天气时使用此工具。输入应为一个明确的城市名称,例如‘北京’。" ) # 工具2:数学计算工具 # LLMMathChain 是一个封装好的链,专门用于将自然语言问题转化为数学表达式并计算。 math_chain = LLMMathChain.from_llm(llm=llm) math_tool = Tool( name="Calculator", func=math_chain.run, # 直接使用chain的run方法 description="适用于回答数学计算问题。输入可以是一个数学表达式(如‘2+2’)或一个文字问题(如‘三百五十六乘以一百二十八是多少’)。" ) # 将工具放入列表,供智能体使用 tools = [weather_tool, math_tool]

关键点解析:

  • Tool对象的三要素name(工具名,LLM用它来指代工具)、func(工具的实际执行函数)、description(工具描述,这是最重要的部分)。LLM完全依靠description来判断在什么情况下使用哪个工具。因此,描述必须清晰、准确,说明工具的用途、输入格式和输出什么。
  • 为什么用LLMMathChain而不是简单eval:直接使用Python的eval()执行用户输入的字符串是极度危险的。LLMMathChain会先让LLM将问题解析成安全的数学表达式(例如,将“三百五十六乘以一百二十八”解析为“356*128”),然后再进行计算,安全得多。
  • 模拟工具的意义:在原型阶段,用模拟工具快速验证智能体的决策流程是否通畅,比一开始就集成复杂的第三方API更高效。验证逻辑正确后,再替换为真实的requests调用。

3.2 第二步:初始化智能体与执行器

有了工具箱和大脑,现在可以创建智能体本身了。

from langchain.agents import initialize_agent, AgentType # 初始化ReAct智能体 agent = initialize_agent( tools=tools, llm=llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, # 指定使用Zero-shot ReAct范式 verbose=True, # 强烈建议设为True,这样可以看到智能体完整的“思考过程” handle_parsing_errors=True, # 优雅地处理LLM输出格式不符合预期时的解析错误 max_iterations=5, # 防止智能体陷入无限循环,限制最大迭代次数 early_stopping_method="generate" # 当智能体连续多次输出无法解析的内容时,让其直接生成最终答案 ) # AgentExecutor已经在initialize_agent内部创建并封装好了,我们直接使用`agent`对象即可。

参数深度解读:

  • AgentType.ZERO_SHOT_REACT_DESCRIPTION:这是最常用的入门类型。ZERO_SHOT意味着它不需要额外的示例(few-shot)来学习工具使用,仅靠工具描述。REACT代表其推理模式。DESCRIPTION强调它依赖工具描述。
  • verbose=True这是学习和调试的生命线。当它被打开时,控制台会打印出智能体完整的思考链(Thought-Action-Observation循环),你能亲眼看到LLM是如何一步步推理、决策的。关掉它,智能体就变成了一个黑盒。
  • handle_parsing_errors=True:LLM的输出偶尔会不严格遵循要求的格式,导致框架解析失败。这个参数能捕获此类错误,并以一种更友好的方式重试或处理,避免程序直接崩溃。
  • max_iterationsearly_stopping_method:这是生产环境必须考虑的防护措施。理论上,智能体应自己决定何时停止。但实践中,LLM可能会陷入“思考-调用无用工具-再思考”的死循环。这两个参数是安全阀,确保系统最终能停下来并给出一个回应(哪怕是“我无法解决”)。

3.3 第三步:运行与深度观察

让我们运行它,并仔细分析输出。

# 提问 question = “北京现在的天气怎么样?” result = agent.invoke({"input": question}) print(f"\n最终答案:{result['output']}")

打开verbose=True后,你会在控制台看到类似下面的输出:

> Entering new AgentExecutor chain... Thought: 用户想知道北京的当前天气。我有一个工具叫GetWeather,就是用来查询城市天气的。我应该使用这个工具。 Action: GetWeather Action Input: 北京 Observation: 北京当前天气:晴,气温 25°C,湿度 40%,东南风2级。 Thought: 我已经通过GetWeather工具获取了北京的天气信息,现在可以直接回答用户了。 Final Answer: 北京当前天气晴朗,温度25摄氏度,湿度40%,东南风2级。 > Finished chain. 最终答案:北京当前天气晴朗,温度25摄氏度,湿度40%,东南风2级。

过程拆解:

  1. Thought:LLM读取用户问题,结合工具描述列表,进行推理。“用户想知道天气 -> 我有天气工具 -> 用这个工具”。
  2. Action/Action Input:LLM输出格式化的决策,指定要使用的工具名称和输入参数。框架会截取这部分。
  3. 框架执行:框架找到名为GetWeather的工具,用Action Input(“北京”)作为参数调用get_weather(“北京”)函数。
  4. Observation:工具执行的结果被返回,作为“观察”反馈给LLM。
  5. 下一轮Thought:LLM看到观察结果,判断信息是否足够。“信息已获取,可以生成最终答案了”。
  6. Final Answer:LLM生成面向用户的自然语言回答。

再试一个需要多步推理或工具选择的例子:

question2 = “上海的气温是不是比广州高?先查一下两地的天气。” result2 = agent.invoke({"input": question2})

观察输出,你会看到智能体可能会先调用GetWeather查询上海,得到结果后,在下一个Thought中意识到还需要广州的信息,于是再次调用GetWeather,最后比较两个Observation,给出最终答案。这就是智能体“自主规划”能力的雏形

4. 避坑指南与效能提升:从“跑通”到“好用”

把例子跑起来只是第一步。在实际开发中,你会遇到各种问题。下面是我踩过坑后总结的关键经验。

4.1 工具描述的“艺术”:清晰度决定智能体性能

工具描述 (description) 是智能体能否正确使用工具的最关键因素。糟糕的描述会导致工具不被调用或被误用。

  • 反面例子“一个有用的工具。”(太模糊,LLM不知道何时用)
  • 正面例子“当用户询问特定地点的当前天气状况、温度、湿度或风力时使用此工具。输入必须是一个明确的城市或地区名称,例如‘伦敦’或‘纽约’。不要用于查询天气预报或历史天气。”

撰写优秀描述的技巧:

  1. 明确触发条件:在什么类型的问题或语境下使用此工具?使用“当用户想要...”、“适用于...”开头。
  2. 定义精确输入:工具函数接受什么格式的输入?是字符串、数字还是列表?举例说明。
  3. 说明输出性质:工具会返回什么?是原始数据还是一段文本?这有助于LLM理解如何利用观察结果。
  4. 划定边界:明确说明什么情况下不要用这个工具,避免工具冲突。

4.2 解析错误与循环失控:如何设置安全护栏

即使有了好的描述,LLM的输出也可能“跑偏”。

  • 现象1:输出格式错误:LLM可能不按Thought/Action/Action Input的格式输出,导致AgentExecutor解析失败。

    • 解决方案handle_parsing_errors=True是第一道防线。你可以将其设置为一个自定义函数,进行更精细的错误处理和提示修正。
    def custom_parse_error_handler(error): return “抱歉,我处理您的请求时出现了理解偏差,让我们重新尝试一下。” agent = initialize_agent(..., handle_parsing_errors=custom_parse_error_handler)
  • 现象2:无限循环或无效循环:智能体反复调用同一个工具或在不同工具间无效切换。

    • 根因:工具返回的Observation可能没有提供足够的新信息,或者LLM的“思考”陷入了局部最优。
    • 解决方案
      1. 硬性限制:务必设置max_iterations(如5-10次)。这是最后的保障。
      2. 优化工具反馈:确保工具返回的信息清晰、结构化。如果工具失败,返回明确的错误信息(如“查询失败:网络错误”),而不是空字符串或None,这能帮助LLM理解状况。
      3. 使用更高级的AgentTypeZERO_SHOT_REACT_DESCRIPTION比较简单。对于复杂任务,可以考虑STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,它要求LLM以更结构化的JSON格式输出,通常更稳定。

4.3 为智能体注入“记忆”:实现多轮对话

我们上面的智能体是“失忆的”,每轮对话都是独立的。要让它能进行多轮对话(比如,用户问“北京天气?”,然后接着问“那上海呢?”),需要引入**记忆(Memory)**组件。

from langchain.memory import ConversationBufferMemory # 创建记忆体,保存对话历史 memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True) # 在初始化智能体时传入memory参数 agent_with_memory = initialize_agent( tools=tools, llm=llm, agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION, # 注意!需要更换为支持对话的Agent类型 verbose=True, memory=memory, # 传入记忆体 handle_parsing_errors=True ) # 第一轮对话 result1 = agent_with_memory.invoke({"input": “今天北京天气如何?”}) print(result1['output']) # 第二轮对话,智能体会记得之前的上下文 result2 = agent_with_memory.invoke({"input": “上海呢?”}) # 它会理解“上海呢?”指的是上海的天气 print(result2['output'])

关键点:

  • ConversationBufferMemory简单地保存所有历史对话的原始文本。
  • 必须切换AgentTypeZERO_SHOT_REACT_DESCRIPTION不支持记忆。需要改用CONVERSATIONAL_REACT_DESCRIPTIONCHAT_CONVERSATIONAL_REACT_DESCRIPTION,这些类型在提示词模板中预留了chat_history的位置。
  • 记忆的代价:记忆会消耗更多的Token,增加API调用成本,也可能导致提示词过长而被截断。对于长对话,可能需要使用ConversationSummaryMemoryConversationBufferWindowMemory来摘要或只保留最近几轮对话。

5. 超越基础:探索更强大的模式与架构

当你熟练掌握了基础ReAct智能体后,你的视野可以投向更广阔的地方,理解当前AI Agent生态的演进。

5.1 ReAct模式的局限性

经典的ReAct模式是线性的“思考-行动”循环。但在处理需要并行、需要复杂协调的任务时,就显得力不从心。例如:“同时监控A、B、C三个数据源,一旦其中两个出现异常,就通知D并执行应急预案。”这种任务涉及条件判断、并行执行和状态管理,用单一的ReAct循环很难优雅地实现。

5.2 LangGraph:将智能体工作流“可视化”与“可控化”

这就是LangGraph出现的意义。它不是一个替代LangChain的新框架,而是LangChain生态系统内一个用于构建有状态、多智能体工作流的库。你可以把它想象成用代码画一个流程图。

  • 核心概念State(状态)和Nodes(节点)。整个工作流有一个共享的状态对象,节点是对状态进行操作的函数(可以是调用LLM、运行工具、条件判断等)。Edges(边)决定流程的走向。
  • 与LangChain Agent的关系:在LangGraph中,一个LangChain Agent可以成为其中一个Node。你可以构建更复杂的图,比如:一个Node负责分析用户意图,然后根据意图路由到不同的专业子智能体(Node),子智能体处理完后,结果汇入状态,再由一个Node负责合成最终回复。
  • 适用场景:需要严格步骤控制、并行执行、循环、人工审批介入的复杂业务流程。例如客服工单处理、复杂数据分析流水线、游戏NPC行为树等。

5.3 Dify/Coze:低代码平台,快速应用化

DifyCoze(扣子)这类平台,可以看作是在LangChain/LangGraph等底层框架之上,封装了可视化编排界面、知识库管理、API部署、用户交互前端等一整套功能的AI Agent应用开发平台

  • 优势:无需编码或少量编码,通过拖拽组件(提示词、LLM、工具、知识库)就能快速搭建一个具备聊天、文件处理、工作流等能力的智能体应用,并一键发布为Web服务或API。
  • 与本文路线的关系:如果你目标是快速构建一个可交付的AI应用产品,Dify/Coze是更高效的选择。但如果你目标是深入理解智能体内部的运作机制、进行深度定制或学术研究,那么从LangChain底层实现开始,仍然是不可逾越的路径。底层原理的知识,能让你在使用高阶平台时,更能理解其边界,并能在出问题时进行底层调试。

从亲手实现一个LangChain基础智能体开始,你获得的是对AI Agent核心范式——感知、规划、行动、反思——的切身理解。这份理解,是你未来无论选择深耕LangChain、探索LangGraph的复杂工作流,还是利用Dify等平台加速产品化,都能牢牢握在手中的导航图。

← 返回列表