最近好几个做后端的朋友问我同一个问题:Agent 开发到底怎么入门?网上资料一堆,但要么是翻译腔浓厚的官方文档,要么直接甩一个 LangGraph 工程让我自己啃,对零基础的人来说其实挺劝退。我的建议一直很明确:先从 LangChain 入手,跑通一个会调用工具、有记忆、能对话的智能体,再去看 LangGraph 这类编排框架。这篇笔记就是我梳理完整个上手过程后沉淀下来的内容,按“为什么要学、核心组件、实战代码、避坑记录、后续升级路线”这条线走,看完你至少能自己搭一个正经能用的 Agent。
我默认你看过 Python,知道什么是 API,但对 Agent、LangChain、智能体这些词只有模糊的概念。没关系,这些概念我拆成大白话讲,配合能直接跑的代码来理解。这篇的核心目标是让你在半天内亲手写出一个“有手有脚”的智能体,而不是停留在概念层面。
1. 为什么从 LangChain 入手做智能体
1.1 先搞清楚 Agent 到底在解决什么问题
我们平时调大模型 API,本质上是一个“输入提示词、输出文字”的过程,模型本身不会主动去查数据库、不会调外部接口,也不会记住上个月你问过什么。智能体的核心就是把大模型从“只会聊天”变成“能干活”,而“干活”通常意味着:根据用户需求自己决定调用哪些工具、然后根据工具返回的结果再决定下一步干什么,甚至多步循环直到任务完成。
举个例子,你问普通 ChatGPT“今天北京天气适合穿什么”,如果模型没有联网工具,它只能凭训练数据编一个答案。但 Agent 的做法是:先识别出“用户想知道天气”,然后调用天气查询工具,拿到实时温度、风力、降水概率,再结合这些数据生成穿搭建议。这个“识别需求 → 调用工具 → 分析结果 → 生成回复”的闭环,就是 Agent 要解决的核心问题,也是它和普通对话机器人最大的区别。
1.2 LangChain 给 Agent 开发提供了什么
我在选框架之前也纠结过一阵子:为什么不直接写一个 while 循环,自己把大模型返回的文本解析一下,然后调函数?理论上当然可以,但你会很快发现要处理的细节多到崩溃——工具返回格式不规范怎么写解析器、对话历史怎么管理、模型突然不按约定格式输出怎么办、多步调用怎么控制循环次数,每一个都是坑。LangChain 的价值在于把这些通用性问题抽象成了标准化组件,你不需要重复造轮子,只需要专注在自己的业务逻辑上。
具体来说,LangChain 提供了一套统一的 Agent 抽象,包括:与大模型交互的接口、工具注册与调用机制、记忆管理模块、提示词模板,以及一个内置的 AgentExecutor 循环调度器。它的设计哲学是“组件化”,就像乐高积木一样,你可以把不同的大模型、不同的工具、不同的记忆方式自由组合。这也意味着你学的东西可迁移,不会绑定在某一家大模型厂商的 API 上。
1.3 我的学习路线规划
如果你是完全的新手,我建议不要一上来就啃 LangGraph,也不要一上来就研究多智能体编排。一条比较顺的路线是:第一周用 LangChain 跑通单 Agent、单工具,理解 ReAct 循环原理;第二周加记忆、加多个工具,理解工具选择的机制;第三周再去看 LangGraph,理解复杂流程控制,比如条件分支、人工审批、多智能体协作。
我当时就是吃了“想一步到位”的亏,一开始直接看 LangGraph 的 StateGraph 文档,结果被节点、边、状态 这些概念绕晕,连聊天的上下文怎么传递都没搞清楚。后来老老实实回到 LangChain 的 AgentExecutor 手写几个示例,反而一下子通了。所以这篇笔记的核心定位就一句话:先让你把最简单、最经典的 Agent 跑起来,跑通了再谈优化和架构升级。
2. 动手前必看:LangChain Agent 的核心组件与运行机制
2.1 六个组件事先有个印象
LangChain 里构建一个 Agent,本质上就是在组装六个东西:模型(LLM)、工具(Tools)、记忆(Memory)、提示词(Prompt)、解析器(OutputParser)和执行器(AgentExecutor)。模型负责“思考决策”,工具负责“落地执行”,记忆让对话有上下文,提示词告诉模型要按什么风格和逻辑走,解析器把模型输出的乱七八糟文本整理成结构化指令,执行器则负责循环调度。
我在学习时把它们类比成一个小团队:大模型是项目经理,只负责想方案、下指令;工具是执行员工,指哪打哪;记忆是会议纪要员,记录之前聊过什么;提示词是公司制度手册,规定做事的套路;解析器是翻译员,把项目经理想说的话翻译成员工听得懂的指令;执行器是项目推进的流程引擎,整个团队按循环流程走完每一个任务。
2.2 AgentExecutor 的循环过程:ReAct 原理一次讲清
LangChain 经典的 AgentExecutor 背后是一个叫 ReAct 的推理框架。ReAct 是 Reason + Act 的缩写,意思是模型在每一步都会先“思考”再“行动”。一个典型的循环是这样的:模型看到用户问题 → 输出思考和下一步行动指令(比如调用某个工具)→ 执行器调用工具并拿到结果 → 把结果返回给模型 → 模型再次思考,直到它认为信息足够,输出最终回答。
从代码层面看,AgentExecutor 会一直重复“调模型 → 解析动作 → 执行工具 → 把观察结果回填到上下文”这个流程,直到模型输出一个特殊的结束标识,或者达到最大迭代次数。理解这个原理非常重要,因为后面你遇到的百分之八十的 Agent 问题,比如循环不退出、工具调用混乱、上下文爆炸,本质上都是对这个循环理解不够导致的。
2.3 工具描述为什么值得认真写
很多新手写工具时只关注函数逻辑本身,忽略了工具的名称描述和参数描述,结果模型老是调用错工具。我第一次写 Agent 时就踩过这个坑:我注册了一个叫get_stock_price的工具,docstring 只写了“获取股价”,结果用户问“帮我看看今天黄金多少钱”时,模型居然也去调了股票工具,因为描述不够具体,模型根本不知道它只适用于股票不适用于黄金。
LangChain 的工具注册机制其实非常依赖描述文本的语义清晰度。你用@tool装饰器定义函数时,函数名、参数注释和 docstring 都会被作为元数据传给模型,模型就是靠这些描述来“理解”这个工具能干什么、在什么场景下用。所以务必要把这个描述当作给一个不了解你业务的产品经理看的说明来写,把边界条件、适用场景写清楚,模型的工具选择准确率会明显提升。
3. 从零实战:一个能查时间、算表达式、查库存的 Agent
3.1 环境准备与版本选择
在开始之前先把环境准备好。Python 版本建议 3.10 或 3.11,langchain、langchain-openai、langchain-community这几个核心包装好就行。注意版本问题:LangChain 的 API 更新比较快,0.1.x 和 0.2.x 版本之间有些导入路径变了,如果你看到网上老代码报错,大概率是版本不匹配。建议用较新的 0.2.x 版本,但也不要盲目追求最新,因为社区文档和示例往往滞后。
pip install langchain langchain-openai langchain-community python-dotenv我用的是 OpenAI 接口来演示,因为生态最成熟,但实际操作中你完全可以替换成国产模型。很多国产模型提供 OpenAI 兼容接口,LangChain 里可以直接用ChatOpenAI类,只需把base_url改成对应的服务地址就行。另外也可以用 Ollama 跑本地模型,比如llama3.1或qwen2.5,体验一下本地部署的 Agent。不过本地模型对格式遵循能力要求较高,如果 Agent 频繁解析失败,建议先用强大的线上模型跑通全流程,再换小模型做优化。
3.2 手写第一个最小 Agent
我建议不要一上来就叠加记忆和复杂工具,先把一个最简单的单工具 Agent 跑通。下面这个例子我实际跑过很多次,逻辑很简单:用户问“现在几点了”,Agent 调用自定义的current_time工具返回当前时间。
import os from datetime import datetime from langchain.agents import AgentExecutor, create_react_agent from langchain.tools import tool from langchain_openai import ChatOpenAI from langchain_core.prompts import PromptTemplate @tool def current_time(format: str = "%Y-%m-%d %H:%M:%S") -> str: """获取当前日期和时间,格式默认为 年-月-日 时:分:秒。""" return datetime.now().strftime(format) llm = ChatOpenAI( model="gpt-4o-mini", temperature=0 ) prompt = PromptTemplate.from_template( """你是一个有帮助的助手。请根据用户的输入,决定是否使用工具来回答问题。 尽可能使用工具获取实时信息,而不是依赖自己的记忆。 可用工具如下: {tools} 工具名称列表:{tool_names} 请严格按如下格式输出,不要输出多余内容: Question: 用户输入的问题 Thought: 你应该怎么思考 Action: 要调用的工具名称,必须是 [{tool_names}] 中的一个 Action Input: 工具的输入参数 Observation: 工具返回的结果 ...(重复 Thought / Action / Action Input / Observation 直到拿到结果) Thought: 我现在已经知道最终答案 Final Answer: 最终回答给用户的答案 用户的问题是:{input} {agent_scratchpad}""" ) agent = create_react_agent(llm=llm, tools=[current_time], prompt=prompt) agent_executor = AgentExecutor(agent=agent, tools=[current_time], verbose=True, handle_parsing_errors=True) result = agent_executor.invoke({"input": "现在几点了?"}) print(result["output"])这里有几个操作关键点要强调。第一,create_react_agent里传的prompt必须包含{tools}、{tool_names}、{input}、{agent_scratchpad}这几个占位符,因为它们分别对应工具描述列表、工具名称列表、用户输入和中间推理过程,LangChain 在执行时会自动填充这些变量。第二,handle_parsing_errors=True这个参数我强烈建议加上,因为模型偶尔会输出不符合要求的格式,如果不开这个,程序会直接抛异常退出,开了之后它会提示模型“输出格式不正确,请重新输出”,极大提高稳定性。
跑起来之后你会在终端看到完整的日志——Thought、Action、Action Input、Observation 一步步刷出来,那个瞬间你对 ReAct 循环的理解会一下子具体起来。我建议新手一定要开verbose=True把这套过程看完,能直观体会到模型是怎么“思考”的。
3.3 给 Agent 加记忆:让它记住上下文
第一个 Agent 跑通之后,第二个问题很快就会暴露:你问完“现在几点了”,再问“那东京呢”,它完全不知道“东京”指的是什么,因为每次调用都是独立的,上下文没有被留存。这就是我们要引入记忆的原因。
LangChain 的记忆模块种类很多,ConversationBufferMemory是最简单的一种,它会把所有历史对话原封不动地拼到提示词里。还有一种很好用的是ConversationSummaryBufferMemory,它会在对话太长时把旧内容总结压缩,避免超出模型上下文窗口。这里用最基础的做演示:
from langchain.memory import ConversationBufferMemory from langchain.agents import AgentExecutor, create_react_agent memory = ConversationBufferMemory( memory_key="chat_history", return_messages=True ) prompt_with_memory = PromptTemplate.from_template( """你是一个有帮助的助手。请根据用户的输入,决定是否使用工具来回答问题。 可用工具如下: {tools} 工具名称列表:{tool_names} 历史对话: {chat_history} 用户的问题是:{input} {agent_scratchpad}""" ) agent_with_memory = create_react_agent(llm=llm, tools=[current_time], prompt=prompt_with_memory) agent_executor_with_memory = AgentExecutor( agent=agent_with_memory, tools=[current_time], memory=memory, verbose=True, handle_parsing_errors=True )注意示例里我把chat_history加进了 PromptTemplate 的变量列表里,同时在 AgentExecutor 里传入了memory。运行后你可以先问“现在几点了”,再问“那明天呢”,会发现 Agent 能理解“那明天”指的是相对当前时间推一天,因为历史对话已经作为上下文传给了模型。
需要提醒的是,ConversationBufferMemory是“无脑全存”型,对话一长,Token 消耗会非常快,而且模型输入可能被撑爆。生产环境里我更推荐ConversationSummaryBufferMemory或外接向量数据库做长期记忆,这是后面进阶要解决的事,但至少你现在知道了记忆机制是怎么接进去的。
3.4 在同一个 Agent 里挂多个工具:让它自己选
真正有用的 Agent 肯定不止一个工具。比如我想做一个“销售助手”,既要知道当前日期,又要能查商品库存,还要能做数学计算(比如算订单总价)。挂多个工具其实非常简单,只需要把它们全部放到 tools 列表里,剩下的事交给模型决策。
下面是我用过的另一个真实场景的简化版,展示了多工具的协作方式:
@tool def calculate_expression(expression: str) -> str: """计算一个数学表达式的值。输入必须是合法的算术表达式,如 '12 * 2 + 3'。""" # 安全起见,只允许数字和基本运算符 allowed_chars = set("0123456789+-*/(). ") if not set(expression).issubset(allowed_chars): return "表达式包含非法字符,拒绝执行" result = eval(expression) return str(result) @tool def query_inventory(product_name: str) -> str: """查询商品库存数量。支持的商品:笔记本电脑、手机、耳机、键盘。""" inventory = { "笔记本电脑": 15, "手机": 32, "耳机": 50, "键盘": 0, } return f"{product_name} 的库存为 {inventory.get(product_name, '未知商品')} 件" tools = [current_time, calculate_expression, query_inventory]这里值得展开说一下的是工具描述的设计思路。我在calculate_expression的描述里写了“输入必须是合法的算术表达式”,是因为大模型有时候会直接把自然语言问题原样塞进来,比如“帮我算一下23乘以45”,如果你不写清楚参数必须是表达式,它就会把整个自然语句传给工具,导致工具无法解析。这就是 2.3 节说的“描述文本是模型理解工具的唯一窗口”的实际体现。
三个工具都注册好之后,你可以抓一个任务测试:“今天是几号?帮我算一下 25 号之前(含当天)还剩几天,顺便查一下笔记本电脑的库存够不够给 3 个客户每人一台。”这个任务同时考验时间获取、数学计算、库存查询三件事,看看 Agent 的命令链能不能合理编排。实际上它可能会先调 current_time 拿日期,再调 calculate_expression 做减法,最后调 query_inventory 查库存,这就是一个简单的多步骤调用闭环。
测试下来你会发现,只要工具描述写得好,模型在绝大多数情况下能自主完成拆解、选工具、评估结果、给出综合结论。但也不是百分之百可靠,偶尔会有调用顺序错乱或者参数传错的情况,这正是后面要讲的问题排查部分。
4. 常见问题与排查技巧实录
4.1 工具返回报错:模型拿到一堆 Exception 后开始胡说
我遇到最多的坑是:工具内部执行出错,异常直接抛给 Agent,模型看到报错信息后不知道怎么办,就开始编造结果。比如query_inventory函数里如果查询的商品不在字典里,返回了“未知商品”,模型可能为了面子上过得去,会编一个“该商品库存充足”的假结果。
解决方案有两个层面。第一层是工具内部要做兜底,宁可返回明确错误信息也不要抛异常,而且错误信息要带有可操作的建议。比如上面库存查询,如果商品不存在,应该返回“未查询到该商品,可支持的商品包括:笔记本电脑、手机、耳机、键盘”,这样模型就知道怎么纠正。第二层是给 Agent 传递“如何处理工具错误”的规则,在系统提示词里明确写一句“如果工具返回错误信息,请告知用户查询失败的真实原因,不要编造数据”。
4.2 死循环:Agent 卡在一个工具调用里出不来
当模型反复调用同一个工具,且每次都拿不到关键信息,就会陷入“调用 → 报错 → 再调用”的循环。这就是我们前面讲到的 ReAct 循环不够健壮的情况,处理不好会白烧一大笔 Token。最直接的处理办法是在AgentExecutor里设置max_iterations参数,比如设为 5,达到上限后 Agent 会停止循环并返回当前信息。在实战中我通常这样写:
agent_executor = AgentExecutor( agent=agent, tools=tools, max_iterations=5, early_stopping_method="generate", verbose=True, handle_parsing_errors=True )这个early_stopping_method="generate"的意思是当达到最大迭代次数时,让模型基于已有的中间结果尝试生成一个最终答案,而不是直接抛超时异常。这在实际项目中能救回不少请求。当然,设置迭代上限只是止血,要根治还得优化提示词和工具设计,让模型第一步就能准确选择合适的工具。
另外我还建议在代码里给工具调用加上超时控制,尤其是外部 API 类工具。如果工具内部一直卡在 HTTP 请求上,Agent 整个线程都会被阻塞,表现起来就像“死机了”,实际上是在等一个永远不会回来的响应。给 requests 请求加上timeout=10是一个非常好的习惯。
4.3 记忆错乱:Agent 把上一轮的内容当成这轮的新输入
加了记忆之后,新手很容易遇到一个诡异的局面:你明明问的是“今天天气怎么样”,Agent 却在回答“根据你刚才说的股票信息……”,活像一个记性差的人在瞎聊。排查这类问题的思路是:看看传给 Prompt 的chat_history变量是否被正确填充、是否有多个消息源混乱拼接。
我见过一个典型误用:在ConversationBufferMemory里设置了return_messages=True,但 PromptTemplate 却用了字符串拼接的方式(比如直接{chat_history}),结果模型收到的是消息对象而非纯文本,完全没法理解。解决办法是检查 Prompt 里的变量类型和 memory 的return_messages配置是否匹配,要么都走文本格式,要么都走消息格式,不要混用。另外对话轮次多了之后,历史记录会占用大量 Token,甚至超出上下文长度导致请求报错,这时你就要考虑用ConversationSummaryBufferMemory对历史做摘要了。
4.4 模型选型建议:不是越强越好,而是越“听话”越好
很多新手在 Agent 里直接用了最强的大模型,但真正跑起来表现却不一定好。因为 Agent 对模型的要求集中在两个维度:一是格式遵循能力,能不能稳定输出指定格式的 Action;二是工具选择合理性,能不能在多个工具中正确挑出最合适的一个。
如果你用本地小模型,比如 qwen2.5:7b 甚至更小,大概率会出现“模型完全无视 ReAct 格式,直接输出一个人类能看懂的答案”的情况,Agent 就会一直解析失败。此时不要急着加各种纠错逻辑,先换一个大一点的模型测试,确认链路本身没问题,再回头优化小模型。如果非要用小模型,可以考虑用create_tool_calling_agent配合支持 function calling 的模型接口,对格式遵循的要求更低一些,效果会更稳定。我在本地跑 Ollama 时就是这么处理的,兼容性和成功率明显比 ReAct 格式好。
接下来我把这个对比整理成表格,方便你在选型的时候快速参考:
| 对比维度 | ReAct 格式 Agent | Tool Calling Agent |
|---|---|---|
| 对模型的格式要求 | 高,必须产出结构化文本 | 较低,原生 function calling 支持 |
| 适用的模型 | GPT-4 级别、Claude 级别 | 支持 function calling 的模型,如 qwen、glm、gpt-4o 系列 |
| 调试友好度 | 直观,能看到完整推理链 | 黑盒,模型内部决策过程不可见 |
| 复杂工具场景表现 | 一般 | 更稳,尤其参数多时 |
| 生产环境建议 | 较稳但需注意提示词约束 | 优先推荐 |
5. 从 LangChain 到 LangGraph:什么时候该升级、怎么选
5.1 LangChain 的“裸奔 Agent”在复杂场景下的局限性
当你用 LangChain 的 AgentExecutor 跑通若干个 Demo 之后,会逐渐感觉到一些吃力。最明显的问题是流程不可控:AgentExecutor 像一个全自动的“黑盒调度器”,模型想调用哪些工具、按什么顺序调用,开发人员很难干预。但在真实的业务场景中,你一定会有“必须在某个环节人工确认后才能继续”的需求,或是“如果用户输入包含 A 就必须走流程 X,否则走流程 Y”的条件分支需求。这些东西用 AgentExecutor 几乎没法优雅实现,你只能往提示词里塞各种规则,让模型的“自觉”来保证流程正确。
另一个痛点是多智能体协作。当你需要多个擅长不同领域的 Agent 配合——比如一个负责信息检索、一个负责内容生成、一个负责质量审查——AgentExecutor 本身并不提供清晰的协作机制。你虽然可以在工具层面把一个 Agent 作为另一个 Agent 的工具来嵌套,但这种嵌套方式调试起来非常痛苦,状态管理几乎靠打印日志人肉梳理。
5.2 LangGraph 解决了什么:状态、节点和边的协作思维
LangGraph 和 LangChain 的关系是:LangGraph 不是一个替代品,而是在 LangChain 基础之上更底层的“状态机编排框架”。在 LangGraph 里,你的整个业务逻辑被建模为一张图:节点就是具体执行的计算单元(可以是大模型调用、工具调用、人工审批),边就是节点之间的流转条件,全局状态是所有节点共享的一个数据容器,每个节点结束后更新状态、决定下一个走向哪个节点。
这个思维方式的优势非常明显——流程可视化、可控性强、可支持人工介入,也天然适配多智能体协作场景。举例来说,在你需要“用户输入 → 判断是否需要调用工具 → 如果需要则调用 → 汇总结果 → 人工确认 → 生成最终回复”这样的流程时,LangGraph 里每个环节都是一个显式的节点,中间任何一步都可以插入逻辑来改变走向。从这个角度看,LangGraph 更像是把 Agent 的整个执行过程从“一把手”变成“看得见的流水线”。
5.3 我的建议:什么时候值得切换
根据我在实际项目中的体会,可以给你这样一条判断标准:如果你的 Agent 只是“根据用户问题调用一个或几个工具然后回复”,LangChain 的 AgentExecutor 完全够用,没必要引入额外的复杂度。但如果你开始考虑“用户可能要经过多轮确认才能完成下单”、“需要多个专职 Agent 协同完成一份报告”、“需要在特定场景下绕过模型直接执行某段硬逻辑”,那就是切换到 LangGraph 的正常时机了。
另外我想说一个反直觉的经验:LangGraph 学起来更像是在做后端流程开发,而不是纯粹的大模型调用。你会用到处处都是{"key": value}的全局状态更新,你会写很多回调函数来处理条件边判断,甚至要面对并发和状态一致性之类的问题。但是一旦你把状态流转搞明白,回头看 LangChain 的 AgentExecutor,反而会觉得那只是 LangGraph 的一个非常简化的特例。所以我的学习建议是两者都学,LangGraph 作为进阶,LangChain 作为基础,它们组合起来才算一个完整的智能体开发知识体系。
就我个人经验来说,从 LangChain 入门智能体开发,最大的收获不是学会了一个特定框架,而是明白了 Agent 这类应用的本质:它不追求“一步到位”的魔法,而是把大模型的推理能力、外部工具的执行能力、记忆的状态管理能力组合成一个可靠的工程系统。很多人在学完基础组件之后容易产生一个误区,觉得把工具堆得越多、把模型换得越大,Agent 就越强大,实际测试下来往往会被非理想情况打脸。真正的功力体现在提示词约束、工具描述、状态控制这类容易被忽略的细节上。先把这篇笔记里的示例跑通,再试着改一改工具的描述、加一加新的工具,你会发现自己对 Agent 的直觉判断力完全不一样了。