跳转至

07. Tool Calling 与 MCP

七、Tool Calling 与 MCP

Tool Calling、Function Calling、MCP、Skills、Agent 常被一起提起,容易只剩一团名词。默认顺序是先把 Tool Calling 讲成一条受控执行链,再把 MCP 放回接入层,把 Skills 放回任务级工作流复用层。系统要先回答的问题是”模型到底负责什么,服务端必须把哪些权力收回来,哪些知识该做成单次工具契约,哪些又该沉淀成可复用能力”。这条边界立住后,工具治理、审批、人机协同、统一适配层和多 Agent 运行时才不会混在一起。

flowchart LR A["用户请求"] --> B["模型提出结构化调用建议"] B --> C["服务端校验与补参"] C --> D["工具执行 / 审批 / 拒绝"] D --> E["统一结果 envelope"] E --> F["模型总结与解释"] F --> G["最终回答或人工接管"]

总判断:模型负责建议,服务端保留执行权

模型会提出结构化调用建议,真正的执行权留在服务端。用户用自然语言说”帮我查一下昨天那张报销单为什么还没过审”,模型把它翻译成更接近系统能力的结构,例如建议调用 get_expense_claim_status,并尝试从上下文里抽出单号、时间范围和动作意图。到了真正执行这一步,服务端还要继续判断 schema 是否满足、当前用户有没有权限、该工具在当前场景是否允许使用、是否命中风控规则、是否需要审批、是否已达到重试预算。

这条边界不清时,系统很快会变成高风险演示代码。模型可能选错工具、缺参数、误用别人的主键,或在只需要只读查询时误触发写操作。后端把这类建议直接当成命令执行后,问题不再是”回答偏了一点”,而是”错误判断开始驱动真实动作”。Tool Calling 的核心是让模型参与决策,同时由服务端保留契约校验、权限判断、风险分级和副作用控制的主导权。

调用建议:schema、Function Calling 和字段归属

schema 是 Tool Calling 的正式契约。它至少要说明工具名、输入字段、字段类型、必填条件、枚举边界、输出结构和失败语义。没有 schema,模型生成的就是后端难以稳定承接的半结构化文本。稳定的 Tool Calling 既定义输入也定义输出,因为输出结构直接影响 trace、回放、前端展示和离线评测。只返回自由文本的工具,后续很难判断这一步查到了什么事实。返回结构化字段——状态、审批节点、失败原因和时间戳——系统后续才能稳定复用这份结果。

schema 里的 description 决定了工具是否被选中、参数是否填错、边界是否越过。好的 description 至少说明四类信息:这个工具干什么,什么场景该用,什么场景不该用,字段怎样从用户语义里抽取。例如”根据报销单号查询审批状态。仅用于只读查询。不要用它执行催办、撤回、修改状态。若用户未提供单号,但给出明确时间范围和报销场景,可先请求服务端补全候选单号。若用户同时提到’帮我催一下’,请不要调用此工具而应转入催办审批流程。”这种 description 把调用策略、负向约束和字段抽取边界一起写进契约。

工具 schema 可以理解为 agent-computer interface。模型和外部世界接触到的,不是后端代码,而是工具名、字段名、description、输入输出契约和失败语义。这层接口写模糊,模型就会在”什么时候该调用、怎么填参数、调用后期待什么结果”上同时发散。schema 和 description 足够清楚时,很多误调问题在进入运行时前就被压掉了。

直接比较坏 description 和好 description,是理解 schema engineering 最稳的方式。坏的写法通常像:”description”: “查询报销单”。系统不知道”查什么、何时查、何时不能查、缺参数怎么办”。成熟的写法同时写出用途、边界、前置条件和负向约束:

{
  "name": "get_expense_claim_status",
  "description": "只读查询报销单审批状态。仅用于状态查询,不得用于催办、撤回、修改审批流。若用户未给 claim_id,但给出明确时间范围和报销场景,可先请求服务端按 tenant_id 补候选单号。若用户表达的是执行动作,禁止调用本工具。"
}

这里重要的是模型拿到了一份可以做语义判别的执行契约。很多工具误调问题,不在模型”不会选”,而在 description 从未把”何时不该选”写出来。

schema engineering 本质上是运行时治理工作。参数名和类型只解决格式问题,description 才在解决语义边界问题。工具越多、字段越像、场景越接近,description 的质量差距越会直接反映到误调工具率和无效调用率上。

Function Calling 应放回这条契约链里理解。模型先根据 schema 生成结构化调用建议,服务端再补齐可信字段并决定是否执行。关键是字段归属。claim_idquery_texttime_range 这类可以从用户语义里抽取的字段由模型来填。tenant_iduser_id、权限范围、审批链、风控标签这类服务端可信字段由服务端注入。这条边界分清后,模型生成 JSON 只是建议的载体,而非系统执行权的载体。

面试里如果问”Function Calling 的 JSON 为什么不等于自动执行”,回答是:JSON 解决的是结构化表达,不是执行授权。真正的执行还要经过字段补全、schema 校验、权限判断和风险分级。补上这层之后,Function Calling 才是后端系统可接的接口。

这条执行链的最小实现顺序通常如下:

suggestion := llm.ProposeToolCall(ctx, input, toolSchemas)
call := validateSchema(suggestion)
call = injectTrustedFields(call, tenantID, userID, policy)
decision := authorizeAndClassify(call)
if decision.RequireApproval {
    return pendingApproval(decision)
}
result := executeTool(ctx, call)
envelope := normalizeResult(result)
return llm.Summarize(ctx, envelope)

这个伪代码把模型负责什么、服务端负责什么、审批插在哪里、结果何时被标准化,全部固定为可复述的控制线。把这条线讲清楚后,Function Calling 不再是”模型生成 JSON”的炫技点,而是可治理的后端执行链。

自由文本建议适合低风险问答、辅助分析和只面向人阅读的链路。结构化 Function Calling 适合把模型决策交给程序承接、执行路径保留一定灵活性的链路。受控工作流节点适合高风险、强审计、步骤相对固定的场景。默认路线从 schema 化的 Function Calling 起步,某类动作高度稳定且错误代价高时,就继续下沉成工作流节点。

执行控制:参数补全、权限判断、风险分级和审批

Tool Calling 进入执行层后,从”模型懂不懂”切换为”系统收不收得住”。基本控制包括参数补全、权限判断、超时设置、幂等、重试预算、重复调用限制和审计日志。成熟的系统按风险把工具分层。只读工具的重点是权限过滤、查询超时和结果结构稳定。低风险幂等写工具的重点是幂等键、状态回查和可重试语义。高风险非幂等写工具——退款、发信、关闭工单、执行 shell、操作浏览器——必须纳入更严格的审批、人机确认和完整审计。

工具失败后按类型处理。参数缺失、schema 不合法、找不到业务主键属于构造失败,适合回问用户或让模型修参数。429、短暂超时、网络抖动适合在预算内重试。权限拒绝、审批缺失、业务状态不允许应该直接收口、降级或转人工接管,不自动重放。模型提示词的职责是帮助读懂错误语义并选择下一步动作,真正决定是否重试、重试几次、是否触发人工确认的是服务端策略。

审批在 Tool Calling 里很重要,因为一旦工具具有真实副作用,”建议”和”执行”必须进一步拆开。助手可以建议”要不要催办””要不要发邮件”,但执行前是否需人确认、确认后恢复到哪一步、审批痕迹如何记录,都是服务端和运行时的责任。成熟的 Tool Calling 系统不会在高风险节点假装”让模型谨慎一点”,而是把审批点明确设计进主链路。

可回放的执行链需要固定的错误分类。稳的分类至少区分构造失败、权限失败、业务状态失败、暂时性基础设施失败和不可恢复失败。构造失败修参数或回问。权限和业务状态失败直接解释或转人工。暂时性基础设施失败在预算内重试。不可恢复失败尽快停止并保留现场。错误分类成为稳定对象后,trace、回放、离线评测和失败样本回流才有抓手。

结果回灌:统一 envelope、trace、回放和多工具适配层

很多系统只重视工具输入,忽略工具结果怎样回到运行时。多工具、多 server 场景最容易在此处混乱。不同工具有各自的字段命名、错误码、分页方式和输出粒度。把原始结果直接暴露给模型后,模型每次都要重新学习一种返回方言。服务端做统一适配层,把原始结果先收敛成统一 envelope,再回送给模型或上层运行时。

envelope 至少包含工具名、版本、执行状态、标准化 payload、错误分类、原始结果引用和必要的 trace 标识。好处有三:模型每次看到稳定的结果形状;trace 和回放有了统一对象,更容易定位是模型选错工具还是工具本身失败;评测和审计可以跨工具横向比较。多个 MCP server 同时接入时这一点尤为重要。MCP 统一的是发现与调用,不会自动统一业务语义。把结果收敛成工程对象的是自己的服务端适配层。

选型有明显的层级差异。直接暴露原始工具适合工具少、调用方少的内部原型。统一适配层解决输入输出异构和错误语义不一致。统一结果 envelope 进一步把 trace、回放、评测和审计串到同一个对象上。工具、团队、协议变多时,直接暴露原始输出的成本迅速上升。默认路线:工具数量少时先做轻适配,跨团队和跨协议接入变多时,尽早收敛成统一 envelope。

MCP 与 Skills:接入标准化和任务复用不是一回事

MCP 适合被理解成一种接入协议,解决工具、资源和提示模板怎样被标准化暴露、发现和协商。MCP 统一的是接入方式和能力发现,不是权限、安全和风控。工具通过 MCP 暴露后,身份映射、租户边界、参数补全、危险动作确认、审计日志和版本控制仍由服务端负责。这条边界讲清楚,就能回答”为什么 MCP 不能替代权限和治理”这类高频题。

多个工具或多个 MCP server 一起接入时,服务端几乎总需要统一适配层和统一结果 envelope。协议层能让客户端发现能力,但不会自动变成同一套业务语义。企业系统需要统一的身份映射、权限校验、输出标准和审计链,而不是仅仅”都能被发现”。MCP 最合适的位置是接入层和能力发现层,真正的业务治理属于运行时和服务端策略层。

Skills 解决另一类问题。它不是协议,也不是”更高级的工具定义”,而是把说明、示例、脚本、参考资料和质量门槛打包成任务级可复用能力。MCP 回答”怎么让 Agent 发现并连接外部能力”,Skills 回答”怎样把反复出现的工作方法和上下文沉淀成可按需加载的能力包”。前者靠近接入面,后者靠近任务复用和上下文管理。Skills 适合承载工作流经验、领域知识、脚本入口和最佳实践,不代替权限和执行控制本身。

Tool、MCP、Skills、应用内原生工具接口和普通 HTTP 或 SDK 适配放在同一层比较,概念不会混。应用内原生工具接口适合单应用内部边界清楚的能力。普通 HTTP 或 SDK 适配适合完全掌握接入逻辑的场景。MCP 解决跨工具和跨 server 的标准化暴露、发现和协商。Skills 把说明、脚本、资源和工作流知识打包成任务级复用能力。上哪一种取决于解决的是”一个应用怎么接一个工具”,还是”很多 Agent 和很多工具怎么形成统一接入面”,以及”某类工作方法值不值得被沉淀成可重复触发的能力包”。先分清接入问题和工作流复用问题,MCP 与 Skills 才不会被误当成同一类东西。

MCP 从连接到调用回传的时序,按 client/runtime/server 三层讲

MCP 相关问题常被答成”客户端连上 server,然后就能调工具了”。时序应该拆成三层:宿主应用 runtime、MCP client、MCP server。宿主应用先根据当前用户、租户和可用策略决定能否连接某个 server,再由 MCP client 通过 stdio、streamable HTTP 或其他 transport 建立连接。初始化阶段,client 和 server 交换协议版本、implementation 信息与 capability,能力发现完成后,runtime 把”当前有哪些 tool/resource/prompt 可用”暴露给上层 Tool Router。真正调用时,宿主 runtime 先把模型建议映射成受控调用,再把结构化参数发给 MCP client,由它转成协议请求。server 执行完后返回结构化结果或错误,client 把结果收进统一 envelope,runtime 再决定继续让模型总结、还是直接收口或转人工。

这条时序压成最小步骤:connect -> initialize -> capability discovery -> route -> call -> normalize -> returnconnectinitialize 解决连接和会话建立,capability discovery 解决”当前 server 到底暴露了什么”,route 解决”这次任务应不应该调用它”,call 是协议请求本身,normalize 解决把协议返回重新收成业务统一对象。这六步讲清楚后,MCP 不会被和普通 HTTP SDK 调用、MCP 和治理平台混成一层。

排查与取舍:Tool Calling 出问题时,先查哪一层

Tool Calling 出错时,排查顺序是:看模型是否选对工具,看参数是否满足 schema,看服务端是否因权限或风控拒绝执行,看工具本身是否成功,最后看模型是否在拿到结果后解释错了。这个顺序能快速判断问题属于决策错误、契约错误、执行错误还是表达错误。很多看似”模型理解错了”的问题,真正坏的其实是服务端没有补齐可信字段,或者没把权限关住。

候选工具一多,工具路由变成独立选型。规则路由最稳,适合高风险路径、明确关键词和强权限边界。检索式工具发现适合工具数量大、工具描述和能力标签相对完整的场景。LLM Router 适合边界模糊、需结合上下文语义做判断的场景,但更难解释和回归测试。分层路由是最稳的工业默认方案:先用规则和权限做硬过滤,再用检索缩小候选集,最后只让模型在很小的候选集里做语义判断。高风险和明显边界收进规则层,真正模糊的部分交给模型。

工具很多时,建的是一套用元数据索引。工具名、功能描述、权限标签、输入输出 schema、风险等级、依赖系统、可用租户、最近失败率都值得成为检索式预选择的候选字段。默认工业链路:按用户身份和租户范围做硬过滤,按场景和风险等级做规则裁剪,用检索把几十上百个工具缩成 5 到 10 个候选,最后才让模型在小候选集里做语义选择。这不仅解决上下文窗口,更在降低工具间相互干扰和 description 污染。

取舍也很明确。单服务内部、工具有限、调用方单一时,普通内部函数、HTTP 或 gRPC 够用。只读查询和结构化抽取比高风险写工具更早上线。工具越多、接入方越复杂,统一适配层和统一 envelope 越值得优先做。同一种工作方法反复出现、总需要同一组说明、脚本和上下文资源时,才沉淀成 Skills。评估 Tool Calling 是否成熟,看工具成功率、审批命中率、重复调用率、平均步数、人工接管率和可回放性。这些指标说不清时,系统多半还停留在能演示、但未真正治理住的阶段。