如果你做过基于大模型的Agent类项目大概会遇到一个很常见的尴尬一开始把工具调用、任务流程、上下文处理全部塞进系统提示词里看起来什么都会一点可真的跑起来以后要么指令互相打架要么新增一个能力就得把整段提示词重写一遍。我上个月刚把一个内部知识库Agent从灵光一现的提示词重构为显式技能库也就是我在标题里写的 agent-skills 这套实践。这篇文章不是给你一个开箱即用的框架而是把我在做技能抽象、注册、编排和调试过程中踩过的坑以及为什么这么设计全部摊开来讲明白。如果你正准备给Agent加能力或者已经因为技能之间互相耦合而开始头疼可以照着这个思路重新捋一遍。1. 从什么都会一点到专精分工技能层为什么值得单独做最开始做Agent Demo的时候我的方案很朴素把所有能力都写进系统提示词里。角色设定一段工具说明一段任务流程一段。听起来很直接对吧效果其实也还行因为Demo里只有两三个技能。等真正把这个思路往生产环境推问题就开始成串地冒出来了。首先提示词是线性文本所有技能描述被拼接在一起模型注意力会被严重稀释。我实际测过技能数量从4个涨到12个之后选错工具的比率从不到3%跳到了15%上下。为什么会这样因为技能描述之间有语义重叠。比如查询用户资料和获取用户联系方式在模型眼里几乎是一回事它经常挑一个看起来顺眼、但实际不适用的工具。模型不是不懂你的任务而是你的技能清单让它选择困难。其次是变更成本。每加一个技能都要重新刷一遍系统提示词还要担心新增的描述把之前稳定的能力挤掉。加上提示词长度本身有限制总有一天会装不下。当时我更崩溃的是另一个问题任何一个技能出现异常整个系统提示词链路都会受影响日志里根本分不清是模型选错了技能还是技能本身执行出错。所有的错误都在一口锅里炖排查起来极度痛苦。到了这个阶段我意识到问题出在架构上而不是某个技能实现上。Prompt里塞的是能力的描述但不是能力本身把描述当能力用迟早会出问题。于是我把整套架构切成两层一层是决策层也就是模型本身只负责根据对话上下文选择和编排技能另一层是执行层由一套独立的技能注册中心管理具体能力。这就是 agent-skills 的起点。决策层不再关心技能内部长什么样执行层也不关心模型用的是什么、怎么被调用。两者之间只通过一套固定的协议通信互不越界。这个拆分的收益几乎立竿见影新增技能时不需要改动提示词注册一份元数据即可模型看到的是精简后的技能清单而不是一大段混杂的描述每个技能可以单独测试、单独加日志、单独灰度技能执行失败时不会污染决策层上下文也不会让整个对话流程雪崩。后面所有的工作包括注册中心设计、编排模式、调试手段、评测方法都是在这条边界上展开的。1.1 技能与大模型原生工具调用机制的区别很多人第一反应会问模型不是本来就有工具调用机制吗再自己造一个注册中心是不是多此一举我的理解是模型原生的工具调用解决的是把请求转成结构化参数这一层问题。它约定了输入输出格式但完全没有规定技能应该怎么建模。同样一个模型你可以把工具定义成一段本地函数也可以把它定义成另一个Agent、一个HTTP接口、一条数据库查询。原生机制只是一个管道管道里跑的东西需要你自己设计。在我现在的实践里技能被抽象成输入-处理-输出的通用计算单元而不是绑定某一种具体实现。这样做的好处是让模型学习的是哪个技能名对应哪个场景这种稳定映射关系而不是每次都要解析五花八门的工具签名。技能类型底层实现暴露给模型的形式检索类向量库召回加重排统一skill_name query操作类内部API调用统一skill_name params计算类本地Python函数统一skill_name inputs流程类多步骤子Agent统一skill_name 任务描述拿一个实际场景举例。某内部知识库Agent里有一个查员工信息的技能底层调用的是人事系统接口还有一个查组织架构的技能底层调用的是同一个系统、同一个接口的不同端点。如果让模型直接面对这两个接口的原始参数它经常会混淆字段但把它们包成两个语义清晰的技能之后模型只需要从查员工信息和查组织架构里二选一输入参数也都统一成员工姓名或工号准确率一下子提上来了。2. 技能注册中心的抽象设计接口、元数据与运行时解耦技能注册中心是整个 agent-skills 体系的中枢。它的作用不是存技能而是把技能的元信息和实际执行分开让决策层只跟元信息打交道执行层只跟运行逻辑打交道。2.1 技能注册表的数据结构每个技能在注册中心里有一条类似下面这样的元数据记录{ skill_name: query_employee_info, description: 根据员工姓名或工号查询员工基本信息适用于入职时间、职位、部门等信息的查询。当用户询问某个人的联系方式或所属团队时使用。, parameters: { type: object, properties: { employee_id: { type: string, description: 员工工号如果用户提供了工号则优先使用工号 }, employee_name: { type: string, description: 员工姓名如果没有工号时使用姓名查询 } }, required: [employee_name] }, executor: internal_api_employee_reader, tags: [employee, info, contact], permission: read:employee, timeout_ms: 3000, version: 1.2.0 }这套字段里最需要花心思的是description和parameters因为它们直接决定模型能不能正确调用。后面踩坑章节里我会细说这里先强调一点description不是写给人类看的注释而是写给模型看的调用说明书。它必须说明什么时候用、什么时候不要用、参数怎么填、返回什么。2.2 执行层与注册层的解耦注册中心只保存元数据和一个执行器引用不直接嵌入业务逻辑。运行时通过一个分发器dispatcher把请求打到具体的执行器上。这个设计最大的价值是支持插件式注册——新技能写好后只要实现统一接口并注册进去就能被模型调用完全不用改Agent主流程。我用的执行器接口比较简单类似于这样class BaseSkillExecutor: async def execute(self, params: dict, context: dict) - dict: 统一执行入口。 params: 模型按技能参数schema生成的结构化参数 context: 运行时上下文包含会话ID、用户ID、权限信息等 返回: 结构化结果必须为可JSON序列化的dict raise NotImplementedError每个技能必须是一个可独立运行的模块。我见过一些项目把技能实现直接在Agent主文件里一手写完这就又回到一口锅里炖的局面了。拆成独立模块之后好处是可以在不启动完整Agent的情况下单独调试某个技能跑一个技能自测脚本直接喂参数进去看结果。2.3 技能描述给模型看还是给人看这个问题经常被忽略。在注册中心里技能描述同时被两个角色消费模型和开发人员。很多人写描述时默认读者是未来的维护者所以写得又长又泛结果模型根本不知道什么时候该用这个技能。我总结出一个相对好用的描述模板当用户想要[场景A]或[场景B]时使用此技能。不需要处理[场景C]。参数[名称]必须来自[来源]如果用户未提供你可以通过[其他技能]获取。返回结果为[格式说明]其中[关键字段]的含义是[说明]。这样写有三个好处第一让模型快速建立场景→技能的匹配关系第二通过不需要处理的排除项降低误用率第三明确参数来源和返回值语义模型就能在多个技能的衔接中做出合理判断。3. 编排模式顺序、条件与并行技能如何组织而不失控注册中心解决了技能怎么被找到的问题但一个真实任务往往需要多个技能接力完成。比如用户问帮我查一下A同事所在的团队最近上线了什么项目这个任务至少涉及三个技能查同事信息、查团队列表、查项目动态。技能之间怎么串起来就是编排层要做的事。3.1 顺序编排上一个技能的输出是下一个技能的输入最基础的编排就是链式调用。模型先调用技能1拿到结果后把技能1的输出作为参数的一部分再调用技能2。这个模式看起来简单但有一点需要注意不能让模型临场决定把上个技能的完整原始返回直接塞给下个技能那样很容易因为字段格式不一致导致失败。我的做法是在技能返回结构里加入一个轻量级的extracted_data字段专门放可以被下一个技能直接使用的核心数据。比如查同事信息返回的完整JSON可能有几十个字段但下一个技能真正需要的可能只有department_id。在技能内部就把这个字段提取好编排层传递时只传extracted_data既可以减少模型处理负担也能屏蔽底座接口波动。3.2 条件分支与循环让模型做路由而不是思考更复杂的场景需要条件分支。比如如果查不到员工信息就尝试用别名查询、如果团队没有项目动态就返回空结果而不是报错。这类分支如果全部要求模型自己推理出全套流程通常会变成一场灾难因为模型在长流程中容易跳步。我在实践中的策略是把分支逻辑写成显式的规则让模型只负责在当前节点选择下一跳。也就是说模型做一个决策执行层就检查一个条件条件满足就继续不满足就按预设路径降级。这样可以避免模型一口气把五六步全部规划完然后在第三步就出错的情况。这里给一个我用过的简单决策矩阵条件动作降级路径技能A执行超时重试一次如果重试仍失败转人工提示技能B返回空结果触发技能C进行补充检索技能C也无结果时返回未找到技能D需要权限但用户无权限跳过D返回部分结果记录日志并提示用户权限不足这种每步一判的方式跟我之前让模型一口气规划到底相比成功率显著更高。毕竟模型在单步决策上的可靠性远高于多步规划。3.3 并行技能共享状态是最容易埋雷的地方有些任务里多个技能之间没有依赖关系可以并行执行以缩短总耗时。比如用户想同时知道天气和会议室状态这两个技能互不依赖。但并行带来的一个坑是共享状态冲突。我踩过一次很典型的坑两个并行技能同时写入同一个会话上下文变量导致其中一个技能读到的数据被另一个技能覆盖。后来我定了一条铁律并行技能之间禁止共享可变状态。如果某个技能确实需要读取另一个技能的运行结果那就把它们改成顺序编排或者在执行前把所需数据通过参数显式传进去。并行执行时每个技能拿到的context必须是同一个会话共享上下文的一个只读快照写操作要么不写要么只写进自己的局部命名空间最后再由编排层统一合并。3.4 编排层的超时与降级任何一个技能都可能慢、可能挂、可能返回不合法数据。如果编排层没有兜底机制一个技能挂了整个Agent调用链都会卡住。我现在的做法是给每个技能设置独立的超时阈值并且在编排层设置总预算时间。技能超时后不阻塞主流程而是生成一个技能异常报告返回给决策层让模型决定是换一个技能还是直接给用户一个可接受的答复。降级还有一个容易被忽视的点降级动作本身也要有预算。如果降级链路又调用了三个技能实际上是把超时问题放大了。我一般限制降级链路最多跳转两次超过就返回固定的安抚提示。4. 真实项目里的踩坑记录与调试方法论这一节我把实际开发中遇到的最有价值的问题整理出来每一个都是真金白银换来的经验。4.1 技能描述写太长模型反而不会用了我最早给技能写描述总想把所有细节都交代清楚一段描述写到两百多字。结果模型经常在几个语义相近的技能之间犹豫甚至有时干脆报没有合适的工具。后来我做了一个对照实验同一个技能把描述压缩到60字以内并明确写清使用条件和禁止使用条件模型调用准确率从76%涨到了90%。为什么会这样因为模型在决策时本质在做一次极度受限的文本匹配。描述越长匹配的模糊空间越大短描述反而让边界更清晰。所以我现在写描述的原则是能用20个字说明白的场景绝不用40个字。4.2 参数Schema嵌套太深模型生成的参数总是漏字段这是另一个高频问题。技能A需要查项目列表项目列表需要过滤条件过滤条件里又要传排序规则和分页参数于是我把parameters嵌套了三层。结果模型经常只生成第一层的字段内层参数干脆丢掉了。后来我把参数Schema全部拍平能拆成多个独立技能就拆开不能拆开的就把嵌套结构控制在一层。模型生成参数的可靠性提升非常明显。这里面有个规律模型对结构化参数补全的能力远不如人类直觉想象的那么强你的Schema每多一层出错率大约翻一倍。这句话我是在项目里反复验证过的。4.3 技能副作用与重试不是所有技能都可以无脑重试最初我给所有技能统一加了失败重试一次的逻辑结果出过一次事。某个发送通知技能因为下游接口超时导致第一次调用其实已经发出去了但Agent这边收到的是超时错误于是触发重试结果用户收到了两条一模一样的通知。从此之后我把技能按是否有副作用分成了两类。查询类技能可以无脑重试有副作用的技能发消息、改状态、下单默认禁止自动重试只返回失败原因由决策层决定要不要换一个方式或请求用户确认。这个分类建议直接写进技能元数据里像side_effect: true/false这样的字段重试逻辑在编排层统一读取。4.4 调试方法论先看模型选没选对再看技能跑没跑对踩过这么多坑之后我总结出了一套排查套路。当Agent回复有问题时先不要急着看技能代码而是先看日志里模型这一步的决策记录。完整的排查链路是这样的确认模型选择的是哪个技能、输入参数是什么如果选择错误去对比技能描述和用户问题的语义差异如果选择正确再看执行器是否正常返回、是否有异常日志异常日志里重点看超时、参数校验失败、下游接口报错如果是下游接口问题判断是重试可解还是需要降级。我在注册中心里给每个技能挂了两层日志决策日志模型选了什么技能、为什么选和执行日志参数、耗时、状态码、返回摘要。并把它俩按request_id串联起来。这样每一次用户会话都可以从决策起点一路追到执行终点排查效率高了很多。另外还有一个细节技能返回值一定要做结构化归一。不同技能内部用的数据源五花八门但返回给决策层的格式必须统一否则模型在编排时会因为看不懂某个返回结果而做出错误的下一个决策。5. 技能评估与迭代怎样让一套技能库越用越好用技能库不是写完就完事的它和代码库一样需要持续迭代。没有评估机制的技能库最后一定会退化成谁都不敢动的老代码。5.1 构造技能选择的评测集我建了一个相对小但有效的评测集大概三百条用户问题覆盖了Agent的主要业务场景。每条评测数据包含三部分用户原始输入、期望使用的技能名、期望的参数关键字段。每次对技能描述做改动或者新增技能我就在这个评测集上跑一遍统计技能选择准确率和参数正确率。这个评测集一开始是从历史对话日志里人工挑出来的后来按场景类型做了补充比如模糊提问、别名提问、跨技能提问。它不需要很大但一定要覆盖关键边界。5.2 覆盖率与误用率光看准确率是骗自己的评估技能选择时只看正确率会掩盖很多问题。比如某个技能调用次数很少即使选对了对整体指标的贡献也不明显但它在业务上很关键。所以我还会额外统计每个技能的覆盖率和误用率。覆盖率该场景出现时技能是否被选择 误用率技能被选择时是否其实不应该用它。这两个指标结合起来能发现一类很隐蔽的问题某个技能描述写得太泛导致它频繁被选中但它真正合适的场景只占少数。这种技能就需要把描述收窄或在描述里加更明确的禁止使用条件。5.3 技能版本管理与灰度技能升级和代码升级一样需要有版本概念。我把技能元数据里的version字段作为硬性字段每次改描述或改执行逻辑都必须递增版本号。同时在Agent的配置层加了一个技能灰度比例的开关可以控制某个新版本技能只对固定比例的流量生效。灰色发布这一步特别重要。有一次我想把某个检索技能换一个重排模型如果没有灰度直接上线可能会有相当比例的检索结果出现质量问题通过灰度我先让5%的流量走新版本观察一天再逐步放大。这个习惯帮我躲过了好几次隐性事故。5.4 维护技能库的三条经验最后分享三条我觉得最实在的维护经验。第一技能数量超过20个之后每新增一个技能都必须回答一个问题已有技能是否已经覆盖了这个场景如果覆盖就去优化原技能描述而不是新增。技能越多模型选择负担越大边际成本并不低。第二定期做技能调用审计。我每周导一次上周的技能调用占比发现那些连续数周调用率为零的技能要么是有Bug没人修要么是描述跟实际业务脱节了。对零调用技能我会选择下线或重构而不是让它继续占着注册中心的位置。第三技能内部不要写一次性业务代码。如果一个技能只服务某一个特定问题它就是伪技能早晚会被更通用的技能替代。真正值得注册成技能的东西必须满足可复用和场景明确两个条件。收尾一点个人的体会在做完这套技能库重构之后我最大的体会是Agent的复杂度不会因为你把东西都塞进提示词而消失它只是被你藏起来了然后在你最想不到的地方反咬一口。把技能显式地建模、注册、编排、评估本质上是在把模糊的复杂度变成结构化的复杂度。后者虽然看起来多了很多代码和流程但它可测试、可观测、可追溯这才是生产环境的真正底线。如果你也在做类似的Agent项目我建议不要一上来就做一个大的Agent框架而是先用一个技能注册中心加两个技能把调用链路跑通再慢慢往上面加编排、加评估、加灰度。这条路我走下来整体的感受是前面慢一点后面省出来的时间远超预期。