跳到主要内容前端直连模型 vs 完整 MCP:大模型驱动地图原理与实践 | 极客日志JavaScriptNode.jsAI大前端
前端直连模型 vs 完整 MCP:大模型驱动地图原理与实践
对比了前端直连模型与完整 MCP 架构在大模型驱动地图场景下的差异。通过 Vue、Cesium、Node.js 和 WebSocket 构建最小化 Demo,解析了从用户指令到地图动作执行的完整链路。核心在于区分“前端直连”与“Host 调度”的角色分工,明确 Host 负责模型与工具交互,Node 暴露标准工具,浏览器仅执行渲染动作。文章提供了时序图、职责拆分及实战代码,帮助开发者理解如何构建可扩展的大模型工具调用系统,避免架构混乱。
随缘1 浏览 1. 这篇教程要解决什么问题
很多人第一次做'大模型驱动地图'的时候,都会有一个非常自然的想法:
我在网页上放一个聊天框,用户输入'飞到上海',然后把这句话发给模型;模型决定调用工具;前端收到工具调用后,直接控制地图飞过去。
- 可以被不同宿主统一调用
- 可以被大模型按工具方式自动发现
- 可以在前端、桌面、服务端之间分层复用
- 可以持续扩展成'通用地图执行器'
'前端直接请求模型并本地执行工具',和
'真正完整的 MCP 链路',其实是两套不同的设计思路。
我们会先讲原理,再讲架构,最后用一个 Vue + Cesium + Node + MCP 的最小 demo 把整个链路跑起来。
2. 先别写代码:先搞懂两个很像但本质不同的方案
2.1 方案一:前端直连模型
- 用户在网页输入'飞到上海'
- 前端把用户消息和工具定义一起发给模型
- 模型返回:要调用
flyToShanghai
- 前端收到
tool_calls
- 前端本地执行
flyToShanghai
- 前端控制 Cesium 飞到上海
- 前端再把工具结果发回模型
- 模型生成最终回复
用户 -> 前端页面 -> 大模型 API -> 前端页面(收到 tool_calls) -> 前端本地工具执行 -> Cesium 地图 -> 前端页面 -> 大模型 API -> 前端页面 ->用户
- 容易理解
- 容易开发
- 页面交互很直接
- 没有额外的 Host 层
前端同时承担了'聊天客户端'和'工具执行器'两种角色。
2.2 方案二:真正完整的 MCP
完整 MCP 不是前端自己决定怎么调用工具,而是由一个 Host 来负责:
- 和用户对话
- 和模型交互
- 把工具提供给模型
- 在模型决定调用工具后,真正去调用 MCP Server
- 再把结果回灌给模型
在这个架构里,浏览器不再负责'模型调度',而只负责'地图动作执行'。
用户 -> Host -> 模型 -> Host -> Node MCP Server -> 浏览器 -> Cesium -> 浏览器 -> Node MCP Server -> Host -> 模型 -> Host -> 用户
模型不直接调用 Node。Host 才是真正替模型调用工具的人。
2.3 它们最核心的区别
'前端直连模型'和'完整 MCP'不都是模型调用工具吗?
| 维度 | 前端直连模型 | 完整 MCP |
|---|
| 谁和模型对话 | 前端页面 | Host |
| 谁拿到 tool_calls | 前端页面 | Host |
| 谁真正调工具 | 前端页面 | Host |
| Node 在哪里 | 可以没有 | 是 MCP Server |
| 浏览器扮演什么 | 聊天端 + 执行端 | 纯执行端 |
| 是否符合完整 MCP 分层 | 不一定 | 是 |
- 前端直连模型:页面自己又当指挥,又当工人
- 完整 MCP:Host 当指挥,浏览器只负责干活
3. 为什么很多人一开始会把两套方案混在一起
- tools
- tool_calls
- function calling
- flyToShanghai
- 地图飞行
'我已经把 tools 发给模型了,模型也返回要调用的工具了,那我就是在做 MCP。'
如果是前端自己在做调度,那你更接近'前端直连模型 + 本地执行工具'。
如果是 Host 在调度,Node 暴露 MCP 工具,浏览器只执行动作,那才是完整 MCP。
4. 先建立整体认知:完整 MCP 里有哪些角色
4.1 用户
4.2 Host
- 接收用户输入
- 请求大模型
- 把工具列表告诉模型
- 当模型决定调用工具时,真正去调用 MCP Server
- 把工具结果再喂回模型
- 把最终自然语言回复返回给用户
4.3 MCP Server
MCP Server 负责把系统能力暴露成标准工具。
flyToShanghai
flyToLocation
pingBrowser
addPoint
drawPolyline
'我这里有这些工具,你要调哪个,我帮你转成真实动作。'
4.4 浏览器 + Cesium
浏览器里的地图页面不再和模型直接对话,而是只做一件事:
{"method":"flyTo","params":{"longitude":121.4737,"latitude":31.2304,"height":8000}}
5. 完整 MCP 的时序图:一句'飞到上海'是怎么穿过整个系统的
┌──────┐ ┌───────────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌────────────┐ ┌─────────┐ │ 用户 │ │ MCP Host │ │ 模型 │ │ Node MCP │ │ WebSocket │ │ 浏览器前端 │ │ Cesium │ │ │ │ │ │ (LLM) │ │ Server │ │ 通道 │ │ + Bridge │ │ Scene │ └─┬────┘ └──────┬────────┘ └────┬─────┘ └──────┬───────┘ └────┬─────┘ └─────┬──────┘ └────┬────┘ │ '飞到上海' │ │ │ │ │ │ │──────────────────>│ │ │ │ │ │ │ │ 把用户消息发给模型 │ │ │ │ │ │ │──────────────────>│ │ │ │ │ │ │ │ 判断需要用工具 │ │ │ │ │ │<──────────────────│ tool_call: │ │ │ │ │ │ │ flyToShanghai │ │ │ │ │ │ 调 Node MCP 工具 │ │ │ │ │ │ │────────────────────────────────────>│ │ │ │ │ │ │ │ sendToBrowser() │ │ │ │ │ │ │─────────────────>│ │ │ │ │ │ │ │ JSON-RPC 消息 │ │ │ │ │ │ │───────────────>│ │ │ │ │ │ │ │ bridge.execute│ │ │ │ │ │ │──────────────>│ │ │ │ │ │ │ 相机飞到上海 │ │ │ │ │<─────────────────│ 执行结果 │ │ │ │<────────────────────────────────────│ tool result │ │ │ │ │ 再把结果发给模型 │ │ │ │ │ │ │──────────────────>│ │ │ │ │ │ │<──────────────────│ 返回自然语言回复 │ │ │ │ │<──────────────────│ │ │ │ │ │
5.1 模型并没有直接连 Node
真正去执行 callTool() 的,是 Host。
5.2 浏览器并不参与'思考'
6. 为什么这个架构更适合地图场景
- 相机飞行
- 地物绘制
- 图层显示隐藏
- 交互式选点
- 实时动画
- Host:负责智能调度
- Node MCP Server:负责能力暴露与转发
- 浏览器:负责地图执行
7. 本教程的 demo 架构
前端页面(Vue + Cesium)
- 初始化 Cesium
- 连接 Node 发来的 WebSocket 消息
- 收到动作后调用
MiniBridge.execute()
- 真正控制地图飞行
MiniBridge
flyTo -> viewer.camera.flyTo(...)
Node index.ts
- 启动 WebSocket Server
- 启动 MCP Server
- 注册
flyToShanghai / pingBrowser
- 收到工具调用后把动作转发给浏览器
Host
最初可以是终端版 host.ts,后面再升级成页面版 host-web.ts。
- 和模型对话
- 获取 MCP 工具列表
- 调用 MCP 工具
- 把工具结果回灌给模型
8. 实战之前,先看一遍项目职责拆分
第 1 层:执行层
- 连接 WebSocket
- 执行收到的地图命令
- 返回结果
第 2 层:工具层
第 3 层:智能调度层
- 和模型交互
- 决定是否调用工具
- 触发 MCP 调用
- 收尾生成自然语言回答
一开始把前端、模型、工具、地图执行全写在一个文件里。
9. 实战搭建:从零把链路跑通
9.1 第一步:浏览器只负责执行地图动作
class MiniBridge {
constructor(viewer) {
this.viewer = viewer;
}
async execute(cmd) {
const action = cmd?.action;
const params = cmd?.params || {};
switch (action) {
case 'flyTo':
await this.flyTo(params);
return { success: true };
default:
return { success: false, error: `Unsupported action: ${action}` };
}
}
flyTo(params) {
const { longitude, latitude, height = 8000 } = params;
return new Promise((resolve) => {
this.viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
complete: resolve,
});
});
}
}
- 上层不用直接知道 Cesium API
- 上层只需要说'执行
flyTo'
- 浏览器负责把它翻译成具体地图动作
这一步很关键,因为它让地图执行能力变成了一个可复用的动作接口。
9.2 第二步:Node 负责 MCP Server + WebSocket 桥接
9.2.1 注册 MCP 工具
server.tool('flyToShanghai', 'Fly camera to Shanghai in connected browser Cesium scene.', {
height: z.number().default(8000),
duration: z.number().default(2),
}, async ({ height = 8000, duration = 2 }) => {
const result = await sendToBrowser('flyTo', {
longitude: 121.4737,
latitude: 31.2304,
height,
duration,
heading: 0,
pitch: -45,
});
return {
content: [
{ type: 'text', text: JSON.stringify(result ?? { success: true }) },
],
};
});
这里看起来像是在'飞到上海',但其实 Node 没有自己控制地图。
- 把工具名称暴露给外界
- 收到工具调用时,转发一个
flyTo 命令给浏览器
9.2.2 通过 WebSocket 把动作发给浏览器
function sendToBrowser(method, params, timeoutMs = 15000) {
return new Promise((resolve, reject) => {
const ws = getBrowser(DEFAULT_SESSION);
if (!ws || ws.readyState !== WebSocket.OPEN) {
reject(new Error('No browser connected.'));
return;
}
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
pendingRequests.set(id, { resolve, reject, timer });
ws.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
});
}
9.3 第三步:Host 负责'模型 ↔ 工具'循环
这是完整 MCP 里最容易被忽略,但又最关键的一层。
- 收到用户输入
- 请求模型
- 把工具列表发给模型
- 模型返回
tool_calls
- Host 调用 MCP Server
- Host 拿到工具结果
- Host 再把结果发给模型
- 模型生成最终回复
const tools = await client.listTools();
const first = await callChatCompletions(messages, openAITools);
const toolCalls = first?.choices?.[0]?.message?.tool_calls || [];
for (const tc of toolCalls) {
const result = await client.callTool({
name: tc.function.name,
arguments: JSON.parse(tc.function.arguments || '{}'),
});
secondMessages.push({
role: 'tool',
tool_call_id: tc.id,
content: toolResultToText(result),
});
}
const second = await callChatCompletions(secondMessages, openAITools);
Host 是连接'模型世界'和'工具世界'的那个人。
9.4 第四步:把'终端输入'升级成'页面输入'
一开始,为了最小验证链路,你可以用终端版 Host:
- 在终端输入'飞到上海'
- Host 去请求模型
- Host 再调用 MCP Server
这时,正确思路不是把'前端直连模型'改回来,而是:
给 Host 再包一层 HTTP 接口,让页面把聊天请求发给 Host。
页面输入 -> Host Web API -> 模型 -> Host Web API -> Node MCP Server -> 浏览器执行地图动作 -> Node MCP Server -> Host Web API -> 模型 -> 页面
也就是说,页面输入 和 完整 MCP 是可以同时成立的。
10. 两种页面聊天方案对比:哪种是'真 MCP'
10.1 方式 A:页面直接请求模型
页面 -> 模型 -> 页面收到 tool_calls -> 页面本地执行工具 -> 页面再请求模型
10.2 方式 B:页面请求 Host Web API
页面 -> Host Web API -> 模型 -> Host Web API -> MCP Server -> 浏览器地图执行 -> Host Web API -> 模型 -> 页面
这种方式更符合完整 MCP,因为页面没有直接参与工具调度。
一句话判断标准
如果页面自己在处理 tool_calls,那通常不是完整 MCP。
如果页面只是把用户输入交给 Host,然后等待回复,那更接近完整 MCP。
11. 一次完整调用的分层理解
当用户在页面输入'飞到上海'时,可以从三个层面来看这件事。
11.1 语义层
11.2 工具层
11.3 执行层
11.4 把读者最容易追问的几个问题串起来理解
在真实学习过程中,很多人不会一下子就被'架构图'讲明白,反而会在写 demo、跑代码、看日志的时候不断冒出一些非常具体的问题。
这些问题看起来零散,但其实正好能帮助你把完整 MCP 理解得更扎实。
问题一:'大模型和 Node 这条链路到底怎么连起来的?'
很多人第一次看到 index.ts 里的 McpServer,会本能地以为:
用户 -> Host -> 模型 -> Host -> Node MCP Server -> 浏览器 -> Cesium
- 模型只负责'决定要不要调用工具'
- Host 才是'真正去调用 MCP 工具的人'
- Node MCP Server 只是'被调用的工具服务'
- 浏览器只是'执行地图动作的地方'
所以真正连模型和 Node 的,不是模型自己,而是 Host。
问题二:'host-web.ts 里没有把工具传给 StdioClientTransport,那工具是怎么来的?'
const transport = new StdioClientTransport({ command: getNpxCommand(), args: ['tsx', 'index.ts'], });
- 用
tsx index.ts 启动一个 MCP Server 进程
- 再让 Host 通过 stdio 和这个进程通信
args 不是工具内容本身,它只是告诉 transport:去启动哪一个 Server。
await client.connect(transport);
const toolsRes = await client.listTools();
toolsCache = toolsRes.tools || [];
openAITools = toolsCache.map(mcpToolToOpenAITool);
args:找到餐厅地址
connect:走进餐厅
listTools:向餐厅要菜单
openAITools:把菜单整理成模型能看懂的格式
所以工具并不是被'静态读取源码'拿到的,而是 index.ts 运行起来以后,通过 server.tool(...) 向外暴露,再由 Host 用 listTools() 动态读取出来的。
问题三:'后面看起来不是和前端直连一样吗?不也还是把 tools 发给模型吗?'
这个问题问得非常好,因为它正好点出了'哪里像、哪里不一样'。
在模型接口这一层,确实很像;在系统架构这一层,本质上不一样。
messages + tools -> model -> tool_calls
前端直连模型
- 工具定义在前端
- 工具由前端发给模型
- 模型返回
tool_calls
- 前端本地执行工具
完整 MCP
- 工具定义在 MCP Server
- Host 用
listTools() 动态发现工具
- Host 把工具发给模型
- 模型返回
tool_calls
- Host 再用
callTool() 调 MCP Server
- 浏览器只负责执行动作
完整 MCP 不是'不把工具发给模型',而是把'工具定义权'和'工具执行调度权'从前端抽离了出来。
问题四:'既然终端里输入能飞到上海,为什么还要做页面输入?'
这是从'验证链路'过渡到'真正产品形态'的关键一步。
终端输入 -> Host -> 模型 -> Host -> MCP Server -> 浏览器 -> Cesium
- Host 没问题
- MCP Server 没问题
- 浏览器执行链没问题
这时候再把'终端输入'换成'页面输入',本质上不是重新发明一套逻辑,而是把 Host 的入口从命令行换成 HTTP API:
页面输入 -> host-web.ts -> 模型 -> host-web.ts -> MCP Server -> 浏览器 -> Cesium
所以页面输入版的核心不是'让前端重新直连模型',而是:
页面把用户输入交给 Host,由 Host 继续完成完整 MCP 链路。
问题五:'我到底该怎么判断自己现在是不是在走完整 MCP?'
这个问题非常重要,因为很多 demo 表面上看都叫'工具调用',但架构上完全不是一回事。
- 前端自己写
getTools()
- 前端自己发给模型
- 前端自己执行
runLocalTool()
index.ts 里 server.tool(...) 定义工具
- Host 通过
listTools() 发现工具
- Host 通过
callTool() 调用工具
- 浏览器只负责执行动作
前端直连看的是'页面会不会调工具',完整 MCP 看的是'工具是不是独立存在于 MCP Server 里'。
问题六:'为什么我一开始总觉得两套方案混在一起?'
- tools
- tool_calls
- flyToShanghai
- bridge.execute
- WebSocket
这些词在两套架构里都出现,所以很容易产生一种错觉:
'我都在用 tools 了,那我就是在做 MCP。'
- 谁是 Host
- 谁是 MCP Server
- 谁是浏览器执行器
- 谁才是真正的工具源头
一旦你把角色看清楚,之前那些零散的疑问就会突然全部串起来。
你会发现,上面这些问题并不是无关的'零碎提问',而是学习过程中非常自然的一条理解升级路径:
- 先困惑模型和 Node 的关系
- 再困惑工具是怎么被发现的
- 再进一步意识到'为什么看起来和前端直连很像'
- 最后才真正明白:完整 MCP 的价值,不在'有没有 tools',而在工具定义、工具调度和动作执行被拆成了独立角色
12. 新手最容易踩的坑
12.1 把'tool calling'误以为就是 MCP
tool calling 是模型返回工具调用决策。
MCP 是一整套 client-server 协议和角色分层。
12.2 让浏览器既当聊天端又当执行端
12.3 误以为模型直接调用 Node
12.4 忘了浏览器必须先连上 WebSocket
如果浏览器没有连上,Node 收到工具调用也没法执行地图动作。
12.5 同时起了两个 Server,导致端口冲突
比如 9100 已经被占用,再起一个同样监听 9100 的进程,就会报端口占用错误。
13. 你可以如何扩展这套地图能力
当'飞到上海'跑通以后,这套架构就已经具有扩展性了。
13.1 通用飞行类
flyToLocation({ longitude, latitude, height })
setCameraView(...)
lookAt(...)
13.2 地图绘制类
addPoint(...)
addPolyline(...)
addPolygon(...)
13.3 图层控制类
showLayer(...)
hideLayer(...)
toggleLayer(...)
13.4 场景查询类
getCurrentCamera()
getSelectedEntity()
pingBrowser()
13.5 复杂业务类
- 无人机巡航
- 路线规划
- 智慧城市场景切换
- 事件定位与高亮
你扩展的是'工具能力',而不是把业务越写越塞进页面里。
14. 学完这篇教程后,你应该真正记住什么
完整 MCP 不是'模型自己调地图',而是'Host 根据模型决策去调工具,再让浏览器执行地图动作'。
- 前端直连模型 能跑,但不等于完整 MCP
- Host 是'模型和工具之间的真正调度者'
- 浏览器 在地图场景里最适合做执行器,而不是全能中枢
前端直连模型
完整 MCP
Host 负责调度,Node 负责工具,浏览器负责执行
结语
从'让地图飞到上海'这件小事出发,你其实已经接触到了一个很有代表性的工程问题:
当大模型开始接管工具调用时,系统应该怎么分层,才不会越做越乱?
- 把思考留给模型
- 把调度交给 Host
- 把能力暴露给 MCP Server
- 把渲染执行留给浏览器
如果你已经把'飞到上海'跑通了,那么下一步,不是继续把代码堆在页面里,而是:
把更多地图能力,整理成一套干净的 MCP 工具体系。
这时,你就真正迈进了'通过大模型聊天驱动地图系统'的工程化阶段。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online