从使用到开发:掌握MCP协议构建自定义LangChain Agent工具

📅 2026/8/8 3:47:05
从使用到开发:掌握MCP协议构建自定义LangChain Agent工具
1. 项目概述从“用工具”到“造工具”的范式跃迁如果你正在用LangChain构建AI应用那么“MCP”这个词最近一定频繁地出现在你的视野里。它不再是那个“物料控制计划”或者“主控程序”在AI Agent的世界里它特指Model Context Protocol。简单来说MCP定义了一套标准让AI模型尤其是大语言模型能够安全、可控地调用外部工具和数据源。过去几个月我亲眼见证了社区从讨论“哪个MCP Server好用”到“如何为自己的业务定制一个MCP Server”的转变。这标志着一个关键拐点我们不再满足于仅仅组装现成的乐高积木使用别人提供的工具而是开始学习烧制自己的砖块编写符合自己业务逻辑的工具。这个过程正是AI应用从“玩具”走向“生产力”的核心一步。为什么这个转变如此重要因为现成的工具无论是搜索、数据库查询还是文件操作解决的往往是通用问题。一旦你的Agent需要与内部CRM系统交互、调用特定的审批流程API或者处理公司独有的数据格式通用工具就立刻失灵。这时拥有自己编写MCP Server的能力就等于给你的AI应用装上了专属的机械臂让它能直接操作你业务环境里的真实对象。本文将基于我近期将一个内部日志分析系统封装成MCP Server并集成到LangChain Agent的实战经验拆解从概念理解、协议剖析、动手实现到集成调试的全过程。无论你是想为团队内部搭建一个智能助手还是希望将AI能力深度嵌入产品掌握MCP开发都将为你打开一扇新的大门。2. MCP核心机制与LangChain集成原理拆解在动手写代码之前我们必须先吃透MCP到底是如何工作的。把它想象成AI模型与外部世界之间的一个“标准化插座”和“安全协议”。2.1 MCP协议的三层架构资源、工具与提示词MCP协议的核心思想是将外部能力抽象为三种类型并通过JSON-RPC进行通信资源Resources这是数据的抽象。一个资源可以是一个文件、一个数据库表的最新视图、一个API的实时状态快照。资源通过唯一的URI标识并且可以附带一个用于显示的“文本表示”方便LLM理解其内容。例如file:///logs/app.log可以是一个资源其文本表示是日志文件的前100行内容。工具Tools这是操作的抽象。一个工具代表一个可执行的动作比如“搜索文件”、“执行SQL查询”、“发送邮件”。每个工具都有明确的输入参数JSON Schema定义和输出格式。当LLM决定使用某个工具时它会提供参数MCP Server执行后返回结果。提示词模板Prompts这是交互范式的抽象。它允许Server预定义一些带有占位符的提示词模板。Client如LangChain可以获取模板列表并通过填充占位符来动态生成最终提示词引导LLM进行特定风格的思考或输出。这对于构建复杂、多步骤的Agent工作流非常有用。MCP Client如LangChain通过JSON-RPC与MCP Server建立连接。初始化的“握手”过程initialize交换后Client会通过list_resources、list_tools、list_prompts等方法发现Server提供了哪些能力。之后整个交互就变成了LLM根据任务决定调用哪个工具call_tool或读取哪个资源read_resourceServer执行并返回结果LLM基于结果继续思考或输出。2.2 LangChain如何与MCP协同工作LangChain作为最流行的AI应用框架之一其价值在于提供了构建复杂链Chain和智能体Agent的高层抽象。它内置的AgentExecutor就像一个大脑的调度中心负责理解用户指令、规划步骤、选择工具、执行并解释结果。在引入MCP之前我们为LangChain Agent添加工具通常需要直接编写Python函数并用tool装饰器包装。这种方式紧密耦合且工具的逻辑分散在应用代码中。MCP的引入改变了这一点。通过LangChain的MCPIntegration或相关第三方库如langchain-mcp-adaptor我们可以将任何一个MCP Server“挂载”到LangChain的Agent上。这个过程是动态的连接LangChain Agent在启动时连接到指定的MCP Server可能是本地进程也可能是网络服务。发现Agent自动获取该Server提供的所有工具列表。集成这些工具被无缝地添加到Agent的工具箱Toolkit中。调用当Agent运行时它可以像使用原生工具一样调用这些来自MCP Server的工具。LangChain负责将Agent的调用意图转换为MCP协议的call_tool请求并将结果返回给Agent。这种架构带来了巨大的灵活性解耦工具的逻辑完全独立于AI应用可以用任何语言编写只要遵循MCP协议。热更新更新或新增工具只需重启或更新MCP ServerLangChain Agent可以动态重新发现无需修改主应用代码。安全性工具运行在独立的Server进程中权限和资源访问可以被严格控制避免了AI应用主进程权限过高的问题。复用性同一个MCP Server可以被多个不同的AI应用或Agent使用。注意当前LangChain对MCP的原生支持仍在快速演进中。你可能需要关注langchain-core中关于MCPClient的更新或者使用社区维护的适配库。实践中直接使用MCP协议的Python SDK如mcp编写Server然后通过标准输入输出stdio或HTTP与LangChain集成是目前最稳定可靠的方式。3. 实战构建你的第一个MCP Server——内部日志查询工具理论讲得再多不如动手写一行代码。我们来实现一个实用的MCP Server一个内部日志查询工具。假设我们有一个按日期滚动的应用日志目录我们希望AI Agent能够回答诸如“昨天下午的错误日志有哪些”或“用户‘张三’最近登录成功了吗”之类的问题。3.1 环境准备与项目初始化首先确保你的Python环境在3.8以上。我们使用官方推荐的mcpSDK来开发Server。# 创建项目目录并进入 mkdir log-query-mcp-server cd log-query-mcp-server # 创建虚拟环境推荐 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装核心依赖 pip install mcp # 安装用于CLI测试的客户端可选但推荐 pip install mcp[cli]接下来创建我们的主文件server.py。3.2 定义工具Tools让Agent能“动手”MCP Server的核心是暴露工具。我们首先定义一个用于搜索日志的工具。# server.py import os from datetime import datetime, timedelta from typing import Any from mcp.server import Server from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types # 初始化Server实例 app Server(log-query-server) # 假设日志根目录 LOG_BASE_DIR /var/log/myapp app.list_tools() async def handle_list_tools() - list[types.Tool]: 返回Server提供的工具列表 return [ types.Tool( namesearch_logs, description在应用日志中搜索包含特定关键词的行。可以按日期范围和日志级别过滤。, inputSchema{ type: object, properties: { keyword: { type: string, description: 要搜索的关键词如‘ERROR’‘UserLogin’或一个用户ID。 }, days_back: { type: integer, description: 查找多少天内的日志默认为1今天。, default: 1 }, level: { type: string, description: 日志级别过滤如‘INFO’‘WARN’‘ERROR’。留空则不过滤。, enum: [INFO, WARN, ERROR, ], default: } }, required: [keyword] } ) ] app.call_tool() async def handle_call_tool(name: str, arguments: dict[str, Any]) - list[types.TextContent]: 处理工具调用请求 if name search_logs: return await handle_search_logs(**arguments) else: raise ValueError(f未知工具: {name}) async def handle_search_logs(keyword: str, days_back: int 1, level: str ) - list[types.TextContent]: 实际执行日志搜索的逻辑 results [] end_date datetime.now() start_date end_date - timedelta(daysdays_back) current_date start_date while current_date end_date: date_str current_date.strftime(%Y-%m-%d) log_file_path os.path.join(LOG_BASE_DIR, fapp-{date_str}.log) if os.path.exists(log_file_path): try: with open(log_file_path, r, encodingutf-8) as f: for line_num, line in enumerate(f, 1): # 基础关键词匹配 if keyword.lower() in line.lower(): # 可选日志级别过滤 if level and f[{level}] not in line: continue results.append(f{date_str} L{line_num}: {line.strip()}) except Exception as e: results.append(f读取日志文件 {log_file_path} 时出错: {e}) current_date timedelta(days1) output_text f搜索关键词 ‘{keyword}‘最近{days_back}天级别‘{level if level else ‘任何’}‘的结果\n if results: output_text \n.join(results[:50]) # 限制返回行数避免上下文过长 if len(results) 50: output_text f\n... 以及另外 {len(results) - 50} 条记录。 else: output_text 未找到匹配的日志条目。 return [types.TextContent(typetext, textoutput_text)]代码解读与注意事项工具定义app.list_tools这里我们定义了一个名为search_logs的工具。inputSchema部分至关重要它用JSON Schema精确描述了工具所需的参数。LLM通过LangChain会根据这个描述来生成调用参数。描述写得越清晰LLM调用得就越准确。工具实现handle_search_logs这是实际的业务逻辑。它按日期遍历日志文件进行关键词匹配和过滤。注意我们限制了返回的行数50行这是为了避免一次返回过多数据撑爆LLM的上下文窗口。错误处理文件读取被包裹在try-except中确保Server不会因为单个文件问题而崩溃并将友好错误信息返回给Agent。异步支持MCP SDK基于异步async/await。如果你的工具涉及网络IO如调用另一个API可以轻松地用async函数实现提高并发性能。3.3 暴露资源Resources让Agent能“看见”除了主动操作的“工具”我们还可以提供被动的“资源”让Agent能直接读取某些信息的快照。例如暴露当前服务器的状态摘要。# 在 server.py 中继续添加 app.list_resources() async def handle_list_resources() - list[types.Resource]: 返回Server提供的资源列表 return [ types.Resource( urifile:///server/status, nameserver-status-summary, description当前服务器状态摘要包括磁盘空间和最近错误数。, mimeTypetext/plain ) ] app.read_resource() async def handle_read_resource(uri: str) - list[types.TextContent]: 处理读取资源请求 if uri file:///server/status: return await handle_read_server_status() raise ValueError(f未知资源: {uri}) async def handle_read_server_status() - list[types.TextContent]: 生成服务器状态摘要 import shutil status_lines [] # 获取磁盘使用情况 try: disk_usage shutil.disk_usage(LOG_BASE_DIR) usage_percent (disk_usage.used / disk_usage.total) * 100 status_lines.append(f磁盘使用率: {usage_percent:.1f}% ({disk_usage.used // (1024**3)}GB / {disk_usage.total // (1024**3)}GB)) except Exception as e: status_lines.append(f获取磁盘信息失败: {e}) # 获取最近1小时错误日志数示例 error_count 0 try: log_file_path os.path.join(LOG_BASE_DIR, fapp-{datetime.now().strftime(%Y-%m-%d)}.log) if os.path.exists(log_file_path): with open(log_file_path, r, encodingutf-8) as f: for line in f: if [ERROR] in line: # 简单时间判断实际应解析时间戳 error_count 1 except Exception as e: pass status_lines.append(f今日错误日志数粗略: {error_count}) status_text 服务器状态摘要:\n \n.join(status_lines) return [types.TextContent(typetext, textstatus_text)]资源使用的场景当用户问“服务器现在健康吗”时Agent可以先read_resource获取状态摘要再结合其内容进行推理和回答无需调用一个需要参数的“工具”。资源更适合提供静态或缓存的视图。3.4 运行与测试你的MCP Server现在让我们把这个Server跑起来并用MCP CLI客户端测试一下。# 在 server.py 末尾添加启动代码 import asyncio async def main(): async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, InitializationOptions( server_namelog-query-server, server_version0.1.0 ) ) if __name__ __main__: asyncio.run(main())在终端运行python server.py此时Server会在标准输入输出stdio上等待连接。这是MCP最常见的一种传输方式特别适合与本地父进程如LangChain集成。打开另一个终端使用MCP CLI进行测试# 假设你将上述代码保存为 server.py # 通过管道将CLI连接到你的Server进程 mcp dev server.py在打开的CLI交互界面中你可以输入/list查看Server提供的所有工具和资源。/call search_logs {keyword: ERROR, days_back: 2}调用工具搜索最近两天的错误日志。/read file:///server/status读取服务器状态资源。如果一切正常你将看到工具返回的搜索结果。这个测试确保了你的MCP Server协议实现是正确的。4. 将自定义MCP Server集成到LangChain AgentServer准备好了下一步是让它成为LangChain Agent的“左膀右臂”。这里的关键是建立一个连接桥梁。4.1 使用Stdio进行本地集成对于本地开发最直接的方式是通过子进程标准输入输出集成。我们需要一个“适配器”将MCP Server进程的stdio转换为LangChain能识别的工具列表。以下是一个使用subprocess和mcp客户端库进行集成的示例# langchain_agent_with_mcp.py import asyncio from langchain.agents import AgentExecutor, create_openai_tools_agent from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain.tools import Tool from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client # 1. 定义MCP工具加载函数 async def load_mcp_tools(): 启动MCP Server进程并加载其工具 # 配置MCP Server进程参数 server_params StdioServerParameters( commandpython, # 你的Python解释器 args[/path/to/your/server.py], # 你的MCP Server脚本绝对路径 ) tools [] async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: # 初始化会话 await session.initialize() # 获取Server提供的所有工具 response await session.list_tools() for tool_info in response.tools: # 为每个MCP工具创建一个LangChain Tool包装器 async def mcp_tool_func(**kwargs): # 这个内联函数会捕获tool_info.name tool_name tool_info.name async with stdio_client(server_params) as (r, w): async with ClientSession(r, w) as inner_session: await inner_session.initialize() result await inner_session.call_tool(tool_name, argumentskwargs) # 假设返回的是TextContent return \n.join([c.text for c in result.content if c.type text]) # 创建LangChain Tool对象 tool Tool( nametool_info.name, descriptiontool_info.description or , funclambda **kwargs: asyncio.run(mcp_tool_func(**kwargs)), # 注意这里需要处理异步同步化 args_schemaNone, # 可以基于tool_info.inputSchema创建Pydantic模型 ) tools.append(tool) return tools # 注意上述异步加载函数在同步的LangChain环境中直接使用较复杂。 # 更常见的做法是在Agent启动前预先运行一次load_mcp_tools将工具加载到内存。 # 或者使用专为LangChain设计的适配器库如 langchain-mcp-adaptor。 # 2. 构建Agent简化版假设工具已加载 # 假设我们已经通过其他方式获得了tools列表 # tools [ ... your mcp tools ... ] # 添加一些LangChain原生工具可选 from langchain_community.tools import DuckDuckGoSearchRun search_tool DuckDuckGoSearchRun() all_tools tools [search_tool] # 3. 创建LLM和Prompt llm ChatOpenAI(modelgpt-4o, temperature0) prompt ChatPromptTemplate.from_messages([ (system, 你是一个有帮助的助手可以查询服务器日志和搜索网络。请根据工具描述合理使用它们。), (user, {input}), MessagesPlaceholder(variable_nameagent_scratchpad), ]) # 4. 创建Agent和Executor agent create_openai_tools_agent(llm, all_tools, prompt) agent_executor AgentExecutor(agentagent, toolsall_tools, verboseTrue) # 5. 运行测试 async def main(): # 这里需要先异步加载工具为简化示例我们跳过。 # 实际使用时应优化工具加载流程避免每次调用都创建新进程。 result await agent_executor.ainvoke({input: 帮我查一下昨天有没有关于‘登录超时’的错误日志}) print(result[output]) if __name__ __main__: asyncio.run(main())重要提示上述代码是一个原理性示例直接在生产中使用会有性能问题每次调用都创建新进程。生产环境需要考虑Server进程常驻将MCP Server作为独立的HTTP服务或常驻后台进程运行。客户端连接池在LangChain侧使用一个持久的MCP客户端连接或实现连接池。使用社区适配器积极寻找和评估如langchain-mcp-adaptor这类库它们封装了这些复杂性。4.2 通过HTTP/Socket进行远程集成生产环境推荐对于生产部署更推荐将MCP Server部署为HTTP或WebSocket服务。这样LangChain应用可以通过网络远程调用实现解耦、负载均衡和独立扩缩容。mcpSDK也支持创建HTTP Server。你需要修改server.py的启动部分# server_http.py from mcp.server import Server from mcp.server.http import create_http_app import uvicorn app Server(log-query-http-server) # ... 之前的工具和资源定义保持不变 ... # 创建FastAPI应用 fastapi_app create_http_app(app, development_modeTrue) if __name__ __main__: uvicorn.run(fastapi_app, host0.0.0.0, port8000)运行python server_http.py你的MCP Server就在http://localhost:8000上提供了HTTP端点。LangChain端可以使用对应的HTTP客户端库来连接和调用工具。5. 高级技巧与生产环境避坑指南当你掌握了基础开发后下面这些从实战中总结的经验能帮你走得更稳。5.1 工具设计哲学让LLM用得顺手设计给LLM用的工具和设计给人用的API思路截然不同。单一职责一个工具只做一件事。不要设计一个handle_logs工具参数包含actionsearch|analyze|clean。应该拆分成search_logsanalyze_log_trendclean_old_logs三个工具。LLM更擅长从描述中理解简单直接的功能。描述即契约工具的description和参数的description是给LLM看的“说明书”。要用自然语言清晰说明工具的用途、每个参数的意义、以及输出的格式。例如“搜索应用日志。keyword参数支持模糊匹配。days_back默认为1即搜索今天和昨天的日志。返回结果将包含日志日期、行号和内容。”结构化输出尽可能让工具返回结构化的文本。例如用Markdown表格或清晰的条目列表。避免返回一大段无格式的文本这会增加LLM解析的负担和出错率。善用枚举对于有限的选项如日志级别[INFO,WARN,ERROR]一定要在参数的JSON Schema中使用enum定义。这能极大提高LLM提供正确参数的概率。5.2 错误处理与稳定性保障MCP Server的稳定性直接决定了Agent的可靠性。输入验证在工具函数内部务必对输入参数进行二次验证。即使LLM按照Schema调用也可能传空值或越界的值。友好的错误信息如“days_back参数必须为正整数”能帮助Agent进行纠正。超时与重试如果工具操作可能耗时较长如查询慢速数据库要在Server端实现超时控制并向Client返回明确的超时错误。LangChain Agent通常具备一定的错误处理和重试逻辑。资源隔离每个工具调用应尽可能独立避免共享可变状态。如果必须共享如连接池要处理好并发安全。健康检查为HTTP Server添加/health端点方便运维监控。5.3 性能优化与可观测性连接管理避免为每次工具调用都创建新的数据库连接或网络会话。在Server生命周期内管理好这些资源。结果缓存对于耗时的、数据变化不频繁的查询如“今天错误总数”可以在Server端实现短期缓存并在资源描述中注明缓存策略。日志与追踪在MCP Server内记录详细的日志包括收到的请求、处理耗时、错误信息。这对于调试Agent的决策过程至关重要。考虑集成OpenTelemetry来追踪跨服务的调用链。限流为防止被恶意或错误的Agent循环调用拖垮需要实现基本的限流机制。5.4 调试当Agent行为不符合预期时这是MCP开发中最常见的挑战。你的工具逻辑没错但Agent就是不用或者用错了。检查工具描述回到第一步用“LLM的视角”读一遍你的工具描述。是否足够清晰无歧义是否和用户问题能匹配上测试工具本身用MCP CLI直接调用你的工具传入各种边界参数确保其行为符合描述。观察Agent的思考过程将LangChain Agent的verbose设为True。你会看到Agent的思考链ReAct模式包括它考虑了哪些工具、为什么选择某个工具、它“认为”的参数应该是什么。这能直接暴露LLM对你工具的理解偏差。简化问题用一个最简单的用户问题测试。如果简单问题能正确调用复杂问题不能可能是你的Prompt或Agent设定需要优化引导LLM进行任务分解。利用MCP的提示词模板如果某个工具的使用需要特定的前置思考步骤可以在MCP Server中定义提示词模板引导LangChain在调用工具前先让LLM按照特定格式思考。从使用现成的MCP Server到自己动手编写这一步跨越的不仅仅是技术更是思维模式的转变。你不再仅仅是AI能力的消费者而是成为了AI与真实世界交互接口的设计师。这个过程初期会充满挑战比如如何精准地定义工具边界如何编写LLM友好的描述如何调试难以捉摸的Agent行为。但一旦你打通了这个闭环你会发现你能赋予AI的能力边界被极大地拓展了。那些曾经觉得必须写死在后端逻辑里的业务规则现在可以通过自然语言由AI Agent灵活地组合调用。这种将复杂业务能力“口语化”并交付给AI调度的体验是构建下一代智能应用的核心竞争力。我个人的体会是开始可以从一个非常具体、微小但实用的工具做起比如一个查询本周会议安排的工具或者一个格式化代码片段的工具。在实现和集成的过程中你会迅速积累起对MCP协议和LangChain Agent协同工作方式的直觉这才是最宝贵的经验。