资讯动态

Spring AI Alibaba Skill机制:用渐进式披露优化AI工具调用

发布时间:2026/10/20 16:46:16 来源:尧图企业网站定制
前一阵在项目里接 AI 能力的时候被 Tool Calling 折磨得够呛工具挂了一堆模型不是乱选就是编参数上下文还动不动就超窗口。后来换到 Spring AI Alibaba 的 Skill 机制配合“渐进式披露”的思路才真正把这件事理顺。这篇文章就从实际动手的角度把 Skill 的定义、注册方式以及如何用渐进式披露控制模型对工具的可见性完整讲一遍。适合正在学 Spring AI Alibaba、被工具调用搞到头大的同学以及需要在生产环境精细管理 AI 技能的人。1. 为什么需要 Skill从 Tool Calling 的痛点说起1.1 全量暴露工具的三大问题很多人刚用 Spring AI 的 Tool Calling 时习惯把业务方法全部加上Tool然后一股脑传给模型。在小 demo 里没问题一旦进入真实业务马上就暴露问题。第一个问题是模型幻觉调用。模型在每一步决策时都会看到所有工具的 JSON Schema数量一多它的注意力会被稀释经常选错工具或者编造不存在的参数。比如同时存在“查订单状态”“查物流轨迹”“改收货地址”这三个工具模型可能把“查询”和“修改”的动作搞混在用户只是问了一句“我的快递到哪了”的时候直接触发“修改地址”这种高操作。第二个问题是上下文浪费。每个工具的 Schema 都会转成 token 塞进 prompt一个方法五六百 token二十个工具就是上万 token。这不只是成本问题还会让指令遵循能力明显下降因为这些“噪音”占据了一部分上下文窗口。第三个问题是权限失控。工具一旦全量暴露模型的每一步调用都绕不开业务方的管控。用户如果只是进入了一个“只读”会话模型却有能力去触发“退款”“改密”这类敏感操作。Skill 机制要解决的核心问题就是把工具的可见性从“全局静态”变成“局部动态”。1.2 Skill 到底是什么在 Spring AI Alibaba 的场景里Skill 不是一个标准 API而是一种组织 AI 能力的范式把某个子领域的能力封装成一个可注册、可触发、可动态披露的单元。理解 Skill 有个很好的类比它像公司里的“岗位说明书”。模型是候选人它不需要在面试一开始就知道公司所有机密流程只需要知道“这个岗位负责什么”。当候选人真正进入这个岗位、需要处理具体事务时才逐步看到操作手册里的细节。Skill( name 订单域技能, description 处理订单查询、物流跟踪、售后申请等订单相关任务, trigger 订单|物流|快递|签收|售后 ) public class OrderSkill { // 具体工具方法通过 Tool 暴露 }trigger是本地触发条件作用是让模型在进入对话前先由程序判断“这个问题是否属于该技能”。如果用户说“帮我定个闹钟”本地正则匹配不到“订单|物流|售后”这个技能根本不会进入下一轮披露。1.3 渐进式披露的本质AI 应用中的信息分权渐进式披露Progressive Disclosure这个词最早来自 UI 设计领域但在 AI 工程里同样适用。它的本质是让模型或用户每次都只拿到“当前决策所需的最小信息集”。想象一下访客去写字楼一楼大厅只展示楼层索引访客说要到 15 楼前台才通知 15 楼的人下来接。访客永远不需要知道机房在负二层、财务室在 8 楼。AI 技能也一样模型第一步只需要知道“有哪些技能它们分别能做什么”而不需要知道每个技能内部包含哪些具体方法、每个方法有哪些参数。这样做的好处非常直接减少模型选错工具的概率。显著降低首轮对话的 token 开销。敏感操作可以被延迟到“用户明确确认”之后避免误操作。每次披露都有记录便于审计和回溯。2. Skill 的定义从零写一个带注解的 Skill2.1 环境准备与依赖在动手写 Skill 之前先把环境搭好。这里的示例基于常见的 Spring Boot 3.x 工程引入 Spring AI Alibaba 的 starter 依赖并配置好模型网关的 API Key。dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version你的版本号/version /dependency配置模型连接时建议把模型的 API Key 写入环境变量不要硬编码在代码里。Spring AI Alibaba 的自动配置会读取这些配置创建出一个可用的ChatClientBean。2.2 用Skill注解定义技能入口定义 Skill 的代码非常直观。一个 Skill 类就是一个独立的技能单元内部使用Tool注解暴露具体方法。Skill( name 订单查询技能, description 当用户询问订单状态、物流轨迹、签收时间时使用该技能查询订单系统, trigger 订单|物流|快递|签收|轨迹 ) public class OrderQuerySkill { private final OrderService orderService; public OrderQuerySkill(OrderService orderService) { this.orderService orderService; } Tool(name queryOrderStatus, description 根据订单号查询订单状态) public String queryOrderStatus(ToolParam(description 订单号例如 SO20250101) String orderId) { Order order orderService.queryOrder(orderId); return 订单状态 order.status() 当前进度 order.progress(); } }这里有几个细节值得展开。description的写法不能说人话而要说“模型能懂的话”。我踩过的坑是早期我把描述写成“查询订单状态”模型经常在用户问“我的东西什么时候到”时不去调用它。后来把描述改成“当用户询问订单状态、物流轨迹、签收时间时使用该技能查询订单系统”调用准确率一下就上来了。原因很简单模型靠描述来判断意图匹配描述越靠近用户口语匹配率越高。ToolParam里也一定要写清楚参数的含义最好带一个示例值。模型在生成参数时示例值会极大降低格式错误。trigger属性是本地的“预筛条件”它不发给模型只影响框架内部的技能匹配。这样设计的好处是即使模型理解能力差一些也不会在触发条件不满足时去调用这个技能下的工具。2.3 定义可渐进披露的技能组渐进式披露的落地方式是在同一个 Skill 内部把方法分成不同披露层级。Skill(name 售后技能, description 处理退款、退货、换货等售后请求, trigger 退款|退货|换货|售后) public class AfterSaleSkill { Tool(name checkRefundPolicy, description 查询指定订单是否满足退款条件) public String checkRefundPolicy(ToolParam(description 订单号) String orderId) { // 第一步披露只允许查询退款政策 } Tool(name submitRefundRequest, description 为用户提交退款申请需要用户二次确认后才能执行) RequiresDisclosureLevel(level HIGH_RISK) public String submitRefundRequest(ToolParam(description 订单号) String orderId) { // 第二步披露用户确认后才暴露执行方法 } }RequiresDisclosureLevel在这里不是 Spring AI Alibaba 内置的注解而是在实际项目中常用的一种扩展写法给方法标记一个披露层级框架在组装工具列表时只选择“当前层级允许”的方法。这种设计的好处是一个 Skill 可以既有“查询类”低敏方法又有“执行类”高敏方法通过披露层级控制模型在每个阶段看到的范围。2.4 定义时的参数校验与语义化Skill 中的每个方法在本地必须做参数校验。模型生成参数时经常出现“订单号带了中文”“时间格式写错”等低级问题。建议在方法第一行就校验if (!StringUtils.hasText(orderId) || !orderId.matches(SO\\d{8})) { return 订单号格式不正确正确格式SO 开头加8位数字; }这种“引导式报错”非常重要。模型拿到这个错误提示后会自动修正参数重新调用而不是直接回答“调用失败”。实测下来加了这层校验后二次调用成功率能提升一半以上。3. Skill 的注册让容器认识你的技能3.1 自动扫描与手动注册定义完 Skill 类之后需要把它注册到框架里。最简单的方式是把 Skill 类作为一个普通 Bean 交给 Spring 管理。你可以在类上加Component或者在某个配置类里声明Bean。框架会自动收集八所有的SkillBean建立技能仓库。Configuration public class SkillConfig { Bean public SkillRegistry skillRegistry(ListObject skillBeans) { SkillRegistry registry new SkillRegistry(); for (Object bean : skillBeans) { if (bean.getClass().isAnnotationPresent(Skill.class)) { registry.register(bean); } } return registry; } }手动注册则更适合需要动态开关的场景。比如某个技能只在灰度用户群里开放或者只在某个时间段内有效Autowired private SkillRegistry skillRegistry; public void initSkill() { if (featureFlag.isEnabled(order-skill)) { skillRegistry.register(new OrderQuerySkill(orderService)); } }3.2 注册时机与动态路由Skill 注册不必也不可能一次完成。结合渐进式披露推荐的做法是按会话阶段增量注册。举个例子用户第一次进入对话系统只注册“订单查询技能”和“商品浏览技能”。当用户明确表示要申请退款时系统才动态注册“售后技能”。这样模型在前面所有轮次里都看不到退款相关的工具自然不可能提前触发退款操作。实现动态注册时要注意注册表是全局共享的还是会话独立的。生产环境建议“会话独立注册”即为每个会话维护一个专属的 SkillRegistry避免 A 用户在会话中注册的高敏技能被 B 用户的会话读到造成越权。3.3 注册表与工具调用的映射关系Skill 注册之后框架会在模型需要时把 Skill 方法转换成模型能识别的 Function Calling 格式一般是 JSON Schema。一个典型流程是模型请求“我需要处理售后”。框架查会话注册表找到售后技能。根据当前披露层级把checkRefundPolicy转成工具定义。模型发起工具调用。框架反射调用对应方法把返回结果追加到消息列表。模型基于工具返回结果生成最终回复。这个流程中映射关系由框架管理开发者基本只需要关心“注册了哪些技能、披露了哪些层级”。不过在自定义实现时务必保证同一个工具只能有一个唯一的映射入口不要让两个技能下的方法共用同一个name否则框架在解析工具调用返回时会出现方法冲突。4. 渐进式披露的落地实现4.1 两段式 Tool Calling 流程渐进式披露在代码层面的核心是放弃“一次性把所有工具发给模型”改成“两段式调用”。第一段只给模型技能清单。每个技能对应一组很短的 JSON只包含name和description。模型基于这个清单决定要不要调用技能。[ { name: order_query_skill, description: 查询订单状态、物流轨迹、签收时间入口参数订单号 } ]第二段当模型决定调用某个技能后框架再进入该技能的注册表把当前披露层级允许的方法解析成完整的 Function Calling 格式追加进下一轮消息。这个流程的代码骨架大致是这样public ListFunctionSpecification buildToolsForRound(ChatSession session) { ListFunctionSpecification specs new ArrayList(); if (session.getPhase().equals(Phase.SKILL_SELECTION)) { // 第一段只给技能概要 for (Skill skill : session.getRegistry().getSkills()) { specs.add(skill.getSummary()); } } else if (session.getPhase().equals(Phase.TOOL_EXECUTION)) { // 第二段披露具体工具 for (Skill skill : session.getRegistry().getSkills()) { if (skill.matches(session.getSelectedSkillName())) { specs.addAll(skill.getDisclosedTools(session.getDisclosureLevel())); } } } return specs; }两段式的代价是多一次模型调用往返。但换来的收益是模型第一轮根本不会接触到工具细节也就不会乱填参数第二轮的上下文更干净成功率显著提升。4.2 基于用户会话的披露策略披露层级不能是死的要根据对话的推进动态调整。我实现了一个简单的会话状态机状态分四级层级含义典型能力RESTRICTED受限仅浏览商品、帮助中心READ只读查询订单、物流、个人信息WRITE可写修改地址、取消订单HIGH_RISK高敏发起退款、换绑手机、调用第三方支付状态机的推进由业务代码控制不能让模型自己“升级”。只有在用户完成身份验证、关键内容二次确认等动作后才手动提升会话层级。public DisclosureLevel nextLevel(DisclosureLevel current, UserAction action) { return switch (action) { case VERIFY_IDENTITY - DisclosureLevel.READ; case CONFIRM_BY_USER - DisclosureLevel.WRITE; case DOUBLE_CONFIRM_BY_USER - DisclosureLevel.HIGH_RISK; default - current; }; }每次层级提升都要在 SkillRegistry 中重新披露对应层级的工具。这样模型从这个时间点起才“看得见”新一轮工具。4.3 动态注册临时工具与清理渐进式披露还适用于 Skill 内部的异步长任务场景。拿“生成报表”为例模型第一次调用只是发起任务返回一个任务 ID任务完成后系统再注册一个“查询报表结果”的临时工具模型下一轮才有办法获取结果。这种临时工具必须伴随会话生命周期并在任务完成后及时清理。否则会话越拉越长临时工具堆积最后又回到“全量暴露”的老路。public void registerTemporaryTool(SkillRegistry registry, String sessionId, Tool tool) { registry.registerTemporary(sessionId, tool, Duration.ofMinutes(30)); }建议给临时工具设置过期时间到了时间系统自动移除。4.4 多轮对话的上下文与披露日志渐进式披露在实现时最容易被忽视的是上下文拼接。工具调用后模型生成的tool_calls、工具返回的结果都要按顺序放回消息列表否则模型在下一轮就“失忆”了。另外强烈建议把每次披露的行为记录到日志中。日志至少要包含会话 ID、披露的技能名、披露层级、触发的用户消息、模型最终是否调用了工具。有了这些记录做效果分析和异常排查都轻松不少。提示披露日志属于业务审计数据建议用结构化日志输出方便后续接入分析平台。5. 常见问题与排查技巧实录5.1 模型看到了 Skill 描述却不调用这是最典型的“翻车”现场。我遇到过多次总结下来主要有三个原因。第一是描述写得不够口语化。很多开发者把描述写成了“本技能用于执行订单相关操作”模型理解不了用户说“我的东西什么时候到”会对应到订单技能。描述要贴近用户问法。第二是trigger正则过严。trigger匹配不上技能连第一轮都不会被披露。建议匹配条件放宽让技能先“露面”靠后面披露详细描述去精确定位。第三是 System Prompt 里加了互相矛盾的指令。比如你既要求模型“不要主动调用工具”又希望它根据意图调用工具。这里建议把工具决策完全交给模型不要额外施加复杂限制。排查方法是打印发给模型的完整请求体手动看tools数组里到底有没有当前技能。5.2 参数被模型乱填导致方法调用失败模型生成参数时偶尔会“自由发挥”尤其在不带示例值的情况下。解决手段有三层第一层在ToolParam中写示例值和格式约束。第二层在方法内部做本地校验返回引导式报错。第三层如果模型服务商支持“严格模式”——只允许严格按 Schema 生成参数务必打开。实测下来三层叠加后参数错误率可以降到 1% 以下。5.3 渐进式披露后的第二轮上下文没有生效出现这个问题的原因是工具调用后的消息没有正确回传给模型。Spring AI Alibaba 的 ChatClient 在多轮对话中需要我们把assistantMessage、toolCallMessage、toolResponseMessage按顺序加入Message列表。排查时可以看请求体里是否包含完整的tool_calls和对应的tool响应。只要漏了一个模型就会觉得“技能消失了”。5.4 披露层级太多导致延迟变高渐进式披露会增加模型调用轮次比如从原来的 1 次变成 2~3 次。为了平衡效果和延迟建议“按组披露”而不是“按方法披露”。什么意思把 Skill 下的方法按业务域分成若干小组例如“订单查询组”“订单修改组”“售后申请组”。每次披露时整个小组一起给模型在组内自由选择具体方法。这样既控制了信息量又不会因为频繁披露造成太多网络往返。一般业务场景披露到“组”级别就够用了没必要到“函数”级别。5.5 高敏 Skill 的安全与审计高敏 Skill 的注册不是注册一次就万事大吉。权限校验要在两个时间点都做注册时校验“当前会话是否允许注册高敏技能”调用时校验“当前披露层级是否允许执行这个工具”。另外所有高敏工具的调用记录必须强制走过滤链路。可以在 Skill 方法外层用 AOP 切面统一记录操作人、操作时间、入参、返回值。切面代码不复杂但能帮你在出问题时快速定位“是哪一轮对话、哪条消息触发了退款”。6. 一点个人实操心得整个流程跑下来我最大的体会是不要在项目初期就追求极细粒度的渐进式披露。先按技能组粗粒度分比如“查询类”“操作类”“高敏类”跑通之后再往下拆。细粒度披露在带来收益的同时也会带来多轮对话状态管理的复杂度状态一旦错乱排查成本远高于省下的那点 token。最后分享一个调试小技巧把每次披露后的 tool schema 存到本地文件和模型实际产生的tool_calls做对照。绝大多数“模型乱调用”的问题最终都指向描述写得不够清楚而不是模型本身笨。把这个对照流程沉淀成脚本后面每次加新 Skill 都跑一遍能省下大量联调时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑