为什么长输出需要背压控制?
当一个 MCP 工具执行 git log 或查询上万条监控数据时,如果不做流式分片,工具会将所有数据一次性拼装成超大 JSON 响应。这会导致:
- 客户端在数十秒内处于白屏等待状态;
- 进程内存瞬间暴涨,甚至触发 Node.js 或 Python 运行时的 OOM 崩溃。
分块流式通道架构
MCP 规范通过支持 progressToken 与分片消息,允许服务端在长时间计算过程中持续上报进度通知(Progress Notifications)。
结合客户端的流式消费队列,当消费者消费速度跟不上生产者产出时,暂停从操作系统网络缓冲读取,利用 TCP 滑动窗口自然反压服务端生产速率,保证极端负载下的系统平稳运行。
实操:给高输出量工具加上分片与进度
- 接口先收口。为工具设计显式的范围参数(游标、页大小或行数上限),默认值取保守档位,宁可让模型多调一次,也不让它一次拉爆;
- 长任务带进度。客户端在
tools/call请求中携带progressToken,服务端在执行期间周期性发送进度通知,进度值只需单调递增,不必强行换算成百分比; - 分片控制粒度。以"单片数据能让客户端在数百毫秒内处理完"为经验值切分,按行或按记录边界切,不要在 JSON 结构中间硬切;
- 断流可续传。每片携带序号或游标,连接中断后客户端从最后成功位置续拉,而不是整个任务重来。
三种交付方式对比
| 方式 | 首字延迟 | 内存峰值 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 一次性完整 JSON | 高 | 高 | 低 | 小结果集(几百行内) |
| 游标分页轮询 | 低 | 可控 | 中 | 可预测的结构化数据 |
| 进度通知 + 分片推送 | 低 | 低 | 中高 | 长耗时且需实时反馈的任务 |
传输通道本身的选择(stdio 还是 SSE/WebSocket)会显著影响分片行为,建议先读 stdio vs SSE vs WebSockets:MCP 传输层选型对比与网络拓扑架构 再定方案。
验证方法
构造最坏输入压测:对超大仓库执行日志类命令、对全表发起无限制查询,观察工具进程的常驻内存曲线是否平稳;再做慢消费者测试——故意让客户端延迟处理每一片,验证生产者速率随之下降、服务端缓冲有上限,而不是内存一路走高。
常见故障速查
| 现象 | 原因 | 处理 |
|---|---|---|
| 客户端长时间收不到后续分片 | 中间代理空闲超时切断连接 | 增加心跳通知并调长代理超时 |
| 分片合并后顺序错乱 | 并行生产者未带序号 | 每片携带单调序号,客户端按序拼接 |
| 有背压仍内存暴涨 | 应用层队列无上限,未真正暂停读取 | 限制队列长度,超限时停止从套接字读取 |
| 进度通知刷屏 | 每条记录都上报一次 | 节流合并,经验值为每数百毫秒一次 |
小结
验收标准可以量化:在最坏输入下,工具进程内存峰值不随结果集大小线性增长(曲线趋平即合格),且慢消费场景下生产者能自动降速。任何一条不满足,说明缓冲仍然在替系统"扛雷"。设计顺序建议是:先加分页参数兜底,再上进度通知,最后才处理精细的滑动窗口调优。