资讯详情

资讯详情

Agent技能库设计与落地:从工具调用到多技能编排的实战指南

在LLM应用开发里折腾久了你就会发现一个特别尴尬的阶段单轮对话、简单的工具调用都跑通了但一旦想让Agent稳定地完成稍微复杂点的工作——比如“先查资料再整理成表格最后发到群里”就开始各种翻车。工具定义散落各处提示词里塞满了“如果...就...”模型时而漏参数时而调错函数。我大概在半年前被这个问题折磨得不行于是动手整理了一个叫“agent-skills”的项目本质上就是一套给Agent用的“技能仓库”方案。这篇文章就是来聊聊这个项目怎么设计、怎么落地以及我在过程中踩过哪些坑。这套东西适合谁适合那些已经用大模型工具调用做过几个Demo但想让Agent真正具备多步骤、可复用工作能力的开发者。不管你是用开源的Agent框架还是自己在裸写ReAct循环这套技能抽象的思路都能直接搬过去用。它不是某个框架的插件而是一套组织代码、提示词、参数校验规则的方法论。下面我把整个项目的设计思路、核心数据结构、从零搭建的实操过程、以及多技能编排的难点逐一拆开讲。1. 整体设计思路把Agent的“能力”当作独立工程产物来管理1.1 为什么要单独做一个“技能仓库”而不是直接写Function很多人第一步会问我直接把所有能力都写成Python函数注册给模型不就行了吗为什么还要搞一个“agent-skills”项目出来答案是当你只有三五个工具时直接写函数完全没问题但当工具数量到二十个以上并且需要多个工具配合完成复杂任务时函数列表本身就变成了一个难以维护的“大杂烩”。举个例子。假设你要做一个“会议纪要高亮提取”的功能。你需要用语音转文字工具把录音转成文本再调用摘要模型提炼要点然后提取决定事项和负责人最后还要把结果按固定模板生成Markdown。这四个步骤如果分别写成四个裸函数它们在逻辑上是割裂的——模型并不知道“转写文本”之后下一步该做什么。而如果把它们打包成一个“会议纪要处理”技能并且配上明确的输入输出说明、处理步骤、常见边界情况描述模型调用时就会像一个老员工拿到SOP一样知道全流程。所以我在设计这个项目的第一天就定了一个原则技能Skill不是指单个函数而是指“一个可以独立完成某项任务的最小完整能力单元”。它包含函数实现、参数描述、副产物比如记录、日志、以及最关键的——一组教模型“什么时候用、怎么用、用完以后怎么接下一步”的元数据。1.2 技能库的三层抽象模型项目把Agent能力拆成了三层这个分层是我觉得整个项目里最有价值的部分第一层叫“工具层”就是最基础的原子操作比如调用某个API、读一个文件、发一条消息。工具层不感知业务只负责把一件事执行到位。第二层叫“技能层”它把一个或者多个工具按照特定业务场景编排起来形成一条“操作路径”。比如“日报生成”这个技能内部可能是先读取今日所有聊天记录再筛选关键进展再调用模板生成正文。这一层是Agent可以直接通过名称参数调用的最小业务单元。第三层叫“任务层”它负责把多个技能按照目标拆解成执行序列。任务层是一个跑在技能之上的调度器它决定先执行哪个技能、后执行哪个技能、失败后怎么降级。这个三层模型最大的好处是每一层都可以独立测试、独立演进。工具底层换了实现只要接口不变技能层不用动技能层内部调整了步骤顺序任务层和模型都不用感知。这种隔离性让Agent系统的可维护性提升了一个量级。1.3 为什么用“DSL目录结构”而不是纯数据库存储一开始我也纠结过技能定义到底放哪。放在数据库里查询灵活放在代码里好管理版本。最后我选择了折中方案每个技能用独立的目录前后端一致的结构化描述文件YAML来定义目录就是技能ID文件里存放技能的描述、参数、测试用例、调用逻辑入口。整个技能库就是一个普通文件系统天然支持Git版本管理、Code Review和CI测试。这么做有一个实打实的好处——技能的变更历史和代码变更历史高度绑定。你新增一个参数校验审计时可以直接git diff看改动技能A升级之后导致技能B的编排出现回归也能快速git bisect定位。如果当初把这一切塞进数据库做审计和回溯会痛苦得多。1.4 项目目录总览整个项目的结构大致长这样后面实操部分我会逐步展开agent-skills/ ├── registry.yaml # 技能注册总表用于启动时快速扫描 ├── skills/ │ ├── meeting-minutes/ # 一个技能一个目录 │ │ ├── skill.yaml # 技能描述包含参数、用途、边界 │ │ ├── handler.py # 技能执行逻辑 │ │ ├── samples/ # 给模型看的few-shot示例 │ │ └── tests/ # 单元测试和端到端验证脚本 │ ├── daily-report/ │ └── ... ├── orchestration/ │ ├── planner.py # 任务编排器把任务拆成技能序列 │ └── executor.py # 执行器负责调用技能并处理结果 └── runtime/ ├── loader.py # 启动时加载所有技能到注册表 └── context.py # 运行时上下文跨技能传参和状态管理2. 技能定义的标准化让模型“看得懂、调得准”2.1 技能描述文件的设计要点一个技能要能被模型准确调用光靠函数名加注释是不够的。我在实战中反复打磨最后skill.yaml里固定包含这几组关键字段字段说明作用id全局唯一技能ID模型调用时的唯一标识name人类可读名称便于调试和日志输出description一句话说明技能整体作用模型选择技能时的第一判断依据when_to_use什么时候应该用这个技能大幅提高模型路由准确率parameters参数列表每条含类型/是否必填/描述/枚举值决定模型能否正确传参dependencies依赖的技能ID列表用于编排器判断执行顺序returns返回结果的结构说明决定模型能否正确解析输出examples2-3组输入输出样例few-shot提示降低模型理解成本version语义化版本号支撑技能独立升级与灰度这里我最想强调的是when_to_use这一个字段。别人的工具描述往往只写“这个函数能做什么”但我要求每个技能必须写清楚“什么情况下用它、什么情况下千万别用它”。比如某个“查询天气”的技能我除了写“输入城市名返回天气”还会补一句“如果用户问题是‘明天适合洗车吗’不要直接调用天气查询应该走‘生活建议生成’技能”。正是这样的“负样本”规则让模型选错工具的概率大幅下降。2.2 为什么参数校验必须用JSON Schema第二个极其重要的点是参数校验。模型通过自然语言抽取参数进行调用时经常会出现“幻觉参数”——目标函数根本不存在的字段也被它生成出来。前期我的项目里模型经常把city: 北京朝阳区传给只接受city_code的接口。自从技能定义里全面引入JSON Schema之后这类问题几乎绝迹。JSON Schema的作用体现在两方面。第一它能对模型输出的参数做运行时校验发现多传了未知字段立刻拦截并返回友好报错第二Schema本身也是给模型看的“参数说明书”。我在生成系统提示词时会把Schema完整渲染进提示词里模型看到清晰的字段约束后输出规范程度明显提升。一个简化的参数Schema示例长这样parameters: - name: city_code type: string required: true description: 城市行政区划代码例如110000代表北京 pattern: ^\\d{6}$ example: 110000 - name: days type: integer required: false description: 查询未来几天的天气范围1-7 minimum: 1 maximum: 7 default: 3这种显式约束既方便代码做运行时校验也给模型传递了非常精确的格式预期相当于把“出题范围”提前画定了。2.3 few-shot样例的写法不是数量多而是差异大每个技能我在samples/目录里放了两到三组“输入-技能调用参数-期望输出”的完整JSON样例。关键在于这些样例绝不是简单重复的参数变换而是故意覆盖不同类型的情况第一组标准情况参数齐全模型直接命中。第二组模糊情况用户表达缺省参数展示模型应该如何主动填默认值。第三组边界情况输入涉及技能不该处理的范围展示模型应该如何拒绝对接其他技能。用“日报生成”技能举例第二组样例就是用户只说“帮我写今天的日报”没说项目名、没说时间范围模型需要根据当前日期自动补全date今天、project_name未指定这组样例教会模型“缺省策略”。2.4 版本管理与兼容性技能一定会迭代这是躲不开的。我规定技能升级必须遵守语义化版本规则大版本号变化意味着接口不兼容——参数变了、返回结构变了小版本号意味着功能增强但保持兼容补丁版本意味着只修内部逻辑。编排器和依赖方在声明依赖时可以写成meeting-minutes: 1.2.0 2.0.0这样升级一个技能时不会被其他技能隐性破坏。3. 从零搭建一套可用技能库注册、装载、调用全流程3.1 技能注册总表启动时快扫而不是全量加载实际操作中技能数量多起来以后全部加载到内存是不现实的既费token又拖慢启动速度。所以我设计了一个registry.yaml作为技能总表它不是完整技能的拷贝而是一份“索引”启动时只加载这个索引技能在首次被需要时才做懒加载。version: 1.0.0 skills: - id: meeting-minutes path: skills/meeting-minutes enabled: true load_policy: lazy - id: daily-report path: skills/daily-report enabled: true load_policy: eagerload_policy字段区分了两种加载策略常驻型技能比如会话管理用eager预加载重型和低频技能比如视频转码用lazy按需加载。这一设计让Agent的冷启动时间在一个80技能规模的库里依然能控制在几百毫秒级别。3.2 技能处理器handler的接口约定每个技能的handler.py都遵循同一个接口约定这很重要。统一接口让编排器不用关心每个技能内部逻辑长什么样# handler.py 统一入口示例 from dataclasses import dataclass from typing import Any dataclass class SkillResult: status: str # success / failed / partial data: Any # 业务返回数据 error: str | None # 失败原因 metadata: dict # 副产物信息耗时、调用链等 class BaseSkill: async def execute(self, params: dict, context: dict) - SkillResult: 所有技能必须实现这个异步入口 raise NotImplementedError为什么必须异步Agent类应用里技能执行往往要并发调用多个底层服务同步阻塞会严重拖慢整体响应。统一异步接口之后编排器可以用简单的asyncio.gather同时跑多个技能实现“并行能力组合”。3.3 技能执行逻辑里的“内聚”原则写技能内部逻辑时最容易犯的错误是“把Agent的编排逻辑写死在技能里”。有些同学会在“周报生成”技能里直接判断“如果当前是星期五则额外调用邮件发送技能”这就把编排责任放错了位置。技能内部应该只负责“完成本技能的既定任务”至于何时触发、触发后要串联什么统统应该交给上层编排器。我后来的做法是技能内部只包含“完成这项任务必须要做的事”所有分支逻辑都通过context里的开关量注入。比如周报技能是否发送邮件由编排器根据“当前日期”决定并在参数里传入send_email: true技能自己不做时间判断。这么改完以后技能的可复用性大大提高测试也好写。# 一个典型技能的骨架 async def execute(self, params: dict, context: dict) - SkillResult: raw_text params.get(transcript) # 步骤1文本清洗 cleaned clean_text(raw_text) # 步骤2调用摘要模型 summary await self.summarizer.run(cleaned) # 步骤3结构化提取 action_items await self.extractor.run(summary) return SkillResult(statussuccess, data{ summary: summary, action_items: action_items }, metadata{model: summarizer-v3})3.4 装载器与上下文管理技能的装载不能只把函数加载进来还要把技能声明里的描述、参数Schema、few-shot样例全部结构化存好后续生成系统提示词时直接引用。我做了个SkillRegistry类启动时扫描注册表调用load()时解析YAML、加载Python模块、校验Schema合法性最后注册到全局。上下文管理方面我引入了一个轻量的Context对象所有技能可以读取和写入键值对。它解决的核心问题是“步骤与步骤之间的数据接力”——技能A产出的结果要通过上下文传给技能B作为输入。这里的坑在于上下文里的数据必须是可序列化的Json天然规避了Python对象跨技能传递时出现引用混乱的问题。4. 多技能编排实战从“会调用”到“会配合”4.1 编排器的工作机制当Agent面对一个复杂用户请求时光有技能库是不够的还需要一个“拆解和调度的大脑”。我实现的编排器分为两步第一步是planner接收用户目标和可用技能索引输出一个技能执行计划第二步是executor按计划逐个调用技能并在每一步之间传递数据。规划阶段的提示词里我会把技能索引压缩成“技能ID一句话描述主要参数”的紧凑列表不让模型直接读全部YAML。实测这样能显著减少模型翻看无用信息而导致的注意力分散。一个典型计划输出长这样{ plan_id: 8f1a2a3e, steps: [ {skill: meeting-transcribe, params: {file: rec.mp3}, order: 1}, {skill: meeting-minutes, params: {transcript_from: step_1}, order: 2}, {skill: daily-report, params: {include_minutes: true}, order: 3} ] }4.2 失败降级与重试策略真实环境里技能调用失败太常见了——第三方API超时、模型返回非JSON、依赖服务限流等等。编排器必须有明确的失败处理策略我沉淀了两条原则。第一条失败信息必须回传到决策点而不是让编排器直接吞掉。技能返回的error字段会被拼接成格式化消息由模型决定下一步是重试、换参数还是换技能。第二条幂等技能才允许自动重试。比如“查询”“生成草稿”这类无副作用的操作超时可以自动重试一次而“发送邮件”“创建订单”这类有副作用的操作宁可返回失败让用户确认也不能盲目重发。4.3 上下文数据命名的防冲突设计多个技能在同一个Context里读写数据时命名冲突是很容易踩的坑。两个技能都用output这个key后写覆盖先写下游技能拿到的数据就是错的。我的解决办法是强制要求技能写入上下文时key必须以技能ID为前缀例如meeting-minutes:summary meeting-minutes:action_items daily-report:content这个约定虽然简单却让跨技能的数据流动变得非常清晰调试时候一眼就能定位数据是谁写的。4.4 组合技能的“原子性”问题最后一个编排层的核心问题是当一个任务需要多个技能配合完成时如果中间某一步失败前面已经执行的操作怎么处理比如一个“生成周报并发送到群”的任务周报生成成功了但发送失败了整个计划算成功还是失败我采用了“手动补偿”策略不搞复杂的分布式事务而是在计划层面定义了on_rollback回调。一旦后续步骤失败编排器会调用前面已执行步骤注册的回调做一些补偿动作——比如发送失败时回调负责把已生成的周报保存到草稿箱并附上失败原因。这种做法实现成本低在实际业务里已经覆盖了九成以上的补偿需求。5. 常见问题与排查技巧实录5.1 问题速查表症状根因解决方案模型调用了错误技能技能描述与when_to_use写得太模糊重写description增加负例描述模型传参总是多余字段参数Schema缺少严格unknownFields配置校验器开启禁止未声明参数技能执行成功但结果不达预期技能内部缺少输入数据前置校验在execute开头检查params完整性多技能编排时上下文数据被覆盖未按ID前缀隔离数据强制key以skill-id前缀开头技能升级后其他流程报错大版本升级未同步下游依赖严格执行语义化版本约束启动加载过慢全部技能做了eager加载低频技能改为lazy并按需加载模型不按样例格式输出few-shot样例覆盖不足增加边界情况、缺省情况样例5.2 三个绕不开的坑第一个坑是模型会在技能返回数据的“缝隙”里自己脑补内容。技能A返回了三个字段模型准备调用技能B时如果B需要四个字段模型很可能“编造”一个看起来合理的值。这非常危险。我的对策是在技能B的Schema里把关键字段标记为required并在校验失败时明确返回“该字段与技能A输出字段不一致”的报错引导模型回头取数。第二个坑是提示词里的技能描述过多过长。技能库有三四十个技能后如果把所有完整描述都塞进提示词模型的表现反而会大幅下降。不能偷懒“全量灌输”而要先做意图路由——用一次轻量分类把候选技能缩小到三五个再让模型在候选集里做选择。这一步改造之后我的实测准确率提升了大概两成。第三个坑是本地测试没问题一接真实模型就出岔子。原因是本地测试往往使用固定的合成输入而真实用户输入里的噪声和省略语五花八门。后来我把每个技能的samples/扩展成一套“自动化用例集”每次修改技能描述文件后必须跑一遍回归夹具里特意混入用户多发的最典型几种错误输入形成“护栏”。5.3 调试技能调用的“侦察兵”手段定位技能调用问题时整个链路太长用户输入、模型推理、规划输出、技能执行、数据回传。每一步都可能出错。我的经验是给Agent的运行时加一个“调试日志模式”所有关键节点都输出可追踪的结构化日志{event: skill_call, skill_id: meeting-minutes, params: {file: rec.mp3}, ts: 2025-06-01T10:00:00Z} {event: skill_result, skill_id: meeting-minutes, status: success, duration_ms: 3200} {event: plan_step_start, plan_id: 8f1a2a3e, step: 2}这个日志模式还有一个妙用我可以用它快速“回放”一次完整的Agent执行过程观察模型是在哪一步开始“跑偏”的。加上时间戳和耗时之后还能顺手发现性能瓶颈——比如有几次用户体感很慢就是某个技能长期在冷启动加载加了预热之后立刻缓解。最后分享一个我后来越用越顺手的小技巧技能库刚搭起来的时候我的维护方式完全靠手工改YAML。后来我发现最省力的做法是给每个技能配一个“自检脚本”每次提交前跑一遍把描述文件里过时的example、缺失的when_to_use、未覆盖的parameters自动检测出来。刚开始写这个自检脚本花了半天但之后每次升级技能都节约了大量人工复核时间。如果你也准备长期维护一套Agent技能体系建议从第一天就把“自动化校验”考虑进去而不是等技能多到改不动了再回头补。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →