LangChain Agent实战:从零构建可调用工具的智能体 你们发现没有从 ChatGPT 开始聊天这件事已经不太能满足真正的业务需求了。用户问一句“今天北京天气怎么样”模型哪怕知道答案也没法实时拿到天气数据。这时候就需要一个能“动手”的架构让大模型在理解问题之后自己去决定调用哪个工具、传什么参数、拿回结果再继续推理。这就是 LangChain Agent 干的事。这篇文章我会从零开始带你完整走一遍“快速构建可调用工具的智能体”的实战流程。不是泛泛讲概念而是直接把能跑通的代码、设计思路、常见坑都摆出来。适合刚接触 LangChain 的人也适合已经写过一点 Prompt 流程、但没系统搞过 Agent 的开发者。你看完之后至少能自己搭一个带两个以上外部工具的助手并且知道怎么排查 Agent 不听话的问题。1. 先把 Agent 到底在解决什么问题搞明白1.1 LLM 本身没有“动手”能力工具调用的由来先回到最朴素的问题大模型本质上是“预测下一个词”的模型训练完那一刻它的知识就凝固了。你问它今天几号、某个城市的实时 PM2.5、某个电商订单的物流状态它只能靠训练数据里的统计规律去“猜”猜得越具体越容易错。所以我一直强调LLM 不适合当数据库更不适合当实时接口它适合当“调度中心”和“对话引擎”。那怎么让 LLM 拥有实时能力早期做法很笨在 Prompt 里把外部数据塞进去比如先把天气 API 的结果拼到 Prompt 里再让模型回答。这叫 RAG 或者叫上下文注入能解决一部分问题但没办法处理“用户问 A 需要调接口 1问 B 需要调接口 2而且调用条件还要模型自己判断”这种动态流程。比如用户说“帮我安排明天下午两点的会议顺便看看明天会不会下雨”你总不能把所有接口结果都塞进去吧太浪费 token也无法应对组合逻辑。Agent 的思路完全不同模型不负责“算”只负责“决定”。用户提需求之后模型输出一个结构化的“工具调用意图”——我要调哪个函数、传什么参数然后由代码真正执行函数把结果返回给模型模型再根据结果组织回答。这就是目前大家常说的 Function Calling / Tool Calling也是 LangChain Agent 最核心的基石。你可以把它理解为模型是项目经理工具是执行员工项目经理不亲自搬砖但负责拆任务、验收结果。1.2 主流 Agent 实现模式与框架选型实现 Agent 的流派很多。OpenAI 官方直接支持 function calling你可以自己手写循环LangChain 提供了一套封装好的 Agent 框架LangGraph 则把流程控制做得更细支持状态机、循环、人工介入。我自己在实际项目里的体感是如果只是做原型、做内部工具、快速交付LangChain Agent 足够它的抽象层能让你少写大量样板代码如果业务流程特别复杂比如要支持分支、回退、人工审核、超时重试那 LangGraph 更合适因为它把“图”的概念引入流程控制状态流转非常清晰。很多人纠结 langchain 和 langgraph 的区别我大概总结一下LangChain 更像一个包含模型封装、Prompt 模板、文档加载、向量存储、Agent 执行器的全家桶适合快速搭建LangGraph 是基于图的状态编排引擎适合精细控制。实际项目里两者常常混用LangGraph 底层还是会用到 LangChain 的模型封装和工具抽象。你在学习的时候我建议先玩熟 LangChain Agent再迁移到 LangGraph曲线会平滑很多。这个阶段你不需要把所有概念都背下来你只要记住一句话Agent 模型 工具 循环控制。接下来的实战就是围绕这三样东西展开的。2. Agent 的核心概念拆解模型、工具与执行器2.1 工具Tool在 LangChain 里的标准形态LangChain 里工具的标准形态是一个类叫Tool它包含了名称、描述、参数结构、执行函数几个部分。比较新的写法是用tool装饰器直接装饰一个普通函数LangChain 会自动从函数的类型注解和 docstring 里提取参数结构。这个设计很实用你不需要手写 JSON Schema只要把函数签名写清楚。工具怎么写才是关键。很多新手上来就写一个def get_weather(city)docstring 只写“查询天气”结果模型经常传错参数。我自己的经验是docstring 一定要写清楚这个工具有什么用、每个参数的含义、单位、取值范围因为这些内容会原封不动喂给大模型它就是模型判断“该不该调用这个工具”的依据。比如同样一个查天气工具下面两种写法效果差别很大# 不推荐的 docstring def get_weather(city): 获取天气 # 推荐的 docstring def get_weather(city: str) - str: 查询指定城市的实时天气情况。 Args: city: 城市中文名例如北京、上海。 Returns: 包含温度、天气状况、风力的字符串。 第二个写法之所以好是因为它把“城市名”的范围说清楚了。没有这个约束用户说“北京天气”模型可能传“Beijing”而不是“北京”你的 API 匹配不到Agent 就失败了。这类问题不是模型不行而是工具定义不规范。2.2 create_tool_calling_agent 与 AgentExecutor 的分工LangChain 里有两个东西经常被混淆一个是 Agent 本身一个是执行器。Agent 就是“模型 Prompt 工具列表”的组合它负责决定下一步做什么AgentExecutor 则是驱动循环的引擎它负责任务分发、结果回传、终止判断、错误处理。在代码上体现得特别明显。create_tool_calling_agent返回的是一个Runnable对象它只做一次“推理”你直接调用它它只给你一个“下一步动作”有可能是AgentAction继续调工具也有可能是AgentFinish得到最终答案。真正让它循环起来的是AgentExecutor执行一次 Agent拿回动作执行工具把结果放回上下文再执行 Agent直到拿到AgentFinish为止。所以实战里你的标准写法是先创建模型再定义工具列表再用create_tool_calling_agent拼接模型和工具最后把 Agent 和工具一起塞进AgentExecutor。我们接下来就按这个流程走。注意我下面示例用的是 LangChain 0.2 的 API。如果你看到网上很多老教程在讲initialize_agent和ZeroShotAgent那套 API 虽然还能用但官方已经不太推荐了。新项目直接用create_tool_calling_agent或者create_react_agent就好。3. 从零开始写一个能查天气和算日期的 Agent3.1 设计意图为什么选这两个演示用的工具我先说下这个 Demo 的设计思路。实战示例我准备了两把“工具”一个是查实时天气一个是计算两个日期之间相差多少天。选择这两把工具是有讲究的查天气要求模型能理解“城市”实体并提取参数计算日期则要求模型能处理逻辑运算两个工具一个偏“外部数据获取”一个偏“本地逻辑计算”正好覆盖了工具调用的两种典型场景。这个 Agent 能做什么用户说“帮我看看这个周末北京适合出门吗”模型会先判断这需要天气信息于是调用get_weather拿到结果后组织回答如果用户说“从今天到国庆节还有多少天”模型会调用date_diff_calculator。如果用户的问题跟这两个工具都不相关模型会直接回答不强行调用。这一点很关键Agent 不是每轮都必须调工具它会自己判断。先装依赖。我建议你新建一个虚拟环境然后把下面几个包装上pip install langchain langchain-core langchain-openai python-dotenv这里说一句langchain 和 langchain-core 分开是有原因的核心抽象都放在 core 里具体模型实现放在各自的第三方包里。你如果只是用 OpenAI 模型装langchain-openai就够不用装一堆用不到的集成省得环境乱。3.2 工具函数实现tool 装饰器的正确用法在项目目录下新建一个tools.py把工具函数写在独立文件里是个好习惯后面加工具、改逻辑都方便不会污染编排代码。示例工具实现如下from datetime import datetime from langchain_core.tools import tool tool def get_weather(city: str) - str: 查询指定城市的实时天气情况返回温度、天气状况和风力信息。 Args: city: 城市中文名比如北京、上海、广州。 # 这里在实际项目中可以替换成真实的天气 API 请求 # 例如调用和风天气、心知天气等把返回结果格式化后返回 mock_data { 北京: 晴24℃风力2级, 上海: 小雨27℃风力3级, 广州: 雷阵雨30℃风力2级, } return mock_data.get(city, f暂未收录{city}的天气数据请确认城市名称。) tool def date_diff_calculator(date_str: str) - str: 计算给定日期与今天之间的相差天数。 Args: date_str: 目标日期格式为 YYYY-MM-DD例如 2025-10-01。 try: target_date datetime.strptime(date_str, %Y-%m-%d).date() today datetime.now().date() diff (target_date - today).days return f{date_str} 距离今天还有 {diff} 天。 except ValueError: return 日期格式错误请使用 YYYY-MM-DD 格式。看到tool这个装饰器了吗它会自动把函数包装成 LangChain 的工具对象并生成一组args_schema也就是模型能看到的结构化参数定义。这里有个很容易被忽略的点函数里面我写了.get(city, ...)这种容错逻辑这是因为即使你把参数说明写得再清楚模型仍然有可能传一个不在预期范围内的城市名工具必须能“优雅失败”而不是抛异常。工具异常会导致整个 Agent 循环中断那体验就很差了。很多人问工具返回的数据格式用什么比较好我的建议是尽量返回“人话字符串”因为模型最终要基于这个结果生成面向用户的回答。如果返回一个乱了套的 JSON 对象模型也能看但描述成本高容易理解偏差。简单场景就直接返回字符串复杂场景可以在字符串里用 JSON 格式但外层最好加一句人类可读的摘要。3.3 构建模型与 Agent代码一步步跑通现在写主流程文件main.py。先初始化模型和工具列表这里我用 OpenAI 风格的接口来写。你如果用国产的、开源部署的模型只要它们支持函数调用并且接入了 LangChain替换ChatOpenAI的 base_url 和 model 名就行整体结构不用动。import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain_core.prompts import ChatPromptTemplate load_dotenv() llm ChatOpenAI( modelgpt-4o-mini, temperature0, ) tools [get_weather, date_diff_calculator]关于temperature我给 Agent 用的模型一般直接拉到 0。Agent 不是写诗它的任务是从工具列表里选一个正确答案带随机性只会增加“选错工具”的概率。有人担心 temperature 为 0 会让模型太死板但在 Agent 场景下死板一点反而是好事。接着写 Prompt。Agent 的 Prompt 跟普通聊天 Prompt 不太一样它有几个关键占位符{input}是用户输入{agent_scratchpad}是中间推理记录{chat_history}是历史对话。这些占位符不能乱改AgentExecutor 会往里面填充内容。prompt ChatPromptTemplate.from_messages([ (system, 你是一个有用的助手可以根据用户问题调用合适的工具。 如果工具返回的信息足以回答用户就直接整理回答 如果工具返回异常或没有可用信息请如实说明。), (placeholder, {chat_history}), (human, {input}), (placeholder, {agent_scratchpad}), ])这里的(placeholder, {chat_history})值得单独说一下。placeholder是 LangChain 里一种特殊消息类型它允许在运行时插入任意数量的消息对象历史对话是多条消息所以必须用 placeholder。你如果直接写(human, {chat_history})当历史有多条消息时会直接报错。这是我踩过的坑先帮你排掉。然后组装 Agent 和执行器agent create_tool_calling_agent(llm, tools, prompt) agent_executor AgentExecutor( agentagent, toolstools, verboseTrue, handle_parsing_errorsTrue, )handle_parsing_errorsTrue是实战里强烈建议打开的开关。模型偶尔会输出格式错乱的工具调用如果不处理AgentExecutor 会直接抛异常开了这个开关它会把这些错误包装成一条错误消息再喂给模型重试容错性高很多。最后跑一个测试if __name__ __main__: result agent_executor.invoke({input: 北京今天天气怎么样}) print(result[output])运行之后因为verboseTrue你会看到 Agent 的思考过程它先输出一个AgentAction说要调用get_weather参数是“北京”然后工具返回结果再继续推理得到AgentFinish最后打印最终答案。整个过程一目了然。再试一下日期计算“从今天到 2025-10-01 还有多少天”。模型会调用date_diff_calculator拿回结果并给出回答。到这里一个能调用两个工具的 Agent 就跑通了。整个过程不到 50 行代码这就是 LangChain 封装的价值所在。4. 把这个 Agent 做得更接近真实项目记忆、流式、结构化输出4.1 让 Agent 记住上下文第 3 节的示例有个明显不足它没有记忆。你每次invoke都是独立对话Agent 对之前的用户输入没有任何概念。真实项目中这肯定不行用户不可能每次都把背景完整说一遍。LangChain 处理记忆推荐的方式是维护一个message_history每次调用前把历史消息填充进{chat_history}占位符。你可以自己用数组维护也可以用官方封装好的记忆模块。我自己更推荐手动维护一个列表简单直接后续换 LangGraph 也好迁移from langchain_core.messages import AIMessage, HumanMessage chat_history [] def chat_with_agent(user_input: str): global chat_history response agent_executor.invoke({ input: user_input, chat_history: chat_history, }) chat_history.append(HumanMessage(contentuser_input)) chat_history.append(AIMessage(contentresponse[output])) return response[output]这里有个坑chat_history只保存对话消息千万不要把中间的工具调用过程、Agent 思考过程塞进历史。工具调用的中间记录已经包含在agent_scratchpad里了AgentExecutor 在单轮执行结束后会清掉不需要永久保存。如果历史里混入这些过程模型容易被绕晕而且会浪费大量 token。长期记忆是另一个话题一般要接向量库或者数据库这里不展开。你先把chat_history机制理解透后面做持久化就很自然。4.2 流式输出与回调机制用户对“打字机效果”的需求几乎是无条件的。Agent 的流式输出比普通 LLM 调用复杂一点因为 Agent 执行过程包含“推理 - 调工具 - 再推理”多个阶段不可能一次性把 token 都吐出来。AgentExecutor提供了stream方法但它的流式输出粒度比较粗按每一步的输出块切分而不是每个 token。你在终端里直接跑for chunk in agent_executor.stream(...)大概率看到的是中间输出一段一段出现最后输出完整回答。如果你追求前端逐字输出我建议用回调函数来接管内容传递或者干脆把 Agent 的最终 LLM 调用单独拆出来做流式。回调是 LangChain 里非常强大但很多人忽略的功能。下面这个写法可以让你在工具开始执行和结束执行时打日志from langchain_core.callbacks import BaseCallbackHandler class ToolCallback(BaseCallbackHandler): def on_tool_start(self, serialized, input_str, **kwargs): print(f开始调用工具: {input_str}) def on_tool_end(self, output, **kwargs): print(f工具返回: {output}) agent_executor AgentExecutor( agentagent, toolstools, verboseFalse, callbacks[ToolCallback()], )实际项目里这些回调可以接日志系统、监控平台甚至把关键信息推到消息队列里。Agent 的“可观测性”比普通接口重要得多因为它的决策链路不可控没有日志你根本不知道它为什么答错。4.3 结构化输出让 Agent 的结果能被程序直接消费聊天场景里Agent 返回字符串就够了但如果你想把它接进业务流程比如让 Agent 根据用户描述生成一个订单对象返回字符串就必须再做一层解析麻烦还容易出错。一个比较实用的做法是在工具里直接规定返回格式为 JSON并让 Agent 在最终回答时严格按 JSON 输出。引入with_structured_output是 LangChain 最近版本里更优雅的方案它会给模型绑定一个 Pydantic 模型作为输出结构from pydantic import BaseModel, Field class OrderInfo(BaseModel): product: str Field(description商品名称) quantity: int Field(description数量) address: str Field(description收货地址) structured_llm llm.with_structured_output(OrderInfo)不过要注意这套with_structured_output和create_tool_calling_agent组合使用时对模型的输出格式有额外要求并非所有模型都兼容。如果你发现结构化输出总报错一个更稳定的兜底方案是让 Agent 先自由输出再用一个单独的 LLM 调用把文本解析成结构化数据。这样整个链路分两段任一段失败都容易排查。5. 散装经验Agent 开发中的常见坑与排查思路5.1 工具描述不清晰导致 Agent 选错工具这个坑出现的频率最高。模型选工具本质上是一个文本匹配问题用户问题里的实体和意图跟哪个工具的描述最相似它就选哪个。如果两个工具描述模糊模型就会随机选或者选了一个明显不相关的。我有一次做客服 Agent内置了“查订单”和“查物流”两个工具我在描述里都写了“获取订单相关信息”结果模型经常把物流问题直接路由给查订单工具。后来我把描述改成“调用该工具获取订单的基本信息包括商品、金额、下单时间适用于查询订单详情类问题”和“调用该工具获取包裹的物流轨迹包括运输状态、当前位置适用于查询快递到哪了类问题”路由准确率立刻提升。改关键词比改代码有效得多。排查这类问题记得打开verboseTrue看模型每一步选的什么动作。如果你发现它选了错误工具优先怀疑描述其次是参数定义最后才是模型能力。5.2 工具循环与 Token 消耗控制另一个常见现象是 Agent 陷入循环调完工具 A结果不能满足又调工具 B再调 A来回折腾好几次。这种问题在复杂业务里格外致命因为每次循环都会产生新的 token 消耗而且会在用户界面里表现为“长时间没反应”。控制循环的手段有三个方向第一工具本身要能“一次给够”。你设计工具时尽量让一次调用返回足够多的信息减少模型二次调用的欲望。比如查天气时除了当前温度把湿度、风力、天气趋势也一起返回模型就不太会再追问。第二AgentExecutor支持max_iterations参数你可以设置最大迭代次数从根源上避免死循环。比如AgentExecutor(..., max_iterations3)。第三Prompt 里可以明确交代“如果没有把握直接告诉用户信息不足”引导模型尽早结束而不是硬要调用工具。这个约束非常简单但很多人忘记写。我给生产环境的默认配置一般是max_iterations5再配合超时控制宁可让 Agent 快速失败也不能让它无限烧钱。5.3 同步阻塞、异步调用与高并发场景LangChain 的 AgentExecutor 同时支持同步和异步接口。如果你想在生产环境提供 Web 服务我强烈建议你用异步入口。我自己最早犯过一个错误FastAPI 的def端点里直接同步调用agent_executor.invoke结果每个请求都阻塞事件循环并发一上来整个服务就卡死了。后来改成async def端点内部调用await agent_executor.ainvoke()问题才解决。如果你的工具函数本身是 IO 密集型的——比如请求外部 API、查数据库——建议直接写成async def工具然后调用await astream或ainvoke。LangChain 对异步工具的支持已经很成熟了。不过有一点要注意如果你在异步 Agent 里塞了一个同步的阻塞工具比如time.sleep或者同步的requests.get它仍然会卡住整个事件循环。这个问题的常见解法是用asyncio.to_thread把阻塞调用丢到线程池或者选一个异步 HTTP 客户端库。5.4 Agent 报告异常时如何高效定位Agent 运行时报错的形态很多我遇到过最多的几种可以列成一个速查表现象常见原因排查思路Could not parse LLM output模型输出格式不符合协议检查模型是否真的支持函数调用尝试升级模型版本降低 temperature 到 0Tool not foundAgentExecutor的 tools 和 Agent 绑定时的 tools 不一致确认两处用的是同一个工具列表对象工具传参一直报参数缺失tool函数类型注解缺失或 docstring 没写参数含义给每个参数加类型注解和完整说明调用工具后模型重复问用户工具返回信息里没有包含用户问题里的关键实体检查工具内部逻辑确认有没有把输入吃全无限循环token 消耗异常工具拆分太细或 Prompt 缺少终止引导合并相关工具、加max_iterations、Prompt 加“无信息可直接告知”Agent 在verbose下输出正常但外部拿不到结果输出结构被封装在intermediate_steps里用result[output]获取最终回答不要试图直接打印整个 result这些坑我在不同项目里几乎都踩过一遍。最有效的排查工具就是verboseTrue它会打印每个步骤的输入输出是整个调试流程的下限配置。生产环境不建议开 verbose但一定要有完善的日志记录。6. 最后聊点实在的个人经验用 LangChain 搭 Agent 这件事上手容易做好很难。如果非要说有什么核心心得我会把“工具设计”排在第一Prompt 设计只能排第二。大多数 Agent 表现不好不是模型不行是工具没定义好。你把工具当成一个“对模型说话的产品”来打磨描述精准、参数规范、返回信息完整Agent 的智商立刻提升一个档次。另外我会建议你尽早接触 LangGraph。我用下来的体会是LangChain Agent 非常适合做 MVP 验证但真正要落地生产流程比如加入人工审批、条件分支、中断恢复这些控制逻辑时LangGraph 的状态图模型会舒服得多。不过这不代表你要二选一LangChain 的工具抽象和生态在 LangGraph 里依然是底层基础先啃透任意一个再切换都不吃亏。写到这里我其实最想提醒你的是不要追求把 Agent 做得“全能”。一个只绑定三五个高质量工具的 Agent远好过一个塞了五十个混乱工具的 Agent。工具越少模型的决策空间越小准确率越高。这个道理在你把项目扩展到生产环境时感受会更深。