ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent技能系统设计:从注册到路由的工程实践

Agent技能系统设计:从注册到路由的工程实践

1. 从“agent-skills”这个标题说起:它到底在解决什么问题

第一次看到“agent-skills”这个标题,我脑子里蹦出来的不是某个具体框架,而是一类很典型的需求:让一个智能体(Agent)具备可复用、可组合、可插拔的能力模块。说白了,就是别每次做一个新任务,都从零写一遍提示词、工具调用逻辑和状态管理,而是把“技能”抽出来,像搭积木一样拼装。

这个标题背后对应的场景非常广。往小了说,是一个对话机器人在不同任务间切换时,需要动态加载不同的处理逻辑;往大了说,是一个多智能体协作系统里,每个 Agent 都要能按需获取“查资料”“算数据”“调接口”“写文件”这些能力。核心痛点就一个:能力与执行体解耦。没有这层解耦,代码会迅速膨胀成一团乱麻,改一个功能牵动全身。

我见过不少团队一开始图快,把所有逻辑塞进一个巨大的提示词模板里,结果就是:加一个新技能要动主流程,删一个旧技能又怕影响别的分支,测试成本高得离谱。agent-skills 这个思路,本质上是在做能力注册与发现机制,让 Agent 在运行时知道自己“会什么”,以及“怎么用”。

适合谁来参考?如果你正在做智能体应用、任务自动化、或者任何需要动态编排能力的系统,这个标题下的内容都值得细看。哪怕你只是写一个稍复杂的脚本工具,里面“技能注册—路由—执行—回传”这套模式也能直接借鉴。下面我会从设计思路、核心细节、实操落地和踩坑排查四个层面,把这件事拆透。

2. 整体设计与思路拆解:为什么是“技能”而不是“功能”

2.1 技能与功能的本质区别

很多人第一反应是:技能不就是功能吗?我写个函数不就行了。这里有个关键差异。功能是面向开发者的调用单元,技能是面向 Agent 的决策单元。功能只需要输入输出正确,技能还必须附带“什么时候该用我”的元信息。

举个例子,你有一个“查询天气”的功能,函数签名是get_weather(city)。但 Agent 面对用户说“明天出门要不要带伞”时,它得自己判断:这需要调用天气技能,参数是城市,而且可能还要结合降水概率做推理。所以一个完整的技能定义至少包含四部分:技能名称、自然语言描述、参数模式、执行入口。描述是给模型看的,参数模式是给校验用的,执行入口是给运行时调的。

我试过只写函数不写描述,结果模型根本不知道有这个能力存在,或者知道存在但不知道怎么填参数。后来把描述补上,命中率立刻上来了。这就是技能和裸函数的第一个分水岭。

2.2 为什么选择注册制而不是硬编码

设计上我强烈建议走注册制。硬编码的意思是,在主流程里写一堆 if-else 或者 switch,根据意图分发到不同处理函数。这种做法在技能少于五个时还能忍,一旦超过十个,主流程就变成了一棵巨大的判断树,而且每加一个技能都要改核心代码,违反开闭原则。

注册制的做法是:每个技能自己声明“我是谁、我能干什么、我怎么被调用”,然后统一注册到一个技能仓库里。Agent 运行时只跟仓库打交道,通过检索或匹配找到合适的技能。这样新增技能就是新增一个文件,主流程一行不用动。实测下来,这种模式在技能数量增长到二三十个时,维护成本几乎是线性的,而硬编码是指数级上升的。

注意:注册制带来的一个副作用是“技能发现”问题。技能多了之后,模型可能选错或者选重。所以描述的质量比数量更重要,后面会专门讲怎么写好技能描述。

2.3 技能粒度的取舍逻辑

粒度太粗,一个技能干太多事,参数复杂、复用性差;粒度太细,技能数量爆炸,模型选择困难。我的经验法则是:一个技能对应一个明确的动作意图,且这个意图在自然语言里有稳定的表达方式。

比如“发送邮件”可以是一个技能,但“发送邮件并记录日志并更新客户状态”就不适合做成一个技能,应该拆成三个,由 Agent 编排。判断标准很简单:如果用户的一句话里经常同时包含这几个动作,那可以考虑合并;如果它们经常独立出现,就必须拆开。这个取舍直接决定了后续路由的准确率。

3. 核心细节解析与实操要点:技能定义怎么写才靠谱

3.1 技能描述的三段式写法

技能描述是模型选择技能的唯一依据,写得好不好直接决定成败。我总结了一个三段式模板:动作 + 对象 + 边界。动作说明干什么,对象说明对什么干,边界说明什么情况下不该用。

比如一个“文本摘要”技能,差的描述是“总结文本”。好的描述是:“对用户提供的长文本生成简洁摘要,适用于文章、报告、会议记录;不适用于代码文件或结构化数据表。”后面这句边界特别重要,因为模型经常把代码文件也丢进来摘要,结果输出一堆无意义的自然语言。

我实测过,加上边界说明后,误触发率能降一半以上。这个细节很多教程不讲,但实际用起来差别巨大。

3.2 参数模式的设计与校验

参数模式建议用 JSON Schema 来描述,因为大多数模型对 JSON Schema 的理解已经比较成熟。每个参数要写清楚类型、是否必填、描述和示例。示例尤其关键,模型看到示例后填参准确率明显提升。

{ "name": "search_docs", "description": "在内部文档库中检索信息,适用于查找产品说明、流程规范;不适用于实时数据查询。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "检索关键词,建议使用名词短语", "example": "报销流程" }, "top_k": { "type": "integer", "description": "返回结果数量,默认5", "default": 5 } }, "required": ["query"] } }

校验环节不能省。模型偶尔会漏填必填参数或者类型填错,运行时必须做一次校验,校验失败就返回明确的错误信息让模型重试。我一般会给两次重试机会,两次还不行就降级到兜底回复。这个机制能挡住大部分参数层面的问题。

3.3 技能执行入口的隔离设计

执行入口最好做成无状态的,所有上下文通过参数传入。这样做的好处是技能可以被并发调用,也方便测试。如果技能内部依赖全局变量或者外部会话状态,调试起来会非常痛苦。

另外建议给每个技能加超时和异常捕获。一个技能卡住不能拖垮整个 Agent。我通常设 10 秒超时,超时后返回“技能执行超时”让模型决定下一步。异常也要转成结构化信息返回,而不是直接抛栈,否则模型看不懂。

提示:技能执行结果建议统一成{status, data, message}格式。status 用 success/error,data 放实际内容,message 放人类可读的说明。模型对结构化结果的处理能力远强于纯文本。

4. 实操过程与核心环节实现:从零搭一个技能系统

4.1 技能仓库的目录结构

我习惯用文件系统来组织技能,每个技能一个目录,里面放定义文件和执行脚本。这样新增技能就是复制目录改内容,非常直观。

skills/ search_docs/ skill.json handler.py summarize_text/ skill.json handler.py send_email/ skill.json handler.py

skill.json放名称、描述、参数模式,handler.py放执行逻辑。启动时扫描 skills 目录,把所有 skill.json 加载进内存,形成一个技能列表。这个列表就是后续路由的依据。

4.2 技能加载与注册的实现

加载逻辑很简单,遍历目录、读 JSON、校验必填字段、注册到内存字典。关键是要做去重和冲突检测。如果两个技能同名,必须报错而不是静默覆盖,否则排查起来很麻烦。

import json import os class SkillRegistry: def __init__(self, skills_dir): self.skills = {} self.skills_dir = skills_dir def load_all(self): for name in os.listdir(self.skills_dir): path = os.path.join(self.skills_dir, name, "skill.json") if not os.path.exists(path): continue with open(path, "r", encoding="utf-8") as f: meta = json.load(f) skill_name = meta["name"] if skill_name in self.skills: raise ValueError(f"技能重名: {skill_name}") self.skills[skill_name] = meta return self.skills

这段代码我用了很多次,稳定可靠。注意编码统一用 utf-8,否则中文描述会乱码。

4.3 技能路由的两种策略

路由就是根据用户输入决定调用哪个技能。我常用两种策略,视技能数量而定。

技能少于十五个时,直接把所有技能描述拼进提示词,让模型输出技能名和参数。这种做法简单直接,准确率也够用。技能多了之后,提示词会超长,成本和延迟都上去了,这时候改用两阶段路由:先用一个轻量检索模型(比如基于关键词或向量相似度)召回前五个候选技能,再让主模型在这五个里选。实测下来,两阶段路由在五十个技能规模下,准确率只比全量低一点点,但延迟降了六成。

策略适用技能数优点缺点
全量提示< 15实现简单,准确率高提示词长,成本高
两阶段路由>= 15延迟低,可扩展召回环节可能漏选

4.4 技能执行的完整链路

一次完整的技能调用包含:解析模型输出、校验参数、执行 handler、格式化结果、回传给模型。每一步都要有日志,否则出问题根本不知道卡在哪。

def execute_skill(registry, skill_name, params): if skill_name not in registry.skills: return {"status": "error", "message": "技能不存在"} meta = registry.skills[skill_name] # 参数校验 required = meta["parameters"].get("required", []) for key in required: if key not in params: return {"status": "error", "message": f"缺少参数: {key}"} # 动态加载 handler module_path = f"skills.{skill_name}.handler" module = importlib.import_module(module_path) try: result = module.run(**params) return {"status": "success", "data": result} except Exception as e: return {"status": "error", "message": str(e)}

这里用动态导入是为了避免启动时加载所有 handler,减少内存占用。异常统一捕获成 error 状态,模型看到后可以决定重试还是换技能。

4.5 多轮对话中的技能状态管理

Agent 经常需要多轮才能完成一个任务,比如先查资料再总结再发送。这时候技能之间需要传递中间结果。我的做法是在会话上下文里维护一个scratchpad,每个技能执行完把结果写进去,下一个技能可以从里面读。

但要注意,scratchpad 不能无限增长,否则提示词会爆。我一般只保留最近三轮的结果,更早的做摘要压缩。这个细节在长任务里特别重要,不然跑到第五轮就开始报上下文超限。

5. 常见问题与排查技巧实录:那些文档里不会写的坑

5.1 技能选错或选重怎么办

这是最高频的问题。模型面对两个描述相近的技能时,经常随机选一个。排查思路分三步:先看描述是否有重叠关键词,再看参数模式是否相似,最后看是否有边界说明缺失。

解决办法有两个。一是强化边界描述,明确写出“不适用于什么”。二是加负样本示例,在描述里直接写“当用户说 X 时不要用本技能”。我试过在描述里加一句“本技能不处理实时数据”,误触发立刻少了很多。

5.2 参数填错或漏填的排查

模型填参出错通常有三种:类型错、漏必填、值不合理。类型错和漏必填靠校验能挡住,值不合理比较麻烦,比如把“明天”填成日期格式但格式不对。

我的经验是,在参数描述里给具体示例比给格式说明更有效。写“日期格式 YYYY-MM-DD,例如 2024-01-15”,比写“请传入标准日期”管用得多。另外可以在 handler 里做二次校验,不合法就返回明确提示让模型重填。

5.3 技能执行超时与降级

外部接口慢或者技能逻辑重时,超时是常态。一定要设超时,并且超时后返回结构化错误,而不是让整个流程挂住。我一般设 10 秒,超时后返回“技能执行超时,请稍后重试或换用其他方式”。模型收到后通常会尝试别的技能或者告知用户。

注意:超时时间不要设太短,有些检索类技能本身就需要几秒。我踩过的坑是设了 3 秒,结果正常检索全被截断,误判成失败。

5.4 常见问题速查表

问题现象可能原因排查动作解决方向
技能不被调用描述缺失或太模糊检查 skill.json 描述字段补全动作+对象+边界
选错技能描述重叠对比候选技能描述加边界和负样本
参数报错必填缺失或类型错看校验日志补示例,加二次校验
执行超时外部依赖慢看 handler 耗时设超时,加降级
上下文爆掉scratchpad 太长看上下文长度压缩历史,只留三轮

5.5 独家避坑技巧

第一个技巧:技能描述里不要用同义词堆砌。有人为了提升命中率,把“查询、检索、搜索、查找”全写进去,结果模型反而困惑。用一个最准确的词就行。

第二个技巧:handler 里不要做模型该做的推理。技能只管执行,判断该不该调用是模型的事。我见过有人在 handler 里写一堆 if 判断用户意图,这就本末倒置了,维护起来极其痛苦。

第三个技巧:给每个技能加一个 dry-run 模式。调试时只校验参数不真正执行,能快速定位是路由问题还是执行问题。这个模式在联调阶段能省大量时间。

6. 技能系统的扩展方向与个人体会

这套东西搭起来之后,扩展空间其实很大。我目前尝试过两个方向,效果都不错。一个是技能组合,把常用的一串技能调用固化成一个复合技能,减少模型编排负担。另一个是技能版本管理,同一个技能保留多个版本,灰度切换,出问题能快速回滚。

技能组合的思路是:如果发现某三个技能经常连续出现,就把它封装成一个新技能,内部依次调用。这样模型只需要选一次,而不是选三次,准确率和速度都提升。但要注意别过度封装,否则又回到粒度太粗的老路。

版本管理则是给 skill.json 加一个 version 字段,注册时保留多版本,路由时按配置选版本。这个在技能逻辑变更时特别有用,可以先小流量验证再全量。

我个人在实际操作中的体会是,agent-skills 这套模式的价值不在于技术多复杂,而在于它强迫你把“能力”想清楚。很多团队做智能体做不下去,不是因为模型不行,而是因为能力边界没定义好,导致模型在模糊地带反复横跳。把技能描述写清楚、参数定明白、边界划出来,很多问题自然就消失了。最后再分享一个小技巧:每次新增技能后,拿二十条真实用户问法跑一遍回归,看命中率和参数准确率,比拍脑袋调描述靠谱得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进