构建 MCP 服务:为 Agent 而设计,不是为 API 而封装
第一次接触 MCP(Model Context Protocol)时,我的直觉是「把外部 API 包一层,让模型能调用」。这个理解没有错,但停在这层的话,做出来的东西会让 Agent 用得很别扭——工具一堆,却很难完成一个完整的任务。
MCP 服务真正的设计问题不是「封装什么」,而是「Agent 要完成哪些任务,每个任务需要什么形状的工具」。
工作流优先,而不是端点优先
API 文档里的端点往往是原子操作:查一条记录、创建一个对象、更新一个字段。直接把这些端点翻译成工具,Agent 要完成一件事,就得自己串起三四次调用,而且每次调用都要处理上一次返回的格式。
更好的设计方式是先问:这个服务最常见的五个场景是什么? 然后围绕场景设计工具,让每个工具代表一个完整的工作流步骤,而不是一个原始的 API 端点。
举个典型例子:如果你在对接日历类服务,与其分别暴露「检查时间段空闲」和「创建日程」两个工具,不如合并成一个「安排会议」工具,在内部完成可用性检查和创建两个步骤,只把最终结果返回给 Agent。Agent 的上下文预算是稀缺资源,减少一次往返,就是减少一次上下文消耗。
这并不意味着要把所有端点都合并成一个超级工具。原子工具有它的价值,用于精细控制和边缘场景。设计策略是:高频场景给组合工具,低频精细操作保留原子工具,两层并存。
把上下文当稀缺资源来经营
Agent 的上下文窗口有限,每次工具调用的返回都在消耗它。如果一个工具默认返回一个包含几十个字段的对象数组,Agent 在处理一个简单问题时就已经把上下文填满了一大半。
我在设计工具返回格式时养成了两个习惯:
第一,默认返回精简格式,需要详情时再展开。可以在工具参数里加一个 detail_level,默认 concise 只返回关键字段,verbose 才返回完整数据。大部分 Agent 调用都是探索性的,精简格式够用;只有确认要深入一条记录时才需要完整数据。
第二,优先返回人类可读的标识符,而不是技术性 ID。返回用户名而不是用户 UUID,返回状态描述而不是状态码,返回相对时间(「3 天前」)而不是时间戳。这样 Agent 在推理时不需要额外的翻译步骤,减少幻觉风险。
字符限制也是要主动设计的,而不是让模型自己处理截断。工具层负责截断,并在返回里说明「已截断,共 N 条,当前显示前 20 条」,让 Agent 知道它看到的不是全集。
错误信息是工具文档的一部分
工具的错误信息通常是最后才写的,结果经常是「Request failed with status 400」这种对 Agent 毫无帮助的信息。
好的错误信息应该做两件事:说清楚发生了什么,以及建议下一步怎么做。不是「参数无效」,而是「start_date 不能晚于 end_date,请检查日期范围,或者使用 list_available_slots 工具先查询可用时段」。
这种风格的错误信息会直接影响 Agent 的后续行为。模型在接收到带有明确建议的错误后,会尝试按建议调整参数或改用推荐的工具;接收到模糊错误时,则更容易陷入重试循环或错误推断。从这个角度看,错误信息是工具文档在运行时的延伸。
MCP 协议支持给工具标注 readOnlyHint、destructiveHint、idempotentHint 等元信息。这些注解不只是文档——支持这些注解的客户端会根据它们改变 Agent 的执行策略,比如对标注为 destructive 的工具请求额外确认。花五分钟把这些注解填准确,比事后调试 Agent 的意外行为省时间。
评估驱动开发:在真实调用中打磨工具
MCP 服务在本地构建完成后,最难判断的是「Agent 真的能用它完成任务吗」。工具能跑起来是一回事,Agent 能把工具组合起来解决一个现实问题是另一回事。
我用的验证方式是为每个服务写 10 个「真实问题」——不是「调用这个工具」,而是「用这个服务回答这个业务问题,答案是 X」。然后跑评估,观察 Agent 用了哪些工具、绕了多少弯、最终答对了没有。
答错的 case 往往能暴露两类问题:工具边界设计不对(Agent 不知道该用哪个工具),或者工具返回格式太噪(Agent 被无关字段干扰了推理)。这两类问题都是工具设计问题,不是 Agent 能力问题。评估是在告诉你工具哪里需要改,而不是在测试模型能力的边界。
一句话心法
MCP 服务的质量不由工具数量衡量,而由 Agent 能否用这些工具完整地走完一个真实任务来衡量。