1. agent-skills 到底是什么:先别急着把它当成“插件”
最近“agent-skills”这个词在 AI 圈子里窜得很快。很多人第一次听说时,第一反应是“这不就是给 Agent 加插件吗?”——对了一半,但如果只停留在这一层,后面踩坑会非常痛苦。我自己的理解是:agent-skills 本质上是把大模型智能体的“能力”从“一次性提示词”变成“可复用的技能单元”,每个技能单元具备独立的描述、参数约定和触发方式,让 Agent 在复杂任务中按需调用,而不是每次把全部逻辑塞进上下文里硬推理。
举一个生活化的类比。以前的 Agent 像是一个记忆力一般、但什么都能聊两句的实习生,你给他一个任务,他只能靠临场发挥;而 agent-skills 相当于给这个实习生配备了一整套标准作业程序手册,每个程序有明确的适用场景、输入格式和操作流程。遇到对应任务时,实习生不是从零开始想,而是直接翻手册、按流程执行。这个转变看似简单,实际是在改变 Agent 的行为模式:从“什么都靠模型硬猜”变成“把确定性逻辑固化下来,让模型只做决策和编排”。
这个方向适合谁?适合正在做 LLM 应用落地、搞过几轮 prompt 工程但发现效果不稳定的人,也适合那些想给团队沉淀一套可复用 Agent 能力的人。如果你只是拿 API 写个聊天机器人,agent-skills 可能有点杀鸡用牛刀;但一旦你的 Agent 需要处理多步骤任务、需要调用外部系统、需要团队成员协作维护一套行为库,那 skills 的收益会非常明显。
很多人会问,这和 function calling、工具调用有什么区别?区别在于粒度。function calling 通常是“模型决定调用哪个函数”,本质上还是点对点的接口对接;agent-skills 更强调“技能包”的概念——一个技能往往不是一个单次调用,而是包含前置条件、调用流程、后处理逻辑、甚至多条工具链的一整套方案。比如“联网搜索”可以是一个 function,“调研某个行业并生成报告”就是一个 skill,它内部会拆分成搜索、抓取、总结、排版等多个步骤。这也是为什么 skills 在复杂任务里的价值远高于单纯的工具列表。
2. 为什么需要 skills:从 prompt 堆砌到系统性能力沉淀
我在早期做 Agent 项目时,习惯把所有的任务指令、工具说明、输出格式约束全部塞进 system prompt。结果非常典型:小任务还好,任务一复杂,模型就开始“选择性失忆”——要么忽略部分工具说明,要么输出格式不统一,要么在关键步骤上突然发挥过头。后来我统计了一下,程序里的 system prompt 从最初的 500 字膨胀到 2000 字,每次改需求都要小心翼翼,生怕动了一句话导致其他功能崩掉。这种模式的本质问题在于:能力和调用时机耦合得太紧。
agent-skills 的核心价值之一,就是把“能力描述”和“调用策略”解耦。每个技能独立描述自己是什么、什么时候用、怎么用、需要什么参数,模型只需要根据当前任务去匹配技能描述,而不是在上下文中同时维护所有技能的细节。这有点像代码重构里的“模块化”思想:把散落各处的逻辑封装成函数,调用方只关心函数的签名,不关心函数内部实现。
另一个重要价值是“可演进”。如果你的 Agent 只有一坨 prompt,那么每一次能力升级都等于重写整个提示词,而且很容易引入回归问题。但如果每个技能独立存在,你可以单独增强某个技能、测试某个技能、甚至让某个技能并行迭代多个版本,然后再决定是否上线。对于团队协作尤其重要:负责任务 A 的人只需要维护技能 A 的代码和描述,负责任务 B 的人不会因为 A 的改动而受影响。
从效果层面看,技能库还能减少模型的无效推理。在大模型执行任务时,每多一个候选工具,模型的计算负担和决策错误率都会上升。工具列表太长时,模型可能选错工具,或者在不同工具之间犹豫不决。把工具按场景打包成技能之后,顶层只需要暴露尽量少的技能入口,模型的选择空间变小,准确率自然提升。我实测下来,同样的任务,把 8 个散装工具重组成 3 个技能之后,工具调用的准确率从 78% 涨到了 92% 左右,这个提升比调什么温度参数都明显。
还有一个容易被忽略的好处:技能可以附带“经验教训”。你可以在技能内部写入各种边界条件处理,比如“如果搜索结果为空,则返回默认模板”“如果请求超时,则重试一次”等等。这些经验不需要模型去临场推理,而是作为一种确定性策略固化在技能里。这样即便下游换了更弱的模型,只要技能描述写得足够好,整体效果也不会崩得太厉害。这也是为什么很多团队在模型降本、换小模型时,都会优先把关键路径上的逻辑固化成技能。
3. 设计一个技能库的核心思路:从用户意图倒推技能边界
3.1 先划分技能边界,再考虑技术实现
很多新手设计 skills 时容易陷入“按工具划分”的误区,比如一个 skill 负责数据库查询、一个 skill 负责发邮件、一个 skill 负责调用外部 API。这种划分其实还是 function calling 的思路,技能只是函数的马甲。真正的技能划分应该按“用户意图”和“任务场景”来,一个技能要能完整解决一类问题,而不是只负责某个动作。
我在自己的项目里常用的方法是:把高频任务场景列出来,然后问自己“如果一个人要做这件事,他会顺序做哪些步骤”。比如“安排一场会议”这个任务,需要的动作包括检查日程、查找空闲时间、创建邀请、通知参会人。如果每个动作都拆成单独技能,模型需要依次做四次选择,中间还可能遗漏;但如果打包成一个 “schedule_meeting” 技能,模型只需要一次调用,内部依次完成四个步骤。这种粒度对模型最友好,因为选择成本最低、失败概率也最低。
当然,技能粒度不能过度膨胀。如果一个技能内部包含超过十几个分支逻辑,描述会非常难写,模型也难以判断该在什么时候调用。我建议保持一个原则:技能的边界应该对得上用户的一句话需求。用户说“帮我安排一个会议”,这就是一个技能;用户说“帮我安排会议并收集参会者反馈”,这可以是一个技能,也可以是两个技能,取决于反馈收集是否是独立高频场景。
3.2 技能描述的质量直接决定调用准确率
技能描述是模型做决策的依据,写得烂,再好的实现也白搭。我给技能描述定的标准是:说明“这个技能做什么”“在什么场景使用”“绝对不要用于什么场景”“输入参数是什么”“输出形态是什么”。其中“不要用于什么场景”很多人会忽略,但恰恰特别重要。因为模型的匹配逻辑是语义相似,它可能把“发送邮件”误匹配成“发送短信”技能,这时候明确禁止描述,可以大幅降低误配。
参数定义上,不要只写“参数名和类型”,还要写清楚每个参数的含义、取值范围、默认值和使用示例。模型解析参数时,如果把数值传成字符串,或者把日期格式传错,技能执行必然失败。我见过一个真实案例:技能定义里写了一个“limit”参数,类型是 integer,描述是“返回结果数量”,但模型在实际调用时传了 “limit=10条”,导致解析直接报错。这不能全怪模型,描述里如果能补上“只接受纯数字,比如 10,不要带单位”,问题概率就会小很多。
再补充一点关于“技能选择策略”的设计。现在主流做法是让模型在 function calling 框架里选择技能,也就是把每个技能描述塞进 tools 数组。但当技能数量超过二三十个时,很多模型的检索能力会下降。这种情况可以把技能分成两层:上层是一个“技能导航”能力,先根据用户请求归类到某个领域(比如“资讯类”“数据处理类”“系统操作类”),下层再展开该领域的候选技能。这种分层虽然增加一跳调用,但换来的是更高的选择精度,尤其在技能库规模持续扩张时,几乎是必须的。
3.3 技能内部的确定性逻辑与模型逻辑要分开
一个技能内部通常既有确定性的代码,也有需要模型推理的环节。比如“生成周报”这个技能,收集数据是确定性的,写总结段落是需要模型的。很多人在设计时容易把两者混在一起,导致技能执行过程中模型突然开始自由发挥,输出不稳定。我建议在技能内部明确标记“确定性步骤”和“模型步骤”:确定性步骤用代码强制流程,模型步骤只允许在特定结点调用模型,并且给模型输入固定的上下文模板。
以“生成周报”为例,技能的完整逻辑可以是:调用数据接口拉取本周提交记录(确定性)→ 将记录按类型聚合(确定性)→ 把聚合结果和固定模板拼成 prompt(确定性)→ 调用模型生成润色后周报(模型)→ 把结果写入指定文档(确定性)。这样一来,模型发挥的空间被压缩到“润色”这一个环节,整体输出质量自然稳定。
我自己的经验是:能不用模型的地方尽量不用。不仅因为模型调用有成本、有延迟,更因为模型输出的不可控是 Agent 系统最大的风险来源。技能化的过程,某种意义上就是“把模型不需要思考的地方全部用代码焊死”的过程。焊死的地方越多,系统越稳,排查问题也越容易——出了问题先看代码逻辑,而不是去猜模型哪句话理解错了。
4. 实操:从零构建一套最小可用的 agent-skills
4.1 定义技能基类和注册器
直接上手写代码。我假设你已经在用 Python 做 Agent 项目,并且接入了某种 LLM 的 function calling 能力。我们先设计一个最朴素的技能基类:
from typing import Any, Dict, Optional import json class Skill: name: str = "" description: str = "" parameters: Dict[str, Any] = {} required_params: list = [] def __init__(self): self.name = "" self.description = "" self.parameters = {} self.required_params = [] def execute(self, **kwargs) -> Dict[str, Any]: raise NotImplementedError def to_tool_schema(self) -> Dict[str, Any]: return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": { "type": "object", "properties": self.parameters, "required": self.required_params, } } }实际项目中你不需要写这么薄的一层,很多框架已经有现成的基类,但理解这个结构很重要。to_tool_schema的作用是把技能变成模型能看懂的工具描述,这就是 model 选择技能的接口。所以我总是强调,description和parameters的编写质量,会和代码质量一样直接影响最终效果。
注册器更简单,一个全局字典就够了:
SKILL_REGISTRY = {} def register_skill(skill_instance: Skill): SKILL_REGISTRY[skill_instance.name] = skill_instance def get_all_tool_schemas(): return [skill.to_tool_schema() for skill in SKILL_REGISTRY.values()]按需加载很重要。如果你的技能库特别大,不要启动时全部 import,也不要全部塞进模型上下文。可以做一个懒加载机制:先给模型一组“技能摘要”,模型决定用哪个技能后再把该技能完整描述传给模型做参数提取。这就是我上文提到的“分层策略”的简单实现。
4.2 实现两个不同粒度的技能
第一个技能是“查询知识库”,它的特点是单次调用,直接对应用户的搜索需求。描述里我写了明确的排除场景。
class KnowledgeSearchSkill(Skill): def __init__(self): super().__init__() self.name = "knowledge_search" self.description = ( "在内部知识库中检索相关信息,用于回答用户关于产品文档、技术规范、团队成员信息等问题。" "仅在需要事实型信息时使用;不要用于询问用户个人偏好或生成创意内容。" ) self.parameters = { "query": { "type": "string", "description": "搜索关键词,建议提取用户问题中的核心实体,例如'退款政策'。", }, "top_k": { "type": "integer", "description": "返回结果数量,只接受 1 到 10 的整数,默认 3。", } } self.required_params = ["query"] def execute(self, **kwargs): query = kwargs.get("query") top_k = int(kwargs.get("top_k", 3)) # 这里替换成你的向量检索或关键词检索 results = search_in_kb(query, top_k) return {"status": "success", "results": results}第二个技能是“周报生成”,它的内部会串起多个步骤,属于“复合技能”。执行时不仅调用外部数据,还会调用一次模型来润色。这里演示一下技能内部的流程控制。
class WeeklyReportSkill(Skill): def __init__(self): super().__init__() self.name = "generate_weekly_report" self.description = ( "根据用户提供的任务信息或自动获取的工作记录,生成一份结构化的周报。" "适合在用户需要总结本周工作、周报输出时使用。" "不要用于生成简历、项目总结等其他文档。" ) self.parameters = { "project_name": {"type": "string", "description": "项目名称,例如'智能客服平台'。"}, "week": {"type": "string", "description": "周数,格式是 YYYY-MM-DD 到 YYYY-MM-DD。"}, } self.required_params = ["project_name", "week"] def execute(self, **kwargs): project_name = kwargs["project_name"] week = kwargs["week"] # 步骤 1:拉取数据(确定性) records = fetch_work_records(project_name, week) if not records: return {"status": "empty", "suggestion": "该周没有工作记录,请确认项目名称和日期范围。"} # 步骤 2:聚合并生成中间结构(确定性) grouped = aggregate_records(records) # 步骤 3:调用模型润色(模型步骤) drafted = self._draft_with_llm(project_name, week, grouped) return {"status": "success", "report": drafted} def _draft_with_llm(self, project_name, week, grouped): prompt = build_report_prompt(project_name, week, grouped) return chat_completion(prompt)这里想特别说下fetch_work_records返回空数据的情况。很多初学者会在这里直接让模型续写“本周没有工作记录,所以我来编一个”——这是灾难。对空数据的处理必须是确定性的,直接返回空状态,并且告知模型当前的处境,让模型重新向用户确认。这种边界情况的处理逻辑,才是技能与普通函数的本质区别:技能不仅要做正常流程,还要定义异常流程的输出。
4.3 主循环:让模型决定调用哪个技能
接入 LLM 的循环大概是这样的:
def agent_run(user_message): messages = [{"role": "user", "content": user_message}] # 第一轮,只给技能 schema response = chat_with_tools(messages, tools=get_all_tool_schemas()) assistant_message = response.message if assistant_message.tool_calls: for tool_call in assistant_message.tool_calls: skill_name = tool_call.function.name arguments = json.loads(tool_call.function.arguments) skill = SKILL_REGISTRY.get(skill_name) if not skill: continue result = skill.execute(**arguments) # 把执行结果作为 tool 消息发回给模型 messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result) }) # 再让模型基于结果生成最终回复 final_response = chat_with_tools(messages, tools=get_all_tool_schemas()) return final_response.message.content else: return assistant_message.content实际生产环境还需要处理多轮、重试、超时、校验等问题,但上面这个骨架已经能跑通最小闭环。一个值得执行的建议是:在把执行结果塞回模型之前,先做一次结果“净化”。比如execute返回的字典里可能包含非常长的原文、内部错误堆栈,这些内容不应该原样发给模型。你需要在技能内部定义“输出白名单”,只把用户需要的信息、以及必要的状态码返回给模型。这样可以大幅减少 token 消耗,也避免模型被无关信息干扰。
5. 常见问题与排查技巧实录
5.1 模型就是不会调用技能
这是最常见的问题,通常不是模型蠢,而是技能描述写得不够“显眼”。排查时先问自己:用户这句话,和技能描述之间是否存在明显的语义关联?比如你的技能叫data_query,描述是“根据用户指定的条件查询数据”,但用户说的是“帮我把上个月的报表导出来”,模型很可能把“导出”理解成别的能力。解决方式是给技能补充别名和触发场景示例:“也适用于导出报表、拉取数据、获取统计结果等说法。”别指望模型做推理,直接把可能的说法列举出来。
如果描述已经够具体还是不调用,检查一下技能列表是否过长。我建议单轮对话暴露的 skills 数量保持在 10 个以内,超过这个数量就做分层路由。你还可以在 system prompt 里加一条“如果用户请求与某个技能高度相关,必须调用该技能;如果拿不准,宁可不调用也不要猜测。”这类指令能明显提升调用的倾向性。
5.2 技能参数解析错误
参数解析错误集中表现在模型把数字传成字符串、日期格式错误、参数名被篡改。我的处理习惯是:在execute里做“宽容解析”。比如top_k即便来了字符串,也强制int()并加 try-except;日期格式如果错了,尝试用datetime解析常见格式;枚举值只接受固定几个选项,可以做一个映射表,把模型可能传的变体统一归一化。
如果发现模型反复传错某个参数,不要指望通过提示词一次性修复,更稳妥的办法是修改参数描述,加入明确的禁忌:“不要把 '三' 这种汉字作为数字,只接受阿拉伯数字”。如果是日期,描述里附一个示例:“2025-02-10”。模型对示例的敏感度远高于规则描述。
5.3 技能执行成功但最终回答错误
经常遇到技能返回了正确结果,但模型在总结时胡编乱造,特别是结果中如果包含数字、日期、名称,模型可能会“自己发挥”。这时需要在系统提示里强制要求:“只能基于 tool 消息中返回的内容进行回答,禁止添加任何 tool 返回中不存在的细节。”同时让技能输出结构尽量简洁,并且考虑在execute中把关键事实用固定模板包装成fact_statement,让模型直接引用。
另外要留意技能内部调用模型时的 prompt 是否会受主对话污染。很多框架会把主对话上下文一起传给技能内部的模型步骤,导致内部模型看到用户和 Agent 的闲聊内容,被带偏。我通常会在技能内部模型步骤前清空上下文,只传入该技能需要的结构化数据。
5.4 技能间的“职责重叠”
技能库规模变大后,两个技能可能在语义上高度重叠,模型就容易选错。比如既有search_docs又有search_web,描述都很模糊。我的做法是建立技能冲突检查机制:每次新增技能时,把新技能的描述与现有技能描述做一次向量相似度计算,如果相似度超过阈值,就重新修改边界,确保每个技能有独占的触发场景。这个机制看起来多余,但在技能数量超过 15 个之后,几乎是刚需。
5.5 调试技能时的日志规范
最后说一个容易被忽视的点:技能必须有完整的日志。每次调用要记录:技能名称、入参、出参、耗时、异常堆栈、调用的模型请求与响应。我见过很多 Agent 项目,调试时两眼一抹黑,完全不知道技能有没有被调用、参数是什么、结果如何。我现在的习惯是给每个execute加一个装饰器,自动记录日志,并把日志格式统一为 JSON,方便检索。排查问题时,先看日志里技能调用是否正常,再往上下游查,效率至少翻一倍。
6. 我在实际维护 skills 时的一些心得体会
技能库不是写一次就完了,它需要像业务系统一样长期迭代。我自己的维护节奏是:每两周检查一次所有技能的调用成功率,把没有被调用过的技能找出来,逐个判断是不是描述有问题、场景被别的技能覆盖了或者设计本身就不合理。定期“瘦身”非常重要,冗余技能不仅拖累模型选择准确率,还会让新成员接手时一头雾水。
还有一个很实际地经验:技能描述中的示例远比抽象规则管用。我之前给一个“翻译”技能写描述,写了“将文本从一种语言翻译成另一种语言,保持语义准确”,模型在个别场景下还是会乱用。后来我改成“常用于用户发送外文内容时提供中文翻译,或者将中文内容翻译成英文、日文等;如果用户只是在聊天中使用少量外语词汇,不需要调用。”误用率下降了一大截。技能描述的每条规则,最好都能对应到用户会说的自然语言样例。
实际上很多成熟的 Agent 开发框架已经开始内嵌 skills 概念,但底层思路都是一致的:让模型少做决策、多做编排,把确定性的东西从模型手里拿走,把非确定性的部分小心地包好。如果你现在正陷入 prompt 越写越长、效果越来越不稳定的困境,不妨从技能化的角度重构自己的 Agent。刚开始可能会觉得麻烦,但一旦技能库成型,后续所有迭代都会轻松很多。这种模式也可以继续扩展:比如给技能加权限控制、做技能间的依赖编排、甚至让技能本身具备学习和升级能力,这些方向都很有意思,值得慢慢摸索。