Agent Skills技能体系设计:从注册表到调度器的完整实践
发布时间:2026/10/21 22:06:51 锦皓数字建站

1. Agent Skills到底是什么——先建立整体认知最近大模型应用圈子里“Agent Skills”这个词出现的频率越来越高了。但说实话很多人一上来就把技能等同于写几个工具函数或者简单理解成多轮对话里的“插件开关”。我实际做下来发现事情远没有这么简单。从一个最简单的现象说起同样是让模型调用一个天气查询功能有的人直接把接口文档塞进System Prompt让模型“看着办”有的人则会把查询动作封装成一个带名字、带描述、带参数协议的“技能”让模型按照结构化方式去调用。前一种办法在小规模实验时够用但一旦技能数量超过十个模型就会开始出现选择困难、参数乱传、结果不会解析等一系列问题。后一种办法就是Agent Skills的雏形。我这里理解的Agent Skills落地形态是一套“可注册、可发现、可调用、可组合”的能力单元体系。每个技能并不只是函数本身而是包含三个关键组成部分触发条件、调用契约、结果协议。触发条件告诉模型这个技能在什么场景下该用调用契约规定了模型必须以什么JSON结构传参数结果协议则定义了技能执行完以后回传给模型的文本或数据长什么样。技能化封装能解决的问题很明确第一把模型“该怎么用某个能力”的决策成本从复杂的Prompt描述中剥离出来交给结构化的元数据去表达第二让多个能力之间能够以统一协议协作而不是每个函数都有一套自己的调用风格第三也是最重要的让技能本身可以像积木一样被复用、替换、组合而不是每次新场景都从头写一遍。这篇文章适合谁看如果你正在做AI Agent相关的应用开发比如智能客服、工作流自动化、个人知识助手或者正准备把大模型接入到自己的业务系统里那这套技能体系的设计思路能直接搬去用。哪怕你只是对“Agent到底怎么控制工具”这件事好奇文中的拆解也能帮你弄清楚背后的运行逻辑。我在后面的章节里会以一个模拟项目X的技能调度系统为主线从设计、实现到排查问题逐步展开。这个项目是我自己一直在迭代维护的一套轻量级框架不算复杂但踩过的坑足够典型值得拿出来说说。2. 技能库的整体设计思路与方案拆解2.1 为什么不能把所有能力都塞进一个“万能函数”先说一个我自己走过的弯路。早期版本的模拟项目X把所有的工具能力集中做成一个超级函数靠一个大的if-else分支去区分内部动作。表面上看起来只有一个技能接入成本低但真正调起来问题一堆。首当其冲的是参数冲突。比如一个函数里既有“搜索关键词”又有“生成报告”的逻辑两套参数的schema叠在一起模型经常会混淆。一次实测中模型本想生成报告却把搜索关键词传进了报告标题字段最后生成的文档内容完全没法用。其次是安全边界。所有能力共用一个入口意味着权限控制只能在函数内部做细粒度判断。一旦逻辑写得不严谨模型就可能通过巧妙地组合参数访问到不该访问的资源。这个风险在技能数量增加以后会被不断放大。正确的思路是“一事一技能”。每个技能只干一件事参数最小化职责单一化。这不只是工程上的洁癖而是为了给模型提供更清晰的决策信号当模型的注意力集中在一小撮参数上时选错参数的概率会显著降低。2.2 技能注册表与统一调用契约技能化之后下一个关键问题是模型怎么知道有哪些技能可用答案不是把每个技能的完整代码都丢给模型而是只暴露“技能描述元数据”让模型基于这些描述完成选择与参数生成。在我的模拟项目X里技能注册表是一个核心组件。每个技能模块在启动时都通过装饰器注册到注册表中注册信息包括技能名、功能描述、参数Schema、返回类型和示例调用。注册表会把这份元数据列表交给上层调度器调度器在构建模型上下文时只把精简后的“发现信息”拼进提示词。这样设计的直接好处是上下文长度可控。若你有五十个技能完整描述可能有好几万Token但精简描述只需保留技能名称、一句话功能描述和关键参数总量可以压到几千Token以内。统一调用契约也至关重要。我在项目中定义了标准的协议。技能接收一个结构化字典作为输入并返回一个标准化的结果对象其中包含执行状态、数据内容、耗时和辅助信息。无论技能内部是调用API还是查询数据库对外表现都必须一致。这样调度器接收到结果后不需要区分技能类型就能统一做后处理。2.3 技能描述写得好不好直接决定模型的调用准确率这一节我想多说几句因为技能描述是整个技能体系里最容易被低估的部分。我见过很多人的技能描述就写“获取天气信息”这样的描述模型确实能理解但理解得比较模糊。举一个具体例子。模拟项目X中有一个“查询知识库”技能最初描述是“查询知识库内容”。结果模型在应答闲聊问题时频繁调用它导致回答速度慢还会返回大量不相关的检索片段。后来我把描述改成了“仅当用户明确询问某特定文档或产品手册内容时使用日常闲聊、通用知识问答不要调用”。模型的选择准确率一下子提高了很多。技能描述本质上是在给模型划定使用边界。写描述时有三件事必须写清楚技能解决什么问题、什么场景下不该用、参数分别有什么约束。如果参数允许为空也最好显式声明否则模型会因为不确定而编造参数。提示技能描述尽量用“否定式”圈定边界比只写“什么时候该用”更能约束模型行为。3. 实操过程从零实现一个可运行的技能调度框架3.1 环境准备与基础目录结构这一节进入正题我会把模拟项目X的技能调度框架逐步搭建起来。整个项目语言选用Python依赖尽量精简核心只用到Pydantic做数据校验框架本身不绑定特定的大模型SDK方便你迁移到自己常用的模型平台上。先看目录结构agent-skills-demo/ ├── main.py # 程序入口组装组件并启动交互循环 ├── registry.py # 技能注册表管理技能元数据 ├── schema.py # 统一数据模型定义 ├── dispatch.py # 调度器路由模型调用指令到具体技能 ├── skills/ │ ├── __init__.py # 技能包入口自动完成技能注册 │ ├── base.py # 技能基类约定接口规范 │ ├── search_kb.py # 示例技能知识库检索 │ └── calculator.py # 示例技能数学计算在动手前先明确一下运行环境。我用的是Python 3.10及以上版本安装Pydantic即可。不建议在旧版本上跑因为Pydantic V2对类型注解的支持更完整尤其是在处理嵌套Schema时比V1顺畅很多。pip install pydantic3.2 定义技能基类与数据结构整个框架的第一步是把技能的通用契约定死。我在skills/base.py中定义了一个技能基类所有具体技能都继承它。基类里最重要的是三个方法get_skill_schema()返回该技能的元数据描述包括名称、功能描述、参数Schema。execute(params: dict)真正执行技能逻辑接收已校验的参数。validate_params(params: dict)执行前做一次防御式验证。基类定义如下from abc import ABC, abstractmethod from typing import Any from schema import SkillResult class BaseSkill(ABC): # 技能唯一标识注册表用它做索引 skill_name: str # 给模型看的描述决定模型何时调用该技能 skill_description: str abstractmethod def execute(self, params: dict[str, Any]) - SkillResult: 执行技能核心逻辑返回标准化结果 raise NotImplementedError def validate_params(self, params: dict[str, Any]) - dict[str, Any]: 参数校验默认直接返回子类可覆盖实现专门的逻辑 return params def get_skill_schema(self) - dict[str, Any]: 生成供模型读取的调用协议 return { name: self.skill_name, description: self.skill_description, parameters: self._parameters_schema(), } def _parameters_schema(self) - dict[str, Any]: 默认参数Schema子类按需覆盖 return {type: object, properties: {}, required: []}参数Schema这一块我推荐直接用Pydantic的model_json_schema()来生成避免手写JSON错误百出。具体到每个技能里只需要定义一个继承自BaseModel的参数模型然后调model_json_schema()即可。3.3 技能注册与路由实现有了基类下一步是把技能装进注册表。我实现了一个极简的注册表核心逻辑是“类级别的自动收集”。from typing import Type from skills.base import BaseSkill _SKILL_REGISTRY: dict[str, BaseSkill] {} def register_skill(skill_cls: Type[BaseSkill]): 类装饰器将技能类实例化后注册到全局表 instance skill_cls() _SKILL_REGISTRY[instance.skill_name] instance return skill_cls def get_all_skill_schemas() - list[dict]: 返回所有技能元数据供构建提示词使用 return [skill.get_skill_schema() for skill in _SKILL_REGISTRY.values()] def get_skill(name: str): 按技能名取出技能实例 return _SKILL_REGISTRY.get(name)每个具体技能只需要在自己的模块里添加装饰器即可完成注册。举例知识库检索技能。from skills.base import BaseSkill from registry import register_skill from schema import SkillResult from pydantic import BaseModel, Field class SearchKBParams(BaseModel): query: str Field(description检索关键词必须明确指向知识库中的主题) top_k: int Field(default3, description返回结果条数1-5之间) register_skill class SearchKnowledgeBaseSkill(BaseSkill): skill_name search_knowledge_base skill_description 在内部知识库中检索与关键词相关的文档片段。仅当用户明确询问产品手册、公司政策或项目文档时使用日常闲聊请勿调用。 def _parameters_schema(self): return SearchKBParams.model_json_schema() def execute(self, params): validated SearchKBParams(**params) segments self._fake_search(validated.query, validated.top_k) return SkillResult(statussuccess, data{segments: segments}) def _fake_search(self, query, top_k): # 实际项目中可接入向量数据库或全文检索 return [f模拟检索结果{query} 相关的第{i}段 for i in range(top_k)]你可能已经注意到校验逻辑放在了execute内部。我这里用Pydantic将字典转成参数模型天然支持类型转换和必填校验。如果参数不符合要求会抛出验证错误调度器可以捕获并回传给模型修正。3.4 调度器核心把模型输出转成可执行指令技能调度器是整个体系的中枢。它的任务有三个把全部技能描述交给模型、解析模型的调用意图、分发调用并回收结果。先看调度器的核心代码import json from registry import get_all_skill_schemas, get_skill from schema import SkillResult class Dispatcher: def __init__(self, llm_client): self.llm_client llm_client def build_system_prompt(self): skills_schema json.dumps(get_all_skill_schemas(), ensure_asciiFalse) prompt f 你是一个具备工具调用能力的智能助手。以下是可用技能协议 {skills_schema} 当用户需求与某个技能匹配时你必须输出如下JSON格式调用指令 {{skill: 技能名, parameters: {{...}}}} 若无需调用技能直接输出自然语言回复。 return prompt def process_user_message(self, user_message: str) - str: sys_prompt self.build_system_prompt() model_output self.llm_client.chat(systemsys_prompt, useruser_message) # 尝试将模型输出解析为调用指令 call self._parse_call_instruction(model_output) if call is None: return model_output skill get_skill(call[skill]) if skill is None: return 未找到对应技能请重新表述需求。 try: result skill.execute(call[parameters]) except Exception as e: # 参数校验失败时将错误信息回传模型用于自修正 return self._ask_model_to_fix(call, str(e)) return f技能执行完成结果{json.dumps(result.data, ensure_asciiFalse)}_parse_call_instruction是一个比较关键的函数。模型输出的内容不总是干净JSON前面可能带有思考过程或者被Markdown代码块包裹。我做了解析容错def _parse_call_instruction(self, text: str): text text.strip() # 去掉代码块包裹 if text.startswith(): lines text.split(\n) lines [line for line in lines if not line.strip().startswith()] text \n.join(lines) try: obj json.loads(text) if isinstance(obj, dict) and skill in obj: return obj except json.JSONDecodeError: pass return None如果模型就是不肯输出JSON可以在System Prompt里加一个few-shot示例用真实的技能调用样例去示范。这一步通常能把成功率从70%拉到90%以上。3.5 将调度器接入主循环最后把调度器接入一个简单的命令行交互循环验证整条链路。from dispatch import Dispatcher from skills import * # 确保技能模块导入并完成注册 class MockLLMClient: 模拟模型客户端实际项目替换为真实模型调用即可 def chat(self, system, user): # 演示逻辑如果用户消息包含“计算”返回调用指令 if 计算 in user: return json.dumps({skill: calculator, parameters: {expression: user.replace(计算, )}}) if 知识库 in user or 文档 in user: return json.dumps({skill: search_knowledge_base, parameters: {query: user}}) return 抱歉我不明白你的意思。 if __name__ __main__: client MockLLMClient() dispatcher Dispatcher(client) while True: user_input input(用户: ) if user_input in (exit, quit): break reply dispatcher.process_user_message(user_input) print(助手:, reply)这个演示用的是模拟客户端真实场景下你需要换成实际的模型服务。相应的调用参数也要映射到模型原生支持的Function Calling或工具调用接口。这个过程不难核心是把模型返回的工具调用结构转成“技能名参数”的字典再交给调度器。4. 技能调度链路中的关键细节与优化空间4.1 参数Schema的生成与微调Pydantic的model_json_schema()生成的JSON Schema比较啰嗦会带不少默认字段。直接用给模型看会浪费不少Token。我通常会对Schema做一层精简只保留type、properties、required和description去掉title、additionalProperties等字段。举个例子原始Schema可能长这样{ properties: { query: { description: 检索关键词, title: Query, type: string } }, required: [query], title: SearchKBParams, type: object }精简后{ type: object, properties: { query: {type: string, description: 检索关键词} }, required: [query] }别小看这一步。在多个技能的情况下Schema体积会直接影响模型的注意力分布。越精简模型越容易抓住关键参数。4.2 执行结果的后处理策略技能执行完之后返回的数据一般不适合原样塞回给模型。比如知识库检索技能返回了五段切分后的文档片段总量可能超过两千字直接拼接到回复中会让回应变得冗长。我的做法是在技能内部先做一轮结果摘要。技能返回的内容是已经整理过的、可读性较高的文本而不是原始检索结果。这样调度的后续压力会小很多也避免了大段噪声进入模型上下文。注意技能内部最好完成“数据加工”而非只做“数据搬运”。把脏活留在技能里把干净的文本交给模型。4.3 多技能并发执行与依赖编排单技能调用基本已经够用但真实业务里经常要多个技能搭配。比如用户问“对比一下某产品两个型号的区别”智能体可能需要同时查产品库和用户手册然后把结果合并。我这里出于轻量考虑没有引入复杂的编排引擎而是提供了一种简单的“顺序规划”模式调度器在思考阶段让模型输出一组有序的技能调用列表逐个执行并把前一次的结果作为上下文传给下一次调用。代码上只需要在调度器的主循环里加一层循环把单次调用换成多次迭代。不过这个模式很快会遇到上下文无限膨胀的问题。因此我在每次迭代后会压缩历史记录仅保留每个技能结果的摘要而不是完整内容。这个方法思路清晰实现起来也不难适合中小规模项目。5. 常见问题与排查技巧实录5.1 模型明明看到了技能描述却从来不调用这是我在开发中最常遇到的状况也是最让人抓狂的。技能写得清清楚楚模型却像没看见一样每次都用自然语言直接回答。排查这类问题我一般按三个步骤来。先确认技能描述是否已经拼进了System Prompt。很多人犯的低级错误是技能元数据生成后没有真正拼接到请求里。第二步检查技能描述是否与用户提问语料脱节。如果描述里全是内部术语而用户表达比较口语化模型很难建立关联。这时候需要把技能描述改写成能和用户意图直接对齐的措辞。第三步是看模型本身是否支持工具调用有些模型不具备原生工具调用能力需要靠提示词硬推导效果自然会差不少。如果以上都排除了还是不行可以在System Prompt里强制加上一句“当用户请求与你掌握的技能直接相关时你必须输出JSON调用指令而不是自然语言回复”。措辞强硬一些效果会立竿见影。5.2 参数校验报错以后模型反复死循环当技能执行抛出异常时我把错误信息直接回传给了模型希望它自我修正。但有些情况下模型会陷入死循环把同样的错误参数再传一遍或者干脆疯了一样地反复调用同一个技能。后来我加了一个限制同一轮对话中技能调用失败后最多再给模型两次修正机会。超过次数后调度器直接返回兜底话术而不是继续循环。这看起来很简单却极大提升了系统的稳定性。5.3 技能选择冲突多个技能同时匹配用户意图技能数量多了以后可能出现“选择困难”。比如用户问“帮我查一下产品售价”库存技能和促销技能都可能匹配。我的处理策略是调整技能描述的优先级信号在描述前加“优先使用”“仅当”之类的限定词。同时我还在调度器里加了一条规则当模型输出的技能名在多个高相似描述间摇摆优先选择历史记录中调用频率更高的那个。这个策略在工程上叫“带置信度的路由”不需要复杂的训练只需要在注册表里维护一个简单的调用计数。5.4 技能结果返回过大撑爆上下文窗口技能执行结果超过上下文窗口的情况在接向量检索类技能时特别常见。要么结果条数太多要么单条片段太长。我给出的解法比较务实限定返回数量、截断单条长度、强制技能内做摘要。以知识库检索技能为例我在参数里强制约束了top_k最大值同时实现了一个简单的滑窗摘要方法只保留每段文本的首尾各两句话中间用省略号替代。这样既能保留语义主旨又能显著压缩Token消耗。5.5 常见问题速查表问题现象可能原因解决方向模型从不调用技能技能描述与用户意图不匹配重写描述对齐口语化表达技能调用但传参错误参数Schema缺失描述或描述混乱精简Schema逐一完善字段描述校准一次后再次报错模型陷入修正死循环限制重复调用次数走兜底多个技能同时匹配描述边界不清晰添加否定式约束和优先级词技能返回结果不可读技能内缺少数据加工将摘要、格式化移到技能内部上下文快速膨胀结果原样回传且不压缩截断、摘要、历史压缩5.6 一条很实用的调试技巧最后分享一个既能调试又不过度侵入系统的技巧给Dispatcher加一个“调试模式”。输出模型原始响应、解析后的技能调用指令、参数校验结果以及每个技能的耗时和返回摘要。class Dispatcher: def __init__(self, llm_client, debugFalse): self.debug debug # ... def _log_debug(self, tag, content): if self.debug: print(f[{tag}] {content})调试模式下排错了几天的问题往往半天就能找到答案。特别是模型输出格式不稳定的问题通过日志可以直接看到原始内容不必反复猜。6. 我在实操中对Agent Skills的几点体会把这套技能调度体系跑起来以后我对Agent开发的理解发生了一个比较明显的变化与其花大量时间调Prompt不如把精力花在定义边界清晰的技能协议上。前者是让模型猜后者是给模型明确的操作手册。这就像带一个新同事干活。你光跟他说“多帮帮忙”他大概率会帮倒忙。但如果你给他一本写好操作流程、接口说明、注意事项的手册他就能立刻上手。Agent Skills其实就是这本手册。我实际项目里已经把技能数量扩到了四十多个其中有查询类的、计算类的、报告生成类的、甚至还有触发外部流程的。只要每个技能都遵守统一的注册协议与结果协议新增一个技能基本不需要改动调度器代码只需要写一个新的技能类并在包里导入。最后再说一个小技巧给技能安排版本号。技能逻辑经常会迭代但模型上下文里只会有一份当前描述。如果新旧版本切换时出了问题没有版本号根本没法定位是哪次变更导致的。现在我在技能元数据里加了一个version字段排查问题时能直接对上号。这套技能体系谈不上完美但胜在结构简单、粘合度低可以逐个模块替换。如果你正在折腾自己的智能体项目不妨先按这个思路把技能协议定下来再慢慢迭代会比一味堆功能扎实得多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。