ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LangGraph+MCP Server实战:构建具备工具调用与长期记忆的智能聊天机器人

LangGraph+MCP Server实战:构建具备工具调用与长期记忆的智能聊天机器人 简介在智能体Agent开发中工具调用与长期记忆是决定应用实用性的两大核心难题。模型上下文协议MCP通过标准化接口将外部数据源与工具能力以统一方式注入大模型成为构建可扩展Agent的关键基础设施。LangGraph作为基于图的状态编排框架则能清晰管理Agent的分支流程与状态流转实现可控的决策循环。二者结合可让开发者高效搭建具备实时查询、API调用及跨会话记忆能力的生产级聊天机器人。从基础概念到工程实践本文结合LangGraph与MCP Server在聊天机器人项目中的落地详解工具接入、状态图编排、记忆持久化及常见排错经验为Agent工程化提供完整参考。 最近在做一个基于 LangGraph Agent 和 MCP Server 的聊天机器人项目从确定方案到跑通首版前后大概用了一周。这个项目不是那种“你问我答”的玩具 demo而是把 Agent 开发里最核心的几件事都过了一遍状态图编排、工具调用、跨会话记忆、还有和本地模型的对接。期间踩了不少坑也沉淀了一套比较顺手的工程化套路。这篇就把整个项目的设计思路、核心实现和排错过程完完整整整理出来给正在做 Agent 开发、或者准备把聊天机器人往工程化方向推进的朋友做个参考。这个项目能做什么简单说它是一个具备工具调用能力、能记住上下文和用户偏好的智能聊天机器人你可以让它回答问题时自动去查天气、查数据库、调内部 API也能在下次对话时记得你上次交代过的信息。整个架构基于 LangGraph 做流程编排通过 MCP Server 把外部工具暴露给模型再配合持久化机制解决记忆问题。如果你刚开始接触 LangGraph想知道它和 LangChain 到底有什么区别如果你在纠结怎么把 MCP Server 接进自己的 Agent 项目如果你做完一个 demo 后想把它做得更工程化——这篇应该都对得上。我会把所有代码、配置和踩坑记录都展开讲不讲虚的。1. 项目整体设计为什么选 LangGraph MCP Server 这套组合1.1 这个项目要解决的三个核心问题普通的聊天机器人做出来容易做好很难。我复盘下来核心痛点集中在三件事第一工具调用。用户问“今天北京天气怎么样”模型如果手里没有工具只能凭训练数据瞎编。传统做法是在 prompt 里塞一堆 function description然后自己解析模型返回的 function call拿到参数后再调 API最后把结果拼回去让模型生成回复。这套流程代码量不小而且每接入一个新工具都要改解析逻辑、改 prompt 模板时间一长就变成一堆没人敢动的意大利面条。第二记忆。对话上下文如果只靠把历史消息全部塞进 contexttoken 消耗会爆炸而且跨会话完全失忆。用户今天说“我喜欢简洁的回复风格”明天新开一个会话再问它就什么都不记得了。真正好用的聊天机器人应该能记住用户的长期偏好。第三流程控制。真实的聊天机器人不是一条直线它是“理解意图 - 决定是否调工具 - 拿结果 - 生成回答 - 更新记忆”这样的带分支的流程。用简单的 if-else 去维护逻辑一多就完全没法看。LangGraph 解决的核心问题就是流程控制。它把 Agent 的逻辑建模成一张有向图节点是处理步骤边是状态的流转条件边让 Agent 自己决定下一步走哪里。而 MCP Server 解决的是工具接入的标准化问题它把工具调用从“每个模型一套解析方式”变成了“一个统一协议走天下”。1.2 技术选型对比LangGraph、LangChain、裸调模型 API到底怎么选这里先回答一个被问了无数次的问题LangGraph 和 LangChain 到底什么关系我项目里直接用了 LangGraph 而不是只躺在 LangChain 上因为 LangChain 本质是组件库和编排框架而 LangGraph 是基于图的状态机框架。两者可以配合使用但 LangGraph 独立也能跑而且它对复杂流程、循环、分支的控制力要强得多。我实际做选型时做过一个对比方案适合场景优点缺点LangGraph复杂 Agent 流程、需要状态管理、分支循环可控性强、可视化调试、原生支持持久化上手成本略高需要理解图的概念LangChain快速搭 RAG 对话、简单链式调用生态齐全、封装好上手快复杂流程难控制容易写出“面条代码”裸调模型 API极简 demo、一次性脚本没有任何依赖工具调用、记忆、多轮都要自己造轮子我的建议是如果只是做一个“查资料聊天”的轻量应用LangChain 足够用一旦你准备做多工具、多步骤、需要持久化的 Agent直接上 LangGraph不要犹豫。这个项目就是按后者来设计的。其实 LangGraph 里的节点函数本身也可以用 LangChain 的组件比如 ChatOpenAI、ToolNode 这些两者不是对立关系LangGraph 负责“流程怎么走”LangChain 负责“每一步具体做什么”这种组合在工程上是最舒服的。1.3 MCP Server 在整个架构里扮演什么角色MCPModel Context Protocol是一种开放协议目标是让 AI 应用访问外部数据源和工具的方式标准化。你可以把它理解成 AI 世界的 USB-C 接口只要你的工具实现了 MCP 协议任何支持 MCP 的客户端都能直接调用它而不需要针对每家大模型单独写一套工具适配层。在这个项目里MCP Server 承担的是“工具供应方”的角色。它把天气查询、数据库查询、内部 API 封装成一个个标准 toolAgent 通过 MCP 客户端动态获取这些工具的 schema在需要的时候发起调用。好处非常明显工具的注册、更新和移除都不需要改 Agent 主流程代码属于典型的“插拔式”架构。这也是我坚持用 MCP 而不是直接在 LangGraph 里硬编码工具函数的原因——后者在项目早期看起来更简单但一旦工具多起来维护成本会直线上升。2. 环境准备与基础脚手架从零搭一个能跑的 LangGraph 项目2.1 依赖安装与版本选择这个项目我用的 Python 3.113.10 也能跑但 3.11 对类型注解和异步的支持更舒服。核心依赖安装命令pip install langgraph langgraph-cli langchain-core langchain-mcp-adapters mcp几个容易踩的版本坑提前说langgraph和langchain的版本要匹配。建议装的时候直接用最新版因为 LangGraph 迭代很快API 变动也比较频繁老版本和新版本文档对不上会非常痛苦。langchain-mcp-adapters是连接 LangChain/LangGraph 和 MCP 的关键桥接包这个别漏掉。它的作用是把 MCP Server 暴露的 tools 转换成 LangChain 的 Tool 对象这样 LangGraph 的 ToolNode 才能直接使用。后面要跑本地模型的话还需要装ollama的 Python 库不过 LangGraph 走的是 OpenAI 兼容接口直接配置base_url就行。2.2 项目结构与基本配置我习惯把项目按模块拆开这样后续扩展工具和节点都不需要重构主图agent_chatbot/ ├─ agent/ │ ├─ state.py # 状态定义 │ ├─ nodes.py # 节点函数 │ └─ graph.py # 构建状态图 ├─ mcp_servers/ │ ├─ weather_server.py # 天气 MCP Server │ └─ db_server.py # 数据库 MCP Server ├─ memory/ │ └─ store.py # 长期记忆存储 ├─ config.py # 模型、服务地址等配置 └─ main.py # 入口config.py 里我集中管理模型参数和 MCP Server 地址# config.py LLM_MODEL gpt-4o-mini LLM_BASE_URL None # 默认用 OpenAI 官方 API LLM_API_KEY sk-xxxx TEMPERATURE 0.7 MCP_SERVERS { weather: { transport: stdio, command: python, args: [mcp_servers/weather_server.py], }, db: { transport: sse, url: http://localhost:8080/mcp, } }2.3 两个 MCP Server 的搭建路线Python 和 Java/Spring AI项目里我先写了一个 Python 的 MCP Server 来验证链路后来又用 Java/Spring AI 搭了第二个因为团队里有 Java 同学而且 Spring AI 对 MCP Server 的支持已经相当成熟。如果你关注过近期的 Agent 开发趋势应该知道 MCP 已经成了工具接入的主流方案Java 生态这边 Spring AI 也跟进得很快。Python 版用 FastMCP 实现代码非常短# mcp_servers/weather_server.py from mcp.server.fastmcp import FastMCP mcp FastMCP(weather-server) mcp.tool() def get_weather(city: str) - str: 查询指定城市的当前天气 return f{city} 今天晴气温25℃微风 if __name__ __main__: mcp.run()Java/Spring AI 版需要加spring-ai-starter-mcp-server依赖然后用注解暴露工具Tool(description 查询指定城市的当前天气) public String getWeather(String city) { return city 今天晴气温25℃微风; }两种方式都只是第一步让 MCP Server 跑起来。接下来才是重头戏——在 LangGraph 里把它接进 Agent 的图中这就是第三节要详细展开的内容。3. 核心实现节点编排、MCP 工具接入与长期记忆落地3.1 定义状态 State聊天机器人的“记忆载体”LangGraph 的核心抽象是状态State它会在图的节点之间传递节点每次执行都会接收当前状态、返回更新后的状态。对聊天机器人来说State 的最少字段是消息列表# agent/state.py from typing import TypedDict, Annotated from langgraph.graph.message import add_messages class ChatState(TypedDict): messages: Annotated[list, add_messages] user_profile: dict tool_results: list这里messages字段用add_messages作为 reducer它的作用是自动把新消息追加到已有消息列表中而不是覆盖。这是 LangGraph 里最常用的 reducer 模式能省掉大量手动拼接历史消息的代码。user_profile用来存放用户的偏好和画像tool_results用来记录工具调用的结果这两个字段在记忆节点里会用到。你可以这样理解 State 和节点的关系State 是整个聊天机器人的“工作台”节点是这个工作台上的一道道工序每道工序完成之后把结果放回工作台下一道工序接着用。这种设计的好处是每个节点都是纯函数式的输入输出清晰测试和调试都很方便。3.2 节点与条件路由让 Agent 自己决定什么时候调工具一个典型的聊天 Agent 图包含四个节点chat_node调用大模型生成回复如果模型认为需要工具会在回复里带上tool_callstools_node收到tool_calls之后执行对应的 MCP 工具把结果写回状态memory_node在合适时机更新长期记忆router判断下一步是走 tools 还是直接结束节点函数只是一个普通 Python 函数入参是当前状态出参是状态的部分更新。构建图的代码如下# agent/graph.py from langgraph.graph import StateGraph, END from agent.state import ChatState from agent.nodes import chat_node, tools_node, memory_node graph StateGraph(ChatState) graph.add_node(chat, chat_node) graph.add_node(tools, tools_node) graph.add_node(memory, memory_node) graph.set_entry_point(chat) graph.add_edge(chat, memory) graph.add_conditional_edges(memory, should_continue, {tools: tools, end: END}) graph.add_edge(tools, chat) # 注意memory 节点的两条出口中走 tools 的路径会经由 tools 回到 chat最终还是要回到 memory 走 end app graph.compile(checkpointercheckpointer)router 是条件路由函数它根据最后一条消息是否包含tool_calls来决定图走向# agent/nodes.py def should_continue(state: ChatState) - str: last_message state[messages][-1] if getattr(last_message, tool_calls, None): return tools return end这个设计的精妙之处在于模型读完用户输入如果判断需要外部信息就生成tool_calls图会自动把它送进 tools 执行如果不需要就直接输出最终回答。整条链路是闭合的而且因为工具执行结果会以消息形式追加到 messages模型在下一轮能看到工具返回的数据再基于这个数据生成最终回答。我在实际调试中发现条件路由是最容易出错也最容易优化的地方。比如有时候模型会连续多次调工具因为第一次的结果不够明确我就会在 router 里加一个工具调用次数的统计超过阈值就强制结束防止死循环。这个细节在第四节会细讲。3.3 用 ToolNode 接入 MCP Server 的工具LangGraph 这一侧通过langchain-mcp-adapters来动态加载 MCP Server 提供的工具这是整个项目里最值得细说的部分。我用MultiServerMCPClient一次性可以挂多个 MCP Serverfrom langchain_mcp_adapters.client import MultiServerMCPClient client MultiServerMCPClient({ weather: { transport: stdio, command: python, args: [mcp_servers/weather_server.py], encoding: utf-8, }, db: { transport: sse, url: http://localhost:8080/mcp, }, }) tools client.get_tools()配置好之后tools里就是一组带 schema 的 LangChain Tool 对象可以直接传给ToolNodefrom langgraph.prebuilt import ToolNode tool_node ToolNode(tools) graph.add_node(tools, tool_node)这段代码跑通之后Agent 就会拥有 weather 和 db 两个 MCP Server 暴露的全部工具。tools 的加载过程是在运行时动态获取的也就是说 MCP Server 新增工具后只要重启客户端进程就能自动识别不需要改 Agent 代码。这在团队协作场景下优势非常明显服务端同学加了一个新工具Agent 侧完全不需要动。3.4 长期记忆跨会话记忆与 Checkpointer 的正确用法聊到“长期记忆”很多人以为就是把对话历史存数据库其实这是两个层次的问题。第一个层次会话内记忆。LangGraph 本身通过 checkpointer 支持“会话内记忆”也就是把图上每一步的状态都持久化用thread_id来区分不同的会话。这样即使在多轮对话中间出了异常也能从某个断点恢复。一个简单用法from langgraph.checkpoint.sqlite import SqliteSaver checkpointer SqliteSaver.from_conn_string(checkpoints.sqlite) app graph.compile(checkpointercheckpointer)调用时带上thread_idconfig {configurable: {thread_id: user-123}} result app.invoke({messages: [{role: user, content: 我喜欢简洁的回复}]}, config)这个解决的是“多轮对话不能断片”的问题。第二个层次跨会话长期记忆。真正跨会话的长期记忆比如用户昨天说过“我经常出差回复请带航班信息”今天新开会话还要记得这需要额外的记忆存储。我项目里的做法是用 LangGraph 的BaseStore接口实现一个基于 Redis 的存储也可以用内存版 InMemoryStore 或者 SQLite 实现key 是用户 IDvalue 是序列化后的用户偏好和近期事实摘要。memory_node在每轮结束后做一次增量更新把新出现的用户偏好抽取出来合并进 store。抽取偏好最简单可靠的方式是把“用户原始消息 上一轮助手回复 当前画像”拼成 prompt让模型输出更新后的画像 JSON再写回 store。实测用这种“每次全量重算”的方式比试图单独抽新增项要稳得多因为模型更擅长整体理解而不是差分计算。记忆的更新频率也要控制。我一开始让每轮对话都更新记忆结果发现模型会把一些无关紧要的一次性信息也写进去反而干扰后续回答。后来改成“只有在对话出现明确的用户偏好表达时才更新”效果改善很多。3.5 本地模型实战LangGraph Ollama 的组合很多读者的场景是本地开发不想每次调云端 API。LangGraph 对接 Ollama 很顺因为 Ollama 提供了 OpenAI 兼容接口。我用 qwen2.5 系列模型时效果比较理想这里的关键在于模型本身要支持 function calling否则工具调用链路完全走不通。接入方式非常直接from langchain_openai import ChatOpenAI llm ChatOpenAI( modelqwen2.5:14b, base_urlhttp://localhost:11434/v1, api_keyollama, temperature0.7, )然后用这个 llm 替换chat_node里的模型实例即可。实测下来14B 级别的本地模型在简单工具选择上表现接近云端小模型但在复杂意图识别上还是会差一些。我的建议是正式环境仍然用云端模型本地模型主要用于开发和演示。这里插一句如果你本地装了 Ollama想快速验证 MCP 工具链路可以先拉一个 qwen2.5 的 7B 模型跑通全流程然后再切回云端模型做精细调优这样能省不少 token 费用。4. 常见问题与排查技巧实录我踩过的坑和排错方法4.1 MCP Server 工具加载失败日志污染 stdout 的坑症状客户端显示连接成功但get_tools()返回空列表或者一直卡住不动。原因排查MCP Server 启动时把日志打印到了 stdout污染了 stdio 传输通道。MCP 协议走的是 stdio 传输即通过标准输入输出传数据一旦有额外内容混进 stdout客户端解析就会失败。解决在 MCP Server 启动命令里加日志重定向或者把日志输出配置到 stderr。Python 的 logging 默认输出到 stderr但如果你在服务端代码里用了print()调试就会严重踩雷。这个问题的直观类比是协议通道是一条干净的管子你往里面丢了一堆调试信息客户端就不知道哪段是协议数据、哪段是垃圾日志了。我后来调试 MCP Server 时都直接用 logging 模块并且强制走 stderr再也没出过这个问题。4.2 Agent 死循环 / 工具反复触发症状一次简单的提问模型连续调了七八次工具最后报recursion limit错误。原因工具返回的内容格式不适合模型解析或者返回了空结果模型认为“没拿到答案”于是反复重试。解决设置recursion_limit调大但更重要的是让工具返回值足够结构化最好带明确的成功/失败标记和简短结论。在should_continue里加一个工具调用次数的上限统计超过即强制结束。本地部署的模型要检查 function calling 能力能力弱的模型很容易误触发工具调用。我实际遇到的场景是一个数据库查询工具在没查到数据时返回空字符串模型每次都拿到空结果以为工具没执行就反复调用。最后把工具返回改成{status: empty, message: 未查询到数据}这样结构化的输出问题立刻解决。4.3 图编译报错与 reducer 类型不匹配症状compile()时报InvalidUpdateError或者节点重复注册。原因add_node用了重复名称、或者返回的字典里出现了 State 中未定义的 key、又或者某个字段的 reducer 类型不对。解决State 里每个字段都要有明确的类型注解。list 字段如果要做追加必须配Annotated reducer否则 LangGraph 默认按覆盖处理。这类报错信息通常很直白顺着栈往上找是哪一步更新的字段就行。这里有一个排错心得LangGraph 在编译和运行阶段的报错信息已经做得比较友好了关键是把每一段报错和它对应的节点对应上。我遇到这类问题时的做法是先看是不是 State 字段名写错了再看是不是 reducer 用错了最后看节点函数是不是有非法的变更。4.4 长期记忆的序列化与存储坑症状往 Redis 存记忆时直接报not JSON serializable错误。原因把 LangChain 的AIMessage对象以及里面的 content 块直接塞进了 JSON 存储。正确做法是先调message.dict()或按字段拆成基础类型读取时再重建消息对象。这里还隐含一个经验不要把完整历史消息全量存进长期记忆库长期记忆只应该存“提炼后的画像和偏好”原始对话历史交给 checkpointer 管理就好。否则存储会无限膨胀而且对回答质量的提升也没有帮助。4.5 可视化调试从 LangGraph Studio 到 ECharts 节点看板调试图流程时LangGraph Studio 是比较顺手的工具安装langgraph-cli后可以启动可以看到每个节点的输入输出对理解状态流转帮助很大。但 Studio 更适合单机调试跑在服务端的时候就不好用了。我项目里额外做了一个简单的 ECharts 看板把每次运行的节点路径和时间打印出来前端用 ECharts 画成有向图用来观察 Agent 的行为轨迹。这个方案虽然不是官方功能但对排查“为什么这个分支没走到”之类的问题帮助很大。具体做法不复杂在节点函数里埋一个钩子记录当前节点名、执行时间和关键字段的摘要攒够一批后输出 JSON。前端用 ECharts 的 graph 系列渲染节点和连线就能直观看到每次对话的完整流转路径。成本很低收益却很直观。如果你在用 LangGraph 做复杂 Agent我强烈建议也做这样一个简单的观测层。4.6 一个容易被忽略的点MCP 工具名冲突如果你挂载了多个 MCP Server可能会出现两个 Server 暴露了相同名字的 tool导致get_tools()时其中一个被覆盖。解决方式是在 MCP Client 的配置里手动重命名或者在上层再做一层工具名映射。这个坑只在工具数量多的时候才会遇到但遇到一次就会让你花不少时间排查。我后来在项目里加了一个工具注册表统一管理工具名、来源 Server 和用途说明类似的命名冲突就再也没出现过。5. 项目扩展方向与工程化建议这里再聊几个我做完这个项目之后的想法也算是对项目的展望。如果你正在做类似的 Agent 项目这几个方向大概率也会遇到。第一个方向多 Agent 协作。这个项目是一个单 Agent 架构也就是一个图里只有一个大模型节点。但实际业务里经常需要“前端客服 Agent”负责理解用户意图再转给“售后 Agent”或“订单查询 Agent”。LangGraph 天然支持在同一个图里塞多个 chat 节点每个节点配置不同的 prompt 和模型节点之间通过条件路由切换。改造的时候不需要推翻现有代码只需要在图上加节点和边。第二个方向MCP Server 的权限管理和审计。当工具多起来之后哪些工具对哪些用户开放、每次工具调用是否要做审计日志都是工程上绕不开的问题。MCP 目前的生态还在快速发展阶段权限这块主要是靠上层自己做拦截我建议在 ToolNode 外层包一层装饰器统一做权限校验和日志记录。第三个方向长期记忆的向量化。我项目里用的是“提炼成画像 JSON”的方式对于简单的用户偏好足够用。但如果想让机器人记住大量历史事实文本摘要的方式会有信息损失。更好的做法是把用户历史消息分段向量化存进向量数据库在生成回答时做语义检索召回。LangChain 生态里对这块支持已经很成熟LangGraph 里加一个 retrieve 节点就能实现。这些方向我都还没有完整跑通但方向是明确的。Agent 开发最大的特点是迭代快先跑通闭环再逐步加复杂度是最稳妥的路径。6. 我的实操体会几个越早知道越好的经验聊到最后分享几个我在这周里最深切的体会。第一先把状态字段设计好再写节点代码。我一开始是先写节点再补状态结果反复重构。State 是整个图的“契约”字段没定好后面所有节点的输入输出都要跟着改。建议先画一张状态流转的草图把每个字段在哪一步被读、在哪一步被写标出来再动手写代码。第二工具返回值的结构比内容更重要。模型做“下一步决策”时主要看工具返回的文本结构和关键标记。返回{status: success, data: ...}这样的结构比返回一段含糊的文本要稳得多。我后来把所有 MCP 工具的输出都统一成 JSON 格式模型的理解准确率明显提升。第三不要试图在一张图里塞太多节点。图和代码一样可读性和可维护性是要点。如果某个子流程特别复杂把它拆成独立的子图再在主图里调用子图这样调试和扩展都会舒服很多。这个项目本身还有很多可以打磨的空间比如多轮记忆的压缩策略、工具调用的超时处理、不同模型的 fallback 机制等等。但核心骨架已经搭起来了后续在这个基础上做扩展会顺畅很多。希望这篇实操记录能给正在折腾 Agent 开发的朋友一些参考少走几个我走过的弯路。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进