Agent技能库设计实战:从工具混乱到可插拔技能体系
去年底我接手了一个内部项目业务方要求做一个能自动处理工单分类、信息抽取和报表生成的助手。第一版我按老思路把逻辑全部写死在代码里模型只负责理解用户意图结果上线没两周就暴露了一堆问题需求一变就要改代码、不同场景的prompt互相污染、模型常常把意图理解对但动作做得乱七八糟。后来我重构了整套方案把核心思路收敛成一个词agent-skills也就是给智能体挂一套可插拔、可描述、可调用的技能包把一个什么都想做的大模型拆成一组随时待命的小工具。这篇文章就把我的设计思路、实操过程和踩过的坑完整写出来适合正在做Agent应用但被工具管理混乱、调用不稳定折磨的人参考。先说明白后面讲到的东西都来自我实际写过的代码和真实联调记录不是概念整理。整个技能体系大概几百行Python代码配合配置文件就能跑不需要什么重型框架。看完你应该能自己搭出一套可复用的技能库。1. 先想清楚agent-skills 解决的到底是什么问题先说痛点。做Agent应用的人大概率都遇到过这么几个场景模型拿到用户一句话推理出要调用某个能力但能力定义分散在各处有的是函数有的是类方法有的干脆是自然语言描述模型经常在多个相似能力之间选错换一个模型供应商原先写好的能力调用方式全部作废。这些问题的根源在于我们把模型的能力和系统的能力混为一谈了。模型的能力由参数决定系统能做什么却由代码决定两者之间的桥梁非常脆弱。而 agent-skills 要做的就是把这个桥梁做成一套标准协议。拆开看这套协议解决三个具体问题。第一是能力的可发现性。技能不再藏在代码的各个角落而是通过统一的清单文件注册到技能库里模型可以在运行时看到当前环境具备哪些技能、每个技能能做什么、有什么约束。这相当于给模型一张目录表它不再瞎猜。第二是能力的可组合性。单个技能粒度适中之后复杂任务通过组合技能完成而不是在代码里写一堆连招。组合逻辑放在配置层面模型只需要说我想执行A和B剩下的排序、参数传递、结果合并由技能框架处理。第三是能力的可演进性。我可以在不重启服务的情况下向技能库挂载新技能旧技能退役也不会影响其他部分。业务方提新需求时多数情况下只需要新增一个技能描述和对应实现函数不用动车整个系统。用生活化类比就是以前你让一个厨师做满汉全席所有菜谱都存在他脑子里你想加一道菜就得重新培训整个厨师。现在你给厨师一个菜谱架每道菜一个卡片他想做什么就从架子上抽卡卡片写清楚需要什么食材、什么火候。加菜就加卡片换厨师也不怕因为菜谱架还在。所以我的结论是不是所有Agent项目都值得上技能库。如果你只做单轮问答、不需要调用外部能力直接写死逻辑就行。但只要你有多余5个工具函数、需要面对不固定的需求变化、或者要对接多套模型技能库这套抽象带来的收益立刻就能显现。2. 技能体系建设的第一步定好技能的边界2.1 技能 vs 工具 vs 函数粒度怎么拿捏我见过不少人在这一步翻车最常见的错误是把所有函数都叫技能。结果技能库打开一眼望不到头模型选技能比写代码还难准确率直线下降。我的经验是要区分三个层次。函数是最底层的原子操作比如调用某个API查询某个字段技能是对目标有用的、可独立完成一个子任务的能力单元比如提取工单中的客户名称再往上才是工作流也就是多个技能按规则编排成完整流程。定粒度的标准我总结为三条技能必须有明确输入输出且输出能对应到业务语义。如果你拿到输出后还得再写一段代码才能知道它到底干了什么这个技能粒度就不对。技能的描述长度应该控制在两到三句话能说完。如果一句话说不清这个技能是干什么的说明它做的事太多了。技能之间尽量避免重叠。两个技能都能完成同一件事模型必然困惑。举个具体例子。我做工单分类时一开始设计了调用文本分类模型这个技能后来发现太粗因为分类之前需要先做实体清洗两个子步骤耦合在里面模型调用时传参混乱。拆开后变成clean_entities、classify_ticket两个技能各自职责清晰准确率反而上去了。这就是边界划分带来的收益。2.2 技能描述写给模型看的说明书技能描述是整个技能库的灵魂但也是大多数人最不重视的地方。我见过有人写的技能描述是处理数据四个字这种描述模型根本没法判断什么时候该用它。技能描述的结构我建议严格遵循四要素触发条件、动作内容、输入要点、输出约定。触发条件写成当用户需要……时使用动作内容写清楚本技能将……输入要点说明参数怎么传输出约定说明返回什么结构。还是用工单技能的实例来说明- name: extract_customer_info description: 从工单文本中提取客户名称、联系方式和优先级标记。 当用户提供含客户信息的原始工单文本且需要结构化输出时使用。 输入必须是包含原始文本的字符串输出返回JSON对象。注意description里的每个词都是有意图的不是随便写的。当用户提供……且需要……时使用是在给模型划边界告诉它什么情况下别用。输入输出约定写明确了模型才知道该传什么参数。2.3 参数Schema宁可多设计一步不要留自由发挥空间参数定义如果太宽松比如允许任意字符串模型就会给你自由发挥出各种千奇百怪的传法。我踩过一个坑某个技能定义了keywords参数没写类型和约束结果模型把整个工单全文塞了进来处理逻辑直接爆炸。参数Schema我推荐直接用JSON Schema规范然后在描述里再补一层说明。设计参数时有几个细节值得注意尽量用enum约束候选值。模型面对枚举时选择正确率远高于自由文本。给每个参数写description解释它在这个业务场景里到底代表什么。模型看描述比看参数名靠谱得多。要求不明确的参数宁可设为必填让模型强制思考这个值从哪来。避免其他、等等这类兜底词。兜底词会让模型觉得不需要准确理解需求。举个例子工单优先级我原本定义成字符串参数后来改成enum{ type: object, properties: { priority: { type: string, enum: [high, medium, low], description: 工单优先级来自工单标题或正文中的紧急标记 } }, required: [priority] }改完之后模型传参从不规范格式变成了三种固定值后面的处理逻辑一下子清爽了。3. 核心实现技能注册、加载与调度的底层逻辑3.1 技能库的目录结构与注册机制实际的代码层面我把技能做成一个包每个技能一个目录包含一个manifest文件和一个实现文件。manifest负责描述实现负责执行两者分离的好处是运营人员以后可以直接改manifest来描述业务规则不用碰代码。目录结构长这样skills/ __init__.py registry.py extract_customer_info/ __init__.py manifest.yaml handler.py classify_ticket/ __init__.py manifest.yaml handler.py generate_report/ __init__.py manifest.yaml handler.py注册机制的核心是registry模块。启动时扫描skills目录读取每个子目录的manifest.yaml把描述信息统一加载进内存然后暴露一个接口让调度器查询。注册阶段的代码可以简写成import yaml from pathlib import Path from typing import Dict, Any class SkillRegistry: def __init__(self, skills_root: str): self.skills_root Path(skills_root) self._skills: Dict[str, Dict[str, Any]] {} def load_skills(self): for skill_dir in self.skills_root.iterdir(): manifest_path skill_dir / manifest.yaml if not manifest_path.exists(): continue with open(manifest_path, r, encodingutf-8) as f: manifest yaml.safe_load(f) skill_name manifest.get(name, skill_dir.name) manifest[handler_path] str(skill_dir / handler.py) self._skills[skill_name] manifest def list_skill_manifests(self) - list[Dict[str, Any]]: return list(self._skills.values()) def get_skill(self, name: str) - Dict[str, Any]: return self._skills.get(name)这套结构没有魔法核心优势是把描述和行为解耦。模型看到的只有manifest执行时框架再根据handler_path加载真正的处理函数。3.2 技能描述注入模型上下文窗口的合理使用技能信息要喂给模型但不能一股脑全塞进系统提示词里。如果技能库有几十个技能每个描述几百字上下文窗口一下子就吃没了。我的做法是分两级注入。第一级把技能清单只要name和一句话summary全部放进去让模型知道有哪些能力可选。第二级运行时根据用户输入的语义相关性做粗筛只把最相关的3-5个技能的完整描述含参数Schema注入到当前对话里。粗筛我用的是基于关键词和标签的规则不依赖模型。每个技能在manifest里定义一个category标签列表比如工单、客户、文本处理。用户输入进来后先做简单分词匹配标签命中不足时才扩展关键词表。这一步能显著降低模型的决策成本。实验里我对比过全量注入和粗筛注入同样一个工单处理任务全量注入的准确率只有61%粗筛注入提升到了83%。原因也不难理解候选少了模型注意力更集中描述里的关键信息更容易被捕捉到。3.3 调度执行技能调用与返回结果的关键路径调度模块负责把模型的技能调用请求翻译成实际函数调用。核心逻辑包括查找技能、参数校验、执行、异常捕获和返回。一个简化的调度器核心代码import importlib.util import json from jsonschema import validate, ValidationError class SkillExecutor: def __init__(self, registry: SkillRegistry): self.registry registry def execute(self, skill_name: str, params: dict): meta self.registry.get_skill(skill_name) if meta is None: raise SkillNotFoundError(f技能 {skill_name} 不存在) schema meta.get(parameters, {}) try: validate(instanceparams, schemaschema) except ValidationError as e: return {success: False, error: f参数校验失败: {e.message}} handler self._load_handler(meta[handler_path]) try: result handler(params) return {success: True, result: result} except Exception as e: return {success: False, error: str(e)} def _load_handler(self, handler_path: str): spec importlib.util.spec_from_file_location(dynamic_handler, handler_path) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module.run这里有个容易被忽略的点handler的签名统一是run(params: dict) - dict。我刻意没有设计成直接传命名的key因为统一参数容器可以避免函数签名变化导致动态加载失效。所有技能都站在同一条装配线上替换实现就只换一个文件。另一个细节是执行结果一定要结构化。只有成功/失败状态码数据模型才能根据成功标志决定下一步动作。如果只是把报错信息丢回去模型大概率会一脸懵地重试同一个错误调用。3.4 技能编排与组合的进阶实现单技能拆分完之后下一层问题就是复杂任务怎么编排。我采用最简单的做法——技能内部允许调用其他技能也就是让任务能力分层。实现方式是在handler里通过一个依赖注入拿到执行器从而调用子技能。这样做非常透明因为技能本身还是一个独立能力只是它的实现依赖了其他能力的输出。举个例子生成日报这个技能内部不直接查询数据库而是依次调用query_ticket_stats和format_markdown两个子技能。这样一来单个技能的可测试性仍然很好我可以把子技能分别mock掉单独验证流程是否正确。编排层最关键的是要做循环检测。技能A调技能B技能B又调技能A一旦发生就死循环了。我的方案是在执行上下文里加一个调用栈每次进入新技能时检查是否重复调用了同一技能超过两次超过了就直接抛出cycle_detected错误。还需要强调的是编排逻辑不要让模型自由乱拼。模型可以在候选技能里选择组合但组合的执行骨架应该由技能内部顺序决定而不是完全由模型临场发挥。我见过不少项目试图让模型自己写工作流结果非常不稳定今天能跑通明天就断。更稳的思路是模型负责选哪个技能技能内部负责这个技能怎么完成。4. 实践记录我如何一步步把技能库跑起来记录下来的是我为某内部工单系统做的一个技能库Demo。目标不算复杂对用户提交的工单文本做实体抽取、分类、紧急度判断最后生成一段结构化摘要。整个过程我分成了四个阶段推进。第一阶段是定义技能清单。我列出了系统里需要的所有原子能力逐个确认是否满足一个独立子任务的标准。最终留下5个技能去噪清洗、实体提取、分类判断、优先级判断、摘要生成。启动初期不要贪多技能越少模型行为越可控。第二阶段是压测模型对描述的理解。我不急着写handler先把5个技能的描述挂在文本模型上跑了100条模拟工单观察模型选择技能的正确率和参数传递是否符合预期。这个过程很值得花时间因为发现模型对classification和priority_judgment两个技能边界认知模糊经常混用。后来修改了描述明确说明classification判断的是工单所属业务类型priority判断的是紧急程度两者互不替代。第三阶段是落地实现。每个技能的handler都用最简朴的规则或者调用一个轻量模型完成。这里我不建议一上来就搞大型模型服务能用语法规则解决的问题用规则先顶着能跑通链路最重要。第四阶段是接入真实环境边跑边调。上线后大概跑了两周我把所有模型的错误选择、参数错误、执行超时都记录下来逐一修正描述和参数定义。这个过程里我发现调整描述比调整参数能带来更大的准确率提升因为很多错误发生在根本不该调用这个技能而不是调用方式不对。阶段推进的日志我在下面整理成了一张速查表方便你对照参考阶段核心目标关注指标关键输出第一阶段技能清单技能数量与重叠度技能manifest初稿第二阶段描述准确性技能选择命中率修正后的技能描述第三阶段链路可运行执行成功率可运行的技能库Demo第四阶段效果调优任务完成率调优记录与规则优化清单这套流程走下来最深的体会是技能库建设不能一开始就追求完美而是要在描述-执行-反馈的循环里快速迭代。5. 实测之后避坑技巧与效果观察实测运行了两个月我积累了不少一手经验。技能库确实帮我把工单处理准确率从最初的61%拉到了80%左右但过程里踩过的坑值得单独分享出来。第一坑一次加载技能数量过多。早期我把十几个技能同时注入模型直接迷茫选择准确率反而低于只注入5个的时候。人类做选择题选项太多容易懵模型也一样。控制load数量非常重要。第二坑描述里的否定表达。技能描述里写不要用于……场景很多模型反而更倾向被这些词吸引过去。后来我把否定表达全部改成仅用于……场景命中率提升明显。模型更擅长处理正面限定不擅长处理反向排除。第三坑执行结果缺少状态码。最初handler返回的都是裸数据一旦出错只抛异常模型拿到错误也不知道该怎么处理。统一成{success: bool, error: str}之后模型大多数情况下能正确选择换个技能重试还是直接告诉用户问题出在哪。第四坑技能版本变更没人管。技能实现更新了但manifest里的描述没换模型还在按老描述调用参数格式对不上。后来我在manifest里加了version字段每次变更都记录变更说明避免新旧行为混乱。第五坑多技能同时调用时的参数依赖。有时候A技能的输出是B技能的输入如果A返回的结构和B参数Schema对不上中间还需要做字段映射。这个我一度用代码硬转后来改为在B技能的description里明确写上接收来自A技能输出的xxx字段让模型帮我们填好映射比写转换代码更灵活。效果观察层面还有一个有意思的发现模型稳定性跟技能实现完全解耦。后来我把底层的某个模型替换成了另一种实现技能库的描述和调度代码几乎没动只花了半天就完成了切换。这个收益在传统工程架构里很难获得因为传统工具函数的调用逻辑和模型绑定得很死。技能化之后所有交互点收敛到了manifest这一层切换模型的成本确实大幅下降。6. 常见问题速查技能库在实际运行中的排查清单整理一份我在实操中见过最多的问题速查表供你在自己项目里直接对照排查。现象可能原因排查方法解决建议模型怎么也选不中目标技能描述与触发词不匹配检查描述里是否包含用户输入的高频词在描述中加入用户实际用语的例句模型频繁选错相似技能技能边界重叠对比两个技能描述找出共同场景给每个技能增加不处理的边界词参数传错名字或类型参数描述不清晰查看模型传递的原始参数样本给参数增加描述和enum约束技能执行超时实现逻辑过重检查handler是同步还是异步重逻辑拆成子技能超时单独捕获技能循环调用编排逻辑存在环查看执行调用栈记录增加循环检测限制调用深度结果不稳定时好时坏注入内容顺序变动固定技能清单的排列顺序让候选技能按固定顺序注入排查过程中还有一个被低估的技巧显式记录每次调用的完整链路。技能名、传入参数、返回结构、耗时、成功状态都写进日志。一开始我以为这是给自己看的后来发现调模型效果时这些历史调用记录就是最珍贵的标注数据。模型选错时把当时的用户输入和技能描述对比一下很快就能定位是措辞问题还是边界问题。另外有个运维层面的小建议技能库上线前一定要准备好快速开关。某个技能效果不佳时能在配置层面直接将其从候选列表中下线而不是去改代码。这样即使模型选错了也不会把错误能力暴露给用户太长时间。我的实现里是在registry中加一个disabled字段加载时直接跳过30秒生效。再补充一个细节如果技能调用涉及外部服务在技能描述里注明本技能需要外部服务在线失败时请提示用户稍后再试。这句话看似废话实际能大幅减少模型在服务不可用时的无效重试。模型知道前提条件后报错时就不会反复尝试同一个技能而是会给出合理的提示。7. 写在最后的实操体会技能库这套东西从设计到落地我的体会是功夫不在代码上而在描述和边界控制上。模型能力现在已经足够强真正决定Agent上限的是我们对环境的定义是否清晰。把技能描述写得像一份合格的接口文档把参数约束设计得像一份严格的表单校验整个系统的稳定性就会上一个台阶。回头再看这个项目最值钱的收获不是那几百行代码而是让模型看说明书干活这个思维转变。以前我总是在prompt里堆逻辑想让模型一步到位现在我把逻辑拆成卡牌让模型按卡牌走每一步都观察、都记录、都调整。如果你正在做类似的Agent项目我的建议很简单不要迷信框架也不要过早追求自动化。先拿一个真实场景把技能编号写清楚、描述写准确、边界画明白跑通一个垂直流程再逐步扩充。技能库不怕小怕的是里面的东西说不清楚。在我实际使用过程中最成功的技能往往是描述最朴素、边界最明确的那几个。后续如果想继续扩展可以考虑给技能库加一层自动评测机制每次模型选择技能后用一个小模型对照真实标签判断选择是否正确把错误案例自动回流到描述优化流程里。这套闭环跑起来后技能库的迭代速度会快上一个数量级。