
最近几个月只要聊到 AI Agent一定会碰到 agent-skills 这个词。早期我搭 Agent 的时候以为核心就是把 Prompt 写长一点、把工具函数堆上去后来被线上问题反复教育才意识到技能Skill才是决定智能体能不能稳定干活的分水岭。这篇文章不聊概念包装只讲我从零搭建技能体系时踩过的坑、总结出来的设计规范以及一套可以直接抄走的 Python 实现骨架。如果你正在做客服机器人的工具调用、内部知识库 Agent 的动作扩展或者想搞清楚为什么模型有时候不调用工具、有时候乱传参数这篇文章应该对你有用。我不预设你用过 LangChain 或 AutoGen只要写过 Python、调过 LLM API后面这些内容就能直接落地。1. 为什么 agent-skills 是智能体落地绕不开的一环1.1 从对话到干活Agent 缺的是可复用的动作能力很多人第一次接触 Agent第一反应是这不就是个加强版聊天机器人吗实际做过之后才发现聊天只是表象真正难的是让模型稳定地完成一组动作而这一组动作的封装方式就是 agent-skills 要解决的核心问题。举个例子。你让大模型帮用户查一下订单物流如果每次都在 Prompt 里现写你是一个客服你可以调用 get_order 函数参数是 order_id记得先查数据库再返回结果模型确实能执行但每次都要重复解释规则而且一旦订单查询需要经过鉴权→解析订单号→查物流接口→整理状态文案四步Prompt 就会变得无比臃肿。技能化的思路是把这一整套流程固化为一个独立单元告诉模型你有一个技能叫 track_order输入订单号返回物流轨迹模型只需要做决策不需要理解内部实现。这个差异看起来很小实际影响巨大。技能化之后模型的认知负担降低了出错的环节也收敛了。以前是让模型自由发挥现在是让模型在预设好的技能边界里选择稳定性完全不一样。1.2 技能与工具调用的本质区别如果你研究过 Function Calling可能会问技能不就是工具函数吗为什么非要新造一个词叫 agent-skills这里有个容易被忽略的细节。工具Tool通常是一个无状态的函数给它参数它返回结果做完就结束。技能Skill更接近一个有状态、可组合的业务能力它内部可能串联多个工具调用、可能包含前置校验、可能需要在失败时走降级逻辑、甚至可能需要多轮对话来收集缺失的参数。比如预订会议室这个技能它要做日期解析、冲突检测、并发占用处理不是单个 API 能搞定的。所以我的建议是底层用工具做原子操作上层用技能做业务编排。工具负责能调用技能负责会干活。agent-skills 的本质是把模型的决策能力与业务系统的执行能力之间那层胶水标准化。这层胶水不做好Agent 永远只能在 Demo 里跑上不了生产。1.3 我为什么坚持把技能独立建模过去半年我维护过两套 Agent 项目一套把技能直接写在主流程里另一套把技能独立成模块。前者的代码看起来更快但每次加一个新能力都要改动核心循环改完还经常影响旧技能后者前期麻烦后期却是越用越顺。独立建模最大的收益是隔离。技能有自己的声明、校验、文档和执行逻辑主 Agent 只需要读取技能清单就能知道我现在有什么能力、每个能力需要什么输入、什么情况下该调用。这样做模型决策逻辑与业务实现逻辑彻底解耦测试单技能不用跑通整个 Agent新人接手也不需要从头读主循环。我见过很多团队在初期为了赶进度跳过这一步结果技能数量超过二十个以后命名冲突、参数歧义、调用顺序混乱全都冒出来。把技能当成独立产品来设计不是过度设计而是生产环境的刚需。2. 技能设计的基本盘入口、参数与返回值2.1 技能声明名字、描述与参数 Schema一个技能要被模型正确调用首先得让模型看得懂它的接口。我的惯例是每个技能提供三样东西技能 ID、自然语言描述、参数 JSON Schema。技能 ID 要稳定且语义化比如order_track、meeting_book不要用func_001这种编号。描述要说明这个技能做什么、什么时候用、什么时候不要用。参数 Schema 则严格定义每个字段的类型、是否必填、枚举范围。在实际交付中我会把参数 Schema 写成标准 JSON Schema因为大部分 LLM API 原生支持这种格式可以直接拿来约束模型输出。一个典型的订单查询技能声明长这样SKILLS [ { id: track_order, description: 根据订单号查询物流轨迹适合用户在询问包裹到哪了、什么时候送达时使用, parameters: { type: object, properties: { order_id: { type: string, description: 用户提供的订单号例如 SF1234567890 } }, required: [order_id] } } ]这里有个细节描述里不要只写查询订单要写清楚触发场景。模型判断是否调用技能靠的就是描述与用户意图的匹配度描述越贴近真实提问触发越准确。2.2 描述怎么写才不会被模型忽略这是我踩过最多次的坑。早期我写的技能描述都是功能导向的比如获取天气信息计算运费结果模型经常该调用时不调用或者张冠李戴。后来我把描述改成了场景导向效果立刻好转。所谓场景导向就是描述里包含用户在什么情况下会问这个问题以及调用后能解决什么诉求。比如差获取天气信息好根据城市名查询当前天气和未来三天预报当用户问今天冷不冷明天要不要带伞某地天气如何时使用为什么要这样改因为 LLM 理解的是意图不是函数名。它需要判断的是用户这句话背后的需求能不能由这个技能满足而不是这个函数叫什么名字。描述里明确写出触发场景相当于帮模型做了第一轮意图分类。还有一个容易忽略的点描述里要写清什么时候不要用。比如查询天气的技能加上一句仅适用于国内城市海外城市请使用查询全球天气技能能有效减少技能抢单。2.3 返回结构与错误处理约定技能的返回值往往被忽视但它直接影响 Agent 的后续推理。我的经验是返回值必须结构化必须包含状态码和人类可读的说明。一个标准的返回结构长这样{ status: success, data: { order_status: 已签收, tracking_items: [ {time: 2025-01-02 10:30, location: 杭州转运中心, event: 包裹已到达} ] } }如果查询失败不要返回一个裸字符串查不到而是返回{ status: error, code: ORDER_NOT_FOUND, message: 没有找到该订单号请确认用户是否提供完整订单号 }这个约定非常重要。因为 Agent 拿到错误返回后会自己做下一步决策。如果你只返回查不到模型可能直接告诉用户查不到就结束了如果你返回清晰的错误码和引导文案模型会接着说您可以核对一下订单号或者我帮您转人工。错误信息本身就是给模型的下一条指令。3. 技能管理从一堆函数到可维护的技能库3.1 技能注册中心与命名规范技能数量少的时候写在代码里没问题超过十个就必须有技能注册中心。注册中心的核心功能是保存技能声明、加载技能实现、提供统一的调用入口。我通常用一张表来维护技能元信息字段包括技能 ID、版本号、功能描述、参数 Schema、实现模块、开关状态、负责人。为什么需要负责人因为技能一旦多起来修改一个技能可能影响多个 Agent 流程没人负责就容易改坏。命名规范也要提前定。我的习惯是领域_动作的格式比如order_refund、meeting_query、user_blacklist_add。领域放在前面可以避免冲突动作放在后面表达清晰。千万不要用do_something、handle_request这种万金油命名后面排查问题的时候你会疯掉。3.2 技能版本与灰度更新Agent 的技能不像普通函数改完立刻生效可能引发连锁反应。举个真实例子我在一个客服 Agent 里调整了订单查询技能的返回字段把status改成了枚举值结果模型拿到新结构后开始自己发挥在回答里编造出运输中的中间状态。问题不是模型变笨了而是技能变更没有给模型适应期。所以我把技能版本管理纳入了发布流程。每个技能带一个version字段更新时保留旧版本。生产环境做一个简单的技能路由表支持按用户 ID 灰度先放 5% 流量观察模型调用成功率和返回结构解析成功率没问题再逐步放量。这个过程听起来重但其实实现不复杂。技能注册中心里加一个active_version字段路由层根据配置决定加载哪个版本即可。重点不是工具多强大而是要有改技能的意识不能随手改了就上。3.3 技能的依赖与组合技能之间存在三种关系独立、依赖、组合。独立技能最好理解一个技能不依赖其他技能。依赖技能比如创建订单和扣减库存必须保证调用顺序。组合技能则是把多个技能编排成一个更上层的流程比如售后退款技能内部可能调用查询订单“核对用户身份”“发起退款”三个子技能。设计技能依赖时我建议遵循一个原则技能内部可以编排子技能但对外暴露的接口要尽量简单。换句话说不要让模型去决定先查订单再退款而是把整个退款流程封装成一个after_sale_refund技能模型只需要传入订单号和原因。这样做的好处是Agent 的决策复杂度大幅降低。模型不需要理解业务流程只需要理解业务入口。业务流程变化时也只改技能内部逻辑不需要重新调整 Agent 的 Prompt。这个封装思维是 agent-skills 区别于单纯工具调用的核心价值。4. 实操一个带技能的 Agent 从零搭起来4.1 环境与骨架理论说多了容易飘直接上一套可以跑的骨架。我假设你用的是 OpenAI 兼容接口Python 3.10。先装好openai库然后定义技能的抽象基类from abc import ABC, abstractmethod class BaseSkill(ABC): skill_id: str description: str parameters: dict {} abstractmethod def execute(self, params: dict) - dict: 执行技能并返回结构化结果 ...每个技能继承这个基类实现execute方法。执行方法内部可以做任何事比如调 API、查数据库、跑算法但返回值必须遵守前面说的结构约定。这个抽象层让技能实现与 Agent 主循环彻底分离。4.2 技能调用循环推理、执行、反馈一个最小可用的 Agent 主循环只有三层第一步把用户消息和技能列表一起发给 LLM让模型决定是否调用技能第二步如果模型输出工具调用请求就从注册中心找到对应技能执行第三步把执行结果传回 LLM让模型基于结果生成最终回复。这个循环可以迭代多次因为模型可能先查订单再根据查到的物流信息回答用户。核心伪代码如下messages [{role: user, content: user_input}] tool_defs [format_skill(skill) for skill in all_skills] while True: resp client.chat.completions.create( modelgpt-4o, messagesmessages, toolstool_defs ) msg resp.choices[0].message if not msg.tool_calls: final_answer msg.content break messages.append(msg) for call in msg.tool_calls: skill skill_registry.get(call.function.name) result skill.execute(json.loads(call.function.arguments)) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) })这里最容易翻车的点是messages的顺序。工具调用的返回必须带上对应的tool_call_id否则模型会分不清这个结果属于哪次调用。我在早期踩过一次两个技能同时触发返回结果串了模型直接把 A 技能的结果当成 B 技能的来推理答得离谱。4.3 给 Agent 配三个技能查天气、算运费、改备注用一个具体的客服场景来验证。假设我们要做一个物流客服 Agent给它配三个技能第一个是查天气用于回答用户问派送地天气怎么样会不会影响收货。第二个是算运费根据包裹重量和目的地计算费用。第三个是改备注用户说帮我备注放丰巢柜时调用。三个技能分别实现注册到技能中心。然后把技能声明格式化成 OpenAI tools 格式像这样{ type: function, function: { name: calc_shipping_fee, description: 根据包裹重量和收货城市计算运费当用户询问多少钱、邮费多少时使用, parameters: { type: object, properties: { weight_kg: {type: number, description: 包裹重量单位千克}, city: {type: string, description: 收货城市名称} }, required: [weight_kg, city] } } }实测下来模型在运费多少钱到北京多少钱这类问题上的触发准确率很高因为我刻意在描述里写了当用户询问多少钱、邮费多少时使用。如果你把描述写成计算运费输入重量和城市模型碰到到北京多少钱可能犹豫因为这句话里没出现运费两个字。4.4 实测效果与调参心得我拿 50 条真实客服会话做过一次小规模评测对比两种配置一种只给模型一个巨大的 Prompt另一种走技能化架构。结论很直接技能化架构的工具调用正确率从 78% 提升到 94%最终回复的用户满意度也明显更高。为什么因为 Prompt 再长模型也容易漏读关键约束技能化之后约束被拆成了模型的即时决策边界每个技能只关注自己该关注的那部分。还有一个心得是 temperature 不要调太高工具调用场景我固定在 0.2 以下温度越高越容易出现幻觉参数。如果模型偶尔不调用技能我的第一反应不是改 Prompt而是调技能描述。把触发场景写得更贴近真实用户的话术比加一万个字都管用。5. 常见问题与排查技巧实录5.1 模型不调用技能怎么办这是出现频率最高的问题。排查思路按顺序来先看技能描述是否场景化再看参数是否有必填项挡住了模型最后看是否技能数量太多导致注意力分散。我遇到过一个典型情况技能描述写的是查询订单状态用户说我的包裹怎么还没到模型就是不动。把描述改成查询订单物流状态当用户询问包裹是否发出、到哪了、什么时候送达时使用之后触发立刻正常。描述里的触发词覆盖用户真实表达是解决不调用的第一把钥匙。另一种可能是参数必填字段太苛刻。比如技能里有两个必填参数用户只提供了一个模型判断缺参不敢调用。解决方案是允许缺参技能内部做询问和补全。不要把参数完整性压力全压给模型。5.2 参数传错、乱传与幻觉参数参数幻觉是最让人头疼的问题。模型可能把杭州传成杭州市把数字10传成十甚至凭空捏造用户没提过的参数。我的应对措施是三重校验第一重在技能声明里把类型和枚举尽量收紧能写成 number 就不要写成 string。第二重在技能执行前做参数 Schema 校验格式不对直接返回带错误码的结构化结果而不是让技能崩溃。第三重对敏感操作做二次确认比如退款、改价这类高风险技能执行前让 Agent 向用户复述一遍参数。这三重校验下来幻觉参数的影响基本可控。不要指望模型不犯错要在错误到达业务系统之前把它拦住。5.3 技能返回太长导致上下文爆炸技能返回结果会放进 messages 里作为上下文如果一次返回几千字的物流轨迹几轮对话下来上下文就爆了既浪费 token 又降低模型注意力。我的处理办法是技能返回不要太实。查询物流的技能内部可以拿到完整轨迹但返回给模型的只保留最近三条记录和一个聚合状态需要详细时间线时再提供单独技能。这个思维叫结果压缩相当于给每个技能加一个专属的精简视图。还有一个小技巧返回的数据字段名要模型友好。不要返回data.status_code直接返回status字段加上display_text比如display_text: 包裹已签收。这样模型不需要解析枚举值直接引用展示文本回答速度和准确率都会提升。5.4 多技能竞争与路由冲突技能多了以后两个技能可能同时适合一个用户请求。比如查询订单和查询售后进度功能相似模型容易选错。我的做法是给技能描述里增加负向约束明确说若用户已申请售后并询问进度请使用查询售后进度技能而非查询订单技能。还有一种场景是多个技能都被触发模型一次发来多个 tool_calls。这时候要小心并行调用不是所有场景都安全。扣库存、支付、退款这类有依赖的操作绝对不能并行执行。我的建议是给技能加上parallel_enabled标记只有无状态查询类技能允许并行写操作一律强制串行执行。5.5 常见问题速查表现象可能原因解决方向模型不调用技能描述与用户意图不匹配重写为场景化描述加入触发例句模型调用频率过高技能抢单边界不清晰在描述里添加不要使用的负向约束参数乱传参数 Schema 约束不够收紧类型、枚举、必填规则返回结果模型看不懂返回字段语义模糊精简返回结构提供展示文本字段多技能并行导致状态错乱写操作被并行执行为技能添加并行标记强制串行上下文快速增长技能返回完整长文本对返回结果做压缩只保留关键信息灰度更新后行为突变新旧版本混杂技能版本路由按流量逐步放量6. 一点个人体会把技能当成产品来养做 agent-skills 这段时间最大的体会是技能不是写完就结束的东西它需要像产品一样持续维护。用户话术会变、业务规则会变、模型能力也会变技能描述和实现都要跟着迭代。我现在的习惯是每次上线新技能都做一次小样本评测收集模型误判案例定期用来反哺描述优化。最后再分享一个没有写在任何文档里的技巧把技能的触发说明直接连同返回结果一起存进日志。每次模型调用技能我都记录当时的用户输入、模型决策路径、技能返回结果。这个日志是调优 Agent 最宝贵的资产比任何评测集都有说服力。当你面对老板质疑这 Agent 怎么又答错了时翻出这条链路三分钟就能定位问题出在描述、参数还是技能实现上。agent-skills 这条路不算新但远没有到成熟阶段。只要你的 Agent 还要真实干活技能设计就永远是核心命门。希望这篇分享能让你少走几个坑也欢迎在实践中总结出更好的技能组织方式。