工具选对之后还不够:商城 Agent 的参数与权限治理

上一篇讨论了商城 Agent 的 Java 路由治理:模型先识别候选 Intent,Java 再通过置信度、能力白名单和资源契约,把请求路由到指定知识库、MCP Tool 或系统处理器。

路由完成以后,问题并没有结束。

假设系统已经判断用户要查询保修状态,并选中了 getWarrantyStatus。这个工具需要一个设备 SN。此时模型完全可能:

  • 没有从问题里找到 SN;

  • 把设备名称误当成 SN;

  • 根据上下文"猜"出一个不存在的 SN;

  • 把用户输入的其他设备 SN 原样传给后端;

  • 自己生成一个 userId,试图查询不属于当前登录用户的数据。

因此,在真实业务系统中,"选对工具"只解决了第一步。执行之前还要继续回答三个问题:

  1. 这个工具需要哪些参数?

  2. 当前参数是否完整、合法而且来源可信?

  3. 当前登录用户是否有权用这些参数执行该工具?

这就是参数与权限治理要解决的问题。

本文沿着上一章的终点继续,从已经确定的 mcpToolId 出发,一直讲到最终的 tools/call:

复制代码
确定MCP Tool
→ 读取Tool Schema
→ 模型抽取普通参数
→ 参数过滤与格式校验
→ 上游工具依赖
→ 实体解析与消歧
→ 必填参数门禁与缺参处理
→ 私有工具注入服务端身份和HMAC凭证
→ tools/call
→ MCP Server验签与数据归属校验

一、为什么不能相信模型生成的参数

大模型擅长把自然语言转换成结构化信息。例如用户说:

帮我看看上个月买的那块手表还在不在保。

模型可以比较好地提取:

复制代码
{
  "productType": "wearable",
  "purchaseTime": "last_month"
}

但模型并不知道业务数据库中真实存在的订单号和设备 SN。即使它输出了一个格式正确的 SN,也无法证明这个 SN:

  • 在数据库中真实存在;

  • 属于当前登录用户;

  • 对应用户所说的那块手表;

  • 仍然处于当前任务允许访问的范围内。

所以,参数治理的核心并不是"把模型输出转成 JSON",而是区分哪些内容可以让模型理解和抽取,哪些内容必须由可信业务数据绑定。

二、先给参数划分可信等级

项目中不同参数的风险并不相同。可以按来源和用途分成五类。

参数类型 示例 模型的角色 最终可信来源
普通语义参数 商品类别、时间范围、故障描述 可以抽取 用户表达 + 格式校验
格式受限标识 orderNo、serviceNo、手机号 可以抽取 用户表达 + 格式过滤 + 服务端业务校验
关键实体标识 sn、couponId、productId 模型值会被删除 实体解析或上游工具结果
身份参数 userId、principal 不接受模型填写 Sa-Token 登录上下文
上游派生参数 订单查询得到的 SN 不从自然语言猜测 上游工具结构化结果

这里最关键的一点是:模型抽取出来的值只是候选参数,不是天然可信参数。

"手表""上个月""黑色"这类自然语言条件可以由模型提取,再参与后续查询。订单号、维修单号等值经过格式过滤后,还要由 MCP Server 校验业务合法性和数据归属;SN、couponId、productId 等关键实体则会删除模型值,从可信候选或上游工具结果中重新绑定。用户身份始终只能来自服务端登录上下文。

三、完整的工具执行治理链路

路由层生成 AllowedRoute 后,如果资源类型是 MCP,RetrievalEngine 会根据其中固定的 mcpToolId 从 McpToolRegistry 精确取得工具。

后续链路可以拆成几层:

复制代码
AllowedRoute(MCP, getWarrantyStatus)
  ↓
McpToolRegistry取得工具描述和输入Schema
  ↓
模型只针对当前Schema抽取普通参数
  ↓
ExactBusinessParameterFilter
  → 去除指代词和格式不合法的精确业务参数
  ↓
DependencyExecutionCoordinator
  → 优先从上游工具结果绑定依赖参数
  ↓
EntityResolver
  → 删除模型给出的关键实体ID,从可信候选重新绑定
  ↓
MissingParameterGate
  → 按requiredAll和requiredAny检查参数是否齐全
  ↓
仍然缺失或存在多个候选
  → 保存Pending状态并向用户追问
  ↓
私有工具进入HuaweiMallRuntimeArguments
  → 删除模型身份字段,注入服务端身份
  → 生成时间戳和HMAC运行时凭证
  ↓
MCP Client调用tools/call
  ↓
MCP Server验签,将principal单独传入业务服务并检查数据归属

它不是一条"模型生成参数,程序照着调用"的直线,而是一条逐步提高参数可信度的流水线。

四、Tool Schema 只是输入规范,不是安全证明

MCP Tool 会提供名称、描述和输入 Schema。例如保修工具的 Schema 可能要求:

复制代码
{
  "type": "object",
  "properties": {
    "sn": {
      "type": "string",
      "description": "设备序列号"
    }
  },
  "required": ["sn"]
}

Java 已经通过路由选定 getWarrantyStatus 后,才会把这个特定工具的 Schema 提供给模型,让模型从用户表达中抽取普通业务参数。

这种方式比一次性把全部工具交给模型更受控,但 Schema 只能说明:

  • 需要哪些字段;

  • 字段是什么类型;

  • 哪些字段必填;

  • 字段在语义上代表什么。

它不能证明某个值真实存在,更不能证明当前用户有权访问。所以 Schema 校验是必要条件,却不是充分条件。

一个字符串满足 SN 的格式,并不意味着它就是可信设备 SN。

五、ExactBusinessParameterFilter:先做格式过滤

模型很容易根据语言模式生成"看起来合理"的订单号、SN 或优惠券编号。这类参数如果直接进入工具,就可能造成错误查询甚至越权风险。

ExactBusinessParameterFilter 的职责比"来源授权"更具体:它先去掉明显不能作为业务主键的指代词,再检查若干关键字符串是否符合项目约定的格式。

例如"这个""那个""我的""这张优惠券"等表达不能被当作真实 ID;订单号、商品 ID、维修单号和手机号则分别按照确定的格式校验。类似下面的模型输出:

复制代码
{
  "couponId": "这张优惠券",
  "orderNo": "123",
  "phone": "13800138000"
}

经过过滤后,只会保留格式有效的手机号。

需要注意,这一层主要做指代词与关键字符串格式过滤,不是完整的 JSON Schema 校验,也不负责最终证明参数来源可信。枚举范围、字段冲突和业务合法性仍需后续门禁以及 MCP Server 检查。

参数来源的真正治理发生在后续几层:

复制代码
普通参数
  → 经过格式过滤后可以继续参与执行
​
sn / couponId / productId等关键实体
  → EntityResolver会删除模型值
  → 再从可信候选中重新绑定
​
依赖参数
  → DependencyExecutionCoordinator会删除模型值
  → 再从上游工具结果绑定
​
身份字段
  → 直接丢弃模型值,由服务端重新注入

因此,格式过滤解决"像不像一个有效值",实体解析和依赖绑定解决"这个值从哪里来、能不能信任"。两者不能混为一谈。

六、缺参补问:先确定工具,再询问缺少的信息

缺参和意图不明确看起来都需要向用户提问,但两者不是同一个问题。

用户说:

帮我查一下。

系统连用户想查订单、物流还是保修都不知道,这属于意图澄清,发生在路由阶段。

用户说:

帮我查维修进度。

系统已经确定 Intent 是 REPAIR_PROGRESS,工具是 getRepairProgress,只是缺少维修单号。这才属于参数补问。

项目不会只读取 JSON Schema 中简单的 required。ToolParameterContractRegistry 会把 Schema 必填项转换为 requiredAll,同时补充业务上的 requiredAny 规则:

复制代码
requiredAll
  → 列出的字段必须全部存在
​
requiredAny
  → 一组候选字段中至少存在一个

例如:

复制代码
getRepairProgress
  → serviceNo或phone至少提供一个
​
getCouponEligibility
  → couponId必须有
  → orderNo或productIds至少提供一个
​
getSparePartPrice
  → part必须有
  → productId或model至少提供一个

实际主链会先尝试工具依赖和实体解析,再由 MissingParameterGate 根据 requiredAll + requiredAny 做最终检查。此时仍不完整,系统才会进入补参闭环:

复制代码
已确定Tool
→ 先尝试依赖绑定和实体解析
→ 检查requiredAll和requiredAny
→ 发现参数缺失
→ 保存当前任务上下文
→ 向用户提出针对性问题
→ 用户补充
→ 恢复原任务
→ 重新执行参数与权限门禁

例如:

请提供需要查询的维修单号。

这里最好不要泛泛地问"请补充更多信息",因为系统已经知道缺的是哪个字段,问题应该尽可能具体。

七、Pending State:让下一轮回复回到原任务

补问以后,用户下一轮可能只回复:

WX202609001

这句话单独看不出任何业务 Intent。如果重新走普通意图识别,它很可能被判成无法识别。

因此,系统会把待补充任务写入 Redis。Key 使用用户和会话共同隔离:

复制代码
ragent:pending:{userId}:{conversationId}

Pending 状态中保存当前工具、缺失字段、已有参数和必要的恢复上下文,并设置 30 分钟 TTL,避免陈旧任务长期占用状态。

用户下一轮输入到达时,Pipeline 会在正常路由之前尝试恢复 Pending。恢复时不会重新执行完整 Intent 路由,而是根据缺失字段构造一个缩小版 Tool Schema,只从新输入中抽取当前缺少的内容;合并参数时,也只允许覆盖此前标记为缺失的字段,不能借补参机会修改已经确定的参数。

参数重新通过门禁后,执行前使用 Redis getAndDelete() 原子取走任务;如果已经被其他请求处理或状态过期,就不会再次执行。这样同一份补充状态不会被并发消费两次。

不过,恢复任务不意味着直接执行。补进来的参数仍要重新经过格式过滤、完整性和权限检查,不能因为它来自"补问回答"就自动被信任。

八、实体消歧:缺的不是字符串,而是一个可信业务对象

有时工具缺少的参数并不适合让用户手动输入。

例如用户说:

查一下我那块手表的保修。

工具需要的是 SN,而用户表达的是"那块手表"。此时真正的问题不是缺少一个字符串,而是需要把自然语言中的设备指代,绑定到当前用户有权访问的真实设备记录。

系统采用的是"工具作用域候选集 + 确定性匹配 + 多轮确认"的显式消歧方式。

当前代码只为三个关键字段建立了这类实体解析契约:getSparePartPrice 对应 productId,getWarrantyStatus 对应 sn,getCouponEligibility 对应 couponId。执行时,即使模型已经输出这些字段,Java 也会先删除,再根据当前工具和登录用户查询可信候选。

1. 候选范围由当前工具决定

同一个"Mate 60",在不同工具中代表的业务实体不同:

  • 查询保修时,要在用户已购设备中寻找,最终绑定 sn;

  • 查询备件价格时,要在商品目录或设备信息中寻找,最终绑定 productId;

  • 查询优惠券时,只能在当前用户自己的优惠券中寻找,最终绑定 couponId。

因此,系统不会维护一个不分业务场景的全局候选池,而是根据已经选定的 Tool 确定实体类型和候选范围。

2. 候选来自当前用户可访问的数据

以查询保修为例,EntityResolver 会根据登录用户查询其名下设备,再利用商品类型、型号名称等条件筛选:

复制代码
没有候选
  → NOT_FOUND

只有一个候选
  → 直接绑定可信SN

存在多个候选
  → AMBIGUOUS / NEED_SELECTION

如果存在多台手表,系统会返回:

复制代码
找到多个符合条件的设备,请选择:
1. HUAWEI WATCH 4 Pro
2. HUAWEI WATCH GT 5

候选项不仅包含展示名称,还携带后端查询得到的受信绑定关系:

复制代码
{
  "displayLabel": "HUAWEI WATCH GT 5",
  "bindings": {
    "sn": "SN2026XXXX"
  }
}

用户选择的是候选项,而不是通过自然语言向工具注入任意 SN。

3. 用户选择后再次查询和校验

用户可以回复"第二个"、2、候选名称或候选 ID。系统解析选择后,不会直接信任 Redis 中缓存的旧业务对象,而是重新查询数据库,确认:

  • 实体仍然存在;

  • 实体仍然属于当前用户;

  • 实体仍满足当前工具的业务范围。

通过以后,才把数据库中的真实 SN 写入工具参数,并重新执行参数门禁。

当前实现属于规则驱动的显式消歧,主要覆盖商品、设备 SN 和优惠券等明确业务实体。它不是通用的语义实体链接系统,但在业务 Agent 中,可控、可解释和可做权限校验往往比"什么都能猜"更重要。

九、工具依赖:缺失参数也可能来自上游工具

并不是所有缺失参数都应该询问用户。有些参数可以由另一个受信工具提供。

例如:

我上个月买的手表还在保吗?

用户通常不知道设备 SN,但订单系统可能保存了购买记录和设备信息。这时可以按照预先定义的依赖契约执行:

复制代码
getOrder
  → 按当前用户和时间条件查购买记录
  → 返回订单、设备型号和SN
        ↓
getWarrantyStatus
  → SN从上游结构化结果绑定

这里的关键不是让模型自由规划任意工具链,而是 Java 根据已经确定的业务目标和依赖契约安排执行顺序。当前契约明确包含:从 getOrder 结果中的设备 SN 绑定到 getWarrantyStatus.sn,以及从订单商品信息中提取 productId 绑定到 getSparePartPrice.productId。

在真实执行顺序中,依赖协调早于实体解析和最终缺参门禁。对于契约声明必须来自上游的参数,即使模型已经抽取出 SN 或 productId,Java 也会主动删除,然后由上游工具结果重新覆盖绑定。

上游结果也可能出现三种情况:

复制代码
唯一结果
  → 自动绑定并继续

多个结果
  → 保存依赖选择Pending,询问用户

没有结果
  → 明确提示无法找到可用业务对象

SN 的来源在 Trace 中应记录为"上游工具结果",而不是"模型抽取"。参数来源一旦结构化记录,后续排查错误时才能知道:值到底是用户提供的、模型抽取的、实体解析得到的,还是由上游工具传递的。

十、身份参数必须从服务端注入

如果用户在问题中说:

帮我查询用户 U002 的订单。

系统不能因为模型提取出了 userId=U002,就用这个身份访问订单数据。

用户访问 Bootstrap 服务时,首先经过 Sa-Token 登录校验,服务端建立 UserContext。后续与身份有关的参数只认这个登录上下文,不接受模型或用户在自然语言中自行声明。

HuaweiMallRuntimeArguments 会执行两件事:

  1. 删除模型生成或用户注入的身份字段;

  2. 根据当前登录用户重新映射并注入可信业务主体。

当前需要这套身份上下文的私有工具包括 getOrder、getLogistics、getCouponEligibility、getWarrantyStatus 和 getRepairProgress。商品搜索、库存和备件价格等公开工具不需要伪装成用户私有查询。

因此,身份链路是:

复制代码
用户登录Token
→ Sa-Token校验
→ UserContext
→ 业务principal映射
→ 注入工具运行时参数

这种设计把"用户想查谁"和"系统确认他是谁"彻底分开。自然语言可以表达查询目标,但不能改变认证主体。

十一、MCP 调用前为什么还要增加 HMAC

商城 RAG Bootstrap 是 MCP Client,业务工具部署在独立的 MCP Server 中。即使 Bootstrap 已经完成用户登录校验,远端 MCP Server 仍然需要确认:这次请求确实来自受信调用方,身份上下文没有在传输过程中被随意伪造。

因此,调用私有工具前,Bootstrap 会附加:

  • 业务 principal;

  • 当前时间戳;

  • 基于共享密钥计算的 HMAC-SHA256 签名。

当前签名内容为 toolName + principal + issuedAt,MCP Server 会验证签名,并检查时间戳是否处于 60 秒有效窗口内。验证通过后,它会移除 _runtimePrincipal、_runtimeIssuedAt 和 _runtimeProof 等内部字段,把可信 principal 作为独立参数传给业务服务:

复制代码
登录用户
→ Bootstrap绑定principal
→ principal + timestamp + HMAC
→ MCP Server验签
→ 删除内部运行时字段
→ 将principal单独传给业务服务
→ 检查订单、设备等数据的user_id归属
→ 返回当前用户有权访问的数据

MCP Server 还会拒绝业务参数中出现 userId、principalId 或 principal_id 等受保护字段。查询订单或设备后,业务服务继续比较数据中的 user_id 与可信 principal,防止调用者通过业务参数绕过身份上下文。

十二、鉴权、授权和参数校验不是一回事

这三个概念在工具调用中经常被混在一起。

**鉴权(Authentication)**回答"调用方是谁"。例如 Sa-Token 确认登录用户,HMAC 确认 MCP 请求来自受信客户端。

**授权(Authorization)**回答"这个主体能访问什么"。例如订单查询必须限制在当前 principal 名下。

**参数校验(Validation)**回答"输入是否满足工具要求"。例如 SN 格式是否正确、维修单号是否缺失。

一个参数通过格式校验,不代表用户有权访问;请求带有合法登录 Token,也不代表可以查询任意订单。三者必须分层处理。

十三、从一句话到工具执行:完整示例

以用户问题为例:

我上个月买的手表还在保吗?

第一步:路由确定业务目标

复制代码
IntentResolver
  → WARRANTY_STATUS

ResourceContractGuard
  → MCP / getWarrantyStatus

第二步:根据 Schema 检查参数

getWarrantyStatus 需要 SN,但用户没有提供。模型只能提取"手表"和"上个月"等筛选条件,不能生成可信 SN。

第三步:尝试通过依赖工具补齐

复制代码
DependencyExecutionCoordinator
  → 调用getOrder
  → 使用服务端身份查询上个月的购买记录

第四步:处理候选结果

如果只找到一块手表,系统直接绑定其 SN;如果找到多块,则保存待选择状态并让用户确认;如果没有结果,则停止后续调用。

第五步:重新校验并绑定身份

选定设备后,系统重新确认设备归属,把可信 SN 写入参数,再删除所有模型身份字段,注入当前登录用户对应的 principal。

第六步:调用 MCP Server

Bootstrap 生成时间戳和 HMAC 签名,通过 MCP Client 发起 tools/call。Server 验签、移除内部字段,并检查设备归属后返回结构化结果。

第七步:生成回答并保存 Trace

模型根据真实工具结果生成自然语言回答,Trace 同时记录:

  • 最终工具;

  • 参数值和参数来源;

  • 是否发生依赖调用;

  • 是否经过用户选择;

  • 工具执行结果和最终任务状态。

至此,"自然语言中的那块手表"才被安全地转换成一次具体的保修查询。

十四、为什么不能只靠 Prompt 约束

我们当然可以在 Prompt 中告诉模型:

不要编造订单号,不要查询其他用户的数据,参数不足时请先提问。

这些指令有帮助,但不能成为安全边界。Prompt 属于模型行为引导,而不是强制控制。模型仍可能因为上下文干扰、工具描述歧义或输出不稳定而违反要求。

真正的约束需要落在模型外部:

复制代码
工具白名单
  → 模型只能接触当前授权工具

参数门禁
  → 缺少必填参数时不能执行

实体解析
  → 精确业务ID来自可信数据

身份覆盖
  → 删除模型身份,服务端重新注入

MCP验签
  → 远端确认请求来自受信客户端

数据归属校验
  → 业务服务检查订单、设备等数据是否属于当前principal

Prompt 可以提醒模型怎么做,Java 和数据库则决定它实际上能做什么。

十五、Trace 应该记录参数是从哪里来的

如果 Trace 只记录"调用了 getWarrantyStatus,参数是某个 SN",仍然无法判断这个 SN 为什么可信。

因此,参数记录最好同时携带来源:

复制代码
USER_QUERY
  → 模型从原始问题中抽取,经Java过滤

CLARIFICATION_INPUT
  → 用户第二轮补充,只允许填充缺失字段

AUTH_CONTEXT
  → 服务端登录身份

ENTITY_RESOLUTION
  → 从业务候选集中解析

TOOL_OUTPUT
  → 上游工具结构化结果

DEFAULT
  → 工具Schema提供的默认值

当工具调用出错时,可以继续追问:

  • 工具是不是选错了?

  • 参数是不是缺失却被放行了?

  • 实体是不是绑定错了?

  • SN 是模型编的,还是订单工具返回的?

  • 身份是不是来自服务端上下文?

这种可追溯性,也是后续 Golden Case 能够验证执行链,而不只是比较最终答案的基础。

十六、当前实现需要诚实说明的边界

1. 实体消歧仍然是业务规则驱动

当前主要覆盖商品、设备 SN 和优惠券等明确实体,依赖型号、类别、序号和候选名称等规则,不是通用语义实体链接系统。别名和复杂口语表达的覆盖仍需要持续补充。

2. HMAC 不能替代完整的传输层安全

当前 HMAC 主要保护私有工具的身份上下文,并通过时间窗口降低伪造风险,但它不能完全替代 HTTPS、mTLS、OAuth2、密钥轮换和更严格的防重放机制。

生产部署中,MCP 服务仍应位于内网或受控网关后,签名内容也应覆盖规范化参数并增加 nonce。

3. 同一会话并发还需要进一步治理

不同 conversationId 通过用户和会话维度隔离,不会共用 Pending 状态。但同一用户对同一会话同时提交两个请求时,仍可能并发读取旧状态或竞争同一个 Pending。

生产化时可以使用 userId + conversationId 作为分布式锁或队列分区键,并为 Pending 增加 taskId/version 做 CAS,保证同一会话中的任务按顺序推进。

4. 当前工具以查询类能力为主

查询类工具即使发生误调用,风险通常低于修改订单、退款或支付等写操作。如果未来开放有副作用的工具,还需要增加二次确认、审批、幂等键、操作审计和补偿机制,不能直接沿用只读查询的安全等级。

十七、总结

工具调用真正困难的地方,不是把 JSON 发给 MCP Server,而是让 JSON 中的每一个关键字段都有可信来源。

这套参数与权限治理可以概括为:

复制代码
Tool Schema
  → 告诉系统需要什么

模型抽取
  → 从自然语言中提取普通条件

格式过滤
  → 去掉指代词和格式不合法的候选值

依赖工具与实体解析
  → 从可信业务数据补齐精确标识

必填参数门禁
  → 根据requiredAll和requiredAny判断能否执行

Pending State
  → 在多轮对话中恢复未完成任务

服务端身份绑定
  → 决定调用主体是谁

HMAC与数据归属校验
  → 保护跨服务调用和最终数据访问

最终仍然可以回到商城 Agent 的设计原则:

模型负责理解自然语言和抽取普通参数;Java 负责判断参数能不能使用、精确实体绑定到谁、调用以什么身份发生;MCP 负责把已经通过治理的请求送到业务工具。

路由治理解决"应该调用哪个能力",参数与权限治理解决"这次调用是否具备安全执行条件"。两层合起来,才构成一个真正可控的业务 Agent。

相关推荐
cmpxr_42 分钟前
【MicroPython】如何刷新ESP8266的固件
开发语言·micropython
天赐范式1 小时前
天赐范式第186天:让方差开始自洽——Bulmer效应的闭式验证
python·数字生命·天赐范式·动态运行时·遗传方差·bulmer效应
泡茶喝茶写代码1 小时前
A股量化数据工程:从 REST 接口到策略信号(第 8 篇):成长能力因子:营收与利润增速
java·python·股票数据api·股票数据api接口·股票量化数据api·股票量化数据接口·股票数据api数据
搭贝1 小时前
上厕所的时间搭好一套系统:AI自动生成实测
开发语言·人工智能·低代码·ai·php·搭贝
智感子1 小时前
智能称重·数据上云:从仪表读数到可追溯的计量凭证
开发语言·php
\光辉岁月/2 小时前
7.javase-面向对象
java·开发语言
Είναι η κοπέλα2 小时前
显存计算与模型选择:你的显卡能跑多大的模型
人工智能·pytorch·python·开源·conda
weixin_531094692 小时前
heap_4内存管理
java·开发语言·算法
ROSF68682 小时前
2026 仿石漆乳液供应商:市场布局与行业发展态势梳理
大数据·人工智能·python