1. 从"会聊天"到"会干活":Agent Skills 到底解决什么问题
近半年我一直在折腾各类智能体项目,最深的感受是:模型本身的能力已经够强了,真正拉开体验差距的反而是那些"看起来不起眼"的技能定义。同样是调用一个天气接口,有人写的技能让模型一次就精准命中参数,有人写的技能能让模型连续犯错、胡编乱造。这个现象让我意识到一件事——Agent Skills 才是智能体落地时最值得花心思打磨的部分。
打个比方,大模型本身就像一个刚入职的高学历新人,知识储备充足、理解能力很强,但它不知道你们公司的报销流程是什么、不知道内部系统的字段含义、不知道哪些操作有风险需要人工确认。Skills 就是那份"新人入职手册",把每个任务拆解成可执行的动作、说清楚触发条件和注意事项。没有这份手册,新人只能靠猜;有了它,才能稳定地干出及格线以上的活。
这篇文章写给正在做 Agent 应用、尤其是被"模型调用不听话""工具参数老填错""多步骤任务总断链"这些问题折磨过的开发者。我会从技能设计的基本原则、参数约束的关键技巧、多技能编排的取舍,再到排查思路和评估方法,把我在真实项目里踩过的坑和验证过的做法完整梳理一遍。内容偏工程实践,不涉及复杂的数学推导,只要写过几段调用大模型接口的代码,就能看懂并直接用起来。
2. 技能设计整体思路拆解:先想清楚"边界"再动手写
2.1 技能、工具、函数调用:三个名词的边界在哪
很多人把 Agent Skill、Tool、Function Calling 混着用,但在实际工程里它们是有明显分工的。我的理解是这样的:
- Function Calling / Tool Calling:模型输出结构化的调用意图,告诉系统"我想调用某个函数,参数是这样的"。这是通信协议层的东西,解决的是"模型怎么表达意图"的问题。
- Tool:一个可以被调用的具体能力单元,比如"查询天气""发送邮件""计算运费"。它通常是独立的接口封装,对应一个或一组函数。
- Skill(技能):更高一层的抽象,它把"完成某个用户目标所需的若干步骤、知识、约束条件"打包在一起。一个 Skill 内部可以包含多个 Tool 的调用、固定的处理流程、甚至一段领域知识文本。
举个例子:"帮用户完成订单退款"是一个 Skill。它内部可能包含:校验订单状态(Tool A)、计算可退金额(Tool B + 一段规则知识)、发起退款(Tool C)、通知用户(Tool D)。如果只给模型四个孤立 Tool,它可能顺序搞错、漏掉校验、退款金额算错;但如果把这些编排和约束放进 Skill 的描述里,模型就能按剧本执行。
理解了这层差异,你就明白为什么说 "Agent Skills 是让模型从会聊天到会干活的关键" 了。工具是零件,技能是装配说明书,模型则是那个拿着说明书干活的工人。
2.2 设计技能时的五大边界问题
在动笔定义任何技能之前,我会强制自己回答五个问题,回答不清楚就不写:
第一,这个技能的触发条件是什么?用户说什么样的话、处于什么样的上下文时,才应该调用这个技能?不写清楚,模型就会滥用。比如"发送邮件"技能,如果没有明确"仅当用户明确要求发送时才调用",模型可能在用户只是询问"邮件功能怎么用"时就去调用发送接口。
第二,这个技能能做什么、不能做什么?边界不清晰是技能设计最常见的失误。我见过一个"文本润色"技能,描述里写着"负责优化用户输入的文本",结果用户问数学题时模型也去调用它"润色"答案。正确做法是明确写"本技能仅用于优化已有文本的表达质量,不用于生成新内容、不用于回答事实性问题"。
第三,这个技能需要哪些输入?每个输入的约束是什么?这一步直接决定了调用成功率。参数缺失、格式错误、枚举值超范围,是最常见的三类调用失败原因。设计阶段就要对每个参数定义清楚类型、取值范围、默认行为、以及缺失时的兜底策略。
第四,这个技能的输出格式是什么?模型拿到结果后还要继续推理,输出格式混乱会连锁导致后续步骤出错。我会坚持给每个技能定义结构化的输出,最好是 JSON Schema 级别的严格程度。
第五,这个技能的失败模式有哪些?失败后怎么办?真实的接口调用总会出错:超时、限流、返回空数据、业务异常。技能描述里必须写清楚"如果收到 XXX 错误码,应该重试一次""如果查询结果为空,要告知用户未找到而不是编造数据"。
把五个问题写进技能定义,相当于给模型划了一条清晰的跑道,它不会乱跑,你也更容易定位问题。
2.3 为什么不要一上来就追求"全能型技能"
很多开发者倾向于把技能设计得又大又全,希望一个"超级技能"解决所有问题。我的经验恰恰相反:技能的粒度越细,越容易被模型正确理解和调用。
细粒度技能的优势体现在几个方面。第一,描述可以写得极其精确,不会因为功能太多而含糊;第二,模型选择技能时更容易匹配用户意图,减少误调用;第三,单个技能出问题时的影響面可控,排查成本低;第四,可以针对高频技能做专门的测试和优化。
当然,粒度太细也会带来副作用——技能数量爆炸,模型在第一步"选择哪个技能"就犯难。我目前的经验阈值是:单领域内的技能数量控制在 10~15 个以内,超过这个数量就要考虑分层或合并。技能越多,对描述质量的要求越高,这个下文会详细展开。
3. 核心参数与描述规范:模型"听话"的底层逻辑
3.1 技能描述的结构化模板
模型不像人,它不会"理解言外之意",只会严格按照字面信息做匹配。因此技能描述必须结构化、无歧义。我在项目中总结了一套模板,每个技能都按这个结构填写:
技能名称:短名词短语,一眼能看出功能。如"查快递物流""计算房贷月供"。 触发条件:什么情况下必须使用;什么情况下禁止使用。 功能概述:2~3句话说清楚这个技能做什么、达到什么效果。 输入参数:每个参数的名称、类型、必填性、取值范围、约束条件、示例值。 执行流程:核心处理步骤,按序排列,说明特殊情况如何处理。 输出格式:结构化输出字段及其含义。 错误处理:常见异常场景的处置方式。 安全与限制:涉及权限、敏感操作时的确认要求。示例一个"查快递物流"技能的描述:
【技能名称】查快递物流 【触发条件】 - 当用户提供快递单号并要求查询物流轨迹时使用。 - 当用户询问"包裹到哪了""物流状态"等意图时使用。 - 禁止在用户未提供或未授权提供快递单号时自行推测查询。 【功能概述】 根据用户提供的快递单号和快递公司代码,查询该包裹的最新物流轨迹及当前状态,返回带时间线的物流摘要。 【输入参数】 - tracking_number(必填,string):快递单号,支持数字和字母组合,长度6~32位。示例:"SF1234567890"。 - carrier_code(必填,string):快递公司代码,枚举值:SF(顺丰)、ZT(中通)、YT(圆通)、YD(韵达)、JD(京东)。若用户只说了快递公司中文名,需映射到对应枚举值。 - phone_last4(可选,string):收件人手机号后4位,部分快递公司查询需要校验,如无需要可不传。 【执行流程】 1. 调用物流查询接口,传入 tracking_number 和 carrier_code。 2. 若接口返回需要手机号校验的错误码,提示用户提供收件人手机号后4位。 3. 若查询成功,提取物流轨迹节点(时间、地点、状态描述),按时间倒序排列。 4. 若轨迹为空,检查单号是否符合当前快递公司的格式,格式错误则提示用户核对。 【输出格式】 JSON对象,包含: - status: "in_transit" | "delivered" | "exception" | "not_found" - latest_time: 最新轨迹时间 - latest_location: 最新轨迹地点 - summary: 一句话物流摘要 - milestones: 轨迹节点数组,每项包含time、location、description 【错误处理】 - 接口超时:重试一次;仍失败则告知用户"物流系统繁忙,请稍后再试"。 - 单号不存在:返回 not_found,并提示用户核对单号。 - 严禁在查询失败时编造物流节点信息。 【安全与限制】 - 仅允许查询用户主动提供的单号。 - 物流轨迹数据仅用于回答用户本次查询,不用于任何其他用途。写这么长的描述看似繁琐,但它就是技能的"说明书"。实测下来,描述越细致,模型调用的准确率和成功率就越高,反而节省了后续排查的时间成本。
3.2 参数定义决定调用成败
很多时候模型不是"不聪明",而是你给的参数要求让它无从下手。我整理了参数设计的三条铁律:
铁律一:能枚举就不开放。凡是取值范围有限的字段,一律给出枚举值。比如快递公司代码就不要让模型自由填字符串,它可能会填"顺丰速运""SF-Express"等各种变体。给出枚举表,模型照抄就行。
铁律二:能给默认值就给默认值。比如"查询天气"技能的 units 参数,默认 celsius;"发送邮件"技能的 priority 参数,默认 normal。减少模型需要决定的事情,就是减少出错的概率。
铁律三:明确缺失参数时的处理策略。是让模型主动追问用户,还是用默认值替代,还是宁可失败也不猜测?这个策略不同,体验差异很大。对于安全敏感操作,我倾向于"缺失关键参数就向用户确认";对于非关键参数,直接用默认值更流畅。
3.3 一个技能一个文件:工程化组织方式
当技能数量多起来之后,组织方式会影响维护效率。我采用"一个技能一个文件、外加一个索引清单"的方式:
skills/ ├── index.yaml # 技能清单,含每个技能的简介和入口 ├── shipping/ │ ├── track_express.yaml # 技能定义文件 │ └── README.md # 设计备注和变更记录 ├── payment/ │ ├── refund_order.yaml │ ├── query_balance.yaml │ └── README.md └── common/ ├── parse_address.yaml └── format_datetime.yaml每个技能定义文件用 YAML 或 JSON 编写,因为这类格式对机器和人都是可读的。index.yaml 里同时维护每个技能的版本号,技能逻辑变更时版本号递增,方便回溯。
这种组织方式的直接好处是:可以针对单个技能做代码评审、走查和回归测试。我在项目里还加了自动化检查,确保每个技能文件都包含必填字段、描述长度达标、参数约束合法。
4. 多技能编排的实操要点:从"点状能力"到"流程闭环"
4.1 拆解一个完整任务的多技能协作流程
用户的一个请求往往需要多个技能协作完成。以"用户想申请退换货"为例,可能涉及:查询订单状态、生成退货单、预约快递上门取件、通知用户。如果没有编排逻辑,模型会手忙脚乱。
我在实践中把多技能协作的编排策略固定为两种:顺序流水线和条件分支。
顺序流水线适合固定步骤的操作:
步骤1:调用 order_query 获取订单详情 步骤2:根据订单详情中的商品状态,调用 return_create 创建退货单 步骤3:调用 courier_book 预约上门取件 步骤4:调用 message_send 给用户发送进度通知条件分支适合需要判断的场景:
判断订单状态: - 若 status = "已发货",先调用 return_apply 走售后审核流程 - 若 status = "未发货",直接调用 order_cancel 取消订单并退款 - 若 status = "已完成",提示用户该订单已超出退货时效关键在于:编排逻辑不能只靠模型自由发挥,必须在技能描述或者系统 Prompt 中给出明确的路径依赖。我的做法是在系统 Prompt 里固定写一段"流程规则",模型只能遵循,不能自由创造流程。
4.2 依赖关系与状态管理:多步骤任务的翻车点
多技能协作的另一个大坑是状态管理。步骤2依赖步骤1的输出,但模型在步骤3的时候可能已经"忘记"了步骤1的某些细节。解决方案不是指望模型记住,而是显式地把上下文传递给下一步技能。
我采用"技能间通过结构化中间结果传递数据"的模式。步骤1的输出不只是一段自然语言,而是一个 JSON 对象,下游技能直接读取其中的字段。系统 Prompt 中规定:
所有技能的执行结果必须先整理为结构化的 JSON 数据再进入下游技能。任意两个技能之间禁止用自然语言段落传递关键数据。这一步彻底解决了"模型把城市名写在句子里导致下一步提取失败"的问题。
4.3 什么时候该用"子任务拆解",什么时候该"一步到位"
有些任务看似复杂,其实单技能就能搞定;有些任务看似简单,却藏着隐性条件依赖。我的判断标准就一条:如果这个任务的中间结果需要被多个下游逻辑复用,就必须拆;如果从头到尾只有一个出口,那就合并。
举个例子,"查询天气并推荐穿衣"可以是一个技能,也可以拆成"查天气"+"根据温度给出穿衣建议"两个技能。如果后续还要做"根据天气推荐出行方式",那拆开明显更划算——查询天气的结果可以被多个下游技能复用。反之,如果只做一次性回答,合并反而更简洁、调用链路更短、响应更快。
技能拆分不是越细越好,复用性和链路长度之间的平衡才是决定因素。我通常会在初版设计时偏向于拆细,等跑通之后再根据实际调用频率合成高频组合,这属于动态演进的过程。
5. 实操过程全记录:从定义到评测,一次完整的技能开发闭环
5.1 环境准备与最小实现清单
做技能开发不需要重型基础设施,但下面几样东西是必须的:
- 大模型 API 一套(支持 Function Calling 或 JSON 输出模式)
- 技能定义文件存放目录(建议用 Git 管理)
- 一个简易的运行时,用于接收模型调用请求、执行技能、返回结果
- 测试脚本(我习惯用 Python 写一个 pytest 文件,对每个技能做冒烟测试)
最小实现的调用流程是这样的:
用户消息 → 系统 Prompt + 技能定义 → 大模型 → 输出技能调用意图 → 运行时执行对应技能函数 → 返回结构化结果 → 大模型生成最终回答关键点是:技能定义文件要随着每次请求动态拼进 Prompt。不要一股脑把所有技能定义都塞进去,除非你的技能数量很少;否则上下文会被撑爆,模型的注意力也会分散。
5.2 为模型正确解析技能定义保驾护航
这里分享几个我在工程中积累的"保命"细节。
细节一:技能描述放在系统 Prompt 中的位置越靠前,遵循度越高。
我做过对比测试:同样的技能定义,放在 Prompt 开头部分的调用成功率比放在末尾高约15%。原因可能是模型对前文的注意力权重更高。所以,把最重要的技能放在最前面,不重要的甚至可以精简。
细节二:使用分隔符把不同技能的定义隔开。
技能定义之间不加分隔符时,模型的解析偶尔会混。我用固定格式的分隔符:
--- 技能模块开始:技能名 --- (技能描述内容) --- 技能模块结束 ---这个简单的格式化管理让模型的调用意图识别稳定了很多。
细节三:裁剪技能数量以降低误召回概率。
技能定义总长度有限时,模型被干扰的概率更低。我做过一组实验,当技能数从20个减少到8个时,准确调用率提升了近20个百分点。代价是部分低频技能失去了自动调用的机会,解决办法是把低频技能做成"按需加载"——先让模型决定"是否需要该领域技能",再把该领域的技能定义追加进去。这种两级召回机制是我的标配操作。
代码示例(Python):
def build_skill_system_prompt(user_message, all_skills): # 第一步:先让模型判断意图匹配哪些技能域 domain_prompt = f""" 用户消息:{user_message} 以下是技能域清单:{list(all_skills.keys())} 请返回匹配的技能域ID列表(JSON数组格式),不要调用任何技能。 """ domains = call_llm(domain_prompt, response_format='json_array') # 第二步:只加载匹配域内的具体技能定义 active_skills = [] for d in domains: active_skills.extend(all_skills[d]) system_prompt = build_system_prompt(active_skills) return system_prompt这个方案既控制住了上下文长度,又保证了关键技能的准确命中。实测下来,效果很稳。
5.3 Prompt 调优实践:让模型对系统规则言听计从
我对系统 Prompt 的写法要求非常高,因为一个模糊的表述就可能让模型跑偏。几个高频踩坑点和优化技巧:
- 负面清单必须明确列举。只说"不要乱编"不够,要写"严禁编造订单号、物流轨迹、退款金额等任何数据;数据必须来自接口返回"。
- 每一步决策都需要输出依据。要求模型在输出中附带一句"依据字段"说明为什么选了某个技能,便于排查。
- 对"无法处理"的情况给出明文处理方式。而不是让模型自作聪明地兜底。
- 所有技能调用尝试都必须在日志中留下痕迹。我会在运行时层记录每次调用请求、响应、耗时、错误,方便回溯和调优。
下面是一段我在项目里用过的系统 Prompt 片段,定义了模型执行技能时的行为准则:
【技能调用行为准则】 1. 必须在每次技能调用前,输出一段"调用理由"说明当前用户意图与技能触发条件的匹配关系。 2. 所有参数必须严格匹配技能定义中列出的枚举值和格式;有歧义时向用户确认或选择最安全的默认值。 3. 技能返回结果必须被结构化暂存,作为后续步骤的唯一数据来源。 4. 禁止在技能执行失败时返回虚构数据;失败时向用户如实说明错误信息,并给出可行的下一步动作。 5. 在一次任务中,每个技能至多执行3次;执行3次仍失败则终止任务并上报。这些行为准则加上结构化的技能描述,组合起来形成了一套"模型不容易犯错"的约束体系。
5.4 评测机制:不给技能做体检,上线就是灾难
我学到的最大教训是:任何技能在发布前必须经过评测,而不是"看起来能用"就上。我的评测方案分三层:
第一层:单技能冒烟测试。对每个技能写5~10个测试用例,覆盖标准输入、边界输入(空字段、超长字符串、非法枚举值)、模拟接口异常。目标是验证技能在模型环节是否被正确选召、参数是否合规、输出格式是否稳定。
第二层:端到端流程测试。模拟真实用户的一整段对话,验证多技能协作链路是否顺畅。我特别关注中间步骤的上下文传递是否完整、是否出现重复操作或步骤跳跃。
第三层:线上灰度评测。把新技能放在一小部分流量中,用"技能调用成功率、参数合法率、任务完成率、用户重问率"四个指标来量化表现。和旧版本做对比,不达标就回滚。
个人经验:评测阶段发现的问题类型,和实际线上问题高度一致。每次上线前认真跑一轮三层评测,可以过滤掉超过70%的线上事故。
6. 常见问题与排查实录:我把踩过的坑都列给你
6.1 模型不调用该调用的技能
现象:用户问了一个明显需要技能才能回答的问题,模型却直接用自身知识编了个答案。
排查步骤:
- 确认技能是否已经载入当前会话的 Prompt。动态加载时最容易遗忘。
- 检查技能描述里的"触发条件"是否写得太隐晦。我建议直接用"当用户提到XXX词时"这种强绑定的表述。
- 看是不是 Prompt 里存在"抑制调用"的冲突指令。比如系统 Prompt 说"尽量用已有知识回答",模型就会倾向于不用技能。
- 尝试把技能描述写得更有"行动号召力"——描述末尾加上"看到这里,你应该调用本技能来完成任务"。
6.2 参数总填错,特别是枚举值
现象:模型填了一个不在枚举列表里的值,或者把字符串格式写错。
排查步骤:
- 检查枚举值是否有歧义或同义词。如果枚举值本身就容易混淆,模型选错的概率就会高。
- 在技能描述中增加"常见混淆提醒"。例如:"快递公司代码是 SF 而不是 shunfeng 或 SF-Express"。
- 提供"参数示例",模型对示例的模仿能力很强。
- 在运行时层做参数合法性校验,非法参数自动回传错误信息让模型自行纠正。这个兜底能救回不少调用。
6.3 多步骤任务中断或跳步
现象:任务进行到一半,模型跳过某个必要步骤直接输出结论。
排查步骤:
- 检查系统 Prompt 中是否定义了"必须按顺序执行"的流程规则。没有明确顺序约束时,模型容易自行简化流程。
- 确认中间步骤的输出没有被下一步技能正确传递。我建议中间步骤必须输出结构化 JSON,而不是自然语言。
- 检查模型是否因为某个步骤"看起来不重要"而省略了它。这时需要在技能描述中强调步骤的必要性,比如"该步骤用于确认用户身份,不可跳过,否则后续操作会失败"。
- 极端情况下,可以考虑用确定性代码限定主流程,只把非关键分支交给模型判断。流程主干锁定,模型只做分支决策。
6.4 常用问题速查表
| 问题现象 | 最可能原因 | 快速解决办法 |
|---|---|---|
| 技能未被调用 | 触发条件含糊 / 技能未加载 | 强化触发词,检查动态加载逻辑 |
| 参数填错 | 枚举值有歧义 / 缺少示例 | 增加示例值,运行时做参数校验并回传 |
| 调用报错 | 接口变化或技能定义过期 | 检查技能文件版本和接口适配情况 |
| 输出格式混乱 | 技能描述未明确输出JSON结构 | 在描述中写死输出 Schema 并配示例 |
| 多技能跳步 | 流程规则未强调不可省略 | 在系统 Prompt 中加硬性顺序约束 |
| 模型幻觉数据 | 错误处理策略缺失 | 明确"失败时禁止编造数据、必须如实上报" |
| 上下文过大 | 一次性加载全部技能 | 采用两级动态加载机制 |
6.5 独家避坑技巧
最后分享几个不一定写进文档、但实战价值很高的技巧:
技巧一:给技能描述加入"反例"说明。只写正面描述远远不够,模型还是可能误用。我会在描述里追加一小段"不要这样做"的说明,比如"不要将退货地址解析为订单地址"。加了反例之后,误用率能再降一个台阶。
技巧二:运行时记录"调用理由"字段。就是之前在行为准则里提到的,让模型每次调用前输出理由。这样出了问题,你能立刻看到模型的思考链路,排查效率翻倍。
技巧三:多版本 A/B 对比。修改技能描述后,不要只做"改前改后"对比,要把改动前后的版本同时挂在灰度流量里对比。因为模型输出有随机性,单次对比很容易误判。
技巧四:把高频组合技能"固化"为一个新技能。当发现"查询天气"+"穿衣建议"经常成对出现时,就把它封装成一个"weather_outfit"组合技能。这样既省了一次模型决策环节,又减少了中间步骤的失败概率。
7. 从技能库到技能资产的进阶路径:体系建设与迭代节奏
做了一段时间技能开发之后,我发现真正的分水岭不是"会不会写技能",而是"能不能把技能管理成一个持续进化、可复用的资产库"。
具体来说,有三个演进方向值得投入。第一个是技能指标的线上化。我会给每个技能挂了三个核心指标:调用成功率、参数合法率、平均响应耗时。每周固定跑一次数据看板,任何一个指标出现异常就立刻定位。这个习惯帮我提前发现了两个因为第三方接口升级引发的隐性故障。
第二个方向是技能编排的可视化。当技能数量超过二十个之后,纯靠梳理文字已经很难看清全貌了。我引入了简单的编排图描述文件,用代码定义每个技能之间的依赖关系和调用方向,再自动生成一张依赖图。这样每次新增或修改技能,都能快速看出影响范围,不会出现"改了一个下游技能导致上游全线崩溃"的连锁问题。
第三个方向是技能评测用例的持续积累。每遇到一次线上事故,我都会把触发事故的输入原样加入评测集。这样一个季度下来,评测集的覆盖度就有了显著提升,再改技能描述时就能及时发现自己引入了回归。技能不是一次写完就完事的,它和业务代码一样,需要持续维护、改进、验证。
我个人实际操作的体会是:把 Agent Skills 当成第一公民来对待,而不是"顺手写给模型看的一段描述",整个项目的稳定性会有一个质的提升。很多团队用最强的模型做 Agent 却依然频繁翻车,问题往往就出在技能定义粗糙、编排逻辑松散、评测机制缺失这些看起来不炫酷但决定成败的地方。技能是智能体的肌肉记忆,做扎实了,模型才能把聪明用对地方。