一.agent定义自己的工具,并绑定他,执行他。
//定义大模型
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL
}
})
//引入工具
const tools = [
readFileTool,
writeTool,
executeCommandTool,
listDirectoryTool
]
//大模型绑定工具
const modelWithTool = model.bindTools(tools);
const messages=[
new SystemMessage(`
你是一个项目管理助手,使用工具完成任务,。。。
`),
new HumanMessage('你帮我干个啥呢')
]
//绑定工具后的大模型执行message
const response = await modelWithTool.invoke(messages);
//看一下需要执行哪个工具
for(const toolCall of response.tool_calls){
const foundTool = tools.find(t=>t.name===toolCall.name);
if(foundTool){
// 这是调用大语言模型(LLM),对应Agent的「思考/决策」环节:
const toolResult = await foundTool.invoke(toolCall.args);
messages.push(new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id
}))
}
}
return messages[messages.length-1].content;
代码逻辑:
- 定义工具,使用import {tool } from '@langchain/core/tools';
- 定义大模型,import {ChatOpenAI} from '@langchain/openai';
- 大模型和工具相互绑定
- 执行invoke()执行提示
- 大模型通过response.tool_calls他需要的工具,工具执行invoke()获取结果。
定义工具的办法:
import {tool } from '@langchain/core/tools';
import fs from 'node:fs/promises';//node之后的规范写法
import path from 'node:path';
import {spawn} from 'node:child_process';
import {z} from 'zod';
//1.读取文件
const readFileTool = tool(
async({filePath})=>{
const content = await fs.readFile(filePath, 'utf-8');
return `文件内容: ${content}`
},{
name: 'read_file',
description: '读取文件',
schema: z.object({
filePath: z.string().describe('文件路径')
})
}
)
二.利用MCP定义远程工具,然后使用他
服务端代码
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs/promises";
import path from "path";
import { fileURLToPath } from "url";
import { resolve } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const dataBase = {
users: {
'001': { id: '001', name: '张三', age: 30 },
'002': { id: '002', name: '李四', age: 28 },
'003': { id: '003', name: '王五', age: 35 },
'004': { id: '004', name: '赵六', age: 32 },
'005': { id: '005', name: '孙七', age: 29 },
'006': { id: '006', name: '周八', age: 27 },
'007': { id: '007', name: '吴九', age: 31 },
'008': { id: '008', name: '郑十', age: 33 },
}
};
// ✅ 1. 创建 MCP Server
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
server.tool(
"Query_user",
"查询数据库中用户信息,输入用户ID,返回该用户的详细信息(id,name,age)",
{
userId: z.string().describe('用户 id,例如 "001"')
},
async ({ userId }) => {
const user = dataBase.users[userId];
if (!user) {
// ✅ 严格标准格式:外层是对象,content是数组,元素必须是{type:"text", text:字符串}
return {
content: [
{
type: "text",
text: `查询失败:用户 ${userId} 不存在`
}
],
// 可选:标记为错误结果,不影响返回格式
isError: true
};
}
return {
content: [
{
type: "text",
text: `查询成功,用户信息如下:\n${JSON.stringify(user, null, 2)}`
}
]
};
}
);
// ✅ 2. 注册 Resource(重点)
server.resource(
"使用指南", // resource name
"docs://guide", // 这个是读取资源的名字,在client的时候使用
async (uri) => {
const filePath = resolve(__dirname, 'docs', 'guide.md');// 读取真实的本地guide.md文件(路径:当前脚本目录下的docs/guide.md)
let content = '';
try {
content = await fs.readFile(filePath, 'utf-8');
} catch (err) {
content = err.message;
}
return {
contents: [
{
uri: uri.href,
mimeType: "text/plain",
text: content
}
]
};
}
);
// ✅ 4. 启动 Server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("✅ MCP Resource Server is running...");
客户端代码
import 'dotenv/config'
import { MultiServerMCPClient } from'@langchain/mcp-adapters';
import { ChatOpenAI } from'@langchain/openai';
import chalk from'chalk';
import { HumanMessage, ToolMessage } from'@langchain/core/messages'
const model = new ChatOpenAI({
modelName:"qwen-plus",
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
}
});
const mcpClient = new MultiServerMCPClient({
"my-mcp-server": {
command: "node",
args: ["src/mcp-test/my-mcp-server.mjs"],
cwd: process.cwd(), // 强制用当前终端的工作目录作为基准,避免路径错位
// 关键新增:显式指定stdio传输协议
transport: "stdio"
}
});
const tools = await mcpClient.getTools();
const modelWithTools = model.bindTools(tools);
async function runAgentWithTools(query,maxIterations = 30) {
const messages = [
new HumanMessage(query)
];
for (let i= 0; i < maxIterations; i++) {
console.log(chalk.bgGreen(`正在等待 AI思考...`));
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 检查是否有工具调用
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(` AI 最终回复:${response.content}`);
return response.content;
}
console.log(chalk.bgBlue( `检测到 ${response.tool_calls.length} 个工具调用~`));
// 执行工具调用
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,
}));
}else{
console.log(chalk.bgRed(`找不到名为 ${toolCall.name} 的工具`));
return `找不到名为 ${toolCall.name} 的工具`;
}
}
}
return messages[messages.length - 1].content;
}
console.log( 12121212);
//await runAgentWithTools("查一下用户 002 的信息?");
//const res = await mcpClient.listResources("my-mcp-server");
const res = await mcpClient.readResource(
"my-mcp-server",
"docs://guide"//服务端有很多个读取静态资源的代码,你要说清楚是哪个资源,我这里用的是guide.md文件,如果不写,client就不知道你要读取的是谁。所以在教程里面不写是不行的
);
console.log(res)
await mcpClient.close();
你自己的本地的tool,就用model.bindTools绑定就好了,现在如果是远程的tools呢?就要用到MCP了,MCP是什么就参考这个文章:cloud.tencent.com/developer/a…
他就是个协议,很像https,在互联网上的tools都可以通过MCP引进来,自己用。
1.静态资源获取
在上面代码里面
server.resource(
"使用指南", // resource name
"docs://guide", // 这个是读取资源的名字,在client的时候使用
async (uri) => {
const filePath = resolve(__dirname, 'docs', 'guide.md');// 读取真实的本地guide.md文件(路径:当前脚本目录下的docs/guide.md)
let content = '';
try {
content = await fs.readFile(filePath, 'utf-8');
} catch (err) {
content = err.message;
}
return {
contents: [
{
uri: uri.href,
mimeType: "text/plain",
text: content
}
]
};
}
);
是获取静态资源的,前端调用的时候,就需要指定资源的名字
如果不指定,就不知道你想要的获取的是哪个静态资源了。
2.工具使用
server定义了工具,还命名了工具名,但是在使用的时候,却没有指定工具名,那大模型具体使用哪个?
回复如下:
言外之意就是,工具是我开发的,工具的description很重要,大模型就是靠着description来决定到底要用哪个工具,开发者不需要手动定义。
三.使用别人的tool
import 'dotenv/config';
import { MultiServerMCPClient } from'@langchain/mcp-adapters';
import {ChatOpenAI} from'@langchain/openai';
import {HumanMessage, SystemMessage,ToolMessage} from'@langchain/core/messages';
import chalk from 'chalk'
const model = new ChatOpenAI({
modelName:'qwen-plus',
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
}
});
const mcpClient = new MultiServerMCPClient({
//这里可以引入很多个mcp服务,这里只引入高德地图的mcp服务
"amap-maps-streamableHttp":{
"url": "https://mcp.amap.com/mcp?key="+ process.env.GAODE_MAP_API_KEY,
"transport": "http",
},
//这里我要引入mcp自己定义的文件处理tools服务
"file-system": {
command: "npx",//这里用的npx是因为我本地安装了@modelcontextprotocol/server-filesystem,如果要全局安装则可以不用npx
args: [
"-y",
"@modelcontextprotocol/server-filesystem",
"E:/AITool/tool-test", // ← 必填:允许访问的根目录
],
},
//要大模型帮我们打开浏览器,然后自动处理网页的工具,这里用的是chrome-devtools-mcp工具包
"chrome-devtools":{
"command":"npx",
"args": [
"-y",
"chrome-devtools-mcp@latest"
]
}
})
const tools = await mcpClient.getTools();
const modelWithTools = model.bindTools(tools);
async function runAgentWithTools(query,maxIterations = 30) {
const messages = [
new HumanMessage(query)
];
for (let i= 0; i < maxIterations; i++) {
console.log(chalk.bgGreen(`正在等待 AI思考...`));
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 检查是否有工具调用
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(` AI 最终回复:${response.content}`);
return response.content;
}
// 执行工具调用
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);
let res = '';
if(typeof toolResult === 'string'){
res= toolResult;
}else if(toolResult && toolResult.text){
res= toolResult.text;
}
messages.push(new ToolMessage({
content: res,
tool_call_id: toolCall.id,
}));
}else{
console.log(chalk.bgRed(`找不到名为 ${toolCall.name} 的工具`));
return `找不到名为 ${toolCall.name} 的工具`;
}
}
}
return messages[messages.length - 1].content;
}
console.log( 12121212);
//await runAgentWithTools("北京南站附近的酒店,以及过去的路线");
//await runAgentWithTools("北京南站附近的5个酒店,以及去的路线,路线规划生成文档,保存到 E:/AITool/tool-test/test.md 文件里,并返回保存的路径。");
await runAgentWithTools("北京南站附近的酒店,最近的 3 个酒店,拿到酒店图片,然后用浏览器打开,一个图片对应一个浏览器标签页,并且把对应页面的标题改成酒店名字");
await mcpClient.close();
我们用高德地图的mcp拿到酒店地址,用file-system的文件处理工具在本地建了一个文件,然后把酒店信息存储在文件里,又利用chrome-devtools工具将酒店图片展示到浏览器上。
上述代码的关系形式:
思考问题1:
下图,为什么有的工具用stdio协议,有的使用http协议?他到底是谁来决定的?
在 MCP(Model Context Protocol)架构中,stdio 和 http(通常指 Streamable HTTP 或早期的 SSE)是两种不同的传输层协议(Transport) ,负责客户端(MCP Adapter)与服务端(MCP Server)之间传递消息。
决定使用哪种协议的,是服务端的部署形态、运行位置以及你在客户端代码中的配置。
本地通信用stdio 远程通信用http
开发者在定义tool的时候是可以规定的,比如:
// 配置本地 stdio 服务
const fileSystemServer = {
command: "node",
args: ["path/to/filesystem-server.js"],
transport: "stdio" // 👈 决定用 stdio
};
// 配置远程 http 服务
const amapServer = {
url: "https://mcp.amap.com/xxx",
transport: "http" // 👈 决定用 http
};
总结
- 本地工具(跑在自己电脑上的脚本、本地文件、本地浏览器):用
stdio,简单、快、安全。 - 远程服务(云端的 API、第三方开放平台如高德):用
http,跨网络、可共享。
一句话概括:谁决定的?是业务场景和你的客户端配置决定的。本地起子进程就用 stdio,调用云端网址就用 http。