做Agent一段时间后我最大的感受是模型本身的推理能力已经不是瓶颈真正卡住项目进度的是“怎么让Agent稳定地把事情做完”。agent-skills 这个项目就是我从实际开发里反复折腾后抽出来的一套做法——把智能体的能力从随意堆砌的 function list 改造成结构化的技能体系。它解决的核心问题是当Agent的技能逐渐变多、任务链路逐渐变长之后如何让模型选对技能、按顺序执行、出错能兜底、新能力还能复用。这套思路适合正在做智能体应用、机器人流程、或者任何想给大模型配“手脚”的团队参考也适合刚入门、想理解 skills 和 tools 到底差在哪的同学。1. 为什么需要技能层从“工具堆积”到“能力编排”1.1 没有技能层的 Agent 项目长什么样很多项目一开始都特别简单。给模型注册几个函数比如查天气、算个加减乘除、查一下数据库里某个订单的状态然后写个循环把用户问题传给模型模型判断要调哪个函数程序执行函数把结果返回再让模型总结。这套东西在前 5 个、10 个工具的时候其实还挺好用的。但一超过 20 个问题就全冒出来了。最直观的就是每次对话都要把全部工具的描述、参数 schema 塞进上下文Token 消耗越来越大接口响应也越来越慢。更要命的是模型开始“选择困难”——工具描述写得稍微模糊一点它就可能把查库存的工具当成查价格的工具用或者明明可以直接算出来的东西它偏要绕一圈去调一个根本不需要的工具。这就像你家里来了个新手管家你不是把钥匙、账单、通讯录分类放好而是把所有东西一股脑堆在桌上然后跟他说“你自己看着办”。他能办成就怪了。所以我在实际项目里越来越觉得单纯堆 function 不是一条能长期走的路。你需要一个中间层把“模型可以调用的原子操作”整理成“模型可以理解、可以决策、可以依赖的能力单元”这就是技能层。1.2 skills 与 tools、functions 的本质差异很多朋友会问skills 不就是把几个 function 包一层吗表面上看是这样但关键是这一层做了什么事情。function 是单个函数tool 是把函数包装成模型能调用的接口而 skill 是一个自带“使用说明书”和“执行策略”的能力单元。一个 skill 内部可能包含多个 tool 调用、中间步骤、异常处理、结果后处理甚至包含自己独立的 prompt。模型面对的不再是二十个孤零零的函数而是一组意图清晰的能力块。我举个例子。你想让 Agent 完成“把某个 CSV 文件里的数据做一个统计分析并画图保存”。如果走工具路线你可能要注册 read_file、parse_csv、compute_mean、compute_std、generate_chart、save_image 这 6 个函数然后指望模型自己想出调用顺序。一旦中间某一步失败模型还得自己想怎么恢复效果基本靠运气。如果走技能路线你只需要注册一个“分析并可视化数据文件”的 skill。这个 skill 内部自己知道要先加载文件、再解析格式、再计算统计量、再画图、再保存。模型只需要决策“当前任务是否匹配这个技能”匹配了就把任务交出去。至于技能内部是分三步还是五步模型不需要关心。这才是 skills 和 tools 最重要的区别工具让模型自己编排流程技能让模型决策调用哪个流程。前者适合小规模玩具后者适合真正要落地的业务。维度function / toolskill粒度单个原子操作一组相关操作 决策信息模型负担需要自己编排调用顺序只需选择技能并传参错误处理靠外层代码兜底技能内部封装重试、回退上下文占用所有工具描述全量暴露只暴露技能级描述按需展开复用方式基本靠复制代码可作为独立单元跨项目复用1.3 技能层改变了什么加了技能层之后Agent 的决策链路从“选函数 排顺序”简化成“选技能 传参数”。这个变化直接影响了三个关键指标。第一是准确率。模型面对的选择空间变小了每个技能的描述可以写得非常详细、非常贴近业务语义命中率自然高很多。第二是可维护性。业务逻辑变更时你只需要改某个技能内部的实现而不是去改一排函数的注册逻辑。第三是可扩展性。新需求来了新写一个 skill 注册进去不影响已有技能模型下一次对话就能用上新能力。我后来看不少成熟的智能体项目也走了类似的路只是叫法不同有的叫 capabilities有的叫 routines有的叫 plug-ins。本质都是同一件事——把“模型直接操作代码”改成“模型调用能力模块”。agent-skills 只是把这件事做成了更通用、更轻量的框架。2. agent-skills 的整体设计技能不是把代码包一层就完事2.1 技能元信息建模先让模型“读得懂”技能设计技能层的第一步是定义一套结构化的技能描述。这套描述有两个读者一个是你自己一个是模型。很多人只把这当成写文档的功夫结果技能描述写得跟代码注释一样干巴巴的模型根本看不出这个技能到底适合什么场景。我在 agent-skills 里给每个技能定义了一套元信息核心字段包括技能名称、功能描述、输入参数 schema、输出结构、适用场景、前置条件、依赖关系、分类标签、版本号。其中功能描述是给模型看的不是给你自己看的。写技能描述有几个硬性要求写清楚这个技能“在什么情况下用”和“在什么情况下不能用”最好写两个正面例子和两个反面例子。比如一个“天气查询”技能描述不能只写“查询天气”要写“根据城市名获取实时天气信息。适合用户询问气温、降水、风力、空气质量时使用。不适合查询历史天气不适合预测未来 7 天以外的天气不适合查询路况”。这段描述虽然长但模型拿到之后判断起来会非常稳。相比之下如果你只写“查询天气”模型可能会在用户问“明天会下雨吗”的时候错误地以为这个技能能做天气预报实际上你根本没接预报服务。我见过不少团队栽在这上面总觉得描述越短越省 Token结果模型频繁选错反而消耗了更多 Token 和调用次数。技能描述里省下的每一分钱最后都会在错误的重试里加倍还回去。2.2 注册中心与动态发现技能多了以后总不能每次全量暴露给模型。agent-skills 里做了一个注册中心负责技能的登记、分类、筛选和依赖解析。注册中心的核心逻辑其实不复杂维护一个技能字典支持按分类、按标签、按关键词去筛选候选技能。每次模型发起请求前先根据用户的意图或上文的上下文做一次粗筛把最可能用到的 5 到 8 个技能挑出来再去调用模型做精细选择。这一步的意义在于上下文里的技能描述不再随技能数量线性增长而是保持在一个相对稳定的量级。技能库可以有 100 个技能但每次模型实际看到的可能只有 6 个。这既控制了成本也提高了选择准确率。依赖解析也很关键。技能之间会有依赖关系比如“生成图表”技能依赖“数据加载”技能的输出格式。注册中心在注册技能时会校验它的依赖项是否已经存在。如果某个技能依赖了未注册的模块注册时会直接报错避免运行期才发现调用链断裂。我在这里特意提醒一下技能粒度不要拆得太细。经验值是一个技能内部的步骤控制在 3 到 8 步太细会让技能退化成普通工具失去模型决策的意义太粗又会变成一个大杂烩模型无法判断什么时候该用它。2.3 技能调用的生命周期管理每个技能在运行时都会经历一个完整生命周期待选、已选中、执行中、成功、失败、降级、完成。我在 agent-skills 里把生命周期状态显式建模原因很朴素——Agent 项目一旦复杂起来你很难靠打印日志去定位问题必须有一套可观测的状态流转。技能执行成功之后结果要标准化。不是说技能内部想返回什么就返回什么而是统一包装成结构化的输出包含状态码、结果摘要、结构化数据和可选的自然语言说明。因为模型后续还需要根据这个结果做进一步决策如果你的输出结构乱七八糟模型根本没法继续。技能失败之后要有明确的降级策略。我在项目里给每个技能配置了可选的回退方案比如主技能挂掉了是否有备用技能可以顶上或者技能内部步骤失败时是否可以跳过某一步继续。比如生成图表的技能如果发现中文字体缺失与其直接报错不如先检查有没有备用字体有就换备用字体继续没有才把错误返回。这里要尤其注意不要把技能内部的所有异常都直接暴露给模型。模型处理不了你底层抛出的文件访问错误它只会开始胡编乱造。技能层应该负责把底层异常翻译成模型能理解的问题比如“图表生成失败原因是字体资源不可用当前环境缺少中文字体配置”。这样模型才有可能给出合理的下一步决策。3. 核心实操从零搭一个最小的技能引擎3.1 技能基类与协议定义说了这么多直接上代码。我先把技能基类定义出来。这个基类不用很复杂但要把通信协议定清楚。from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional class Skill(ABC): # 技能元信息 name: str description: str category: str general version: str 1.0.0 tags: List[str] [] input_schema: Dict[str, Any] {} output_keys: List[str] [] # 依赖项形如 [skill_a, skill_b] dependencies: List[str] [] # 可选的降级技能名单 fallback_skills: List[str] [] # 最大执行时长超过则任务失败单位秒 timeout: int 30 def __init__(self, registryNone): self.registry registry abstractmethod def execute(self, params: Dict[str, Any], context: Dict[str, Any]) - Dict[str, Any]: 执行技能返回标准结果结构。 params 是模型按 input_schema 传入的参数。 context 是技能层自己维护的上下文对象包含状态、历史结果等。 pass classmethod def to_skill_spec(cls) - Dict[str, Any]: 生成面向模型的选择描述。 这里只放 model 需要看到的信息不放内部实现细节。 return { name: cls.name, description: cls.description, category: cls.category, version: cls.version, tags: cls.tags, input_schema: cls.input_schema, output_keys: cls.output_keys, timeout: cls.timeout, }写代码时我觉得最需要注意的就是 to_skill_spec 这个方法。很多项目喜欢把技能类的属性和方法全部序列化给模型这是错的。模型需要的是一个干净的选择界面不是实现细节。比如某个技能内部用了什么第三方库、有多少个配置参数这些模型完全不关心。3.2 注册中心与候选技能筛选接下来是整个技能引擎的心脏注册中心。它的职责是承接技能注册请求并对外提供“技能发现”的能力。import re class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} self._categories: Dict[str, List[str]] {} def register(self, skill_cls): if skill_cls.name in self._skills: raise ValueError(f技能名称重复: {skill_cls.name}) # 依赖校验 for dep in skill_cls.dependencies: if dep not in self._skills: raise ValueError(f技能 {skill_cls.name} 声明了未注册的依赖: {dep}) self._skills[skill_cls.name] skill_cls self._categories.setdefault(skill_cls.category, []).append(skill_cls.name) def unregister(self, skill_name: str): self._skills.pop(skill_name, None) for cat, names in list(self._categories.items()): if skill_name in names: names.remove(skill_name) def select_candidates(self, query: str, top_k: int 8) - List[Skill]: 根据查询语句筛选候选技能给模型做最终决策。 scores [] q query.lower() for skill_cls in self._skills.values(): score 0 spec skill_cls.to_skill_spec() # 简单关键词打分标签和描述命中优先 for tag in skill_cls.tags: if tag.lower() in q: score 5 if skill_cls.name.lower() in q: score 8 # description 中的所有动词、名词都可能命中这里只做粗匹配 for w in re.findall(r[\u4e00-\u9fa5a-zA-Z0-9], spec[description]): if w.lower() in q: score 1 scores.append((score, skill_cls)) scores.sort(keylambda x: x[0], reverseTrue) return [cls for score, cls in scores[:top_k] if score 0]这个筛选器写得非常原始但它传递了一个核心设计思路先把大库缩小到候选集再交给模型精细决策。生产环境里你完全可以换成向量检索甚至让模型先做一次粗分类但分层筛选的思路是不变的。字母全匹配、拼音、多语言混查这些场景关键词打分肯定不够但作为最小框架足够了。实际项目里要稳定的话我建议直接把 query 和技能描述各出一套向量做近邻检索再把 top_k 技能喂给模型。这样可以显著降低模型的选择压力。3.3 把技能选择交给大模型技能引擎的核心循环其实是一个通用的 agentic loop。我简化一下用一个 call_llm 函数代替具体的模型接口调用它支持传入 messages 和 available_skill_specs返回模型决策结果。在实际项目里你只需要替换成自己用的模型协议。def call_llm(messages, skill_specs, configNone): 调用大模型接口遵循通用的 function calling / tool calling 协议。 参数说明 - messages: 对话消息列表 - skill_specs: 候选技能描述列表 # 这里替换为你实际使用的大模型接口即可 pass def run_agent(user_query, registry: SkillRegistry): messages [{role: user, content: user_query}] # 1. 粗筛候选技能 candidates registry.select_candidates(user_query, top_k8) if not candidates: return 没有找到可以处理当前请求的能力 # 2. 让模型在候选技能里选择 skill_specs [c.to_skill_spec() for c in candidates] model_response call_llm(messages, skill_specs) # 3. 解析模型选择 if model_response.type skill_call: skill_name model_response.skill_name params model_response.arguments skill_cls registry.get(skill_name) # 这里也要加一个防御模型可能选了一个粗筛集之外的技能 if skill_cls is None: return 模型选择了不可用的技能请重试 result skill_cls().execute(params, context{}) # 4. 把执行结果返回给模型让它生成最终回复 messages.append({ role: tool, tool_call_id: model_response.id, content: str(result), }) final call_llm(messages, []) return final.text else: # 模型没有选技能直接回答 return model_response.text这里有个很容易踩的坑模型返回的技能名称可能不在候选集里。我在项目里遇到过很多次模型的输出和输入因为各种原因不一致比如生成了多余的空格、换行或者干脆张冠李戴。因此解析模型输出时一定要做严格的名称匹配甚至要做一次“模型选了谁”和“候选列表里有什么”的交叉校验。如果发现模型选了候选集之外的技能稳妥做法是直接拒绝执行重新让模型选择而不是冒险去调用。4. 技能编排与组合让 Agent 真正干完一整件事4.1 顺序编排、分支编排与并行执行单个技能调用跑通之后下一个绕不开的问题就是多个技能如何协作。现实任务几乎没有一个技能从头做到尾的通常需要几个技能按顺序或按条件组合起来。顺序编排对应的是流水线场景。比如“生成数据分析报告”这个任务内部至少三步读文件、算指标、写报告。这三步必须按顺序执行前一步的输出是后一步的输入。在技能层实现顺序编排很简单就是把一个技能内部再拆成几个子步骤。分支编排对应的是决策场景。比如用户说“帮我分析这个 PDF 的内容如果是合同就提取关键条款如果是论文就总结核心观点”。这时你需要几个技能但具体用哪个取决于文件类型判断。分支可以在技能内部做也可以放在技能之上由一个“路由技能”来接。我建议尽量在技能内部做分支因为路由技能的职责不够聚焦模型容易乱。并行执行是提升效率的关键。如果两个技能之间没有依赖关系应该并发执行。比如技能 A 负责从数据库拉数据技能 B 负责读取配置文件它们互不依赖就可以同时启动。但并行执行也带来一个麻烦状态同步。两个技能同时跑谁先完成都可能影响后续步骤。我在 agent-skills 里处理方式是每个技能执行完以后把标准输出写入 context 的独立键位调度层只在“依赖键位齐全”时才继续往下走。4.2 实战让 Agent 完成一份数据分析报告我拿一个实际例子把编排讲清楚。假设用户上传了一个 CSV 文件说“帮我统计一下销售额的月度趋势并生成柱状图”。这个任务在技能层会这样组织第一步模型收到请求后从候选技能里选中“数据加载与解析”技能。技能内部流程是读取文件内容、识别分隔符、推断列类型、返回标准化数据结构。这个技能返回一个统一的表格对象而不是原始文本。第二步模型需要继续操作表格对象于是触发“指标计算”技能。这个技能接收表格对象和统计口径参数比如按月份聚合、求和销售额。它内部调用计算逻辑返回聚合结果。第三步模型调用“图表生成”技能把聚合结构转成图表配置再调用图形渲染模块生成图片存到指定目录返回文件路径和缩略信息。第四步模型调用“自然语言总结”技能或者不用技能直接基于前面的结果生成一段中文报告文本里引用图表路径。整个流程跑下来Agent 从收到一个文件到交付一份带图表的报告一共只做了 4 个技能决策。而如果不用技能层模型面对的是十个左右的函数每一步选哪个、传什么参数都要自己现想出错概率成倍上升。我在实际测试中对比过技能层方案下模型首次成功率明显更高整体耗时反而更短。原因是模型大部分时间花在生成大量中间 JSON 上技能层的结构化输出机制基本消灭了这部分无效开销。4.3 技能编排的失败恢复编排最需要重视的是中途失败怎么办。顺序编排里第二步失败了第三步要不要继续硬跑比如销售额聚合失败还继续调用图表生成最后只能生成一张空的图。我的原则是状态一旦污染立即终止不要带病执行。落到实现上每个技能执行完毕都会校验自身输出是否符合输出模式如果不符合就标记为失败然后反馈给上层调度。上层调度根据技能配置的 fallback_skills 尝试降级如果降级也不行就把失败原因和已完成的步骤摘要拼成一段上下文交给模型去判断下一步是重试还是改用别的方案。这里有一个很妙的细节失败信息也要写成模型能理解的语言而不是堆字段。比如“图表生成失败数据格式不符合预期的二维数组结构可能是数据中缺少数值列”比“Error: expected array got object”对模型决策有用得多。5. 常见问题与避坑实录5.1 技能太多模型经常选错这是大家问得最多的问题。技能数量到 30 个以上的时候光靠描述优化已经不够了因为候选列表本身就有几十个模型的分辨能力会明显下降。我的做法是三层过滤。第一层是关键词和标签的硬过滤先把明显不相关的技能去掉。第二层是根据上下文过滤比如当前线程里已经执行过的技能、已经采集到的实体信息可以进一步缩小范围。第三层才是模型决策而且每次只喂 5 个左右的技能不让模型做 30 选 1 这种高难度任务。这一套组合下来模型选错率能降一半以上。另外还有个容易被忽略的点技能名称的命名风格要统一。有的技能叫 analyze_data有的叫 生成图表有的叫 sendMail模型面对中英混合、风格混乱的技能列表时语义边界会很模糊。我在 agent-skills 里要求所有技能名一律用英文小写加下划线描述用中文标签统一小写。实测下来这一个小规范就能减少不少误选。5.2 上下文爆炸技能内部执行步骤多的时候每一步的结果塞回消息列表几轮下来上下文就爆了。这其实是技能层最容易出现的问题尤其当你把技能内部中间过程全部暴露给模型的时候。我建议技能内部步骤的中间结果默认不进入模型上下文。模型只应该拿到这个技能的最终输出。比如数据加载技能返回的文档内容如果有几千行绝对不能全部塞回去而是应该把摘要、前几行预览、统计信息放回去完整数据放进外部存储技能需要时通过内部接口读取。你甚至可以直接告诉模型“数据已加载成功可通过技能 x 的上下文检索接口获取明细”让模型按需再去查。5.3 技能执行的副作用与幂等性很多技能不是只读的比如发送邮件、写文件、修改数据库状态。这类有副作用的技能一旦被模型重复调用会产生严重问题。我在项目里真遇到过用户本来只想预览一下邮件内容模型却连发了两封一样的外发邮件。解决思路有两个。第一把“只读预览”和“实际执行”拆成两个技能风险敏感操作必须再次确认。第二给执行型技能做幂等控制。最简单的方式是技能参数里带上一个由Agent生成的 request_id技能执行前检查这个 request_id 是否已经处理过处理过就直接返回上次结果不再执行。这种方式特别适合有外部交互的场景比如对接支付下单、调用第三方通知接口。幂等控制做不做直接决定了你的 Agent 能不能上生产环境。5.4 权限边界还有一个经常被忽略的问题技能层不能默认拥有全部系统权限。你给 Agent 注册了一个“读取数据库”技能它的权限边界是什么是可读所有表还是只能读某几张表我在 agent-skills 里给每个技能增加了权限声明字段在执行入口统一做一次权限检查不支持该技能的权限级别直接拒绝。这不仅仅是为了安全还是为了约束模型的行为空间。如果技能能执行的操作范围太宽泛模型在复杂任务里就会做出一些无法预期的动作。把权限收到最小必要原则出事的概率会小很多。6. 后续扩展方向与我的个人心得agent-skills 这套框架走到目前这个版本我觉得基本够用了但它离一个完善的技能生态还有很长的路。我最想做的三个扩展方向技能版本管理与灰度发布、技能测试集与回归验证、技能市场化的描述交换协议。技能版本管理不只是给技能加个 version 字段而是要做技能的兼容性检测。某个技能改了输入结构依赖它的其他技能是否还能正常工作应该有一套自动检查机制。技能测试集则是为了应对一个很恼人的问题技能在单次调用里表现很好一放到多技能组合场景中就出问题。所以最好给每个技能都配一组“金标准用例”每次技能逻辑更新后自动跑一遍全部测试用例避免改了一个技能把另一个技能的正常路径踩坏。最后聊聊我自己最深的体会。做 skills 这套东西本质上不是在做技术框架而是在做“模型能力的边界管理”。你要替模型把该做决定的做决定把不用它操心的细节全部藏起来同时又要给它足够的信息去理解全局。这个平衡点非常微妙需要不断调。如果你正在做的 Agent 项目已经出现了工具多到选不对、流程长到跑不完、上下文大到撑不住的情况不用急着上多复杂的架构先把技能层补起来把模型的选择面收窄把执行细节封装好。你会发现原来那些让 Agent 看起来“不太聪明”的问题很多都不是模型的问题。写技能的时候多问自己一句这个描述模型读起来是真的懂了吗这个边界模型是真的清楚了吗把这两个问题想透了你的 Agent 就会比大多数项目都可靠得多。