你本地的LLM有时会胡乱使用tools?不妨用MCP规范LLM工具调用!

一个demo,带你体验创建自己的MCP

FunctionCalling Tools

有时你是否会遇到这样的情况: 我给 LLM 写了数个类似的Tools:

scss 复制代码
1. readFiles(path) // 读取整个文件
2. readFileLines(Path,start,end) // 读取指定行
3. getFileHead(path,n) // 读取前n行

我问LLM:"帮我看一下 config.json里写了什么",结果它却只返回了前10行,货不对板。这三个工具描述都包含"读取文件",LLM无法自主区分该用哪个。

这就叫LLM的决策 paralysis,也就是决策瘫痪。LLM在面对相似的工具时,缺乏一个规范让它在指定情况下能选择到指定的工具。

或者是这样的情况: 在开发过程中,我给LLM写的 Tools越来越多,结果LLM反应的速度越来越慢。

例如我定义了三十多个工具,每当我问一个问题,LLM就会把这30个工具的完整定义(名称+描述+参数JSON Schema)都会一股脑塞进上下文,留给LLM"思考和推理"的空间被严重挤压

这就叫LLM的感知过载

此时,我多么希望,有一套规范,能让LLM在诸多工具中,高效准确的找到相应的工具,解决决策瘫痪与感知过载的问题啊

MCP-模型上下文协议 (Model Context Protocol)

  • 制定一套标准,统一LLM与外部工具、系统和数据源的交互方式
  • 工具被定义为服务器(MCPServer)上可执行的功能模块,通过MCP协议暴露给客户端,供LLM调用

核心流程

rust 复制代码
客户端 -> 服务端 ->    ^^     LLM -> 服务端 -> 客户端
                      | |
                  MCPserver

MCP的核心能力:

  1. 工具搜索,LLM不需要一次性知道所有的工具,而是按需发现有哪些工具以供使用
  2. 程序化工具调用,LLM判断需要某个工具后,程序化触发调用。
rust 复制代码
LLM 返回 tool_calls -> 代码自动执行对应工具 -> 拿到结果再传回 LLM
  1. 工具使用示例,给LLM提供工具的使用返利,帮助它更准确地判断什么时候该用、怎么用

一个 MCP 服务端开发流程

我们的服务端用来接受前端的POST/chat,调用llm.chat()

事前准备,我们需要用到MCP,因此要pnpm下来

css 复制代码
pnpm i @modelcontextprotocol/sdk

Schema规范

Schema规范是附在工具定义里给LLM看的说明书,让LLM知道调用某个工具时参数该怎么填。

也就是说,LLM返回的工具调用参数要遵从Schema

例如我给LLM传的数据,按API格式组织

yaml 复制代码
{
  model: 'qwen2.5:14b',
  messages: [...],
  tools: [...],    // 工具定义里"包含"了 schema
  stream: false
}

后续我们引入 zod 来帮我们写JSONS chema

事前引入、创建mcp server

javascript 复制代码
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; 
import {z} from 'zod'
import {exec} from 'child_process'

// 创建的mcpserver不会跑在某个端口上,而是寄生在某个进程上,等待客户端链接
const server = new McpServer({
    name:'Demo',
    version:'1.0.0',
})

讲解一下引入的包用来干啥:

  1. McpServer用来注册工具
  • server.tool()挂载工具函数
  • 客户端调取listTools()的时候,他自动把注册过的工具清单返回给客户端,不需要我们手写返回逻辑。
  • 客户端调用callTool({name,arguments})的时候,自动根据name找到工具,返回结果
  1. transport用来跟进程交互
  2. 引入Zod,让JSON Schema的创建更加简洁且类型安全
  3. 引入的exec是为了后续创建一个子进程使用

注册一个工具函数,后续交给MCP

这里以一个简单的,读取当前目录下的文件工具函数。代码实现如下

javascript 复制代码
server.tool('listFiles','列出执行目录下的文件',{path:z.string()},({path})=>{
    return new Promise((resolve,reject)=>{
        // 用exec开一个子进程,才能在命令行执行
        exec(`dir ${path}`,(error,stdout,stderr)=>{
        // 错误处理
            if(error){
                console.error(`执行命令出错:${error}`)
                resolve({
                    content:[{type:'text',text:`执行命令出错:${error}`}]
                })
                return
            }
            if(stderr){
                console.error(`执行命令出错:${stderr}`)
            }
            resolve({
                content:[{type:'text',text:`已获取到目录${path}下的文件列表:\n\`\`\`${stdout}\`\`\n`}]
            })
        })
    })
   })

让MCPServer跟客户端通信

javascript 复制代码
const transport = new StdioServerTransport() // transport 用来跟进程交互 
await server.connect(transport)
console.log('MCP server connected')

transport是server跟外部通信的管道,基于stdin/stdout,客户端通过启动子进程+读写管道来跟server交互。server.connect(transport)就是把server接到这根管道上

至此,MCP的服务端就已经写好了

MCP client客户端

思路、事前引入

javascript 复制代码
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'

引入这些包,client是MCP客户端,我们在这里要做这样的事情:启动并连接MCP服务端,让外部代码能调用服务端的工具

client的核心作用,抛出创建客户端的函数

javascript 复制代码
export async function createClient(){
    const client = new Client({
        name:'Demo',
        version:'1.0.0',
    })
     const transport = new StdioClientTransport({ // 在客户端定义一个可以启用 MCPServer 的指令
        command:'node',
        args:['mcp/server.js']
     })
     try{
        await client.connect(transport)
        console.log('MCP客户端连接成功')
     }catch(error){
        console.error(`MCP客户端连接失败: ${error}`)
        throw error
     }
     return client
}

lib库,写一个llm.js来封装好并抛出一个类

事前准备,引入axios

我们引入在客户端抛出的创建客户端函数,并引入axios。axios是一个HTTP请求库,用来向LLM发POST请求

相当于浏览器的fetch,但更简洁,自动转换JSON,错误处理更方便

javascript 复制代码
import { createClient } from '../mcp/client.js'
import axios from 'axios'

类的初始化

抛出一个类。进行初始化

kotlin 复制代码
export class LLM {
  mcpClient = createClient()  // 类属性,创建 MCP 客户端实例(Promise)
  
  constructor(model, base_url, api_key = null) {
    this.model = model       // 模型名,如 'qwen2.5:14b'
    this.base_url = base_url // Ollama 地址,如 'http://localhost:11434'
    this.api_key = api_key   // API 密钥(我们用本地部署好的LLM不需要,所以默认 null)
  }

headers属性

设定好 headers属性,用get可以作为一个对象属性调用

kotlin 复制代码
get headers() {
  if (this.api_key) {
    return { 'Authorization': `Bearer ${this.api_key}` }
  } else {
    return { 'Content-Type': 'application/json' }
  }
}

listTools 拿到工具列表

javascript 复制代码
async listTools() {
  return (await (await this.mcpClient).listTools()).tools.map(tool => ({
    type: 'function',
    function: {
      name: tool.name,
      description: tool.description,
      parameters: tool.inputSchema   // zod 生成的 JSON Schema
    }
  }))
}

这里有两层await

  • await this.mcpClient 等待客户端连接成功
  • await ...listTools()调MCP客户端拿工具列表

然后把MCP格式转换成Ollama要求的tools格式,你用其他的LLM则自行查阅文档

callTool 调用工具

让MCP客户端执行工具,返回结果包装成Ollama要求的tool消息格式,方便塞进messages数组

javascript 复制代码
async callTool(tool_name, tool_args) {
  const result = await (await this.mcpClient).callTool({
    name: tool_name,
    arguments: tool_args
  })
  return {
    role: 'tool',
    name: tool_name,
    content: result.content[0].text  // 取工具返回的文本
  }
}

核心方法,chat

javascript 复制代码
async chat(messages) {
  const tools = await this.listTools()  // ① 拿工具列表
  const response = await axios.post(`${this.base_url}/api/chat`, {
    model: this.model,
    messages,
    tools,
    stream: false
  }, { headers: this.headers })

  const data = response.data
  const reply = data.message.content       // ② LLM 直接回复
  const tool_calls = data.message.tool_calls // ③ LLM 要求调工具

  if (tool_calls && tool_calls.length > 0) {
    const toolResArr = tool_calls.map((tool_call) =>
      this.callTool(tool_call.function.name, tool_call.function.arguments)
    )
    const results = await Promise.all(toolResArr)  // ④ 并行执行所有工具
    return await this.chat([...messages, ...results]) // ⑤ 递归
  }

  return reply  // ⑥ 不需要工具了,返回最终回复
}

这里重点在于递归的思想。当LLM发现调用的工具无法满足需求,则会再次返回data.message.tool_calls,一直到需求解决为止。记得在5号的地方要写return,如果没有的话会丢弃最后一次回复,返回倒数第二个回复

执行流程

markdown 复制代码
1. 拿工具列表
     ↓
2. 发给 Ollama(消息 + 工具列表)
     ↓
3. Ollama 返回两种可能:
     ├─ 直接回复 → 6. 返回
     └─ 要调工具 → 4. 并行执行工具
                       ↓
                   5. 把工具结果塞进 messages,递归调 chat()
                       ↓
                   再次发给 Ollama,直到不再需要工具

MCP和LLM连接

  1. 服务端先借助MCPClient向MCPServer获取已注册的工具函数
  2. 配置好tools属性,与LLM通信
  3. 当LLM返回需要调用某些工具时,递归式的执行,重复向LLM通信,并合并工具执行结果
  4. 最终返回LLM最后一次的回复

应用环节,写一个后端实战

写一个Express后端服务,接受前端请求,调用LLM,返回结果。我们用apifox模拟前端发送请求(你也可以自己新建一个前端发送POST请求)

javascript 复制代码
import express from 'express'
import axios from 'axios'
import { LLM } from '../lib/llm.js'
const app = express()
const llm = new LLM('qwen2.5:14b', 'http://localhost:11434')
// 用express解析POST传递的参数
app.use(express.json()) 
app.post('/chat', async (req, res) => {
// POST用body,GET用query
    const { message } = req.body 
    const messages = [
        {
            role: 'system', 
            content: '你是一个ai助手,如果有需要,你可以调用tools函数来解决问题'
        },{
            role:'user',
            content:message
        }
    ]
    const reply = await llm.chat(messages)
    res.json({reply})
})
app.listen(3000,()=>{
    console.log('server is running on port 3000')})

在终端中输入

json 复制代码
Invoke-RestMethod -Uri "http://localhost:3000/chat" -Method Post -ContentType "application/json" -Body '{"message": "请调用工具列出 f:/AAAAIFullStackDevelop/ai/MCP/demo 这个目录下的文件"}' | ConvertTo-Json -Depth 5

可以模拟发送请求,得到

总结

MCP 让 LLM 能按标准协议发现并调用外部工具,本项目通过 Ollama 本地模型 + MCP 工具调用,实现了"AI 助手读取目录文件"的完整闭环。

相关推荐
孟陬7 小时前
Node.js 测试报告格式一览和自定义 json 格式 `--test-reporter=spec | tap | dot | junit | lcov`
node.js
玉鸯10 小时前
多 Agent 系统通信的实现原理与最佳实践
llm·agent·mcp
TunerT_TQ11 小时前
Valhalla 静态工程审阅 #009|Continue 源码证据驱动评测【大厂开源基础设施特辑】
vscode·测试工具·开源·llm·github·jetbrains·ai编程助手
weixin_4713830312 小时前
01 LLM简介
llm
元Y亨H19 小时前
LangChain API 调用语言模型
langchain·llm
元Y亨H19 小时前
大模型技术 LangChain 概述
langchain·llm
元Y亨H19 小时前
大模型技术 提示词模板 概述
langchain·llm
元Y亨H1 天前
大模型技术 模型调用结果解析 概述
google·langchain·llm
元Y亨H1 天前
大模型技术 Ollama 概述
llm·ollama
chaors1 天前
DeepResearchSystem 0x06:LLM as Judge
llm·agent·ai编程