Agent技能包设计:将AI Agent拆成可复用独立技能模块的实践

发布时间:2026/10/14 8:03:52
Agent技能包设计:将AI Agent拆成可复用独立技能模块的实践 做AI Agent开发的朋友八成都会遇到同一个问题模型能力上去了但想让Agent稳定地跑通一条业务链路代码却越写越乱。一会儿在System Prompt里塞一堆操作说明一会儿在代码里写死各种分支判断最后Agent的行为像一团乱麻加一个新功能要改三处地方。我后来把思路整个翻了一遍把Agent的能力拆成一个个可以独立定义、独立测试、独立复用的“技能包”也就是标题里说的 agent-skills才算是把项目从泥潭里拉了出来。这篇文章就把我这套玩法完整拆开讲包括为什么要把技能抽出来、技能包该怎么设计、落地代码长什么样以及我踩过的那些坑。这套方法适合谁如果你正在做一个稍微复杂一点的Agent比如需要调用多个接口、处理多步任务、有不同业务场景切换的项目那看完这篇你至少能避开一半的坑。如果你只是做个Demo一两个工具调用就够了那先别急着上技能体系容易过度设计。但下面要讲的思维方式早看早受益。1. 为什么要把技能从Agent里抽出来1.1 单体Agent的“混沌期”是怎么形成的一开始我做Agent和大多数入门教程一样把所有指令都塞进System Prompt然后在代码里写一堆if-else来判断用户意图、调参数、拼结果。比如做个客服Agent我需要处理订单查询、物流查询、退换货申请、人工转接……每个模块都有一段prompt模板还要配合不同的工具函数。当工具数量超过五个问题就开始冒头了改一个物流查询的规则可能会影响到退换货的判断Prompt越来越长模型对较早的指令忠实度直线下降代码里每个分支的容错逻辑重复且不一致。后来我意识到问题不在于Agent本身而在于能力的组织方式。Agent的“技能”和人的技能一样应该是一块一块独立封装的会查快递是一个技能懂退换货规则是另一个技能两者可以组合但不应该互相纠缠。当你把技能拆开每个技能自己有输入、输出、触发条件、执行步骤、需要的工具Agent只需要一个调度器去选择技能而不需要知道技能内部的每一行代码。1.2 技能和工具、Prompt到底什么关系很多新手把“工具”和“技能”混为一谈这是第一个坑。工具是Agent可以调用的外部函数比如查数据库、发HTTP请求、读写文件技能则是完成一个业务目标的能力它通常会编排多个工具并配合一套特定的提示词逻辑。举个例子工具层面只有“搜索商品”“查库存”“计算价格”三个函数而“推荐替代商品”这个技能则是在用户想要的商品缺货时先搜索同类商品再过滤库存充足的商品最后按价格排序并生成推荐语。这个技能里包含了对多个工具的编排、一条业务规则缺货时推荐替代品以及输出格式要求。Prompt也不是技能本身而是技能的一部分。每个技能可以有自己的指令模板里面写明你需要模型扮演什么角色、按什么顺序执行、什么情况下要调用哪个工具、输出用什么结构。把Prompt从System Prompt里拿出来跟着技能走最大的好处是隔离性。我不用再担心两个技能的指令互相污染而且换一个技能只需要更换对应的指令模块其他技能不受影响。2. 设计一套可复用的技能体系2.1 技能的“标准件”结构输入、输出、元信息、执行体我在实际项目里给每个技能定义了一组标准字段不管什么业务场景都套这套模板。最基本的是这么几个技能名称全局唯一用短横线小写形式比如order-status-check。描述一句话说清楚这个技能干什么适合什么场景写的时候要站在“调度器”的视角因为模型需要靠这个描述来决定该调用哪个技能。输入参数定义好的JSON Schema声明每个参数的类型、是否必填、取值范围。输出格式定义返回给Agent的结构化数据或者自然语言文本的模板。执行体一段可运行的代码或者是一个对模型指令的包装承载技能的实际逻辑。依赖清单需要哪些工具、内部服务、外部API以及版本要求。这个结构参考了函数式编程中“纯函数”的思想技能最好是无状态的输入一样输出就应该一样。这让单元的测试和复现都变得非常简单。如果你的技能内部需要记录状态比如多轮对话的临时记忆也尽量把状态通过上下文参数显式传进去而不是技能自己去读全局变量。2.2 技能的粒度太粗还是太细技能的粒度是设计里最艺术的部分。拆得太粗比如一个“处理所有订单问题”的大技能里面又塞订单查询又塞退款操作那和没拆一样拆得太细比如“计算两数之和”也算一个技能那Agent一上午能“学会”一千个技能调度器自己先懵了。我的经验是拿“业务价值”来衡量当一个技能可以被另一个业务流程独立复用的时候它就有资格成为独立技能。比如“获取订单状态”是底层技能它大概率会被“订单催发货”和“退款进度查询”两个上层技能共同调用。那么“获取订单状态”应该独立出来而不是揉进任何一个上层技能里。反观“计算两数之和”除非你的Agent真的是个计算器否则这东西放在工具层就够了不需要包装成技能。我还总结了一个实用判据如果一个技能的描述里必须写“如果用户提到X并且Y成立并且Z……”这种多条件逻辑那它大概率拆得太粗了。应该把复合条件拆成几个简单的子技能让调度器去组合。2.3 命名与描述AI调度器“找得到”才是王道这里有个很容易被忽略的地方Agent技能的直接调用者往往是另一个大模型而不是人。当Agent收到用户请求后模型会阅读所有技能的描述然后选一个或多个执行。这意味着技能描述写得好不好直接决定了调用的准确率。写技能描述有几个要点。第一用动词开头直接说明“做什么”。第二写明适用条件什么情况下用这个技能。第三说明典型场景示例比如“当用户查询未发货订单的预计到达时间时使用”。第四写下和相似技能的区别比如“这不同于修改订单地址后者用于更改收货信息”。这些看起来像是给同事写代码注释其实比代码注释更重要因为模型是真的会逐字逐句读的。命名同样要遵循简洁、表意清晰的原则。我见过有人用中文命名、有人用UUID命名前者在英文模型上容易出现编码问题后者让调度器完全摸不着头脑。统一用英文短横线命名加详细描述是目前最稳的方案。3. 落地实操在代码里搭一套技能仓库3.1 目录结构与核心接口设计纸上谈兵没用我们来看代码。我常用的目录结构大概是这样的agent-skills/ ├── registry/ │ ├── __init__.py │ └── skill_registry.py ├── skills/ │ ├── __init__.py │ ├── order_status/ │ │ ├── skill.py │ │ ├── schema.py │ │ └── prompt.md │ ├── refund_request/ │ │ ├── skill.py │ │ ├── schema.py │ │ └── prompt.md │ └── product_recommend/ │ ├── skill.py │ ├── schema.py │ └── prompt.md └── tools/ ├── http_client.py └── database_client.py每个技能目录下都有三个核心文件skill.py负责执行逻辑schema.py定义输入输出结构prompt.md存储这个技能专属的指令模板。之所以把prompt单独拆成md文件是因为它可以不经过代码发布流程单独修改运营同学也能参与调优。核心注册表的接口非常简单只做两件事注册技能、根据描述检索技能。下面是兼容Python 3.9的示例# registry/skill_registry.py from typing import Dict, Any, List, Optional class Skill: def __init__(self, name: str, description: str, input_schema: Dict[str, Any], output_schema: Dict[str, Any], executor: Any, prompt_template: Optional[str] None): self.name name self.description description self.input_schema input_schema self.output_schema output_schema self.executor executor self.prompt_template prompt_template class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} def register(self, skill: Skill): if skill.name in self._skills: raise ValueError(f技能重复注册: {skill.name}) self._skills[skill.name] skill # 这里做了一个简易索引把描述中的所有英文单词拆出来后续可做关键词匹配 self._refresh_index() def get(self, name: str) - Optional[Skill]: return self._skills.get(name) def list_skills(self) - List[str]: return list(self._skills.keys()) def _refresh_index(self): # 实际项目里可以接向量数据库做语义检索这里先用简单的包含匹配跑通 pass这套注册表没有很多花哨的东西核心价值在于统一。所有的技能都走同一个注册入口之后做动态加载、热更新、权限管理都顺理成章。3.2 技能动态加载让新技能像插U盘一样接入动态加载的好处是新增一个技能不需要改Agent主程序的代码。我之前在一个项目里试过每星期都会追加几个新技能如果每次都要改调度器代码维护成本根本扛不住。后来我直接扫描目录让技能通过setup()函数自我注册主程序完全不需要知道有哪些技能存在。具体做法是在每个技能的skill.py里定义一个setup(skill_registry)函数然后写一个加载器扫描所有技能目录逐个调用其中的setup# loader.py import importlib from pathlib import Path from registry.skill_registry import SkillRegistry def load_skills_from_directory(base_dir: str, registry: SkillRegistry): base_path Path(base_dir) for skill_dir in base_path.glob(skills/*/): rel_path skill_dir.relative_to(base_path) module_name f{rel_path.as_posix().replace(/, .)}.skill try: module importlib.import_module(module_name) if hasattr(module, setup): module.setup(registry) print(f已加载技能模块: {module_name}) except Exception as e: # 这里必须记录详细错误否则一个技能坏了会导致整个加载静默失败 import traceback traceback.print_exc() raise RuntimeError(f加载技能模块失败: {module_name}, 错误: {e})每个技能的skill.py则长这样# skills/order_status/skill.py from registry.skill_registry import Skill def setup(registry): skill Skill( nameorder-status-check, description查询订单的当前状态和物流进度。当用户询问我的订单到哪了或物流什么时候更新时使用。, input_schema{ type: object, properties: { order_id: {type: string, description: 订单号例如ORD123456}, }, required: [order_id] }, output_schema{ type: object, properties: { status: {type: string}, estimated_delivery: {type: string}, tracking_events: {type: array} } }, executorexecute_order_status, prompt_templateopen(Path(__file__).parent / prompt.md).read() ) registry.register(skill) def execute_order_status(order_id: str) - dict: # 这里写真实业务逻辑比如查数据库、调API # 简化实现仅做示例 return { status: in_transit, estimated_delivery: 2026-02-01, tracking_events: [ {time: 2026-01-28 10:00, location: 华中转运中心, desc: 包裹已到达}, ] }这个模式的核心就是约定大于配置。所有技能模块都在统一位置注册主流程不需要关心技能内部是查询数据库还是调用第三方API真正的“插拔式”开发。3.3 调用技能时的错误处理与降级技能不是永远不出错的。网络超时、下游接口返回脏数据、参数校验不通过这些都是日常。我在最初写Agent时对技能调用只做了最简单的try-except一旦出错就把错误直接抛给模型结果模型开始一本正经地编造理由体验极差。后来我总结了一套错误处理规范技能内部捕获已知异常转换为统一的错误码和可读的提示语。比如“下游服务超时请稍后重试”而不是让模型面对一串Traceback。遇到输入参数不合法技能要在执行前严格校验并给出缺失参数的具体提示这样调度器可以引导用户补充信息。设置超时和熔断。如果某个技能在下游服务连续失败三次短时间内直接不再调用返回降级答复。我在实际项目里见过最夸张的情况是下游接口卡了60秒Agent的一次对话整体超时用户体验直接归零。下面是一段技能调用器的示例嵌入了错误处理逻辑# call_skill.py import time from typing import Any, Dict def call_skill(skill: Skill, params: Dict[str, Any]) - Dict[str, Any]: # 参数校验 errors validate_params(skill.input_schema, params) if errors: return { success: False, error_code: INVALID_PARAMS, message: f参数有误: {errors} } try: start time.time() result skill.executor(**params) elapsed time.time() - start if elapsed 5: # 耗时过长打日志按需告警 print(f[WARN] 技能 {skill.name} 耗时过长: {elapsed:.2f}s) return { success: True, result: result } except TimeoutError: return { success: False, error_code: TIMEOUT, message: 服务响应超时请稍后再试 } except Exception as e: return { success: False, error_code: INTERNAL_ERROR, message: f技能执行失败: {str(e)} }这套错误处理方案让我在后续调试中减少了很多无谓的模型幻觉。模型拿到的永远是结构化的错误信息它知道该怎么和用户解释也不会乱猜原因。4. 常见问题与避坑指南4.1 技能边界模糊导致调度器选错技能技能粒度设计得不好最直接的表现就是调度器选错技能。比如用户问“我的退款多久到账”如果你的“refund_request”技能描述只写了“申请退款时使用”那模型可能误以为用户要申请退款直接走了一堆流程让用户填表单。正确做法是把“query refund status”和“submit refund request”分开并且描述里明确写清“当用户询问进度时使用”“当用户要求退款时使用”。我在实际项目中每次遇到调度器选错技能第一件事不是去调模型参数而是去检查技能的描述是否有歧义80%的问题都出在这。另外技能之间的“重叠”也很致命。两个技能都声称自己能查询退款状态模型会困惑到底调用哪个。我的原则是一个场景只允许一个技能负责。如果确实存在两个相近的技能就把它们合并或者用一个“技能路由”统一对外内部再细化分支。4.2 上下文爆炸与Prompt膨胀技能的prompt模板如果写得太长会让Agent的上下文被大量无用信息挤占。我见过一个朋友把仓库里所有技能的prompt全部塞进System Prompt加起来快两万汉字模型面对用户消息时注意力全被技能描述带走了连基本的对话都做不好。正确的做法是在调度层用embedding把用户问题映射成向量只把最相关的几个技能描述拼接到上下文中而不是全量注入。举例来说注册表里有20个技能但某次对话只涉及订单查询和退款进度那就只把这两个技能的prompt模板传给模型。我用了最朴素的实现先给所有技能做关键词倒排索引命中减少到候选技能后再把候选技能指令加载到上下文。再往后来升级成向量检索也行但核心思路不变——前两步是让Agent的外层大脑做路由内层大脑才真正执行。4.3 技能没有测试上线等于赌运气技能的测试和普通函数测试有一个巨大的差异它不光要验证执行结果对不对还要验证“模型在什么Prompt下会触发它”。我吃过暗亏某个技能的新版本能够正确处理数据了但因为描述改动太小模型在用户聊天时死活不触发反而走了另一个相似技能的fallback。所以我对每个技能做三层测试单元测试直接调用执行体验证输入输出逻辑。调度测试模拟一组用户问题检查是否正确触发目标技能。回归测试保存历史对话样本重放整个Agent流程看行为是否发生非预期变化。调度测试最简单的方式是写一个离线脚本输入问题列表输出每个问题命中的技能名。如果发现某个问题命中了错误技能就将这条问题加入回归集防止未来再次发生。一个几十条样本的回归集就能拦住大多数重构带来的行为漂移。4.4 热更新与版本兼容动态加载技能虽然方便但有版本隐患。我有一次更新了一个技能没有发现另一个技能偷偷依赖它的内部返回值格式结果线上跑了一天另一个技能频繁报KeyError。从那以后我给每个技能都加上版本号并且记录它依赖的其他技能的版本范围。注册表在加载时做一次依赖校验版本不匹配就拒绝加载同时打一条清晰的错误日志。依赖管理这件事听起来像是后端开发才需要关心的事但在Agent技能体系里更突出因为技能之间不仅有代码级依赖还有prompt语义级依赖。一旦一个上游技能的输出措辞变了下游技能的解析Prompt可能就崩了。所以版本升级后必须跑一遍回归测试这是个硬规矩。5. 技能体系还能怎么扩展5.1 技能可观测性给每个技能装上记录仪当技能数量上了两位数出问题时的排查难度直线上升。我现在的做法是在技能执行器外层加一层中间件记录每次调用的输入、输出、耗时、错误码并把记录写到结构化日志里。这样无论是定位模型选错技能还是下游接口抖动都能快速找到证据。配合现有的链路追踪打标签就能还原一次Agent请求全流程的技能调用顺序。这一步操作量不大收益却非常大。有一次用户反馈Agent在某个场景下“答非所问”我查日志发现它连续触发了三个技能第三个技能的输入参数被第二个技能的输出完全污染了。如果没记录中间过程这种bug只能靠猜。5.2 多技能组合与工作流编排单个技能解决单点问题组合技能才能解决完整任务。我在技能定义里增加了一个可选的“上下游技能”字段比如“处理退货退款”技能会把“查询订单状态”“校验退款资格”“生成退款单”“通知用户”四个技能串起来。但我一般不在技能内部直接硬编码调用别的技能而是把这种组合关系交给Agent的调度器通过描述和约束让调度器自行编排。比方说我可以给调度器一句这样的总体指令“当用户申请退货时先调用order-status-check验证订单已签收再调用refund-request-skills提交退款最后调用customer-notifier发送通知。”这比在代码里写死每个技能互相调用的流程要灵活得多也更容易适应模型能力的升级。5.3 让非技术人员也能维护技能最后聊一个组织协作的问题。技能体系最好设计成“技术框架”和“业务描述”分离。代码工程师只需要定义技能的执行体和接口业务同事则负责写技能的描述和prompt模板。只要约定好描述的语气和结构业务同事完全可以自己对技能做调优不用等一个开发周期。我在团队里就是这样的分工我搭好了技能注册表、加载器、测试框架然后把每个技能的prompt.md交给业务同学维护。他们改描述改措辞我跑回归测试两边都很轻松。Agent开发本质上是在做一套“开放式业务流程系统”技能体系才是这个系统的积木块谁都能用这些积木搭出新玩法。最后说几句个人体会我在踩坑的道路上反复摸爬滚打最深刻的感受是Agent开发真正难的不是把模型调通而是把模型周围的组织结构设计好。技能体系解决的核心问题就是“可维护性”它让一个原本依赖提示词和代码分支的混沌体变成了有边界、有契约、可测试的模块组合。如果你现在刚开始做一个稍复杂的Agent我建议你从第一天就把技能注册表搭好初始只有三个技能也没关系后面会发现省下的时间远超那点搭建成本。而如果你已经被单体Agent的嵌套逻辑逼到了崩溃边缘不妨现在就把体系重构过来越早动手越轻松。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询