【Langchain】超详细构建Langchain架构下的MCP与Agent智能结合体

在实现causalchat功能的时候,遇到了需要反复调用不同的工具的场景,如果只有一个mcp服务,那么也许不需要agent智能体的构建,但是如果一旦加入rag等复杂功能的实现,谁先谁后就不能在代码中进行硬编码了,为此我们将实现一个agent架构 相关文章: 【MCP】小白详解从0开始构建MCP服务端全流程 【RAG+向量数据库】小白从0构建一个rag和向量数据库demo

1. MCP工具的参数封装

前提:对于Langchain中的agent模式,需要一个严格的接口定义,而在 LangChain 中,这个标准接口就是 BaseTool 类。 对于所有工具,agent都需要有以下几个参数

  • 它必须是一个 BaseTool 对象:这是最基本的要求。
  • 必须有 name 和 description:Agent 通过这些信息来"思考"和决定用哪个工具。
  • 必须有 args_schema:这是一个 Pydantic 模型,它精确地告诉 Agent 这个工具需要哪些参数,以及这些参数的类型。这是 Agent 能-够正确生成调用参数的基础。
  • 必须有 _arun 方法:这是 Agent 按下工具"启动按钮"的地方。当 Agent 决定使用某个工具时,它就会调用这个工具的 _arun 方法。

有了这个前提,我们再去看我们之前的mcp工具,就会发现几个问题 首先,对于我们之前调用的mcptool,tool包括以下几个参数

python 复制代码
mcp_tools = [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.inputSchema,
            }

也就是包括名字,描述,参数输入格式 但是在agent中,这样的json格式已经不能被识别了,也就是我们需要构建一个格式化的接口

首先我们按照以前的教程构建一个mcp服务,这个服务包括以下参数,这里的tool是从mcp服务器返回的原始数据,原始数据如下,假设我们这个mcp只需要输入username和filename(这个是在mcp服务中构建的,详细请参阅上述文章)

python 复制代码
{
  "type": "function",
  "function": {
    "name": "xx",
    "description": "xx",
    "inputScheme": {
        "username": {
          "type": "string"
        },
        "filename": {
          "type": "string"
        }
      },
      "required": ["username"]
    }
}

我们将原始数据进行处理为以下这个更加简洁的结构

python 复制代码
mcp_tools = [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.inputSchema,
            }
        } for tool in tools_response.tools]

        mcp_session = session

首先是进行agent中的args_schema构建

python 复制代码
def create_pydantic(schema: dict, model_name: str) -> Type[BaseModel]:
	# 这里创建一个返回字典
	fields = {}
	# 这里从mcptool的参数进行一步步解析
	# 从上述的参数中获取这两个值
    properties = schema.get('properties', {})
    required_fields = schema.get('required', [])
    # 进行字典的解包推导
    for prop_name, prop_schema in properties.items():
        # 这里对类型做了简化映射,可以根据未来工具的复杂性进行扩展
        field_type: Type[Any] = str  # 默认为字符串类型
        if prop_schema.get('type') == 'integer':
            field_type = int
        elif prop_schema.get('type') == 'number':
            field_type = float
        elif prop_schema.get('type') == 'boolean':
            field_type = bool
        
        # Pydantic 的 create_model 需要一个元组: (类型, 默认值)
        # 对于必需字段,默认值是 ... (Ellipsis)
        if prop_name in required_fields:
            fields[prop_name] = (field_type, ...)
        else:
            fields[prop_name] = (field_type, None)
    
    
    # 使用 Pydantic 的 create_model 动态创建模型类
    #
    return create_model(model_name, **fields)

create_model 函数在接收到这些参数后,会返回一个全新的类 也就是会创建一个这样的类,用户规定格式

python 复制代码
class moedelname(BaseModel):
	filename:str
	username:str

创建完这样一个格式接口之后,我们还需要一个mcp和agent的通信,也就是前面提到的_run 需要定义一个类,继承于BaseTool 这个类首先必须定义四种参数,然后就会利用mcp中的call_tool进行调用mcp,对于返回的文本,直接返回一个纯文本。对于其他格式的mcp,则更具mcp类型返回不同的格式

content: list[TextContent | ImageContent | EmbeddedResource]

python 复制代码
class Mcptool(Basetool):
	name: str
    description: str
    args_schema: Type[BaseModel]  # 强制工具必须有参数结构
    session: "ClientSession"      # 类型前向引用
    # 这里我们使用异步的方法,由于在mcp中采用的是异步处理,防止阻塞主程序,一般来说使用_run即可
    # 这里的**kawrg是任意项的参数解包
	async def _arun(self,**kwarg:any) -> any:
		# 这里同样使用await参数进行异步调用
		# 这里就是mcp连接中的call_tool调用
		response_obj = await self.session.call_tool(self.name,kwargs)
		# 返回纯文本内容
		# 这里一班包括了content: list[TextContent | ImageContent | EmbeddedResource]
		function_response_text = response_obj.content[0].text
		return function_response_text
	

2. agent主程序的构建

接下来将进行agent主程序的构建

  1. 获取上下文记录 ,这里可以采用langchain架构的历史记录获取,也可以直接获取数据库中的数据,这里不做讲解history_messages_raw = get_chat_history(session_id, user_id, limit=20)

  2. 初始化llm的连接: 获取llm的基础连接细信息,包括密码,模型,温度。注意目前agent不支持流式输出

    python 复制代码
    llm = ChatOpenAI(
            model=current_model,
            base_url=BASE_URL,
            api_key=apikey,
            temperature=0,
            streaming=False,
        )
  3. 遍历工具箱 首先创建一个tool,这个列表存放着的是一个个类 然后对所有的mcptool进行遍历,遍历完之后,将工具的名称、描述、刚刚创建的参数模型类 (args_schema),以及与服务器的连接 (mcp_session) 等信息,打包成一个 McpTool 类的实例。 最后,将这个 McpTool 实例添加到一个名为 langchain_tools 的列表中。现在,我们就有了一个 LangChain Agent 完全能理解的"工具箱"。

python 复制代码
	langchain_tools: List[BaseTool] = []
	    for tool_def in mcp_tools:
	        func_info = tool_def["function"]
	        # 为动态创建的 Pydantic 模型生成一个唯一的类名
	        model_name = f"{func_info['name'].replace('_', ' ').title().replace(' ', '')}Input"
	        # 这里构建一个类,名字的自定义的,参数就是通过mcp_tool获取的
	        args_schema = create_pydantic(func_info["parameters"], model_name)
	        # 这里通过类进行初始化
	        # 
	        langchain_tool = McpTool(
	            name=func_info["name"],
	            description=func_info["description"],
	            args_schema=args_schema,
	            session=mcp_session,
	            username=username
	        )
	        langchain_tools.append(langchain_tool)
  1. 创建一个agent prompt 我们将历史记录和userinput传给agent 这里的MessagesPlaceholder(variable_name="agent_scratchpad")指的是agent的内部运作空间

    python 复制代码
    prompt = ChatPromptTemplate.from_messages([
            ("system", "你是一个有用的工程师助手,可以使用工具来获取额外信息。"
                       "你的主要任务是分析工具返回的JSON数据,并以详细的自然语言(根据用户给你的语言风格)向用户总结关键发现。"
                       "现在有一下几个要求:1.不要在你的回答中逐字重复整个JSON数据。请根据上下文进行回复。2. 请使用Markdown格式(例如,使用项目符号、加粗、表格等)来组织你的回答 3. 回答需要依照因果推断相关的知识和术语进行回答"),
            MessagesPlaceholder(variable_name="chat_history"),
            ("user", "{input}"),
            MessagesPlaceholder(variable_name="agent_scratchpad"), # Agent 内部工作空间
        ])
  2. 创建 Agent Agent (create_openai_tools_agent):这是一个"大脑"。它由三部分组成:LLM、工具箱(langchain_tools)和提示(Prompt)。它知道如何根据提示和用户的输入来决定调用哪个工具。

    python 复制代码
    agent = create_openai_tools_agent(llm, langchain_tools, prompt)
  3. 创建执行器 有了agent,我们就开始执行

    python 复制代码
    agent_executor = AgentExecutor(
            agent=agent, 
            tools=langchain_tools, 
            verbose=True, # verbose=True 会在日志中打印 Agent 的思考过程
            return_intermediate_steps=True # 打印中间的工具调用步骤
        )
  4. 格式化历史记录 在langchain中,如果使用其他格式的历史记录,都需要格式化历史记录信息,包括(HumanMessage, AIMessage)

    python 复制代码
    HumanMseeage(content = msg["content"] if msg["role"] = 'user')
    else AImseeage(conent = mag["content"])
    for msg in history
  5. 调用agent 这部分是agent核心,流程如下

    • 思考:执行官将用户的请求、对话历史和工具列表(包含工具的description)一起打包,发送给 LLM。LLM 分析后认为,用户的意图是"某功能",并且 mcp中的工具的描述最匹配这个意图。LLM 还会从用户输入中提取参数,比如 filename。
    • 行动 :Agent 决定调用 mcp的对应 工具,并附带提取出的参数。
    • 执行:Agent Executor 找到对应的 McpTool 实例,并调用其 _arun 方法。
    • 桥接 (_arun 方法):_arun 方法通过 mcp_session 向 mcp_server.py 发起真正的工具调用请求。
    • MCP 服务器工作,返回参数或者其他
    • 观察 :_arun 方法接收到服务器返回的 JSON,将其作为字符串返回给 Agent Executor。
    • 再次思考:Agent Executor 将工具返回的结果(观察)再次发给 LLM,说:"我调用工具后得到了这个 JSON,请你根据它,并结合我们之前的对话,生成一个最终的、对用户友好的回复。"
    • 最终输出:通过prompt,LLM 生成一段总结性的文字,例如:"好的,我已经完成了因果分析。结果显示,干预变量对结果变量有显著的正向影响..."。
    python 复制代码
    agent_response = await agent_executor.ainvoke({
            "input": text,
            "chat_history": chat_history
        })
    final_output_summary = agent_response.get("output", "抱歉,我在处理时遇到了问题。")

3. 观察总结agent调用

我们打印出agent的中间过程就会发现包括以下几个部分

  1. 'chat_history': [ HumanMessage(content='你好', ...), AIMessage(content='你好!有什么我可以 帮助你的吗?', ...), // ... 大量的历史消息 ... ] 这里的就是所有的已经格式化的历史消息

  2. 'intermediate_steps': [ ( ToolAgentAction(...), '{"success": true, ... }' ) ] 这里就是工具的调用,我们进行拆解 这里的是agent的行动日志,其中包括了一个mcp_tool,然后包括了参数的输出 这整个字典就是传递给 McpTool._arun 方法的 kwargs。

    python 复制代码
    ToolAgentAction(
        tool='perform_causal_analysis', 
        tool_input={'filename': '1.csv', 'username': 'test_user'}, 
        log="...", 
        message_log=[...], 
        tool_call_id=''
    )

    接下来就是'{"success": true, "message": "因果分析成功完成。", "data": {"nodes": [...], "edges": [...]}, "raw_results": {...}, "analyzed_filename": "1.csv"}' 这里就是agent调用完mcp之后的结果,那么我们可以看到这里有McpTool._arun 方法返回的原始字符串。它就是 MCP 服务器 在执行完因果分析后,返回的未经任何处理的 JSON 响应。

  3. 'output':xx这是 LLM 在看到了"行动"(Action)和"观察"(Observation)的全过程后,根据 intermediate_steps 中的信息,并遵循你在 prompt 中设定的指示(例如,"不要逐字重复JSON","使用Markdown格式","用因果术语回答"等),为用户精心生成的、易于理解的自然语言回复。

相关推荐
weikuo05063 小时前
【手搓大模型】从零手写GPT2 — Attention
llm
weikuo05063 小时前
【手搓大模型】从零手写GPT2 — Embedding
llm
小Lu的开源日常4 小时前
AI模型太多太乱?用 OpenRouter,一个接口全搞定!
人工智能·llm·api
Baihai IDP5 小时前
AI 系统架构的演进:LLM → RAG → AI Workflow → AI Agent
人工智能·ai·系统架构·llm·agent·rag·白海科技
柠檬豆腐脑6 小时前
Trae-Agent 内置工具深度解析
python·llm·agent
堆栈future6 小时前
深度剖析Manus:如何打造低幻觉、高效率、安全可靠的Agentic AI系统
llm·aigc·mcp
AI大模型7 小时前
大模型炼丹术(七):LLM微调实战:训练一个垃圾邮件分类器
程序员·llm·agent
_一条咸鱼_8 小时前
LangChain多模态提示词设计探索的源码级深度剖析(16)
人工智能·面试·langchain
_一条咸鱼_9 小时前
LangChain输出解析器的作用与类型解析(17)
人工智能·面试·langchain