第 5 章 Agent

一、Agent 介绍

通用人工智能(AGI)将是 AI 的终极形态,几乎已成为业界共识。同样,构建 Agent则是 AI 工程应用当下的“终极形态”。

将 AI 和人类协作的程度类比自动驾驶的不同阶段:

语言模型本身无法采取行动——它们只是输出文本。LangChain 的一个重要功能是创建Agent。Agent 是一种使用 LLM 作为推理引擎的系统,它决定要采取哪些行动以及这些行动的输入应该是什么。这些行动的结果可以反馈给 Agent,由 Agent 决定是否需要采取更多行动,或者是否可以完成。

与传统的固定流程链不同,Agent 具备一定的自主决策能力,更适合处理开放式、多步骤的问题。它可以拆解任务,根据任务动态决定调用哪些工具,并利用中间结果推进任务。

Agent 的核心能力/组件:

  1. 大模型(LLM):作为大脑,提供推理、规划和知识理解能力。
  2. 记忆(Memory):具备短期记忆和长期记忆,支持快速知识检索。
  3. 工具(Tools):调用外部工具(如API、数据库)的执行单元。
  4. 规划(Planning):任务分解、反思与自省框架实现复杂任务处理。
  5. 行动(Action):实际执行决策的能力。
  6. 协作:通过与其他 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)

image

三、构建 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)

image

如果 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)

image

四、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的名称

image

4.1.4 运行

Windows (CMD)执行命令

cd F:\NexusKnow
set PYTHONPATH=F:\NexusKnow
langgraph dev

Linux下,只需要执行:

# 进入项目路径,执行
langgraph dev

启动:

image

4.2 在 LangSmith 中调试

如果用过 Coze 或 Dify 等工具的,那看见这个界面后会很亲切~~~

4.2.1 调试

image

4.2.2 点击  chat 就是对话页面

image

4.3 远程访问 LangSmith

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

image

4.3.1 运行远程访问启动

要想其他人访问,执行如下命令:

langgraph dev --tunnel

在启动台查看,就变成了类似这种 

https://smith.langchain.com/studio/?baseUrl=https://carter-followed-fibre-gif.trycloudflare.com

如图

image

其他人就可以访问这个地址了。但是其他人直接访问会提示错误:

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

image

dig carter-followed-fibre-gif.trycloudflare.com

Windows下执行 :

nslookup carter-followed-fibre-gif.trycloudflare.com

image

任意选择一个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装饰符来自定义一个外部函数:

image

或者也可以借助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工具在智能体开发过程中实际带来的提效的作用。

8cccf76cdd07741f61424ecdeda88b5c

8dac9069b78b9b41d545ae9bec8da65f

7.2.2 MCP技术架构

不过呢,要做到上面说的这种“车同轨、书同文”的标准化工作,不仅需要制定一套让所有人都信服的标准,而且还需要经过时间的检验,同时还需要有足够多的用户,这个标准才能真正被市场所认同。因此MCP技术也历经了一段时间的沉淀和打磨,自2024年11月发布开始,到2025年3月技术大爆发,再到第二季度开始越来越多的Agent框架和热门应用宣布支持MCP技术,MCP才算是逐渐成为一项智能体开发的通用协议。

截止目前,MCP的技术生态可以划分为三层,最底层是协议层,也就是“文字版”的规定、或者说规范,而为了普及这一规范,让更多的人更加快速的完成自己的MCP工具开发,Anthropic进一步的提供了MCP开发工具,借助这些SDK,我们能够非常快速完成MCP工具开发。而既然是一种标准化的协议,其核心价值就在于用的人足够多、同时分享的人也足够多,才能真正减少“重复造轮子”的时间,因此Anthropic官方和很多第三方平台,也在积极的推进MCP技术生态的构建,尤其是MCP工具平台的建设,通过鼓励开发者更多的分享自己开发的MCP工具,只有形成了更大的(分享和引用的)协作规模,MCP的技术才能更有价值。
948d40d029ac457f7f1b656434dee3ee

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

image

而借助这些库(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工具发送请求并接受结果的环境。二者关系如图所示:

7a2f822343b425ddab4d38ab0bd807e1

这种服务器和客户端分离的架构的好处,就在于可以更加便捷的进行模块化开发和维护,而此前我们所说的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.复制上面配置,填入

image

3.测试

image

7.3.3 MCP离线运行与在线运行模式

既然是服务器和客户端分离的架构,那么MCP肯定支持两种调用方法,其一是本地运行,也被成为离线运行,指的是MCP服务器和MCP客户端在同一台电脑上进行运行,其二则是在线运行,或者异地部署运行,指的是MCP服务器在远程服务器或者云端运行,然后借助HTTP网络通信来进行响应。

而这也就对应着MCP工具调用的两种核心模式,分别是stdio(本地)模式调用和SSE&streamable HTTP(在线)模式调用,各调用方法效果对比如下所示:

特性StdioSSEStreamable HTTP
通信方向 双向(但仅限本地) 单向(服务器到客户端) 双向(适用于复杂交互)
使用场景 本地进程间通信 实时数据推送,浏览器支持 跨服务、分布式系统、大规模并发支持
支持并发连接数 中等 高(适合大规模并发)
适应性 局限于本地环境 支持浏览器,但单向通信 高灵活性,支持流式数据与请求批处理
实现难度 简单,适合本地调试 简单,但受限于浏览器兼容性和长连接 复杂,需处理长连接和流管理
适合的业务类型 本地命令行工具,调试环境 实时推送,新闻、股票等实时更新 高并发、分布式系统,实时交互系统
三种传输方式总结如下:
  1. Stdio 传输:适合本地进程之间的简单通信,适合命令行工具或调试阶段,但不支持分布式。
  2. SSE 传输:适合实时推送和客户端/浏览器的单向通知,但无法满足双向复杂交互需求。
  3. 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上的库,

image

除此此外,一些由Python编写的MCP开源工具,则托管在pypi平台上,https://pypi.org/ ,每次运行的时候我们需要填写uvx命令,其本质就是将工具源码从pypi平台上下载到本地再来进行运行。

7.3.5 MCP在线托管服务

伴随着MCP快速调用的请求不断增加,也有很多平台提供了在线托管服务模式,例如魔搭社区的MCP广场中,就有很多是运行在魔搭社区服务器上的MCP工具,开发者可以直接使用SSE或者流式HTTP方式请求调用在线的MCP工具,从而快速完成开发工作。

image

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

image

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

先启动运行服务

image

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

image

至此,两项MCP工具均测试完毕,接下来即可构建LangChain MCP客户端,来接入这些工具搭建智能体了。

7.8.2 创建LangChain MCP客户端接入多MCP服务

使用LangChain接入MCP工具,核心需要使用langchain_mcp_adapters库,该库可以将MCP工具信息进行解析,并让LangChain顺利识别。识别后即可像任意其他工具一样接入LangGraph中并搭建智能体。

bb20299be07177fff88528c1c3bdd855

在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())
posted @ 2025-12-11 17:40  酒剑仙*  阅读(3)  评论(0)    收藏  举报