
1. agent-skills 到底是什么为什么现在就要开始做1.1 从一个真实的尴尬场景说起上周有个朋友给我看他的 Agent 项目说是已经接入了大模型也能跑通几个工具调用。代码里密密麻麻堆着几十个函数什么get_weather、send_email、calc_price、fetch_user_info。乍一看功能齐全但一跑起来就出问题模型明明需要查库存却调用了算价格的函数同一个登录态逻辑在三个函数里各写了一遍改了一下数据库字段名所有工具函数全部报错。他最痛苦的是想加一个新能力总得翻半天代码生怕动一处崩一片。这个问题本质上不是“工具不够多”而是“技能没有体系化”。我在做 Agent 落地时也有过同样的经历后来花了大力气把所有零散函数重构成一个可插拔的技能库思路就是agent-skills这个方向。简单说agent-skills不是某一个具体功能而是一套面向 AI Agent 的“技能管理方案”——把原本散落在代码里的工具函数升级成有规范、有描述、有输入输出协议、可独立注册和调用的技能单元让模型能更准确地按需调用让开发者能持续新增和迭代能力。这篇文章不是给你讲某个开源框架的 API而是把我自己做技能库时的完整思路、接口设计、实现过程和踩坑记录整理出来。无论你是在做聊天机器人、自动化助手还是复杂的多步骤 Agent这套方法都能用得上。边看边抄基本能避开我踩过的所有坑。1.2 技能和工具到底有什么不一样很多人会问我直接用 function calling 不就行了吗为什么还要单独搞一个“技能库”这里有一个很重要的概念区分。工具Tool通常指一个具体的函数或 API比如search_documents(query)它解决的是“做某件事”。而技能Skill更像是“知道在什么情境下、用什么参数、按什么流程去做某件事”的能力封装。举个例子get_weather(city)是一个工具但“帮用户规划周末出行”就是一个技能——它会根据目的地天气、交通状况、用户偏好综合推荐内部可能要调用多个工具还要判断哪一步先做。agent-skills的核心价值就是把工具调用从“碰运气”变成“有章法”。你不仅要告诉模型“有哪些函数可以用”还要告诉它“这个技能适合什么场景、需要哪些上下文信息、输出应该是什么格式”。这样模型才能更加稳定地做出决策不会在简单任务上反复横跳。从我实际测试的经验看没有技能库时即便函数描述写得很清楚模型仍然容易混淆参数和调用时机。有了统一的技能描述和强弱校验成功率能提升不少。尤其是涉及多步骤任务时技能之间的编排逻辑理顺了整个 Agent 的行为可靠度会是两个量级。2. 技能设计的关键思路先定义接口再考虑实现2.1 给每个技能定义统一的“三件套”能力描述、输入参数、输出协议我刚开始做技能库时犯过一个低级错误每个函数都按自己的习惯去写注释有的写一两句话有的写了一整页文档格式五花八门。结果模型一看就懵根本不知道优先用谁。后来我强制自己遵循一套统一的技能描述规范核心就是三件套能力描述description一句话说清这个技能是干什么的、适合在什么情况下调用。不要写“该函数用于获取数据”这种废话而要写“当用户询问某城市当前或未来几天的天气情况时调用此技能获取天气数据”。输入参数parameters列出每个字段的名称、类型、必填性、取值范围以及示例。这一步特别重要因为模型需要从用户对话里提取参数如果字段定义不清楚它很容易填错。输出协议output schema明确返回的数据结构至少要有状态码、业务数据、错误信息这三个字段。这样模型就能判断调用成功还是失败决定下一步是继续还是换一种方式。这三件套看似简单但实际写起来有讲究。比如参数描述不能只写类型还要写“取值逻辑”。我之前写过一个参数叫time_period类型是 string但没说明可取值是today、tomorrow、weekend结果模型在调用时生成了next week这种乱七八糟的值。后来我把枚举直接列在描述里错误率立刻降下来了。2.2 技能分类基础型、复合型、编排型把技能都放在一个平面里到后面一定会乱。我的经验是先按复杂度分成三类分别对待。基础型技能对应一个原子操作比如查询数据库、发 HTTP 请求、读文件。这类技能要尽量“无状态”同一个输入永远得到同一个输出容易缓存和复用。复合型技能由多个基础技能组合而成执行固定流程。比如“生成周报”它会先拉取项目数据再调用文本总结最后格式化输出。复合型技能内部流程是确定的模型只需要提供必要参数。编排型技能这类最灵活它内部不是固定顺序而是根据当前上下文动态决定调用哪些基础技能。比如“帮用户安排旅行计划”需要根据城市、日期、预算去查天气、查酒店、查交通每一步的结果都会影响下一步动作。为什么要做这个分类因为设计和测试的侧重点完全不同。基础型技能要重点测试边界条件比如参数缺失、超时、返回异常复合型技能要重点测试流程中的异常中断情况编排型技能则要重点测试模型在中间决策点上的准确度——这一步往往会受提示词影响很大。我建议你不管项目多小先按这个分类把现有能力整理一遍。整理完后你会发现哪些技能其实是重复的哪些是伪需求哪些结构上就很别扭趁早拆掉重来。2.3 设计技能接口时的三条硬性选择在一次线上分享中我总结过三条自己比较坚持的原则算是从实际教训换来的。第一技能优先做成无状态。不要在技能内部保存用户会话数据所有需要的数据都通过参数传入或者从共享上下文中读取。否则技能之间容易互相“埋雷”尤其当多个技能共享同一个会话时一个技能修改了状态另一个技能的行为就会变得不可预测。第二每个技能都要保证可重试。Agent 调用技能时经常遇到网络抖动、服务超时如果技能不是幂等的重试就会造成数据重复、费用翻倍。最简单的做法是给涉及写操作的技能增加一个request_id参数服务端去重这样即使重试也安全。第三技能的输出必须能支撑后续决策。光返回一行文本是不够的至少要返回结构化的数据加一层“置信度”或“状态说明”。比如查询用户订单如果查不到应该返回status: not_found而不是抛出一个异常。原因很简单模型需要根据状态来决定是道歉、追问还是换一种策略。如果所有失败都表现为异常模型就没有办法做出合理应对。这三条原则看起来会增加一点开发量但对于一个长期维护的 Agent 项目来说省下的麻烦远比投入多。3. 从零到一实现一个可插拔技能库实操记录3.1 目录结构与技能元信息定义我在项目里通常会建这样一个目录结构agent-skills/ ├── skills/ │ ├── weather/ │ │ ├── SKILL.md │ │ ├── impl.py │ │ └── tests/ │ ├── document_summary/ │ │ ├── SKILL.md │ │ ├── impl.py │ │ └── tests/ │ └── ... ├── loader.py ├── registry.py └── examples/每个技能单独一个文件夹SKILL.md是技能的元信息和声明文件。这个文件建议用 Markdown 写既方便人看也可以让解析器读取。我用这样的格式--- name: weather_query description: 当用户询问某城市当前或未来几天天气时使用。支持城市名可指定日期。 version: 1.0.0 author: your-name tags: [weather, query] --- ## input - city: string, required, 城市中文名如“北京”。 - date: string, optional, 日期格式 YYYY-MM-DD默认今天。 ## output - code: int, 0表示成功非0表示失败 - data: object, 包含 temperature, condition, humidity, wind - error: string, 错误信息code非0时有效把技能元信息独立放到一个文件里最大的好处是加载器不用去 import 每个 Python 模块就能知道技能的全貌。在做技能列表展示、权限控制、依赖分析时都特别方便。如果直接去读函数签名不仅慢还需要执行代码容易触发副作用。3.2 技能注册机制如何让新增技能像插 U 盘一样简单注册机制是我一开始就坚持要做的。因为我不希望每次新增技能都要去改一个大配置或者注册表那样迟早会改出问题。我采用的做法是“按目录约定 自动扫描”启动时扫描skills/下所有包含SKILL.md的文件夹。解析SKILL.md的元数据校验必填字段是否完整。动态加载对应实现模块impl.py并将模块中标记了skill_impl(skill_name)的函数绑定到技能名称上。如果校验失败或加载时报错就把该技能标记为disabled并记录错误原因不影响其他技能加载。使用装饰器绑定实现代码写起来很舒服# impl.py from skill_sdk import skill_impl skill_impl(weather_query) def run_weather_query(city: str, date: str None) - dict: # 实现逻辑 ...注册器会自动把这个函数关联到SKILL.md中的name上。这样当我需要新增一个技能时只要创建一个文件夹写好SKILL.md在impl.py里实现函数并打上装饰器就行。删除技能时直接删掉文件夹下次启动自动失效。有一点要注意自动扫描虽然方便但也要做好“技能屏蔽”机制有些技能暂时不想要了不想删文件就加一个enabled: false的字段。然后再明确约定外部不要随便改这个字段避免测试时误伤。3.3 技能调用的统一入口上下文、路由和执行所有技能都通过一个统一的入口执行类似agent_skills.execute(skill_name, params, context)。这里的context是全局上下文对象里面会放一些通用信息比如用户 id、会话 id、当前时间、环境标等。技能函数不需要也不应该自己解析这些信息直接通过context读取即可。执行时有个关键步骤叫路由选择。如果用户说“帮我查一下明天天气”模型可能觉得需要调用某个技能但实际技能库里并没有直接叫get_weather_tomorrow的技能。这时统一入口可以做一个别名映射把常见的口语化名称映射到正式技能名。比如ALIASES { 明天天气: weather_query, 明日天气: weather_query, 天气: weather_query, }在模型调用之前先做一遍别名解析能避免很多因为一个名字没对上而导致的失败。此外统一入口里我还加了一个“前置拦截层”。天然适合做权限检查、参数预校验、流量控制。比如某些技能只允许管理员调用某些技能需要强制刷新参数格式都可以在这一层处理不需要每个技能重复实现。这样做的好处是即使将来换掉后端大模型技能调用层不用跟着变。3.4 实战示例从零实现一个“文本摘要”技能为了让你更有代入感我完整记录一下我实现document_summary技能的过程。首先创建文件夹和SKILL.md--- name: document_summary description: 当用户需要将长文本、文章、会议纪要提炼成简短摘要时使用。适合超过500字的文本。 version: 1.1.0 tags: [text, summary, nlp] --- ## input - content: string, required, 待摘要文本 - max_length: int, optional, 摘要最大长度默认200 - style: string, optional, 摘要风格可选 concise简洁或 detailed详细默认 concise ## output - code: int, 0 成功非0失败 - data.summary: string, 生成的摘要 - data.char_count: int, 摘要实际字符数 - error: string, 错误信息impl.py实现逻辑from skill_sdk import skill_impl skill_impl(document_summary) async def summarize(content: str, max_length: int 200, style: str concise, contextNone): if not content or len(content.strip()) 20: return {code: 400, data: None, error: content too short} if style not in {concise, detailed}: style concise # 这里可以调用大模型 API也可以调用本地模型 # 关键是要控制 prompt并做好长度截断与去重 summary await call_summary_api(content, max_length, style) return { code: 0, data: { summary: summary, char_count: len(summary), }, error: None, }我特别想提醒的一点不要只顾着写调用逻辑一定要在技能里考虑“不可用”的情况。比如大模型 API 超时你要捕获异常并返回一个可读的错误状态而不是让技能直接崩溃。否则整个 Agent 可能会卡死用户等半天得不到任何反馈。注册之后这项技能会自动出现在技能列表里。接着用一个小测试验证调用result await execute(document_summary, { content: 这里的正文足够长可以用来测试摘要功能……, max_length: 100, }) assert result[code] 0我在这个示例里故意把参数校验写得简单了点。实际生产环境建议加上更严格的内容审核、敏感信息检测以及成本控制。比如当文本过长时要分块处理然后再合并摘要而不是一次性丢给模型否则 token 费用很惊人。4. 技能评测与多场景适配不能只看“它能跑”4.1 为什么评测要前置而且要持续做有一段时间我盲目相信“技能能跑通”就等于“技能好用”结果上线没几天就就出问题。问题集中在两类一类是调用成功率还行但返回结果质量不稳定另一类是某些调用对应的事件缺失模型误判成技能不可用。后来我吸取教训专门建立了一个评测流程每次技能变更都必须跑一轮数据才允许发布。评测前置的价值在于你能在模型层受到影响之前先发现技能层的问题。比如某个技能在参数缺失时应该返回什么模型会因为你的返回格式而决定要不要继续追问。如果你返回的是error: invalid模型可能直接道歉结束如果返回的是error: missing_parameter: city模型大概率会追问用户城市信息。这个差异非常大直接决定用户体验。4.2 评测集怎么建覆盖正常、边缘、恶意与噪声我通常给每个技能准备三组评测用例正常用例覆盖典型场景。比如天气查询要有不同城市的请求、有日期变换、有切换城市后的查询。边缘用例覆盖参数缺失、参数类型错误、边界值空字符串、超长文本、极小数。噪声用例故意测试无关请求或者夹杂其他意图的话语确保技能不会乱触发。还可以引入一个“意图混淆”的测试比如用户只是随口问“今天会不会下雨”此时天气查询和“出行建议”技能都可能被触发需要看模型到底选了哪个。如果经常选错说明技能描述里的场景区分还不够明确需要调整描述。评测指标上我一般不只看准确率更看重三件事技能调用成功率、技能执行完整率、语义混乱率。前两个好理解。第三个“语义混乱率”是指正常调用技能但返回结果和用户需求明显不符的比例。比如用户问“明天天气”技能返回了昨天的数据虽然调用是成功的但语义是错的。这种问题往往来自技能内部的日期计算逻辑用普通成功率根本测试不出来。4.3 多场景适配同一技能如何服务不同领域agent-skills还有一个很有意思的玩法同一套技能框架可以适配不同业务场景。比如我在公司内部推广这套方案时电商团队做“订单查询”技能医疗团队做“预约挂号”技能但底层的注册、调用、权限校验、日志追踪都是同一套代码。具体适配时要额外保留“场景标识”这个字段。在context里带上scene技能内部可以根据场景决定不同的策略。例如“天气查询”技能在出行场景下会返回进一步的城市建议在农业场景下可能更关注降水概率和温湿度。但注意不要让scene变成一个巨大的 if-else 分支否则技能会变得笨重。我的建议是每个场景只做少量差异化分支其余逻辑共用。多场景适配还有一层含义是指“模型在长上下文里的表现”。当 Agent 同时挂载了十几二十个技能时有些模型会忽略部分技能总挑熟悉的调用。针对这个问题一是要精简技能描述字数描述越短越容易被模型记住二是可以对技能做“排序”把用户可能更常用的排在前面。这些细节只能在多场景反复测试中慢慢调出来。5. 常见问题与排查技巧我在实际运行中遇到的坑5.1 技能响应太慢怎么定位瓶颈技能慢的坑我掉过太多次了。一次是连接远程数据库每次请求都新建连接导致每次查询要花2秒多。另一次是文本摘要技能里因为某个参数没设置超时大模型接口卡了 30 秒才返回整个对话像死掉一样。排查思路我总结成一个四步法第一步先看技能本身在内部耗时多少绕过模型调用直接用脚本调用技能实现函数。如果本身很快瓶颈在模型决策层如果本身很慢继续往下找。第二步检查技能实现里有没有可以优化的连接复用、缓存、索引。比如数据库查询就加上索引HTTP 请求就用连接池重复计算就做结果缓存。第三步检查上下文内容是不是太大。很多技能其实不关心整个对话历史但框架把所有历史都传进去了导致大模型处理很慢。第四步看有没有“重复执行”问题。有些 Agent 框架会为了“保证成功”而自动重试技能调用但失败后并不通知上层结果一条请求实际执行了三次每次都写入数据用户看到结果晚数据还重复了。另外我强烈建议给每个技能加上耗时日志至少记录total_time和stage_times。这样线上出问题可以直接拉日志而不是瞎猜。5.2 技能输出不稳定经常有遗漏字段怎么办模型调技能时一项常见故障是参数漏传。明明技能要求必填city模型却经常只传date。这种情况问题基本不在模型而是在技能描述本身不够“锁定”。我的解决办法是两点在SKILL.md里给每个必填参数加一句“如果用户没有提供该参数且没有合理的默认值直接报错 missing_parameter不要编造。”很多模型很会想象不明确禁止时它就会自己造一个出来。在入口执行时做严格的参数校验。凡必填缺失的直接返回missing_parameter:xxx这样格式明确的错误而不是继续执行。这种做法看起来“不近人情”实际对模型反而友好因为它得到了清晰的反馈可以立刻追问用户。还有一个隐藏点输出字段不稳定。比如摘要技能应该返回summary偶尔返回了content调用方拿不到正确字段。为此入口层我会做“输出协议校验”用轻量 JSON Schema 校验一下返回结构。如果不符合协议可以选择自动修复某些字段名或者记录错误并返回一个标准错误。这种提前校验能避免很多下游出现不可解释的KeyError。5.3 技能之间存在依赖和冲突怎么处理技能多了以后依赖问题会出现。比如“订单查询”和“用户信息查询”都要访问用户中心如果用户中心的接口升级了这两个技能都会受影响。我的做法是引入轻量级的依赖声明和检查比如在SKILL.md里加入dependencies字段列出需要的服务名或共享缓存。启动时统一检查这些依赖是否可用如果不可用技能标记为degraded调用时给上层一个降级提示。冲突的情况常见于“一个业务动作可以由多个技能来完成”。比如“给用户发优惠券”这个操作可能同时有“优惠券系统技能”和“运营活动技能”两个都能做。为了不让模型纠结我会在技能描述里明确边界这个技能适合什么不适合什么。必要时直接禁用另一个技能只保留最合适的一个。如果实在无法避免也可以做一个“技能路由提示”组件在调用前根据业务规则自动去掉不合适的技能。但这属于相对重型的方案大多数项目建议从精简技能数量和描述入手。5.4 关于 agent-skills 项目我最后想分享的一点体会做了这么久的技能库我最大的体会是它不是一个一劳永逸的方案而是需要持续迭代的“活系统”。你今天定义的技能描述明天可能因为模型升级就变得不再合适你原本觉得完美的分类跑了一段时间后可能又会发现新的边界情况。所以不用追求一开始就设计一个完美架构先把几个核心技能用标准方式管起来跑通流程再慢慢扩展。另外有一个小技巧我特别想分享每次给技能写描述时都假设自己完全不了解业务只凭描述去调用。如果描述里出现了“获取信息”这种说了等于没说的话那就要重写。反之如果一段描述让一个外行都能准确判断什么时候该用那就说明它足够清晰了。这个习惯帮我避免了很多上线后才发现的问题。agent-skills看起来只是一个小工具但它背后反映的其实是 Agent 工程里最容易被忽视的部分能力的封装和治理。如果你正被 Agent 的不可控、难扩展困扰不妨从整理自己的技能库开始。相信我这一步投入的收益远超你的预期。