绝大多数 MCP 工具返回的是文本:查询结果、文件内容、日志片段。但真实工作里有大量信息天生不是文本——界面长什么样、图表趋势如何、这段音频里有没有异常、这份 PDF 的表格结构是什么。把它们硬转成文本(describe_screenshot 返回一段自然语言描述)会丢掉细节,而且转述本身又要花一次模型调用。
MCP 的 content 数组允许工具直接返回图像与音频块,让「工具看到的」和「模型看到的」是同一份数据。本文要回答的是:四种内容块怎么用、base64 内联和资源链接各自适合什么场景、一张 1080p 截图到底吃掉多少 token、模型不支持视觉时怎么降级、以及大二进制返回时最容易踩的坑。
1. 内容块的四种类型
tools/call 的返回值是 content 数组,可以混装多种块:
{
"content": [
{ "type": "text", "text": "页面已渲染,检测到 3 处对比度不足" },
{
"type": "image",
"data": "<base64>",
"mimeType": "image/png"
},
{
"type": "audio",
"data": "<base64>",
"mimeType": "audio/wav"
},
{
"type": "resource",
"resource": { "uri": "file:///tmp/report.pdf", "mimeType": "application/pdf" }
}
]
}
1.1 各类型的适用场景
| 块类型 | 载荷 | 典型场景 | 成本 |
|---|---|---|---|
text | 字符串 | 结论、状态、结构化 JSON | 低 |
image | base64 + mimeType | 截图、图表、UI 状态、OCR 前的原图 | 中高(视觉 token) |
audio | base64 + mimeType | 语音转写前的原音频、异常音频检测 | 高 |
resource | URI + mimeType | 大文件、可复用产物、需二次读取的内容 | 取决于后续是否读取 |
1.2 混装是常态
一次工具调用同时返回「结论 + 证据」通常比只返回结论更有用:
截图工具:
text → "登录页已加载,表单可见"
image → 实际截图(模型可自行核对细节)
顺序上把 text 放前面:多数客户端在渲染与上下文注入时按顺序处理,先给结论能让模型快速建立预期。
2. base64 内联 vs 资源链接
2.1 两种方式的对比
| 维度 | 内联 base64 | 资源链接(resource / resource_link) |
|---|---|---|
| 模型能否直接看到 | 是 | 需客户端或后续工具读取 |
| 传输成本 | 体积 ×1.33(base64 膨胀) | 仅 URI |
| 上下文成本 | 立即计入 token | 按需计入 |
| 可复用性 | 无(一次性) | 高(URI 可再次读取) |
| 适用体积 | 建议 < 500KB | 无上限 |
2.2 决策规则
需要模型"看图判断" → 内联 image(缩到必要分辨率)
只是"产物留档/供后续处理" → resource_link
同一图片会被多次使用 → resource_link + 缓存
图片超过 1MB → 先压缩/降采样,仍超则改链接
2.3 返回 resource_link 的形态
{
"content": [
{ "type": "text", "text": "已生成 12 页报表" },
{
"type": "resource_link",
"uri": "file:///tmp/report-2026-10.pdf",
"name": "月度报表",
"mimeType": "application/pdf",
"size": 4821133
}
]
}
resource_link 只给引用不给内容,是大产物的默认选择。客户端可以把它渲染成可点击的附件,模型则在需要时再调工具读取指定页。这种「先给引用、按需取内容」的模式,是控制上下文膨胀最有效的手段,与 https://plumephp.com/mcp-resources-prompts/ 中资源按需注入的思路一致。
3. 图像的 token 成本
3.1 粗略换算
视觉模型通常按图像块计费。以常见的「长边归一 + 分块」策略估算:
图像 token ≈ (宽/块宽) × (高/块高) × 每块 token + 基础开销
经验值(长边上限 1568px 的模型):
512×512 ≈ 250 ~ 400 token
1024×768 ≈ 700 ~ 1100 token
1920×1080 ≈ 1300 ~ 1600 token
4K 截图 ≈ 被自动降采样,但仍按降采样后的尺寸计费
3.2 一张截图有多贵
对比一下:一张 1920×1080 的 PNG 截图约 1500 token,而一段 1500 token 的文本大约是 1000 个汉字。也就是说,返回一张全屏截图 ≈ 返回一篇千字文章。如果一个 Agent 循环里每步都截图,20 步就是 30000 token 的视觉开销。
优化手段按性价比排序:
| 手段 | 效果 | 代价 |
|---|---|---|
| 只截元素区域而非全屏 | 减少 60%~90% | 需可靠的元素定位 |
| 降采样到长边 1024 | 减少 50%~70% | 小字可能不可辨 |
| 转 JPEG(quality 80) | 传输减少 60%~80% | token 不变,仅省带宽 |
| 转 WebP | 传输减少 70%~85% | 部分模型不支持该格式 |
| 只截变化区域 | 依场景 | 实现复杂 |
注意第二、四行的差别:JPEG/WebP 主要省的是传输与内存,不是 token。token 由解码后的像素尺寸决定,所以「压缩格式」和「降采样」是两件不同的事。真正的省钱手段是降低分辨率或缩小截取范围。
3.3 降采样实现
import sharp from "sharp";
async function toContentBlock(png: Buffer, maxEdge = 1024): Promise<ImageBlock> {
const meta = await sharp(png).metadata();
const long = Math.max(meta.width ?? 0, meta.height ?? 0);
const scale = long > maxEdge ? maxEdge / long : 1;
const out = scale < 1
? await sharp(png).resize({ width: Math.round((meta.width ?? 0) * scale) }).png({ compressionLevel: 9 }).toBuffer()
: png;
return { type: "image", data: out.toString("base64"), mimeType: "image/png" };
}
截图类工具的完整交互设计(等待渲染稳定、元素定位、a11y 树替代方案)见 https://plumephp.com/mcp-browser-web-tools/——其中「能用文本结构表达的,优先用文本」这条原则,同样适用于一切多模态工具。
3.4 什么时候不该返回图像
多模态不是「能传图就传图」。下面几种情况,文本比图像更合适:
| 情况 | 为什么不用图像 | 替代方案 |
|---|---|---|
| 内容本身是文本(日志、代码、表格) | 转成像素后模型读错字符 | 直接返回文本 |
| 只需判断「有没有」 | 图像成本远高于布尔值 | 返回 {"found": true} |
| 需要精确定位坐标 | 视觉模型的坐标回归不精确 | 配合 DOM / a11y 树返回结构化坐标 |
| 需要在后续轮次反复引用 | 每轮都要重新传图 | 落盘 + resource_link |
| 图像含大量小字(如整页 PDF) | 降采样后不可读 | 先 OCR 出文本,图作为证据附上 |
第 5 条是最容易翻车的场景:一份 A4 扫描件缩到 1024 长边后,正文小字基本糊成一团,模型只能靠猜。正确顺序是先文本后图像——OCR 给出可检索、可引用的文本,图像块只作为「原证据」补充。
PDF 处理推荐流程:
1. 抽取文本层(若有)→ 直接返回文本
2. 无文本层 → OCR → 返回文本 + 每页缩略图(可选)
3. 图表页 → 单独渲染为图像块(图表信息在像素里)
4. 模型能力探测与降级
4.1 不是所有模型都能看图
MCP 协议层不关心模型能力,但工具设计必须关心。服务器无法直接查询客户端用的是什么模型,可行做法有三种:
1. 客户端能力/元数据:部分客户端在 initialize 的 clientInfo 里暴露模型信息
2. 工具参数显式声明:入参加 modality: ["text","image"] 由 Agent 侧填写
3. 结果双轨:同时返回 text 摘要与 image,让不支持视觉的模型至少能用摘要
方案 3 最稳:它不依赖任何探测,代价是 token 翻倍。折中做法是让 include_image 成为可选参数,默认返回文本摘要。
4.2 双轨返回示例
{
"content": [
{ "type": "text", "text": "图表:Q3 营收 1.2 亿,环比 +18%;Q1/Q2 分别为 0.8/1.0 亿。趋势向上,Q3 斜率最大。" },
{ "type": "image", "data": "<base64>", "mimeType": "image/png" }
]
}
文本部分是信息等价摘要,不是「见下图」这种占位。这样即便图像块被客户端丢弃,模型仍能基于文本完成推理。这与多模态模型本身的输入组织方式有关,可参考 多模态图像理解 。
4.3 音频的额外约束
音频比图像更贵、更少见支持:
- 单次返回建议 < 30 秒,长音频切成片段并按需取
- mimeType 用 audio/wav 或 audio/mpeg,避免冷门格式
- 转写优先:能先转文本就转,把音频作为"原证据"附上
- 采样率 16kHz 单声道足够语音场景,别传 48kHz 立体声
音频推理与部署侧的约束可参考 多模态推理部署 中的显存与批处理章节。
5. 大二进制内容的工程处理
5.1 三类失败模式
| 失败 | 表现 | 根因 |
|---|---|---|
| 上下文爆炸 | 一次返回 50MB PDF 的 base64 | 未设体积上限 |
| 传输超时 | stdio/HTTP 传输长时间无响应 | 大块数据未分片、无进度 |
| 内存耗尽 | 服务器 OOM | 全量读入内存再编码 |
5.2 硬上限与分片
const MAX_INLINE = 512 * 1024; // 512KB 内联上限
const MAX_CHUNK = 256 * 1024; // 分片大小
async function readFileBlocks(p: string): Promise<ContentBlock[]> {
const stat = await fs.stat(p);
if (stat.size <= MAX_INLINE) {
return [{ type: "image", data: (await fs.readFile(p)).toString("base64"), mimeType: guess(p) }];
}
// 超限:只返回链接,由调用方按需分段读取
return [{
type: "resource_link",
uri: pathToFileURL(p).href,
mimeType: guess(p),
size: stat.size,
}];
}
原则:服务器不该把「大」当成自己的问题。超过内联上限就返回引用,把「要不要读、读哪一段」的决策交给调用方。分段读取工具应支持 offset/length 参数,并保证对同一文件多次调用结果一致。
5.3 长任务与进度
多模态工具往往耗时(渲染、转码、OCR),应在返回前先推送进度通知,避免客户端在渲染完成前就判定超时。进度上报与取消语义是长任务工具的通用课题,核心约束是:进度通知必须在请求的令牌有效期内发出,响应一旦返回,令牌立即失效。
6. 缓存、隐私与审计
6.1 缓存
图像/音频的生成通常昂贵(渲染、转码),适合按内容哈希缓存:
cache_key = sha256(输入参数 + 工具版本 + 渲染配置)
命中 → 复用已生成的 base64 或已落盘文件 URI
失效 → 输入变化、工具版本变化、TTL 到期
注意缓存的是产物文件而非 base64 字符串:字符串缓存会让内存随会话线性增长,落盘 + 返回 resource_link 更可控。token 侧的预算控制与结果缓存策略见 https://plumephp.com/mcp-cost-optimization-token/。
6.2 隐私
- 截图可能包含用户隐私(聊天记录、邮箱、令牌)→ 落盘前评估是否脱敏
- 返回 resource_link 时,URI 不应包含明文敏感参数
- 音频可能包含可识别身份的声音 → 明确告知用户"将上传音频"
- 缓存文件要有权限控制(0600)与过期清理
6.3 审计记录什么
记录:工具名、参数摘要(不含 base64)、产物哈希、体积、mimeType、耗时
不记录:base64 内容本身、完整文件路径(按需脱敏)
审计的意义是「能复现这次调用」,不是「把内容再存一份」。
7. 常见陷阱
| 陷阱 | 症状 | 解决 |
|---|---|---|
| 全屏截图当默认 | token 快速耗尽 | 默认只截相关区域,降采样 |
| 用 JPEG 当省钱手段 | 传输省了、token 没变 | 降低分辨率才省 token |
| 只返回图像无文本 | 无视觉模型完全不可用 | 双轨返回文本摘要 |
| 大文件内联 base64 | 上下文爆炸 / OOM | 超限改 resource_link |
| 音频传高采样率立体声 | 体积与成本翻倍 | 16kHz 单声道 + 转写优先 |
| 长任务无进度 | 客户端判超时 | 先推 progress 通知 |
| 缓存 base64 字符串 | 内存线性增长 | 缓存文件 + 返回链接 |
| 截图未脱敏 | 隐私泄露 | 落盘前评估与脱敏 |
8. 小结
多模态工具的设计可以概括为「能给引用就不给内容,能给文本就不给像素」:
| 层面 | 要点 |
|---|---|
| 块类型 | text / image / audio / resource / resource_link 按需混装 |
| 传输 | 需模型看图 → 内联;留档或复用 → 链接 |
| 成本 | 降采样降 token,压缩格式只降带宽 |
| 兼容 | 文本摘要 + 图像双轨,覆盖无视觉模型 |
| 工程 | 512KB 内联上限,超限返回链接;长任务先报进度 |
| 治理 | 缓存产物文件、脱敏、审计记哈希不记内容 |
把多模态能力接进 MCP 的收益很直接:工具不再需要「用文字描述世界」。但代价同样直接——每一张图都是一次上下文消费。把体积上限、分辨率预算、降级路径三件事定死,多模态工具才不会变成 Agent 的奢侈品。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。