很多团队做智能体应用,卡住的地方往往不在模型选型,而在“怎么把能力交给模型”。我见过太多这类项目:提示词写了一长串,看起来什么都能聊,一问到具体业务就东拉西扯;或者API接了不少,模型根本不知道什么时候该调哪个,只能凭感觉乱撞。后来反复调下来的经验就一句话:把能力拆成技能,用一套统一的描述、注册、编排方式来管理智能体的手脚,这个思路圈子里常被叫做agent-skills。这篇东西就是围绕这个思路展开的,适合那些正在把大模型从“聊天Demo”推向真实业务的人,不管是做客服助手、知识问答、内部办公自动化,还是想把已有系统能力开放给模型,都可以参考。
先说清楚,agent-skills 不是一个多么高深的理论框架,它更像一种工程习惯:把业务能力拆成可命名、可描述、可独立测试、可组合调用的“技能”,然后让大模型像人一样,照着技能清单去执行任务。下面我会从设计、定义、注册、编排、评估到排查,把这套做法的完整链路拆开讲,也会把实际项目中踩过的坑、调过的参数一起放进来。
1. 智能体技能化的核心思路
1.1 为什么要把能力拆成“技能”
如果你把所有业务规则都塞进一个系统提示词,等于让一个人在没有SOP、没有工具、也没有明确分工的环境下凭直觉干活。短期对话还行,一旦涉及真实业务链路,比如客服助手既要识别用户情绪,又要查订单、算退款、写安抚语,模型就会频繁出错——中间任何一步理解偏差,后面全崩。
技能化做的事情很简单:把“查订单”变成一个独立技能,它有明确的触发条件、入参、出参、失败返回。模型不需要自己脑补订单系统长什么样,它只需要知道“用户问订单,就去调用查订单技能”。这与后厨分岗是同一个逻辑:大厨不需要亲自买菜、切菜、掌勺,每个岗位有明确SOP,哪个环节出了问题就直接定位到那个岗位。
我在多个模拟项目里试过单一大提示词和技能化两种方案,稳定性差距非常明显。技能化之后,模型不再需要记住每一个业务的处理细节,它只需要学会“选择合适的技能、填对参数、读取返回结果”这三件事。这大大降低了模型的认知负担,也让业务逻辑从提示词里剥离出来,变成可维护的工程资产。
1.2 技能化解决的核心问题
为了看得更清楚,我把单体Promp和技能化这两种方式放在一起做对比,这也是我给别人讲这套思路时最常用的表格:
| 对比维度 | 单体提示词方案 | 技能化方案 |
|---|---|---|
| 稳定性 | 改一处提示词全链路受影响 | 技能独立修改,互不干扰 |
| 复用性 | 换场景需要重写提示词 | 技能一次封装,多处复用 |
| 可观测性 | 模型思考过程不可控,出错难定位 | 技能调用有日志,能定位到具体步骤 |
| 可演进性 | 业务变化只能靠调Prompt | 技能版本升级,不影响全局 |
| 边界控制 | 所有能力都暴露给模型,不可控 | 通过技能白名单控制能力范围 |
这个表做出来之后,团队里讨论方向就清晰多了。技能化的本质不是限制模型,而是给模型一个结构化的工作环境。它让模型从“全能答题选手”变成“会调用工具的调度员”,后者在真实业务里远比前者可靠。
1.3 技能化的适用范围和边界
不过我也得泼一盆冷水:技能化适合的是那些“流程明确、动作可拆、结果可验证”的业务能力。比如查数据、发通知、算价格、改状态,这类动作非常适合做成技能。但如果是纯开放性任务,比如“和用户闲聊”“写一篇营销文案”,把它们做成技能的意义不大,还不如直接用模型原生能力。
技能化真正的价值区间,在于业务动作可以被枚举、被标准化。如果一个能力连团队内部都说不清适用条件,那就不该急着做成技能,先梳理清楚业务逻辑再说。我见过不少团队一上来就建了几十个技能,结果一半连触发条件都写不明白,最后模型调用得乱七八糟,这种“为了技能而技能”的做法是最常见的失败原因。
2. 技能设计的三个关键环节:拆解、命名与边界
2.1 技能拆解的方法论
拆技能是整套体系里最花时间、也最考验业务理解力的环节。我的经验是从用户请求的场景反推,而不是从系统功能往前推。系统功能表往往很细很杂,直接照着拆会把技能数量做得很大;但从用户请求出发,你会发现高频场景就那十几个。
拆解的基本方法有三种,实际项目中通常混着用:
- 按动作拆:“查”“改”“算”“发”“删”,每个动作配一个技能。
- 按信息域拆:客户域、订单域、商品域、库存域,同一个域内的操作放一起管理。
- 按职责拆:接待、业务处理、风险控制,不同职责对应不同的技能集合。
不管是哪种拆法,都要守两个原则。第一是单一职责:一个技能只干一件事。如果一个技能需要传十几个参数,里面塞满了if else,说明拆得太大了,模型很容易在参数填的时候犯迷糊。第二是最小可验证:每个技能都能单独被调起来并验证输出,不依赖其他技能先执行。比如“查订单”技能,如果必须先知道用户ID才能查,那这个拆法就有问题——你应该把“根据会话上下文解析用户ID”也做成独立技能,让查订单技能只负责任务本身。
2.2 技能命名:让模型一眼看懂
技能名的好坏事关重大,因为模型是靠名字和描述来选择调用哪个技能的。我见过最经典的翻车案例是把一个功能命名为workspace_v3,模型完全看不出来这是干嘛的,自然也就不会调用。好的技能名应该是“动词+名词”的直白结构,比如search_user、create_ticket、check_order_status。
命名的另一个细节是加别名。不同模型对中英文的敏感度不一样,同一个技能最好在定义里把常见叫法都列出来,比如查订单技能带上get_order、order_query、查询订单这类别名。这能显著减少模型“明明有技能却不用”的情况。
技能描述文本也不能敷衍。要从使用者的角度写:什么时候用、什么时候不要用、使用前需要知道什么。我习惯用这个模板:
- 触发场景:当用户提到哪些关键词或意图时必须调用。
- 不适用场景:哪些情况千万别调用,避免模型误用。
- 参数要求:每个参数从哪里获取,用户没说全时如何从会话上下文补全。
- 行为约束:这个技能只能做什么,不能顺手做什么。
这个描述就像给模型的产品说明书,写得越清楚,模型调用得越准。很多团队把命名做得很规范,但描述三行完事,结果模型该调的时候不调、不该调的时候乱调,源头就在这儿。
2.3 技能边界与权限控制
技能不是越多越好。技能越多,模型的选择成本就越高,误调概率也会上升。我建议把技能做成目录结构,按业务域分组,模型先在组内做选择,而不是在几十个扁平技能里大海捞针。
安全边界则要前置到技能定义里。对于高风险技能,比如删除数据、发送消息、修改核心配置,必须在描述里加上明确的前置条件。我见过一个团队把“发送营销短信”和“查询用户”放进同一个技能,结果业务方想查一下客户信息,技能顺手就给对方发了条营销短信,用户投诉直接炸了。技能边界一定要做到“最小暴露”:底层API也许能查所有数据,但技能层只允许模型查当前登录用户自己的数据,返回结果也要裁剪,不能把整条数据库记录都丢给模型。模型上下文很金贵,给一堆用不上的字段,既浪费token,又干扰判断。
3. 从定义到注册:让技能真正可被调用
3.1 技能描述文件的核心字段
技能定义最终要落成一个结构化文件,我一般用YAML写,核心字段是这些:
name: check_order_status description: | 根据订单号查询订单的当前状态、物流信息和商品摘要。 当用户询问“我的订单到哪了”“发货没有”“订单状态”时使用。 不要用这个技能修改订单内容,不要用这个技能查询其他用户的订单。 params: - name: order_id type: string required: true description: 用户提供的订单号,如果用户只给了一部分,尝试从会话上下文补全 - name: user_id type: string required: false description: 用户编号,用于权限校验,可从会话上下文中获取 returns: - field: status type: string enum: [pending, shipped, delivered, cancelled] - field: tracking type: array description: 物流轨迹数组,可能为空 - field: summary type: string description: 商品摘要,一句话描述,方便模型转述给用户 version: 1.2.0 examples: - input: {"order_id": "SO202407001", "user_id": "U123"} output: {"status": "shipped", "tracking": [{"time": "2024-07-01 10:00", "location": "转运中心"}], "summary": "手机一部"}这里最容易被忽略的是examples字段。它等于给模型“开卷考试”的答案样式,模型会模仿示例中的返回结构来组织自己的回答。没有示例的技能描述是抽象的,模型拿到返回结果后不知道该怎么转述,经常会漏信息或者自创格式。我给每个技能至少配两个示例:一个是常规情况,一个是边界情况比如空结果。
3.2 技能注册与发现机制
技能定义好之后,要有一个“注册中心”让模型在运行时能发现它们。最简单的做法是把所有技能描述放进系统提示词,但这条路走不远——每个技能描述大约占200到500个token,五十个技能全塞进去,上下文就快挤爆了,主对话的质量会明显下降。
更实用的机制是分组加载。先有一个意图路由层,把用户请求分到对应的技能组,比如订单域、用户域、售后域,然后只把该组内的技能描述加载到上下文中。这就像逛超市,你不需要把整个超市的商品摆在面前,只需要知道哪个分区卖什么,走到对应区域再挑就行。
注册中心还应该承担“健康检查”的职责。技能文件提交上来时,要校验参数格式是否完整、描述里有没有歧义、示例输出是否符合返回结构定义。我在一个项目里就吃过亏:一个技能描述里的示例输出和实际代码返回结构不一致,模型照着示例格式去读结果,读不到字段就自己编数据,那个功能的准确率一度惨不忍睹,最后查了三天才发现是示例和实现脱节。
3.3 技能背后的实现形态
技能描述只是“说明书”,真正干活的是技能背后的可执行程序。常见形态有三种:HTTP API、内部函数、外部插件。最省事的落地方式是把团队已有的系统接口包一层技能描述,不用重写业务逻辑。
但有一个原则必须守住:不要把所有底层接口原样暴露给模型。底层接口往往权限颗粒度很粗,直接暴露相当于把整个系统后门交给模型。技能层要做一次收敛,包括权限校验、参数白名单、返回值裁剪,缺一不可。
我给一个内部函数形态的示意:
def check_order_status(params, context): user_id = context.get("user_id") order_id = params["order_id"] # 权限校验:只能查自己的订单 if not can_access(user_id, order_id): return {"error": "PERMISSION_DENIED"} result = order_api.query(order_id=order_id) # 返回值裁剪,只给模型需要的信息 return { "status": result.status, "tracking": result.tracking_summary, "summary": f"{result.product_name} x {result.quantity}", }这段代码里有几个细节是反复调整之后才确定的。权限校验必须放在最前面,不能等结果出来再判断,否则等于把数据读出来又扔掉,浪费时间和资源。返回值绝不能直接返回整个数据库记录,只挑模型需要转述的字段。裁剪后的返回结构要和技能描述里的returns字段完全对应,否则模型会迷失在字段的海洋里。
4. 技能编排:把单个技能串成完整闭环
4.1 为什么需要编排层
单个技能是零件,完整解决方案是流水线。一个“退款助手”背后通常是这样一串动作:查订单 → 校验是否可退 → 计算退款金额 → 执行退款 → 发送结果通知。如果只把五个技能扔给模型,让它自己排顺序,每次结果都可能不同,这在实际业务里是不可接受的。
所以需要编排层。实现方式有两种:一种是在系统提示词里写明“请依次执行以下步骤”;另一种是用流程编排器把步骤固化成工作流。我个人的判断标准是:低频、变化多、需要灵活理解的场景,用提示词编排,让模型自主安排;高频、固定步骤的业务场景,必须用工作流编排,把顺序固定下来。
编排层的价值在于可回滚、可监控、可人为干预。模型自主安排步骤一旦出错,你很难复现当时它的思路;但工作流编排每一步都有日志,每一步都可以单独暂停、重试、人工修正,这在真实业务里是底线要求。
4.2 两种编排方式的取舍
提示词编排和工作流编排不是互斥的,它们可以同时存在于一个系统里。我用一个表格梳理两者的适用边界:
| 对比维度 | 提示词编排 | 工作流编排 |
|---|---|---|
| 灵活性 | 高,模型可以随机应变 | 低,严格按既定顺序执行 |
| 稳定性 | 不稳定,同一场景可能有不同路径 | 高,每次执行路径一致 |
| 调试难度 | 难,无法精确复现每次决策 | 易,每步有日志可查 |
| 适用场景 | 开放任务、异常处理、多分支决策 | 核心业务链路、固定流程、高合规场景 |
工作流编排的示意可以是这样的:
steps: - skill: check_order_status - skill: check_refundable condition: order_status == "shipped" - skill: calc_refund_amount - skill: confirm_refund require_user_confirmation: true - skill: notify_refund_result固定流程可以减少模型幻觉,但也会降低灵活应变的能力。平衡点在哪?我的做法是把“必须严格”的步骤固化成工作流,把“可以自由发挥”的步骤交给模型,比如给用户解释退款原因的部分,用提示词让模型基于结果自然生成,不固定模板。
4.3 编排中的异常处理
这一步是最容易被忽略的,但恰恰是真实线上最值钱的部分。我总结了三个高频异常场景和对应的处理方案。
第一个是技能返回空结果。模型在拿到空结果时,常常会为了“完成任务”而编造信息。解决办法是让技能出参包含明确的状态字段,比如status为NOT_FOUND或EMPTY,模型拿到这个状态只能走“未找到”话术分支,不允许自由发挥。
第二个是多个技能同时满足触发条件。如果不做干预,模型会随机乱选。解决办法是在技能描述中加入优先级说明,比如明确写“当用户同时需要退换货和维修时,优先使用售后处理技能,再根据用户反馈决定是否调用维修技能”,或者在技能定义里加一个priority字段。
第三个是编排中某个技能超时。解决方案是给每次技能调用设置超时时间,同时在技能描述里注明“如果该技能返回超时,请告知用户系统繁忙,不要自动重试超过一次”。没有这句约束,模型会在技能超时后反复自我重试,把整个链路的耗时拉到不可接受。
5. 技能评估与迭代:数据驱动的技能运营
5.1 评估指标怎么定
技能建好只是开始,后续运营才是决定效果的关键。基础指标包括调用成功率、参数填充完整率、平均调用次数、任务完成率。但更关键的指标是“模型是否在正确场景调用正确技能”,也就是技能选择的准确率与误调率。我习惯用混淆矩阵的思路来看:
- 该调的时候调了,这是真正能完成任务的基础。
- 不该调的时候也调了,这是最需要警惕的误用,往往说明技能描述有歧义,或者技能边界与其他技能重叠。
- 该调的时候没调,说明模型根本没发现这个技能的存在,大概率是描述里的触发场景没覆盖到实际用户用语。
在美国大片里常说的“可观测性”,落实到技能体系里就是日志。每次技能调用都要记录:用户问题原文、模型选择的技能、参数填充情况、返回结果摘要、耗时。没有这些日志,后面所有优化都是盲人摸象。
5.2 建立技能评测集
评测集是技能迭代的守护神。做法是从线上请求里抽一批真实对话,人工标注出“应该调用的技能序列”,形成回归用例集。每次有任何技能定义改动,都跑一遍这个集合,看技能调用序列是否仍然正确。
我常用一个比较轻量的校验方式:用脚本统计“技能序列准确匹配率”,不要求模型的回答逐字一致,只要技能调用的顺序和选择对得上,就算通过。指标达到95%以上再上线,达不到就回滚改动。这个阈值是我在实际项目里反复验证过的,太低会让线上问题频发,太高又会让迭代速度慢到无法接受。
三个典型评测用例分享:第一个是明显应该调用查订单技能的问题;第二个是包含多个条件需要组合调用多个技能的问题;第三个是模糊表述,看模型是会主动追问澄清还是乱猜技能。第三种最能暴露技能描述的质量问题。
5.3 技能迭代与灰度发布
技能描述文件一定要纳入版本管理,和代码一样走分支、评审、合并。改技能时要注意参数兼容性:新增参数要设默认值,删除参数要确认没有其他技能或工作流在依赖它,否则会出现“旧工作流调用新技能时参数对不上”的诡异问题。
线上技能的灰度发布也很有讲究。我建议先让新版本技能覆盖5%的请求,对比旧版本的成功率和用户反馈,观察一两天,如果没有异常再逐步放开到30%、50%、100%。有一次我图省事直接全量上线,结果新技能的参数校验逻辑写得太严,一批线上请求直接失败,排查了一个多小时才定位到问题。从那以后,灰度发布就成了铁律。
还有一条血泪经验:不要在一个版本里同时改参数格式和触发条件。一旦出现问题,你根本不知道是哪一个改动引起的。一次只改一个变量,这是技能迭代最基本的原则。
6. 常见问题与排查技巧实录
6.1 模型总是调用错技能
这是技能化落地后咨询率最高的问题。排查思路按顺序来:先看技能描述是否足够精准,再看技能边界是否和其他技能重叠。一个非常典型的场景是“查订单”和“查物流”经常混淆,因为用户问题里“我的快递到哪了”既可能触发订单查询,也可能触发物流查询。解决办法有两种:要么把两个技能合并成一个,让一个技能同时返回订单状态和物流信息;要么在描述里写清楚边界——“查订单”只回答订单整体状态,“查物流”才返回物流轨迹,再在“查订单”的描述里明确加一句“当用户询问物流轨迹时不要使用本技能”。
6.2 技能返回正确但模型回答错误
这类问题最隐蔽,因为日志里技能调用是成功的,但用户侧看到的回答一塌糊涂。我排查过几次之后发现,根因通常不是模型理解能力差,而是返回结果的结构模型读不懂。比如技能返回一个嵌套很深的JSON,模型根本不知道该取哪个字段来回答用户。解决办法有两个:一是在技能描述里补充每个关键字段的含义,尤其要说明哪些字段可以直接转述给用户;二是在技能内部做一个“结果摘要函数”,把复杂结果直接拼成一句大白话,比如“您的订单已发货,预计今天到达”,让模型拿来就能用。
6.3 上下文太长导致乱调用
这个问题的典型特征是:对话开场还好,聊到第三四轮之后模型开始答非所问,甚至调用完全无关的技能。大概率是技能描述被全量塞进了上下文,占用了大量空间,主对话信息被挤出去了。解决办法很简单:技能描述做精简,examples控制在一到两个;同时严格落地分组加载机制,不相关的技能组不加载。还有一个容易被忽视的细节——技能描述里的版本号、更新时间这类元信息不要放进去,这些对模型没有任何意义,纯粹浪费token。
6.4 技能调试的最小化原则
遇到技能问题,我的第一步永远是把模型完全踢开,直接调用技能本身,输入固定参数,检查它的输出是否符合预期。如果技能本身输出不稳定,上层什么努力都是白费。先确认技能输出稳定,再检查调用参数是否被正确传递,最后才看模型对返回结果的理解对不对。这个顺序不能乱,否则你会在模型幻觉的迷宫里绕很久。
我最后分享一个小工具思路:写一个“技能调用日报”脚本,每天统计每个技能的调用次数、成功率、平均耗时、返回空结果的比例。这个脚本不需要多复杂,但价值极大。它能直观暴露两类问题——有些技能天天被调用但成功率很低,说明技能实现有bug;有些技能从上线的第一天起就没被调用过,说明它根本没有被模型发现,或者已经和现有业务脱节。前者触发技能修复,后者触发技能下线或描述重写。
agent-skills这套思路实践下来,我最大的体会有两点。第一,它不是一个“银弹”,真正困难的地方不是写技能描述文件,而是把业务流程梳理清楚。如果业务团队自己都说不清“什么情况下该做什么”,那就别指望模型能替你搞清楚。第二,别追求一步到位的全自动,务实一点,先让技能辅助人的决策,把单个技能的稳定性做扎实,再考虑让技能串成完整的自动化链路。技能化不是给模型套上的枷锁,而是给模型铺好的轨道——轨道铺得越清晰,模型才能跑得越稳。