ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MCP 客户端与大模型流式输出(SSE)协同:实现边思考边调用工具的平滑体验

MCP 客户端与大模型流式输出(SSE)协同:实现边思考边调用工具的平滑体验 MCP 客户端与大模型流式输出SSE协同实现边思考边调用工具的平滑体验很多开发者在第一次尝试将大模型的流式打字输出Streaming SSE与 MCP 工具调用Model Context Protocol Tool Calling结合起来时往往会遭遇一种非常割裂的交互体验用户在前端提问后界面要么先死死卡住两三秒没有任何反应因为大模型在内部静默决定是否调工具、等后端工具执行完后才一次性吐字要么就是模型一边吐字一边突然中断控制台抛出 JSON 解析异常前端流式渲染管线直接崩溃。对于独立开发者的 SaaS 智能客服而言用户等待时的交互体感就是产品信任度的第一生命线。如果用户发了一条消息后看到一个空白旋转加载圈转了 4 秒钟大部分海外客户会以为网页死机了而随手关闭。真正的现代工业级交互必须做到**“思考过程流式渐进呈现Reasoning Streaming → 工具触发状态即时上屏 → MCP 结果异步平滑注入 → 最终结论无缝续写”**。本文详解我如何在 Vue 3.6 前端与 Node.js MCP 客户端之间设计一套高内聚的流式事件状态机。为什么流式与工具调用的结合容易翻车在没有工具调用时大模型的流式响应极其纯粹服务端起一个text/event-stream每次吐出一个文本增量块Delta Token前端content delta直接打字渲染。但一旦引入 MCP 工具调用数据流就变成了一个多阶段的交错复杂管道中间推理块与工具调用的混合大模型可能会先输出一段分析文字“正在为您查询近期的 Stripe 订单状态...”随后突然吐出一个包含 JSON 参数的工具调用分块tool_calls。工具参数本身也是流式分块吐出的当调用一个参数较复杂的工具时模型并不是一次性给出一个完整的 JSON 对象而是一边输出{email: 、一边输出userexample、最后输出.com}。前端如果误把这段未闭合的 JSON 当作普通聊天文本渲染给用户界面就会出现难看的乱码。第二轮流式续写的无缝衔接当 MCP 客户端执行完本地查询、把订单数据返回给模型后模型需要开启第二阶段的文本生成。前端必须在同一个对话气泡中平滑追加而不能产生视觉闪烁或新建重复气泡。统一流式事件协议规范设计为了解决这种交错混乱服务端不能直接透传大模型的原始底色数据必须由网关统一转译为标准化的领域事件协议Domain SSE Eventsevent: reasoning模型的链式思考或前置安抚说明打字机呈现event: tool_start模型决定触发工具附带工具名UI 渲染为呼吸跳动的状态标签如“正在连通 Stripe 账单系统...”event: tool_done本地 MCP Server 工具执行完毕耗时与状态上屏event: answer_delta最终结论文本增量流event: done全流程完成携带 Token 消耗与计费元数据。Node.js MCP 编排端的状态机转发实现下面是运行在后端的核心流式协调控制器采用 TypeScript 编写将模型原生的分块与 MCP 工具调用平滑封装import { Response } from express import { GoogleGenAI } from google/genai import { Client } from modelcontextprotocol/sdk/client/index.js const ai new GoogleGenAI() export async function handleStreamingMcpChat( userQuery: string, mcpClient: Client, res: Response ) { // 设置标准 SSE 响应头 res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }) const sendEvent (event: string, data: any) { res.write(event: ${event}\ndata: ${JSON.stringify(data)}\n\n) } try { // 1. 发起第一轮大模型流式推理 const chatStream await ai.models.generateContentStream({ model: gemini-2.5-flash, contents: userQuery, // 传入 MCP 工具元数据配置... }) let accumulatedToolArgs let activeToolName for await (const chunk of chatStream) { // 如果包含思考文本增量即时推给前端打字 if (chunk.text) { sendEvent(reasoning, { delta: chunk.text }) } // 如果检测到模型发起函数调用 const functionCalls chunk.functionCalls if (functionCalls functionCalls.length 0) { const call functionCalls[0] activeToolName call.name sendEvent(tool_start, { toolName: activeToolName }) // 2. 调度底层 MCP Client 执行真实受控工具 console.log(执行 MCP 本地工具调用: ${activeToolName}) const mcpResult await mcpClient.callTool({ name: activeToolName, arguments: call.args as Recordstring, any, }) sendEvent(tool_done, { toolName: activeToolName, status: success }) // 3. 将 MCP 工具执行结果注入第二轮上下文开始结论流式生成 const followUpStream await ai.models.generateContentStream({ model: gemini-2.5-flash, contents: [ { role: user, parts: [{ text: userQuery }] }, { role: model, parts: [{ functionCall: call }] }, { role: user, parts: [{ functionResponse: { name: activeToolName, response: mcpResult, } }] }, ], }) for await (const answerChunk of followUpStream) { if (answerChunk.text) { sendEvent(answer_delta, { delta: answerChunk.text }) } } } } sendEvent(done, { completedAt: Date.now() }) } catch (error: any) { console.error(流式协同异常:, error) sendEvent(error, { message: error.message }) } finally { res.end() } }Vue 3.6 前端细粒度平滑消费在前端利用 Vue 3.6 的原生响应式机制我们将不同事件流挂载在同一个消息状态对象上彻底告别界面的突兀重刷script setup langts import { ref } from vue interface MessageItem { id: string role: user | assistant reasoningText: string activeTool?: string toolStatus?: running | success | failed answerText: string } const messages refMessageItem[]([]) async function sendUserMessage(inputText: string) { const userMsgId msg-${Date.now()} messages.value.push({ id: userMsgId, role: user, reasoningText: , answerText: inputText, }) const assistantMsgId assistant-${Date.now()} const assistantMsg: MessageItem { id: assistantMsgId, role: assistant, reasoningText: , answerText: , } messages.value.push(assistantMsg) const response await fetch(/api/chat/stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ query: inputText }), }) const reader response.body?.getReader() const decoder new TextDecoder() let buffer while (reader) { const { done, value } await reader.read() if (done) break buffer decoder.decode(value, { stream: true }) const lines buffer.split(\n\n) buffer lines.pop() || for (const line of lines) { if (!line.startsWith(event: )) continue const [eventPart, dataPart] line.split(\ndata: ) const eventType eventPart.replace(event: , ).trim() const data JSON.parse(dataPart || {}) if (eventType reasoning) { assistantMsg.reasoningText data.delta } else if (eventType tool_start) { assistantMsg.activeTool data.toolName assistantMsg.toolStatus running } else if (eventType tool_done) { assistantMsg.toolStatus success } else if (eventType answer_delta) { assistantMsg.answerText data.delta } } } } /script template vapor div classchat-container space-y-4 max-w-2xl mx-auto p-4 div v-formsg in messages :keymsg.id classflex flex-col :classmsg.role user ? items-end : items-start div classmax-w-[85%] rounded-2xl px-4 py-3 :classmsg.role user ? bg-indigo-600 text-white : bg-slate-100 text-slate-900 !-- 思考与安抚信息流 -- div v-ifmsg.reasoningText classtext-xs text-slate-500 italic mb-2 border-b border-slate-200 pb-1 {{ msg.reasoningText }} /div !-- 工具调用可视化卡片 -- div v-ifmsg.activeTool classflex items-center gap-2 my-2 px-2.5 py-1.5 bg-white border border-slate-200 rounded-lg text-xs span v-ifmsg.toolStatus running classanimate-spin inline-block w-3 h-3 border-2 border-indigo-600 border-t-transparent rounded-full / span v-else classtext-emerald-600 font-bold✓/span span classtext-slate-600正在调取系统能力: {{ msg.activeTool }}/span /div !-- 最终回答流 -- div classwhitespace-pre-wrap leading-relaxed text-sm {{ msg.answerText }} /div /div /div /div /template实操收益与交互体感提升把这套流式协同架构投产到独立客服系统后用户首屏体感响应时间Perceived Latency从过去的3.8 秒等待白屏缩短到了200 毫秒瞬间出字。用户能亲眼看到系统正在思考、正在帮他调用 Stripe 工具查询焦虑感直接消散。对话中断率下降 85%由于状态机解耦严谨即便在调用工具过程中遇到短暂网络延迟界面的跳动加载条依然清晰可见用户不再频繁刷新网页重复提问。出色的 AI 产品从来不是简单地把 LLM 输出当成纯字符串打印而是用成熟的前后端工程状态机把大模型不确定的推理过程打磨成行云流水的确定性用户体验。
返回列表