说实话LLM应用做到一定复杂度之后你会发现真正卡住你的往往不是模型能力而是怎么让模型稳定地复用一套已有能力。刚开始做Agent的时候我也跟大家一样什么逻辑都往System Prompt里堆结果提示词越来越长、效果越来越飘换个场景就得推倒重来。后来我开始把Agent的能力拆成一个个独立的技能模块这就有了agent-skills这组实验性质的设计与实现。这篇文章不会讲什么高深理论就是把这套技能层的设计思路、核心数据结构、以及我在实际搭建过程中踩过的坑尽量完整地记录下来。这套agent-skills体系解决的核心问题很简单让Agent具备一套可插拔、可注册、可被动态调用的标准化技能。它适合所有在做Agent应用的开发者尤其是被提示词越写越长但效果不可控折磨的朋友。你不需要完全照搬我的方案搞清楚它背后的设计取舍对你组织自己的Agent工程会有很直接的帮助。1. 为什么Agent需要一套技能层而不是单纯堆Prompt1.1 从一次重复性劳动说起大概半年前我帮团队做了一个内部知识库问答Agent。第一版方案特别朴素把知识库检索的步骤、结果处理的规则、回答的格式要求一股脑写进System Prompt里然后调用一个封装好的搜索工具。跑通了效果也还行。但接下来问题来了第二个项目要做代码仓库分析第三个项目要做运维日志巡检每个新项目几乎都要重写一遍提示词然后把搜索工具、参数提取逻辑重新接一遍。最让我抓狂的是知识库问答里那段怎样从检索结果里提取关键段落的指令我在三个项目里复制粘贴了三遍每次改格式要求都要同步修改多处地方。这种重复本质上是因为我把能力和使用方式焊死在了对话上下文里。Prompt只是文本它没有结构化的输入输出接口不能被注册、被索引、被别的Agent发现。你需要的是把一段能力封装成带名字、带参数、带执行逻辑的独立模块让Agent像调用函数一样调用它这就是技能层存在的意义。1.2 技能与工具、插件、工作流的边界做Agent开发的人肯定听过Tool、Plugin、Workflow这些概念。它们之间的边界很容易让人混淆我先用一张对比表把它们的定位区分开。概念本质粒度是否参与决策典型场景Prompt文本指令细直接指导行为告诉模型怎么说、怎么想Tool外部函数封装细不参与只执行调用搜索API、执行代码Skill一段能力单元中自带使用说明知识库问答、log分析、SQL查询Workflow多步骤流程编排粗负责流程分支巡检报告、多步骤审批工具只是一个动作没有使用场景说明也没有默认参数Prompt只是一段说法没有可验证的输出结构Workflow则是把多个步骤串成固定流程。而技能恰好站在中间它有Prompt来指导大模型如何执行也有脚本和工具调用来落地操作还带有输入输出的Schema约定。打个比方工具是你手里的扳手Prompt是告诉你怎么拧螺丝的方法而技能是一个拧螺丝工具箱——里面除了扳手还有操作手册、安全须知和质检标准。1.3 技能层要解决的核心问题我设计agent-skills的时候给自己定了四个必须解决的诉求。复用同样的能力在不同项目里可以直接引用不再复制粘贴提示词。标准化每个技能都有统一的元信息、参数定义、执行入口可以被程序化地识别和调用。可组合技能和技能之间能通过接口串联完成更复杂的任务链。可控技能的执行过程要可观测、可干预出错了能回退而不是黑盒。这四个词听起来很虚但落到工程上都是实实在在的设计决策。复用要求技能目录独立于主程序标准化要求定义资源配置格式可组合要求统一IO协议可控要求生命周期管理和执行日志。接下来的内容就是围绕这四个目标逐步展开的。2. agent-skills的核心设计一个技能到底长什么样2.1 技能文件的三段式结构我最早尝试过用单个JSON文件描述技能后来又试过纯代码形式。折腾一圈之后最终定下来的方案是一个目录即一个技能每个技能目录里包含三类核心文件元信息描述、执行指令、可运行脚本。一个标准技能包的目录结构是这样的skills/ └── knowledge_query/ ├── SKILL.yaml # 技能元信息名称、描述、参数Schema ├── INSTRUCTIONS.md # 给大模型的执行说明相当于操作手册 ├── run.py # 具体的执行逻辑可选有些技能纯靠LLM本身 └── examples/ ├── input_1.json └── output_1.jsonSKILL.yaml是技能的身份证负责声明这个技能是干什么的、需要什么参数、产出什么结果。INSTRUCTIONS.md是给模型看的操作手册比如分析日志时需要先按时间排序、生成回答时需要引用原文编号。run.py是具体的执行代码当技能需要真正调用外部系统时由它来处理。这种三段式结构要求你把能力和说话方式分离执行说明负责指导模型行为脚本负责落地的脏活累活元信息负责让Agent在千万个技能里认出它。一开始可能不习惯但一旦技能数量超过20个你会感谢这个结构因为它让技能的检索、更新、版本管理都变得有章可循。2.2 目录组织与命名规范技能多了以后如果没有命名规范Agent大概率会在调度时把日志分析和错误日志查询搞混。我采用的办法是按领域分模块每个领域一个顶层目录领域内的技能以动词名词命名。skills/ ├── analysis/ │ ├── log_pattern_detect/ │ ├── trend_forecast/ │ └── outlier_find/ ├── data_process/ │ ├── sql_query_generator/ │ ├── data_clean/ │ └── format_convert/ ├── comm/ │ ├── summary_email/ │ └── meeting_notes/这个做法的好处有两个。第一Agent在路由技能时可以先用顶层目录缩小搜索范围。第二命名本身包含了动作意图动词名词格式让技能名称天然具备语义sql_query_generator比data_tool_03更容易被模型理解和匹配。2.3 技能的声明式注册与自动发现技能不是直接被程序引用的而是通过一个注册中心被统一管理。我设计了一个简单的注册表机制程序启动时扫描整个技能目录解析每个目录下的SKILL.yaml把技能名称、描述、参数Schema、入口文件路径登记到内存注册表里。# SKILL.yaml 示例 name: trend_forecast description: 基于历史数据序列预测未来趋势支持线性回归和移动平均两种方式 version: 1.2.0 author: internal-ai-team tags: - analysis - forecast input_schema: type: object properties: data_points: type: array description: 按时间排序的历史数据点 method: type: string enum: [linear, moving_average] default: moving_average output_schema: type: object properties: forecast_result: type: array confidence: type: number这种自动发现机制带来的最大好处是即插即用。新增一个技能只需要把目录丢进去重启Agent或者触发一次扫描新技能立刻能被调度器感知。不需要改主程序代码不需要改数据库。我见过有些团队把技能配置写在主代码里每加一个能力就要发一次版本这种重操作会彻底打消你沉淀技能的热情。2.4 技能的生命周期管理注册进来的技能不是一直常驻的我给它设计了五个状态已注册、已加载、执行中、已暂停、已失效。用状态机管理的好处是Agent在执行任务时可以明确知道当前技能处在什么阶段也方便做并发控制和资源回收。执行一个技能的标准流程是这样调度器按需加载技能读取INSTRUCTIONS.md拼接进上下文。校验输入参数是否符合input_schema不符合则报错返回。执行技能逻辑如果是脚本类技能调用运行时执行run.py入口。输出结果按output_schema结构化整理。回收资源记录执行耗时和日志状态复位。这里每一步都有对应的超时控制和异常捕获。技能执行不是一把梭而是像对待一个微服务一样对待它有健康检查、有超时熔断、有失败重试。这个思路后来帮我解决了很多线上问题。3. 从零实现一个可复用的技能SDK3.1 脚手架与最小骨架有了设计蓝图就需要一套脚手架来降低创建技能的负担。我写了一个命令行的技能生成器执行后会自动创建上面说的目录结构和模板文件。agent-skills create skill --name data_clean --domain data_process这个命令会自动生成以下模板# run.py 模板 import sys import json def execute(input_data: dict) - dict: # 在这里实现技能的具体逻辑 result {status: success, data: input_data} return result if __name__ __main__: payload json.loads(sys.stdin.read()) output execute(payload) print(json.dumps(output, ensure_asciiFalse))为什么用标准输入读取JSON、用标准输出打印结果这是为了把技能执行包装成一套与语言无关的IO协议。任何语言写的技能只要能解析stdin、输出stdout都能被主控程序调用。这也为团队里不同技术栈的成员贡献技能提供了便利。3.2 让技能既会对话也会被调用技能在Agent体系里要面对两种调用者一种是大模型另一种是主程序的调度器。这两种调用方式对接口的要求不一样。大模型需要的是通俗易懂的描述和示例调度器需要的是严格的参数结构和返回格式。我的解决方案是把SKILL.yaml里的描述字段写得特别详细除了功能说明还要写上适用场景和不适用场景。同时在INSTRUCTIONS.md里加入few-shot示例让模型知道什么情况下该用这个技能、怎么解读执行结果。比如trend_forecast技能的INSTRUCTIONS里我会写当你接收到一组时间序列数据并需要预测未来走势时可以使用trend_forecast技能。注意该技能只适用于数值型数据如果数据中包含大量缺失值请先执行data_clean技能。预测结果中的confidence低于0.6时需要在回答中明确提示用户结果仅供参考。这种对话式的使用说明比起干巴巴的参数列表对大模型的引导效果要强得多。模型读了这个说明后不但知道怎么用还知道什么时候不该用大幅减少了误召回的几率。3.3 校验与错误边界设计技能执行过程中的错误处理是我觉得整个方案里最需要重视的环节。我在SDK里内置了三层错误捕获机制。参数异常输入不符合Schema要求直接拒绝执行并返回参数错误详情。这一步能把很多垃圾进垃圾出的问题拦在门外。执行超时每个技能有独立的超时时间默认300秒超时后自动终止进程并返回超时错误。防止某个技能卡死拖垮整个Agent。业务异常技能执行过程中发现数据异常或外部依赖不可用通过标准错误码返回由调度器决定是重试、改用其他技能还是向用户说明失败。尤其是重试策略一定要加上退避逻辑。最初我写的是固定延迟重试3次结果下游服务抖动时所有技能同时重试直接把数据库连接池打满了。后来改成指数退避第一次等2秒、第二次等4秒、第三次等8秒效果好很多。3.4 技能测试从单测到与Agent端到端联调很多Agent项目死在开发时看着没问题一上真实环境就露馅。技能层一样需要测试而且测试要分层。我在SDK里内置了一组pytest工具函数可以单独验证技能的输入输出函数、也可以和Mock的Agent主程序做端到端验证。单测层面重点覆盖边界条件和异常分支。比如sql_query_generator我会测空表名、特殊字符注入、超长字段名这些情况。端到端层面我写了一个模拟调度器能直接通过技能名称调用技能并检查返回结果的JSON结构是否满足Schema。这样每次修改技能代码后跑一遍测试就能快速知道有没有破坏原有的Agent调用链路。还有一件很重要的事测试时不要真的调用付费的外部API。我最初直接用真实API做回归测试一周下来账单让人滴血。后来我在测试环境搭了一个Mock服务模拟真实接口的返回格式和延迟所有自动化测试都跑在Mock上真实API只留到上线前的冒烟验证阶段。4. Agent调用技能的路由策略与调度体验4.1 路由是怎么发生的意图解析技能注册好只是第一步真正难的是让Agent在收到用户请求时自动选择合适的技能。我实验了两种路由方案基于关键词规则的匹配和基于向量语义的匹配。关键词匹配最简单。把技能描述里的核心动词和名词提取出来构建倒排索引用户请求命中关键词就映射到对应技能。这种方案在小规模技能库少于30个时表现不错特点是快、可解释、完全可控。但缺点是遇到用户换一种说法就容易漏掉。向量语义匹配是更通用的方案。用嵌入模型把用户请求和技能描述都转成向量然后在向量空间里算相似度选TopN候选技能。这个方案对语言表达的多样性更鲁棒但需要额外维护一套向量检索服务。我在项目里两种都用了先向量语义召回Top5再用关键词规则做精确过滤和排序。实践证明混合策略比单独用任何一种都稳定。4.2 冲突处理多个技能都匹配上了怎么办技能多了以后用户的一个需求经常能同时匹配多个技能。比如分析一下销售数据看哪些区域增长最慢可能同时命中trend_forecast、outlier_find和sql_query_generator。这时候调度器需要一套评分机制来给候选技能排序。我设计的评分公式是这样的总评分 语义相似度得分 × 0.5 描述关键词命中率 × 0.3 历史成功率 × 0.2历史成功率是所有上线技能的环境反馈数据技能在真实任务中执行成功一次就给它的这个指标加一分。这套评分机制看起来不起眼实际效果却非常明显越常用的技能越容易被选中形成一个正向循环。而那些描述模糊的技能因为历史成功率低慢慢会被边缘化促使技能维护者去优化描述。4.3 技能协作从单技能执行到技能链真实的业务需求很少是单技能能搞定的比如帮我把这份CSV里的异常数据清洗掉然后按月份汇总生成趋势分析。这实际上是三个技能的协作。为此我在调度器里加了一个轻量的技能编排模块。编排模块允许以配置的方式定义一条技能链声明各个技能的调用顺序和参数传递关系。chain: name: data_report_pipeline steps: - skill: data_clean input: raw_file output: cleaned_data - skill: sql_query_generator input: cleaned_data output: query_sql - skill: trend_forecast input: sql_result output: trend_result这里的参数传递规则是前一个技能的输出结构化字段自动映射到后一个技能的输入Schema。说白了就是让技能像管道一样串联起来。单个技能只负责局部的死磕如何编排、何时分支、怎么合并由调度器统一控制。这个设计也解决了一个很棘手的问题大模型在自由编排技能时经常犯错要么漏掉关键步骤要么多调用无关技能。把它变成显式的技能链配置后虽然灵活性有所下降但正确率大幅提升尤其适合那些流程固定的企业内部场景。4.4 人工介入与技能回退再聪明的Agent也有判断失误的时候。所以在技能执行链路里我给高风险操作预留了人工确认点。比如删除数据、发送邮件、执行写操作之前调度器会暂停执行向用户展示执行摘要和影响范围等确认后再继续。我实现了一个回退机制技能链执行过程中如果某个技能连续失败超过阈值调度器会触发回退把状态恢复到该技能执行之前并将上下文切换给一个兜底提示模板向用户说明目前无法完成请求。这个兜底设计在初期经常被触发后来随着技能成熟度提升触发率才慢慢降下来。如果没有它Agent会反复重试同一套失败的逻辑既浪费资源又让用户体验变得很糟。5. 跑通之后我总结的五个深坑与优化方向5.1 坑一指令格式写得太死模型反而不听最初我在INSTRUCTIONS.md里写满了硬性规则比如第1步必须做xxx、如果xxx则必须输出yyy。结果发现规则越刚性模型越容易在执行中途卡死。后来我改用建议式示例式的写法在说明里给出正反两个例子让模型通过例子理解期望行为。改完之后技能的稳定执行率提升了不少。这个现象背后的原因是大模型在执行开放任务时天然偏好理解和模仿而不是逐条遵守。技能说明不能写成编程语言的语法规范而要写成新员工培训时的导师语录——既给方向也留空间。5.2 坑二注册表膨胀之后变成了大杂烩技能数量到50个以后注册表就变得非常混乱。有些技能高度相似比如日志分析和日志异常检测描述几乎没有区分度导致路由频繁选错。后来我引入了一套标签治理机制每个技能必须声明所属领域、使用频率、维护负责人并在调度管理后台要求定期下线无效技能。我还会定期跑一遍技能调用日志看看哪些技能从未被调用。连续30天零调用的技能要么被淘汰要么重写描述。这种不破不立的治理态度保证了技能库始终是精兵状态而不是一个单纯越堆越大的垃圾场。5.3 坑三技能并行执行时的上下文污染支持技能链以后我允许部分步骤并行执行。结果发现多个技能同时运行时模型上下文里的临时状态会被互相污染。比如一个技能写入了一个当前任务字段另一个技能读取时读到的却是别人的上下文。这个问题的解法是在调度器里给每个技能执行单元分配一套独立的环境变量和上下文快照。执行结束以后再把需要对外暴露的结果拷贝到主上下文。这个改造做起来不复杂但非常关键它让技能的并行调度真正安全可用。5.4 坑四用真实API做测试烧钱又不可复现前文中途提过这件事但我还是想单独强调一遍。技能调试阶段如果用真实外部API除了烧钱还有一个隐形问题是不可复现下游接口的状态你控制不了测试结果不稳定。后来我建立了完整的Mock体系所有自动化测试全部走Mock服务真实调用只在用户主动触发时发生。这样不但省钱还能在CI流程里全自动跑完整套技能测试。5.5 下一步把技能库变成团队资产我的这个agent-skills项目做到了当前阶段最深的体会是技能库不应该是一个人的积累而应该是一个团队的基础设施。我现在正在做的是把技能包的存储迁到团队共享仓库配合代码评审机制让大家能够互相借鉴已沉淀的能力。另外我也在尝试让技能包支持模板参数——定义一个技能模板时留几个参数位比如数据源地址、API密钥变量这样同一种能力可以在不同项目里实例化成多个技能实例。模板不定死参数可填充复用范围会更广。这一整套方案说到底是给Agent装了一个更聪明的工具箱工具还是那些工具但摆放方式、标签识别、取用逻辑、协作分工都变得井井有条。未来等技能包数量上百也许还需要引入语义化的技能推荐和冷启动评估但那是后话了。至少现在我再也不用为每个新项目复制粘贴那套又臭又长的提示词了。