最近好几个做后端和算法的朋友都在问我 Agent 开发到底怎么入门,说 LangChain 文档翻了好几遍、示例代码也能跑,但一动手做自己的智能体还是发懵。这篇笔记是我自己从零啃 Agent 的踩坑记录,核心目标就一个:把“Agent 到底是什么、LangChain 怎么帮你把它跑起来”这两件事讲透。看完你至少能写出一个带工具调用的最小智能体,也知道下一步该往哪个方向深挖。
这篇笔记更适合两类人:一类是写过 Python、但没系统接触过 Agent 的开发者,另一类是已经在用 LangChain 拼 Prompt 调模型、但觉得“这不像智能体”的人。如果你已经能用 LangGraph 写出多 Agent 协作,可以直接跳到第六节看框架边界。
1. 先搞清楚一件事:Agent 不等于“调一下大模型”
这个点不说清,后面全是在拿工具箱当创可贴。我见过太多所谓“Agent 项目”,本质是脚本加 Prompt:用户问一句,程序把问题塞进模板,扔给大模型,返回一段文字。
这种流程当然能用,但它不是 Agent。真正的差距在“决策权”到底在谁手里。
1.1 多数“智能体项目”其实是脚本加 Prompt
普通的大模型调用,流程是死的:用户输入,拼 Prompt,调模型,输出。所有分支逻辑都需要开发者在代码里写死,模型没有机会根据中间结果改变自己的行动路径。
举一个看得见摸得着的例子。你写一个客服机器人,用户说“我想查一下订单状态”,普通脚本的做法是写一个if "订单" in user_query去命中规则,然后查数据库,把结果拼进 Prompt 再丢给模型润色。规则多了以后,你会发现这个if链条越来越长,用户表达稍微换一种说法就漏接。
而 Agent 的做法是:直接把“查订单状态”封装成一个工具,把用户问题交给模型,让模型自己判断“这个问题需要调用订单查询工具”,然后自动执行、拿结果、组织回答。模型在流程当中是有决策权的,开发者不再需要把所有规则穷举出来。
这两者没有绝对的高下之分,普通脚本在某些简单场景下性能更好、成本更低,但涉及复杂任务编排时,Agent 的开发效率和泛化能力明显更强。
1.2 Agent 的核心机制:模型在循环里做决策
Agent 的本质是一个循环,很多人叫它 ReAct,也就是 Reasoning(推理)加 Acting(行动)。整个循环是这样转的:
- 模型阅读用户问题,先想“要回答这个问题,我需要什么信息”。
- 如果信息不够,模型会提出“我打算调用某个工具”,并给出参数。
- 系统执行这个工具,把结果返回给模型。
- 模型看到工具结果后,再判断“这些信息够不够,还要不要继续调其他工具”。
- 如果够了,模型输出最终答案,循环结束。
这个循环最关键的改变在于:每一步的“下一步做什么”是模型根据当前上下文动态决定的,而不是开发者预先写死的。这也是为什么 Agent 能处理那些步骤不固定的开放任务——比如“帮我查一下北京和上海的天气温差,再算算高铁两个小时能不能到”。
我自己刚学的时候,为了搞懂这个循环,特意不去碰框架,写了一个极简版 ReAct 循环,核心代码大概长这样:
for step in range(max_steps): resp = model.invoke(messages) action = parse_action(resp.content) if action is None: return resp.content tool_result = run_tool(action["name"], action["input"]) messages.append(resp) messages.append({"role": "tool", "content": str(tool_result)}) return "超过最大循环步数,主动停止"这个代码很短,但思路非常纯粹:模型说调什么工具,系统就调什么,结果塞回去,模型再决定下一步。max_steps是必须加的,否则模型可能一直在“调用工具”和“继续思考”之间打转,白白烧掉你的 Token。
1.3 那为什么还要学 LangChain 和 LangGraph
手写循环能帮你理解原理,但到了真实项目里,问题会变得很具体:多个工具的注册和参数校验怎么做?工具调用失败要不要重试?几十轮对话历史怎么裁剪?多个用户会话怎么隔离?
这些通用问题如果都自己造轮子,项目还没开始就废了。LangChain 把模型封装、工具定义、输出解析这些组件标准化了,LangGraph 则把循环、分支、多 Agent 协作这些流程编排问题做成了图结构,你只需要关注业务本身。
我建议的学习路线是:先手写一个极简 ReAct 循环理解原理,再用 LangChain 的组件把它们串起来。这样你后面遇到任何框架报错,心里都有一个“底层发生了什么”的坐标系,不至于完全懵。
2. 认识 LangChain 里的智能体组件
LangChain 不是某一个巨无霸库,它是一堆组件的集合。做 Agent 开发,你最先要认识的是四个角色:模型、工具、记忆、编排器。
理解这四个组件各自的职责和边界,比背 API 重要得多。
2.1 四个核心组件:模型、工具、记忆、编排器
先给一个直观的分工说明:
| 组件 | 职责 | 对应 LangChain 里的常见类型 |
|---|---|---|
| 模型 | 决策大脑,理解意图并决定下一步动作 | ChatOpenAI、ChatOllama 等 |
| 工具 | 模型可调用的外部能力 | @tool 装饰器定义的函数 |
| 记忆 | 保存对话上下文和中间状态 | MemorySaver、BaseStore |
| 编排器 | 控制循环,判断何时停止 | LangGraph 的状态图、AgentExecutor |
模型这块最容易踩坑。Agent 开发对模型有一个硬性要求:必须支持工具调用,也就是 Function Calling。你现在随便拿一个老模型,或者不支持工具调用的本地小模型,你会发现不管怎么调 Prompt,模型都只会输出“我建议你手动去查”,因为它在模型层面就没有输出结构化工具调用指令的能力。
记忆也不是可选项。没有记忆的 Agent 就是一个失忆的客服,用户上一秒说完需求,下一秒它就忘。但记忆又不能无限堆,上下文窗口总有上限,所以需要记忆管理策略。
编排器是灵魂。它决定你整个 Agent 的运行逻辑:先调用哪个节点、工具返回后走哪条分支、失败后要不要重试、达到什么条件就终止输出。
2.2 工具定义方式与规范
LangChain 里面定义一个工具非常简单,用@tool装饰器就行。函数的文档字符串会自动变成模型的工具描述,函数签名里的类型注解会生成参数 JSON Schema。
from langchain_core.tools import tool @tool def calculator(expression: str) -> str: """计算简单数学表达式,支持 + - * / 和括号。传入表达式字符串,返回计算结果。""" allowed = set("0123456789+-*/(). ") if not set(expression).issubset(allowed): return "非法表达式,请检查输入" return str(eval(expression, {"__builtins__": {}}, {}))这里有个安全细节:eval在真实项目里尽量不要直接面向用户输入开放,我用白名单字符集和空__builtins__只是演示。生产环境更推荐用ast.literal_eval或者专门写一个表达式解析器,避免注入风险。
再看一个天气工具:
@tool def get_weather(city: str) -> str: """查询指定城市的当前天气。传入城市名,返回天气描述。""" weather_map = {"北京": "晴,25°C", "上海": "多云,28°C"} return weather_map.get(city, "暂不支持该城市")定义工具最关键的不是代码本身,而是函数名和描述。函数名要直观,比如get_weather而不是fun1;描述要写清楚“什么时候用、传什么、返回什么”,因为模型是靠这段描述来判断该不该调这个工具的。描述写得含糊,模型就会频繁误调用。
2.3 为什么现在主流推荐 LangGraph 而不是 AgentExecutor
如果你翻老教程,会看到大量initialize_agent加AgentExecutor的写法。这套接口在 LangChain 0.3 之前很流行,但现在已经被标记为 deprecated,官方推荐用 LangGraph 的create_agent来创建 Agent。
原因很简单:AgentExecutor 是一条单行管道,脚本从上往下执行,最多套一个循环,它对复杂分支、人工审核、多 Agent 协作基本无能为力。而 LangGraph 把整个流程画成一张有向图,节点是操作,边是跳转逻辑,Agent 只是图里的一个节点。
对比一下:
| 能力 | AgentExecutor | LangGraph |
|---|---|---|
| 循环执行 | 内置 | 图结构自由编排 |
| 分支跳转 | 弱 | 强,支持条件边 |
| 人工介入中断 | 不支持 | 支持 |
| 多 Agent 协作 | 基本不支持 | 原生支持 |
| 调试可视化 | 日志 | 状态快照、逐步回放 |
所以你现在看到的新项目、新招聘需求,基本都是 LangGraph 这套体系。我不是说老代码没用,理解它的设计思路有帮助,但新项目别再踩旧坑。
3. 实操:用代码跑通一个带工具调用的 Agent
理论讲再多,不如跑通一个实在的例子。这一节我会带你从环境准备开始,搭一个能同时处理“查天气”和“算数学题”的智能体。
3.1 环境准备与依赖安装
建议用 Python 3.10 以上版本,创建好虚拟环境后,直接装这几个包:
pip install -U langchain langchain-openai langchain-community langgraph这里有两个细节。第一,为什么不装langchain老版本?因为新版框架结构和 API 变化很大,你搜到的很多老教程会给你装一堆过时代码,装最新版至少能保证和官方文档一致。第二,langgraph是独立的包,不要漏装,create_agent在这个包里。
模型我建议新手先用带工具调用能力且稳定的云端模型,比如gpt-4o-mini,等整个流程跑通以后,再考虑换 Ollama 本地模型。设置 API Key 的方式很简单:
import os os.environ["OPENAI_API_KEY"] = "sk-你的key" from langchain_openai import ChatOpenAI model = ChatOpenAI(model="gpt-4o-mini", temperature=0)这里把temperature设成 0 是我个人的执念。Agent 场景里,我们希望模型在“决定要不要调工具”这件事上尽量稳定,随机性越低越好。温度拉高以后,模型可能会在同一个问题上给出完全不同的行动路径,调试起来会非常痛苦。
3.2 定义工具并创建 Agent
把上面两个工具函数拿过来,然后用create_agent组装:
from langgraph.prebuilt import create_agent tools = [calculator, get_weather] agent = create_agent( model=model, tools=tools, system_prompt=( "你是智能助理。当问题涉及精确计算或实时天气时,必须调用对应工具。" "调用工具拿到结果后,再组织最终回答。如果工具返回异常,如实说明失败原因。" ), ) response = agent.invoke( {"messages": [("user", "北京今天多少度?顺便算一下 (25+17)*3 等于多少")]}, config={"recursion_limit": 20}, ) print(response["messages"])执行这段代码,你会看到response里是一个消息列表,最后一条消息就是 Agent 的最终回答。中间还有模型调用工具的记录、工具返回的结果,这些都被存放在消息列表里。很多第一次用的人不知道去哪里找中间过程,关键就在这个列表里。
3.3 打开调试,看它到底“思考”了什么
为了让过程更透明,创建 Agent 时可以开启调试:
agent = create_agent( model=model, tools=tools, system_prompt=SYSTEM_PROMPT, debug=True, )开启以后,控制台会输出模型每一步的工具调用请求和工具执行结果。你能清晰地看到模型先调用了get_weather,返回“晴,25°C”,然后又调用了calculator,返回计算数值,最后把两个结果拼成一句完整回答。
我一直觉得,Agent 开发最大的心智门槛不是写代码,而是习惯“模型的思考过程可见”。传统编程里,程序怎么走完全由你控制,但 Agent 里,模型会在你控制之外决定调用什么工具。如果不能实时观察它的推理轨迹,出问题基本没法排查。
3.4 加记忆:让 Agent 记住多轮对话
默认情况下,create_agent不保留多轮会话记忆,每次invoke都是独立的。如果你需要 Agent 记住用户上一句话,需要给它挂一个 Checkpointer:
from langgraph.checkpoint.memory import MemorySaver checkpointer = MemorySaver() agent = create_agent( model=model, tools=[calculator], checkpointer=checkpointer, ) thread_config = {"configurable": {"thread_id": "user-001"}} agent.invoke({"messages": [("user", "我叫小明")]}, config=thread_config) agent.invoke({"messages": [("user", "我叫什么名字?")]}, config=thread_config)这里最关键的是thread_id,它就是会话 ID。同一个thread_id下的多轮调用来共享一套消息历史,不同thread_id之间完全隔离。MemorySaver适合本地测试,重启进程后记忆会丢,生产环境一般用 PostgreSQL 或者 Redis 这类持久化存储。
4. 深入理解几个关键机制
跑通 Demo 只是第一步。如果只满足于“能跑”,你会在后面对接真实业务时处处碰壁。这一节重点讲清楚三个机制:Tool Calling 到底是怎么发生的、怎么让模型输出结构化结果、以及 Prompt 在 Agent 里的真实作用。
4.1 Tool Calling 是怎么发生的
很多人有一个误解,觉得模型“会调用工具”,是模型自己去执行了函数。实际上模型根本没执行任何东西,模型做的是输出一个结构化的 JSON,告诉你“我想调用calculator,参数是(25+17)*3”。真正执行函数的是你的代码、是 LangChain 框架。
与普通文本生成不同,支持 Tool Calling 的模型在训练时就接受了“输出结构化调用指令”的专门优化。当它判断需要查信息或执行计算时,会在回复中带上一个特殊字段,表示要发起的工具调用。
这就解释了为什么工具描述那么重要。模型不是看到函数源码,它看到的是你给它的 JSON Schema,包括工具名、描述、参数格式。描述越清晰,模型越容易在正确时机调用正确的工具。有些项目工具调用准确率特别低,第一个该检查的就是工具描述写得够不够好。
4.2 用结构化输出约束最终结果
生产环境里,Agent 的回答通常不是给人看一眼就完事,而是要对接业务系统。比如客服助手判单,你希望它返回的是一个结构化的 JSON,包含处理状态、原因分类、回复文案,而不是一大段散文。
LangChain 支持with_structured_output,配合 Pydantic 模型定义输出结构:
from pydantic import BaseModel, Field class TicketResult(BaseModel): category: str = Field(description="问题分类") confidence: float = Field(description="置信度,0到1之间") reply: str = Field(description="给用户的回复文案") structured_model = model.with_structured_output(TicketResult) result = structured_model.invoke("我的订单三天了还没发货,帮我问问") print(result.category)这样拿到的就是一个校验过的TicketResult对象,字段缺失或类型不对会直接报错,不用再手写正则去解析文本。把 Agent 的输出和业务系统对接,这一层结构化是少不了的。
4.3 Prompt 在 Agent 里的角色和开放心态
传统大模型应用中,Prompt 决定了回答质量。Agent 里 Prompt 依然重要,但它的职责变了——重点不是让模型“说得好”,而是让模型“知道什么时候该动、什么时候该停”。
我常用的系统 Prompt 模板一般是这样的逻辑:
你是智能助理。 1. 面对实时信息、精确计算、数据查询类问题,必须先调用对应工具,禁止凭记忆编造。 2. 调用工具后,先解释结果,再给结论。 3. 工具返回错误时,向用户说明失败原因,不要假装成功。 4. 如果不需要工具就能回答,直接回答。第四点很容易被忽略。有些人为了让 Agent“看起来智能”,恨不得所有问题都走一遍工具调用。其实很多时候用户就是问一句常识,你让模型调一个无关工具,只会增加延迟和成本。
5. 常见问题与排查技巧实录
这一节整理的是我实际开发中踩过的坑,不是从文档里抄来的。很多问题你早晚会遇到,先存着,省得到时候抓瞎。
5.1 模型死活不调用工具
症状:你明明绑定了工具,但不管怎么问,模型都直接给出答案,完全不碰工具。
先按顺序排查:
- 模型本身是否支持 Tool Calling?有些模型即使支持,也要确认 LangChain 的集成包已经正确加载。
- 工具描述里是否说清了“什么时候用”?如果你只写“计算器”,模型猜不透什么时候该用。
temperature是不是太高?温度高会导致模型行为不稳定,工具调用指令可能被“随机掉”。- 系统 Prompt 有没有明确要求“必须调用工具”?对于拿不准的问题,模型倾向于保守,你要主动给它授权。
我见过最隐蔽的一个问题:工具函数名取的太抽象,比如func_a、process_data,模型根本不知道这是干嘛的。工具名就是给模型看的接口名,别省那几个字。
5.2 上下文爆炸:Token 消耗太大
Agent 的每一轮循环都会把历史消息和工具结果重新发送给模型。假设一个工具调用循环要经历 8 轮,每轮上下文 2000 Token,一轮完整任务就是 16000 Token 起步,这还不算多用户并发的成本。
对策有三个:
- 控制历史消息长度,只保留最近几轮对话。
- 工具结果只保留关键信息,不要把一个超大 JSON 整个塞回上下文。
- 用摘要记忆,把早期对话压缩成一段摘要,而不是保留全部原文。
Token 消耗不是算法问题,是成本问题,项目上线前一定要压测。
5.3 工具返回解析失败或内容不可用
工具返回的结果对模型来说就是一段文本。如果工具返回一段特别长的 JSON,模型很可能被里面的次要字段带偏,答非所问。
我的习惯是:工具返回前就做清理。比如数据库查询,直接拼成“订单 xxx 的状态为已发货,发货时间为 2025-06-01”,而不是把整个记录对象原样返回。
5.4 版本兼容问题速查
LangChain 版本更新频繁,你在网上搜教程时很容易遇到“这段代码跑不通”的情况。这里给一个新旧对照表,方便排查:
| 功能 | 旧写法 | 新写法 |
|---|---|---|
| 创建 Agent | initialize_agent(...) | create_agent(...) |
| 执行 Agent | agent.run(...) | agent.invoke(...) |
| 对话记忆 | ConversationBufferWindowMemory | Checkpointer + 消息历史 |
| 持久化存储 | 缺少标准方案 | BaseStore及相关实现 |
我现在的建议很直接:新项目就装最新版langchain和langgraph,遇到文档里的 API 不存在,优先去官方文档查最新签名,不要硬套旧代码。
还有一个经常出现的报错是GraphRecursionError: Recursion limit reached,意思就是 Agent 循环次数超过了限制。你可以加大recursion_limit,但我建议先想想为什么模型需要那么多步才能完成回答,很多时候是工具结果不清晰导致模型反复调用。
6. 下一步怎么走:学习路线与框架边界
第一篇笔记讲到这里,已经覆盖了 Agent 最核心的工作原理和一个最小可运行项目。但我知道你肯定不满足于此,所以我再给一条可执行的进阶路线。
6.1 推荐的学习顺序
我的建议按这个顺序推进:
- 不看框架,手写一个 ReAct 循环,理解模型决策和工具调用的闭环。
- 用 LangChain 的组件重写一遍,了解模型封装、工具定义、输出解析。
- 换用 LangGraph,从图结构角度理解编排。
- 挑一个真实业务场景做项目,比如客服工单分类、本地知识库问答。
- 最后再研究 Agent 的评估体系,也就是 Agent Evals。
不要一上来就背 LangChain API,背了也会忘,关键是理解链路。框架更新太快,理解原理比记函数名值钱。
6.2 LangChain 和 LangGraph 到底是什么关系
这个问题最近被问得太多了,一句话总结:LangChain 提供组件,LangGraph 提供编排能力。LangChain 负责和模型打交道、定义工具、处理输入输出,LangGraph 负责把这些组件连成一张可以循环、分支、暂停的图。
从职责上看,两者是组合关系,不是竞品关系。新的 Agent 项目基本都是 LangChain 组件加 LangGraph 编排的搭配。现在很多低代码平台,比如 Dify、Coze,底层其实也在做类似的事情,你理解了 LangGraph 的设计,再去看这些平台就很容易看穿它们的套路。
6.3 多 Agent 开发初探
当你单 Agent 玩熟了,会开始遇到一个现象:任务复杂以后,一个 Agent 既要规划、又要执行、还要自我检查,Prompt 越写越长,效果却越来越差。
这时候就该拆多 Agent 了。常见的模式是一个“主管 Agent”负责任务分发,下面挂多个“执行 Agent”,每个执行 Agent 只负责一个窄领域。这种结构在 LangGraph 里很好实现,本质上就是图里面再加几个节点和几条边。
但我要泼一盆冷水:多 Agent 不是越多越好。每多一个 Agent,就多一层延迟、多一份 Token 成本、多一套失败逻辑。能单 Agent 解决的事,就别硬拆成三个 Agent。生产环境里,“架构简单”本身就是一个极大的优点,多 Agent 是最后手段,不是炫技工具。
最后分享一点我个人的体会:从手写 ReAct 再回过头用create_agent,你会特别清楚地感受到框架到底替你干了什么。如果你也在入门,我强烈建议你先花一晚手写那个极简循环,再去用 LangGraph,那种“原来如此”的感觉比看十篇教程都有效。后续我会继续更新 LangGraph 的状态管理、多 Agent 协作模式,以及 Agent 上线前的评测方案。