大疆无人机:如何革新雪崩救援行动
2026-07-14 3398501
2026-07-14 0

在之前开发 Agent 时,我们直接在项目里用 LangChain 的 tool() 函数定义 Tool:
javascript
复制代码const readFileTool = tool(
async ({ FilePath }) => { ... },
{ name: 'read_file', description: '...', schema: ... }
);
这种做法有两个明显的问题:
理想的方案是:Tool 应该独立于 LLM,可以本地/远程、跨进程、跨语言调用。
这就是 MCP(Model Context Protocol)要解决的问题。
MCP 是 LLM 与工具、资源之间的通信协议。它的核心价值在于标准化:


不管是本地工具还是远程工具,Agent 想跨进程调用某个工具,通过 MCP 协议就可以完成。MCP 出现的背景是:Tool 已经编写了很多,但只能在不同项目中使用不同语言不断复写。
项目使用 LangChain 的 MCP 适配器:
bash
复制代码pnpm i @langchain/mcp-adapters @modelcontextprotocol/sdk zod dotenv
@langchain/mcp-adapters:LangChain 官方提供的 MCP 适配器,让 LangChain Agent 可以连接 MCP Server@modelcontextprotocol/sdk:MCP 协议的标准 SDK,用于编写 Server 端zod:参数校验在 my-mcp-server.mjs 中,我创建了一个 MCP Server,注册了一个 Tool 和一个 Resource。
javascript
复制代码import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0'
});
query-userjavascript
复制代码// 模拟数据库,实际项目可以换成真实的数据库连接
const database = {
users:{
'001':{ id:'001', name:'许', email:'[email protected]', role:'admin' },
'002':{ id:'002', name:'锋', email:'[email protected]', role:'user' },
'003':{ id:'003', name:'聂', email:'[email protected]', role:'user' }
},
};server.registerTool(
'query-user',
{
description: '查询数据库中的用户信息。输入用户ID,返回该用户的详细信息(姓名、邮箱、角色)',
inputSchema: {
userId: z.string().describe('用户ID,例如:001、002、003')
}
},
async ({ userId }) => {
const user = database.users[userId];
if (!user) {
return {
content: [
{ type: 'text', text: `用户ID ${userId}不存在。可用的ID:001,002,003` }
]
};
}
return {
content: [
{
type: 'text',
text: `用户 ${user.id} 的信息是:姓名:${user.name}, 邮箱:${user.email}, 角色:${user.role}`
}
]
};
}
);
registerTool 接收三个参数:
description(告诉 LLM 这个工具能做什么)和 inputSchema(用 Zod 定义参数结构)在实际业务中,database 可以换成真实的数据库、API 调用或其他数据源。这里的处理逻辑是:如果查询到用户,返回用户信息;如果没查到,返回友好的错误提示,告诉调用方可用的 ID 有哪些。
javascript
复制代码server.registerResource(
'使用指南', // 资源名称
'docs://guide', // URI,MCP 协议中资源的唯一标识
{
description: 'MCP Server 使用指南',
mimeType: 'text/plain'
},
async () => {
return {
contents: [
{
uri: 'docs//guide',
mimeType: 'text/plain',
text: `
MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。
`
}
]
};
}
);
Resource 和 Tool 的关键区别:
注册 Resource 需要指定一个 URI(docs://guide),Client 通过这个 URI 来读取资源内容。mimeType 告诉 Client 返回内容的格式,text/plain 表示纯文本。
javascript
复制代码const transport = new StdioServerTransport();
await server.connect(transport);
StdioServerTransport 使用标准输入输出进行通信,适合本地进程间通信。启动后 Server 会进入“伺服状态”,等待 Client 通过 stdin/stdout 发送请求。
在 langchain-mcp-test.mjs 中,我用 LangChain 的 MultiServerMCPClient 连接 MCP Server。

javascript
复制代码import { MultiServerMCPClient } from '@langchain/mcp-adapters';const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': {
command: 'node',
args: ['D:/workspace/yjs_ai/ai/agent_in_action/mcp-demo/src/my-mcp-server.mjs']
}
}
});
MultiServerMCPClient 是 LangChain 提供的适配器,可以同时连接多个 MCP Server。每个 Server 通过 command + args 启动一个子进程——相当于 Client 进程会 fork 一个子进程来运行 Server。
javascript
复制代码const tools = await mcpClient.getTools();
const res = await mcpClient.listResources();
getTools() 返回一个 Tool 数组,可以直接用于 model.bindTools(tools)。这些 Tool 背后是通过 MCP 协议跨进程调用的——调用时 Client 通过 stdio 向 Server 发送请求,Server 执行后返回结果。

listResources() 返回一个对象,键是 Server 名称,值是 Resource 列表,结构如下:
javascript
复制代码{
'my-mcp-server': [
{ uri: 'docs://guide', name: '使用指南', description: '...' }
]
}
Resource 作为只读数据,最好的用途是作为 SystemMessage 的一部分,成为 LLM 的上下文。

javascript
复制代码let resourceContent = '';
for (const [serverName, resources] of Object.entries(res)) {
for (const resource of resources) {
const content = await mcpClient.readResource(serverName, resource.uri);
resourceContent += content[0].text;
}
}// 将资源内容作为 SystemMessage
const messages = [
new SystemMessage(resourceContent),
new HumanMessage(query)
];
Object.entries() 把对象的 key 和 value 组成一个二维数组,key 在前面,value 在后面:
javascript
复制代码const obj = { 'bytedance': ['AI全栈开发','Agent工程师'], 'tecent': ['后端开发'] };
for (let [key, value] of Object.entries(obj)) {
console.log(key, value);
}
// 输出:bytedance ['AI全栈开发','Agent工程师']
// tecent ['后端开发']
这种遍历方式比 for...in 更清晰,因为 for...in 会遍历原型链上的属性,而 Object.entries() 只处理对象自身的可枚举属性。
for...in vs for...of 的区别
这里有两种方式遍历对象:
javascript
复制代码const obj = {
'bytedance': ['AI全栈开发','Agent工程师'],
'tecent': ['后端开发','Agent 工程师'],
'163': ['前端开发']
};// 方式一:for...in
for (let key in obj) {
console.log(key, obj[key]);
}// 方式二:for...of + Object.entries()
for (let [key, value] of Object.entries(obj)) {
console.log(key, value);
}
两者输出结果相同,但底层机制完全不同:
| 特性 | for...in | for...of |
|---|---|---|
| 遍历内容 | 对象的可枚举属性名(包括原型链上的) | 可迭代对象的值 |
| 适用对象 | 普通对象 | 数组、字符串、Map、Set、类数组等可迭代对象 |
配合 Object.entries() | 不需要 | 需要,因为普通对象本身不可迭代 |
| 返回值 | 属性名(key) | 元素值(value) |
| 是否遍历原型链 | 会(需要用 hasOwnProperty 过滤) | 不会,只遍历自身元素 |
简单理解:
for...in 是给对象用的,遍历的是 keyfor...of 是给数组/可迭代对象用的,遍历的是 valueObject.entries(obj) 将对象转换成 [[key, value], ...] 的二维数组,让它变成可迭代的这就是为什么在遍历对象时,for...of + Object.entries() 更推荐——它只处理对象自身的属性,不会意外遍历到原型链上的属性。for...in 虽然写法简单,但如果不做过滤,可能拿到意想不到的属性。
获取 Tools 后,绑定到模型,进入 Agent 循环:
javascript
复制代码const modelWithTools = model.bindTools(tools);async function runAgentWithTools(query, maxIterations = 30) {
const messages = [
new SystemMessage(resourceContent),
new HumanMessage(query)
];
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen(`正在等待AI思考,第${i}轮...`));
const response = await modelWithTools.invoke(messages);
messages.push(response);
if (!response.tool_calls || !response.tool_calls.length) {
console.log(`nAI 最终回复: n${response.content}`);
return response.content;
}
console.log(chalk.bgBlue(`检测到${response.tool_calls.length}个工具调用`));
console.log(chalk.bgBlue(`工具调用: ${response.tool_calls.map(t => t.name).join(',')}`));
for (const toolCall of response.tool_calls) {
const foundTool = tools.find(t => t.name === toolCall.name);
if (foundTool) {
const toolResult = await foundTool.invoke(toolCall.args);
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id
}));
}
}
}
return messages[messages.length - 1].content;
}
tools.find() 匹配成功时不会再往后遍历,返回第一个匹配项。这是 find 方法的特点,适合查找唯一工具。如果找不到对应的 Tool,foundTool 为 undefined,这里会跳过不处理。
这里的 tools 是通过 MCP 协议跨进程获取的,调用 foundTool.invoke() 时,LangChain 的适配器会通过 MCP 协议把请求发送到 Server 进程,Server 执行后返回结果。整个过程对 Agent 来说是透明的——它只知道调用了一个 Tool,不知道这个 Tool 在另一个进程里运行。
运行 node langchain-mcp-test.mjs 后,程序执行完任务后不会自动退出。
原因在于进程关系:
langchain-mcp-test.mjscommand: 'node' 启动,运行 my-mcp-server.mjsMCP Server 是一个“伺服”进程——启动后会一直等待请求,不会主动退出。主进程完成任务后,由于子进程还在运行,主进程不会自动退出(需要等待子进程结束)。
解决方案是调用 mcpClient.close():
javascript
复制代码await mcpClient.close();
close() 会关闭所有子进程与通信通道,释放进程资源。如果忘记调用,脚本会一直挂着不退出,占用内存和进程资源。
通过这个项目,我理解了:
listResources + readResource 获取文档内容,注入 SystemMessage。close() 释放资源。Object.entries() 遍历对象:比 for...in 更清晰,只处理自身属性。Object.entries() 和 for...in 遍历对象的区别是什么? 为什么 Object.entries() 更推荐?close() 才能让进程退出? 如果不关闭会有什么后果?