
最近一段时间我一直在折腾 agent-skills 这条技术路线。起因很简单手头几个智能体项目都卡在同一个地方——模型越来越聪明可让它真正干活时不是提示词太长把上下文撑爆就是同一个操作在不同对话里表现飘忽不定。后来我把注意力从“写更好的提示词”转移到“把动作变成可复用的技能”很多问题一下子顺了。先给结论agent-skills 不是一个单一的开源仓库名更准确说是一套智能体技能的抽象与封装思路。它把大模型能够执行的动作按照统一的描述、注册、调度、反馈规范封装成确定性的模块。这套思路能解决三件事让智能体学会调用真实工具、让复杂流程可拆解可复用、让每次执行的输出都可验证、可纠错。我不止一次在内部团队里说智能化转型最难的不是模型选型而是把业务动作拆成足够稳的技能块再让模型按需组合。适合谁看正在做 AI 客服、数据报表自动化、业务流程机器人、多智能体协作的开发者以及刚接触智能体开发、正被“提示词越长越不准”折磨的同学。下面的内容会从原理讲到代码实现最后再把我在项目里踩过的坑摊开说你可以当成一份实战笔记来读。1. 先搞清楚一个事Agent 和 Skill 到底怎么分工1.1 Agent 是大脑Skill 是标准动作库先说我的理解把智能体比作一个人Agent 是大脑负责理解目标、拆解任务、做决策Skill 是经过训练的标准动作库负责把“知道怎么做”变成“实际做到”。你不需要让大脑去记住拧螺丝的每一步手脚早就形成了肌肉记忆。对应到系统里就是由编排器决定先调用哪个技能、什么时候调用而技能内部则是固定流程。一个常见误区是把大量的“怎么做”塞进系统提示词里让大模型临场发挥。这种做法的坏处很明显一旦步骤变多上下文一长模型就开始丢三落四。技能化的思路反其道而行把“怎么做”固化在代码里模型只负责“选哪个技能、需要填什么参数”。比如订单查询声明一个 query_order_status 技能里面定义好调用哪个系统接口、返回哪些字段。模型看到用户问“订单什么时候到”只要识别出意图是查订单状态把订单号传给技能剩下的查询、格式化、超时重试全部由技能自己完成。这样每个环节都可测试、可回滚而不是黑盒里碰运气。所以我会把 agent-skills 分成两个层次上层是“决策智能”由大模型决定当前问题匹配哪个技能下层是“执行确定性”由技能模块保证同样的输入得到同样的输出。两者缺一不可。你只做上层不落下层结果就是模型说得头头是道一执行就变形只做下层不接上层又变成一堆没法被灵活调用的死接口。1.2 技能封装和普通提示词模板的差别很多人在做的其实是“提示词模板”不是技能。模板是给模型看的文本技能是给系统执行的程序。两者有本质差别我整理了一张对比表对比点提示词模板技能封装执行主体大模型自由发挥程序代码执行固定流程可复用性复制粘贴难免走样统一注册一处修改全局生效可测试性结果随机难以断言输入输出结构化可单测上下文负担每轮都占 token只暴露关键输入输出扩展新能力改提示词动辄影响全局新增技能文件独立发布数据上也可以看同样的任务用纯提示词方案在单次会话里的完成率大概 60% 左右换成结构化技能后因为每个环节都能验证整体完成率能稳定到 85% 以上。这个数字不是造出来的是我在自己项目里跑了小半个月对比得到的。核心原因不是模型变聪明了而是容错边界被程序兜住了。面对真实业务稳定性比“偶尔惊艳”重要得多。这里要提醒一个事情技能封装并不等于完全禁止模型发挥。在技能内部的固定流程之外模型仍然可以做话术组织、结果解释、追问澄清。你只需要把“动作”锁死把“表达”放开既稳又不死板。2. 设计一套 agent-skills 的关键决策2.1 技能描述文件里必须有哪些字段动手写代码之前先要把技能描述文件的设计定下来。我在做的 agent-skills 里每个技能用 YAML 描述核心字段包括 name、description、input_schema、steps、output_schema、error_policy。name技能唯一标识机器调用的主键建议用动词加对象格式比如 send_reminder、parse_upload_file。description给大模型看的说明写清楚“什么场景用这个技能、有什么关键限制”。这一段直接决定模型会不会选错。input_schema入参定义包含类型、是否必填、格式、示例值。不要嫌麻烦这一块是参数校验的地基。steps技能内部的执行步骤序列每个步骤对应一个工具调用或一个子动作。output_schema出参定义说明技能最终返回哪些字段、什么类型。error_policy异常兜底规则比如查不到数据时返回什么、超时重试几次。为什么要这么拆因为一套描述文件同时服务两个对象模型要读 description 来选技能执行引擎要读 input_schema 和 steps 来落地。如果字段太少模型选不准字段太多维护成本高。上面这六项是我压过几个项目后留下的最小集合。再加字段可以但每加一个都要问自己“是给模型看的还是给程序用的”。两边的信息混在一起最后只会让模型无所适从。另外我强烈建议把技能描述文件当成代码资产来管理进 Git 仓库走评审流程。技能描述的变化会影响线上行为和改业务逻辑一样要谨慎。不要直接在服务器上改 YAML 文件改完没记录过一个月自己都忘了当初为什么这么写。2.2 让模型“选对”技能描述与触发的艺术技能不被选中是 agent-skills 项目里最让人抓狂的问题。你写好了技能模型就是不用或者用错。我后来发现90% 的原因出在 description 写得不像“产品说明书”。怎么写才对要包含三个信息适用场景、输入要求、输出效果。给个例子description: 当用户询问某个订单的物流状态或者想了解“什么时候到货”时使用。 需要提供订单号作为输入如果用户没给订单号不要调用本技能先反问用户。 输出会包含当前状态和预计送达时间。这个描述里其实隐藏了触发条件和拒单条件。模型看到“什么时候到货”这类问题会优先匹配看到“退货怎么申请”这种问题就不会误用。很多人只写一句“查询订单信息”等于没说。还有一个小技巧在 description 后面追加 1 到 2 个典型问题示例。比如加一句“典型输入订单 SO2025001 现在到哪了”模型匹配的准确率会明显上升。现在的模型对示例比抽象描述更敏感。注意不要把所有问题都堆进去样例太多反而干扰判断。一般来说触发场景写 2 到 3 句典型输入 1 到 2 个就够。核心是让模型能区分“这个技能管什么”和“这个技能不管什么”后者往往更重要。我给很多技能都会写一句“如果用户只是抱怨物流慢建议安抚情绪不调用本技能”这能大大减少误调用。2.3 把多个技能编排成一条任务链路单个技能解决单个动作真实业务往往是“理解问题→查数据→算结果→回消息”的链条。所以技能之外还要有编排逻辑。编排可以是显式的比如用流程引擎把技能按顺序连起来也可以是隐式的让 Agent 自己规划并调用多个技能。我建议刚起步时用显式流程因为它可理解、可调试、可复现。隐式编排听着高级但模型一旦调用错顺序排查成本很高。举例用户上传一份销售表问“哪个区域增长最快”。这个任务可以拆成三步parse_upload_file 解析文件、calculate_growth 计算增长率、generate_report 生成文字结论。显式编排就是写一段类似“先执行 1拿到表结构后执行 2最后执行 3”的逻辑。隐式编排则给模型足够技能说明让它自己决定顺序。我的经验是明确固定的链路用显式编排模型参与决策的链路用隐式编排。混合使用时必须给每个技能标志“前置条件”。比如 calculate_growth 的前置条件是“已经有结构化表格”模型自然知道先调 parse 技能。另外链路中每两个技能之间最好只传必要参数不要图省事把整个上游输出都塞给下游。参数越少模型和代码的负担越轻。3. 从零实现一个 Skill 注册与执行引擎3.1 先写一份可以被解析的技能定义理论说再多不如直接上代码。下面是我在项目里常用的技能定义已经隐去了业务敏感信息结构保留。name: query_order_status description: 当用户询问订单物流状态、想了解“什么时候到货”时使用。 需要订单号作为输入缺订单号时先反问用户。 典型输入订单 SO2025001 现在到哪了。 input_schema: order_id: type: string required: true pattern: ^SO\\d$ description: 订单号形如 SO2025001 steps: - call: http_get_order with: order_id: $input.order_id timeout_ms: 3000 - call: normalize_delivery_status with: raw: $steps.http_get_order.result output_schema: status: type: string description: 已发货/已签收/处理中 estimate_delivery: type: string description: 预计送达日期 error_policy: on_not_found: return_message: 没有查到这个订单请核对订单号 max_retry: 2这里有几个设计点需要注意。第一步 http_get_order 是外部调用第二步 normalize_delivery_status 是本地函数。技能内部步骤可以混合外部 API 和纯函数但每步都要有明确的输入来源。我用 $input.order_id 表示来自用户参数用 $steps.http_get_order.result 表示上一步的输出这样依赖关系一目了然。timeout_ms 是必填的哪怕设个很大值也要写。没有超时约束的步骤在大流量下会把整个执行队列拖死。error_policy 同样必填模型调用一个技能时函数必须在任何分支下都有返回值不能抛异常给模型看。你以为模型能处理异常它只会慌乱地编一个结果那比系统报错更危险。3.2 注册中心怎么管理技能技能文件不能散落得到处都是需要一个注册中心统一管理。我的做法是启动时扫描 skills 目录下的所有 YAML 文件解析后放入内存字典再提供给模型和编排器查询。核心代码结构大概是这样import yaml from pathlib import Path class SkillRegistry: def __init__(self, skills_dir: str ./skills): self._skills {} self._skills_dir Path(skills_dir) def load_all(self): for file in self._skills_dir.glob(*.yaml): skill_id file.stem with open(file, r, encodingutf-8) as f: definition yaml.safe_load(f) self._skills[skill_id] definition return list(self._skills.keys()) def get(self, skill_id: str): return self._skills.get(skill_id) def list_descriptions(self): return { skill_id: skill[description] for skill_id, skill in self._skills.items() }注意到我用了 glob(*.yaml) 而不是逐个写死这样新增技能时只要丢一个文件进去重启服务即自动注册。list_descriptions 方法会把所有技能的 description 汇总出来给模型做技能选择的候选列表。它很关键模型不是靠名字猜的是靠描述匹配的。注册中心还应该做两件事检查 YAML 字段是否合法比如必填字段缺失时直接报错检查技能名是否重复避免后加载的覆盖先加载的。这两步看着不起眼能省掉不少运行时才爆雷的事故。我给团队定的规矩是注册中心加载失败就拒绝启动不要带病上线因为技能配置错了线上行为基本不可控。3.3 执行引擎怎么跑通一个技能注册中心管“有什么技能”执行引擎管“技能怎么跑”。引擎的核心是一个 run 函数接受技能 ID、用户参数、上下文返回标准化的结果对象。import time class SkillExecutor: def __init__(self, registry, tool_map): self._registry registry self._tool_map tool_map def run(self, skill_id: str, params: dict, context: dict None): skill self._registry.get(skill_id) if skill is None: return {ok: False, error: skill not found} clean_params self._validate(skill[input_schema], params) state {input: clean_params, steps: {}, context: context or {}} for step in skill.get(steps, []): tool_name step[call] tool_func self._tool_map.get(tool_name) if tool_func is None: return {ok: False, error: ftool {tool_name} missing} args self._resolve_args(step, state) state[steps][tool_name] tool_func(**args) return {ok: True, output: self._format_output(skill, state)}实际代码我会加上超时、重试、日志这里为了展示主干逻辑把分支都省了。_validate 用来根据 input_schema 校验和转换参数比如把字符串数字转成 int、检查正则匹配。_resolve_args 负责把 $input.xxx 和 $steps.xxx 这样的表达式替换成实际值。执行引擎最容易被忽略的一点它不应该在业务分支里写死判断。比如外部接口返回“订单不存在”引擎不该把整个技能判定为失败而应该按 error_policy 返回“没有查到这个订单”。也就是说业务上的语义结果和系统上的执行失败要分开处理。前者是正常返回后者才是异常。混在一起会导致错误日志满天飞却没一条是真正需要报警的。3.4 一个最小可用的编排示例有了注册中心和执行引擎再写一个最小的编排函数把技能按顺序串起来。def run_pipeline(executor, pipeline: list, params: dict): results {} current_params params for skill_id in pipeline: result executor.run(skill_id, current_params) if not result[ok]: return {ok: False, failed_at: skill_id, error: result[error]} results[skill_id] result[output] if order_id in result[output]: current_params[order_id] result[output][order_id] return {ok: True, results: results}这里的 pipeline 是显式传入的技能 ID 列表。比如处理“查询订单并发送提醒”pipeline 可以是 [query_order_status, send_reminder]。第一步输出订单状态第二步把状态拼进提醒文案。这样的代码没有魔法每一步都能打印日志线上出问题也能按技能 ID 定位。如果你要做隐式编排就是把这套 pipeline 的执行权交给模型让模型在候选技能列表里挑技能并按顺序输出调用请求引擎再去逐个执行。核心逻辑不变只是“谁决定顺序”变了。我建议先跑通显式再加隐式避免一开始就陷入模型乱序调用的大坑。4. 落地 agent-skills 时最容易踩的四个坑4.1 参数从自然语言里抽取别完全信任模型技能写得好不好一半看执行一半看参数抽取。模型从用户的话里提取出参数传给技能时经常出现类型不对、单位不对、格式不对之类的问题。比如用户说“明天下午三点提醒我”模型可能把日期格式化成 2025-01-01 15:00:00也可能给出“明天”这两个汉字完全看模型心情。我的对策是三层校验。第一层在 input_schema 里给每个字段写明 format 和 example让模型有参照。第二层在 _validate 方法里做强制类型转换和时间标准化识别不了的直接返回“参数缺失需要向用户追问”。第三层对于高风险字段比如金额、日期、手机号加一个轻量的正则校验宁可让用户重说一次也不拿脏数据往下游跑。最容易踩的坑是只在描述里写“请提供正确格式”却没有在代码里校验。模型不会对你的业务系统负责它只负责生成看起来合理的文本。真正兜底的必须是执行引擎。我在生产里见过太多“模型生成了看似合理的订单号但订单号在系统里根本不存在”的案例所以校验这一步千万别省。4.2 技能输出太长直接把上下文撑爆技能返回 200 行明细Agent 要基于这些明细做总结于是全部塞进上下文。第一轮没事第二轮再加 200 行第三轮就超长。这是我的项目里真实发生过的情况最后靠给技能加“输出裁剪”解决的。具体做法是给每个技能定义 output_schema 之后再加一个 output_summary 字段。比如完整结果里有所有订单行但下游只需要总数和平均时效那就让技能自己先聚合成总结文本只有总结进上下文完整明细写到缓存文件里用户需要时再取。很多人觉得这样丢信息但其实 Agent 做决策并不需要所有原始行压缩后的结构化摘要足够。还有一个小技巧技能返回结果里可以带 confidence 字段。如果模型判断当前结果可信度低可以主动问用户澄清而不是硬着头皮继续往下跑。这个字段成本很低但对体验提升非常明显。用户会明显感觉到系统“知道自己不知道”比一本正经地胡说八道强太多。4.3 技能失败后的降级策略要提前定义外部接口会挂、本地函数会抛异常、模型解析会失败。没有降级策略的技能等于在项目里埋雷。我见过最典型的情况调用一个报表生成技能数据库连接超时整个对话直接报错用户一头雾水。正确做法是在 error_policy 里给每个可预见的失败类型准备一个回复模板。查不到数据时回复“没有找到匹配记录请换个关键词”超时时回复“系统处理超时已为你转人工”计算失败时回复“统计临时不可用你可以换个时间再试”。不要求覆盖所有场景但核心链路至少要有 3 到 4 个兜底分支。执行引擎层面要把重试和降级分开。重试是同一个技能再跑一次一般最多 2 次降级是换一个更简单的技能或直接返回人工提示。不要无限重试不要把所有失败都丢给模型现场编那样只会得到更混乱的结果。我自己的习惯是业务核心链路必须全部有降级非核心链路可以允许失败并记录日志。4.4 技能权限不能只写在文档里技能的背后是真实工具和系统调用权限控制绝对不能省略。我在初始版本里犯过一个错误为了让 Agent 更灵活给搜索技能配了极高的文件系统访问权限结果模型在测试时“误入”了配置文件目录。虽然没造成损失但这件事让我意识到技能权限和用户权限是两个维度。每个技能在注册时都应该声明访问范围和运行环境。比如 parse_upload_file 只能访问用户上传目录calculate_growth 只能读取白名单数据表send_reminder 只能发送到当前用户绑定的会话。执行引擎在调用工具前要先检查技能声明的权限而不是让工具本身拥有过大权限。另外技能的所有调用都要写审计日志。以后排查问题时能回答“谁在什么时间调用了什么技能、传了什么参数、返回了什么结果”。这个日志不一定要很复杂用 JSON 往日志文件里追一行就行但必须有。没有日志的技能系统出问题就是盲人摸象。别再问为什么模型乱调技能了先查日志。5. 故障排查速查与应用经验5.1 常见故障排查表因为技能系统分层很多故障定位容易让人头大。我把自己遇到的故障整理成一张排查表遇到问题先按表排查能省不少时间。现象可能原因优先处理方式模型完全不用某个技能description 场景描述不清晰重写描述加入典型问题和触发词技能被调用但参数错误input_schema 缺少格式约束补 format/pattern并增加代码校验参数是齐的但技能不执行依赖工具函数缺失检查 tool_map 注册看启动日志输出与预期不一致steps 内部逻辑写错了条件分支用 mock 工具单测该技能上下文很快超长技能返回未裁剪的完整结果增加 output_summary按需截断同一技能时好时坏依赖上游接口不稳定加超时和重试准备降级分支权限报错技能声明范围过窄按最小权限原则调整声明排查时我习惯先看日志里的技能调用链。从技能 ID 开始确认是否被选中、入参是什么、每一步输出是什么通常能快速找到断点。不要一上来就怀疑模型能力多数情况下问题出在描述、参数和工具边界上。模型只是把错误放大了真正的 bug 经常藏在技能配置里。5.2 我最近在用的几条实操经验最后分享几条我目前在项目里固定下来的实操经验。第一技能粒度尽量小。一个技能只做一件可以被验证的事。把“查订单”和“发提醒”拆成两个技能比合成一个“订单提醒”技能容易维护得多。粒度小复用率高调试时也友好。尤其是多个业务流程共用的动作单独成技能后能避免重复开发。第二技能描述文案要当作产品文档来写而不是提示词。前者追求准确、无歧义、可测试后者追求修饰和说服力。我甚至会让非技术人员读一遍技能描述如果他能理解什么时候该调用模型也大概率能理解。如果你发现必须用很多形容词才能说清楚一个技能那可能是技能本身太复杂了应该继续拆。第三给技能系统加一个“影子模式”。新技能先离线跑一段时间记录它的命中率和输出质量效果好再切正式流量。这样避免一个刚写的技能直接把线上对话带崩。影子模式不复杂就是在执行时双写日志但收益非常大。我见过太多因为一个描述写错就让整个客服机器人行为失控的案例影子模式至少能把影响控制在观察期。我自己最近的版本里还加入了技能热更新机制注册中心监听 YAML 文件的变化变更后重新加载不用重启服务。这个功能对频繁调整技能描述的项目特别有用。当然热更新要配合版本号和灰度开关否则改坏了没法快速回退。这些经验不一定适合所有场景但如果你也正在做类似 agent-skills 的智能体技能系统顺着这条路走至少能绕开我踩过的大部分坑。技能系统做扎实以后换模型、换工具、加新业务都会变得轻松很多因为真正的业务逻辑已经沉淀在技能层了。