从零构建Agent技能系统:技能文件设计与工具调用落地实践
发布时间:2026/10/14 9:46:02 锦皓数字建站

1. 先从一次翻车说起为什么我的 Agent 不干活去年下半年我开始认真做 Agent 开发第一个卡住我的不是模型选型也不是算力资源而是 agent-skills 这一层。当时我接了个内部提效项目要求做一个能自己处理数据的智能助理类 Agent用户丢进来一个需求Agent 自己拆任务、调接口、组织最终结果。结果很现实——模型什么都会聊但一落到具体业务上就暴露一个问题它根本不知道自己能调用哪些能力。你说它有工具也可以问题是工具太多了它不知道什么时候该用哪个你说它有提示词也行但提示词写成一大段规则之后改起来简直是一场噩梦。折腾了几周之后我才想明白一件事Agent 能不能落地关键不在模型本身而在于你有没有一套定义清晰、可动态加载、便于维护的技能体系。这就是 agent-skills 要解决的问题。agent-skills简单理解就是给 Agent 配一套“能力操作手册”——哪些操作可以做、每个操作的边界是什么、需要哪些参数、内部按什么步骤执行、结果以什么形式返回全部用结构化配置定义好。它的价值不是帮你多写几行代码而是把模型和业务系统之间的那层胶水标准化、工程化。这篇文章我不打算聊什么玄乎的架构理论就结合我自己从零搭一套技能系统的过程把技能文件怎么写、参数怎么约束、描述怎么措辞、调试怎么进行这些最实际的问题一次性说清楚。想动手搭 Agent 技能的开发者、正在被“模型该调用哪个工具”折磨的工程师、甚至刚入门想把 Agent 落到自己业务里的人都可以照着这份经验走一遍。2. 技能层到底解决了什么实际问题2.1 模型调用与系统能力之间的鸿沟先看一个特别常见的场景。你的业务系统里有几个现成的接口查订单状态的、算优惠价的、查询库存的、给客户打标签的。放到模型面前它面临的问题是——它根本不知道这些接口存在更不知道用户说的哪句话应该触发哪个接口。你可能会说那我把这些接口说明全塞到系统提示词里面不就行了试过的人都知道几个接口还行几十个接口塞进去以后提示词动辄几千字模型的注意力被稀释得一塌糊涂关键信息反而抓不住。这就是最原始的“工具调用”困境能力多了模型反而不会选。agent-skills 换了一个思路。它强调的不是“把所有能力告诉模型”而是“让模型在恰当的时机主动查询某项能力的用法”。模型根据当前对话上下文先决定要做什么再去查对应技能的定义拿到完整的调用方式之后才真正去执行。这个“按需查询”的设计把一个超大的提示词切成了无数个小而精确的技能卡片模型的注意力负担小了很多调用准确率也稳定得多。2.2 技能文件与普通提示词模板的本质区别很多人第一次接触 agent-skills 的时候觉得这玩意不就是个带格式的提示词模板吗这种理解不能说错但确实忽略了最核心的几层差异。普通提示词模板本质上只是文本它鼓励模型“自由发挥”而技能文件是一个完整的执行单元它至少包含四个层面触发条件、输入校验、内部执行逻辑、输出格式化。这四个层面意味着——技能可以被版本管理、可以被单元测试、可以被动态加载卸载、可以被权限管控。我用一个比方来帮你理解提示词模板是“给实习生口头交代任务”技能文件是“给正式员工下发标准作业指导书”。口头交代灵活但容易跑偏指导书虽然要花时间写但每个人执行出来的结果都是稳定可预期的。尤其当你面对的是一个不可控的大模型时标准作业指导书才是保证线上行为稳定的唯一手段。技能文件里每一步都有对应的结构模型只能在你划定的边界里做选择和填空自由度被收窄但正确率大幅提升。2.3 一套可复用的技能系统至少要包含哪几块如果你打算自己做技能体系我最建议从最小可用结构开始不要一上来就搞复杂的编排引擎。一个能跑起来的技能系统至少要有这几块技能定义文件描述这个技能是干什么的、什么时候用、需要什么参数、内部怎么执行。技能执行器接收模型传来的参数做校验然后真正调用你写好的函数或接口。技能索引服务把一批技能文件的摘要信息组织成索引当模型要“查找技能”时返回不超过 N 条匹配结果。结果回填通道执行完技能把结构化结果交回给模型由模型用自然语言组织呈现给用户。这四块缺一不可。我把技能系统搭建起来的第一个版本只做了这三块把“结果回填”这一步漏了导致模型执行完技能后自己又发挥了一段把结构化的返回值改得面目全非。后来补上了结果回填通道让技能返回值以只读的引用形式出现在模型上下文里问题才彻底解决。一个成熟的技能体系一定是模型、技能执行器、业务系统三者互相约束的结果而不是模型从头到尾自由操作。3. 技能文件的字段设计与配置参数3.1 技能命名与触发条件别用花哨的名字技能名是模型识别技能的第一标识。我的习惯是全部使用大写下划线风格比如QUERY_ORDER_STATUS、CALCULATE_TAX_RATE、SEARCH_KNOWLEDGE_BASE。这种风格有两个好处一是和普通自然语言区分度高模型在对话中不会误以为它是正文内容二是在日志追踪里筛技能名称特别方便。很多刚入手的开发者喜欢起英文别名或者缩写比如qry_os、calcTax看起来简洁但实际上模型对这些缩写没有稳定的语义联想召回率会明显下降。触发条件字段的写法更讲究它直接决定模型是否会调用这个技能。我的经验是不要写“何时使用”的总结性描述要写“看到什么信号就想到这个技能”的触发提示。比如查订单状态的技能命名为QUERY_ORDER_STATUS相比ORDER_INFO更明确。描述字段可以写“当用户询问某个订单的物流进度、配送状态、订单当前所处环节时使用如果用户提供了订单编号则直接使用否则先询问订单编号”。这里有一个关键细节要说清楚描述里写的触发信号越贴近真实的高频问法模型命中的概率越高。我通常在写完描述后收集几个月的真实用户提问语料把里面的典型问法直接提炼进描述里效果显著提升。3.2 描述文本的写法从枚举规则到语义引导描述文本是整个技能字段里最容易被低估的部分。很多人写描述等于一句废话“查询订单状态。”这种描述模型当然能看懂但无法判断“什么时候应该主动用它”。我推荐的描述结构是一个三段式第一段用动词开头概括技能功能——注意动词开头比名词开头更容易被模型选中第二段写出这个技能的适用场景尤其要写出边界条件——什么情况下不适用这个同样重要第三段明确调用这个技能的前置条件是什么比如“必须先拿到订单编号如果用户没给就用反问引导用户补充信息不要猜测参数值”。边界条件这个很多人会忽略但实测下来极其重要。我曾经有一个技能是“给用户发送营销短信”描述里写了“当用户要求发短信时使用”。结果模型多次把“我想咨询营销活动”误判成“想发短信”导致误调用排查了很久才定位到是描述里缺少了“用户明确要求发送”这个触发边界。在描述里补上“仅当用户明确要求发送时才能调用咨询、查询、了解活动详情等场景不适用”之后误调用率几乎降为零。3.3 入参定义与参数约束JSON Schema 是最低要求技能函数你写好了参数是模型替你填的怎么保证它填得对答案是最低限度使用严格的 JSON Schema 定义每个字段。这里说的严格包括每个参数的type必须写死、必填参数要列在required数组里、取值范围要用enum或pattern限定、参数描述要说明这个值从哪里来、在什么条件下提供。我见过最有代表性的一个坑是订单编号字段没写描述模型自作主张把用户输入的“帮我看看我前天下单的那个耳机到哪了”里的“耳机”当成了订单编号调用查询接口直接 404。后来我在description里补充了“订单编号一般由数字和字母组成通常在用户消息中作为独立实体出现如果用户没有提供订单编号不要猜测直接询问用户”这类幻觉式填参基本被扼杀了。参数校验里还要加上默认值策略哪些参数可选、缺省时用什么值、填错时是重试还是直接报错这些都要在技能配置里提前说清楚而不是等执行报错了才去补救。3.4 技能内部执行程序的三个关键位置参数约束解决的是“输入”问题真正的执行逻辑技能文件内部还需要定义。我不推荐把大段业务代码写进 JSON 配置里那是自找麻烦。正确做法是在技能配置里保留三段关键信息第一段是执行步骤列表用有序列表描述“先做什么、再做什么、最后做什么”。这段列表不是给机器执行的而是给模型构建思维过程用的。模型拿到这个步骤列表后会在推理时像一个人类员工一样逐步思考和执行而不是直接跳到最终答案。第二段是异常处理约定明确告诉模型如果接口返回错误或超时应该重试几次、超时值设多少、错误信息如何反馈给用户。第三段是结果格式说明定义返回值的数据结构以及模型应如何基于这些数据组织最终回复的口径。三段都齐了一个技能才能真正成为一个闭环的执行单元。我在调试中见过太多技能输入输出都对但逻辑路径混乱——模型跳步执行、非法绕过某些校验、异常时给出错误解释。检查以后发现几乎都是因为技能配置里缺少“执行步骤列表”这个环节模型在技能内部的自由空间依然太大。加上步骤列表后技能内部的确定性也立起来了。4. 从零搭建一套技能库的完整实操过程4.1 项目目录设计与技能文件模板动手搭的时候我强烈建议不要把所有技能塞进一个 Python 文件里硬编码。我自己第一版就是这么干的结果技能数量到十几个后改一个字段要全局搜索加了新技能要重启整个服务排查问题要在代码里翻半天别提多痛苦。后来我参考了行业里比较成熟的做法把技能系统做成了目录驱动、配置与代码分离的结构。目录结构大概长这样agent_skills/ ├── skills/ │ ├── query_order_status/ │ │ ├── skill.json │ │ └── handler.py │ ├── calc_tax_rate/ │ │ ├── skill.json │ │ └── handler.py │ └── search_knowledge_base/ │ ├── skill.json │ └── handler.py ├── index/ │ └── skills_index.json ├── registry.py └── executor.py每个技能一个独立目录skill.json记录技能的全部配置信息handler.py存放真正的业务函数。当一个技能从简化阶段步入成熟阶段或者不再满足要求时不再需要改动其他模块调整目录、更新配置文件、重启加载干净利落。技能数量超过 20 个以后这个目录式管理的优势会加倍体现出来。接下来我以一个实际配置为例展示一个技能文件的全貌{ skill_name: QUERY_ORDER_STATUS, description: 查询订单当前状态。当用户询问订单物流进度、配送状态、当前所处环节时使用。仅当用户提供了订单编号时调用若用户未提供需要先询问订单编号不要猜测。, parameters: { type: object, properties: { order_id: { type: string, description: 订单编号一般由字母和数字组成来源于用户消息中的实体识别。 } }, required: [order_id] }, steps: [ 1. 使用 order_id 调用订单查询接口, 2. 若接口返回超时或 5xx 错误等待 2 秒后重试一次, 3. 根据返回 status 字段映射为用户可读的物流描述, 4. 将状态描述和当前节点时间一起返回给模型 ], result_format: { status: string, current_node: string, update_time: string } }注意看steps这个字段的表达方式——它不是让模型背下来的文本而是引导模型一步步执行的思维路径result_format则锁定了返回结果的结构避免模型拿着自由文本乱组织。4.2 手写三个不同定位的技能案例第一个技能是工具型技能比如查天气或者查快递。这类技能的特点是逻辑单一、参数明确、结果结构化。写这类技能时重点是描述触发信号和结果格式化难点在于覆盖用户的各种口语化表达。第二个技能是流程型技能比如“批量给某分类用户发通知”。这类技能内部会牵扯多个步骤和多个接口。我在配置里强制加上了steps列表并且每一步都写清楚判断条件。有一次我调这个技能时发现模型总是跳掉权限校验步骤直接执行发送原因是steps列表里把权限校验写在了第二步而模型看到第一步“获取用户列表”之后就开始执行后续动作。后来我发现直接把“权限校验通过后才有权执行任何后续操作”写进描述和步骤两个字段里模型才牢牢记住。第三个技能是检索型技能对接的是知识库或者搜索引擎。这类技能最需要强调的是“不要自己编造答案”。在配置里我会明确要求仅返回检索结果中的相关内容如果检索结果为空必须如实告知用户没有找到而不是用大模型自己的知识去补全答案。检索型技能的result_format通常是一个数组我会把它定义成带来源标号的结构模型组织回答时能够引用来源编号这比让它自由发挥要可信得多。4.3 技能注册、索引构建与动态加载的思路技能文件写好之后下一步就是让模型能够在合适的时候找到并加载它们这个环节的工程实现同样关键。我采用的做法是构建一个轻量索引文件。模型的输入长度永远是有限的我们不能把所有技能的完整配置都塞进去。所以索引文件里每个技能只保留三个字段技能名称、描述摘要、参数入口摘要。必要时还可加一个“技能最新更新时间”字段。相当于只给模型看一份“菜单”技能细节等到模型真正点单时才端上桌。每次目录里面的技能文件发生变化之后我会用一个注册脚本重新扫描技能目录、校验每个技能文件的格式、重新生成索引。这个脚本最好在 CI 阶段自动跑一遍配合一个简单的校验规则——字段缺失、格式非法、参数类型不支持都会直接报错。技能数量多了以后手动维护索引是绝对不可行的必须要让注册过程自动化、自动化过程可验证。我在实现注册脚本时还额外加了一个依赖检测逻辑如果一个技能依赖另一个技能的执行结果在注册阶段就完成绑定避免模型把两个独立的技能混在一起调。这一步虽然增加了一点实现量但为后续扩展带来了极大的方便。4.4 一次典型技能调用的全流程复盘举个我实际跑通过的一个场景用户在我的测试系统里说“帮我查一下订单号 20240512ABC 现在到哪了”。系统整体流程是这个样子的第一步Agent 的主对话模型接收这个问题提取到意图是查询订单状态实体是订单号20240512ABC。第二步主对话模型带着这些信息去查技能索引看哪条技能和当前意图匹配度最高。索引返回了QUERY_ORDER_STATUS。第三步主对话模型把这条技能的相关描述和参数定义附加到自己的上下文里生成一个 JSON 调用请求技能名QUERY_ORDER_STATUS参数order_id 20240512ABC。第四步技能执行器校验参数类型和必填约束通过后调用handler.py里的查询函数拿回订单状态的原始数据。第五步技能执行器把原始数据按result_format规整成结构化结果。第六步模型拿到结构化结果按配置里约定的表述口径生成一段面向用户的自然语言回复比如“这笔订单目前已在配送途中当前节点本市分拨中心更新时间14:30”。这六步看起来简单但每一步都有对应的日志输出。排查问题的时候我可以清楚地看到模型是在技能选择环节出了错还是在参数填充环节出了错还是在结果组织环节出了错。这种全程可观测性是我强烈建议从第一天就搭建好的基础设施。技能执行器至少要在每个环节打印一行包含技能名称、耗时、返回码的日志否则等系统上了量模型一通乱调技能你连从哪儿查起都不知道。5. 常见问题与后续拓展方向5.1 模型就是不调用技能怎么排查这是所有做 Agent 的人都逃不过的坑。模型明明已经看到了技能描述用户的意图也明确它偏偏宁可自己瞎编也不去调用技能。我的排查顺序通常是这样的先看描述文本是否清楚定义了触发条件——如果描述里写的是“这个技能可以查询订单状态”那模型确实很难分清什么时候该用改成像我前面说的“当用户询问物流进度、配送状态时使用”这种明确的触发句式后情况立马改善。再看索引返回的结果是否真的把技能放在了正确的位置上因为模型通常会选择排序第一的技能如果排序逻辑不准相关技能排在了后面模型自然就看不见了。最后再看模型的系统提示词里是否有“不要随意调用技能”之类的冲突性指令这个问题常在整合多个模块时无意识引入排查时要特别留意。5.2 模型填错参数、类型不对怎么办模型填参出错几乎是必然事件重要的是系统层面能不能兜底。我的经验是引入三层防护第一层Schema 校验类型不对直接拒绝第二层通过配置约束参数来源明确告诉模型这个参数必须来自用户消息中的某个实体而不是模型自己的推断第三层出现一次错误后把失败信息回传给模型让模型重新阅读技能说明并改正参数这一步实测能挽回相当一部分失败调用。我曾经遇到一个用户场景用户说“帮我取消明天早上 9 点的会议安排”系统识别到动作是取消会议但参数里的会议 ID 模型填了一个根本不存在的编号。后来加了“出现参数错误后询问用户要取消哪一个会议并且列出用户最近的三个会议选项”这个恢复策略整个技能的完全失败率下降了近一半。对于需要谨慎操作的场景宁可多问一步也不要让模型猜参数。5.3 技能之间的状态污染问题技能多了之后另一个让人头疼的问题是状态污染。最常见的情况是技能 A 执行过程中缓存了一个变量技能 B 在同一个会话上下文中被触发读取到了技能 A 的残留变量导致执行逻辑完全错乱。解决思路是每个技能的上下文必须隔离执行时从会话中读取的公共信息要显式声明技能运行期间产生的中间变量要么限定在技能内部要么在技能结束时彻底清理。我早期做技能系统时曾主张“全局共享工具上下文”后来在线上环境被坑了几次才彻底改成按技能隔离。真要说有什么值得提醒的就是虽然一个会话里多个技能共享用户意图信息是合理的但执行状态绝不能共享。5.4 实测下来比较顺手的调试方式技能系统调试起来和普通代码调试有很大区别因为它多了一层模型的行为不可控性。我摸索出几个实用手段。第一做一个技能自测的命令行工具把技能描述、参数样例、执行结果打印在终端里这样调一个新技能时不需要启动整个 Agent 系统单一技能可以直接验证。第二给技能执行器加详细的日志日志里至少要包含模型选中的技能名、模型提交的原始参数、校验结果、执行耗时、返回结果摘要。第三使用录放回放模式调试——把线上的一批真实请求录制下来当技能配置修改后拿这批历史请求重新跑一遍对比新旧执行结果确认修改没有引入回归问题。这套流程虽然占用了一点额外开发时间但长期来看比每次改完配置都要重新手工点测试要高效太多也是能让技能系统像普通软件一样持续迭代的基础设施保障。最后再分享一个我个人的小习惯每次为 Agent 添加一个新技能时我都会先在文档里写清楚这四件事——这个技能的触发信号是什么、不能出现在什么场景下、它依赖哪些外部服务、它输出的结果会被谁消费。这四个问题的答案都清晰了再填技能配置文件、写业务函数就会顺手很多。技能系统建设的难点从来不是代码本身而是把“模型如何理解能力、系统如何执行能力”这两个层面安排得规则明确、配合默契。理解了这一点你手上的 agent-skills 就不会只是一堆配置文件的组合而更像一支分工明确、随时能执行落地任务的团队。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。