最近社区里聊得最多的就是把大模型从“能聊”变成“能干活”,而这一切的落地关键,就在agent-skills这四个字上。你可以把技能理解成Agent的操作手册:模型本身再聪明,如果没法稳定调用工具、读写数据、执行流程,它依然只是个聊天机器人。我过去大半年一直在做技能库的设计与落地,从最初手写十几个if-else串联工具函数,到后来抽象出一套可注册、可编排、可独立测试的技能体系,中间踩了不少坑,也总结出一套直接能用的打法。这篇文章不聊虚的,就讲清楚技能体系到底是什么、怎么拆、怎么写、怎么调,以及在真实业务里会踩到哪些坑。
agent-skills本质上是一层“能力中间层”,它把模型和业务工具解耦开。模型不需要知道每个接口的内部实现,只需要知道“什么场景下该调用哪个技能、传什么参数”,而技能本身负责把模型的意图翻译成可执行的函数调用,再把结果整理成模型能理解的结构化返回。这套设计看着简单,真做起来,从技能粒度划分到参数Schema设计,每一步都有讲究。
1. 内容整体设计与思路拆解
1.1 为什么Agent需要一套“技能”体系
先看一个最原始的场景:你让Agent帮忙查天气并安排行程。没有技能体系时,你会怎么做?大概率是让模型自由发挥,希望它自己调用天气接口、日历接口。但实际上模型很可能把城市参数传错、把天气数据里的温度和风速字段搞混、甚至在用户说“下午出门”时不知道应该查14点到18点的降水概率。这不是模型不聪明,而是它缺少“操作的规矩”。
技能体系的第一个作用是给模型的每次调用立规矩。每个技能都有明确的触发条件、输入参数约束、执行流程和输出格式。模型在决策时看到的不是一堆零散的接口文档,而是几张结构清晰的“技能卡片”,它只需要做选择题:当前任务匹配哪个技能。这大大降低了模型的自由发挥空间,也让错误模式从随机变成可预期。
第二个作用是让能力可以被沉淀和复用。项目做大了之后,你会发现很多操作是跨场景重复出现的,比如“解析用户地址”“格式化金额”“检查库存状态”。这些逻辑如果不抽象成技能,就会分散在多个Prompt里,改一处要动全局。抽象成技能之后,它们就变成了独立模块,任何新的Agent都可以通过“装载技能”的方式获得这些能力,不需要重新写一遍。
第三个作用,也是我后来才深刻体会到的:技能体系是Agent安全性和可观测性的最小抓手。每个技能都是一个受控的入口,调用记录可以被审计,参数可以被校验,执行结果可以被追踪。想让Agent只能做特定范围内的事情,你不需要去约束模型的每一句话,只需要控制它手上有什么技能、每个技能允许什么参数就够了。
1.2 技能粒度怎么定
技能粒度是设计阶段最纠结的问题。定太粗,一个技能里塞了太多逻辑,模型难以准确触发,参数组合爆炸;定太细,技能数量膨胀,模型在决策时面对几十个候选,选择困难,LLM的上下文窗口也吃不消。
我实践下来比较稳妥的粒度标准是:一个技能最好只对应一个完整的用户意图原子操作。什么叫原子操作?就是不需要用户再补充额外信息、不需要依赖另一个技能结果就能完成的最小闭环。“查询天气”算原子操作,“安排行程”就不算,因为它需要先查天气、再查路线、再排时间,这应该是一个工作流(Workflow),由多个技能编排而成。
举个例子。我在一个电商客服Agent里最初设计了“处理售后”这个技能,结果模型经常不知道该传order_id还是refund_id,内部逻辑又长又绕。后来拆成“查询订单”“提交退款申请”“查询退款进度”三个技能,准确率立刻上去了。原因很简单:技能内部的判断分支越少,模型需要做的决策越少,出错的概率自然指数下降。
还有一个经验值分享给你:如果技能描述需要写超过50个字才能讲清楚触发条件,说明粒度多半粗了。技能描述应该像一个精准的电梯演讲,几秒钟就能让模型明白我在什么时候该用你。这个标准我几乎用在了所有项目上,都很灵。
2. 核心细节解析与实操要点
2.1 技能声明与参数设计
技能声明的格式直接决定了模型能不能读懂你的技能。现在主流Agent框架普遍采用类函数描述的方式,一个技能声明包含四部分:技能名称、功能描述、参数Schema、执行目标。其中最重要的是参数Schema和功能描述,这两个地方写不好,后面全盘皆输。
先看参数Schema。大模型本身不擅长解读嵌套过深的JSON结构,你给它一个包含三层嵌套、类型模糊的参数字段,它大概率会传错。我在设计时坚持几条原则:
- 尽量使用扁平结构,不要超过两层嵌套。
- 所有参数必须声明类型和取值范围,比如
city要明确是城市中文名,而不是经纬度;date要明确是YYYY-MM-DD格式。 - 必填参数控制在三个以内,超过三个,模型漏传的概率大幅上升。如果确实需要很多信息,考虑拆成多个技能,或者引导用户说清楚后再触发。
- 参数名用业务语义直给的词,不要用缩写。比如用
delivery_address而不是da,用preferred_time而不是pt。
功能描述同样大有讲究。很多开发者喜欢把描述写成“该技能用于处理查询天气的需求”,实际上这是句废话。好的描述应该包含触发场景、前置条件、执行结果、边界说明。比如天气查询技能的描述,至少明确:
当用户询问某城市当前天气、未来天气预报、气温、降水、风级等气象信息时,应调用本技能。用户未指明城市时,技能应使用上下文已确认的城市,不得自行猜测默认城市。执行后返回城市名、日期、天气现象、最高最低气温、降水概率。
注意那句“不得自行猜测默认城市”,这类负面约束在描述里特别重要。模型天生倾向于“脑补”,你明确划出红线,它才能老老实实向你确认。
2.2 技能描述与上下文注入的取舍
技能描述写得再完善,如果全部塞进上下文,一次要占掉几千Token。模型上下文窗口有限,几十个技能全量注入,双方的预算都会爆炸,直接在真实场景里会因为互相覆盖而参数串台。
我实测过一组对照数据:全量注入还是按策略注入,同为查询库存与物流信息两个技能,后者在复杂场景参数准确性提升了大约两到三成。原因也简单:按需注入让模型在每个决策点只看到少量相关技能,决策面窄,反而更稳。现在我的做法是三段式路由:
第一段,用一次轻量分类请求,让模型判断当前请求属于哪个技能域,这个请求只注入技能名称和一句话描述,开销极小。
第二段,根据分类结果,只加载对应域内两到三个技能的完整描述、参数Schema和示例,让模型做具体决策。
第三段,如果分类置信度低或参数缺失,额外注入一次“澄清引导”指令,让模型主动向用户追问缺失字段,而不是硬着头皮瞎传。
这样整体注入量从全量时的几万个Token压缩到每次几千Token,响应速度提升非常明显。尤其是做线上业务时,每轮调用的延迟直接关系到用户体验,这个优化很值。
2.3 技能命名和归类规范
命名这事,看似是小问题,实际在模型决策时影响很大。大模型对特征的识别是基于语义空间的相似度,如果你的技能名称和别的技能描述相互重叠,它在触发时就会徘徊不定。所以命名要遵循一个原则:名称本身就能独立表达技能的独特语义。
我处理过一个典型的冲撞:原来同时存在“查询订单进度”和“查询物流信息”两个技能,模型经常在两者之间犹豫,甚至出现查订单时返回物流信息的情况。后来把名称改成了“查询订单支付与处理状态”和“查询包裹配送轨迹”,并同步调整了描述,冲撞立刻缓解。因为新名称在语义边界上划分得更清晰:一个管订单状态本身,一个管实物配送环节。
归类方面,建议在技能声明里增加category字段,方便做技能域的聚合管理。比如电商域、出行域、营销域,这样在做路由决策时可以直接按域去检索,效率会高很多。还有一个不易察觉的细节:技能名称不要用过于通用的动词,比如“查询”“获取”“处理”这类词几乎出现在所有技能里,模型区分起来很费劲。尽量用名词性短语,把对象在名称里点出来。
3. 实操过程与核心环节实现
3.1 搭建一个最小技能模块
理论说了不少,下面来点实际的。我们先从一个“查库存”技能看起,这个技能足够小,也足够通用。目标是让Agent只凭一句“这个商品还有货吗”,就能准确调用技能,并拿到结构化的库存结果。
# 最小技能模块示例 { "name": "商品库存查询", "category": "订单_库存", "description": "当用户询问指定商品是否有货、当前库存数量、可售状态等情况时触发。触发前提为:用户已明确指出商品ID或提供可明确对应商品的链接/名称。不得用猜测的商品ID查询,商品ID缺失时返回提示并要求用户补充。", "parameters": { "type": "object", "properties": { "sku_id": { "type": "string", "description": "商品的SKU ID,必须是系统内存在的标识,例如SKU20240101" } }, "required": ["sku_id"] }, "handler": "fetch_stock_by_sku" }这段声明描述里,我特意加了触发前提,并明确“不得用猜测的商品ID查询”。这样模型在用户没说具体商品时,就不会自作主张地猜测后乱调,而是主动追问。这是经验里最实用的一招:把临界命令写在描述里。所谓临界命令,是指用户输入不满足触发条件时,模型应该怎么办。默认情况下模型可能会强行调用,你不如提前替它想好路径。
然后是handler字段,它指向一个实际的业务函数。这个函数只需要做一件事:接收声明里的参数,去查数据库,返回结果。它不关心上下文,也不处理对话逻辑,保持纯粹的执行身份,后续才能被各种上游复用。接着是结果返回格式,我长期用的模板是{ "status": "ok", "data": { "sku_id": "...", "available": true, "stock": 23 } }。用available这类布尔值做第一层摘要,data里放明细,既能被后续流程读取,也可以直接拼装成用户可读的答复。
3.2 技能注册与动态装载
技能模块只是一个静态声明,要让Agent跑起来,还需要把它们注册到一个统一管理的技能仓库里。我推荐的模式是:每个技能对应一个文件,按域建目录,启动时全量扫描注册。这样新增技能不需要改任何主流程代码,塞个文件进去就能生效。
# 技能仓库的注册与装载逻辑(伪码示例) SKILL_REGISTRY = {} def register_skill(skill_definition): key = skill_definition["name"] if key in SKILL_REGISTRY: raise DuplicatedSkillError(key) SKILL_REGISTRY[key] = skill_definition def load_skills_from_directory(skills_path): for domain_dir in os.listdir(skills_path): for skill_file in glob.glob(f"{skills_path}/{domain_dir}/*.json"): with open(skill_file) as f: skill_definition = json.load(f) skill_definition["category"] = domain_dir register_skill(skill_definition) def retrieve_skills(query, top_k=2): # 基于嵌入检索或简单关键词匹配,召回最相关的技能 scored = [] for skill in SKILL_REGISTRY.values(): score = compute_semantic_similarity(query, skill["description"]) scored.append((score, skill)) scored.sort(key=lambda x: x[0], reverse=True) return [skill for _, skill in scored[:top_k]]动态装载的另一个好处是热更新。线上业务往往需要临时下线某个出问题的技能,或者灰度测试一个新技能。在仓库模式下,只需要把对应文件改名或标记disabled,在下次装载时就不再生效。早期我把技能写在Prompt模板里,每次调整都要重新发布整个Agent服务,一次迭代半小时起步。改成独立文件注册后,改个描述、换个函数,五分钟就能生效,开发和联调效率完全是两个量级。
3.3 技能执行的完整链路
有了技能声明和装载机制,还要打通从用户请求到技能执行的完整链路。我的链路分五步,每一步都有明确的数据约定:
第一步,用户输入标准化。把用户原始语句统一转成{ "query": "...", "history": [...] }结构。第二步,技能路由。用第一次模型调用或规则匹配,选出最相关的技能域,并加载技能描述。第三步,参数提取。这一步是模型调用,核心是把用户语句映射成技能参数JSON。第四步,参数校验与执行。校验不通过就返回错误码,由框架决定是追问还是走兜底;校验通过则调用handler,拿到结构化结果。第五步,结果转述。把结构化结果交给模型,用口语化表述回复用户。
这里最容易出问题的是第三步。参数提取时模型容易受聊天历史干扰,比如用户之前说过一个城市,现在问别的事,模型提取参数时可能错误带入上一次的实体。我在做这一步时,会在提取Prompt里显式加一句:仅基于当前用户消息提取参数,不要在历史对话中推测信息。同时,提取结果必须输出严格的JSON,任何多余的文字都会导致解析失败,所以我会在解析层加兜底:如果解析失败,就启用一个基于规则的实体识别模块作为降级方案,保证链路不至于中断。
4. 常见问题与排查技巧实录
4.1 模型“想不起来”用技能
你明明把技能写得清清楚楚,但模型就是在该调用的时候不调用,这几乎每个做Agent开发的人都遇到过。排查时不要直接怀疑模型能力,先看看你的检索环节召回了什么。早期的调试经历里,我遇到最多的情况是:top_k设置太小,真正相关的技能没有被召回;或者候选技能语义太接近,模型在两者之间徘徊之后干脆不调用了。
给一个实用经验:检索结果返回给模型时,不仅给技能描述,还要给每个技能附上一到两个典型触发例句。比如库存技能附带“这个还有货吗”“现在下单几天能发”这样的例句,模型看到例句后能更快理解这个技能对应什么样的用户表达,触发率明显提升。这个技巧实践下来效果很显著,属于低成本高收益的优化方向。
还有一种情况是上下文污染。如果历史消息里有大量无关的对话,模型容易被带偏,忘了手边有工具可用。我的做法是在每次路由决策前,先裁剪历史,只保留与当前请求相关的对话片段,并把“你有以下技能可用”这句话放在上下文最靠后的位置。让指令靠近上下文尾部,模型通常更容易注意到,同时直接削减不必要的历史噪声。
还有一个偏门但有效的思路:如果模型始终不用某技能,不妨检查一下技能名称本身是否过于抽象。我遇到过“获取用户偏好画像”这种命名,语义空间太大不够直观,改成“读取用户历史偏好标签”并补充具体标签示例后,调用率一下子上来了。越是抽象的名字,模型越不知道它到底能干什么,宁可名字长一点,也要把对象和动作说清楚,好让模型明白什么场景该参考它。
4.2 参数幻觉与非法取值
模型传了不该传的值,这是比“不调用”更头疼的问题。比如商品SKU明明不存在,模型还能编一个出来;日期格式不符合业务预期;把“价格”字段传成了字符串。要解决这个问题,单一靠Prompt描述远远不够,必须做三层防御。
第一层是Schema约束,在参数Schema里定义好类型和枚举值,凡是不符合的直接过不了基础校验。第二层是业务校验函数,比如SKU是否真实存在于商品库中,日期是否在有效期内,这些要交给实际代码去判断,不能依赖模型自觉。第三层是语义兜底,如果校验失败,框架自动触发一次澄清追问,让用户补充或确认,而不是把错误参数一路传递下去。
我特别想强调第二层。模型产生幻觉是概率性的,你无法通过调Prompt让它彻底不犯。但从“不可信”变为“不可执行”,框架层面就能拦截大批坏数据,既避免脏数据污染后续业务,也保护了业务流程不被带偏。执行器里我通常还会加一个超时和重试机制,超时后返回一个明确的超时错误,避免模型把旧的失败记录当作成功结果继续向下游传递。
4.3 技能冲突与优先级处理
当技能数量超过三十个,冲突几乎不可避免。常见冲突是不同域的两个技能描述里都提到了“订单”,模型分不清该调哪个。这种情况下不要急着调整描述,先检查是不是技能粒度过粗或者复用逻辑没抽象干净,必要时通过业务规则做一层路由偏好。
我习惯在技能定义里加一个可选的priority字段,规定在触发器重叠时优先使用哪个技能。但这个手段只能解决已知冲突,解决不了模型自己判断时左右摇摆的问题。更干净的做法是:把容易混淆的技能做一次归并。比如把“查询订单支付与处理状态”和“查询包裹配送轨迹”统一到一个“订单全链路查询”技能下,通过内部的子步骤判断用户真正想看什么。这样表面上少了一个技能,实际上因为分工清晰,整体稳定性和准确率反而更高。
另一个常见冲突来自共用参数的表达不一致。比如一个技能用order_id,另一个技能用order_sn,模型即使识别出该用订单查询,也无法确认到底传哪个字段。统一术语表在技能体系里是必需品,所有跨技能引用的概念必须叫同一个名字。我在项目初期吃过这个亏,两个域各自开发,等到联调时才发现参数名对不上,后来专门做了一次数据字典的统一,这个问题才算彻底解决。
5. 更多经验与扩展方向
5.1 技能的一次性验证与回归测试
技能体系做得越成熟,就越需要一个自动化测试机制来兜底。每次新加或修改技能,都不能只靠人工抽几条数据试一下就上线。我采用的模式,是把历史真实请求与期望调用技能组成回归集,每次改动后跑一遍这些用例,把“应该触发哪个技能、提取哪些参数、返回什么格式”作为断言标准,智能体实际执行结果与标准比对。
这套回归测试机制在长期迭代中价值非常大。因为技能的改动能引起几十个既有场景的连锁反应,有时候只是改了一句描述,某个边缘case的触发路径就完全变了。没有回归测试傍身,你根本不敢动老技能。而有了它,改起来心里就有底,测完再上线。
还有一个实操细节:每次跑回归测试时,把模型回答里关于技能选择和参数提取的中间步骤记录下来,可以留存成一份决策轨迹。模型出错时,这份轨迹能直观地看到它是如何走到错误结果的,是检索环节遗漏了技能描述,还是参数提取时被历史噪声干扰,大部分问题都能一眼定位到具体环节,而不是对着黑盒反复猜原因。
5.2 技能的可观测性与数据回流
技能上线之后,日志里暴露出的数据要持续反哺优化。我比较关注四个指标:触发准确率、参数提取通过率、执行成功率、兜底触发次数。前两个直接反映模型对技能描述和Schema的理解程度,第三个反映业务链路稳定性,第四个则能暴露出技能覆盖的盲区。
举一个真实例子。某个客服Agent的退款技能上线后,执行成功率稳定,但触发准确率偏低。查日志发现,用户说“退货”时模型经常触发成“退款”,把语义上相近的两种行为搞混了。后来在技能描述里分别补充了最典型的用户表达,并给“退货”技能加了触发优先级,再跑回归测试,触发准确率直接上了一个台阶。如果没有这类数据回流机制,这个问题可能要在线上潜伏好久,而单靠日常人工体验根本注意不到触发准确率的细微变化。
技能库的数据回流还有一个用途,就是反哺检索模型。凡是发生过错误触发或未触发的样本,都值得人工复核后放进检索训练集。你的召回向量会越用越准,技能路由的自然语言匹配能力也会随之持续变强,形成一个逐渐积累的收益。
5.3 从技能到工作流
单个技能解决的是“怎么执行一个动作”,但真实的业务场景往往是多个动作的连续编排。比如“处理售后”需要一个完整的工作流:查询订单、判断售后类型、生成处理方案、提交审核、通知用户。这五个环节由五个技能完成,但它们之间的衔接顺序和状态流转,已经超出了单个技能的职责范围。
我的做法是引入轻量级的工作流编排层,把技能当作工作流里的节点。每个节点定义输入来源和输出去向,上一个技能的结构化结果自动映射为下一个技能的参数。这样既保证了技能可以被单独复用,又不至于让一个技能承担过多逻辑。你现在设计技能时,不用一下子想得太深,但当技能数量到达一定规模,工作流编排几乎必然是你下一步的演进方向。这里节奏最好踩得稳一点:先把每个技能守住,把调用链跑通,再考虑上工作流。我在实际操作中最大的感受是,工作流如果建立在基础不够稳的技能体系上,调试成本会成倍上升,所以基础打磨值得多花时间。
最后再分享一个调整节奏的小技巧:当你把一套技能写完之后,不妨试着找个完全不懂背后系统的朋友试用一下你的Agent,看用户用日常口语提出需求时,系统是否能准确触发对应技能。很多技能设计上的“死角”只有换了表达方式才能暴露出来。这种体验式测试做上几轮,你打磨技能的方向感会清晰很多。