百度一镜数字人融合本地素材教程
2026-07-17 3404236
2026-07-17 0
这是我第一次跑通测试时截的图,现在看很简单,但当时看到「正在等待第 2 轮 AI 思考」跳出来的时候,确实有点小激动。

我就输入了一句「查一下用户 002 的信息」,剩下的事全是自动完成的:AI 自己判断要调用查询用户的工具,自动把参数传过去,拿到返回结果之后,还自己整理成了表格再回复给我。
整个过程我没写任何手动调用工具的判断逻辑,也没处理参数解析,全是 MCP 协议和 SDK 在背后干活。
MCP 全称是 Model Context Protocol,直译过来叫模型上下文协议。听着挺高大上,其实说白了就是一套统一的通信规矩。
以前我们给大模型加工具,都是和业务代码写在一起的,工具怎么定义、参数怎么传、结果怎么返,全是自己定的。换个项目、换门语言,就得重写一遍,相当于每个电器都自己做个专属插头,互相之间完全不通用。
MCP 就相当于大模型世界的通用插座标准。它规定了客户端(也就是我们写的 Agent)和服务端(提供工具、资源的一方)之间,该怎么打招呼、怎么传参数、怎么返回结果、怎么报错。只要两边都遵守这个标准,不管你服务端是用 Node、Java 还是 Python 写的,不管跑在本地还是远程服务器,客户端都能直接用。
它和普通的 HTTP 接口调用不是一回事。接口调用是我们写代码去拿数据,而 MCP 是给大模型扩展上下文用的 —— 要么给它增加能调用的工具(让它能做更多事),要么给它提供额外的资料(让它知道更多事)。本质上都是在给大模型的上下文添东西,这也是「Context Protocol」名字的由来。

MCP 最大的特点就是天生支持跨进程调用。本地跑就用 stdio 标准输入输出流,父子进程之间传数据;远程部署就换成 HTTP 协议,走网络通信。两种方式底层协议是一致的,只是传输通道不一样。
说再多不如写两行代码。我写了个最小可运行的 demo,就做两件事:提供一个查询用户的工具,再提供一份使用指南文档。整个 demo 分成 Server 端和 Client 端两部分。
Server 端就是提供工具和资源的一方,用官方的 SDK 写起来很简单。
复制代码import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';// 整个假数据库,省得连库麻烦,实际用可以换成真实数据库
const database = {
users: {
'001': {id: '001', name: 'Alice', email: '[email protected]', age: 30},
'002': {id: '002', name: 'Bob', email: '[email protected]', age: 25},
'003': {id: '003', name: 'Charlie', email: '[email protected]', age: 28},
}
}// 初始化服务,起个名字,标个版本
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});// 注册查询用户的工具
server.registerTool('query_user',
{
// 描述一定要写清楚,不然AI真的会瞎调用
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.age}`}
]
}
});// 注册一份使用指南资源
server.registerResource(
'使用指南',
'docs://guide',
{
description: 'MCP Server 使用指南',
mimeType: 'text/plain'
},
async () => {
return {
contents: [
{
uri: 'docs://guide',
mimeType: 'text/plain',
text: `
MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 Cursor 等 MCP Client 中通过自然语言对话,会自动调用相应工具。
`
}
]
}
}
);// 用 stdio 方式启动服务,也就是通过标准输入输出和客户端通信
const transport = new StdioServerTransport();
await server.connect(transport);
这里我一开始没搞懂 registerResource 是干嘛的,以为和工具差不多。后来跑通了才明白:
就像这个使用指南,客户端启动的时候先读一遍,然后塞给 AI 当背景知识,AI 就知道这个 Server 有啥功能、该怎么用了。
Resource 是一种很轻量的上下文补充方式,适合放使用说明、字段定义、固定规则这类内容。和 RAG 不一样,它不需要检索,是 Server 自带的固定内容,直接读取就行。
客户端就是我们的 Agent 程序,负责连接 MCP Server,拿到工具和资源,然后交给大模型去调度。我这里用了 LangChain 的适配包,用起来更顺手。
复制代码 import 'dotenv/config';
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
import chalk from 'chalk';
import {
HumanMessage,
SystemMessage,
ToolMessage
} from '@langchain/core/messages'; // 初始化大模型,这里用的DeepSeek,换成别的也一样
const model = new ChatOpenAI({
modelName: process.env.DEEPSEEK_API_MODEL,
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.DEEPSEEK_API_BASE_URL,
},
}) // 初始化 MCP 客户端,配置要连接的 Server
// 可以同时配置好多个,统一管理
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': {
command: 'node',
args: ['你的server文件路径/my-mcp-server.mjs'],
}
}
}); // 获取所有工具,还有所有资源内容
const tools = await mcpClient.getTools();
const res = await mcpClient.listResources();
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;
}
} // 把工具绑定给模型
const modelWithTools = model.bindTools(tools); // 核心的 Agent 多轮调用逻辑
async function runAgentWithTools(query, maxIterations = 30) {
// 把资源内容当成系统消息,先给AI看一遍说明书
const messages = [
new SystemMessage(resourceContent),
new HumanMessage(query)
]; for(let i = 0; i < maxIterations; i++){
console.log(chalk.bgGreen(`正在等待第${i+1}轮 AI 思考...`));
const response = await modelWithTools.invoke(messages);
messages.push(response); // 没有工具调用了,说明AI思考完了,直接返回结果
if(!response.tool_calls || response.tool_calls.length === 0){
console.log(`n AI 最终回复: n ${response.content}`);
return response.content;
} console.log(chalk.bgBlue(`检测到 ${response.tool_calls.length} 个工具调用`));
console.log(chalk.bgBlue(`工具调用: ${response.tool_calls.map(tc => tc.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);
// 注意这里必须传 tool_call_id,不然AI对应不上结果
// 别问我为什么知道,漏了之后卡了半小时
messages.push(new ToolMessage({
tool_call_id: toolcall.id,
content: toolResult
}))
}
}
}
return messages[messages.length - 1].content;
} // 测试一下
await runAgentWithTools('查一下用户 002 的信息'); // 这行一定要加!不然脚本跑完不会退出
// 别问我怎么知道的,对着卡着的终端发呆了二十分钟
await mcpClient.close();
运行之后,就能看到最开始截图里的效果了。
写这个 demo 看着简单,其实踩了好几个坑,都是不看文档想当然踩的。说出来给大家避避坑。
第一个坑就是上面说的,忘了调用 close ()。MCP Server 是作为子进程被客户端启动的,你不主动关闭,它就会一直挂在后台等着接收请求,主进程自然就不会退出。一开始我以为是异步没处理好,翻了半天 Promise 写法,最后才发现是没关子进程。
使用完 MCP Client 后,务必调用 close () 方法释放资源,终止子进程,否则脚本会一直挂起无法正常退出。
第二个坑,ToolMessage 漏传 tool_call_id。这个坑更隐蔽。我最开始写 ToolMessage 的时候,只传了 content 内容,没带 tool_call_id。结果就是 AI 永远收不到工具的返回结果,控制台一直在刷「第 N 轮 AI 思考」,无限循环调用工具,就是不输出最终答案。
我当时还吐槽模型笨,调用完工具都不会看结果。后来翻了半天才发现,大模型是靠 tool_call_id 来匹配「哪次调用对应哪次结果」的,你不传 id,它根本不知道这个结果是给谁的,自然就一直重复调用了。
第三个坑,工具描述写得太敷衍。最开始 query_user 的描述我就写了「查询用户信息」六个字,结果 AI 经常传错参数,一会儿传姓名,一会儿传邮箱,死活不知道传 userId。后来我把描述补得巨详细,连参数示例都加上了,调用准确率一下就到了 100%。
给 AI 写描述真的比写产品需求文档还要严谨,半点歧义都不能有。
聊到这肯定有人会说,这不就是套了层协议的工具调用吗,我直接写接口不行?
确实,简单场景下完全没必要用 MCP。如果你的项目就是个单语言应用,工具也没几个,直接用框架原生的工具调用就行,上 MCP 反而平白增加复杂度。
但如果碰到下面这几种情况,MCP 就能体现出价值了:
说实话,最开始听到 MCP 这个新名词的时候,我是有点抵触的,觉得又是 AI 圈在炒概念。真的沉下心写了个 demo,踩了几个坑,才明白它解决的是实实在在的痛点 —— 把大模型的能力扩展,从单进程、单语言的限制里解放出来了。
技术这东西就是这样,听一百遍概念讲解,不如自己动手跑一遍。当你亲手把 Server 启起来,看着 AI 自动调用工具、整理结果返回的那一刻,很多原理性的东西自然就通了。
要是你也跑了 MCP 的 demo,或者有不一样的理解,欢迎在评论区聊聊,我也想看看大家都用它做了啥有意思的东西。