LangGraph写React的Agent并调用远程MCP工具

MCP server

python 复制代码
from fastmcp import FastMCP

mcp = FastMCP("My MCP Server")


@mcp.tool()
def multiply(a: int, b: int) -> int:
    """
    返回两个数的乘积
    Args:
        a: 第一个数
        b: 第二个数
    Returns:
        int: 两个数的乘积
    """
    return a * b


@mcp.tool()
def add(a: int, b: int) -> int:
    """
    返回两个数的和
    Args:
        a: 第一个数
        b: 第二个数
    Returns:
        int: 两个数的和
    """
    return a + b


@mcp.tool()
def devide(a: int, b: int) -> float:
    """
    返回两个数的商
    Args:
        a: 第一个数
        b: 第二个数
    Returns:
        float: 两个数的商
    """
    return a / b


if __name__ == "__main__":
    # 3. 启动本地MCP服务器, 传输协议Stdio
    # mcp.run()

    # 远程访问的http
    mcp.run(transport="http", host="0.0.0.0", port=8000)

    # 服务器端的同步函数是FastMCP用线程池在异步框架里兜住的
    # 而你拿到的工具对象之所以只有异步接口,是langchain_mcp_adapters单方面包装的结果
    # 和你的FastMCP代码怎么写无关

启动

python 复制代码
python .\my_server.py

Agent

python 复制代码
import os
from langchain.tools import tool
from langchain.chat_models import init_chat_model
from langchain_mcp_adapters.client import MultiServerMCPClient

import dotenv
dotenv.load_dotenv()


# 定义模型和工具
model = init_chat_model(
    model="deepseek-chat",
    temperature=0.0
)


# @tool
# def multiply(a: int, b: int) -> int:
#     """
#     返回两个数的乘积
#     Args:
#         a: 第一个数
#         b: 第二个数
#     Returns:
#         int: 两个数的乘积
#     """
#     return a * b


# @tool
# def add(a: int, b: int) -> int:
#     """
#     返回两个数的和
#     Args:
#         a: 第一个数
#         b: 第二个数
#     Returns:
#         int: 两个数的和
#     """
#     return a + b


# @tool
# def devide(a: int, b: int) -> float:
#     """
#     返回两个数的商
#     Args:
#         a: 第一个数
#         b: 第二个数
#     Returns:
#         float: 两个数的商
#     """
#     return a / b


# tools = [multiply, add, devide]


# 从MCP获取服务工具
async def get_mcp_tools():
    client = MultiServerMCPClient(
        {
            "my_math_server": {
                "transport": "streamable_http",
                "url": "http://localhost:8000/mcp"
            }
        }
    )
    # 获取工具列表,这些 工具已经是 LangChain 兼容的格式
    tools = await client.get_tools()
    return tools


# 定义状态
import operator
from langchain.messages import AnyMessage, AIMessage
from typing_extensions import TypedDict, Annotated
# 定义模型节点
from langchain.messages import SystemMessage, HumanMessage


class MessagesState(TypedDict):
    # 首次写入(作为初始输入时),它是直接赋值的
    messages: Annotated[list[AnyMessage], operator.add]
    llm_calls: int


async def main():
    tools = await get_mcp_tools()
    print("==============tools==============")
    print(tools)
    print("==============tools==============")

    tools_by_name = {tool.name: tool for tool in tools}
    # 把 MCP 工具绑定到模型
    models_with_tools = model.bind_tools(tools)


    def llm_call(state: MessagesState):
        """
        models_with_tools.invoke(
        {
            "messages": [AIMessage(...)],   # 只含一条新消息的列表
            "llm_calls": state["llm_calls"] + 1
        }

        operator.add对两个list做拼接,所以最终messages = 历史消息 + [新的AIMessage消息]
        """
        return {
            "messages": [
                # 返回值时单条AIMessage消息
                # 返回结构是:

                models_with_tools.invoke(
                    [
                        SystemMessage(
                            content="你是一个乐于助人的助手,负责对一组输入进行算数运算"
                        )
                    ]
                    + state["messages"]
                )
            ],
            "llm_calls": state["llm_calls"] + 1
        }


    # 定义工具节点:工具节点用于调用工具并返回结果
    from langchain.messages import ToolMessage


    async def tool_node(state: MessagesState):
        """执行工具调用"""
        result = []
        for tool_call in state["messages"][-1].tool_calls:
            tool = tools_by_name[tool_call["name"]]
            observation = await tool.ainvoke(tool_call["args"])
            result.append(
                ToolMessage(content=str(observation), tool_call_id=tool_call["id"])
            )
        return {"messages": result}


    # 定义 end 逻辑: 条件边函数用于根据 LLM 是否进行了工具调用来路由到工具节点或终端
    from typing import Literal
    from langgraph.graph import StateGraph, START, END


    # 路由函数
    def should_continue(state: MessagesState) -> Literal["tool_node", "end"]:
        messages = state["messages"]
        last_message = messages[-1]
        print("==============last_message==============")
        print(last_message)
        print("==============last_message==============")

        if last_message.tool_calls:
            return "tool_node"
        
        return "end"


    # 构建和编译Agent
    builder = StateGraph(MessagesState)
    builder.add_node("llm_call", llm_call)
    builder.add_node("tool_node", tool_node)

    builder.add_edge(START, "llm_call")
    builder.add_conditional_edges(
        "llm_call",
        should_continue,
        {"tool_node": "tool_node", "end": END}
    )
    builder.add_edge("tool_node", "llm_call")

    graph = builder.compile()

    with open("graph.png", "wb") as f:
        f.write(graph.get_graph().draw_mermaid_png())

    messages = [HumanMessage(content="10 * 20 = ?,结果已josn格式返回,一个question,一个answer。")]
    res = await graph.ainvoke({"messages": messages, "llm_calls": 0})
    print(res)
    print("==============last messages==============")
    print(res["messages"][-1])
    print(res["messages"][-1].content)


if __name__ == "__main__":
    import asyncio
    # 建立事件循环, 把main这个协程放进去
    # 从此以后,所有await的东西都在这个循环里调度,这是异步启动的含义
    asyncio.run(main())

    # LangGraph对节点函数的签名没有统一强制要求异步,一个图里可以混合同步节点和异步节点
    # graph.ainvoke会妥善处理,遇到同步节点就放到线程池里去执行,遇到异步节点就在事件循环里执行
    
    # 事件循环是主干,同步函数不会自动变成异步,而是被langchain/langgraph用run_in_executor包装成可await的任务
    # 丢进线程池执行,执行完在回到事件循环继续

    # 为什么异步里面要有线程池?
    # 因为同步函数里如果直接做阻塞IO(比如:request.get、time.sleep),会卡死整个事件循环,导致所有其他协程都无法推进
    # 把他们扔进线程池,事件循环本身不被堵住。这是asyncio生态里同步代码兼容异步框架的标准做法

    # 一个容易混淆的点:
    # 线程池不是给所有同步函数准备的默认通道。只有被异步框架显示调度的同步函数才会进线程池。
    # 你自己在main()里调用一个同步函数foo(),它就是在当前线程(事件循环所在线程)里同步执行的,不会自动进线程池
    # 是LangGraph这类框架在调用节点时主动用了 run_in_executor 包装,才进了线程池
    # 不是异步走循环、同步走线程池的静态二分,而是循环为主干,同步函数按需被框架调度到线程池的动态过程

    # 事件循环是单线程的,它靠某个协程遇到await时让出控制权,去跑别的协程来实现并发,
    # 同步函数里如果没有await,(time.sleep、request.get、同步数据库查询都是),它就一口气占着线程跑完才返回
    # 循环没有任何机会切出去,并发的假象直接破灭,所有其他协程一起被卡住

    # 我用了 asyncio.run 启动,所以整段代码都是异步的,这个理解不对,asyncio.run 只是创建了一个事件循环并运行main协程
    # 它不会把里面所有函数都变成非阻塞的,是不是非阻塞,取决于每个函数自己有没有让出控制权,一个协程里塞满同步调用
    # 它在循环里跑起来和普通同步代码一样卡,异步框架提供的是"让出控制权"的机制,不是自动把同步代码变块
    
    # 一句话:LangGraph 替你做了线程池调度,自己写就没有这层保护,同步函数会实打实阻塞事件循环,必须手动 awaig asyncio.to_thread或者 loop.run_in_executor包装

执行Agent

python 复制代码
python .\quick_agent.py
相关推荐
一只游鱼4 小时前
langgraph快速入门 —— 从本地脚本到 HTTP 服务
agent·langgraph
寻道码路5 小时前
AI Agent 工程化开发实战(五):持久化与断点恢复——Checkpointer 与 Store
人工智能·大模型·checkpoint·持久化·ai agent·langgraph·ai工程化
一只游鱼2 天前
LangGraph快速入门 —— Agent 是怎么转起来的
人工智能·ai·agent·langgraph
专职2 天前
LangGraph持久化机制和可恢复执行:如何让Agent拥有“不死之身”
langgraph
Maiko Star2 天前
LangGraph 环境配置:Miniconda、PyCharm、依赖安装与环境验证
langgraph
小易老师AI实战3 天前
用中间件把任务规划嵌进智能体循环
中间件·状态管理·智能体·工具调用·langgraph·任务规划
lee_curry3 天前
第十周完整学习笔记:继续升级为“检索更准、权限安全、索引可维护、失败可恢复、长期可运行”的企业级 RAG 系统
python·ai·langchain·agent·langgraph
Mr. zhihao3 天前
Deep Agent 架构深度解析:主Agent与子Agent完整交互机制与核心参数作用
python·ai·langgraph·deepagent
小易老师AI实战4 天前
LangGraph Agent 任务规划中间件:TodoListMiddleware 原理解析
中间件·agent·langgraph·任务规划·大模型智能体