ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

agent-skills实战:把AI Agent能力拆成可复用技能单元

agent-skills实战:把AI Agent能力拆成可复用技能单元 做 Agent 应用开发的朋友最近应该经常听到 agent-skills 这个词。我自己的感受是凡是把 Agent 真正推到生产环境的人最后都会遇到同一个坎模型能力有了但Agent不会干活——不是不会是不知道怎么把一个大任务拆成具体动作。agent-skills 就是专门解决这个问题的做法把智能体需要的能力从提示词里抽出来做成独立的、可复用的技能单元。这篇文章我想把自己在项目中落地 agent-skills 的完整思考、代码结构和踩坑记录整理出来适合正在做 AI Agent 应用、工具调度、工作流自动化的工程师参考。内容不绕弯子直接讲设计思路和能跑的代码。1. 先搞清楚 Agent Skills 到底解决什么问题1.1 没有技能体系的 Agent 是什么状态先说一个我之前踩过的坑。早期做客服机器人我把所有能力塞进一个 system prompt什么查订单、退换货、推荐商品、转人工全写在里面。Prompt 从 800 字涨到 2400 字模型开始出现一个很典型的问题——动作混淆。用户说“我要退货”模型返回了查订单的工具调用。为什么因为 prompt 里描述查订单的文案和退货流程的文案互相干扰模型对工具边界产生了歧义。还有更麻烦的新需求来了比如增加一个“发票申请”我只能在系统提示词里再塞一段。塞多了以后 token 费用上涨响应时间变长模型还容易忘掉前面的指令。这就是没有技能体系的状态Agent 的能力全部依赖于一段不断膨胀的文本能力之间互相影响新增能力靠堆字问题定位靠人肉读 prompt。1.2 agent-skills 的核心主张agent-skills 换个思路把 Agent 的能力拆成若干个独立技能单元。每个技能单元就像公司里一个岗位有清晰的岗位说明这个技能负责什么、需要的工具能调用哪些函数、工作流程先做什么后做什么、边界哪些事不归我管。我在实际项目中总结的一个技能单元至少包含四部分技能描述给模型看的说明这个技能在什么场景下触发、能完成什么目标工具定义技能内部需要调用的外部函数、API 的参数结构执行逻辑调用工具的顺序、条件判断、结果处理方式边界声明明确这个技能不处理什么防止和别的技能产生歧义这四部分合起来就是一个可独立开发、独立测试、独立版本管理的技能包。Agent 运行的时候根据用户意图去检索和调用对应的技能而不是让模型在几千字的 prompt 里“凭感觉”操作。1.3 声明式与程序式两种建模路线怎么选做 agent-skills 分类时有两条路线我两种都试过给你一个判断标准。声明式技能定义里只写清楚“什么条件下做什么”具体执行交给模型自行规划工具调用。优点是灵活适合开放场景缺点是结果可控性弱模型可能找到你意想不到的调用顺序。程序式技能内部把执行流程写死比如“调用函数 A 拿到结果如果成功则调用函数 B否则返回错误”。优点是稳定可控适合金融、电商这类对准确率要求极高的场景缺点是不够灵活场景一变就得改代码。我现在的做法是混合式核心链路用程序式写死外围探索性动作用声明式。比如电商 Agent 里订单查询是程序式必须走固定流程而优惠券推荐这种偏开放的动作交给声明式。这个比例我建议根据线上评测结果动态调不要一次性全部程序化否则 Agent 会失去多步推理的价值。2. 技能目录怎么拆分和命名一套好用的 Agent Skills2.1 粒度切分粗了失控细了碎技能粒度是 agent-skills 设计里最需要拿捏的地方。切太粗比如一个“客服技能”包含所有客服动作等于没切切太细比如“读取订单号”和“校验订单号”分两个技能Agent 在多个技能之间反复跳转性能下降非常明显。我实践下来比较好用的判断标准一个技能对应一个用户可感知的任务闭环。什么意思用户说“帮我查一下订单到哪了”从识别意图、提取订单号、调用查询接口、解析物流信息到组织回答这是一个完整闭环应该是一个技能。这中间“提取订单号”这个动作不单独做技能它是技能内部的一个步骤。用这个标准我当时把客服 Agent 拆成了 9 个技能订单查询、退款处理、物流跟踪、商品推荐、价格保护、发票申请、人工转接、售后反馈记录、优惠券使用指导。每个技能都可以单独验收。2.2 命名规范和目录结构的落地实践技能命名直接影响模型检索技能的效果。我最早的命名是中文描述式比如“处理退款”后来评测发现模型召回的准确率不高。排查后发现问题出在命名太口语化和工具函数名、用户措辞之间缺乏一致映射。现在的命名我统一用三段式动作目标 对象类型 返回结果。英文缩写全部小写下划线分隔。例如query_order_progress查询订单进度apply_refund_request提交退款申请recommend_price_protection推荐价格保护方案目录结构上我的项目里长这样agent_skills/ ├── skills/ │ ├── query_order_progress/ │ │ ├── SKILL.md # 技能描述和触发条件 │ │ ├── schema.json # 工具入参的 JSON Schema │ │ ├── execute.py # 程序式执行逻辑 │ │ └── tests/ │ ├── apply_refund_request/ │ │ ├── SKILL.md │ │ ├── schema.json │ │ └── execute.py │ └── recommend_price_protection/ │ ├── SKILL.md │ └── schema.json ├── registry.py # 技能注册表 └── runtime.py # 技能调度器这套结构有一个额外的好处新同事上手只看 SKILL.md 就能理解技能边界不用去翻代码。SKILL.md 是我们团队约定俗成的核心文档后面我会拆开讲。2.3 技能之间的依赖关系怎么管理技能不能完全是孤岛。退款技能需要先调用订单查询技能拿到订单状态这就是依赖。我在项目里用一个轻量的依赖声明方式在 SKILL.md 头部加一个depends_on字段列出依赖的技能 ID。调度器执行前先做依赖拓扑排序保证被依赖的技能先初始化。这里有个教训不要做技能的隐式调用。早期我图省事在退款技能的执行逻辑里直接 import 了订单查询的函数结果后面订单查询内部改了返回字段退款技能直接崩了。改成显式依赖后通过接口调用依赖变更时可以在技能测试阶段就暴露问题。3. 技能核心描述文件 SKILL.md 的编写实战3.1 SKILL.md 是给模型看的不是给领导看的很多人写技能描述犯一个错写得像需求文档。大段大段的功能介绍、业务背景、名词解释。实际上SKILL.md 的唯一阅读者是语言模型它的作用就是让模型准确判断“这个技能管不管当前这个问题”。我写的 SKILL.md 结构固定包含五个区域name技能 ID与目录同名description一句话描述技能目标包含触发关键词和关键词的常见变体when_to_use什么场景下必须使用、什么场景下不要用tools本技能会用到的工具列表和调用约束examples2 到 3 个用户输入和对应技能调用的示例关键的经验在于 when_to_use 里的负例尤其重要。模型犯的错里很多是“过度调用”——不该用技能的时候调用了。我必须在描述里明确写清楚“不要做什么”。3.2 描述话术怎么写才不容易被模型误读一个实用的技巧用正反例而不是形容词。不要写“高效地查询订单”要写当用户询问订单当前状态、物流位置、预计送达时间时使用此技能。 不要将此技能用于修改订单信息或发起退款那些属于 apply_refund_request 的职责。我对比过两个版本的评测准确率用正反例描述的版本在意图匹配上准确率提高了接近 13 个百分点。原因不复杂——大模型对具体例子的理解远好于抽象描述。3.3 示例的选择有讲究示例不是越多越好而是覆盖度越广越好。我要求自己每个技能写正例 2 个、负例 1 个覆盖三种用户表达风格直接命令“查一下我的订单到哪了”、模糊提问“我买的那个东西发了没”、场景描述“下周要出差看看我新买的行李箱送到没有”。示例写完之后要跑一轮自测把示例输入测试模型能不能正确触发对应技能。如果触发不了优先检查 when_to_use 的描述而不是改示例硬凑。4. 技能注册与调度机制让 Agent 能“选对技能”4.1 注册中心技能的统一入口技能拆好了得有个地方统一管理。我在registry.py里做的是一个简单的注册表所有技能启动时注册到这个表里调度器只认注册表不直接扫描文件目录。这样做的好处是可以灵活控制某个技能在特定环境启用或停用比如灰度阶段先让 10% 流量使用新技能。注册表的核心数据结构我用了 dataclassfrom dataclasses import dataclass, field from typing import Dict, Any, Callable dataclass class SkillEntry: skill_id: str description: str when_to_use: str schema: Dict[str, Any] executor: Callable depends_on: list field(default_factorylist) enabled: bool True每个技能包在__init__.py里导出自己的SkillEntry注册中心加载所有技能包模块收集导出数据形成全量注册表。4.2 调度器的选择策略调度器是 Agent 的大脑触手。它做的事是拿到用户的原始输入结合当前对话上下文从注册表里选出一个或多个技能。我的方案分两步走第一步是粗筛基于关键词和 embedding 相似度从几十个技能里快速选出一个候选集控制在 5 个以内。这一步不追求准确追求召回率高避免漏掉正确技能。第二步是精排把候选技能的 SKILL.md 描述拼接进一个小的提示词模板让模型从中挑选最终要执行的技能。这一步准确率高因为候选集小了干扰项少模型不需要在几十个描述里做选择。我之前试过直接让模型从全量技能列表里选效果很差召回率和准确率都不到 70%。改成两级策略之后准确率稳定在 92% 上下。4.3 多技能组合执行的顺序控制用户的问题有时需要多个技能协作。比如“查一下我上一单的物流然后帮我把这单退了”需要先查订单再申请退款。调度器支持多技能并行和串行两种模式。串行模式下我在 runtime 里维护一个依赖图技能 B 依赖技能 A 的输出就等 A 执行完再把结果传进 B 的输入。并行模式相对简单用于相互独立的技能比如同时查订单和查优惠券。还有个细节串行链路上每个技能的返回结果都要做结构校验。我吃过亏技能 A 返回的字段名和技能 B 期待的不一致导致 B 执行时 KeyError用户的请求直接失败。在技能接口层做一个统一的响应包装类所有技能返回相同结构这样串联时就不容易出现键名错位。5. 从零实现一个小型 agent-skills 运行时5.1 最小化工程骨架我给你一个可以直接抄作业的最小实现适合先跑通流程再迭代。目录结构沿用前面说的规划我把核心实现压缩到两个文件里。首先是注册和加载模块这里用 importlib 动态扫描import importlib import pkgutil from typing import Dict import agent_skills.skills as skill_package class SkillRegistry: def __init__(self): self._skills: Dict[str, SkillEntry] {} def load_all(self): for module_info in pkgutil.iter_modules(skill_package.__path__): module importlib.import_module( fagent_skills.skills.{module_info.name} ) if hasattr(module, skill_entry): entry module.skill_entry self._skills[entry.skill_id] entry def get(self, skill_id: str) - SkillEntry: return self._skills.get(skill_id) def list_skills(self): return list(self._skills.values())这段代码的核心逻辑就是统一约定每个技能模块必须暴露一个skill_entry变量。新技能开发人员只需要维护自己目录内的文件注册中心自动发现不用改核心代码。5.2 调度执行器把技能调起来调度器需要承担的职责包括候选筛选、技能选择、依赖排序、执行和结果回收。我把核心执行逻辑写成一个简单模式from typing import List, Dict, Any class SkillRuntime: def __init__(self, registry: SkillRegistry): self.registry registry self.candidate_selector EmbeddingSelector() self.intent_ranker LLMRanker() def execute(self, user_input: str, context: Dict[str, Any] None): candidates self.candidate_selector.rank( user_input, self.registry.list_skills(), top_k5 ) selected self.intent_ranker.select(user_input, candidates) if not selected: return self._fallback_response() results [] for skill_id in self._topological_sort(selected): entry self.registry.get(skill_id) if not entry.enabled: continue params self._build_params(entry.schema, user_input, context) result entry.executor(params) results.append({skill_id: result}) return self._compose_response(results)这里_build_params是重点它负责把用户原始输入映射成工具需要的参数。比如用户说“查一下尾号 8832 的订单”需要从中提取订单号。这一步我通常用一个小的抽取模型或基于规则的 entity parser不建议让调度模型同时做参数提取否则一次对话里模型任务太重容易出错。5.3 技能内部执行逻辑的模板程序式技能我统一用模板写 execute 逻辑以订单查询为例def execute(params: Dict[str, Any]) - Dict[str, Any]: order_id params.get(order_id) user_id params.get(user_id) if not order_id or not user_id: return { status: error, error_code: PARAM_MISSING, message: 缺少订单号或用户标识 } order_data order_service.query(user_id, order_id) if order_data is None: return { status: error, error_code: ORDER_NOT_FOUND, message: 未找到对应订单 } track_info logistics_service.track(order_data.tracking_number) return { status: success, data: { order_status: order_data.status, logistics: track_info } }这个模板有三个关键设计参数校验在最前面缺参数直接返回结构化错误而不是抛异常每个分支返回统一结构调度器容易解析业务逻辑调用都封装在 service 层技能本身不直接写 SQL 或 HTTP 调用5.4 参数提取与校验的一个推荐写法参数提取我强烈建议用 JSON Schema 配合校验函数。给出一个 schema 例子{ type: object, properties: { order_id: { type: string, pattern: ^[A-Z0-9]{6,20}$ }, user_id: { type: string, minLength: 4 } }, required: [order_id, user_id] }调度器拿到这个 schema 之后先让模型产出 JSON再用模板库校验结构性约束。校验通过才执行技能不通过就向模型返回“参数不合法”的反馈让它修正参数。这一步避免了流程里很重要的一个 bug模型说“好的马上帮你查询”但参数是错的用户等来一个错误提示。6. 落地过程中一定会踩的坑6.1 模型“不调用技能”怎么办这个现象我见得太多了。用户输入明明在技能覆盖范围内模型却选择直接回答“这个问题我无法处理”。排查发现最普遍的原因是技能描述的触发词和用户口语表达差距太大。比如技能描述里写的是“查询订单进度”但用户说的是“我那个包裹怎么回事”。模型没有把“包裹”和“订单进度”关联起来。解决办法是在 SKILL.md 的 examples 里补充更多口语化变体而不是在描述里堆关键词。每个技能至少准备 5 种不同风格的正例覆盖口语、书面语和省略主语的短句。另一个原因是候选粗筛没把正确技能拉进候选集。embedding 检索时技能描述最好能覆盖高频同义词。我维护了一份同义词映射表比如“包裹-订单-快递-物流”在粗筛阶段同时用原词和同义词扩展检索范围。6.2 技能误触发边界声明的作用误触发比不触发更危险因为用户会收到完全错误的操作结果。有一次用户问“你们怎么退差价”系统触发了退款技能而不是价格保护技能差点造成重复退款。排查后确定问题出在两个技能的 when_to_use 描述重叠度高。解决方法是明确边界划分。退款负责“资金退回”价格保护负责“差价补发”两个技能描述里都要写清楚对方的存在# 退款技能 when_to_use 里加一条 如果用户要求退回购买时多付的资金差价请调用 recommend_price_protection 不要使用本技能。 # 价格保护技能 when_to_use 里加一条 如果用户要求退回整笔订单款项请调用 apply_refund_request不要使用本技能。6.3 技能执行结果的质量不稳定程序式技能的结果通常稳定不稳定多出在声明式技能上。比如商品推荐技能同一批输入在不同会话里给出的推荐差异很大。这不是随机性造成的而是模型对用户偏好信息的利用不一致。我的对策是给声明式技能额外做一个结果校验环节用一个轻量级规则引擎检查输出是否包含关键字段。商品推荐至少要有推荐理由、商品 ID、适配的用户约束三个字段缺哪个就让模型重新生成。另一个提升稳定性的手法是统一所有技能的输出格式要求在 SKILL.md 的 examples 里给 2 到 3 个完整输出样例模型会明显更倾向输出和样例结构一致的内容。6.4 随着技能数量增长出现的性能问题技能数超过 20 个之后embedding 粗筛的延时和准确率都会下降。我的经验是两个优化手段组合使用第一是对技能描述做二次精简粗筛用的 embedding 文本和精排用的 SKILL.md 可以不同。粗筛用一句话高度概括版减少向量化长度精排用完整版保障选择准确率。第二是启用技能分组索引。比如客服域、营销域、售后域各维护一个分组先根据对话场景定位到域再在域内做技能筛选。这个分层在评测上能把单次调度的平均响应时间从 420ms 降到 180ms 左右。6.5 灰度发布时怎么保证技能质量新技能上线前我固定跑三轮验证第一轮离线意图测试用历史真实用户问题构造测试集跑技能匹配准确率。 第二轮模拟执行用录制的接口 mock 跑流程看参数传递和异常处理是否符合预期。 第三轮小流量灰度只开启 5% 流量监控误触发率和用户投诉率跑 48 小时。这个流程帮我在上线前拦住了不少问题。最典型的一次灰度期间发现新技能在深夜时段误触发率升高调查发现是夜间值班场景下用户表达更简短模型更难判断意图。后来补充了短句负例问题解决。7. 最后说点个人心得agent-skills 这套方法论真正的价值不是代码多漂亮而是它逼着你把每个能力的边界想清楚。写 SKILL.md 描述的过程本质上是在梳理业务逻辑梳理得越清楚模型的表现越稳定。我现在每次在线上看到一次成功的技能调用都会觉得这个抽象值得。如果你准备从零搭我建议别急着定义一堆技能先挑一个用户反馈最多、业务逻辑最成熟的动作做成第一个技能把注册、调度、执行、测试整条链路跑通再逐步扩大。技能库是越积累越顺手的东西前提是每一块都经得起单独考验。
返回列表