
1. 从“一次能跑”到“换个说法就废”一个被忽视的Skill设计陷阱你有没有遇到过这种情况自己写了一个Skill第一次测试的时候输入一句指令它跑得漂漂亮亮输出结果完全符合预期。你心里一阵窃喜觉得大功告成。然后你换了个说法意思完全一样只是措辞变了结果它要么报错要么输出一堆莫名其妙的东西要么干脆装死不动。你开始怀疑人生明明是同一种需求为什么换个说法就废了这个问题在Skill开发圈子里极其普遍但很少有人把它当作一个正经的技术问题来对待。大多数人第一反应是“模型不行”或者“运气不好”然后草草了事。但如果你真的想把一个Skill做到可用、耐用、别人也能用这个“换个说法就废”的问题就是你必须跨过去的一道坎。我把它称为Skill的表述鲁棒性问题。鲁棒性这个词在软件工程里指的是系统在异常输入或环境变化下仍能保持稳定运行的能力。放到Skill开发里就是指你的Skill能不能扛住用户千变万化的表达方式。用户不会按照你预设的那句话来说话他们有各自的习惯、口音、省略方式、甚至错别字。如果你的Skill只能识别一种说法那它本质上就是一个硬编码的if-else只不过披了一层智能的外衣。这篇文章适合所有正在写Skill或者准备写Skill的人。不管你是刚入门的新手还是已经写过几个Skill但总觉得“不太稳”的老手我都会从根因分析、设计思路、实操方法、测试策略几个维度把这个问题彻底讲透。我会告诉你为什么会出现这种情况怎么从架构层面去解决它以及我在实际项目中踩过的那些坑和总结出来的经验。2. 为什么你的Skill只认一种说法根因拆解2.1 意图识别被“关键词匹配”绑架了很多人写Skill的时候脑子里想的是“用户会说这句话”然后就把这句话当作触发条件。比如你写一个天气查询Skill你预设的触发词是“今天天气怎么样”于是你在代码里写了一个判断如果用户输入包含“天气”两个字就执行查询逻辑。看起来没问题对吧但用户可能说“外面下雨吗”、“今天冷不冷”、“出门要不要带伞”、“气温多少度”。这些表达里有的包含“天气”有的完全不包含但意图是一样的。这就是典型的关键词匹配陷阱。你把意图识别简化成了字符串包含判断而人类的语言表达是高度灵活的。同一个意图可以有几十种甚至上百种表达方式你不可能穷举完。更麻烦的是不同关键词之间还有交叉。比如“今天冷不冷”既可以是在问天气也可以是在问空调温度。如果你只靠关键词就会频繁误判。我在早期做的一个日程管理Skill里就犯过这个错误。当时我预设的触发词是“帮我安排一个会议”结果用户说“明天下午三点我要跟张三碰一下”完全没触发。后来我统计了一下用户实际使用的表达方式有二十多种而我预设的只有三种。覆盖率不到15%。2.2 参数提取过于依赖固定句式除了意图识别参数提取也是重灾区。假设你写一个“设置提醒”的Skill你需要从用户输入里提取时间、事项、提醒方式等参数。如果你预设的句式是“提醒我明天早上八点开会”你的提取逻辑可能是按位置切分先找“提醒我”再找时间词再找事项。但用户可能说“明天八点记得叫我起床”、“帮我设个闹钟七点半”、“下午两点有个会别忘了提醒我”。这些句式的结构完全不同按位置切分必然失败。参数提取的核心难点在于槽位填充的灵活性。用户不会按照你设计的模板来说话他们会在句子里随意插入修饰语、语气词、甚至无关信息。你需要的是语义层面的理解而不是句法层面的模式匹配。2.3 上下文缺失导致“断章取义”还有一种情况是用户的表达本身是完整的但你的Skill没有考虑到上下文。比如用户先说“我想订一张去北京的机票”你的Skill成功识别了意图。然后用户接着说“改成后天”这时候如果你的Skill没有维护对话状态它就会把“改成后天”当作一个全新的输入来处理完全不知道这是在修改之前的订票需求。上下文缺失是很多Skill“第一次能跑第二次就废”的直接原因。第一次用户给了完整信息Skill跑通了。第二次用户基于上一轮对话做了补充或修改Skill就懵了。这不是表述问题而是状态管理问题但表现出来就是“换个说法就废了”。2.4 训练数据偏差与过拟合如果你用的是基于机器学习或大模型的方案还有一个隐蔽的坑训练数据偏差。你在设计Skill的时候可能只用了自己习惯的表达方式来构造训练样本或提示词。这些样本天然带有你的语言习惯比如你习惯说“帮我查一下”但用户可能说“我想知道”、“麻烦看一下”、“查查”。如果你的训练数据里只有“帮我查一下”这一类表达模型就会过拟合到这种句式上换一种说法就识别不了。这个问题在Few-shot prompting里特别常见。你给了模型三个例子都是“请帮我做X”的句式模型就会倾向于只识别这种句式。你需要刻意在示例里加入多样化的表达方式才能让模型学到更泛化的模式。3. 让Skill扛住“一百种说法”的设计思路3.1 从“匹配句式”转向“理解意图”解决表述鲁棒性问题的第一步是转变思维模式。不要再想“用户会说什么”而是想“用户想做什么”。这两者的区别在于前者关注表面形式后者关注深层意图。具体到实现上你需要把意图识别从字符串匹配升级到语义理解。如果你用的是大模型可以通过设计好的提示词让模型做意图分类。如果你用的是传统NLP方案可以用意图分类模型或者语义相似度匹配。核心思想是不管用户怎么说只要意图相同就应该被路由到同一个处理逻辑。举个例子对于天气查询Skill你可以定义意图为“查询天气”然后给模型一些示例“今天天气怎么样”“外面下雨吗”“今天冷不冷”“出门要不要带伞”“气温多少度”这些示例覆盖了不同的表达方式模型就能学到“查询天气”这个意图的语义边界而不是死记某几个关键词。3.2 槽位提取要“抓语义”而不是“抓位置”参数提取的思路也要转变。不要试图用正则表达式去匹配固定句式而是让模型或解析器去理解“这句话里包含了哪些关键信息”。以设置提醒为例你需要提取的核心槽位是时间、事项、提醒方式。不管用户怎么说只要这句话里包含了这三个信息就应该能被提取出来。你可以用大模型来做信息抽取给它一个结构化的输出格式要求比如{ intent: set_reminder, time: 明天早上八点, event: 开会, notify_method: 默认 }然后在提示词里说明从用户输入中提取上述字段如果某个字段没有出现填null。这样模型就会专注于语义理解而不是句式匹配。3.3 维护对话状态让Skill“记得”之前说了什么对于多轮对话场景你必须维护一个对话状态。这个状态可以是一个简单的字典记录当前意图、已填充的槽位、待填充的槽位等信息。每次用户输入新内容时先判断这是新意图还是对上一轮的补充/修改。如果是补充/修改就更新对应的槽位而不是重新开始。实现上你可以在每次模型调用时把之前的对话历史一起传进去让模型基于完整上下文做判断。或者你也可以自己维护一个状态机显式地管理槽位填充过程。两种方式各有优劣前者更灵活后者更可控。3.4 用“意图槽位”的解耦架构替代“端到端硬编码”很多新手写Skill喜欢端到端硬编码输入一句话直接输出结果。这种写法在Demo阶段很快但一旦要扩展或维护就会变得极其脆弱。更好的架构是解耦的意图识别层判断用户想做什么。槽位提取层提取做这件事需要的参数。业务逻辑层根据意图和参数执行具体操作。回复生成层把结果组织成自然语言返回给用户。这种分层架构的好处是每一层都可以独立优化和测试。意图识别不准你只需要调整意图识别的提示词或模型槽位提取漏了参数你只需要优化提取逻辑。而且这种架构天然支持多轮对话因为状态可以在层与层之间传递。4. 实操一步步打造一个“换个说法也能跑”的Skill4.1 定义意图空间与槽位 schema动手写代码之前先想清楚你的Skill要支持哪些意图每个意图需要哪些槽位。这一步看起来简单但直接决定了后续的扩展性和鲁棒性。以“个人助理Skill”为例假设它要支持以下意图意图名称说明必需槽位可选槽位query_weather查询天气地点、时间无set_reminder设置提醒时间、事项提醒方式query_schedule查询日程时间范围无add_schedule添加日程时间、事项地点、参与人cancel_schedule取消日程事项关键词时间定义好这个表格之后你就有了一个清晰的“能力边界”。用户不管怎么说最终都会被映射到这五个意图之一或者被判定为“不支持”。槽位schema则告诉你在提取信息时应该关注哪些字段。4.2 构造多样化的意图识别提示词如果你用大模型做意图识别提示词的设计至关重要。不要只给一两个示例要给足够多的、多样化的示例。以下是我在实际项目中用的一个模板你是一个意图分类器。请根据用户输入判断其意图并从以下列表中选择最匹配的一个 - query_weather查询天气情况 - set_reminder设置提醒或闹钟 - query_schedule查询已有日程 - add_schedule添加新日程 - cancel_schedule取消已有日程 - unknown无法判断 注意 1. 用户可能用各种方式表达同一个意图请关注语义而非关键词。 2. 如果用户输入包含多个意图选择最主要的一个。 3. 如果无法判断返回unknown。 示例 输入今天天气怎么样 → query_weather 输入外面下雨吗 → query_weather 输入明天冷不冷 → query_weather 输入出门要不要带伞 → query_weather 输入帮我设个闹钟七点半 → set_reminder 输入明天八点记得叫我起床 → set_reminder 输入下午两点有个会别忘了提醒我 → set_reminder 输入我明天有什么安排 → query_schedule 输入看看后天的日程 → query_schedule 输入明天下午三点跟张三碰一下 → add_schedule 输入帮我约个会周五上午 → add_schedule 输入把明天的会取消了 → cancel_schedule 输入周五的约见不用了 → cancel_schedule 用户输入{user_input} 意图这个提示词的关键在于示例的多样性。每个意图我都给了至少三个不同表达方式的例子覆盖了陈述句、疑问句、省略句、口语化表达等。这样模型就能学到意图的语义边界而不是死记某几个词。4.3 槽位提取的提示词设计与容错处理槽位提取的提示词需要更结构化。以下是一个示例你是一个信息提取器。请从用户输入中提取以下字段并以JSON格式返回 - time时间信息如果没有提到则为null - event事项内容如果没有提到则为null - location地点信息如果没有提到则为null - people参与人如果没有提到则为null 注意 1. 时间信息可能是模糊的如“明天”、“下周”、“下午”请原样提取。 2. 事项内容可能是短语或句子请提取核心动作或事件。 3. 如果某个字段在输入中没有出现填null。 4. 不要编造信息。 用户输入{user_input} 输出这个提示词的关键是容错。用户可能只说了一部分信息比如“提醒我明天开会”没有说具体时间。这时候time字段应该填“明天”event字段填“开会”其他字段填null。后续的业务逻辑层再判断哪些必需槽位缺失然后向用户追问。4.4 业务逻辑层的槽位校验与追问策略业务逻辑层拿到意图和槽位之后第一件事是校验必需槽位是否齐全。如果不齐全就要生成追问。追问的策略也很讲究不能太生硬。比如用户说“提醒我明天开会”提取到的槽位是time“明天”event“开会”但缺少具体时间点。你可以追问“明天几点开会我帮你设提醒。”这样既自然又明确。如果用户说“帮我设个提醒”什么信息都没给你可以追问“好的提醒什么事情大概什么时候”一次性把缺失的信息都问出来减少来回次数。这里有一个经验追问时尽量用选择题而不是填空题。比如不要问“你想什么时候提醒”而是问“你是想上午提醒还是下午提醒”选择题能降低用户的认知负担也能减少歧义。4.5 回复生成让输出也“扛得住”不同场景回复生成同样需要考虑鲁棒性。同一个操作在不同上下文下可能需要不同的回复方式。比如设置提醒成功如果用户之前已经有过一次追问回复可以是“好的已设置”如果是首次请求回复可以是“已为你设置明天早上八点的开会提醒”。你可以用模板变量的方式生成回复也可以用大模型动态生成。模板方式的优点是稳定可控缺点是灵活性差。大模型方式的优点是自然灵活缺点是有时候会“自由发挥”。我的建议是核心确认信息用模板保证准确附加说明可以用模型生成。5. 测试策略怎么知道你的Skill是不是“换个说法就废”5.1 构造“同义句测试集”这是最基础也是最重要的一步。针对每一个意图你需要构造至少10-20个不同表达方式的测试用例。这些用例应该覆盖正式表达“请问今天天气如何”口语表达“今天天气咋样”省略表达“天气”间接表达“出门要不要带伞”带错别字“今天天汽怎么样”中英混合“今天weather怎么样”带语气词“今天天气怎么样呀”倒装句“怎么样今天天气”把这些用例整理成一个测试集每次修改Skill之后都跑一遍看看通过率有没有下降。这个测试集就是你的“鲁棒性基准线”。5.2 用“对抗性输入”找边界除了同义句你还需要构造一些对抗性输入专门用来找Skill的边界。比如意图模糊的输入“今天怎么样”可能是问天气也可能是问日程包含多个意图的输入“帮我查下天气然后设个八点的闹钟”完全无关的输入“你觉得人生的意义是什么”空输入或纯符号输入“。。。”超长输入一段几百字的文本里面夹杂着一个意图这些输入不一定都要正确处理但你的Skill至少不应该崩溃或输出莫名其妙的结果。对于无法处理的输入应该优雅地返回“抱歉我没理解你可以换个说法吗”。5.3 自动化回归测试的搭建手动测试只能覆盖有限的情况你需要自动化。最简单的做法是写一个脚本把测试集里的每一条输入依次喂给Skill记录输出然后和预期结果对比。可以用Python写test_cases [ {input: 今天天气怎么样, expected_intent: query_weather}, {input: 外面下雨吗, expected_intent: query_weather}, {input: 明天冷不冷, expected_intent: query_weather}, # ...更多用例 ] for case in test_cases: result skill.process(case[input]) if result[intent] ! case[expected_intent]: print(fFAIL: {case[input]} - {result[intent]}, expected {case[expected_intent]})每次修改提示词或逻辑之后跑一遍确保没有回归。这个习惯一旦养成你的Skill质量会有质的飞跃。5.4 真实用户表达的收集与迭代自动化测试集再全也比不上真实用户的表达。你需要在Skill上线后持续收集用户的输入特别是那些识别失败或识别错误的案例。把这些案例加入测试集不断迭代你的提示词和逻辑。我在一个项目中做过统计上线第一周意图识别准确率只有72%。收集了200条真实用户输入后我发现主要问题集中在省略表达和口语化表达上。针对性地补充了示例之后第二周准确率提升到了89%。第三周又收集了一批发现中英混合和错别字是新的问题点继续优化最终稳定在94%左右。这个过程没有捷径就是收集-分析-优化-验证的循环。6. 那些年我踩过的坑与总结出的经验6.1 示例不是越多越好而是要“有代表性”刚开始做意图识别的时候我犯过一个错误给模型塞了50个示例结果模型反而懵了。后来我才明白示例的质量比数量重要。10个覆盖不同表达模式的示例比50个同质化的示例效果好得多。什么叫有代表性就是要覆盖不同的句式结构、不同的语气、不同的信息密度。比如疑问句、陈述句、祈使句各来几个长句、短句、省略句各来几个正式表达和口语表达各来几个。这样模型才能学到意图的“不变本质”而不是“表面形式”。6.2 不要试图用规则覆盖所有情况有些开发者喜欢用正则表达式或关键词列表来兜底觉得这样更可控。但语言的变化是无穷的你永远覆盖不完。而且规则之间会冲突维护成本极高。我的建议是能用模型解决的就不要用规则。模型天然具备泛化能力你只需要给它好的示例和清晰的指令。当然这并不意味着规则完全没用。对于一些格式非常固定的输入比如日期、时间、数字用规则提取反而更准确。关键是分清哪些部分适合模型哪些部分适合规则。6.3 多轮对话的状态管理比想象中复杂多轮对话的状态管理是一个深坑。你以为只需要记录上一轮的意图和槽位就行了但实际上用户可能在中途切换意图、修改之前的参数、甚至否定之前的说法。比如用户帮我订一张去北京的机票 Skill好的什么时间出发 用户等等我不去北京了改去上海 Skill好的去上海什么时间出发 用户后天这里用户先修改了目的地然后补充了时间。如果你的状态管理只是简单地覆盖就会丢失“订机票”这个意图。你需要维护一个完整的对话状态包括当前意图、已确认的槽位、待确认的槽位、以及历史修改记录。我的做法是用一个字典来维护状态每次用户输入后先判断是“新意图”还是“修改/补充”然后更新对应的字段。修改时要注意保留未修改的字段而不是全部重置。6.4 测试用例要“活”起来测试用例不是写一次就完了。每次发现新的失败案例都要把它加入测试集。每次修改提示词都要跑一遍全量测试。我习惯用版本管理工具来管理测试集每次修改都提交一次这样可以看到测试集的增长轨迹也能回溯是哪个修改导致了回归。另外测试用例的预期结果也要定期审查。有时候模型的行为变了原来的预期结果可能不再适用。这时候需要人工判断是模型变好了还是变差了然后更新预期结果。6.5 用户不会按照你的预期说话但你可以引导他们虽然我们一直在强调要适应各种表达方式但反过来你也可以通过设计来引导用户。比如在Skill的欢迎语里说明它支持哪些功能在追问时给出明确的选项在回复里使用统一的术语。这些都能在一定程度上减少用户的表达多样性降低识别难度。但引导只是辅助手段核心还是要把鲁棒性做好。你不能指望用户都按照你的引导来说话那是不现实的。7. 从“能跑”到“耐用”一个持续迭代的过程写一个第一次能跑的Skill可能只需要半小时但写一个换个说法也能跑的Skill可能需要几天甚至几周的持续打磨。这个投入是值得的因为只有耐用的Skill才能真正被用户接受才能在真实场景中产生价值。我现在的习惯是任何一个Skill在正式使用之前至少要有50条以上的同义句测试用例并且通过率要达到90%以上。上线之后持续收集失败案例每周迭代一次。这样坚持下来Skill的鲁棒性会越来越好用户的满意度也会越来越高。最后分享一个我常用的技巧在Skill的回复里加一句“如果我没理解对你可以换个说法再试一次”。这句话看起来简单但能有效降低用户的挫败感也能为你收集到更多的表达样本。用户换了一种说法之后如果成功了你就知道原来的说法是盲区如果还是失败你就知道这个意图本身可能有问题。无论哪种情况都是宝贵的反馈。这个内容后续还可以这样扩展针对特定领域的Skill比如电商客服、智能家居控制、日程管理可以建立领域专用的同义句库和测试集进一步提高鲁棒性。另外如果你用的是大模型方案可以尝试用微调来替代提示词工程效果可能会更稳定。