ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于MCP与Senparc.AI的网页端代码推荐服务实战:从SSE流式到Monaco集成

2026/10/6 16:26:08 拓冰建站 浏览量
基于MCP与Senparc.AI的网页端代码推荐服务实战:从SSE流式到Monaco集成 如果你觉得网页端 AI 代码推荐 调大模型接口 把结果流式打回去那后面大概率会吃大亏。我最初交付的第一版就是这样编辑器里取几行代码、拼进 prompt、等补全。内测时推荐十次里只有两三次能真正落盘剩下全在看图说话——模型根本不知道当前项目有哪些模块、依赖里有没有这个包、团队规范允许不允许这么写。后来我把方案整体切换成 Senparc.AI MCPSSE由 Senparc.AI 负责模型接入与智能体调度通过 MCP 协议把工程上下文封装成一组模型按需调用的工具推荐结果通过 SSE 流式推到网页端才算真正跑通。这组方案目前已经稳定用在我们内部的 Web IDE 辅助功能里。我会把完整落地过程写下来从 MCP Server 搭建、SSE 通信、Senparc.AI Agent 接入到前端编辑器集成和线上排坑给正在做代码推荐生成服务或内部代码助手的开发者一份可参考的实战记录。1. 这不是一个调大模型 API的项目先看清需求全貌1.1 第一版为什么长满幻觉代码第一版 demo 只有几百行代码逻辑简单到不需要画图前端把光标前的代码截 2000 字符后端调大模型补全接口模型返回的文本直接塞进编辑器。给领导和同事演示的时候效果确实唬人——敲完函数签名按一下快捷键后续代码就自动续了出来。可一进入真实项目试用问题就全暴露了。最典型的一类错误我印象很深模型推荐了一个OrderService.GetByUserAsync()的调用看起来命名风格、参数类型都像是我们团队会写的东西但代码库里根本没有这个方法。因为 prompt 里只有光标前方那段代码模型完全看不到项目里实际有什么它只能根据训练数据里大多数项目的惯例去猜。这类推荐我统称为幻觉代码。它最大的危害不是报错而是看起来特别合理容易让开发者不做检查就直接写进去。1.2 补全之外的四个隐性需求第一版被打回去之后我把需求重新拆了一遍。做网页端代码推荐生成服务真正要满足的其实有四件事第一推荐必须知道项目上下文。包括文件结构、命名空间、常用类与方法、关键依赖。模型不应该靠猜而是按需求去查。第二结果要够快、且能流式展示。网页端体验和本地 IDE 的补全不一样用户等不了十几秒的转圈必须边生成边渲染。第三推荐结果要能安全地插入编辑器。不是纯文本覆盖而是能预览、接受、拒绝甚至是部分接受。第四能力要能持续扩展。今天做推荐明天可能就要做提交信息生成接口文档补全不能让每一次新能力都从零搭一套 AI 接入管道。前两点直接促成了我对 MCP 的选择把查询上下文做成模型按需调用的工具让工具层去接各类数据源把流式输出做成 SSE 通道。后两点则落在了 Senparc.AI 上它本身内置了 Agent、工具调度和会话管理后续扩展新能力不需要我再单独维护一套调度逻辑。2. MCPSSE与 Senparc.AI 在链路中的职责边界这一节先把几个关键概念对齐一下后面写实现的时候不用来回解释。2.1 MCP 的标准化夹层把工具变成 AI 可发现的资源MCPModel Context Protocol模型上下文协议解决的核心问题是让 AI 应用能以标准方式发现并调用外部能力和数据。你可以把它理解成给 AI 配了一套外部设备的 USB 接口标准只要是支持 MCP 的服务Agent 不需要额外写对接代码就能通过一套约定好的协议去发现它有什么工具、每个工具要什么参数、调用后返回什么。在协议层面MCP 基于 JSON-RPC 2.0。客户端连接服务器后第一步做 initialize 握手并协商能力然后通过 tools/list 获取工具清单需要执行时通过 tools/call 发起调用。整个流程不依赖任何特定模型也不绑定任何特定语言。对我们做网页端代码推荐来说这套工具层的价值非常大。工程上下文查询查文件结构、搜代码模式、解析依赖版本原本要后端写一堆 REST 接口然后我再去 Agent 里硬编码适配。现在只需要把一个一个查询能力注册成 MCP 工具Agent 里的模型就能在生成代码时自主决定是不是该查一下项目结构再写这是我第一版方案里最缺的环节。2.2 为什么远程场景必须走 HTTP SSEMCP 的传输层有两种主流模式stdio 和 HTTP SSE。stdio 模式适合本地进程直接拉起的场景例如在命令行工具里运行一个 MCP Server两个进程通过标准输入输出通信零网络开销。我们做的是网页端服务Server 要么部署在内网机房里要么作为独立容器运行客户端来自浏览器所以只能走远程传输。从 MCP 官方对 HTTP 传输的定义来看远程模式使用 Server-Sent EventsSSE往客户端推送数据。SSE 是单工、基于 HTTP 长连接的技术服务器可以在一个连接上持续向客户端发送事件。它和 WebSocket 最大的区别是方向性WebSocket 会建立全双工通道而 SSE 只需要服务端往客户端推客户端发请求走普通 HTTP POST 就够了。MCP 正好符合这个形态浏览器/后端 Agent 发起一次 POST 请求并携带工具调用指令MCP Server 在执行期间把进度消息、工具结果通过 SSE 逐个推送回来。我对 SSE 的另一个偏好是它天然带重连和事件格式。连接意外断开时浏览器端 EventSource 对象会自动重连如果是自定义 fetch 读取也可以根据事件流里的 retry 字段做重试策略。这对线上环境很重要后面会专门讲我在这个环节踩的坑。2.3 Senparc.AI 的定位Agent 调度与模型接入的统一入口Senparc.AI 在整套链路里不是又一个模型 SDK而是模型接入与智能体调度的中间层。我在项目里同时面对了 OpenAI、内部大模型网关等几家不同的模型来源如果每家服务的请求格式、鉴权方式都自己维护一遍工作量会失控。Senparc.AI 把这层差异封装掉了我只需要在配置里改模型提供方和 Key上层业务代码不用跟着变。它更关键的能力是 Agent 管道。代码推荐并不是用户问一句、模型回一句那么简单而是理解当前代码 - 决定是否需要查询工程上下文 - 调用工具 - 结合工具结果生成代码的多步过程。Senparc.AI 的 Agent 会把模型的意图分析、工具调用、结果回填串成一个可编排的流程并且允许我把 MCP Server 地址注册进来让 Agent 直接使用 MCP 工具。这样一来我的业务层代码只需要关心拿到推荐结果并推给前端工具边界和上下文管理都交给了 Agent 框架。为了避免误解我补充一句MCP 和 Senparc.AI 不是竞争关系而是不同层的组件。MCP 解决的是模型如何标准化地访问工具Senparc.AI 解决的是如何把模型、工具、会话编排在一起服务业务。二者组合起来才会出现模型能按需调工具、Agent 能管理工具调用过程的完整链路。3. 服务端实现从 MCP Server 注册到 Senparc.AI 接入3.1 用 C# 起一个最小的 MCP ServerSSE 传输因为整个后端本来就是 .NET 技术栈MCP Server 我直接挂在 ASP.NET Core 进程里省掉一类额外进程的部署成本。下面是一段简化的 MCP Server 注册代码核心是把代码推荐需要的能力暴露成工具// 示意代码具体 API 以当前使用的 MCP SDK 版本为准 var builder WebApplication.CreateBuilder(args); var mcpServer builder.Services.AddMcpServer() .WithTool( name: get_project_context, description: 获取当前项目的目录结构、命名空间、关键依赖摘要。 当模型需要了解项目整体情况、或不确定引用的包是否存在时使用。 参数 projectId目标项目标识。, handler: async (string projectId) { return await projectService.GetCompactContextAsync(projectId); }) .WithTool( name: search_code_pattern, description: 在项目代码库中搜索与关键字相关的类、方法或代码片段。 模型准备推荐代码前若不确定项目是否已有相同实现或约定接口应先用本工具确认。, handler: async (string keyword, int limit 5) { return await codeIndex.SearchAsync(keyword, limit); }) .WithTool( name: get_package_usage, description: 查询指定依赖包在项目中的安装版本和常用用法摘要。, handler: async (string packageName) { return await dependencyService.GetPackageUsageAsync(packageName); }); mcpServer.WithHttpTransport(/mcp/sse); var app builder.Build(); app.MapMcpServer(); app.Run();我建议工具粒度按模型决策的最小单元来切不要做一个工具干所有事。比如把获取项目上下文和搜索代码模式分开模型在推荐一个 Service 方法时可能只需要搜代码模式不一定非要读整个项目结构合并成一个大工具会让模型图省事每次把一大堆不相关内容拉进上下文既费 token 又干扰推理。3.2 工具 Handler 内部的数据压缩技巧工具 Handler 的返回值会直接作为上下文喂给模型所以返回什么和工具本身一样重要。我最初的 Handler 直接返回项目文件树结果一个中型仓库的树文本就有几万 token模型根本处理不过来。后来我总结出一个原则给模型的结构优先返回摘要 全文兜底。比如 get_project_context 默认只返回一级目录结构核心项目文件清单如工程文件、解决方案、配置文件每个关键类文件的类名 命名空间 公开方法签名而不是完整源码等模型真的需要看某个文件的实现细节时再由另一个工具比如 read_file_snippet去取指定文件、指定行区间的原文。这样一次工具调用消耗的 token 能压到几百模型反而更容易从摘要里找到它需要的信息调用下一轮工具的准确度也更高。3.3 Senparc.AI Agent 配置与一次推荐请求的完整内部流转MCP Server 准备好之后剩下的工作就是把 Senparc.AI 的 Agent 接进来。我这边大致是这样配置的示意代码var client new SenparcAiClient(new SenparcAiOptions { ModelProvider ModelProvider.OpenAI, ApiKey Environment.GetEnvironmentVariable(AI_API_KEY) }); var agent await client.CreateAgentAsync(code-recommend-agent); agent.SystemInstructions 你是一个代码推荐助手。你的任务是 1. 先理解用户提供的当前代码上下文与光标位置 2. 如果当前代码中引用到的类型、方法或包存在不确定性必须先调用 MCP 工具确认 3. 未经工具确认不得输出可能不存在的 API 或类 4. 最终只提交代码块和极简注释不要输出冗长解释。; agent.RegisterMcpServer(code-context, http://localhost:5231/mcp/sse);一次网页端推荐请求内部流转大致是这样的前端把当前文件内容 光标偏移 项目标识发过来Senparc.AI 的 Agent 收到消息模型综合分析后决定先调用 get_project_context 或 search_code_patternAgent 启动 MCP 客户端通过 SSE 长连接向 MCP Server 发起调用请求等待工具结果MCP Server 的 Handler 执行查询先把结果压缩成摘要再返回工具结果被回填到模型上下文模型开始续写推荐代码模型输出的文本流式返回给业务层业务层再包装成 SSE 事件推给前端。这个过程里有一点很容易被忽略模型不是每次推荐都会调用工具它可能判断当前内容足够清楚就直接生成了。所以 Agent 的系统提示词和工具的 description 写得越具体模型什么时候该查、什么时候不该查就越不容易出错。我在后台留过一段日志能直观看到这种多步决策[Agent] 收到推荐请求 (file: Controllers/OrderController.cs, cursor: line 42) [Agent] 意图分析用户正在编写 CreateOrder 方法不确定项目内是否已有订单校验逻辑 [Agent] 决定调用工具 search_code_pattern关键字 OrderValidator [MCP] 通过 SSE 通道发送 tools/call [MCP] 工具返回找到 OrderValidator.ValidateAsync3 处引用摘要完成 [Agent] 工具结果已回填继续生成推荐代码 [Agent] 流式输出开始...4. 前端接入Monaco Editor 如何拼接流式推荐4.1 用 fetch ReadableStream 解析 SSE而不是 EventSource很多教程里提到 SSE 就默认用 EventSource但它有一个硬限制只能发 GET 请求。代码推荐要提交的是完整文件内容和光标位置体量动辄几千字符甚至还带鉴权 header塞进 URL 既不合适也不安全。所以我用的是 fetch 发起 POST然后通过 response.body 的 ReadableStream 手动解析 SSE 格式。SSE 的消息格式其实很简单多个字段以换行分隔不同消息之间隔一个空行。最常见的字段是 data。一个推荐片段大概长这样data: {type:delta,content: public async TaskOrder}前端解析的示意代码如下async function requestCodeRecommend(content, cursorOffset, filePath, signal) { const resp await fetch(/api/code-recommend, { method: POST, headers: { Content-Type: application/json, X-Token: token }, body: JSON.stringify({ content, cursorOffset, filePath }), signal: signal }); const reader resp.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const parts buffer.split(\n\n); buffer parts.pop(); // 保留未完成的半条事件 for (const part of parts) { for (const line of part.split(\n)) { if (line.startsWith(data:)) { const payload line.slice(5).trim(); try { const json JSON.parse(payload); if (json.type delta) appendSuggestion(json.content); } catch (e) { // 兼容非 JSON 的纯文本事件 } } } } } }一个实用细节TextDecoder 一定要开启{ stream: true }。中文等多字节字符在 UTF-8 下可能被分在两个网络包里如果不传 stream 参数解码器遇到截断的字节序列会抛错或者输出乱码。我第一次没注意线上出现过一阵偶发的乱码字幕排查了很久才发现是 TextDecoder 的默认非流式模式导致的。4.2 触发时机、取消与悬浮建议的交互策略流式解析只是第一步真正影响体感的是编辑器交互策略。我用的是 Monaco Editor 提供的 completion provider 机制用户输入停顿约 500ms 后触发一次推荐请求返回值通过一个自定义的建议槽位展示用户按 Tab 或点击才接受。有个交互细节值得单独说在流式生成过程中用户可能已经继续打字了。如果此时还在把生成的代码往建议里灌会出现推荐结果跟用户新输入打架的情况。所以我在发请求时就保存了 AbortController一旦编辑器内容发生变化且偏移量和请求时不一致立刻中止还在运行的 SSE 流。宁可这次推荐作废也不要展示一份基于旧代码的结果。另外Monaco 的 completion provider 是同步返回建议项的流式内容不能直接塞进去。我的处理方法是先注册一个空的占位建议项在推荐服务流式返回片段时更新该项的 insertText 和 detail 描述。这样用户看到的效果就是推荐内容逐字往上跳而且按 Tab 能正确插入。提示代码推荐这类场景取消机制不是附加功能而是必需功能。没有取消的流式推荐几乎必然会在用户快速输入时造成旧推荐覆盖新意图的体验事故。5. 线上踩坑记录断流、超时与上下文漂移5.1 SSE 流在半路断开一份完整的排查链路服务上线后第一个投诉是网页端的推荐流偶尔生成到一半就停了浏览器控制台里能看到类似 stream disconnected before completion 的报错也有人管它叫 idle timeout waiting for sse。第一次遇到这个错我以为是后端代码问题但开发环境怎么测都复现不了。排查链路大概走了一遍严格来说是从连接链路的外层向内层逐层排除的先是 nginx。线上流量都走 nginx 反代开发环境直连后端端口这个差异立刻成为最可疑点。我用 curl 连着打了几次线上 SSE 端点发现长时间没有数据时连接在大约 60 秒后被 nginx 掐断。SSE 虽然能维持连接但 nginx 的 proxy_read_timeout 默认 60 秒期间没有任何数据返回上游就认为超时了。修复方式是调整代理配置并为 SSE 单独设置路由location /mcp/sse { proxy_pass http://backend; proxy_buffering off; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_http_version 1.1; proxy_set_header Connection ; }这里 proxy_buffering off 也很关键。nginx 如果开着缓冲会把上游吐出来的小块事件攒成大批再发给浏览器流式效果直接被打没了。外层搞定后我又在后端加了一层心跳保证。因为代码推荐工具在正常执行时会有几秒空窗这段没有事件输出的时间足够让任何中间层以为连接死了。我在后端按固定间隔输出 SSE 注释行: pingSSE 格式里以冒号开头的行是注释浏览器会忽略它但连接被续命。对长期保持的 MCP SSE 连接这套注释行心跳是标准做法。5.2 推荐过的代码被当成项目已有代码上下文漂移处理第二个坑更隐蔽。我的会话机制最初设计成保留历史消息方便用户连续追问改成异步版本加上异常处理。结果跑了一段时间后推荐质量明显劣化模型动不动就把上一轮自己推荐过的代码当成项目里已经存在的代码围绕它继续展开甚至出现自相矛盾的推荐。根因是上下文策略不对。代码推荐这个场景里历史消息里的AI 推荐的代码和用户原本的代码对模型来说都是文本它根本分不清哪段是事实、哪段是自己生成过的假设。既然分不清模型就很容易把假设当作事实继续推理。我的处理方案有三层缺一不可代码推荐会话只保留最近两轮上下文超过的部分定期裁剪不再做完整历史回溯系统提示词里明确写了一句你在此会话中生成过的任何代码不得作为后续推荐的依据在消息数据结构里给 AI 推荐结果单独打一个 role 标记工具层面把它与用户代码隔离存储。这个经验倒不只在代码推荐场景里成立。凡是AI 输出物会被写回上下文、并且后续还会继续生成的场景都建议用类似的方式把生成的东西和事实的东西分开否则模型一定会在某一轮开始自我引用。提示发生了上下文漂移不要只靠改 prompt 解决。prompt 是告诉模型不要这么干而数据结构隔离是让模型无法这么干后者往往更可靠。5.3 多用户并发的隐性串话会话边界必须显式隔离第三个问题在压测时暴露。同时开三个浏览器标签做推荐A 标签页的推荐内容里混进了 B 标签页才可能出现的代码引用。查了很久才发现原因不是模型调错了而是我的 Agent 被当成了单例复用工具调用的返回值放在一个静态缓存里并发请求到达时后一个请求覆盖了前一个的结果。这类问题在普通 Web 应用里不常见但 AI 工具调用是异步的时间线会拉长静态状态很容易被交叉污染。我给每个编辑器页签/用户会话创建独立的 ChatSession 实例并在每次推荐请求中显式携带 sessionIdMCP Server 的 Handler 一律只按请求参数计算不读任何全局状态。这里最反直觉的一点是MCP 工具本身设计成无状态服务但你自己写的 Handler 可能会因为贪方便把结果缓存到静态字段里。正确做法是让工具执行期间的所有中间结果都走请求作用域连并发缓存都要带 SessionId 作为 key。6. 性能复盘与让推荐结果更可靠的调优思路6.1 一次推荐请求的时间都花在了哪儿线上稳定之后我统计了一批请求的耗时分布。用表格整理出来阶段约占比说明模型意图判断与工具选择15%受工具数量和 description 质量影响MCP 工具执行与结果压缩20%查询索引、读依赖、摘要生成模型生成推荐代码55%与 max_tokens、模型参数量相关网络与前端渲染10%SSE 分发 Monaco diff 计算最值得压缩的是模型生成阶段。一个常见的误区是把 max_tokens 拉到很大希望模型一次把方案写完整。我的实践是把它控制在 300~600 token 左右让模型专注生成关键代码片段如果用户需要完整方案再通过后续指令继续展开。推荐任务不是写论文一次给太多的输出反而容易失控。工具执行阶段也有优化空间。原本 get_project_context 每次都是即时查询并实时做摘要后来我在内部索引服务里加了缓存项目结构变化触发的版本号更新才刷新缓存。实测这块耗时从 1.2 秒降到了 300 毫秒以内。6.2 工具描述与采样参数两个常被忽视的质量杠杆很多人调推荐质量只盯着 prompt 和模型版本却忽略了两个同样重要的点。一个是 MCP 工具的 description 要写得会让模型困惑。description 写得太泛模型会把不相干的请求也调工具写得太窄该调的时候又漏调。我后来遵循的模板是这个工具什么时候用、什么时候不用、参数应该怎么传、返回结构是什么。比如 get_package_usage 的 description 明确写了当用户代码引用了某个包而不确定版本是否兼容时使用模型就能更准确地触发它。另一个是采样参数。代码类任务不适合高随机性我用的 temperature 在 0.2~0.4 之间top_p 也收敛在 0.8 左右。这个配置下生成速度快于激进采样且幻觉代码出现的频率有肉眼可见的下降。当然不同模型提供方的参数意义略有差异但逻辑是一致的代码推荐更接近检索增强的补全不是创意写作模型的确定性应该优先。除了这些调优之外还有两个调试层面的习惯想提醒一下。第一个MCP Server 的工具都注册完之后先别急着接 Agent。用官方调试控制台或者一个简单的测试客户端把每个工具手动调一遍确认返回值结构是模型友好的扁平 JSON。否则等模型调用时报错排查链路会长很多——你没法确定到底是工具写错了还是模型的调用参数传错了。第二个务必保留一条无 MCP 工具的降级链路。当 MCP Server 短暂不可用的时候直接走仅用当前文件上下文推荐的老逻辑。哪怕推荐质量低一点也比整个服务报错强用户对没有推荐的容忍度远低于对推荐了但不如不推荐的容忍度。至少在我们内部这两条建议帮我节约了大量应急排查时间。