资讯动态

Agent Skills架构设计:从技能注册表到编排调度的完整落地路径

发布时间:2026/10/17 7:23:48 来源:尧图企业网站定制
最近在折腾 agent skills 架构设计说句实话最初接触这个概念时我以为又是套了一层皮的“插件系统”等真正把一个多技能智能体从原型推到可维护状态之后才意识到这里面的门道比想象中深不少。简单讲skills 架构解决的是“给智能体装技能”这件事的标准化问题让模型在需要时能准确找到某个能力、调用某个工具、拿到结构化结果而不是把所有逻辑都塞进一个巨大的提示词或者一堆散落的函数里。如果你正在做 agent 应用或者想把现有的工具函数整理成可复用的技能体系这篇内容基本覆盖了从设计思路到落地实现的完整路径。1. 先看懂 agent skills 到底在解决什么问题我们先把问题摆到台面上。一个智能体最开始只有两三个内置功能时直接在代码里写if 意图 查天气就够了。但一旦功能多起来比如你的 agent 需要查日程、发邮件、写周报、审批流程、调内部 API、搜索知识库、处理报表事情就开始失控。我见过最典型的失控场景是这样所有功能以函数形式暴露给模型一个函数列表里排了四五十个条目每个条目一大段描述。模型在做工具选择时面对海量候选会变得极其不稳定——有时候明明该用“查询日程”却调了“创建日程”有时候该走 A 接口却选了 B 接口。这不是模型笨而是候选太多之后描述重叠、边界模糊准确率自然会下降。这时候你自然会想到分组、加前缀、优化描述。但如果你再往前想一步就会发现真正需要的是一套“技能基础设施”把每个能力封装成独立单元附带清晰的元数据、输入输出契约、权限声明和触发条件然后由一个注册中心和调度器统一管理。这就是 agent skills 架构设计要解决的核心问题。它本质上是把“函数列表”升级为“技能列表”再把“技能列表”升级为“带路由规则的技能注册表”。每个 skill 不仅包含实现代码还包含一堆机器可读的说明信息。模型不是一次性看到所有技能而是先根据用户输入做一次粗筛再在候选技能里做精确选择。这有点像你手机里的应用商店你不会把几万个 App 全部平铺在桌面上而是需要时搜索、按分类浏览、看评分描述再决定下载哪个。落到实际价值上这套架构能带来几个直接收益技能可独立开发、独立测试、独立上线不污染主流程代码。模型收到的候选列表大幅缩短工具选择的准确率肉眼可见地提升。技能之间的边界清晰一个技能出问题不会拖垮整个 agent。新增能力不需要改调度主逻辑只需要往注册中心里登记一个新 skill。权限和依赖可以集中管理而不是散落在各处。这里需要澄清一个容易混淆的点skills 不是新的算法也不是某种神秘框架更多的是一种工程组织模式。它把声明、路由、执行三层拆开让每层各司其职。理解了这一点后面所有设计决策都会变得顺理成章。2. 从零搭建一套 agent skills 架构目录、元数据与调用约定想明白要解决什么问题之后我们就可以动手设计了。在我看来一套能落地的 skills 架构至少要包含三个基础模块技能注册表Registry、技能调度器Dispatcher、技能执行器Executor。而在这三个模块之上最关键的其实是两个前置设计目录规范和元数据规范。2.1 目录与命名规范先定边界再写代码我先讲目录。没有目录规范技能一旦多起来代码会乱成一锅粥。我推荐一个很朴素的约定每个技能独立目录拥有完整的生命周期目录名即技能名。skills/ calendar/ list_events/ SKILL.md main.py schema.json create_event/ SKILL.md main.py schema.json mail/ send_email/ SKILL.md main.py schema.json report/ generate_weekly_report/ SKILL.md main.py schema.json这个结构有几个好处。第一每个技能自带“岗位说明书”也就是 SKILL.md 和 schema.json不依赖他人讲解也能直接接入。第二按领域分组之后调度器可以做领域级粗筛比如识别到用户输入涉及“发邮件”就直接进入 mail 域而不是遍历所有技能。第三版本管理非常方便每个目录可以单独评审、单独发布。我还习惯在每个技能目录里约定一个固定的生命周期状态写在 SKILL.md 头部version: 1.2.0 status: active # draft | active | deprecated owner: agent-corestatus字段特别实用。开发中的技能标成draft调度器默认不会对线上模型开放出问题的技能标成deprecated可以快速下线而不需要改代码。这个小习惯帮我避免了好几次“半成品技能被模型选中”的尴尬事故。2.2 元数据设计让 agent 知道什么时候该用它目录只是骨架元数据才是灵魂。一个 skill 能不能被准确调用基本取决于它的元数据写得怎么样。我把元数据拆成两个文件SKILL.md给人看、给模型做语义理解schema.json给程序做参数和返回值的强校验。SKILL.md里最核心的部分是描述和触发条件。描述不能写“这是一个日程查询功能”这种太弱了。合格的描述要包含三个要素这个技能做什么、在什么场景下使用、什么情况下不要用。我用过一段描述你可以感受一下差异name: list_events description: 查询用户在指定日期范围内的日程安排返回事件列表。 适用于用户询问“我明天有什么安排”“下周有哪些会议”“某天是否有空”等场景。 当用户想要创建、修改或删除日程时不要使用本技能应该交给 create_event 或 update_event。 triggers: keywords: - 日程 - 安排 - 会议 - 有空 - 排期 intent_patterns: - 查询.*日程 - .*(明天|今天|下周|下周[一二三四五六日]).*(安排|会议|有空) input_schema: schema.json你可能会问description和triggers是不是重复了不完全重复。我在实际设计里把两者做了分工description是给模型看的当模型在做候选排序时它依赖的是文本语义triggers是给规则引擎用的适合做第一轮的快速过滤。比如用户说“我下周的会多不多”关键词“下周”“会”就能直接命中根本不需要模型参与。这种混合策略能减少大量不必要的模型调用省钱、省延迟。schema.json则是严格的输入契约。我还是以查询日程为例{ type: object, properties: { start_date: { type: string, description: 查询起始日期格式 YYYY-MM-DD, format: date }, end_date: { type: string, description: 查询结束日期格式 YYYY-MM-DD含当天, format: date }, calendar_ids: { type: array, items: { type: string }, description: 可选指定要查询的日历 ID不传则查全部 } }, required: [start_date, end_date] }参数定义得越严格后续的校验和排错就越轻松。尤其建议把description写到每个参数字段里因为当模型需要自己填参数时它完全依赖这些描述来理解“这个参数到底该填什么”。2.3 调用协议与错误返回别把异常当成“没这个技能”执行阶段最容易被忽略的是返回值约定。我强烈建议所有技能统一返回一个结构化信封而不是直接返回原始文本或直接抛异常。我自己用的协议长这样{ status: success, skill: list_events, data: { events: [ { id: evt_001, title: 产品评审, start: 2025-01-15T14:00:0008:00, end: 2025-01-15T15:00:0008:00 } ], total: 1 }, meta: { cost_ms: 356, truncated: false } }出错时返回统一错误结构{ status: error, skill: list_events, error: { code: SKILL_INVALID_PARAMS, message: start_date 不能晚于 end_date, details: {} } }为什么这么强调统一协议因为 agent 主流程在拿到结果后要做两件事一是判断这次调用成功没有二是决定怎么向用户表达。如果每个技能各返回各的格式有的直接抛异常有的返回字符串有的返回None主流程就要写一大堆分支去适配还很容出错。统一协议还有一个隐藏好处可以接通用的后处理逻辑。比如超时、截断、重试、日志上报全部可以做成中间件不用每个技能各自实现一遍。3. 实操过程与核心环节实现设计好规范和协议之后真正写起代码来其实并不复杂。我来拆一套我实际用过的最小内核实现去掉业务噪音之后核心逻辑就三块。3.1 最小内核实现注册中心、调度器与执行器我先写注册中心。它的职责很简单启动时扫描技能目录把每个技能的元数据加载到内存并提供查询接口。import json from pathlib import Path from typing import Any, Dict, List, Optional class SkillRegistry: def __init__(self, skills_root: str): self.skills_root Path(skills_root) self._skills: Dict[str, Dict[str, Any]] {} def load_all(self) - None: for skill_dir in self.skills_root.glob(*/*): skill_md skill_dir / SKILL.md schema_json skill_dir / schema.json if not skill_md.exists() or not schema_json.exists(): continue meta self._parse_skill_md(skill_md) meta[input_schema] json.loads(schema_json.read_text(encodingutf-8)) meta[entry] skill_dir / main.py self._skills[meta[name]] meta def get_by_name(self, name: str) - Optional[Dict[str, Any]]: return self._skills.get(name) def search(self, query: str, domain: Optional[str] None) - List[Dict[str, Any]]: candidates [] for meta in self._skills.values(): if meta.get(status) ! active: continue if domain and meta.get(domain) ! domain: continue score self._match_score(query, meta) if score 0: candidates.append((score, meta)) candidates.sort(keylambda x: x[0], reverseTrue) return [meta for _, meta in candidates] def _match_score(self, query: str, meta: Dict[str, Any]) - float: # 简易关键字打分生产环境可以换成向量召回 score 0.0 for kw in meta.get(triggers, {}).get(keywords, []): if kw in query: score 1.0 return score这个类做了三件事加载目录、按名字查技能、按关键字做粗筛。注意我特意留了一个_match_score函数生产环境里你可以把它替换成 embedding 向量检索或者调用一个小模型做粗排。这里的关键设计是注册中心只负责“候选召回”不负责“精确决策”。接着是调度器。调度器拿用户输入先做粗筛再决定最终调哪个技能。我推荐的方式是先把候选列表压缩到 3 到 5 个再让模型做精细选择。这样模型既不会面对 50 个选项也不会因为候选太少而误选。class SkillDispatcher: def __init__(self, registry: SkillRegistry): self.registry registry def dispatch(self, user_input: str, llm_choose_fn) - Optional[str]: candidates self.registry.search(user_input) if not candidates: return None if len(candidates) 1: return candidates[0][name] # 候选多于一个时交给模型做语义匹配 return llm_choose_fn(user_input, candidates)llm_choose_fn是一个回调函数由调用方注入。实际项目里它的实现就是一次 LLM 调用把用户输入和候选技能的 name、description 拼进提示词让模型返回最合适的那个技能名。这里我没有把 LLM 调用写死在调度器里是为了方便测试——单测时可以注入一个固定返回的 fake 函数。最后是执行器。执行器负责加载 skill 的实现代码、校验参数、执行并捕获异常。import importlib.util import traceback class SkillExecutor: def __init__(self, registry: SkillRegistry): self.registry registry def execute(self, skill_name: str, params: Dict[str, Any]) - Dict[str, Any]: meta self.registry.get_by_name(skill_name) if not meta: return {status: error, skill: skill_name, error: {code: SKILL_NOT_FOUND, message: fskill {skill_name} not found}} errors self._validate_params(params, meta.get(input_schema, {})) if errors: return {status: error, skill: skill_name, error: {code: SKILL_INVALID_PARAMS, message: ; .join(errors)}} try: module self._load_module(meta[entry]) result module.run(params) return {status: success, skill: skill_name, data: result} except Exception as exc: return {status: error, skill: skill_name, error: {code: SKILL_RUNTIME_ERROR, message: str(exc), details: {traceback: traceback.format_exc()}}} def _validate_params(self, params: Dict[str, Any], schema: Dict[str, Any]) - List[str]: # 这里用 JSON Schema 校验库会更严谨演示版做必填检查 errors [] for field in schema.get(required, []): if field not in params: errors.append(fmissing required param: {field}) return errors def _load_module(self, entry: Path): spec importlib.util.spec_from_file_location(entry.stem, entry) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module执行器里有几个点要说明。_validate_params在演示代码里只做了必填检查真实项目务必引入 JSON Schema 的校验库否则参数类型错误会在运行中才暴露。_load_module动态加载每个技能的main.py要求每个技能文件必须实现一个run(params)函数这是我们对技能实现者的统一约束。三块代码拼起来一个最基础的 agent skills 架构就已经能跑通了。技能开发者只需要在目录里加文件主流程代码一行都不用改。3.2 一次完整调用的链路拆解把代码串起来看一次实际调用。假设用户说“帮我看看明天下午有哪些空会议室。”第一步主流程拿到用户输入先走调度器。注册中心里的全部技能缩成了几个领域这个输入明显属于“会议室领域”粗筛命中了search_available_rooms这一个候选。调度器一看候选数量是 1直接返回技能名不需要额外调用 LLM省了一次模型请求。第二步执行器拿到技能名和参数。参数怎么来的这里有两种做法一种是由调度器或主流程调用 LLM 将自然语言转换为参数 JSON另一种是由 skill 入口函数自己解析。我推荐前者因为主流程统一负责“自然语言到结构化参数”的转换技能本身只接收干净参数实现会简单很多。第三步技能内部执行。search_available_rooms调用了内部会议室系统 API拿到房间列表和空闲时间段把结果整理成统一信封返回。主流程拿到{status: success, data: {rooms: [...]}}再交给 LLM 组织语言回答用户。整个链路里模型一共参与了两次一次是参数提取一次是结果转自然语言。技能匹配这一步基本没让模型做选择题因为粗筛阶段已经把候压缩到极致。这就是这类架构节省 token、提升准确率的关键所在。3.3 权限、依赖与并发被忽视的三个细节最小内核跑通之后接下来就要面对生产环境的三座大山权限、依赖、并发。权限是所有 skill 架构里最容易被拖到后面处理的问题。你肯定不想“查询日程”的技能拥有“删除日程”的权限更不想一个第三方技能能随便读环境变量里的数据库密码。我的做法是在SKILL.md里强制声明权限需求permissions: network: allow: - api.internal-rooms.example.com deny: [] env: allow: - ROOM_SERVICE_API_KEY filesystem: read: allow: [/tmp] write: allow: []然后在执行时用统一的权限中间件做拦截。凡是代码里出现的对外请求域名、环境变量名、文件路径都必须和声明匹配不匹配直接拒绝。这个机制看起来死板但能防止很多人为疏忽。依赖管理也是大坑。一个技能可能依赖另一个技能比如“发送会议邀请”依赖“查询参会人空闲时间”。要处理这种情况我会在元数据里加一块depends_on: - list_events - search_contacts注册中心加载完成后会对全量技能做一次拓扑排序检测。如果 A 依赖 B、B 又依赖 A直接报错拒绝启动避免运行到一半才发现循环依赖。另外技能内部的 Python 第三方库依赖也不要全局安装最好每个技能虚拟环境独立或者用依赖锁文件防止两个技能需要同一个库的不同版本。并发控制同样值得提前设计。多个技能同时执行时不能用一条线程跑到底否则一个技能里的阻塞调用会卡死整个 agent。我习惯给每个 skill 一个独立线程池并设置最大并发数同时加大全局超时控制。超时后的处理要融合到统一协议里{ status: error, skill: search_available_rooms, error: { code: SKILL_TIMEOUT, message: execution exceeded 5000ms limit } }这样主流程能够区分“技能不存在”“参数错误”“运行超时”“依赖服务挂了”等不同错误类型分别给出不同的用户提示和重试策略。4. 技能编排复杂任务的“串”与“并”单技能跑通只是第一步真实业务场景里往往需要多个技能配合完成一个复杂任务。比如用户说“帮我安排明天下午的部门会议并订一间能容纳十人的会议室会前把资料发给所有人”。这句话至少涉及创建日程、查会议室、查联系人、发邮件四个技能。这时候就需要编排层。4.1 顺序编排前一个的输出是后一个的输入最简单的编排是顺序执行。我习惯用一个轻量级 DSL 来描述流程而不是在代码里写死。下面是一段 YAML 编排定义workflow: arrange_meeting steps: - id: fetch_participants skill: search_contacts params: department: {{user.department}} - id: find_room skill: search_available_rooms params: capacity: 10 time: {{event.time}} - id: create_event skill: create_event params: title: {{user.title}} participants: {{steps.fetch_participants.data.contacts}} room: {{steps.find_room.data.room_id}} time: {{event.time}}每个步骤声明依赖前一个步骤的输出编排引擎按依赖顺序执行前一步失败就终止整个流程返回具体是哪个步骤出错。这个 DSL 的好处是流程可以做成可视化配置换一个场景只需要改 YAML不用重新发版。写这套 DSL 时要注意“输入来源”的清晰性。我自己踩过一个坑步骤 A 的输出字段是data.contacts步骤 B 却读成了data.users最后运行时才发现字段对不上。现在我在 schema 里会定义每一步的输出结构并在编排加载时做静态校验尽量减少这类低级错误。4.2 并行编排能同时做的绝不串着等编排里还有一种常见需求多个相互独立的步骤可以并行执行。比如“查会议室”和“查参会人空档”互不依赖串行执行白白浪费时间。我在 DSL 里加了一个parallel节点workflow: arrange_meeting_v2 steps: - id: prepare parallel: - id: fetch_participants skill: search_contacts params: department: {{user.department}} - id: find_room skill: search_available_rooms params: capacity: 10 time: {{event.time}} - id: check_calendar skill: list_events params: start_date: {{event.date}} end_date: {{event.date}} - id: create_event skill: create_event params: title: {{user.title}} participants: {{steps.prepare.fetch_participants.data.contacts}} room: {{steps.prepare.find_room.data.room_id}} time: {{event.time}}并行节点里每个分支的变量访问要用steps.prepare.branch_id前缀避免命名冲突。我在实现并行时用了asyncio.gather同时给每个分支设置了独立超时一个分支超时不会影响其他分支继续执行但最终主流程会拿超时分支的错误信息。4.3 条件路由根据分支结果决定下一步第三种常用编排模式是条件路由。比如“如果上午没有空闲会议室就自动改为下午并再查一次如果下午也没有就把错误返回给用户”。我用一个switch节点表达- id: room_retry switch: - condition: {{steps.find_room.data.total}} 0 next: find_room_next_day - condition: {{steps.find_room.data.total}} 0 next: create_event default_next: report_no_room条件表达式走一段表达式引擎不直接写 Python 代码因为 DSL 未来可能暴露给非研发人员配置。条件里只允许访问步骤输出、用户上下文和固定的常量。在实现时我限定表达式白名单不允许任意代码执行这是安全底线。4.4 从链式编排到图编排守住无环底线当编排越来越复杂YAML 看起来已经不是简单的“链”而是一张图。这时候我建议你至少在内存里做一次有向无环图检查。方法不复杂把所有步骤看成一个有向图节点是 skill 调用边是依赖关系然后用拓扑排序检测是否存在环。如果存在环直接报错并提示是哪个步骤组合导致的循环引用。很多人会问既然都图编排了为什么不直接用现成的工作流引擎我的体会是现成引擎确实功能全但跟 agent 的对话场景不太匹配。agent 编排是动态生成的流程往往依赖用户当轮输入和中间结果而不是静态固化的业务流。所以我在实现中采用了“半静态编排”常见流程用 YAML 预置异常分支和动态调整由模型在运行时修改编排参数但修改范围被 DSL 严格约束。这样既有确定性又保留了灵活性。5. 常见问题与排查技巧实录这套架构从开发到上线我大概积累了不少值得记录的排查经验。这里整理成一个速查表每一条都是真实踩过的坑。现象可能原因排查与解决用户输入明确但技能没被选中元数据 trigger 关键词覆盖不全或 description 描述与真实场景偏差太大打印注册中心粗筛候选列表看每个命中的分数用真实用户语料回放测试技能被选中但执行时参数频繁缺失schema 描述不够明确或者参数示例不足在 schema.json 的每个字段增加examples字段LLM 提取参数时会参考返回结果太大导致模型上下文爆炸skill 返回了完整列表而未做分页在协议中增加分页字段限制单次page_size主流程需要时再请求下一页技能执行超时但主流程无感外部服务接口慢skill 自身没有设置超时在统一中间件层添加全局超时并在 skill 输出中附加cost_ms字段监控多个技能间接产生循环依赖编排 DSL 配置错误启动阶段做拓扑排序检测打印循环链上的技能名半成品技能被模型误选技能已经加载到注册表但逻辑未完成用status: draft标记调度器粗筛时直接过滤非 active 技能技能内部抛出的异常信息不够明确skill 开发者直接抛了通用 Exception约定所有技能异常必须带错误码并在统一协议里透传error.code同一个外部 API 被多个技能重复调用缺少共享数据层出现重复查询在编排层增加简单的结果缓存按输入参数哈希存储除了表格里的内容我再分享两条独家心得。第一条一定要给 skill 调用链路加一层可观测日志。不是只记“调用了哪个技能”而是记录完整链路用户输入原文、粗筛候选列表及其得分、最终选中的技能名、提取到的参数 JSON、技能返回值、耗时、错误信息。把这七件事打成一个日志对象。有了这层日志绝大多数问题都能在几分钟内定位原因否则就只能靠抽查复现瞎猜。第二条开发阶段一定要做“技能回放测试”。我建议把线上真实用户输入脱敏后收集成测试语料库每次新加一个技能就跑一遍全量语料看是否有“旧技能被挤掉”“新技能被误选”的情况。这个回放测试看起来笨重但它是防止技能数量膨胀后系统准确率滑坡的最有效手段。我在一次技能从 8 个增加到 23 个时靠这套回放测试发现了 3 个原本会被误选的场景提前把描述修正了。6. 从 v1 到演进这条架构路径还能怎么走这套架构跑到稳定之后我发现它留出了几条很自然的演化路径如果你正在设计自己的版本可以提前留好扩展点。第一技能库可以往“多角色共享”方向走。同一个组织内不同角色的 agent销售助手、HR 助手、研发助手不需要各自维护一套技能而是共享同一个注册中心只是在调度层增加角色过滤条件。也就是说一个技能在创建时声明适用于哪些角色调度器在做粗筛时直接按当前会话角色过滤掉不可见技能。这样技能的管理成本和复用效率都会明显改善。第二可以考虑技能热更新机制。当注册中心支持从远程配置拉取元数据时你可以做到某个技能描述更新后立刻生效不用重新发布整个 agent 应用。我当时是给技能文件加了一个版本字段和远程仓库同步机制SKILL.md 变化后自动 reload 对应技能。要注意的是热更新只更新元数据和实现文件不要在执行过程中改动注册表否则会出现并发读写的奇怪问题稳妥做法是每次更新先构建一份全量快照再原子切换。第三可观测性还能继续加深。除了调用日志我会建议给每个技能增加“置信度”字段由调度器在候选排序后记录自己对每个候选的把握。汇总这些置信度之后你就能发现哪些技能描述写得模糊、哪些技能经常和同类技能竞争。这是一个持续优化的入口——与其凭感觉反复改描述不如让数据告诉你该改哪里。第四安全方面还值得再做一层。技能执行时可以考虑放进受限环境比如容器或子进程沙箱限制 CPU、内存、网络访问。尤其是当你准备引入第三方技能开发者生态时沙箱不是可选项而是必选项。我见过很多内部 demo 因为觉得“都是自己写的代码”就不做隔离结果一次误操作删了共享目录里的测试数据得不偿失。最后说说我对这套架构的总体判断。它不是银弹解决不了模型能力不足的问题它更像一个管理框架让智能体在多能力场景下保持可控。我踩过最深的坑是过早追求复杂——一开始就想做可视化编排、插件市场、动态热更新结果基础元数据都没设计好上线后天天救火。如果你的项目正处于起步阶段我的建议很简单先把注册、调度、执行三条链路跑明白把元数据写清楚把回放测试建起来。这四件事做完你的 agent 就已经比大多数没有架构直接堆代码的项目稳出一条街了。至于更复杂的编排、沙箱、热更新等业务规模逼到那一步再上也不迟。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑