普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月23日首页

🧙‍♂️《当 Web 遇上 MCP:一场“模型上下文协议”的奇幻漂流》

作者 LeonGao
2025年8月22日 10:10

📜 目录(点击瞬移)

  1. 🌌 MCP 为何物?—— 从“对话”到“上下文”
  2. 🧠 底层原理:一条 SSE 流如何串起三大洲
  3. 🏗️ 对 Web 行业的五连击
  4. 🪄 实战:30 行 JS 搭一个“会自己写代码”的 Next.js 页面
  5. 🔥 踩坑 & 彩蛋:当 MCP 遇到 CORS、Nginx 与老板的 KPI
  6. 🚀 尾声:下一站,“模型即后端”?

1. 🌌 MCP 为何物?

官方一句话:
MCP(Model Context Protocol)= HTTP + SSE + JSON-RPC,让大模型“长”在你的 Web 里。

白话翻译:

曾经 现在(MCP 加持)
用户 → 前端 → 后端 → OpenAI → 后端 → 前端 用户 → 前端 ↔ 大模型(长连接)
上下文靠开发者手动拼 上下文自动累积,像 Git 一样可回滚
Token 计费 = 黑盒 Token 计费 = 实时仪表盘

🖼️ 配图:左边是传统“HTTP 回旋镖”,右边是 MCP “双向高速公路”。


2. 🧠 底层原理:一条 SSE 流如何串起三大洲

2.1 协议三明治

------------┐
│   JSON-RPC │  业务语义(方法/参数)
├------------┤
│    SSE     │  服务器推送事件(text/event-stream)
├------------┤
│    HTTP/2  │  复用连接、Header 压缩
└------------┘

2.2 生命周期(伪代码版)

// 1. 建立 SSE 流
const eventSource = new EventSource('/mcp');

// 2. 发送 JSON-RPC 请求
eventSource.onopen = () => {
  eventSource.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'initialize',
    params: { capabilities: {} }
  }));
};

// 3. 接收增量上下文
eventSource.onmessage = (e) => {
  const delta = JSON.parse(e.data);
  appendToUI(delta.content);
};

2.3 底层八卦

  • 每条 SSE 事件最大 8 KB,再大就拆包(浏览器:我谢谢你)。
  • 上下文窗口用 滑动数组 实现,超出长度就 Array.shift(),像贪吃蛇掉尾巴。
  • Token 计数在服务端 逐字累加,前端实时看到账单跳动,心跳也跟着跳。

3. 🏗️ 对 Web 行业的五连击

领域 旧玩法 MCP 新姿势
前端框架 useEffect 调 API useMcp() Hook,上下文即状态
低代码 拖拽生成死页面 拖拽后让模型实时补全逻辑
搜索 关键字 → 10 条链接 关键字 → 模型总结 + 链接
IDE 插件 补全一行代码 直接补全整个 feature 分支
运维 看日志 让模型读日志并自愈(🐍:危险但刺激)

🎭 配图:一张漫画,React 图标和 GPT 图标手牵手,脚下踩着 Express 说:“兄弟,你歇会儿。”


4. 🪄 实战:30 行 JS 搭一个“会自己写代码”的 Next.js 页面

4.1 目录

app/
 ├─ api/
 │  └─ mcp/
 │     └─ route.ts     ← SSE 端点
 └─ page.tsx           ← 前端魔法阵

4.2 服务端:/api/mcp/route.ts

import { NextRequest } from 'next/server';
import { Readable } from 'stream';

export async function GET(req: NextRequest) {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    start(controller) {
      const send = (data: string) =>
        controller.enqueue(encoder.encode(`data: ${data}\n\n`));

      // 假装这里连的是真 MCP
      send(JSON.stringify({
        jsonrpc: '2.0',
        id: 1,
        result: { greeting: 'Hello MCP, from Next.js Edge Runtime!' }
      }));

      // 每 2 秒推一次代码补丁
      const timer = setInterval(() => {
        send(JSON.stringify({
          jsonrpc: '2.0',
          method: 'textDocument/patch',
          params: { code: 'console.log("✨ auto-generated line")' }
        }));
      }, 2000);

      req.signal.addEventListener('abort', () => clearInterval(timer));
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  });
}

4.3 前端:app/page.tsx

'use client';
import { useEffect, useState } from 'react';

export default function Home() {
  const [lines, setLines] = useState<string[]>([]);

  useEffect(() => {
    const es = new EventSource('/api/mcp');
    es.onmessage = (ev) => {
      const { params } = JSON.parse(ev.data);
      if (params?.code) setLines(l => [...l, params.code]);
    };
    return () => es.close();
  }, []);

  return (
    <main className="p-8 font-mono">
      <h1 className="text-2xl mb-4">🪄 MCP 正在替我写代码:</h1>
      <pre className="bg-gray-900 text-green-400 p-4 rounded">
        {lines.join('\n')}
      </pre>
    </main>
  );
}

🖼️ 动态图:页面每 2 秒自动追加一行 console.log,背景是黑客帝国雨。


5. 🔥 踩坑 & 彩蛋

原因 解药
浏览器 6 条 SSE 并发限制 HTTP/1.1 规范 升级到 HTTP/2,或把多个流复用在同一条 SSE
Nginx 缓存 60 秒断流 proxy_buffering=on 关掉: proxy_buffering off;
Token 爆炸 上下文无限增长 前端定期 session.clear(),像清理浏览器缓存
老板 KPI 想让模型 100% 准确 告诉他:模型是概率引擎,不是真理机

彩蛋:
在 DevTools 的 Console 输入

fetch('/api/mcp', { method: 'POST', body: '{"method":"ping"}' })

会看到服务端回你一个 🏓 pong,附带随机一句程序员笑话。


6. 🚀 尾声:下一站,“模型即后端”?

“如果 MCP 继续进化,我们可能会看到:
前端直接 import model from 'gpt://v1'
后端变成一行 export default model
而产品经理的 PRD 直接喂给模型,
它吐出前端 + 后端 + 测试 + 上线脚本。”

到那时,Web 行业的岗位 JD 也许只剩下一句:
“会呼吸,能写 prompt。”


🧙‍♂️ 彩蛋口令

在评论区打出

npx create-mcp-app@latest

即可召唤官方脚手架,一键进入“模型上下文”新世界!

昨天以前首页

基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

作者 得物技术
2025年8月19日 10:20
客服知识库是一个集中管理和存储与客服相关的信息和资源的系统,在自研知识库上线之前,得物采用的承接工具为第三方知识库系统。伴随着业务的发展,知识的维护体量、下游系统的使用面临的问题愈发明显,

基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

作者 得物技术
2025年8月19日 10:20

一、 背景

客服知识库是一个集中管理和存储与客服相关的信息和资源的系统,在自研知识库上线之前,得物采用的承接工具为第三方知识库系统。伴随着业务的发展,知识的维护体量、下游系统的使用面临的问题愈发明显,而当前的第三方采购系统,已经较难满足内部系统间高效协作的诉求,基于以上业务诉求,我们自研了一套客服知识库。

二、富文本编辑器的选型

以下是经过调研后列出的多款富文本编辑器综合对比情况:

2.1 编辑器的选择

  • 自研知识库要求富文本编辑器具备表格的编辑能力,由于Quill不支持表格编辑能力(借助表格插件可以实现该能力,但经过实际验证,插件提供的表格编辑能力不够丰富,使用体验也较差),被首先被排除。
  • wangEditor体验过程中发现标题和列表(有序、无序)列表两个功能互斥,体验不太好,而这两个功能都是自研知识库刚需功能,也被排除。
  • Lexical是facebook推出的一款编辑器,虽功能很丰富,但相较于CKEditorTinyMCE,文档不够完善,社区活跃性较低,插件不成熟,故优先选择CKEditorTinyMCE

CKEditorTinyMCE经过对比,由于当前正在使用的第三方知识库采用的是TinyMCE编辑器,选择TinyMC在格式兼容上会更友好,对新老知识库的迁移上更有利。且TinyMCE在功能丰富度上略占优势,故最终选择TinyMCE作为本系统文档知识库的编辑器

2.2 TinyMce编辑器模式的选择

经典模式(默认模式)

基于表单,使用表单某字段填充内容,编辑器始终作为表单的一部分。内部采用了iframe沙箱隔离,将编辑内容与页面进行隔离。

※ 优势

样式隔离好。

※ 劣势

由于使用iframe,性能会差点,尤其对于多实例编辑器。

内联模式(沉浸模式)

将编辑视图与阅读视图合二为一,当其被点击后,元素才会被编辑器替换。而不是编辑器始终可见,不能作为表单项使用。内容会从它嵌入的页面继承CSS样式表。

※ 优势

性能相对较好,页面的编辑视图与阅读视图合二为一,提供了无缝的体验,实现了真正的所见即所得。

※ 劣势

样式容易受到页面样式的影响。

三、系统总览

3.1 知识创建链路

3.2 知识采编

结构化段落

为了对知识文档做更细颗粒度的解析,客服知识库采用了结构化段落的设计思想,每个段落都会有一个唯一标志 ,且支持对文档的每个段落单独设置标签,这样在后期的知识检索、分类时,便可以精确定位到知识文档的具体段落,如下图所示。

知识文档编辑页面

3.3 应用场景

客服知识库的主要应用场景如下:

知识检索

基于传统的ES检索能力,用于知识库的检索,检索要使用的知识,且可以直接在工作台打开对应的知识并浏览,并可以定位、滚动到具体的知识段落。同时还会高亮显示知识文档中匹配到的搜索关键字

智能问答(基于大模型能力和知识库底层数据的训练)

※ RAG出话

辅助客服了解用户的真实意图,可用于客服作业时的参考。

原理阐述: RAG是一种结合了检索和生成技术的人工智能系统。它是大型语言模型的一种,但特别强调检索和生成的结合。RAG的最主要的工作流程包括:

  • 检索阶段:系统会根据用户的查询,从客服知识库中检索出相关信息。这些信息可能包括知识库内容、订单信息和商品信息等
  • 生成阶段:RAG使用检索到的信息来增强其生成过程。这意味着,生成模型在生成文本时,会考虑到检索到的相关信息,以生成更准确、更相关的回答。你可以直接将搜索到的内容返回给用户也可以通过LLM模型结合后生成给用户。

※ 答案推荐

可以根据用户搜索内容、上下文场景(如订单信息、商品信息)辅助客服更高效的获取答案。

流程示意:

※ 联网搜索

当RAG出话由于拒识没有结果时,便尝试进行联网搜索给出结果,可作为RAG能力失效后的补充能力。

原理阐述: 底层使用了第三方提供的联网问答Agent服务。在进行联网搜索之前,会对用户的查询信息进行风控校验,风控校验通过后,再进行 【指定意图清单】过滤,仅对符合意图的查询才可以进行联网搜索。

四、问题和解决方案

4.1 解决图片迁移问题

背景

在新老知识迁移的过程中,由于老知识库中的图片链接的域名是老知识库的域名,必须要有老知识库的登录台信息,才能在新知识库中访问并渲染。为了解决这个问题,我们对用户粘贴的动作进行了监听,并对复制内容的图片链接进行了替换。

时序图

核心逻辑

/**
 * 替换编辑器中的图片URL
 * @param content
 * @param editor 编辑器实例
 * @returns 替换后的内容
 */
export const replaceImgUrlOfEditor = (content, editor) => {
  // 提取出老知识中的图片访问链接
  const oldImgUrls = extractImgSrc(content);
  // 调用接口获取替换后的图片访问链接
  const newImageUrls = await service.getNewImageUrl(oldImgUrls);
  // 将老知识库的图片链接替换成新的可访问的链接
  newContent = replaceImgSrc(newContent, replacedUrls.imgUrls);
  // 使用新的数据更新编辑器视图
  editor.updateView(newContent);
};

4.2 解决加载大量图片带来的页面卡顿问题

背景

知识库内含有大量的图片,当我们打开一篇知识时,系统往往因为在短时间内加载、渲染大量的图片而陷入卡顿当中,无法对页面进行其他操作。这个问题在老知识库中尤为严重,也是研发新知识库过程中我们需要重点解决的问题。

解决方案

我们对图片进行了懒加载处理:当打开一篇知识时,只加载和渲染可见视图以内的图片,剩余的图片只有滚动到可见视图内才开始加载、渲染。

由于我们要渲染的内容的原始数据是一段html字符串,一篇知识文档的最小可渲染单元是段落(结构化段落),而一个段落的内容大小事先是不知道的,因此传统的滚动加载方式在这里并不适用:比如当滚动到需要加载下一段落的位置时,如果该段落的内容特别大且包含较多图片时,依然会存在卡顿的现象。

我们采用正则匹配的方式,识别出知识文档的html中所有的  标签(将文档的html视作一段字符串),并给  标签插入 loading="lazy" 的属性,具备该属性的图片在到达可视视图内的时候才会加载图片资源并渲染,从而实现懒加载的效果,大大节省了知识文档初次渲染的性能开销。并且该过程处理的是渲染知识文档前的html字符串,而非真实的dom操作,所以不会带来重绘、重排等性能问题。

知识文档渲染的完整链路

4.3 模板缩略图

背景

在知识模板列表页或者在创建新知识选择模板时,需要展示模板内容的缩略图,由于每个模板内容都不一样,同时缩略图中需要可以看到该模板靠前的内容,以便用户除了依靠模板标题之外还可以依靠一部分的模板内容选择合适的模板。

解决方案

在保存知识模板前,通过截屏的方式保存一个模板的截图,上传截图到cdn并保存cdn链接,再对截图进行一定的缩放调整,即可作为模板的缩略图。

时序图

实际效果

模板列表中缩略图展示效果:

新建知识时缩略图展示效果:

4.4 全局查找/替换

背景

知识库采用了结构化段落的设计思想,技术实现上,每个段落都是一个独立的编辑器实例。这样实现带来一个弊端:使用编辑器的搜索和替换功能时,查找范围仅限于当前聚焦的编辑器,无法同时对所有编辑器进行查找和替换,增加了业务方的编辑费力度。

解决方案

调研、扩展编辑器的查找/替换插件的源码,调度和联动多编辑器的查找/替换API从而实现全局范围内的查找/替换。

※ 插件源码剖析

通过对插件源码的分析,我们发现插件的查找/替换功能是基于4个基本的API实现的: find 、 replace 、 next 、 prev 、 done 。

※ 设计思路

通过在多个编辑器中加入一个调度器来控制编辑器之间的接力从而实现全局的查找/替换。同时扩展插件的API辅助调度器在多编辑器之间进行调度

※ 插件源码API扩展

  1. hasMatched: 判断当前编辑器是否匹配到关键字。
  2. hasReachTop:判断当前编辑器是否已到达所查找关键字的最前一个。
  3. hasReachBottom:判断当前编辑器是否已到达所查找关键字的最后一个。
  4. current: 滚动到编辑器当前匹配到的关键字的位置。
  5. clearCurrentSelection: 对编辑器当前匹配到的关键字取消高亮效果。

UI替换

屏蔽插件自带的查找/替换的弹窗,实现一个支持全局操作的查找/替换的弹窗:使用了react-rnd组件库实现可拖拽弹窗,如下图所示:

「查找」

※ 期望效果

当用户输入关键字并点击查找时,需要在文档中(所有编辑器中)标记出(加上特定的背景色)所有匹配到该关键字的文本,并高亮显示出第一个匹配文本。

※ 流程图

「下一个」

※ 期望效果

当用户点击「下一个」时,需要高亮显示下一个匹配结果并滚动到该匹配结果的位置。

※ 流程图

五、总结

在新版客服知识库的研发和落地过程中,我们基于TinyMce富文本编辑器的基础上,进行了功能扩展和定制。这期间既有参考过同类产品(飞书文档、语雀)的方案,也有根据实际应用场景进行了创新。截止目前已完成1000+老知识库的顺利迁移,系统稳定运行。

自研过程中我们解决了老版知识库系统的卡顿和无法满足定制化需求的问题。并在这些基本需求得到满足的情况下,通过优化交互方式和知识文档的加载、渲染性能等方式进一步提升了使用体验

后续我们会结合用户的反馈和实际使用需求进一步优化和扩展客服知识库的功能,也欢迎有同样应用场景的同学一起交流想法和意见。

往期回顾

1.AI质量专项报告自动分析生成|得物技术

2.Rust 性能提升“最后一公里”:详解 Profiling 瓶颈定位与优化|得物技术

3.Valkey 单点性能比肩 Redis 集群了?Valkey8.0 新特性分析|得物技术

4.Java SPI机制初探|得物技术

5.社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术



文 / 煜宸

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

React 牵手 Ollama:本地 AI 服务对接实战指南

作者 LeonGao
2025年8月17日 14:03

在这个 AI 大模型如雨后春笋般涌现的时代,让前端应用与本地大模型来一场 “亲密接触”,就像给你的 React 应用装上一个 “本地智囊团”。今天,我们就来实现一个看似高深实则简单的需求:用 React 对接本地 Ollama 服务。这就好比教两个素未谋面的朋友打招呼,Ollama 是守在本地的 “AI 达人”,React 则是活泼的 “前端信使”,我们要做的就是搭建它们之间的沟通桥梁。

底层原理:通信的奥秘

在开始编码前,我们得先搞明白这两个 “朋友” 是如何交流的。Ollama 作为本地运行的大模型服务,会在你的电脑上开启一个 “通信窗口”—— 也就是 HTTP 服务器,默认情况下这个窗口的地址是 http://localhost:11434。而 React 应用要做的,就是通过 HTTP 协议向这个窗口发送 “消息”(请求),并等待 “回复”(响应)。

这就像你去餐厅吃饭,Ollama 是后厨的厨师,React 是前厅的服务员,http://localhost:11434 就是厨房的传菜口。服务员把顾客的订单(请求)通过传菜口递给厨师,厨师做好菜后再通过传菜口把菜(响应)送回给服务员。

准备工作:工具就位

在正式开始前,我们需要准备好 “食材” 和 “厨具”:

  1. 安装 Ollama:去 Ollama 官网下载并安装,这一步就像把厨师请到厨房里。安装完成后,打开命令行,输入 ollama run llama3 来启动一个基础模型,这里我们用 llama3 作为示例,你也可以换成其他喜欢的模型。
  1. 创建 React 应用:如果你还没有 React 项目,可以用 Create React App 快速创建一个,命令是 npx create-react-app ollama-demo,这就像搭建好前厅的场地。

代码实现:搭建沟通桥梁

一切准备就绪,现在我们来编写核心代码,实现 React 与 Ollama 的通信。

首先,我们需要一个发送请求的函数。在 React 组件中,我们可以用 fetch API 来发送 HTTP 请求到 Ollama 的 API 端点。Ollama 的聊天接口是 http://localhost:11434/api/chat,我们需要向这个接口发送包含模型名称和消息内容的 JSON 数据。

import { useState } from 'react';
function OllamaChat() {
  const [message, setMessage] = useState('');
  const [response, setResponse] = useState('');
  const sendMessage = async () => {
    try {
      // 构建请求体,指定模型和消息
      const requestBody = {
        model: 'llama3',
        messages: [{ role: 'user', content: message }],
        stream: false // 不使用流式响应,等待完整回复
      };
      // 发送 POST 请求到 Ollama 的聊天接口
      const response = await fetch('http://localhost:11434/api/chat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestBody),
      });
      // 解析响应数据
      const data = await response.json();
      
      // 提取并显示 AI 的回复
      if (data.message && data.message.content) {
        setResponse(data.message.content);
      }
    } catch (error) {
      console.error('与 Ollama 通信出错:', error);
      setResponse('抱歉,无法连接到 AI 服务,请检查 Ollama 是否正在运行。');
    }
  };
  return (
    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
      <h2>React × Ollama 聊天 Demo</h2>
      <div style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="输入你的问题..."
          style={{ width: '70%', padding: '8px', marginRight: '10px' }}
        />
        <button onClick={sendMessage} style={{ padding: '8px 16px' }}>
          发送
        </button>
      </div>
      <div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '4px' }}>
        <h3>AI 回复:</h3>
        <p>{response}</p>
      </div>
    </div>
  );
}
export default OllamaChat;

代码解析:庖丁解牛

让我们来仔细看看这段代码的工作原理,就像拆解一台精密的机器。

  1. 状态管理:我们用 useState 钩子创建了两个状态变量,message 用来存储用户输入的消息,response 用来存储 AI 的回复。这就像两个储物盒,分别存放要发送的消息和收到的回复。
  1. 发送消息函数:sendMessage 是核心函数,它通过 fetch 发送请求到 Ollama。请求体中指定了要使用的模型(llama3)和用户的消息。这里的 stream: false 表示我们希望一次性收到完整的回复,而不是逐字接收。
  1. 处理响应:当 Ollama 处理完请求后,会返回一个 JSON 格式的响应。我们从中提取出 AI 的回复内容,并更新 response 状态,这样页面上就会显示出 AI 的回答了。
  1. 错误处理:如果通信过程中出现错误(比如 Ollama 没有运行),我们会捕获错误并显示友好的提示信息。

运行测试:见证奇迹的时刻

现在,让我们来测试一下这个 Demo 是否能正常工作。

  1. 确保 Ollama 正在运行:打开命令行,输入 ollama run llama3,等待模型加载完成。
  1. 启动 React 应用:在项目目录下运行 npm start,打开浏览器访问 http://localhost:3000
  1. 发送消息:在输入框中输入一个问题,比如 “你好,Ollama!”,然后点击 “发送” 按钮。稍等片刻,你应该就能看到 AI 的回复了。

如果一切顺利,你会看到 React 应用和 Ollama 成功 “牵手”,完成了一次愉快的对话。如果遇到问题,先检查 Ollama 是否正在正常运行,模型名称是否正确,网络连接是否通畅。

进阶思考:拓展可能性

这个简单的 Demo 只是一个开始,就像我们只是搭建了一座简陋的小桥。你可以基于这个基础进行很多拓展:

  1. 实现流式响应:将 stream 设置为 true,然后处理流式响应,让 AI 的回复像打字一样逐字显示,提升用户体验。
  1. 增加聊天历史:用状态管理存储聊天记录,让对话可以上下文连贯。
  1. 切换不同模型:在界面上增加模型选择功能,让用户可以根据需要切换不同的 Ollama 模型。
  1. 优化错误处理:增加更详细的错误提示,帮助用户排查问题。

总结:本地 AI 的魅力

通过这个 Demo,我们展示了 React 对接本地 Ollama 服务的全过程。相比于调用云端的 AI 服务,本地部署的 Ollama 具有隐私性好、响应速度快、无需网络连接等优点,就像把 AI 助手请到了自己家里,随时可以交流。

希望这篇文章能帮助你理解 React 与本地 AI 服务对接的原理和方法。现在,你可以基于这个基础,开发出更强大、更有趣的本地 AI 应用了。让我们一起探索前端与 AI 结合的无限可能吧!

❌
❌