最近不少做智能体应用的朋友都在聊同一个话题:模型本身的能力边界已经摸得差不多了,真正让 Agent“变聪明”的,其实是挂在它身上的那一堆技能。刚好我最近在梳理自己的 agent-skills 项目,把一年多来踩过的坑、沉淀下来的设计习惯都整理了一遍,这里直接拿出来分享,希望能给正在做同类项目的你一点参考。
先说清楚这个项目是干什么的。agent-skills 不是一个具体的业务应用,而是一套面向智能体(Agent)的“技能库”设计与实践方案。它解决的核心问题是:当你有一个大模型作为大脑,你怎么让这个大脑稳定、可靠、可维护地去调用外部工具、执行具体任务。换句话说,它是一套让 Agent “手脚麻利”的基础设施。
这篇文章适合三类人:一是正在用大模型 API 开发智能体应用的个人开发者;二是团队里负责 Agent 工程化、需要设计工具调用规范的工程师;三是想理解“技能系统”到底是什么、怎么落地,但被各种概念绕晕的产品和技术负责人。文章不会堆砌抽象概念,而是从设计思路到具体代码,再到问题排查,尽量完整地还原一套技能系统的搭建过程。
1. 先把概念说透:技能(Skill)到底是什么
1.1 技能和函数调用不是一回事
很多人一看到 agent-skills,脑子里第一反应是“这不就是 function calling 吗”。说实话,我最早也是这么理解的,但做着做着就发现,这两者在抽象层次上有本质区别。
函数调用(Function Call)是模型 API 提供的一种机制:你在请求里声明一批函数,模型根据用户输入决定调用哪个、填什么参数。它是“一次性”的、面向单次交互的。而技能(Skill)是更高一层的概念:它是一组能力封装,包含函数实现、调用描述、参数约束、前置条件、错误处理策略、甚至对应的测试用例。技能是可复用、可版本化、可组合的单元。
举个例子。给模型暴露一个 get_weather 函数,这叫函数调用。但如果你定义了一个“天气查询技能”,它不止包含 get_weather 函数本身,还包括:
这个技能的适用场景描述(“当用户询问当前天气、未来预报、穿衣建议时使用”)、参数约束(城市名必须标准化、时间格式必须为 YYYY-MM-DD)、异常处理(查不到城市时返回模糊建议而不是报错)、以及一组验证用例(“北京今天天气怎么样”应触发该技能)。
这两者的差别,决定了项目后期是“能跑”还是“好维护”。函数调用解决的是“模型能不能调用工具”的问题,技能系统解决的是“工具能不能被正确、稳定、规模化地调用”的问题。agent-skills 这个项目的核心工作,就是把后者做实。
1.2 从“让模型干活”到“让模型会干活”的转变
我见过不少初期的 Agent 项目,把十几个函数一股脑塞进 System Prompt,结果模型要么选错函数,要么参数填得乱七八糟,要么干脆一个都不调用。问题不出在模型智商上,而是出在“你只是给了模型一堆工具,但没有给它一套规范的技能使用手册”。
技能系统本质上是把“干活的能力”和“干活的方法”解耦。模型不需要知道某个技能内部的实现细节,只需要知道:什么场景下该用什么技能,用的时候需要提供什么信息,技能执行后返回什么结果。
把这个思路落到实处,技能的定义就不能只是一个函数签名,得是一份完整的“技能说明书”。我在项目里用结构化格式来定义技能,通常包含以下几个字段:
name: weather_query description: 查询指定城市在指定日期的天气情况,包括温度、天气现象、风力等 version: 1.2.0 scenarios: - 用户询问某个城市的实时天气 - 用户询问未来几天的天气预报 - 用户询问穿衣建议、出行建议 parameters: city: type: string required: true description: 城市中文名,如“北京”,不接受拼音或缩写 date: type: string required: false description: 日期,格式 YYYY-MM-DD,默认为今天 returns: - 当前温度 - 天气现象 - 风力等级 errors: - 城市不存在时,返回相近城市建议 - 日期超出预报范围时,返回可查询的最远日期你可能会问,把这些信息写得这么详细,值得吗?值得,而且越早开始这么做越值得。因为技能一旦多起来(超过 20 个),模型的选择准确率就会直线下降。而一份好的描述,就是模型做判断的核心依据。我自己的实测数据是:对技能描述做精细化改写后,在 30 个技能的场景下,模型首次选对技能的概率从不到 70% 提升到了 88% 左右。
2. 技能系统的核心设计:注册、描述与路由
2.1 技能注册中心:所有技能的“户口本”
技能多了以后,第一个要解决的问题是:系统里到底有哪些技能?每个技能现在是什么版本?谁在负责维护?这就是技能注册中心的作用。我把它设计成一个集中式的技能配置,用一个 JSON 文件维护所有技能的元信息和启用状态。
{ "skills": [ { "name": "weather_query", "description": "查询城市天气", "version": "1.2.0", "enabled": true, "owner": "team-weather", "tags": ["life-service", "public"] }, { "name": "todo_list_add", "description": "添加待办事项", "version": "2.0.1", "enabled": false, "owner": "team-productivity", "tags": ["productivity"] } ] }注册中心存在的意义,不只是记录,更重要的是支持“动态加载”。当 Agent 启动时,它会读取这份注册表,筛选出 enabled 为 true 的技能,生成当前可用的技能列表。这样做的好处是:你可以在不停机的情况下,通过修改注册表来启用或下线某个技能。比如某个技能的上游接口不稳定,你直接在注册表里把 enabled 改成 false,Agent 下次请求就不会再带上这个技能,避免了模型调用一个“已知不可用”的工具。
还有一个实战建议:注册中心里一定要保留“最近一次调用时间”和“平均调用耗时”这两个字段。它们不是用来展示的,而是用来做技能健康度评估的。我有一段时间发现某个技能被模型高频调用,但业务方反馈效果不好,一看指标才发现是这个技能描述写得过于宽泛,导致很多本不该走它的请求都走了它。把描述收紧之后,调用量降了一半,但成功率明显提高了。
2.2 技能描述:决定模型选对选错的关键
技能描述是技能系统里投入产出比最高的优化点。模型通过语义匹配来决定调用哪个技能,描述写得好不好,直接影响调用准确率。结合我的实际经验,写技能描述有几个关键技巧。
第一,从用户意图的角度来写,而不是从函数功能的角度来写。比如一个搜索技能,不要写成“执行一次搜索”,而要写成“当用户想要查找信息、了解资讯、确认事实、寻找答案时使用该技能”。前者是描述动作,后者是描述场景。模型更擅长匹配场景而不是动作。
第二,写清楚负面场景,也就是“不要用它”。这个非常反直觉,很多人会忽略。例如天气查询技能,描述里可以明确加一句“该技能仅用于查询天气,不用于查询空气质量、地震信息等”。就是这个补充,让模型在相近意图上少犯很多错。我在技能描述模板中专门加了 negative_scenarios 字段,实测调用准确率能再涨 3 到 5 个百分点。
第三,参数描述同样重要。模型不仅要知道技能是干什么的,还要知道参数怎么填。很多调用报错不是因为技能选择错了,而是参数填错了。参数描述要说明格式、示例、边界情况。比如日期参数,要写清楚“格式为 YYYY-MM-DD,例如 2025-03-18”,否则模型可能给你传一个“明天”或者“2025年3月18日”这样的自然语言值。
2.3 技能路由:让调用过程可追踪、可控制
技能系统的调用链路,不能只是“模型直接调用函数”。中间应该有一层路由模块,负责做三件事:记录每次调用的完整日志、校验技能参数合法性、在异常情况下做降级或重试。
我实现的技能路由模块是这样的:模型先输出结构化结果(包含技能名和参数),路由模块解析之后,先做一次参数校验,不通过就直接返回给模型一个修正提示,要求重新填参;通过之后,再执行技能,并记录完整的调用日志,包括请求参数、耗时、返回值、错误信息。
为什么要多这一层?因为直接让模型调用函数,出错了你都不知道错在哪。加了路由层之后,每一个调用环节都有日志,排查问题时效率翻倍。有一次我排查一个“技能偶尔不返回结果”的问题,查了日志才发现,是某个技能在并发超过 5 个请求时会超时,而单次测试是测不出来的。没有日志,这种问题只能靠猜。
3. 技能从开发到上线的完整实操流程
3.1 第一步:从零搭建一个技能原型
一个技能从想法到上线,正常需要走完五个步骤:需求分析、原型开发、联调测试、灰度发布、全量上线。我以一个“今日热榜查询”技能为例,演示完整流程。
需求背景是:用户会问“今天有什么热门话题”“大家都在看什么”,需要提供一个能获取各大平台热榜信息的技能。接到这个需求后,我先写了一份一页纸的技能定义,核心内容包括:技能名称为 hot_list_query;触发场景是用户询问热搜、热榜、热门话题;参数是平台名称和榜单类型;返回结果是榜单标题列表和热度值。
原型阶段,用 Python 实现最简版本。这里的关键是先把函数跑通,不追求完美。
def hot_list_query(platform: str, list_type: str = "default"): # 伪代码:对接平台提供的热榜数据接口 data = fetch_hot_list(platform, list_type) items = [{"title": item["title"], "hot": item["hot_value"]} for item in data["items"][:20]] return {"status": "success", "data": items, "update_time": data["update_time"]}原型写完后,马上做的是“模型调用测试”。把技能描述和参数 schema 配置进 Agent,然后用一批典型问题去测:包括“今天热搜是什么”“微博上有啥热门”“帮我看看某平台的热榜”。跑下来之后,你会发现模型对平台名称的表述五花八门,有的说“微博”、有的说“weibo”、还有说“渣浪”的。这就是参数约束要发挥作用的场景:在参数描述里明确“平台名称必须是官方名称,不接受别称、昵称或缩写,如微博不可写为 weibo 或渣浪”。
3.2 第二步:技能测试的三种级别
个人开发者和团队协作有个明显区别:个人可能跑几个例子就算测完了,团队则必须建立三级测试体系。
第一级是单元测试,直接测技能函数本身。不考虑模型,手动传入构造好的参数,验证函数对正常输入、边界输入、非法输入的处理是否符合预期。这个过程能发现很多低级 bug,比如日期格式解析错误、空列表处理不当、接口超时没加异常捕获。
第二级是模型调用测试,也叫技能选择测试。用一批覆盖不同意图的语料去跑 Agent,重点验证两件事:该调用的技能是否被正确调用;不该调用的技能是否被错误调用。我习惯把语料分成正样本和负样本。正样本是应该触发该技能的语句,负样本是相似但不应该触发的语句,比如“今天天气怎么样”就不应该触发热榜技能。每轮改动之后跑一遍回归,看准确率变化。
第三级是端到端测试,模拟真实用户完整对话。这不仅是测技能本身,还是测多轮对话中的技能组合。比如用户先问“今天有什么热搜”,然后追问“第一个话题详细讲讲”。这个追问需要触发另一个“内容查询”技能,同时要能拿到上一轮结果中的话题 ID,这就需要对话状态管理模块和技能系统联动。
三级测试补全之后,才算真正达到上线标准。否则你只是验证了“技能本身没问题”,完全没验证“技能放到 Agent 里也没问题”。
3.3 第三步:版本管理与灰度发布
技能上线之后,维护才刚刚开始。我在 agent-skills 项目里把技能当独立服务来管理,每个技能都有版本号、变更记录、回滚方案。
版本管理方面,我使用的是语义化版本号:主版本号变化意味着接口不兼容(参数变了、返回结构变了),次版本号变化意味着向下兼容的功能改动(新增描述、增加参数可选值),补丁版本号则是修 bug 或优化描述。
灰度发布是吃过亏后才补上的环节。之前有一次,我更新了一个技能的描述,让模型更准确地选择它,结果上线后模型确实更频繁地调它了,但它调用的成功率反而下降了,原因是上游接口扛不住突增的流量。那次事故之后,我就给技能系统加了灰度配置:注册中心里增加一个 rollout 字段,值为 0 到 1 之间的小数,表示该技能对多少比例的请求生效。灰度期间只放 30% 流量,观察调用成功率、平均耗时、用户反馈三个指标稳定后,再逐步放大到 100%。
4. 常见问题与排查技巧实录
4.1 模型就是不调用某个技能,怎么办
这是被问得最多的问题:技能已经配置了,但模型就是不用它。排查思路按优先级排列:
第一步,检查技能的 enabled 状态。听起来简单,但真的有人在这个问题上耗了半天。注册表更新之后没重启服务,或者配置中心缓存没刷新,都会导致技能实际上是“离线”状态。
第二步,检查技能描述是否和常见表达“语义距离”太远。如果用户习惯说“帮我看看今天有啥大事”,但你的技能描述写的是“热搜查询”,模型可能匹配不上。建议把描述改成更口语化的表达,同时补充 3 到 5 个典型触发问法作为示例。
第三步,检查是否被其他技能“抢占”。技能选择本质上是一个多分类问题,如果两个技能描述相近,模型会出现误选。解决办法就是拉开技能描述的区别度,或者在描述里明确写清楚你负责什么、不负责什么,让模型能分清楚边缘场景。
第四步,如果上面都排查了还是不行,可以临时把模型推理参数里的温度调低一些,让模型更倾向于确定性输出,而不是发散性选择。我在项目中实测过,把 temperature 从 0.7 降到 0.3,技能调用准确率有明显提升,因为模型的调用决策变得保守了。
4.2 参数幻觉:模型经常填错参数
模型输出了一个本不存在的参数值,或者参数值的格式不对,这就是参数幻觉。比如城市名传了“Beijing”而不是“北京”,日期传了“3月18日”而不是“2025-03-18”。
针对这个问题,我做了一件事:给参数加一层“标准化清洗”。在技能路由层拦一道,做格式校验和值映射。比如城市参数,建立一个常用别名映射表,把 Beijing、BJ、帝都 都映射为“北京”。日期参数则用专门的解析函数,把“明天”“后天”“3月18日”这类自然语言表达解析成标准日期字符串。
更深一层的做法是调整参数 Schema,让它更“严”。可选值用枚举就不用字符串,能用结构化对象就不用自由文本。比如平台名,直接在 schema 里限制为一个枚举列表,模型就没法自由发挥了。
| 问题现象 | 可能原因 | 排查顺序 | 解决办法 |
|---|---|---|---|
| 模型不调用技能 | 技能未启用/描述不清晰/被其他技能抢占 | 1.enabled 状态 2.描述 3.竞争技能 | 检查注册表、改描述、加负面描述 |
| 参数格式错误 | schema 约束过松/模型自由发挥 | 1.schema 检查 2.清洗层 | 用枚举、加值映射、加正则校验 |
| 调用结果不稳定 | 技能接口波动/并发上限 | 1.日志检查 2.上下游状态 | 加超时重试、灰度降级、限流 |
4.3 技能返回结果“答非所问”
还有一种典型问题:技能确实被调用了,参数也正确,但返回结果和用户的问题对不上。
这类问题大多数情况不是技能系统的锅,而是技能返回的结果“不够结构化”。比如热榜技能,如果直接返回一大段 JSON,模型会把 JSON 原文输出给用户,而不是做归纳和总结。解决办法是让技能返回结果更接近“已经整理好的人类可读文本”,或者至少是经过裁剪的摘要。
我在设计技能返回格式时确立了一条原则:技能尽量返回“结论”,而不是“原料”。如果技能查询到的数据是 100 条,不要让模型去筛选,而是在技能内部就先排序、截断、提炼,只返回 Top 5,并附上一句简单的话。这背后的逻辑是:大模型的上下文窗口是宝贵的,你给它太多噪音,它反而提取不到关键信息。
4.4 技能系统的性能瓶颈在哪里
技能系统的性能瓶颈主要是两个:一个是技能数量,一个是描述长度。每增加一个技能,模型在调用决策时就需要多比较一个选项,计算量和决策难度都会增加。每增加一段描述,Prompt 的 token 数也会增长,直接影响每次请求的延迟和成本。
我的实践经验是:技能数量尽量控制在 30 个以内。超过这个数,模型的调用准确率会明显下降。如果确实需要更多技能,可以考虑做“两级技能路由”:第一级只暴露一组“分类召回”技能,识别用户意图属于哪一类,再针对性地加载该类下的具体技能。这相当于把大分类问题拆成了两个小分类问题,准确率反而更高。
还有一个常被忽略的优化点:动态裁剪 Prompt。不是所有技能在每次对话中都需要暴露给模型。比如用户第一句问的是天气,后面整个对话大概率都在聊天气,没必要把 30 个技能全部放进上下文。我在项目中加了“技能预热”机制:根据当前对话的话题,动态决定下一次请求携带哪些技能,既能保证调用准确率,又能减少 token 消耗。
5. 从个人项目到团队协作:技能库的演进路线
技能系统做到后面,不再只是“技术问题”,更多是“协作问题”。人一多,技能的命名、描述风格、参数规范就容易被搞乱。我在实践后期逐渐沉淀出一套协作约定。
技能命名统一采用“领域_动作_对象”的格式,如 weather_query、todo_add_item、news_get_detail。命名规则的好处是规范清晰、层次分明,也方便注册中心做前缀分类和权限管理。
描述由技能负责人来写,而不是让写代码的人顺便写。这个看起来是流程问题,实际影响很大。因为写好技能描述,需要很清楚地了解用户可能的表达方式和模型的匹配习惯,这是专门的技能工程能力。动手写代码的人,最熟悉接口如何调,但往往不太熟悉用户的自然语言表达。
最后,技能库的演进一定要有“回顾机制”。我每个月会做一次技能健康度盘点:哪些技能被高频调用,哪些低效甚至几乎不触发;哪些技能描述改了反而变差了,哪些技能该合并、该拆分、该下线。把技能库当成一个持续迭代的产品来经营,而不是写完就扔的工具箱。
6. 写在最后:一点实践感悟
如果你正在搭自己的 agent-skills,我最大的建议是:别急着写一堆技能,先把手头最常用的三五个技能做深做透。
技能系统的复杂度是随着技能数量线性上升的,但很多坑要等到规模上来才会暴露。一开始就设计好注册中心、技能描述规范、日志记录、测试用例模板,后面扩量会顺很多。如果一上来就闷头写工具函数,等到技能多了再回头补规范,那才叫苦不堪言。
我回顾自己这段经历,最值钱的不是最终的代码,而是那套从踩坑中总结出的原则:技能的本质是把模型的能力边界变成可管理的产品边界;描述质量决定智能体能力的上限;可观测性是技能系统的灵魂;协作规范决定了技能库能走多远。这些原则,比任何单点技术实现都更持久。希望这篇梳理能给你的 agent-skills 项目带来实际帮助,踩坑少一点,上线快一点。