如果你最近在折腾大模型应用,一定对 Agent 这个词不陌生。但真正把 Agent 推向生产环境的同学,大概率会撞到同一个痛点:模型“会说话”,却不会干活。它知道要查日历,但不知道怎么连接日历 API;它知道要发邮件,但不知道你的邮件服务长什么样。这时候,agent-skills 这类技能库方案就出现了。它不是又一个 GUI 编排工具,也不是简单的函数列表,而是把 AI Agent 需要的外部能力包装成一组有结构、有约束、可组合、可审计的“技能”,让模型像工具箱里的师傅一样随手取用。这篇文章想聊的,就是我在使用 agent-skills 过程中的设计思路、核心实现和踩坑记录。如果你正在做智能体落地,或者被 function calling 搞得头疼,应该能从里面找到一些能直接抄作业的东西。
1. 为什么 Agent 需要技能层:核心思路与设计取舍
1.1 从“会对话”到“会干活”,差的是一个技能抽象
任何一个 Agent 应用,本质上都在做两件事:理解用户意图,然后完成具体动作。前者可以靠模型参数和 prompt 解决,后者却需要接入真实世界的系统。早期做法是把所有工具函数都塞进 system prompt,让模型自己“看着办”。在 Demo 阶段,二十个函数还能跑;一旦工具数量超过三十个,token 开销、调用错误率、模型选择困难这些毛病全出来了。
agent-skills 的核心思路,是把“工具函数”升级为“技能”。工具函数只描述一个可执行动作,技能则包含这个动作的完整使用契约:参数是什么、依赖什么上下文、有哪些前置条件、失败以后怎么降级、需要什么权限。模型不再面对一堆孤立的函数签名,而是面对一组封装好的能力单元。拿发邮件举例,普通函数可能是send_email(to, subject, body),技能则是“向指定收件人发送邮件,并根据收件方类型自动选择发送通道”,其中包含校验规则、模板、重试逻辑和敏感标记。模型只需要根据用户意图选择技能,具体执行细节由技能内部消化。
我在实际项目里体会最深的是:技能抽象能把复杂度限制在可控范围。每个技能是一个独立的小模块,测试和维护都方便。你在提示词里描述技能时,也不需要把函数实现细节暴露给模型,只需要告诉它“做什么、什么时候用、有哪些约束”。这在多人协作的场景尤其重要,后端同学改接口、前端同学换服务,Agent 侧不需要跟着改 prompt,只要更新技能注册表就行。
1.2 agent-skills 与 RAG、工作流、原始 function calling 的边界
很多同学会问,agent-skills 和 RAG、工作流引擎、原生 function calling 有什么区别?我自己的理解是,它们解决的问题不一样。RAG 解决“模型不知道的知识怎么补充进来”,工作流引擎解决“流程确定、步骤不能乱”的场景,函数调用则是一个单次动作的最小接口。agent-skills 更像是对函数调用的“收编”,把零散的工具调用组织成可复用的技能层,同时兼顾动态决策和确定性执行。
举个例子,RAG 可以告诉模型“会议室 A 在下午空闲”,但它不能替你去调用会议室系统完成预订。工作流可以完成预订的固定步骤,但用户问“明天下午有什么会议室能用”时,它没法临时组合出一条链路。agent-skills 跟它们不是替代关系,而是可以叠加使用:用 RAG 提供知识,用技能执行动作,用工作流串起复杂流程。如果你需要的只是“查询天气”这种一次性函数,确实不必引入技能层;但当你面对一个经常变化、需要复用、还要按团队规范管理的动作集合,技能抽象就非常值得投入。
2. 核心实现拆解:技能描述 YAML、注册表与执行器
2.1 一个技能描述文件长什么样
在 agent-skills 里,一个技能不是写在代码里的某个类,而是先写成一份结构化的描述文件,再由执行器动态解释执行。这样做的最大好处是“描述”和“实现”解耦,模型看到的是一份干净的能力说明,开发者维护的是具体的处理函数。
我常用 YAML 来写技能描述,大致结构如下:
name: book_meeting_room description: 根据会议室编号和时段完成预订,适合处理“订会议室”相关请求 parameters: type: object required: - room_id - start_time - end_time properties: room_id: type: string description: 会议室编号,例如 A201 start_time: type: string description: 开始时间,ISO8601 格式 end_time: type: string description: 结束时间,ISO8601 格式 examples: - input: 帮我订 A201 明天下午两点到四点 arguments: room_id: "A201" start_time: "2025-01-23T14:00:00+08:00" end_time: "2025-01-23T16:00:00+08:00" permissions: - calendar:write retry: max_attempts: 2 fallback: notify_user_no_room这里的参数描述和示例最关键。我踩过的坑是,就算模型再聪明,它也不会从“start_time 是字符串”这种描述里知道该怎么填时间。当你给出一组对齐的示例,模型的输出稳定性会明显提升。特别是中文场景,用户说“明天下午两点”,没有示例的话模型可能把时间格式写对但日期算错,示例能帮它学会相对时间的换算逻辑。
2.2 注册表与自动发现:模型如何知道该调用谁
技能多了以后不可能全部丢给模型,所以 agent-skills 在中间加了一层注册表和检索器。注册表维护所有技能的名称、描述、标签、依赖和执行状态;检索器根据用户意图、会话上下文、Agent 当前任务,从注册表里召回候选技能,只把最有可能用到的几个技能描述拼到模型输入里。
简单场景下,用关键词匹配就够:把用户请求和技能描述里的关键词做一次交集。更复杂的场景,可以用向量化召回。用户问“有没有地方开会”,能匹配到book_meeting_room,而不是发邮件技能。召回结果控制在 3 到 5 个技能比较合适,太少容易漏,太多会稀释模型的注意力。这个参数也不是死的,和模型上下文窗口、技能描述长度都有关系。我在实测中常以 4 个为基准,再根据误召率调整。
2.3 执行器的上下文粘合与并发安全
技能执行器负责把模型决定的技能调用变成真实操作。它要处理三类事情:参数校验、上下文注入、状态隔离。参数校验很简单,按照技能描述里的 JSON Schema 过滤参数;上下文注入是关键,模型经常在前一个技能的结果里拿到数据,但下一个技能并不知道,执行器需要把会话级的上下文变量按需传递给技能。
比如“帮我查一下订单,如果有未发货的就申请退款”,这个链路需要两个技能协作,订单查询的结果要作为退款请求的输入。我在设计执行器时,把技能调用设计成无状态的,所有状态都存在会话上下文中。这样多个用户并发使用时不会串数据。每次调用都从上下文中取出当前技能的依赖字段,执行完再把结果写回去。用最简单的键值结构就能完成,但一定要给每个会话分配独立上下文,否则并发场景会乱套。
3. 从零搭建技能库:三个阶段的实操记录
3.1 第一个只读技能:查库存怎么接
我建议第一次尝试不要贪多,先从“查询库存”或者“查天气”这种只读技能开始。原因是只读技能不涉及写操作,出问题最多是查错数据,不会造成删除或者覆盖。拿查库存举例,先写好技能描述文件,再实现一个最精简的处理函数。
def handle_inventory_query(args, context): product_id = args["product_id"] stock = inventory_repository.get_stock(product_id) if stock is None: return {"status": "not_found", "message": "没有找到该商品"} return {"status": "ok", "product_id": product_id, "stock": stock}跑通之后,再在 Agent 的入口处把技能接上:初始化注册表、加载描述文件、让模型根据用户输入选择技能。这个阶段不用处理太复杂的失败恢复,只要确认模型能按描述调用正确技能,并且参数解析准确,第一步就成功了。我第一次跑通时,模型调用正确率大约在八成左右,剩下的主要错在参数名称写错。后来发现把参数描述加上类型约束和示例就能大幅改善。
3.2 多技能组合:用一条自然语言请求串联预订和通知
真正体现 agent-skills 价值的是多技能组合。假设用户说:“帮我准备明天的项目评审会,用 A201 会议室,下午两点开始,并给参会人发一封通知邮件。”这个请求里有两个技能:预订会议室和发送会议通知。Agent 需要先通过检索器识别出两个技能,然后决定调用顺序。
在实现上,我用了两种路径。轻量方案是让模型在一次回复里输出两个连续调用,agent-skills 的执行器按顺序执行;稳健方案是引入一个简单的计划模块,由模型先生成一个调用序列,再逐项执行,每步检查结果。两种方案各有取舍,轻量方案代码少,但模型一旦生成错误调用顺序就可能出问题;计划模块更稳,但需要额外开发和延迟。我建议如果技能链路不超过三个,先用轻量方案,重点在技能描述里写明依赖关系。比如会议通知技能可以声明“依赖会议室预订成功后的 room_id”,描述清晰的话,模型自己就能排出正确顺序。
3.3 生产环境必备:权限设计与审计日志
把技能接到真实系统之前,必须处理权限和审计。agent-skills 的做法是在技能描述里声明权限范围,执行器根据声明决定是否放行。比如查库存只需要read:inventory权限,发送通知则需要email:send权限。敏感操作之前,最好再插一步人工确认。我的做法是给技能加一个escalation标记,标记为敏感的技能,执行器不会立即执行,而是把待确认操作推给用户,等用户点确认后再发起真实调用。
审计方面,每一轮技能调用都记录:技能名、参数、结果、耗时、由哪个会话发起。这些日志除了满足合规要求,还能拿来做后续的故障分析和技能优化。有人嫌日志麻烦,但一旦线上出问题,没有日志你就只能靠猜,基本无从下手。尤其是涉及退款、删除、覆盖这类不可逆操作,完整的审计链路是最后一道防线。
4. 踩坑与排查:模型乱输出、并发串状态、技能效果度量
4.1 模型不按技能描述输出,先别怪模型
这是最常遇到的问题。现象是模型该调技能时不调,或者把参数填得乱七八糟。多数时候不是模型笨,而是技能描述写得不够“显眼”。解决思路有三个:一是把技能描述精简,把关键触发词放到description开头,避免模型在长文本里漏看;二是增加示例,特别是中文口语触发词和标准参数之间的对应关系;三是在模型输出后加一道解析和校正层,检测调用格式不对就重试,而不是直接报错。
我遇到过最奇怪的情况是模型把“会议室编号”填成了会议主题。后来发现是因为我在参数 example 里放了真实会议室名称,模型误以为那是描述的一部分。解决方案是把示例里的值改成不存在的占位符,比如room_A201_demo,并且在描述里明确“编号严格来自系统列表,不要主动创造”。
操作上,我建议在 Agent 外层加一个兜底策略:如果模型两次解析失败,直接向用户回复“暂时无法处理,请换个说法试试”,而不是把原始参数错误暴露给用户。这听起来简单,却能显著改善用户体验。模型出错并不可怕,可怕的是一出错就把错误链路暴露出去,让用户对产品失去信任。
4.2 多用户并发时状态为什么串了
技能执行器如果内部持有可变量,多用户同时使用时会出现数据串扰。典型症状是 A 用户查询完订单,B 用户接着发起退款,退款用的订单号却是 A 的。根因就是执行器把用户 A 的上下文当成全局状态了。解决方法不复杂:坚持无状态执行器,所有状态放在会话上下文中。
我的具体做法是,每个会话生成一个context对象,在技能处理函数入口注入,出口回收,处理函数内部绝不允许修改全局变量。如果某个技能确实需要缓存计算结果,也放在会话级缓存里,并加上过期时间。这块在并发测试下最容易被忽视,一定要用多个会话同时跑一轮多技能链路,确认不会互相影响。我见过不少项目在联调阶段好好的,一上生产就出乱子,基本都是这个原因。
4.3 用三项指标判断一套技能库做得好不好
没有度量就没有优化。我会把三类指标列入常规监控:技能调用成功率、参数解析错误率、用户侧超时率。调用成功率是真实执行成功的次数除以模型发起的调用次数;参数解析错误率高,说明技能描述不清晰;用户侧超时率高,则要考虑是不是链路设计太长、技能太重。
下面是我自己常用的参考基线:
| 指标 | 参考基线 | 说明 |
|---|---|---|
| 技能调用成功率 | > 95% | 低于 90% 时需要检查技能内部错误和权限配置 |
| 参数解析错误率 | < 5% | 过高说明描述中的示例和约束不足 |
| 平均单技能耗时 | < 800ms | 超过 1s 要关注技能本身是否有慢调用 |
| 检索命中率 | > 90% | 候选技能覆盖不到真实意图时,需要扩展描述标签 |
这些数字不是绝对标准,但能帮你快速定位问题。我见过很多项目上线前只测“模型说得对不对”,完全不看技能执行成功率,结果就是 Demo 惊艳、生产翻车。加入这些指标以后,至少你知道到底哪个环节在拖后腿。
最后再说一点个人体会。agent-skills 能跑得稳,靠的不是把 prompt 写到完美,而是把技能描述、执行器、上下文、审计这些基础模块老老实实搭好。模型的能力会迭代,你的技能库不会;今天只封装了查库存,明天再加一个下单技能,整体框架不需要动。这种“把功夫放在边界上”的思路,才是这个项目给我最大的收获。如果你也在做 AI Agent,我的建议是从一个只读技能开始,先把描述、检索、执行这条链路跑通,再逐步扩展。等你回头再看那些堆了一百个函数调用的大 prompt,会觉得当初的折腾完全是值得的。