Agent技能库设计与实现:从工具列表到可编排技能系统 先把话说在前面这个标题看起来就三个词但做 AI Agent 的人应该一眼就明白这是奔着“技能系统怎么设计”去的。我在实际项目里折腾了大半年从最早把所有工具塞进一个数组到最后整理出完整技能库中间踩的坑比写业务代码多得多。这篇就完整拆一遍 agent-skills 的核心思路、定义方法、实现链路和排障经验全程是能直接抄作业的干货。1. Agent技能库为什么不能把工具全塞进一个列表1.1 技能库要解决的真实问题很多第一次接触 Agent 开发的人会有一个天真的想法我有十个工具函数全都传给模型让模型自己选不就行了。这在小 Demo 里确实能跑比如三个计算函数、两个查天气接口模型每次都能选对。但一旦业务复杂起来事情就不受控了。我在某跨平台系统的 Agent 模块里接过一个需求需要让智能助手同时处理订单查询、库存校准、物流轨迹跟踪、售后话术生成、报表导出、权限核验、第三方 ERP 同步等二十多个操作。如果按照最初方案把所有函数直接塞进 system prompt 加 tools 列表实际跑起来会出现几个很致命的问题。第一个问题是 Token 消耗失控。每个函数的描述信息、参数定义加起来轻松超过两千 Token二十个就是四万往上调用一次模型就要烧掉这么多 Token还没干活先亏一大笔。第二个问题是模型选择困难。当候选函数超过十五个时模型的命中率会明显下降尤其两个函数参数大部分相似、功能却不同的情况模型经常选错。第三个问题是权限没法精细管。函数列表是平铺的谁都能调但实际业务里查询自己订单和查询别人订单的权限等级完全不同。技能库存在的意义就是把“所有工具的平面列表”升级成“一组可索引、可校验、可编排、可观测的技能集合”。它不是简单包装一层函数而是从工程的视角重新定义 Agent 调用外部能力的边界和方式。这也是 agent-skills 这类系统越来越受欢迎的根本原因当 Agent 从玩具走向生产环境技能的组织方式直接决定了系统的稳定性、成本和可维护性。1.2 技能与函数的本质区别做完这个项目后我对技能的理解已经不再是最初那个“技能函数”的认知了。函数是代码层面的最小执行单位处理的是“输入进去、结果出来”的逻辑而技能是 Agent 层面的能力封装它至少包含四层信息做什么、什么时候用、怎么调用、出错了怎么办。举个生活化的类比函数像是一台咖啡机你往里放豆子它给你出咖啡。但技能像是“会做咖啡的金牌店员”这位店员不仅知道咖啡机的操作还知道你加糖加奶的偏好知道咖啡机常出现的两三个小故障怎么排除知道客人要求不喝咖啡因时推荐什么替代品。Agent 需要的不是一个能跑的函数而是一个能理解情境、灵活应对的技能。在设计技能库架构时我会把技能定义成下面这种结构体{ skill_name: check_order_status, description: 查询特定订单的实时状态适合用户询问订单进度时使用, capability_type: order_management, input_schema: { type: object, properties: { order_id: { type: string, description: 订单编号通常以ORD开头 } }, required: [order_id] }, output_schema: { type: object, properties: { status: { type: string }, estimated_delivery: { type: string } } }, permission_scope: owner_or_admin, execution_timeout_ms: 5000, fallback_skill: query_order_via_customer_service, tags: [order, query, real-time] }这段 schema 里每一行都有它的用意后面我会详细拆开讲。关键要理解的是技能比函数多出的那几层信息恰好就是让 Agent 能“正确选择、安全执行、优雅失败”的基石。2. 技能拆解与定义规范怎么做原子化设计2.1 技能拆解的两条原则我从实际项目中总结出两个拆解原则基本上所有技能设计都能套用。第一条是单一职责。每个技能只做一件事而且是粒度尽可能小的一件事。比如“查询订单”和“修改订单收货地址”必须拆成两个独立技能不要试图做一个“订单管理”的大函数包。因为 Agent 工具选择的准确率很大程度上取决于每个工具描述是否足够清晰、参数是否足够聚焦。单一职责的技能描述起来简单模型理解起来也直接而且索引和复用都方便。第二条是业务闭环。虽然每个技能负责的事很小但一个技能从进入到结束它的状态转换必须是闭环的。举个例子如果做“订单退款”技能不能只调一个接口返回“成功”或“失败”你需要把退款的状态、退款流水号、预计到账时间都拿回来必要的时候还要把这个技能拆成“创建退款申请”和“查询退款进度”两步。闭环节点上是 Agent 判断后续动作的依据状态不清的技能会导致 Agent 反复调用同一个接口浪费钱还影响体验。这两条原则放在一起看会有一个挺大的收益技能集变成了一组高内聚低耦合的能力单元后面做 Agent 编排、热更新、权限管理都不需要动代码只改配置就行。2.2 技能描述怎么写模型才爱看技能描述是给大模型看的“用户说明书”这个文本的质量直接决定了工具选择的准确率。我见过很多人写了半天工具函数描述全是“该函数用于订单查询”“此接口用于库存管理”这种描述别说让模型精准判断了人来读都读不出边界。我自己的写法是遵循一个操作公式触发场景 执行内容 边界说明。举一个实际的例子改写前查询订单信息改写后当用户想了解订单当前状态、物流情况或预计送达时间时使用。传入订单编号返回订单当前状态、承运商、物流单号和预计送达时间。若订单不存在或已超过售后时效请改调查询售后进度技能。这段描述起码包含了四个关键信息什么情况触发、需要什么参数、返回什么信息、边界情况怎么办。模型拿到这段描述基本能做准确的工具路由。这里要提醒一个很容易踩的坑描述里不要写否定句。例如“不要用于查询售后进度”模型对否定信息的辨识度远低于对正向信息的辨识度它看到“售后”两个字反而容易触发误匹配。正确的做法是把“售后查询”作为另一个技能的名称和触发描述写清楚弱化当前技能的负责范围。我后来对整份技能描述做了一次系统性重写工误差错率直接降了差不多 30%。这个环节的价值被严重低估了。2.3 输入输出 Schema 的设计要点输入输出 Schema 决定了模型能不能正确提取参数也决定了你对技能结果的解析方不方便。这块我有三个切身体会的经验。第一个经验是参数描述必须写清楚枚举和约束。比如订单类型字段你要明确列出“normal、presale、resale”并说明每个值代表的含义。模型在提取参数时会优先匹配语义枚举描述不清容易把“预售订单”提取成“presale_order”。这个错误很隐蔽返回结果却是错的。第二个经验是输出 Schema 不要限制太死。很多时候技能内部会调用多个外部接口结果字段的可变性很强。我的做法是 output_schema 只保留公共核心字段额外的数据统一放 ext 字段由下游按需解析。这样做的好处是技能内部升级时不用频繁修改 Agent 端的数据结构能够在一定程度上降低耦合。第三个经验是必填参数越少越好。模型提取参数零错误的概率并不高必填项越多越容易触发校验失败。设计时我会把所有参数都设为可选然后在技能内部做兜底逻辑比如缺省时间范围就默认查近一个月缺省排序规则就默认按时间倒序。用默认值换兼容性在 Agent 场景下行得通。3. 技能注册、检索与调度的实现链路3.1 技能注册中心的设计当一个项目的技能数量超过十五个以后你就不可能再把它们写在一个常量文件里了。我的实践方案是实现一个技能注册中心统一管理技能的元数据、实现代码和运行时状态。注册中心的存储我选用的是本地 JSON 文件加内存索引的组合没有引入复杂的服务发现框架。原因很简单Agent 技能集本质上是静态配置加动态扩展但动态扩展的频率极低大多数时候是发版时更新。JSON 文件可读性好方便人工审计而且加载后建立内存索引查询效率非常高。注册的流程是启动时扫描技能目录读取每个技能目录下的 skill.json 定义文件。对技能定义执行校验校验项包括技能名是否合法、描述是否为空、输入 Schema 是否符合 JSON Schema 标准、实现函数是否存在。加载实现模块注入运行时依赖。这一步我通常会做一个依赖解析保证技能内部需要的服务客户端都已被初始化。建立索引包括技能名索引、能力类型索引、标签索引。这里有一个容易被忽略的点技能注册过程必须做版本校验。如果加载的技能定义和当前代码版本的预期不符比如新增了参数但实现函数还没有更新直接启动会让线上 Agent 的调用逻辑全部错乱。我后来在注册环节加了一层哈希校验实现代码和技能定义打包时生成统一版本号版本不一致直接拒绝注册。3.2 基于向量检索的技能路由技能数量一大靠关键词前缀匹配技能名就完全不够用了。我在 Agent 的工具选择链路中引入了一个向量检索层效果提升非常明显。具体做法是每个技能注册时利用嵌入模型把技能描述转换为向量存进一个轻量的向量索引里面。当用户提问进来Agent 准备选择工具时先把用户问题也转为向量在技能库里做相似度检索取 Top 8 个候选技能拼成缩减后的 tools 列表再交给大模型做最终选择。这个方案的收益是双重的首先是大模型的 tools 列表从二十多个缩减到八个左右token 消耗降了一半以上其次是模型的选择准确率显著提高因为候选集里都是和用户问题高度相关的技能干扰项少了很多。我测试过一组对比数据同一个用户问题全量工具列表时模型准确命中率为 76%用向量召回 Top 8 后再选准确率提升到 94%并且首次调用耗时也缩短了将近一半。这个数据只是代表我的业务场景不同模型和不同技能集的表现会有差异但方向是一致的。实现的时候要注意一个细节向量检索的候选数怎么定。太小了容易漏太大了又回到全量列表的老路。我的经验是从 6 到 12 之间找最优值配合你技能集的平均语义重叠度来调。如果技能之间描述差异明显6 个就够如果技能描述有很多近义表述建议上线前做一次混淆测试来确定。3.3 技能调度器的执行逻辑检索到候选技能后就要进入技能调度器。调度器是整个技能系统里最容易出幺蛾子的环节很多人只关心“模型选出哪个技能”忽略了“技能是怎么被执行起来的”其实执行阶段藏着很多性能和安全问题。调度器的主要职责有三块权限校验、超时控制、并发限制。权限校验这里我的做法是维护一张“角色到技能”的映射表。普通用户只能调普通查询技能管理员才能调配置类技能。在技能定义里加 permission_scope 字段调度器执行前先核对当前会话的角色不符合直接返回权限不足不浪费模型一次调用。超时控制是必须做的。模型调用技能时它是同步等结果的如果技能内部调用的外部接口非常慢整个 Agent 响应就会被拖住。我给每个技能设计了独立的超时阈值超时后立即返回一个可读的错误信息让 Agent 决定是重试还是改走其他技能。这个设计在面向真实的用户请求时非常重要做 Demo 时可能感受不到但线上没人愿意等你十秒。并发限制针对的是某些稀有的外部资源。比如同一个第三方物流查询接口每秒最多允许 20 次调用那技能调度器就需要维护一个简单的滑动窗口计数超过阈值就排队或降级。4. 实操案例从零搭建一个可运行的技能库4.1 环境准备与基础框架选型我以自己常用来验证技能库方案的“某智能助手项目”为例完整演示一下从零搭建的过程。技术栈选择Python 3.10、大模型接口调用、轻量 Web 框架做演示入口。向量索引选用的是一个支持本地运行的轻量库具体品牌不重要关键是支持余弦相似度检索。整体目录结构如下agent-skills/ ├── skills/ │ ├── check_order/ │ │ ├── skill.json │ │ └── impl.py │ ├── refund_query/ │ │ ├── skill.json │ │ └── impl.py │ └── inventory_check/ │ ├── skill.json │ └── impl.py ├── registry/ │ ├── __init__.py │ └── registry.py ├── scheduler/ │ └── dispatcher.py ├── router/ │ └── vector_router.py └── main.py这个结构看起来简单但已经覆盖了技能定义、技能实现、注册中心、路由检索、调度执行五个核心模块。我强烈建议从这个小结构起步不要一上来就套分布式框架很多问题的根源反而是过度设计。4.2 技能定义 JSON 的标准化落地看一下技能定义文件的标准化写法以“检查订单状态”技能为例{ skill_name: check_order_status, description: 当用户查询订单发货状态、物流进展或预计送达时间时使用。传入订单号返回详细状态信息。若订单已取消或退款引导用户使用退款进度查询技能。, capability_type: order_service, tags: [order, tracking, status], input_schema: { type: object, properties: { order_id: { type: string, description: 订单编号通常以ORD开头例如ORD20250101 }, include_timeline: { type: boolean, description: 是否返回物流时间线默认false } } }, output_schema: { type: object, properties: { order_id: { type: string }, status_code: { type: string }, carrier: { type: string }, tracking_number: { type: string }, estimated_delivery: { type: string } } }, permission_scope: user, execution_timeout_ms: 3000, fallback_skill: refund_query }这个文件里的每个字段都不是随便写的。比如 execution_timeout_ms 设为 3000 毫秒是因为这个技能内部调用的是内部订单服务的查询接口平时 P99 耗时在 500 毫秒左右10 倍的超时余量能够应对瞬时抖动又不至于让用户等太久。如果是一个调用第三方物流查询接口的技能我会把超时设成 5000 毫秒因为外部服务的稳定性不可控。4.3 技能实现函数的写法与依赖注入每个技能目录下都有一个 impl.py里面实现真正执行的逻辑。我的写法如下class CheckOrderSkill: def __init__(self, order_service, loggerNone): self.order_service order_service self.logger logger def execute(self, params: dict, context: dict) - dict: order_id params.get(order_id) include_timeline params.get(include_timeline, False) user_id context.get(user_id) if not order_id or not order_id.startswith(ORD): return { success: False, error_code: INVALID_PARAM, message: 订单号格式不正确请检查订单号以ORD开头 } order_info self.order_service.fetch_order(order_id, user_id) if order_info is None: return { success: False, error_code: ORDER_NOT_FOUND, message: 未找到对应的订单或当前账户无权查看该订单 } result { success: True, order_id: order_info.id, status_code: order_info.status_code, carrier: order_info.carrier, tracking_number: order_info.tracking_number, estimated_delivery: order_info.estimated_delivery.strftime(%Y-%m-%d) } if include_timeline and order_info.events: result[timeline] [ {time: e.time.isoformat(), event: e.event} for e in order_info.events ] return result这段实现里有几个细节我说一下。构造函数里的依赖注入是非常关键的。技能内部不要自己去 new 一个服务客户端而是由注册中心统一注入。这样做的好处有两个一是统一管理外部连接避免每个技能重复创建连接导致资源浪费二是测试方便注入一个 mock 服务就能做单测。方法的第二个参数是一个 context里面装着会话上下文信息比如当前用户、角色、语言偏好等。技能实现时如果需要边界判断从这里取而不是从 params 里读用户信息。完全可信的用户信息放在 context 里由调度器从会话系统拉取后传入这个边界一定要维持住。返回值统一使用 success 标记外这种方式的优势在 Agent 端能够快速判断执行结果。模型看到 success 为 false 时会自动转向 fallback 技能或给出提示性回复比让模型从一堆异常文本里猜测发生了什么高效得多。4.4 注册中心的实现逻辑注册中心的代码不复杂但逻辑要严密。核心是一个扫描加载函数def load_all_skills(self, skill_root_dir): loaded [] for skill_dir in skill_root_dir.glob(*/): skill_json skill_dir / skill.json if not skill_json.exists(): continue skill_def json.loads(skill_json.read_text()) module_path fskills.{skill_dir.name}.impl module importlib.import_module(module_path) skill_cls getattr(module, skill_def[entry_class]) skill_instance self.dependency_inject(skill_cls, skill_def) skill_instance.validate() loaded.append(SkillRuntime(skill_def, skill_instance)) return loaded这里容易踩的坑是模块导入顺序和循环依赖。技能之间尽量不要互相导入如果有类似“查询订单之后拉起物流查询”的联动需求不应该在技能内部 import 另一个技能模块而应该把这种联动逻辑放在更高的编排层或者 Agent 的思维链中。技能保持独立是系统可维护性的重要保障。校验逻辑也必须放在加载时。我是用一个独立的 validate 方法检查每个技能的输入输出 schema 是否符合预期同时检查实现函数里是否包含了危险操作。比如技能内部出现 eval、特殊文件删除等直接判为校验失败。Agent 技能库的系统权限一旦被滥用影响会非常大这部分宁可严格不能放松。4.5 向量检索层与最终调用链路向量路由核心逻辑如下def retrieve_candidates(self, query: str, top_k: int 8): query_vec self.embedding_model.embed(query) scores [] for skill_id, skill_vec in self.index.items(): score cosine_similarity(query_vec, skill_vec) scores.append((skill_id, score)) scores.sort(keylambda x: x[1], reverseTrue) return [skill_id for skill_id, _ in scores[:top_k]]这段代码很简单真正需要注意的是索引更新策略。技能库发版更新技能定义后向量索引必须同步更新。我的实现里给每个技能加了向量版本字段注册时如果发现向量版本和索引库不一致就重新计算并更新条目。这样避免了技能描述调整后向量还停留在旧版本的问题。完整调用链路是用户输入进入 Agent 框架。Agent 判断需要寻找外部能力时调用路由检索层。向量路由返回 Top 8 候选技能。Agent 构建一个精简后的函数调用候选池。大模型选择其中一个技能并提取参数。调度器执行权限校验、超时控制、并发控制然后调用技能实现。执行结果返回给 Agent 上下文模型基于结果组织回复文案。整个链路跑下来一次带技能调用的对话延迟在 1.5 秒到 3 秒之间核心耗时都在大模型生成上技能本身的执行时间几乎可以忽略。5. 调用链路上的各类坑我的排查经验5.1 描述相似导致的技能误召这是我在项目上线后遇到的最典型问题。订单查询和售后查询两个技能描述里都出现了“订单”“状态”这些高频词向量检索阶段它们经常互相干扰导致 Agent 选错技能。排查过程也很有意思。我先调出模型选技能时的完整 thinking 日志发现两个技能在候选列表里挨着分数很接近最后模型基于参数形态选了售后查询但实际用户是想看订单物流。这一步其实不是模型“笨”而是技能描述信息结构相似度太高。处理的方法不是简单改描述而是做了两件事。第一给每个技能加了独立的“触发关键词”字段向量检索时对触发关键词加权。订单查询的触发关键词里有“物流”“快递”“送达”售后查询里有“退款”“退货”“售后”这样一来两个技能的向量距离就被拉开了。第二调整了描述的顺序把最具辨识度的场景词前置到描述开头。经过这次调整技能误召率下降了大约 68%。这个数字很能说明问题大部分 Agent 工具选错问题根源在技能定义阶段而不是模型能力不够。5.2 技能执行超时与重试风暴有些技能内部要调外部接口外部接口偶尔会变慢一旦我设置了 3 秒超时技能返回超时错误Agent 会觉得这个工具不可用转而尝试其他候选技能。问题是其他候选技能也可能要调同一个慢接口于是出现重试风暴。我在压测环境复现过这个场景当时的结果很奇怪技能本身的 P99 耗时明明只有 800 毫秒为什么会出现大量超时排查后发现了真实原因并发量上来后技能内部使用的 HTTP 客户端连接池被打满了请求在排队等连接等排到时已经超过 3 秒。解决方案有两个层面。首先给池子设置了合理的连接数和超时参数避免因连接等待耗尽导致技能假死。其次在技能内部为有缓存可能的结果设计一个短 TTL 的本地缓存相同的订单在短时间内反复查询时直接返回缓存结果。这两个措施合起来技能超时率压到了 0.5% 以下。5.3 参数提取错误的定位方法模型提取参数时偶尔会把 “ORD20250101” 提取成 “20250101”或者如果两个不同接口的参数含义有重叠提取结果就会张冠李戴。这种问题的定位靠经验很难猜中我把 Agent 每一次的技能调用请求、提取参数、模型选中的技能名都记成了结构化日志。一旦发现某个技能参数错误率高就对比模型历史提取的参数字段和正确值的分布找到规律后去优化技能描述。比如把参数描述改成 “必须是完整的订单编号包含ORD前缀请从用户消息中原文提取不要做任何格式转换”。模型的提取行为很大程度上受描述影响描述写得越具体提取准头越好。有一点需要提醒的是模型参数提取错误的原因其实是双重的至少有两个主要原因经常叠加描述歧义和用户口语化表达。如果用户说 “查一下我昨天的单子”模型可能不知道“单子”就是订单也不会自动带出昨天的时间范围。这种问题最有效的解决办法是让 Agent 在调用工具前做一轮追问澄清或者技能内部对缺失参数使用默认值而不是生硬地让调用失败。5.4 技能间数据传递规范问题技能调度时几种常见的业务场景需要考虑技能之间的数据传递。比如查询订单以后下一步可能生成订单简报。如果第一个技能输出的数据格式定义得不够规整第二个技能接收时就非常难受。最理想的做法是在技能定义里明确规定输出某个统一结构的对象。要传递JSON片段时使用纯数据格式不要夹杂任何人类可读的文案。我在实践中要求所有技能的输出必须遵循统一的包装结构不裸露原始接口数据。这样在技能编排和 Agent 复盘时能够比较方便地追溯每一条数据的来源和流向。6. 技能库的后续扩展方向6.1 技能编排与组合执行技能库稳定以后下一个阶段就是把多个技能编排成复合流程。比如“订单全链路查询”这个复合技能它内部由订单状态查询、物流轨迹查询、预计送达时间推算三个基础技能串联而成。我实践下来的方式是把编排逻辑定义为一份 DSL 配置而不是在代码里硬编码顺序。workflow_name: order_full_trace steps: - skill: check_order_status output_key: order_base - skill: track_logistics input_from: order_base output_key: logistics - skill: estimate_delivery input_from: [order_base, logistics] output_key: estimate这种配置如果做成可视化配置平台也就是常说的技能工作流编排界面。运营人员可以相对独立地调整步骤顺序而不需要每次改代码。Agent 开发者可以把精力专注于技能本身的原子化打磨做深做专。我先把这套工作流引擎搭起来后复合技能的交付周期明显缩短了原先要写不少胶水代码的工作现在都是配置化完成。6.2 技能效果的可观测性技能上了生产环境后效果好不好不能靠感觉判断。我给每个技能都挂了四个基础指标调用次数、平均耗时、失败率、参数提取错误率。这四个指标基本能反映一个技能的健康度。然后要关心 Agent 技能成本。技能调用成本不只是单个技能的耗时还有模型反复思考和工具路由的额外 token 消耗。我的经验是一边持续关注向量召回的候选技能数量一边对比模型最终选择结果如果在某段时间内频繁出现模型在前几个候选技能之间犹豫不决说明技能描述有重叠需要做一轮合并或差异化。6.3 技能热更新与版本回滚Agent 项目迭代节奏快技能经常要调整描述、修改参数、修复 bug。我引入了技能热更新机制更新技能定义文件并重新触发注册中心加载不需要重启整个服务进程。但热更新一定要配合版本回滚能力。我维护了三份技能版本配置线上稳定版、预发布版、历史版本。回滚操作只需要切换注册中心读取的版本标签不需要重新发版。这个机制在几次技能描述调整出现质量回退时帮助很大秒级回滚用户基本无感。写在最后经过多次迭代后我的几个核心体会技能库这件事做成功一半靠的是对 Agent 工作方式的理解更重要的另一半其实在于对工程细节的敬畏。我见过很多人技能数量一多就崩溃然后跑到网上问是不是模型不够聪明其实大部分情况都是技能定义不清晰、路由链路设计不合理、调度逻辑太脆弱。真正解决问题的方法从来不在模型参数上而在于把技能拆到足够原子化把描述写得足够明确把调度器做得足够健壮把整个调用链路做成可观测的。做到这四点即使模型换一个技能库的稳定性和准确率也还是能保持在可用水平线上。如果理解了一个技能库的价值想快速验证这套方案实践落地这部分需要提前有一个认知先在本地把目录结构搭好写三个原子小技能跑通全链路再逐步扩到更复杂的场景。不要一上来就追求大而全的设计市面上很多 Agent 工程的失败都是开局想得过于复杂把基础链路压垮了。先跑通这一小步后面所有调度、编排、路由这些复杂机制才有真实业务场景去支撑。