↓ 跳过正文

MCP 工具太多、结果太大怎么办:让代码处理中间数据

正能量6 分钟阅读

围绕构建记录统计,说明 MCP 与代码执行怎样分工,以及分页、完整性、超时、结果校验和沙箱边界如何影响可靠性。

MCP 客户端、服务器与模型交互时上下文累积的结构示意

让 AI 查看大量构建记录时,逐条读日志并不总是合适。查总数、按状态分组、处理分页,这些有明确规则的工作可以交给代码;模型再依据统计结果解释现象、选择需要深入查看的异常记录。这样设计的关键,是减少无用数据,同时保留判断所需的完整性信息。

工具负责取数,代码负责确定性处理
#

大量工具定义和反复传入模型的中间数据,都会增加开销。一种处理方式是让模型编写代码调用工具,在执行环境中完成分页、筛选和计算,再返回必要结果。MCP 负责连接工具,具体怎样编排调用,则由应用选择。

工具还可以包装成 TypeScript 接口,供执行环境调用。这样,循环和统计规则能够用普通代码表达,模型只需关注任务范围及结果解释;不过,应用也需要额外维护执行环境和错误处理。

从一个只读统计任务开始
#

设想要回答:“某测试项目昨天的构建中,有多少失败,是否集中在某个分支?”第一步应确定项目和时间范围,并统一时间口径。否则,“昨天”可能分别被客户端和服务端理解成不同日期。

先定义一个只读的构建查询函数,输入项目、起止时间和分页游标,返回记录与下一页游标。这个接口需要由业务服务提供:

type Build = {
  id: string;
  branch: string;
  status: "success" | "failed" | "cancelled" | "running";
};

type BuildPage = {
  records: Build[];
  nextCursor: string | null;
};

设计中把取消、运行中和失败分开。如果直接把“非成功”都算成失败,统计会带入错误含义。是否需要把这些状态合并展示,应由问题定义决定,不能图方便就混算。

还应明确时间过滤针对构建开始还是结束。一个昨天开始、今天结束的任务,在两种定义下归属不同。这个问题没有统一答案,但一份统计必须采用一致定义并把它写进输出。

中间处理应留下可检查的结果
#

代码可以负责翻页和计数,返回给模型的对象则应包含范围、完整性、各状态总量和少量失败样本。假设查询得到八条成功、两条失败和一条取消记录,可以返回这样的对象:

{
  "project": "demo-service",
  "timeBasis": "started_at",
  "complete": true,
  "counts": {"success": 8, "failed": 2, "cancelled": 1, "running": 0},
  "failedSampleIds": ["b-101", "b-107"]
}

模型可以据此说明有失败构建,但还不能认定原因,也不能回答是否集中于某分支,因为当前结构没有分支分组。缺什么就追加哪项统计,不应从两条样本推断整体分布。

完整性也不是装饰字段。如果只读取到前几页,complete 必须为 false,并附上中断原因。部分结果仍可用于查看样本,却不能伪装成整天的总数。

循环要有退出条件
#

以下是围绕假设接口编写的简化逻辑,用于解释控制流。listBuilds 必须由实际客户端适配提供,不能直接复制运行:

const counts = { success: 0, failed: 0, cancelled: 0, running: 0 };
const seen = new Set<string>();
let cursor: string | null = null;
let complete = false;

for (let page = 0; page < 20; page++) {
  const result: BuildPage = await listBuilds({ ...query, cursor });
  for (const build of result.records) {
    if (seen.has(build.id)) continue;
    seen.add(build.id);
    counts[build.status]++;
  }
  cursor = result.nextCursor;
  if (cursor === null) { complete = true; break; }
}

return { counts, complete, nextCursor: cursor };

20 页只是示例上限,实际限制应根据查询规模和执行预算设置。实现中还要处理单次请求超时、重复游标、未知状态和服务限流;遇到异常时保留已处理页数,明确返回部分结果。类型声明只是开发约定,网络返回仍需校验,不能假设它天然符合 BuildPage。

按构建标识去重能避免重复记录被重复计数,但不能弥补分页期间的数据变化。若要求严格核对某个时刻的结果,需要服务端提供快照、稳定查询条件或可追溯导出,而不是仅靠客户端循环。

还有一个容易忽略的区别:查询失败与查询成功但没有记录,不应返回同样的空数组。如果权限不足、网络超时都被包装成“没有构建”,模型会给出貌似合理的结论。适配层应把这两类结果分开,让调用方知道是数据为空还是证据未取得。

恢复任务时,也不要只保存下一页游标。应同时保存项目、时间范围、过滤条件、已处理记录标识或可靠的统计状态。如果更换过滤条件后继续使用旧游标,得到的结果可能前半段属于一个范围,后半段属于另一个范围。不能恢复一致查询时,重新开始一次有明确标识的统计更容易复核。

用小数据验证统计规则
#

接入真实服务前,可以在本地准备一组人工写出的分页结果:第一页两条成功,第二页一条失败,再重复第一页的一条记录。期望输出是三条独立构建,而不是四条。用这组数据检查去重与计数规则。

再准备一个第二页超时的案例,期望得到部分结果和未完成标记;准备未知状态的案例,期望报告协议数据不符合约定,而不是把它塞进成功计数。最后加入空记录页但仍有下一页的情况,确认程序依据游标判断结束,而不是把空页直接当作查询完成。

协议负责连接,业务仍要自己定义
#

MCP 2026-07-28 工具规范定义了工具的输入模式与结构化结果,并说明跨调用状态应使用显式标识传递。应用包装可以利用这些信息,但不会自动获得正确的统计语义。

因此,构建游标是否过期、查询属于哪个项目、调用者是否有权访问,仍应由服务端检查。客户端拿到了一个游标,也不意味着它可以绕过项目权限。接入 SDK 时,还应核对客户端、服务端与协议版本的兼容关系。

如果工具本身支持按状态和分支聚合,优先考虑直接调用聚合接口。把所有记录下载后再本地计算会增加数据传输与分页一致性问题。只有服务端能力不足、需要跨数据源计算或临时转换时,代码执行层才更有必要。

少给模型数据,不等于数据自动安全
#

执行环境仍然能接触原始记录,运行日志也可能保存它们。只向模型返回摘要,减少了进入对话的内容,却没有消除底层访问风险。

执行环境需要同时约束文件系统和网络访问。对于只读构建统计,可以只允许访问目标构建服务,不注入项目写权限,不挂载开发者主目录,并限制时间、内存与输出大小。

不要因为模型生成了一段“看起来只读”的代码,就把管理员凭据传进去。权限应围绕任务需要授予;如果以后要重跑失败构建,那是新的写入能力,必须重新设计调用范围和重复执行处理。

用相同问题比较两种实现
#

在决定引入代码执行前,可以用同一组虚构记录比较直接工具调用与代码汇总:结果是否一致、分页是否完整、失败是否正确暴露、模型是否能引用具体构建标识。再记录实际请求耗时、输入量与维护成本,而不是只比较回答长度。

对于几条记录的一次查询,直接调用通常已经足够清楚。对于多页记录的稳定计算,代码更便于检查规则。最终选择应由任务规模和可维护性决定:少一次模型调用如果换来一个难以运维的执行服务,未必划算。

参考来源
#