你有没有遇到过这种情况:精心开发并部署上线了一个 Skill,本地测试时一切正常,可真正放到 Agent 里跑起来,模型就好像完全“无视”它的存在,要么去调用别的相似工具,要么干脆告诉用户“我没有这个能力”。
这个问题在搞 Cloud Code 相关开发的朋友里太常见了。很多团队把大量精力花在功能实现上,最后发现卡住他们的不是业务代码,而是“模型压根不认知这个 Skill 存在、不理解它什么时候该被调用”。换句话说,Skill 不被调用,绝大多数时候不是“执行层”的问题,而是“认知层”的问题。
我们团队在几次实战中彻底踩了一遍这些坑,从最开始的抓瞎到后来摸索出一套稳定的设计方法,最后沉淀成 9 条最佳实践。这篇东西完全基于我们在 Cloud Code 体系下的真实经历,适合正在做 Agent 应用、工具插件开发、或者刚接触 Skill 设计的朋友对照参考。读完之后,你大概率能自己定位出“为什么我的 Skill 不被调用”的真凶。
1. 现象与根因:Skill 为什么“叫不动”
1.1 一次典型的 Skill 失灵现场
先还原一个我们实际遇到过的情况。当时团队做的是一个“天气助手”功能,我在平台上写了一个查询天气的 Skill,逻辑很简单:接收城市名和日期,返回天气现象、温度和风力。在测试环境里手动调用接口,响应完全正常。结果接入到 Agent 对话后,用户问“明天上海热不热”,模型给出的回复是“抱歉,我还无法获取天气信息”。
第一反应是代码出问题了。我排查了接口、权限、网络、部署状态,全部正常。接着又怀疑是平台配置错了,检查了一圈也没问题。最后才意识到,真正的故障点根本不在运行链路,而是在“模型有没有正确理解这个工具是干嘛的”这件事上。
为了讲清楚这件事,我想用一个类比。你可以把 Skill 想象成一栋大楼里的服务窗口,模型是前台接待员。窗口开得再好,如果接待员不知道这个窗口提供什么服务、不知道什么情况下应该把客人领过去,那这个窗口就等于不存在。你的代码只是窗口内部的处理流程,而模型能不能把你指过去,取决于窗口外面挂的“招牌”和“服务说明”写得好不好。
1.2 理解调用链路:大模型如何发现你的 Skill
要解决问题,先得搞明白模型是怎么“看见”一个 Skill 的。现在主流的 Agent 平台大多采用这样一个机制:把已经注册的 Skill 元信息(名称、描述、参数说明、示例等)拼装进 Prompt,让大模型基于这些信息在每一步推理时选择调用哪个工具。模型的选择过程本质上是一个“意图分类”任务——它读用户的话,推断用户的意图,再从候选工具列表里挑出最匹配的那一个。
这就解释了一个非常关键的现象:模型选工具时看到的不是你的源码,也不了解你的内部实现,它只看到你写的那几行描述性文字。描述准确,它就知道该用;描述模糊,它就猜,猜错了就不调用,或者调用了但传参不对。所以当你发现 Skill 不被调用时,第一步不是看代码,而是把它的“自我介绍”拿出来,重新读一遍,站在模型的角度问自己:如果有人对我说“明天上海热不热”,我会不会觉得这个工具适合回答这个问题。
1.3 问题其实就两层:没选中,和选了但没跑通
在后续复盘里,我们把所有“Skill 不被调用”的问题归纳成两大类。
第一类是“模型根本没选中它”。这类问题占大多数,通常出在名称不合理、描述没覆盖用户真实表达、参数定义让模型无法理解、候选 Skill 之间互相干扰。
第二类是“模型选了它但执行失败”。这一类容易被人忽略,因为表面上看起来是运行时的问题,但本质上还是设计问题。比如权限没配好导致调用报错,模型一旦收到错误反馈,轻则当场告诉用户“失败了”,重则后续对话里对这个 Skill 产生“不信任”,再也不尝试调用。
把这两类问题厘清之后,下面的实践就都有了明确指向:前五条解决“模型选不中”,后四条解决“选了但跑不好”。
2. 九条最佳实践逐一拆解
2.1 实践一:名称要像斜杠命令一样有记忆点
模型在众多工具里做选择时,名称是第一眼看到的信息,也是最强的启发信号。这一点上我踩过的坑是:一开始图省事,给 Skill 起名叫query,结果接入多个 Skill 后,模型完全分不清它是查天气还是查新闻。后来改名成query_weather_info_v2_2019,又撞上了另一个问题——名字太长,还带版本号,模型反而犹豫不决。
我的建议是三个原则。
第一,动词开头,让模型一眼看到“它能做什么”。比如get_weather_forecast、book_flight_ticket,一看就知道动作对象。第二,去掉版本号,平台层面管理版本就够了,别写进名字里。第三,避免和已有的其他 Skill 语义重叠,否则即使这个 Skill 写得再好,模型也会随机选一个。
名称这东西看似无关紧要,但它决定了模型对你的第一印象。我后来专门做过一次小实验:同样的描述和参数,只把名称从weather_001改成get_weather_by_city_and_date,在一批测试对话里的调用命中率直接提高了两三成。
2.2 实践二:描述信息要覆盖真实意图,别只写实现细节
如果说名称是招牌,描述就是服务说明。模型判定“该不该调用”的时候,依靠的主要就是这段描述。它最典型的问题有两个。一个是写得像“接口文档”,只讲技术功能,比如“获取天气数据接口”,完全没讲清楚应该在什么场景下使用。另一个是写得过于宽泛,比如“查询各种信息”,模型看了等于没看。
一段合格的描述应该包含四块内容:触发场景、必要参数、返回值概况、不适合的场景。
拿天气这个例子来说,反面写法是“获取天气数据”,正面写法可以是这样:当用户询问某个城市今天、明天或未来几天的天气、温度、风力、空气质量等情况时,调用本工具。输入城市名称和日期。返回字段包含天气现象、最高温度、最低温度、风力等级。如果用户只问季节建议或气候统计,请勿调用本工具。
这里的最后一条特别容易被忽略,但非常有价值。告诉模型“什么时候不要用”,能显著减少误调用。模型在意图不确定时会倾向于试错,边界描述得越清楚,它越容易做出正确的“不调用”决策。
2.3 实践三:参数定义要像数据库表一样严谨
参数 Schema 是模型构造调用请求的唯一依据。我见过太多 Skill 参数写得极其随意,全部类型都是 string,没有必填标记,也没有格式说明。结果就是模型不知道该传什么、怎么传。你让模型“填报日期”,它可能给你传“后天”,让“城市”参数传“SH”而不是“上海”——这些看似是小问题,却直接让下游解析崩溃。
所以定义参数时,要把模型当成一个完全不懂业务的新同事,给它足够的提示。每个字段都要有:类型、是否必填、取值范围或枚举、格式说明、以及给模型看的“翻译”。
比如日期参数不能只写date: string,应该写明“格式 YYYY-MM-DD,当用户说今天/明天/后天时,请转换成具体日期再传入”。城市参数如果有固定的支持列表,一定要写成枚举,而不是让模型自由发挥。
这么做的原因很好理解:模型是个“概率机器”,你给它的约束越少,它越容易放飞自我。参数定义就是你和它之间的契约,契约越严密,执行越可靠。
2.4 实践四:用 Few-Shot Examples 让模型照葫芦画瓢
描述能说清“什么时候用”,但很难说清“怎么把用户的一句话翻译成结构化请求”。这时候你要给模型示例。很多 Skill 平台都支持配置示例,也就是 few-shot examples。它的作用相当于给模型看了几个“标准答案”,模型会模仿示例的映射方式去处理新的输入。
举个例子。示例里写:用户说“明天上海热不热”,那么配置为:city 参数为“上海”,date 参数为“明天对应具体日期”,需要关注 temperature。这看起来好像有点多余,但在实际测试里,加了类似三五个示例之后,参数正确率提升非常明显,尤其是在用户用口语化、缩写化表达时,模型更需要参照物。
一个建议是:示例要覆盖不同的表达方式。比如正常问法、省略问法(“上海明天”)、带比较的问法(“上海比北京热吗”)。每种类型给一两个例子,别贪多,模型看了几十个例子反而不知道哪个是重点,三到五个高质量示例就够用。
2.5 实践五:控制 Skill 数量并划分边界,避免兄弟打架
另一个很常见的问题场景:模型候选列表里有七八个功能相似的工具,它直接犯选择困难症。我曾经见过一个项目,把“查天气”“查空气质量”“查湿度”“查风力”拆成四个 Skill,各自独立注册,结果模型经常张冠李戴,选了 A 还是幸运的,更多时候直接不知道选哪个。
模型的工具选择本质上是一个多分类问题,候选越大,每个类别的区分度要求就越高。如果你不是迫不得已,一定要控制“兄弟类”Skill 的数量。最好用的方案是合并同类项:把高度相关的功能收敛成一个 Skill,在内部用子操作(action)或二级参数来区分。查询天气、空气质量、风力可以统一成一个“天气相关查询”的 Skill,通过传入不同的字段来获取对应内容。
如果实在无法合并(比如不同 Skill 归属不同的后端服务),那就在描述里明确各自的“领地”,主动告诉模型“如果你要查询的是空气质量,应该用另一个工具,而不要用我”,同时上线前用一批典型对话实测一下区分度,发现问题及时调整。
2.6 实践六:鉴权和运行时资源提前配置,别让执行链在最后一步断开
这一类问题特别隐蔽,它表面上不像“不被调用”,更像是“调用失败”,但背后的原因还是设计时没考虑周全。常见场景是:模型终于正确选择了 Skill,开始构造请求,结果真正发出去的时候,因为域名没有加入白名单、密钥过期、服务没有绑定正确的运行环境,调用直接失败。
这种失败对 Skill 的“信誉”伤害很大。模型收到错误结果之后,会把它记成“这个工具不可用”,后续可能就会绕着你走。更严重的是,某些平台还会因为多次调用失败自动禁用这个 Skill,你需要手动去后台重新启用。
我们的经验是:Skill 的权限、资源、依赖要在开发阶段就全部配齐,不要留到上线前。而且在测试的时候,一定要覆盖“无权限”“参数缺失”“服务不可达”这种异常输入,确保即使失败,返回值也是结构化的、可被模型理解的错误信息,比如包含 error_code 和提示文案,而不是一个裸的异常堆栈。
2.7 实践七:超时和重试策略要配合模型决策节奏
模型调用工具和你手动调接口有一个巨大的区别:模型没有“耐心”。如果你在 Skill 里设置了一个非常短的超时时间,比如 3 秒,而你的后端服务在处理复杂查询时经常要花 5 秒以上,那么这次调用大概率会被判定失败。这时候用户还等着模型回答,体验直接崩塌。
我的建议是:默认超时时间可以设置在 30 秒左右,但这里要看平台支持和你的服务特性。对于耗时较长的操作,不要用同步等待,应该设计成异步任务模式——先提交任务,返回任务 ID,模型再通过轮询或回调获取最终结果。这样既不会让模型干等,也不会因为超时误杀正常请求。
还有幂等性。因为网络抖动或平台自动重试,同一个操作可能被触发多次。比如创建订单、发送通知这类有副作用的操作,一定要做幂等处理。最朴素的做法是接收一个 request_id,后端根据它判断是否重复执行。
2.8 实践八:输出内容要结构化,模型才能稳定接住
Skill 的返回值是模型继续推理的依据。如果一个 Skill 返回的是一大段自由文本,语言风格还不稳定,那模型需要花额外的精力去理解和提取其中的关键信息。自由文本里的信息一旦有歧义,模型很容易产生误解,进而影响下一步决策。
我们现在的规范是:Skill 的输出统一采用结构化 JSON。哪怕是给用户看的最终文案,也分成两个部分——原始结构化数据,以及一段供模型参考的摘要文本。模型可以用摘要快速理解结果,需要更细的数据时再去读结构化字段。
举个例子。返回“上海明天晴天,气温 26 度到 31 度,风力三级”这种纯文本,模型勉强能懂;但如果你返回{"city":"上海","date":"2025-07-11","condition":"sunny","temp_min":26,"temp_max":31,"wind_level":3,"summary":"明天晴天,气温26到31度,风力三级"},那模型对结果的解析就非常稳定,后续生成更自然的回复也容易得多。这一点在做复杂业务的时候尤其重要,它直接决定了整个 Agent 在调用完工具之后的“下半场”顺不顺。
2.9 实践九:可观测性和回归测试是最后一公里
最后一个实践我放在这里,是因为它看起来最“不紧急”,但等你紧急的时候再补就晚了。没有日志、没有 Trace、没有回归用例,Skill 失灵的时候你连问题出在哪都不知道。你打开后台,只能看到“调用次数为 0”,根本无从判断是模型没选它,还是选了但请求没发出来,还是发出去了但解析出了问题。
我们团队后来给所有 Skill 配了统一的调用日志埋点,至少包含以下信息:候选 Skill 列表、模型最终选择的 Skill、请求参数、返回结果、完整耗时时长、错误码。有了这些日志,排查问题就变成查表,而不是瞎猜。
回归测试同样不能少。我们从真实对话里整理了一批“黄金用例”,覆盖每个 Skill 的典型触发场景和常见边界场景。每次改动 Skill 的名称、描述还是参数结构,都先拿这批用例跑一遍,对比调用准确率,防止“改一处坏一片”的连锁事故。
3. 一套可复制的开发与验收流程
3.1 设计阶段:先写意图声明,再写代码
很多团队开发 Skill 是反着来的,拿到需求就写代码,写完才去平台上补描述。这样做的结果就是代码写得很好,但描述完全是后来硬凑的。
我现在习惯要求先写一份精简的“意图声明”。不用长,就回答四个问题:这个 Skill 服务什么意图;用户通常会怎么表述这个意图;什么情况下不该调用;请求参数和返回结构分别是什么。写清楚这份声明,再开始编码。这相当于先把“招牌文案”定了,再去做“窗口后面的业务”。
3.2 验证阶段:离线模拟和在线灰度缺一不可
Skill 开发完成后,别急着直接发布到生产环境。我们一般先做一轮离线模拟:把一批测试 prompt 批量发给配置了这个 Skill 的模型环境,检查模型是否在正确的位置选择了它、参数映射是否准确、输出结果是否稳定。
离线模拟过的 Skill 再进入在线灰度。灰度期间主要盯三个数:调用率、成功率、无效调用率。调用率太低说明模型不识别;成功率低说明运行时有问题;无效调用率高说明边界描述不清,模型在不需要它的时候也调了它。
3.3 发布阶段:一次只改一个变量
上线后如果要做优化,记住一个原则:一次只改一个东西。很多人发现调用率不高,同时改了名称、描述、加了示例,结果确实变好了,但完全不知道是哪一步起的作用。
正确做法是每次都只动一个配置项,上线后再观察一两天的数据,确认这个改动是正向还是负向。虽然慢,但你能真正积累出对这个模型行为的理解,下次改别的 Skill 就有经验可以参考了。这一步稳扎稳打,长期收益远比“碰运气式改配置”高得多。
4. 常见问题排查手册与避坑速查
4.1 模型完全无视我的 Skill,先查这四个点
如果日志显示调用次数为 0,基本上可以断定模型从来没选中过它。这时候按照下面的顺序排查:名称是否足够直观、描述是否覆盖了用户真实表达的意图、参数是否让模型望而却步、示例是否缺失。绝大部分情况都出在这四项里,而且往往是“描述太模糊”或者“参数太自由”导致的。我建议你先去把描述用用户的话再说一遍,而不是用开发者的话。
4.2 调用时不时失败,顺着调用链路逐段排查
如果调用次数有,但错误率高或者结果不稳定,那就顺着链路一段段查:模型是否选择了正确 Skill、请求参数是否有缺失或类型错误、权限鉴权是否通过、后端执行是否异常、返回结果是否结构化。我们实际排查中,最容易被忽略的是“模型选了但传参不对”和“后端报错但 Skill 没做错误兜底”这两个位置,日志里看下完整链路,基本马上就能定位。
4.3 九条实践对照速查表
下面这张表是我们团队内部经常拿出来对用的清单,你排查问题的时候可以直接参考。
| 实践编号 | 核心问题 | 最典型错误 | 正确姿势 |
|---|---|---|---|
| 1 | 名称 | 名称过短、带版本号、语义模糊 | 动词开头,一眼看出功能,不带版本号 |
| 2 | 描述 | 写得像接口文档,没覆盖意图 | 写清触发场景、参数、返回内容、不该用场景 |
| 3 | 参数 | 全部用 string,没有枚举和格式约束 | 类型、必填、枚举、日期格式、给模型“翻译” |
| 4 | 示例 | 完全没有示例 | 配 3-5 个覆盖不同表达的 few-shot 示例 |
| 5 | 边界 | 相似 Skill 互相打架 | 合并同类项或明确“分工” |
| 6 | 鉴权 | 上线后才发现没权限 | 开发期配好权限,测试覆盖异常场景 |
| 7 | 超时 | 超时过短导致正常请求失败 | 异步任务、幂等键、合理超时设置 |
| 8 | 输出 | 返回自由文本,模型解析不稳定 | 统一结构化 JSON,错误码明确 |
| 9 | 观测 | 没有日志和回归用例 | 记录完整调用链路,维护黄金用例集 |
结合这张表,即便你对模型内部的运行机制理解得不够深,也能照着清单一步步排除问题。Skill 开发本质上是“和模型的沟通设计”,而这张表就是我们和自己内心那份“经验沉淀”之间的最短路径。
我个人体会最深的一点是:很多“不被调用”的 Skill,代码逻辑完全正确,就差在几行描述和几个参数约束上。下次再遇到类似情况,先别急着动业务代码,把它的名字、描述、参数和示例从头看一遍,当成一个从没见过这个功能的新同事来审视。如果只凭这些文字,你能不能准确说出它在什么情况下、该怎么用?想清楚这一步,大部分疑难杂症都开了。