第 5 章 Agent
一、Agent 介绍
通用人工智能(AGI)将是 AI 的终极形态,几乎已成为业界共识。同样,构建 Agent则是 AI 工程应用当下的“终极形态”。
将 AI 和人类协作的程度类比自动驾驶的不同阶段:

语言模型本身无法采取行动——它们只是输出文本。LangChain 的一个重要功能是创建Agent。Agent 是一种使用 LLM 作为推理引擎的系统,它决定要采取哪些行动以及这些行动的输入应该是什么。这些行动的结果可以反馈给 Agent,由 Agent 决定是否需要采取更多行动,或者是否可以完成。
与传统的固定流程链不同,Agent 具备一定的自主决策能力,更适合处理开放式、多步骤的问题。它可以拆解任务,根据任务动态决定调用哪些工具,并利用中间结果推进任务。

Agent 的核心能力/组件:
- 大模型(LLM):作为大脑,提供推理、规划和知识理解能力。
- 记忆(Memory):具备短期记忆和长期记忆,支持快速知识检索。
- 工具(Tools):调用外部工具(如API、数据库)的执行单元。
- 规划(Planning):任务分解、反思与自省框架实现复杂任务处理。
- 行动(Action):实际执行决策的能力。
- 协作:通过与其他 Agent 交互合作,完成更复杂的任务目标。

二、Tools
2.1 Tools 介绍
要构建更强大的 AI 工程应用,只有生成文本这样的“纸上谈兵”能力自然是不够的,借助工具,才能让 AI 应用的能力真正具备无限的可能。
工具封装了一个可调用函数及其输入模式。这些参数可以传递给兼容的聊天模型,从而允许模型决定是否调用工具以及调用哪些参数。在这种情况下,工具调用使模型能够生成符合指定输入模式的请求。
2.2 创建工具
一个 Tool 通常包括工具名称,工具描述,以及工具参数的类型注解。
可以通过 @tool 装饰器来创建工具。
2.2.1 举例:通过 @tool 创建工具
from langchain.tools import tool @tool def add_numbers(a: int, b: int) -> int: """Add two numbers together.""" return a + b print(f"{add_numbers.name=}\n{add_numbers.description=}\n{add_numbers.args=}") # 输出: add_numbers.name='add_numbers' add_numbers.description='Add two numbers together.' add_numbers.args={'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
2.2.2 举例:通过 @tool 的参数修改属性
from langchain.tools import tool from pydantic.v1 import BaseModel,Field class FieldInfo(BaseModel): a: int = Field(description="第一个计算参数") b: int = Field(description="第二个计算参数") @tool( name_or_callable="sum_numbers", # 工具名称 description="计算两个整数之和", # 描述 args_schema=FieldInfo # 参数模型, 使用上面定义的FieldInfo ) def add_numbers(a: int, b: int) -> int: """Add two numbers together.""" return a + b print(f"{add_numbers.name=}\n{add_numbers.description=}\n{add_numbers.args=}") # 输出: add_numbers.name='sum_numbers' add_numbers.description='计算两个整数之和' add_numbers.args={'a': {'title': 'A', 'description': '第一个计算参数', 'type': 'integer'}, 'b': {'title': 'B', 'description': '第二个计算参数', 'type': 'integer'}}
2.2.3 绑定工具
要想让大模型能够使用工具,首先需要将工具给到大模型。只需要在创建模型实例后,通过 bind_tools 方法将工具绑定到大模型即可。

(1)大模型通过分析用户需求,判断是否需要调用工具。
(2)如果需要则在响应的 additional_kwargs 参数中包含工具调用的详细信息。
(3)使用模型提供的参数执行工具。
from langchain_core.tools import tool from langchain_ollama import ChatOllama @tool def query_user_info(user_id: int) -> str: """查询用户信息""" return {1001: "张三", 1002: "李四", 1003: "王五"}[user_id] # 11.初始化大模型 llm = ChatOllama( base_url="http://127.0.0.1:11434", model="qwen3:8b" ) # 准备工具列表, 每个工具都是一个函数, 可以添加多个工具 tools = [query_user_info] # 为模型绑定工具 llm_with_tools = llm.bind_tools(tools) # 调用模型执行, 注意这里的参数是工具调用的参数,但是模型不会调用工具,只是返回了工具调用的信息 resp = llm_with_tools.invoke("帮我查询1001的用户信息") print(resp) # content='\n\n我来帮您查询1001用户的信息。\n' # additional_kwargs={'tool_calls': [{'id': '...', 'function': {'arguments': '{"user_id": 1001}', 'name': 'query_user_info'} # 返回的响应中 additional_kwargs 参数中包括了工具调用的信息,此时还没有调用工具,只是返回了要调用的工具及参数 # 手动调用工具 for tool_call in resp.tool_calls: tool_name = tool_call["name"] # 获取工具名称 tool_args = tool_call["args"] # 获取工具参数 tool_result = globals()[tool_name].invoke(tool_args) # 调用工具 print(tool_name, tool_args, tool_result)

三、构建 Agent
使用 create_agent 来创建 Agent,create_agent 使用 LangGraph 构建基于图的 Agent运行时。此 Agent 会在一个循环中反复调用模型和工具,直到某次模型输出中不再包含工具调用则结束。

使用 create_agent 创建 Agent 时,需传入模型和工具、可选地也可以传入系统提示词。这里使用自定义的天气查询函数作为工具,
from langchain.agents import create_agent from langchain_ollama import ChatOllama from nexus_rag.llm_functions import get_weather # 1.初始化大模型 llm = ChatOllama( base_url="http://127.0.0.1:11434", model="qwen3:8b" ) # 2.准备工具列表, 每个工具都是一个函数, 可以添加多个工具, 这里导入自定义查询天气的工具 tools = [get_weather] # 3.创建agent agent = create_agent(model=llm, tools=tools, system_prompt="你是一个智能助手,请根据用户输入的指令,进行相应的查询。") # 4.调用agent res = agent.invoke({"input": "查询北京今天天气"}) print(res['messages'][-1].content)

如果 Agent 执行多个步骤,这可能需要一些时间。为了显示中间进度,我们可以使用 stream 流式返回消息。
from langchain.agents import create_agent from langchain_core.messages import ToolMessage from langchain_ollama import ChatOllama from nexus_rag.llm_functions import get_weather # 1.初始化大模型 llm = ChatOllama( base_url="http://127.0.0.1:11434", model="qwen3:8b" ) # 2.准备工具列表, 每个工具都是一个函数, 可以添加多个工具, 这里导入自定义查询天气的工具 tools = [get_weather] # 3.创建agent agent = create_agent(model=llm, tools=tools, system_prompt="你是一个智能助手,请根据用户输入的指令,进行相应的查询。") # 4.调用agent # res = agent.invoke({"input": "查询北京今天天气"}) # print(res['messages'][-1].content) chunks = agent.stream({"input": "查询北京今天天气"}) for chunk in chunks: # 检查是否有工具调用结果 if "tools" in chunk: for msg in chunk["tools"]["messages"]: if isinstance(msg, ToolMessage): # 判断是否是工具调用结果,是则打印出来,ToolMessage表示工具调用结果 print(msg.content, end="", flush=True)

四、LangSmith
LangSmith 是一个用于调试、测试、评估和监控 LLM(大语言模型)应用程序的统一平台。 它由开发流行框架 LangChain 的公司打造,但它被设计用于任何 LLM 应用程序,而不仅仅是那些用 LangChain 构建的程序。
可以把它看作是 AI 应用开发生命周期的“开发者工具包”。就像软件开发者使用 GitHub、集成开发环境和调试器一样,LangSmith 为构建 LLM 应用所面临的独特挑战提供了类似的能力。
安装 langgraph 的 python 依赖
pip install langgraph "langchain[openai]" "langgraph-cli[inmem]"
4.1 langgraph_agent
4.1.1 创建一个目录,并创建如下文件
langgraph_agent/ ├── test_agent.py # test_agent 代码 └── langgraph.json # langgraph 配置文件
4.1.2 agent.py实现agent的创建
from langchain.agents import create_agent from langchain_core.messages import ToolMessage from langchain_ollama import ChatOllama from nexus_rag.llm_functions import get_weather # 1.初始化大模型 llm = ChatOllama( base_url="http://127.0.0.1:11434", model="qwen3:8b" ) # 2.准备工具列表, 每个工具都是一个函数, 可以添加多个工具, 这里导入自定义查询天气的工具 tools = [get_weather] # 3.创建agent agent = create_agent(model=llm, tools=tools, system_prompt="你是一个智能助手,请根据用户输入的指令,进行相应的查询。") # 4.调用agent res = agent.invoke({"input": "查询北京今天天气"}) print(res['messages'][-1].content) # # chunks = agent.stream({"input": "查询北京今天天气"}) # for chunk in chunks: # # 检查是否有工具调用结果 # if "tools" in chunk: # for msg in chunk["tools"]["messages"]: # if isinstance(msg, ToolMessage): # 判断是否是工具调用结果,是则打印出来,ToolMessage表示工具调用结果 # print(msg.content, end="", flush=True)
4.1.3 langgraph.json
{ "dependencies": ["."], "graphs": { "agent": "./test_agent.py:agent" }, "env": {}, "version": "0.0.1" }
注意:json文件中agent key对应的值,需要写你的agent代码文件的名称和里面agent的名称

4.1.4 运行
Windows (CMD)执行命令
cd F:\NexusKnow set PYTHONPATH=F:\NexusKnow langgraph dev
Linux下,只需要执行:
# 进入项目路径,执行
langgraph dev
启动:

4.2 在 LangSmith 中调试
如果用过 Coze 或 Dify 等工具的,那看见这个界面后会很亲切~~~
4.2.1 调试

4.2.2 点击 chat 就是对话页面

4.3 远程访问 LangSmith
之前看到的地址类似 https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024,其中baseUrl都是127.0.0.1这种内网ip,这种地址其他人是访问不到的。

4.3.1 运行远程访问启动
要想其他人访问,执行如下命令:
langgraph dev --tunnel
在启动台查看,就变成了类似这种
https://smith.langchain.com/studio/?baseUrl=https://carter-followed-fibre-gif.trycloudflare.com
如图

其他人就可以访问这个地址了。但是其他人直接访问会提示错误:
Failed to initialize Studio Please verify if the APl server is running or accessible from the browser. TypeError: Failed to fetch
4.3.2 获取公网IP添加到hosts
只时候,只需要在打开一个命令窗口:
Linux下执行:
注意这个就是上面中返回地址中代表的那一段:carter-followed-fibre-gif.trycloudflare.com

dig carter-followed-fibre-gif.trycloudflare.com
Windows下执行 :
nslookup carter-followed-fibre-gif.trycloudflare.com

任意选择一个IP地址,在hosts文件中添加:
104.16.231.132 carter-followed-fibre-gif.trycloudflare.com
然后就可以愉快的使用了
六、记忆
为了给代理添加短期记忆(线程级持久化),在创建代理时需要指定一个 checkpointer,并在调用代理时指定线程 ID。这个短期记忆的能力是借助 LangGraph 的状态和检查点实现的。
from langchain.agents import create_agent from langchain_core.messages import ToolMessage, SystemMessage, HumanMessage from langchain_ollama import ChatOllama from langgraph.checkpoint.memory import InMemorySaver from nexus_rag.llm_functions import get_weather # 1.初始化大模型 llm = ChatOllama( base_url="http://192.168.130.154:11434", model="qwen3:8b" ) # 2.准备工具列表, 每个工具都是一个函数, 可以添加多个工具, 这里导入自定义查询天气的工具 tools = [get_weather] # 3.创建agent agent = create_agent(model=llm, tools=tools, checkpointer=InMemorySaver()) # 4.调用agent # res = agent.invoke({"input": "查询北京今天天气"}) # print(res['messages'][-1].content) # 使用 create_agent(...) 创建 agent 时,它的默认输入格式是: # { # "messages": [ # ← 必须是 key="messages" 的 dict # HumanMessage(content="查询北京天气") # ] # } # todo注意这里的输入格式和上面不同,因为有系统提示词,必须是messages开头相关的,输入格式为: input_data = { "messages": [ {"role": "system", "content": "你是一个助手,请按照要求回答用户问题。"}, {"role": "user", "content": "查询北京今天天气怎么样?"} ] } chunks = agent.stream(input=input_data, config={"configurable": {"thread_id": "1"}}) for chunk in chunks: # 检查是否有工具调用结果 if "tools" in chunk: for msg in chunk["tools"]["messages"]: if isinstance(msg, ToolMessage): # 判断是否是工具调用结果,是则打印出来,ToolMessage表示工具调用结果 print(msg.content, end="", flush=True)
七、MCP
7.1 LangGraph接入外部工具技术实现方法
说到智能体开发,无论使用何种框架,有一项绕不开的核心技术,那就是MCP(Model Context Protocol)技术。
在智能体开发过程中,接入外部函数工具是至关重要的一环,而在前面的章节中说到LangChain&LangGraph技术生态中,可以非常便捷的通过@tool装饰符来自定义一个外部函数:

或者也可以借助LangChain丰富的、数以百计的内置工具,三行代码即可在智能体中进行工具调用。
| 功能类别 | 工具名称 | 简要说明 |
|---|---|---|
| 🔎 搜索工具 | TavilySearchResults | 快速搜索实时网络信息 |
| SerpAPIWrapper | 基于 SerpAPI 的搜索结果工具 | |
| GoogleSearchAPIWrapper | 调用 Google 可编程搜索引擎 | |
| 🧠 计算工具 | PythonREPLTool | 执行 Python 表达式并返回结果 |
| LLMMathTool | 结合 LLM 和数学推理能力 | |
| WolframAlphaQueryRun | 基于 Wolfram Alpha 的计算引擎 | |
| 🗂 数据工具 | SQLDatabaseToolkit | 构建 SQL 数据库查询工具集 |
| PandasDataframeTool | 用于在 Agent 中操作表格数据 | |
| 🌐 网络/API | RequestsGetTool / RequestsPostTool | 执行 HTTP 请求 |
| BrowserTool / PlaywrightBrowserToolkit | 自动化网页浏览与抓取 | |
| 💾 文件处理 | ReadFileTool | 读取本地文件内容 |
| WriteFileTool | 写入文本到指定文件中 | |
| 📚 检索工具 | FAISSRetriever | 基于向量的文档检索工具 |
| ChromaRetriever | 使用 ChromaDB 的检索器 | |
| ContextualCompressionRetriever | 上下文压缩检索器,适合长文档 | |
| 🧠 LLM 工具 | ChatOpenAI / OpenAIFunctionsTool | 使用 OpenAI 模型作为工具调用 |
| ChatAnthropic | Anthropic Claude 模型封装工具 | |
| 🔧 自定义工具 | @tool 装饰器 | 任意函数可封装为 Agent 可调用工具 |
| Tool 类继承 | 自定义更复杂逻辑的工具实现 |
调用搜索工具实现agent代码:
from langchain.agents import create_agent from langchain_ollama import ChatOllama from langchain_tavily import TavilySearch import dotenv dotenv.load_dotenv() # 1.初始化Tavily搜索工具 # 登录网站,获取key: https://app.tavily.com/home search_tools = TavilySearch(tavily_api_key=os.getenv("TAVILY_API_KEY")) # 2.准备工具列表, 每个工具都是一个函数, 可以添加多个工具 tools = [search_tools] # 3.创建agent agent = create_agent( model=ChatOllama(model="qwen3:8b"), tools=tools ) # 4.调用agent res = agent.invoke({"messages": [{"role": "user", "content": "请帮我搜索有哪些开源的基于langchain1.0实现的多模态rag项目"}]}) print(res['messages'][-1].content)
7.2 MCP技术概述
实际开发Agent的过程中,外部函数工具的开发会占用大量的开发者的时间精力。
而与此同时,人们发现,很多外部工具的功能其实是通用的,例如查询时间、查询天气、网络搜索、操作本地文件夹等等等等,如果有一种规范,能够减少重复造轮子的时间,一个人开发完成后全体开发者都能共享,那么整体的研发效率都将得到大幅提高。
在这一设想下,MCP技术诞生了。MCP的全称是Model Context Protocol,模型上下文协议,由Claude母公司Anthropic于2025年11月正式提出。
Model Context Protocol(MCP,模型上下文协议)是一个开源协议,它标准化了大语言模型与外部工具和数据源通信的方式,允许开发者和工具提供商只需集成一次,就能与任何兼容 MCP 的系统交互。MCP 就像 USB-C 标准:不需要为每个设备使用不同的连接器,而是使用一个端口来处理多种类型的连接。


7.2.1 MCP技术定位与技术价值介绍
我们可以将MCP技术简单理解为智能体外部工具开发的一种通用规范(范式)。举个例子,以天气查询工具为例,在MCP技术诞生之前,要给大模型添加查询天气的功能,至少需要经历这么两个开发阶段:
-
阶段一:编写查询天气的外部函数,
- 阶段二:将外部工具进行进一步封装,以适配不同的开发框架。
然后再分别接入
- 接入谷歌ADK时
- 接入LangGraph时:
- 接入OpenAI Agents SDK时:
这就使得实际开发Agent的过程中,外部函数工具的开发会占用大量的开发者的时间精力。而与此同时,人们发现,很多外部工具的功能其实是通用的,例如查询时间、查询天气、网络搜索、操作本地文件夹等等等等,如果有一种规范,能够减少重复造轮子的时间,一个人开发完成后全体开发者都能共享,那么整体的研发效率都将得到大幅提高。
在这一设想下,MCP技术诞生了。MCP的全称是Model Context Protocol,模型上下文协议,由Claude母公司Anthropic于去年11月正式提出。该技术核心目标,就是创建一种统一的大模型调用外部工具的通信规范,相当于这种标准的通信规范,一项特定功能的外部函数,只需要开发一次,就能被各种不同类型的Agent开发框架所识别。例如同样是查询天气,如果我们遵循MCP技术协议开发一个查询天气的外部函数,那么接下来全体开发者就都能直接用我开发好的这个天气查询工具,带入任何智能体开发框架,快速搭建智能体应用了。
通过下面这组图能够非常清楚的解释MCP工具在智能体开发过程中实际带来的提效的作用。


7.2.2 MCP技术架构
不过呢,要做到上面说的这种“车同轨、书同文”的标准化工作,不仅需要制定一套让所有人都信服的标准,而且还需要经过时间的检验,同时还需要有足够多的用户,这个标准才能真正被市场所认同。因此MCP技术也历经了一段时间的沉淀和打磨,自2024年11月发布开始,到2025年3月技术大爆发,再到第二季度开始越来越多的Agent框架和热门应用宣布支持MCP技术,MCP才算是逐渐成为一项智能体开发的通用协议。
截止目前,MCP的技术生态可以划分为三层,最底层是协议层,也就是“文字版”的规定、或者说规范,而为了普及这一规范,让更多的人更加快速的完成自己的MCP工具开发,Anthropic进一步的提供了MCP开发工具,借助这些SDK,我们能够非常快速完成MCP工具开发。而既然是一种标准化的协议,其核心价值就在于用的人足够多、同时分享的人也足够多,才能真正减少“重复造轮子”的时间,因此Anthropic官方和很多第三方平台,也在积极的推进MCP技术生态的构建,尤其是MCP工具平台的建设,通过鼓励开发者更多的分享自己开发的MCP工具,只有形成了更大的(分享和引用的)协作规模,MCP的技术才能更有价值。
7.2.3 MCP SDK与MCP技术生态
而在这些MCP完整的技术架构中,开发者尤其需要关注MCP的SDK(开发工具)和MCP技术生态。所谓MCP的SDK,指的是官方提供的用于开发MCP工具的第三方库,截至目前,MCP SDK已支持Python、TypeScript、Java、Kotlin和C#等编程语言进行客户端和服务器创建。
SDK文档:https://github.com/modelcontextprotocol

而借助这些库(sdk),仅需几行代码,即可快速构建一个MCP工具。下面是利用Python MCP SDK实现
# pip install mcp from mcp.server.fastmcp import FastMCP # 创建 MCP 实例 mcp = FastMCP("Demo") # 为 MCP 实例添加工具 @mcp.tool() def add(a: int, b: int) -> int: return a + b # 为 MCP 实例添加资源 @mcp.resource("greeting://default") def get_greeting() -> str: return "Hello from static resource!" # 为 MCP 实例添加提示词 @mcp.prompt() def greet_user(name: str, style: str = "friendly") -> str: styles = { "friendly": "写一句友善的问候", "formal": "写一句正式的问候", "casual": "写一句轻松的问候", } return f"为{name}{styles.get(style, styles['friendly'])}" if __name__ == "__main__": # mcp.settings.host = "0.0.0.0" # mcp.settings.port = 8888 mcp.run(transport="streamable-http") # 默认启动在 127.0.0.1:8000
反之,如果没有这些MCP开发工具,想要开发MCP工具,就必须从MCP技术协议出发,借助其他库来完成开发,其实现难度非常大。
而如果我们并不需要开发MCP工具,而只想要借助现成的MCP工具快速完成智能体开发,那么就需要重点关注现在的MCP集成平台,也就是集成了各类目前非常流行的MCP工具的平台,借助这些平台,我们能够快速找到想要的MCP服务,然后根据指示说明快速进行接入,甚至伴随着MCP流式HTTP功能的上线,很多平台还提供了这些MCP工具后端运行服务,我们只需要输入指定的后端地址,就能调用运行在云端的MCP工具。
主流的MCP集成平台如下:
- MCP官方服务器合集:https://github.com/modelcontextprotocol/servers
- MCP Github热门导航:https://github.com/punkpeye/awesome-mcp-servers
- Smithery:https://smithery.ai/
- MCP导航:https://mcp.so/
- 阿里云百炼:https://bailian.console.aliyun.com/?tab=mcp
- 魔搭社区MCP广场:https://www.modelscope.cn/mcp
- mcp.run:https://www.mcp.run/
7.3 MCP核心技术概念
而在正式开始MCP智能体开发之前,我们还需要补充两个MCP技术体系中至关重要的技术概念,分别是:MCP客户端与服务器、以及两大类MCP工具运行模式。
7.3.1 MCP客户端与服务器
由于MCP是一种围绕大模型外部函数工具创建的统一范式,因此MCP工具从诞生之初就是客户端(client)与服务器(server)分离的架构。服务器与客户端的技术概念可以借助MySQL这个通用的数据库软件进行理解,在MySQL中,服务器指的是数据库实际运行环境,例如公司内部的某个统一用于数据存储的物理机,而客户端,则指的是SQL编写和运行的环境,可以是比如数据分析师用的笔记本。每次要进行查数时,数据分析师就可以在自己的笔记本上运行MySQL WorkBench(一个SQL编程的IDE),然后借助MySQL客户端,给公司的MySQL服务器发送查数的请求。
类似的,所谓MCP Server(服务器),指的是MCP工具运行的环境,而MCP Client(客户端),则指的是能够调用MCP工具、或者说给MCP工具发送请求并接受结果的环境。二者关系如图所示:

这种服务器和客户端分离的架构的好处,就在于可以更加便捷的进行模块化开发和维护,而此前我们所说的MCP工具,其实就指的是MCP服务器,而那些MCP工具集合,其实就是MCP服务器集合网站。
同时,基于这种技术划分,当我们在开发一个智能体,并希望这个智能体能够接入MCP工具时,其实从MCP技术角度来说,我们本质上是开发一个MCP的客户端(Client)。例如当我们基于LangChain开发一个接入MCP工具的智能体,其实我们就开发了一个基于LangChain的MCP客户端。而现在也有很多大模型聊天工具允许接入MCP工具,例如Claude Desktop、Cherry Studio等,这些也都是MCP客户端。
7.3.2 标准MCP工具接入客户端流程
这里以ChatBox为例,为大家展示一个标准的MCP客户端接入MCP服务器的基本流程,从中我们能够看出,填写对应的配置文件,是运行MCP工具的关键。
1.现在打开:https://www.modelscope.cn/mcp/servers/@Joooook/12306-mcp,获取12306的mcp
{ "mcpServers": { "12306-mcp": { "args": [ "-y", "12306-mcp" ], "command": "npx" } } }
2.复制上面配置,填入

3.测试

7.3.3 MCP离线运行与在线运行模式
既然是服务器和客户端分离的架构,那么MCP肯定支持两种调用方法,其一是本地运行,也被成为离线运行,指的是MCP服务器和MCP客户端在同一台电脑上进行运行,其二则是在线运行,或者异地部署运行,指的是MCP服务器在远程服务器或者云端运行,然后借助HTTP网络通信来进行响应。
而这也就对应着MCP工具调用的两种核心模式,分别是stdio(本地)模式调用和SSE&streamable HTTP(在线)模式调用,各调用方法效果对比如下所示:
| 特性 | Stdio | SSE | Streamable HTTP |
|---|---|---|---|
| 通信方向 | 双向(但仅限本地) | 单向(服务器到客户端) | 双向(适用于复杂交互) |
| 使用场景 | 本地进程间通信 | 实时数据推送,浏览器支持 | 跨服务、分布式系统、大规模并发支持 |
| 支持并发连接数 | 低 | 中等 | 高(适合大规模并发) |
| 适应性 | 局限于本地环境 | 支持浏览器,但单向通信 | 高灵活性,支持流式数据与请求批处理 |
| 实现难度 | 简单,适合本地调试 | 简单,但受限于浏览器兼容性和长连接 | 复杂,需处理长连接和流管理 |
| 适合的业务类型 | 本地命令行工具,调试环境 | 实时推送,新闻、股票等实时更新 | 高并发、分布式系统,实时交互系统 |
- Stdio 传输:适合本地进程之间的简单通信,适合命令行工具或调试阶段,但不支持分布式。
- SSE 传输:适合实时推送和客户端/浏览器的单向通知,但无法满足双向复杂交互需求。
- Streamable HTTP 传输:最灵活、最强大的选项,适用于大规模并发、高度交互的分布式应用系统,虽然实现较复杂,但能够处理更复杂的场景。
而伴随着2025年5月MCP更新了Streamable HTTP的SDK,目前越来越多的MCP工具都选择采用Streamable HTTP形式进行部署和运行。
7.3.4 离线MCP工具托管平台
当然,如果是调用在线MCP服务,开发者只能了解其功能,而如果是离线的MCP服务,则对应的MCP工具是完全开源的,在每次调用之前,开发者都需要将其源码先下载到本地,然后再运行。例如上面的章节12306mcp,我们使用npx命令,其本质就是先将指定的MCP工具下载到本地,然后在有需要的时候对其进行调用。例如: 12306MCP配置文件如下:
{ "mcpServers": { "12306-mcp": { "args": [ "-y", "12306-mcp" ], "command": "npx" } } }
代表的含义就是我们需要先使用如下命令:
npx -y 12306-mcp
对这个库12306-mcp进行下载,然后在本地运行,当有必要的时候调用这个库里面的函数执行相关功能。而这个12306-mcp库是一个托管在https://www.modelscope.cn/mcp上的库,

除此此外,一些由Python编写的MCP开源工具,则托管在pypi平台上,https://pypi.org/ ,每次运行的时候我们需要填写uvx命令,其本质就是将工具源码从pypi平台上下载到本地再来进行运行。
7.3.5 MCP在线托管服务
伴随着MCP快速调用的请求不断增加,也有很多平台提供了在线托管服务模式,例如魔搭社区的MCP广场中,就有很多是运行在魔搭社区服务器上的MCP工具,开发者可以直接使用SSE或者流式HTTP方式请求调用在线的MCP工具,从而快速完成开发工作。

7.4 MCP 架构
MCP 遵循客户端-服务器架构,架构中包括:
|
MCP 主机 |
协调和管理一个或多个 MCP 客户端的 AI 应用 |
|
MCP 客户端 |
一个保持与 MCP 服务器连接的组件,通过 MCP 定义的消息处理通信,从服务器查找并请求资源和工具,并管理与服务器的连接生命周期 |
|
MCP 服务器 |
一个向 MCP 客户端提供服务的程序,通过协议暴露工具、资源和提示模板功能 |

7.5 MCP 层级
MCP 分为两个层级:
⑴.数据层
数据层实现了一个基于 JSON-RPC 2.0 的交换协议,该协议定义了消息结构和语义。
数据层包括生命周期管理(连接初始化、能力协商、连接终止)、服务器功能(提供工具、资源和提示模板)、客户端功能(调用LLM、获取输入、记录消息)、其他功能(实时更新通知、长时运行操作跟踪)。
⑵.传输层
传输层定义了客户端与服务器之间数据交换的通信机制和通道,包括特定传输方式的连接建立、消息帧定界和授权。
MCP 支持多种传输机制,包括 Stdio、Streamable HTTP、SSE。
|
Stdio |
使用标准输入和输出流,与在终端输入命令并看到响应时使用的机制相同。适用于本地开发 |
|
Streamable HTTP |
该传输使用 HTTP POST 和 GET 请求,服务器可以选择使用SSE来流式传输多个服务器消息。支持流式传输和服务器到客户端通知,并支持标准 HTTP 身份验证方法,包括授权令牌、API 密钥和自定义头信息 |
|
SSE |
带有 SSE(Server-Sent Events 服务器发送事件)的 HTTP,MCP早期传输机制,现逐渐被 Streamable HTTP 取代 |
7.6 MCP 工作流程
⑴.初始化
在初始化过程中,AI 应用程序的 MCP 客户端管理器连接到配置的服务器,并将它们的能力存储起来以供后续使用。应用程序使用这些信息来确定哪些服务器可以提供特定类型的功能(工具、资源、提示),以及它们是否支持实时更新。
初始化有几个重要的作用:
|
协议版本协商 |
确保客户端和服务器使用兼容的协议版本,避免因版本不一致导致的通信问题 |
|
能力发现 |
声明各自支持的功能,包括他们能够处理的基元类型(工具、资源、提示)以及是否支持通知等特性 |
|
身份交换 |
交换客户端与服务器的身份及版本信息,便于后续的调试与兼容性管理 |
⑵.工具发现
AI 应用程序从所有连接的 MCP 服务器中获取可用工具,并将它们组合成一个语言模型可以访问的统一工具注册表。这使得 LLM 能够理解它可以执行哪些操作,并在对话期间自动生成相应的工具调用。
连接建立之后,客户端可以通过发送 tools/list 请求来发现可用的工具。这个请求是 MCP 工具发现机制的基础—它允许客户端在尝试使用工具之前了解服务器上有哪些可用的工具。响应包含一个 tools 数组,该数组提供了关于每个可用工具的全面元数据。这种基于数组的结构允许服务器同时展示多个工具,同时保持不同功能之间的清晰界限。响应中的每个工具包括几个关键字段:
|
name |
工具标识符 |
|
title |
工具的易读显示名称 |
|
description |
工具描述 |
|
inputSchema |
一个定义预期输入参数的 JSON Schema,支持类型验证并提供关于必需和可选参数的清晰文档 |
⑶.工具执行
当语言模型在对话中决定使用工具时,AI 应用程序会拦截工具调用,将其路由到合适的 MCP 服务器,执行该工具,并将结果作为对话流程的一部分返回给 LLM。这使 LLM 能够访问实时数据并在外部世界中执行操作。
客户端使用 tools/call 方法执行一个工具。tools/call 请求遵循结构化格式,确保客户端和服务器之间的类型安全和清晰通信。请求结构包括几个重要组件:
|
name |
工具标识符 |
|
arguments |
包含工具的 inputSchema 定义的输入参数 |
响应返回一个内容对象数组,允许进行丰富、多格式的响应(文本、图片、资源等)。每个内容对象都有一个 type 字段。
⑷.实时更新
MCP 支持实时通知,使服务器能够在未经明确请求的情况下通知客户端有关变更。当 AI 应用程序收到关于工具变更的通知时,它会立即刷新其工具注册表并更新 LLM 的可用功能。这确保了正在进行的对话始终能够访问最新的一组工具,并且 LLM 可以随着新功能的可用而动态适应。
7.7 MCP SDK
7.7.1 Stdio 服务端与客户端
可通过 mcp 包来简单创建 Stdio 服务器。
- 服务端 mcp_server_stdio.py:
# pip add mcp from mcp.server.fastmcp import FastMCP # 创建 MCP 实例 mcp = FastMCP("Demo") # 为 MCP 实例添加工具 @mcp.tool() def add(a: int, b: int) -> int: return a + b # 为 MCP 实例添加资源 @mcp.resource("greeting://default") def get_greeting() -> str: return "Hello from static resource!" # 为 MCP 实例添加提示词 @mcp.prompt() def greet_user(name: str, style: str = "friendly") -> str: styles = { "friendly": "写一句友善的问候", "formal": "写一句正式的问候", "casual": "写一句轻松的问候", } return f"为{name}{styles.get(style, styles['friendly'])}" if __name__ == "__main__": mcp.run(transport="stdio")
- 客户端 mcp_client_stdio.py
import asyncio from mcp.client.stdio import stdio_client from mcp import ClientSession, StdioServerParameters async def stdio_run(): server_params = StdioServerParameters( command=r"D:\Anaconda3\envs\ai_llm\python.exe", # 指定python解释器, 默认为python,系统多个解释器时,请指定具体的解释器路径 args=["mcp_server_stdio.py"], ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: # 初始化连接 await session.initialize() # 获取可用工具 tools = await session.list_tools() print(tools) print() # 调用工具 call_res = await session.call_tool("add", {"a": 1, "b": 2}) print(call_res) print() # 获取可用资源 resources = await session.list_resources() print(resources) print() # 调用资源 read_res = await session.read_resource("greeting://default") print(read_res) print() # 获取可用提示 prompts = await session.list_prompts() print(prompts) print() # 调用提示 get_res = await session.get_prompt("greet_user", {"name": "Jack"}) print(get_res) print() asyncio.run(stdio_run())
7.7.2 Streamable HTTP 服务端与客户端
- 服务端 mcp_server_streamablehttp.py:
# pip add mcp from mcp.server.fastmcp import FastMCP # 创建 MCP 实例 mcp = FastMCP("Demo") # 为 MCP 实例添加工具 @mcp.tool() def add(a: int, b: int) -> int: return a + b # 为 MCP 实例添加资源 @mcp.resource("greeting://default") def get_greeting() -> str: return "Hello from static resource!" # 为 MCP 实例添加提示词 @mcp.prompt() def greet_user(name: str, style: str = "friendly") -> str: styles = { "friendly": "写一句友善的问候", "formal": "写一句正式的问候", "casual": "写一句轻松的问候", } return f"为{name}{styles.get(style, styles['friendly'])}" if __name__ == "__main__": # mcp.settings.host = "0.0.0.0" # mcp.settings.port = 8888 mcp.run(transport="streamable-http") # 默认启动在 127.0.0.1:8000
- 客户端 mcp_client_streamablehttp.py:
import asyncio from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async def streamablehttp_run(): url = "http://127.0.0.1:8000/mcp" headers = {"Authorization": "Bearer sk-atguigu"} async with streamablehttp_client(url, headers) as (read, write, _): async with ClientSession(read, write) as session: # 初始化连接 await session.initialize() # 获取可用工具 tools = await session.list_tools() print(tools) print() # 调用工具 call_res = await session.call_tool("add", {"a": 1, "b": 2}) print(call_res) print() # 获取可用资源 resources = await session.list_resources() print(resources) print() # 调用资源 read_res = await session.read_resource("greeting://default") print(read_res) print() # 获取可用提示 prompts = await session.list_prompts() print(prompts) print() # 调用提示 get_res = await session.get_prompt("greet_user", {"name": "Jack"}) print(get_res) print() asyncio.run(streamablehttp_run())
7.7.3 将多个 Streamable HTTP 服务器挂载到 ASGI 服务器
ASGI(Asynchronous Server Gateway Interface)是 Python 的 异步 Web 服务器接口标准,定义了服务器与应用之间的通信协议,支持异步调用,能够处理高并发和长连接。
可以使用 streamable_http_app 方法将 StreamableHTTP 服务器挂载到现有的 ASGI 服务器。这允许将 StreamableHTTP 服务器与其他 ASGI 应用程序集成。
# pip add mcp fastapi import uvicorn import contextlib from fastapi import FastAPI from mcp.server.fastmcp import FastMCP # 创建 MCP 实例 tool_mcp = FastMCP("tool server") resource_mcp = FastMCP("resource server") prompt_mcp = FastMCP("prompt server") # 为 tool_mcp 实例添加工具 @tool_mcp.tool() def add(a: int, b: int) -> int: return a + b # 为 resource_mcp 实例添加资源 @resource_mcp.resource("greeting://default") def get_greeting() -> str: return "Hello from static resource!" # 为 prompt_mcp 实例添加提示词 @prompt_mcp.prompt() def greet_user(name: str, style: str = "friendly") -> str: styles = { "friendly": "写一句友善的问候", "formal": "写一句正式的问候", "casual": "写一句轻松的问候", } return f"为{name}{styles.get(style, styles['friendly'])}" # 设置 MCP 的 HTTP 根路径 tool_mcp.settings.streamable_http_path = "/" resource_mcp.settings.streamable_http_path = "/" prompt_mcp.settings.streamable_http_path = "/" # 创建一个组合生命周期来管理会话管理器 @contextlib.asynccontextmanager async def lifespan(app: FastAPI): async with contextlib.AsyncExitStack() as stack: await stack.enter_async_context(tool_mcp.session_manager.run()) await stack.enter_async_context(resource_mcp.session_manager.run()) await stack.enter_async_context(prompt_mcp.session_manager.run()) yield app = FastAPI(lifespan=lifespan) # 挂载 MCP 服务器 app.mount("/tool", tool_mcp.streamable_http_app()) app.mount("/resource", resource_mcp.streamable_http_app()) app.mount("/prompt", prompt_mcp.streamable_http_app()) if __name__ == "__main__": uvicorn.run(app)
客户端代码和之前一致,注意修改 URL 路径。
7.8、LangGraph搭建MCP客户端流程
接下来进入到实操环节,正式为大家介绍如何将MCP工具接入LangGraph中并创建智能体。
7.8.1 创建自定义MCP工具
作为大模型开发者,掌握MCP工具开发流程是基本功,这里我们先尝试自定义MCP工具,并将其接入LangGraph。对于一个完整的MCP项目来说,要有完整的项目代码结构、以及符合MCP服务器基本调用规范。具体项目创建流程如下:
Step 1. 借助uv创建Python项目
MCP开发要求借助uv进行虚拟环境创建和依赖管理。uv 是一个Python 依赖管理工具,类似于 pip 和 conda,但它更快、更高效,并且可以更好地管理 Python 虚拟环境和依赖项。它的核心目标是替代 pip、venv 和 pip-tools,提供更好的性能和更低的管理开销。
uv 的特点:
- 速度更快:相比 pip,uv 采用 Rust 编写,性能更优。
- 支持 PEP 582:无需 virtualenv,可以直接使用 __pypackages__ 进行管理。
- 兼容 pip:支持 requirements.txt 和 pyproject.toml 依赖管理。
- 替代 venv:提供 uv venv 进行虚拟环境管理,比 venv 更轻量。
- 跨平台:支持 Windows、macOS 和 Linux。
首先使用pip安装uv:
pip install uv
然后按照如下流程创建项目主目录:
# 创建项目目录 uv init mcp-get-weather cd mcp-get-weather
然后输入如下命令创建虚拟环境:
# 创建虚拟环境 uv venv # 激活虚拟环境 source .venv/bin/activate
此时这个.venv文件就负责保存当前虚拟环境的各项依赖。
| 文件/文件夹 | 作用 |
|---|---|
.git/ |
Git 版本控制目录 |
.venv/ |
虚拟环境 |
.gitignore |
Git 忽略规则 |
.python-version |
Python 版本声明 |
main.py |
主程序入口 |
pyproject.toml |
项目配置文件 |
README.md |
项目说明文档 |
Step 2. 添加项目依赖
接下来继续使用uv工具,为我们的项目添加基础依赖。根据此前的代码解释不难看出,当前项目主要需要用到httpx、dotenv、langgraph、langchain-ollama和langchain_mcp_adapters等核心库,我们可以使用如下命令安装相关依赖,并同时安装mcp sdk:
# 安装 MCP SDK uv add mcp httpx dotenv langgraph langchain-ollama langchain-mcp-adapters
注意,对于uv管理库来说,相关依赖会安装到.venv文件中,并不会和系统库产生冲突。
Step 3.编写MCP服务器
接下来继续创建MCP服务器,为了更好的模拟真实场景,这里我们创建多个MCP服务器。
-
查询天气服务器weather_server.py
-
用于进行天气信息查询的服务器,完整代码如下
import json import os import aiohttp from mcp.server.fastmcp import FastMCP # 初始化MCP服务器 mcp = FastMCP(name="WeatherServer", host='0.0.0.0', port=8080) class WeatherInterface: def _strip_suffix(self, name: str) -> str: """去除城市名末尾的常见行政区后缀""" suffixes = ["特别行政区", "自治区", "自治州", "自治县", "县级市", "市", "县", "区"] for suffix in suffixes: if name.endswith(suffix): return name[:-len(suffix)] # 删除后缀, 返回新的字符串,-len(suffix)表示删除末尾的几个字符 return name async def get_weather(self, location): """ 根据城市、日期查询天气信息 :param location: 城市,如:北京、上海 :return: 天气信息 """ # 读取城市编码json文件,获取城市编码 # 获取项目根路径,不是文件路径 project_root = os.path.dirname(os.path.abspath(__file__)) # 路径拼接 city_json_path = os.path.join(project_root, "data", "weather_json", "city_code.json") with open(city_json_path, "r", encoding="utf-8") as file: city_data = json.load(file) # 将用户输入和 JSON 中的城市名都标准化后再比较 input_clean = self._strip_suffix(location.strip()) city_code = None # 查询的城市编码 for city_info in city_data: city_name = city_info["city_name"] # json中的城市名 city_name_clean = self._strip_suffix(city_name) # 将城市名进行标准化 if city_name_clean == input_clean: # 如果标准化后的城市名相等,则返回城市编码 city_code = city_info["city_code"] break if city_code is None: # 如果城市编码为None,则返回 return f"❌ 未找到城市:{location}, 请确定是否输入错误!" # 拼接url url = f"http://t.weather.itboy.net/api/weather/city/{city_code}" # TODO requests发起请求 # result = requests.get(url).json() # TODO aiohttp发起请求 async with aiohttp.ClientSession() as session: async with session.get(url) as response: if response.status == 200: result = await response.json() else: result = f"❌ 获取天气信息失败,HTTP状态码:{response.status}" return result @mcp.tool(title="在线实时天气查询", description="查询指定城市的天气预报") async def get_weather(location: str, date: str = "今天") -> str: """ 查询指定城市的天气预报 支持: - 单日:今天、明天、后天 - 7天:未来一周、这周、接下来几天 - 15天:未来15天、未来半个月、未来几周 示例: - location="北京", date="今天" - location="上海", date="明天" - location="广州", date="未来15天" - location="重庆", date="未来一周" - location="高碑店市",data="今天" :param location: 城市名称 :param date: 时间日期描述,默认"今天" :return: 天气信息摘要 """ try: # TODO 异步方式 调用天气接口 weather_interface = WeatherInterface() data = await weather_interface.get_weather(location) print(f"data的值输出如:{data}") # 获取天气信息查询接口是否正确 # if data.get("status") != 200: # return f"❌ 天气接口错误,请稍后再试!" # 获取返回的本次查询的城市 city_name = data.get("cityInfo").get("city").replace("市", "") # 将城市名中的“市”去掉 forecast = data.get("data").get("forecast") # 获取天气预报 # 判断查询的类型 date_lower = date.lower() # 将时间日期描述转为小写 # 未来15天 if any(kw in date_lower for kw in ["15天", "十五天", "半个月", "未来两周", "未来15天"]): # 判断查询的类型,any判断是否包含某个元素,如果满足条件,则返回True davs = min(15, len(forecast)) # 获取天气预报的条数,并取最小值 lines = [f"{city_name}未来{davs}天天气预报:"] # forecast[:davs]表示获取天气预报的条数,并取最小值 for i, forecast_day in enumerate(forecast[:davs]): ymd = forecast_day.get("ymd") week = forecast_day.get("week") weather_type = forecast_day.get("type") fx = forecast_day.get("fx") f1 = forecast_day.get("f1") high = forecast_day.get("high").replace("高温", "") low = forecast_day.get("low").replace("低温", "") # 获取摘要 notice = forecast_day.get("notice") # 组合信息 lines.append(f"{i + 1}.{ymd} ({week}): {weather_type}, {low} ~ {high}, {fx}, {f1}, 💡 {notice} ") return "\n".join(lines) elif any(kw in date_lower for kw in ["一周", "这周", "接下来", "本周", "7天", "七天"]): # 判断查询的类型,any判断是否包含某个元素,如果满足条件,则返回True davs = min(7, len(forecast)) # 获取天气预报的条数,并取最小值 lines = [f"{city_name}未来{davs}天天气预报:"] # forecast[:davs]表示获取天气预报的条数,并取最小值 for i, forecast_day in enumerate(forecast[:davs]): ymd = forecast_day.get("ymd") week = forecast_day.get("week") weather_type = forecast_day.get("type") fx = forecast_day.get("fx") f1 = forecast_day.get("f1") high = forecast_day.get("high").replace("高温", "") low = forecast_day.get("low").replace("低温", "") # 获取摘要 notice = forecast_day.get("notice") # 组合信息 lines.append(f"{i + 1}.{ymd} ({week}): {weather_type}, {low} ~ {high}, {fx}, {f1}, 💡 {notice} ") return "\n".join(lines) else: day_offset = 0 if "明天" in date_lower: day_offset = 1 elif "后天" in date_lower: day_offset = 2 if day_offset >= len(forecast): return f"❌ 暂不支持查询{date}的天气" # 根据输入的条件获取天气预报 target = forecast[day_offset] # 获取当前温度 current_temp = data.get("data").get("wendu") # 获取当前湿度 current_humidity = data.get("data").get("shidu") # 获取空气质量 aqi = data.get("data").get("quality") # 获取健康建议 health_suggestion = data.get("data").get("ganmao") ymd = target.get("ymd") week = target.get("week") weather_type = target.get("type") wind = f"{target['fx']} {target['fl']}" high = target.get("high").replace("高温", "") low = target.get("low").replace("低温", "") # 获取摘要 notice = target.get("notice") if day_offset == 0: return ( f"📍 {city_name} 今日({week})天气({ymd})\n" f"🌡️ 实时温度:{current_temp}℃\n" f"🌤️ 天气:{weather_type} | 💨 {wind}\n" f"📊 温度:{low} ~ {high}\n" f"💧 湿度:{current_humidity} | 🌫️ 空气质量:{aqi}\n" f"💡 {notice}\n" f"🤧 {health_suggestion}" ) else: day_str = ["今天", "明天", "后天"][day_offset] return ( f"📍 {city_name} {day_str}({week})天气({ymd})\n" f"🌤️ {weather_type} | 💨 {wind}\n" f"📊 {low} ~ {high}\n" f"💡 {notice}" ) except Exception as e: return f"❌ 天气接口错误,请稍后再试!" if __name__ == "__main__": print("🌤️ Weather MCP server starting on http://127.0.0.1:8080/mcp") mcp.run(transport="streamable-http")
Step 4.测试MCP服务器功能
1.当我们完成MCP服务器开发后,即可使用MCP-Inspector进行MCP工具功能测试。
回到项目主目录下,要立即启动并运行MCP-Inspector UI,只需执行以下操作:
npx @modelcontextprotocol/inspector
服务器将启动,用户界面将可在以下位置访问 http://localhost:6274

2.运行我们创建好的mcp服务,然后在调试工具连接mcp服务
先启动运行服务

在启动连接,选择工具查询北京今天(2025年12月14日)的天气

至此,两项MCP工具均测试完毕,接下来即可构建LangChain MCP客户端,来接入这些工具搭建智能体了。
7.8.2 创建LangChain MCP客户端接入多MCP服务
使用LangChain接入MCP工具,核心需要使用langchain_mcp_adapters库,该库可以将MCP工具信息进行解析,并让LangChain顺利识别。识别后即可像任意其他工具一样接入LangGraph中并搭建智能体。

在stdio模式下LangChain接入MCP的核心原理为: weather_server.py → 启动为子进程() → stdio 通信 → MCP 协议 → 转换为 LangChain 工具 → LangGraph Agent 执行读写,核心转换过程为::
- @mcp.tool() → 标准 LangChain Tool
- stdio_client() → 自动处理 read/write 流,其中read 表示从 MCP 服务器读取响应的流,write 表示向 MCP 服务器发送请求的流,对于 stdio weather_server.py,它们就是子进程的 stdout 和 stdin
- MultiServerMCPClient → 一键转换所有工具
下面是 Streamable HTTP 模式接入 MCP 的核心原理,按逻辑分层说明:HTTP Server(MCP 服务) ←→ Streamable HTTP Client ←→ LangChain Tool ←→ LangChain Agent
即:
- MCP 服务以 支持流式响应的 HTTP 接口 形式运行(如 FastAPI、Starlette 等);
- 客户端通过 streamable HTTP 请求/响应 与之通信;
- 客户端封装为标准 LangChain Tool;
- LangGraph Agent 调用该工具时,自动发起流式 HTTP 请求,并处理流式响应。
Step 1. 创建MCP配置文件(多模式、多MCP)
而为了完整实现一个标准的MCP调用流程、即通过配置文件灵活说明MCP工具信息然后再进行调用,这里我们同样先创建一个servers_config.json文件,用于记录MCP工具信息:
{ "mcpServers": { "weather": { "url": "http://127.0.0.1:8080/mcp", "transport": "streamable_http" }, "write": { "url": "http://127.0.0.1:8081/mcp", "transport": "streamable_http" }, "mcp-server-starrocks": { "url": "http://127.0.0.1:8082/mcp", "transport": "streamable_http" }, "filesystem" : { "command" : "npx.cmd", "args" : [ "-y" , "@modelcontextprotocol/server-filesystem", "E:/code/LlmMcp" ], "transport": "stdio" }, "12306-mcp": { "command": "npx.cmd", "args": [ "-y", "12306-mcp" ], "transport": "stdio" } } }
Step 2. 准备提示词模板
然后为当前Agent创建一个提示词模板agent_prompts.md:
你是一个智能助手,具备以下能力,请严格遵守规则并合理使用: 👋 你好!我是首衡集团智能助手「AgriLink」 当用户发送以下类型的消息时(例如:“你好”、“您好”、“hi”、“hello”、“你是谁?”、“你能做什么?”、“介绍一下你自己”等),请主动、友好地回复: 您好!我是首衡集团智能助手「AgriLink」,很高兴为您服务! 我可以帮助您完成以下任务: 🌤️ 查询天气 —— 例如:“北京今天天气怎么样?” 🎫 查询12306火车票信息 —— 例如:“北京到上海的高铁有哪些?”、“明天从广州去成都还有余票吗?” 💾 保存内容到文件 —— 例如:“保存刚才的天气结果” 📄 读取已保存的文件 —— 例如:“看看我刚刚存的内容” 🗃️ 查询与分析 StarRocks 数据库 —— 例如:“订单表前10条数据是什么?” 💡 注意:此回复仅用于问候或身份询问场景,不触发任何工具调用,不能自由发挥想象,必须按照上面的描述回答 ------ ## 🌤️ 1. 查询天气 - **工具名称**:`get_weather` - 参数说明: - `location`(必填):城市名称,例如 `"北京"`、`"上海市"`、`"广州"`。请尽量使用标准地名。 - date(可选,默认为"今天"):时间描述,支持: - 单日:`"今天"`、`"明天"`、`"后天"` - 多日:`"未来一周"`、`"这周"`、`"接下来几天"`、`"未来7天"` - 长期:`"未来15天"`、`"未来半个月"`、`"未来两周"` - **注意**:该工具可返回从系统当前日期起未来最多15天的天气预报。若用户未指定日期,默认查询“今天”。 - ⚠️ **必须原样输出工具返回的全部内容,不得删减或改写**。 ------ ## 📝 2. 保存文本内容 - **工具名称**:`save_note` - 参数说明: - `content`(必填):要写入的文本内容(如天气报告、摘要、生成的内容等)。 - **用途**:当用户要求“保存”、“导出”、“写入文件”、“存为笔记”等操作时**必须使用此工具**。 - ✅ **写入成功后,请告知用户**:“文件已保存,路径为 `[xxx]`”。 > 💡 此工具由自定义 Python 服务提供(`WriteServer`,端口 8081),会自动将文件保存至 [xxx] 目录,无需用户提供路径。 ------ ## 📁 3. 文件系统操作(只读 + 受限写入) 底层由 `@modelcontextprotocol/server-filesystem` 提供,**仅允许访问以下目录**: ``` E:\code\LlmMcp └── (及其所有子目录,如 output/、docs/ 等) ``` ### ✅ 支持的操作 | 场景 | 工具 | 说明 | | ---------------- | ---------------- | ------------------------------------------------------------ | | **读取已有文件** | `read_text_file` | 用户说“看看刚保存的内容”时使用,需提供完整路径(如 `E:/code/LlmMcp/output/note_xxx.txt`),也可以指定查看当前路径下面的某个文件 | | **列出目录** | `list_directory` | 可查看 `output` 等目录内容 | | **搜索文件** | `search_files` | 支持按模式查找(如 `*.txt`) | - 📖 路径自动补全规则: - 若用户仅提供文件名(如 agent_prompts.txt)→ 默认路径为: E:/code/LlmMcp/agent_prompts.txt - 若提及 output 中的文件(如 note_20251126.txt)→ 路径为: E:/code/LlmMcp/output/note_20251126.txt - 若说“当前目录下的 xxx” → 指 E:/code/LlmMcp/xxx - ❌ 禁止假设文件位于其他位置(如桌面、C盘等) ### ⚠️ 写入类操作(谨慎使用!) 虽然 `filesystem` 服务理论上支持 `write_file`、`edit_file` 等写入工具,但: > ❗ **你不得主动调用 `filesystem.write_file` 或 `edit_file` 来保存用户内容!** 原因: > > - 这些工具要求用户提供 `path`,而用户通常未指定 > - 与 `save_note` 功能重复,且易引发参数错误(如 `path is undefined`) > - 所有“保存”意图应统一由 `save_note` 处理 ✅ **例外情况**:仅当用户**明确指定路径和内容**(如“在 config.txt 中写入 hello”),才可考虑使用 `write_file`,但仍建议优先引导至 `save_note`。 ------ ## 🗃️ 4. 查询与分析 StarRocks 数据库 - **支持的工具**: - `read_query`:执行任意 SELECT 查询,返回表格数据(纯文本) - `table_overview`:获取表结构与样本数据摘要(用于探索) - `query_and_plotly_chart`:**执行查询并生成 Plotly 图表(Base64 图像)** - **适用场景与路由规则**: | 用户意图 | 必须调用的工具 | 行为要求 | | ------------------------------------------------------------ | -------------------------------- | ------------------------------------------ | | “查前10行”、“有哪些表”、“字段是什么” | `read_query` 或 `table_overview` | 返回结构化文本 | | **包含以下任一关键词**: “图表”、“画图”、“绘图”、“可视化”、“折线图”、“柱状图”、“趋势图”、“生成图”、“形成图表分析” | **`query_and_plotly_chart`** | **必须生成图像,不得返回文字摘要或提问!** | - **使用规则**: 1. 当用户要求图表时,**自动构造聚合 SQL**(如 `SELECT sale_date, SUM(sales_amount) FROM sales_report GROUP BY sale_date ORDER BY sale_date`) 2. 图表类型根据语义推断: - “每天”、“趋势”、“变化” → 折线图(line) - “各地区”、“对比”、“分布” → 柱状图(bar) - “占比”、“比例” → 饼图(pie) 3. **禁止在图表请求后回复“是否要画图?”之类的问题**——用户已明确要求! 4. 若客户端不支持图像显示,仍需调用工具,并说明:“图表已生成(Base64),但当前界面可能无法显示。” 5. 禁止高危操作(`DROP`/`DELETE`),除非用户确认。 6. 大结果集应限制行数(如 `LIMIT 1000`)。 ### 🔄 图表生成必须遵循“先探查,后执行”原则 当用户要求生成图表时,**不得直接假设字段名**!必须按以下顺序操作: 1. **第一步:获取表结构** - 调用 `table_overview` 或执行 `DESCRIBE sales_report`(通过 `read_query`) - 确认真实列名(如 `sales_amount` 而非 `amount`) 2. **第二步:构造并执行可视化查询** - 基于真实字段名编写 SQL(如 `SUM(sales_amount)`) - 调用 `query_and_plotly_chart` 生成图表 > ⚠️ **禁止跳过第一步直接写 SQL**!即使字段名看似“ obvious”(如“金额”→`amount`),也必须验证。 ------ ## 🎫 5. 查询12306火车票信息(通过 MCP 服务) - **工具名称**:`search_trains`(或其他由 `12306-mcp` 提供的工具,如 `check_seat`) - 参数说明: - `from_station`(必填):出发城市或车站名,例如 `"北京"`、`"上海虹桥"`。请使用常见站名。 - `to_station`(必填):到达城市或车站名,例如 `"广州南"`、`"成都东"`。 - travel_date(可选,默认为“今天”):出行日期,支持: - `"今天"`、`"明天"`、`"后天"` - 具体日期如 `"2025-12-01"` - 相对描述如 `"本周五"`(需能被解析为有效日期) - 功能范围: - 查询指定日期、区间内的**所有车次列表** - 返回信息包括:车次号、出发/到达时间、历时、席别(如二等座、硬卧)、余票状态等 - **不支持订票、支付、身份证验证等操作**,仅提供公开余票与时刻信息 - ⚠️ **必须原样输出工具返回的全部内容,不得删减、美化或自行解释余票状态** ### 📌 使用规则 1. **必须明确三要素**:出发地、目的地、日期。若用户未提供完整信息,应主动询问缺失项。 - ❌ 错误示例:“查火车” → 缺少出发/到达/日期 - ✅ 正确引导:“请问您要从哪里出发?到哪里?哪一天出行?” 2. **禁止猜测车站名**:若用户说“去上海”,优先使用 `"上海"` 作为站名;若工具返回无结果,可建议尝试 `"上海虹桥"` 等主要车站。 3. **结果处理**: - 若工具返回空或错误,如实告知:“未查询到符合条件的车次,请检查出发/到达站或日期。” - 若返回多条车次,**完整列出**,不得摘要或只选部分。 4. **与其他功能联动**: - 用户说“把车次保存下来” → 调用 `save_note` 保存完整结果 - 用户说“看看刚查的火车票” → 调用 `read_text_file` 读取最近保存的文件内容,原样输出,不要理解总结,内容是什么就输出什么。 ## 🧠 通用行为准则 ### 1. 理解意图优先 - 若请求模糊(如“查天气”但无城市),先询问缺失信息,不要猜测。 - 若用户说“看看我刚保存的内容”,需先确认文件名或路径,再调用 `readFile`。 ### 2. 精准调用工具 - **保存内容** → **只能用 `save_note`**(自动生成路径,无需 `path`) - **读取文件** → **使用 `readFile`**(需用户提供或你推断出完整路径,如 `E:/code/LlmMcp/output/note_xxx.txt`) - **严禁在保存时调用 `filesystem.write_file`**(因其需要 `path` 参数,而用户未提供,会导致错误) - **图表请求** →强制可视化:只要用户提及“图表”“画图”“可视化”等词,必须调用 **`query_and_plotly_chart`**,不得降级为文本摘要或交互引导。 ### 3. 友好简洁回复 - 工具返回后,用自然语言总结,避免输出原始 JSON 或技术细节。 - 保存成功 → 告知完整路径。 - 读取成功 → 直接输出文件内容(无需额外包装)。 ### 4. 拒绝无关请求 如果用户问题与以下无关: - 查询天气 - 保存文本(通过 `save_note`) - 读取已保存的文件(通过 `readFile`,路径在允许目录内) - 查询 StarRocks 数据 则回复: > “抱歉,我目前只能帮您查询天气、保存文本到文件、读取您刚保存的特定文件,或查询数据库,其他问题暂时无法处理。” ### 5. 错误处理 - 若文件不存在、路径无效或超出允许目录,如实告知用户。 - 若因误用 `write_file` 导致失败,立即改用 `save_note` 并说明原因。 ------ > ✅ **核心原则**: > > - **写用 `save_note`,读用 `readFile`** > - **绝不混淆 `save_note` 与 `filesystem.write_file`** > - **所有文件操作必须在 `E:/code/LlmMcp` 或其子目录内进行** > - 所有火车票查询必须通过 **12306-MCP 服务** 完成,不得模拟或虚构数据 > - 不得承诺“一定能买到票”或提供非公开信息(如候补人数、内部余票) > - 若 12306-MCP 服务不可用,应明确告知:“火车票查询服务暂时不可用,请稍后再试。”
Step 3. 创建client.py主函数文件
然后再创建client主函数文件
import asyncio import json import logging from typing import Dict, Any from langchain_mcp_adapters.client import MultiServerMCPClient from langgraph.checkpoint.memory import InMemorySaver from langchain_ollama import ChatOllama from langchain.agents import create_agent # 创建一个内存保存器, 用于保存模型参数 checkpointer = InMemorySaver() # 读取提示词模版 with open("agent_prompts.md", "r", encoding="utf-8") as f: prompt = f.read() config = { "configurable": {"thread_id": "1"} # ✅ 字典 } class Configuration: @staticmethod def load_servers(file_path: str = "servers_config.json") -> Dict[str, Any]: with open(file_path, "r", encoding="utf-8") as f: servers = json.load(f).get("mcpServers", {}) # 获取服务器列表, 如果没有则返回空字典 return servers async def run_chat_loop() -> None: cfg = Configuration() servers_cfg = cfg.load_servers() # print("------------------------------测试-------------------------------") # print(f"已加载 {len(servers_cfg)} 个MCP服务器,服务器列表: {servers_cfg}") # 1.创建一个 MultiServerMCPClient 对象, 并传入服务器列表, mcp_client = MultiServerMCPClient(servers_cfg) # 2.获取工具列表 tools = await mcp_client.get_tools() logging.info(f"已加载 {len(tools)} 个MCP工具,工具列表: {[tool.name for tool in tools]}") # 3.初始化模型 # 确保 model= 是 ChatOllama 实例 llm = ChatOllama( model="qwen3:8b", base_url="http://127.0.0.1:11434", temperature=0.0, top_p=0.9, repeat_penalty=1.1 ) # 4.创建一个代理 agent = create_agent( model=llm, tools=tools, system_prompt=prompt, checkpointer=checkpointer ) print("\n🤖 MCP Agent 已经启动,输入quit即可推出,开始对话...") while True: user_input = input("\n你:").strip() if user_input.lower() == "quit": print("已退出") break try: # 调用代理 result = await agent.ainvoke({"messages": [{"role": "user", "content": user_input}]}, config) print(f"🤖 MCP Agent:{result['messages'][-1].content}") except Exception as e: print(f"❌ 错误:{e}") continue if __name__ == '__main__': asyncio.run(run_chat_loop())
代码解释如下:
✅ 从 .env 文件读取模型配置(由于我使用的本地ollama部署的模型,故而.env暂时未使用)
✅ 从 servers_config.json 读取 MCP 服务器配置(支持多个服务器)
✅ 启动 MCP 客户端加载所有工具
✅ 用 LangChain 创建 Agent,把所有工具挂载
✅ 在命令行与用户进行对话,模型自动选择工具
八、监督者模式多 Agnet 架构
监督者(主管)模式是一种多 Agnet 架构,其中中央主管 Agnet 负责协调各专业工作 Agnet 。当任务需要不同类型的专业知识时,这种方法非常有效。与其构建一个管理跨领域工具选择的 Agnet ,不如创建由了解整体工作流程的主管协调的、专注的专家。
在 LangChain 中可以将 Agent 封装为工具,将工具绑定到主管 Agent 来实现主管多代理模式。
import os import asyncio import smtplib from langchain.tools import tool from urllib.parse import urlencode from email.mime.text import MIMEText from langchain.agents import create_agent from langchain.chat_models import init_chat_model from langchain_mcp_adapters.client import MultiServerMCPClient llm = init_chat_model( model="z-ai/glm-4.5-air:free", model_provider="openai", base_url="https://openrouter.ai/api/v1", api_key=os.getenv("OPENROUTER_API_KEY"), ) # ========== 创建一个有搜索功能的子Agent ========== class SearchSubAgent: """带搜索功能的子Agent""" def __init__(self): self.tools = asyncio.run( MultiServerMCPClient( { "WebSearch": { "transport": "sse", # 服务器发送事件 (SSE):针对实时流通信进行优化的可流式 HTTP 的变体。 "url": "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/sse", "headers": { "Authorization": f"Bearer {os.getenv('DASHSCOPE_API_KEY')}" }, }, # https://bailian.console.aliyun.com/?tab=mcp#/mcp-market/detail/WebSearch "RailService": { "transport": "streamable_http", # 流式 HTTP:服务器作为独立进程运行,处理 HTTP 请求。支持远程连接和多客户端。 "url": f"{'https://server.smithery.ai/@DeniseLewis200081/rail/mcp'}?{urlencode({'api_key': os.getenv('SMITHERY_API_KEY')})}", }, # https://smithery.ai/server/@DeniseLewis200081/rail } ).get_tools() ) self.agent = create_agent(model=llm, tools=self.tools) async def __call__(self, input: str) -> str: return await self.agent.ainvoke( {"messages": [{"role": "user", "content": input}]} ) # ========== 创建一个能发送邮件的子Agent ========== @tool async def send_email(to: list[str], subject: str, body: str) -> str: """ 发送邮件。需要自动生成邮件主题。 Args: to: 收件人 subject: 邮件主题 body: 邮件正文 """ SMTP_HOST = "smtp.qq.com" SMTP_USER = os.getenv("SMTP_USER") SMTP_PASS = os.getenv("SMTP_PASS") # 需要在邮箱中开启 SMTP 并生成授权码 msg = MIMEText(body, "plain", "utf-8") msg["From"] = SMTP_USER msg["Subject"] = subject try: server = smtplib.SMTP_SSL(SMTP_HOST, 465, timeout=10) server.login(SMTP_USER, SMTP_PASS) server.sendmail(SMTP_USER, to, msg.as_string()) try: server.quit() except smtplib.SMTPResponseException as e: if e.smtp_code == -1 and e.smtp_error == b"\x00\x00\x00": pass # 忽略无害的关闭异常 else: raise return "success" except Exception as e: return f"Send failed: {type(e).__name__} - {e}" class EmailSubAgent: """带发送邮件功能的子代理""" def __init__(self): self.tools = [send_email] self.agent = create_agent(model=llm, tools=self.tools) async def __call__(self, input: str) -> str: return await self.agent.ainvoke( {"messages": [{"role": "user", "content": input}]} ) search_subagent = SearchSubAgent() email_subagent = EmailSubAgent() # ========== 将子 Agent 包装为工具 ========== @tool async def search(input: str) -> str: """ 一个具有搜索功能的子Agent,功能包括: - 搜索网页 - 搜索火车票相关信息 """ return await search_subagent(input) @tool async def email(input: str) -> str: """ 一个具有发送邮件功能的子Agent """ return await email_subagent(input) # ========== 创建主管 Agent ========== supervisor_agent = create_agent( model=llm, tools=[search, email], system_prompt="你是一个主管,需要调用子Agent来帮助用户", ) async def main(): async for chunk in supervisor_agent.astream( { "messages": [ { "role": "user", "content": "北京明天天气怎么样,要是还不错的话,帮我看看明天上海到北京的车票。如果天气好的话,发送邮件给xxxxxx@qq.com告诉他我明天去北京。如果天气不好的话就告诉他我明天不去北京了。", } ] } ): print(chunk, end="\n\n") asyncio.run(main())

浙公网安备 33010602011771号