
做Agent开发的人多半都被同一个问题折磨过功能逻辑都写好了模型却总是“不听使唤”。该调工具的时候不调调了又传不对参数一个简单的任务翻来覆去地出错。我去年从零搭过一套多智能体应用前前后后把几十个工具函数塞进prompt里结果维护成本直接爆炸后来把整套逻辑重构成agent-skills体系也就是把能力封装成独立、自描述、可复用的技能模块问题才算真正解决。这篇文章我就把 agent-skills 这套玩法的核心思路、设计方法和落地细节完整拆一遍。内容包括为什么工具调用会失效、技能体系如何重新组织Agent能力、SKILL.md该怎么写、调度器和技能注册表怎么实现、多技能协同怎么处理以及我踩过的坑和排查技巧。适合正在做Agent应用开发、被工具管理和模型调用稳定性困扰的朋友参考。我一直觉得写Agent最难的不是模型能力而是“工程化”。模型本身是聪明的但你得用它能理解的方式把你的能力边界讲清楚。agent-skills 就是干这件事的。1. Agent Skills到底是什么从工具堆砌到技能体系1.1 散装工具调用的四个典型痛点先聊聊最传统的实现方式。很多人在第一版Agent里都是把所有工具函数写在一个list里每个工具用一份JSON Schema描述参数然后通过函数调用机制一起丢给模型。我在项目早期就是这么做的当时维护了23个工具看起来井井有条。但工具一多问题就接踵而至。第一选择准确率断崖式下跌。模型要在几十个工具里挑一个最合适的本质上是做一次多分类。当工具描述含糊、边界重叠时模型会频繁选错工具。我遇到过最离谱的一次用户问“帮我查一下今天的天气”模型去调了“日程管理”工具因为两个工具的描述里都有“今天”这个词。第二重复逻辑没法复用。搜索、总结、格式化这些基础能力散落在不同工具里两个任务要用到同一段逻辑只能复制粘贴。改了一个地方忘了另一个Bug就悄悄埋下了。第三状态管理一团乱麻。很多工具依赖前置步骤。比如“先搜索、再总结、最后保存到记忆”用散装工具时这些依赖关系只能用全局变量加if-else硬编码代码丑得没法看。第四测试几乎没法做。散装工具没有统一结构你只能一个个手写测试用例回归测试更是奢望。1.2 技能体系的本质给工具套上工程化外壳Agent Skills解决的就是这些问题。它不是什么玄乎的框架本质就是给“模型可调用的一段能力”套上一个标准化的工程外壳。每个技能至少包含四样东西一个唯一的名字、一份自描述文档、一段可执行的代码逻辑、一组可验证的测试用例。我用一个表格对比一下两种方式的差异。维度散装工具列表技能体系组织单元函数技能目录代码描述测试模型理解依据JSON Schema片段结构化自描述文档SKILL.md逻辑复用复制粘贴按技能目录引用状态管理全局变量硬编码技能内聚上下文对象传递测试方式零散手写每个技能自带测试用例扩展成本改主流程新增目录注册即可这套体系最大的聪明之处是把“模型该理解什么”和“开发者该维护什么”这两件事彻底分开了。模型不需要看代码它看的是结构化的技能描述而开发者不用把每个工具的细节塞进prompt上下文只需要维护好自己的技能目录。两边各管各的配合反而更顺。1.3 一个Skill的标准目录长什么样我实际用下来的技能目录结构如下以Python为例skills/ ├── web_search/ │ ├── SKILL.md │ ├── search.py │ └── test_search.py ├── code_interpreter/ │ ├── SKILL.md │ ├── execute.py │ └── test_execute.py └── memory_manager/ ├── SKILL.md ├── memory.py └── test_memory.py每个技能目录下最核心的文件是SKILL.md因为这就是技能与模型之间的“接口文档”。模型在读你的技能目录时主要就是看这份文档。一份合格的SKILL.md大概长这样--- name: web_search description: 当用户需要查询实时信息、最新资讯、论文资料或事实验证时使用。 input: query: 用户的搜索关键词尽量保留用户原意 max_results: 返回结果条数默认5最大10 output: 搜索结果列表包含标题、链接和摘要 --- # web_search 技能说明 ## 触发条件 - 用户提问涉及实时新闻、时效性信息 - 用户要求查找资料、验证事实 - 模型已有知识无法覆盖的最新内容 ## 使用注意 - query 不要做模糊化改写保留用户核心意图 - max_results 不得超过10 ## 不适用场景 - 用户只需要基于已有知识的推理不需要外部信息 - 搜索历史对话记录我习惯把SKILL.md类比成一份招聘JD岗位名称是技能名岗位职责是触发条件任职要求是使用注意。JD写得越清楚来投简历的候选人模型就越靠谱。2. 模型与技能的协作逻辑描述怎么写才能被可靠调用2.1 模型视角模型看到的技能清单其实是一份菜单很多人以为模型调用技能时“看懂”了你的代码其实不是。模型看到的是一个高度抽象后的技能清单元信息通常就是每个技能的名字和一段描述。举个实际的prompt片段你有以下技能可供调用 1. web_search当用户需要查询实时信息、最新资讯、论文资料或事实验证时使用。输入为用户的搜索关键词。 2. execute_code当用户需要运行Python代码、计算数学问题或验证算法逻辑时使用。输入为一段完整的Python代码。 3. save_memory当用户明确要求记住某条偏好或事实且该信息需要长期保存时使用。 4. query_memory当需要回顾该用户的长期偏好、历史事实时使用。模型看到这个菜单后会根据用户请求做“意图匹配”选择一个技能并填充参数。所以它的本质是一个文本匹配加参数抽取的任务。你写的描述就是模型做决策的全部依据。这也解释了为什么很多人的工具调用老是失败你以为模型看懂了你的字段约束其实它只是基于描述做了一次语义匹配任何模糊、歧义、过度抽象的描述都会让它做出错误决策。2.2 技能描述的四条黄金准则关于SKILL.md怎么写我总结了四条经验每条都是真金白银换来的。第一条触发场景要写具体。不要写“此技能用于搜索”要写“当用户需要查询实时信息、最新资讯、论文资料或事实验证时使用”。让模型能把用户的表达和你的技能直接对上。描述里多放几个同义触发词比如“查一下”“搜一搜”“最新情况”都能提高命中率。第二条输入参数要给范围和示例。模型填充参数时如果没有参考只能瞎猜。我在web_search技能里写明“query: 用户的搜索关键词尽量保留用户原意长度不超过200字”模型的参数生成质量明显提升。第三条明确写出“不适用场景”。这是最容易被人忽略的一点。技能描述里的反面案例能有效减少误调用。我曾经有一个“save_memory”技能没写反例模型每轮对话都把上下文存一遍浪费大量token。加了“不适用于临时性猜测、不适用于模型可以在上下文直接访问的信息”之后误调用少了一半以上。第四条参数名要符合模型直觉。字段命名不要用缩写和内部术语。之前我把搜索关键词字段命名为kw模型经常不传或乱传。改成query之后准确率直接上了一个台阶。模型的直觉来自训练数据越贴近自然语言的字段名它越容易理解。2.3 兜底策略别全指望模型自觉就算描述写好了模型依然可能选错或者不选。我建议在调度层加两重兜底。第一重置信度兜底。在模型返回工具调用结果时别急着执行。可以做个简单判断如果返回的技能名不在注册表内或者参数校验失败直接回退到“重新提问让模型修正”。我曾遇到模型编造技能名的情况比如输出一个“search_internet”而注册表里只有“web_search”。这个时候如果直接执行必然报错。第二重用户澄清兜底。当模型返回tool_choice: auto且没有选择任何工具但用户请求明显需要外部能力时不要硬跑而是让Agent说一句“我需要确认一下你的意图”引导用户把需求说清楚。这两重兜底加起来系统的容错性会好很多。3. 从零搭一套技能体系注册、调度与执行3.1 第一步梳理你的能力清单别拍脑袋很多人设计技能是“我想做什么就写什么”我不推荐这么干。我建议从用户真实任务日志去反推。把你过去一周的用户对话拉出来逐条看用户到底让你做了什么归类成能力项。我当初梳理完发现所有需求归纳下来只有6类能力搜索、代码执行、记忆读写、文本总结、内容翻译、格式化输出。原来23个工具合并压缩成12个技能一下子清爽了很多。经验之谈不要为了设计而设计技能一定是给真实需求服务的。3.2 第二步实现技能基类与注册机制技能要统一管理先得定义抽象基类。我的基础实现长这样# base_skill.py from abc import ABC, abstractmethod class BaseSkill(ABC): property abstractmethod def name(self) - str: 技能唯一标识必须与SKILL.md中的name一致 property abstractmethod def description(self) - str: 技能描述供模型决策使用 abstractmethod def execute(self, context: dict, **kwargs) - dict: 执行技能并返回标准化结构{success, result, error}然后是注册表。注册表的核心功能是登记技能、查询技能、列出所有技能# skill_registry.py class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: BaseSkill): if skill.name in self._skills: raise ValueError(f技能重复注册: {skill.name}) self._skills[skill.name] skill def get(self, name: str) - BaseSkill: return self._skills.get(name) def list_catalog(self) - list[dict]: return [ {name: s.name, description: s.description} for s in self._skills.values() ]为什么一定要搞注册机制因为它把“新增技能”变成了一件低风险的事。新技能只需写个类实例化后调一次registry.register()主流程完全不用改。我后来扩展技能时基本不动调度代码只需要关心技能自身逻辑。3.3 第三步实现技能调度器核心逻辑调度器是整个技能体系的中枢。它负责送技能目录给模型、解析模型的调用意图、校验参数、执行技能、把结果回填给模型继续推理。# skill_agent.py def run_agent_with_skills(user_request: str, registry: SkillRegistry, llm, max_rounds: int 5): catalog registry.list_catalog() messages [ {role: system, content: build_system_prompt(catalog)}, {role: user, content: user_request} ] for round_idx in range(max_rounds): response llm.chat.completions.create( modelyour-model, messagesmessages, ) assistant_msg response.choices[0].message # 模型决定调用技能 if assistant_msg.tool_calls: for tool_call in assistant_msg.tool_calls: skill_name tool_call.function.name arguments json.loads(tool_call.function.arguments) skill registry.get(skill_name) if not skill: # 兜底技能不存在让模型重新选择 messages.append({ role: function, name: skill_name, content: json.dumps({error: f技能不存在: {skill_name}请从技能目录中选择。}) }) continue # 执行技能 result skill.execute(context, **arguments) messages.append({ role: function, name: skill_name, content: json.dumps(result, ensure_asciiFalse) }) # 把执行结果放回模型继续生成最终回复 messages.append({role: system, content: 请基于技能执行结果继续回答用户问题。}) continue # 模型不再调用工具生成最终回复 return assistant_msg.content return 已达到最大轮次任务未完成。这段代码是简化版但已经能展示完整的调度闭环。有个细节需要注意messages里的role: function消息是给模型“看”的执行结果。结果里一定要包含状态字段success还是fail因为模型要根据这个判断下一步该怎么做。3.4 技能代码的防御性设计别让模型拿到异常栈技能执行层的防御性设计很多人不重视结果就是模型用不了技能。我踩过一个很深坑技能内部报错后直接抛Python traceback模型看到一串英文堆栈完全不知道发生了什么于是开始胡编乱造说“搜索已经完成”。正确的做法是所有技能执行结果都统一封装成结构化的返回错误信息要面向模型编写。# web_search.py class WebSearchSkill(BaseSkill): name web_search description 当用户需要查询实时信息、最新资讯、论文资料或事实验证时使用。 def execute(self, context: dict, query: str, max_results: int 5) - dict: try: # 参数校验 if not query or len(query) 200: return {success: False, result: None, error: query参数不能为空且不得超过200字请重新提供查询词。} if max_results 1 or max_results 10: return {success: False, result: None, error: max_results必须在1到10之间请调整参数。} results self._do_search(query, max_results) # 空结果也要告诉模型而不是抛异常 if not results: return {success: True, result: [], error: None, tips: 搜索无结果可能需要更宽松的关键词} return {success: True, result: results[:max_results], error: None, tips: None} except Exception as e: # 吞掉底层异常返回模型能理解的错误描述 return {success: False, result: None, error: f搜索服务暂时不可用{str(e)}请稍后重试或换一个查询。}注意看返回结构固定是{success, result, error, tips}。error写给模型看不要写技术堆栈tips可以给模型额外的修正建议。模型读了这份结果知道哪里错了、该怎么改才能做出正确补救。4. 多技能协同与状态管理实战4.1 技能拆分的颗粒度别太粗也别太细技能拆分多少合适我的经验是单技能负责的“任务动作”要清晰边界要单一。太粗的典型例子把“搜索并生成总结”做成了一个技能。模型用这个技能时通通把搜索关键词传进去但却说不清楚总结的侧重点最后生成的内容用户根本不满意。因为一个技能里塞了“搜索”和“总结”两个动作而总结需要依赖搜索结果实时判断。太细的典型例子把“打开网页”“提取正文”“裁剪内容”“保存链接”分成四个独立技能。技能列表变臃肿模型每次光看目录就要花大量token选择也容易乱。我现在的原则是一个技能对应一个可独立验证的能力动作。“搜索”是一个动作“总结”是另一个动作。但“搜索并保存到记忆”就不行因为保存到记忆应该由另一个技能负责或者由调度层组合。4.2 技能间协作让模型编排还是内置工作流实际任务经常要多个技能配合。比如用户说“帮我查一下最新的AI Agent论文然后把核心观点整理出来”。这需要web_search先搜索然后调用文本总结技能可能还要结合记忆技能判断用户的历史偏好。我是怎么处理的分两种场景。简单串联场景让模型自己编排。模型只要会按顺序发技能调用请求就行调度器天然支持多轮工具调用。第一轮搜索第二轮总结每一轮都把结果回填给模型。这个方式灵活容忍度高适用于大多数日常任务。固定流程场景内置“复合技能”。如果某个流程特别稳定比如“每日早报生成”固定是搜索三条新闻加翻译加格式化我会封装一个daily_report复合技能内部编排多个基础技能。这个时候模型只需要调用一个技能省心也省token。# composite_skill.py class DailyReportSkill(BaseSkill): name daily_report description 生成每日技术早报包含搜索、翻译、格式化三个步骤。 def execute(self, context: dict, topics: list[str]) - dict: results [] for topic in topics: search_out self._search.execute(context, querytopic) if not search_out[success]: continue translate_out self._translate.execute( context, textsearch_out[result][0][summary]) results.append(translate_out[result]) return {success: True, result: format_report(results), error: None, tips: None}这里有个权衡复合技能灵活度低但稳定性高模型自由编排灵活度高但偶尔会出错。我建议对“保底能力”用复合技能对“探索型任务”用自由编排。4.3 记忆类技能的特殊性读写分离是关键记忆是Agent技能体系里最特殊的一类因为它的处理时机很微妙。我在做记忆技能时踩过不少坑最后摸索出一个稳定的模式记忆读写要分开且记忆读取不是被动的“工具调用”而是每轮对话开始前的“前置动作”。具体做法是在用户每轮提问前Agent首先从记忆库里查询相关资料作为上下文的一部分注入到消息里。而不是等模型觉得“需要回忆历史”时才去调query_memory。因为模型经常意识不到自己需要记忆。def build_messages_with_memory(user_id, user_request, registry): memory_skill registry.get(query_memory) mem_result memory_skill.execute( {user_id: user_id}, queryuser_request[:100]) memory_context mem_result[result] system_prompt f用户长期偏好如下{memory_context}\n请结合以上信息回答用户问题。 return [{role: system, content: system_prompt}, {role: user, content: user_request}]至于写入记忆则应该在用户明确表达偏好、或者Agent完成一次高价值交互后触发。我给save_memory技能设了一条铁律用户说“记住”才写入Agent自己不能主动把每轮对话都塞进长期记忆。要不然记忆库很快会变成垃圾场查询质量直线下降。4.4 上下文对象技能间传递状态的桥梁技能执行时要共享很多状态。比如当前用户ID、会话ID、页面内容、时间等。我设计了一个context字典沿两条规则传递全局上下文所有技能可读存用户ID、会话ID等基础信息技能本地上下文单个技能内部使用执行完释放。这种设计让技能保持“无状态”特性技能之间不互相依赖全局变量只依赖传入的context。排查问题的时候特别舒服不会出现“哪个技能改了全局状态导致另一个技能行为怪异”的灵异事件。5. 常见问题与排查技巧实录这一节把我实战中遇到的问题整理成速查表再挑几个典型的展开说说。现象常见原因排查思路解决办法技能存在但模型不调用描述过于抽象/与用户表达匹配度低把技能目录单独发给模型问它“这个请求你会调哪个技能”在描述中增加触发词和同义表达模型调用了错误的技能多个技能边界重叠描述不清查看模型实际看到的技能描述明确“不适用场景”字段模型传参错误参数名不规范/缺少示例打印模型返回的arguments参数名贴近自然语言加上范围和默认值技能报错后模型编造结果模型读不懂技术错误信息检查技能返回的error字段错误信息面向模型编写增加tips修正建议技能调用进入死循环执行失败后模型反复重试相同调用统计每轮tool_calls增加max_rounds上限和重试规则5.1 模型就是不调用技能怎么办这是被问得最多的问题。技能明明写得很好模型就是“徒手回答”。我的排查套路很固定第一步把系统提示词里的技能目录单独拿出来发到你用的模型对话框里配上一条真实用户请求直接问“你会调用哪个技能吗”如果模型说不调说明描述本身有问题需要重写。第二步检查是不是上下文太长了。上下文越长模型对工具调用的“注意力”越容易被稀释。如果对话历史有一大堆无关内容模型会倾向直接回答而非调用工具。这时要把历史消息压缩成摘要。第三步降低技能选择成本。很多框架的function calling机制是默认auto模型会权衡是否值得调用。你可以把调用阈值调低或者在系统提示里加上一句“只要用户请求涉及实时信息、计算、历史偏好就必须调用对应技能。”5.2 技能返回的错误模型看不懂才是真问题我遇到过最诡异的一个Bug搜索技能因为网络超时报错模型看了错误信息后竟然告诉用户“搜索已完成结果是某某”。用户当然不信追问了几个细节模型就开始编。后来我意识到问题的根源不是模型说谎而是它没读懂我的报错。Python的traceback对模型来说只是一堆无意义符号它只能基于自己的“语义猜测”继续作答。从那以后我要求所有技能的错误返回必须遵循一个模板{ success: false, error: 搜索服务超时请稍后重试或换个说法描述搜索需求。 }错误文案要用模型能理解的口吻描述最好再给一句修正建议。这个改动上线后模型在技能失败后的胡言乱语基本绝迹。5.3 多个技能部分重叠模型经常选错怎么办当你的技能里同时有“web_search”和“query_memory”用户说“帮我查一下上周和A总聊了什么”模型可能去调web_search而不是query_memory。因为“查一下”这个词让模型觉得需要搜索。这种场景我的解决办法是在描述里明确技能之间的边界。query_memory的描述改成“当用户需要回顾本系统内已保存的历史对话、总结、偏好时使用。是系统内部记忆检索不是互联网搜索。如果用户需要外部实时信息请用web_search。”看起来只是加了几句“不是”和“如果”但模型做决策时的匹配准确率提升非常明显。这本质上是在帮模型划分类别边界。5.4 技能调用卡死和token爆炸的防御多技能编排时最容易出现的问题就是轮回转模型不断调用技能但始终得不到最终结论。除了调度层加max_rounds之外我还会加一个“无进展检测”如果连续两轮模型都在调同一个技能、传相似的参数、拿到相似的结果就直接打断让模型改用其他方式回答或者向用户澄清。还有一个常见的token浪费点技能返回结果太长。比如web_search返回了十个网页的全文摘要模型读这些内容要花大量token。建议在技能结果里做预裁剪只保留关键字段并把超长文本用“标题首段链接”的结构压缩。6. 给Agent技能体系长期维护的几条建议6.1 技能新增大战别急着加先看调用日志我一开始的冲动是遇到一个新需求就给系统加一个新技能。结果不到两周技能数从12涨到25但调用质量反而下降了。后来我给自己立了个规矩一个新能力需求出现三次以上再考虑加技能。而且加之前先翻历史对话确认这几条需求能归纳成同一个技能逻辑。技能越来越多不是好事技能的“平均选择准确率”会随着数量增加而下降。保持精简是技能体系长期稳定的关键。6.2 SKILL.md也要版本化技能代码可以入库版本管理但SKILL.md经常被人忽略。其实描述文档变了模型的调用行为就可能完全改变。我现在把SKILL.md和技能代码放在同一个仓库任何改动都走pull request。如果线上调用准确率有波动第一步就是回退最近的SKILL.md改动。6.3 用真实对话做技能调用回归测试每个技能目录下的test文件我不只是测代码逻辑对不对还测“模型视角下的调用正确率”。具体做法是准备一批标准用户问答对跑一遍完整Agent流程统计技能调用命中率作为技能体系的基准指标。每次改动后跑一遍低于基准就回滚。这个方法听着笨却是最让我放心的护城河。最后再分享一点个人经验构建技能体系是一个持续迭代的过程不要追求一步到位。先按一个典型场景搭出最小可用的技能闭环跑通之后再慢慢扩充。技能体系的收益是长线的前期的设计和维护投入会在后面每一次新增能力时加倍还回来。