☰
Agent技能封装实战:手写agent-skills框架,让大模型从会聊天到会干活
2026/10/7 2:10:11 网站建设 项目流程

这段时间一直在折腾Agent开发,越做越觉得一个残酷的事实摆在眼前:大模型本身的能力上限,其实没有大多数人想象的那么高。真正决定一个Agent是“玩具”还是“生产力工具”的,恰恰是挂在它身上那些不起眼的技能(Skills)。市面上讨论Agent的文章,十篇有八篇在讲提示词工程、讲RAG、讲模型选型,但很少有人系统地讲清楚“技能体系”这件事。我自己在多个项目里反复重构、踩坑之后,把一套叫agent-skills的框架沉淀了下来,这篇文章就把这中间的思考、设计和落地细节完整拆出来,希望能给正在做Agent开发的同行一些参考。

不管你是刚接触Agent开发不久,还是已经被LangChain、AutoGPT这类框架折磨过几轮,这篇文章都值得看完。我会先说清楚agent-skills到底解决什么问题,再讲核心设计思路,然后直接手把手带你把一套可用的技能框架写出来,最后把我在实际运行中遇到的坑和排查方法一并掏出来。保证不是那种泛泛而谈的概念稿,你照着做,今晚就能跑起来。

1. 项目整体拆解:agent-skills到底解决什么问题

1.1 从“会聊天”到“会干活”,Agent缺的是技能封装

先问一个问题:你手上的大模型接口,直接调用它,它能帮你干什么?写一段文案、改一段代码、总结一篇文档,这些都没问题。但你要是让它“把公司这个月的销售数据整理成一份带图表的PDF报告,然后发到指定的企业微信群”,它当场就懵了——不是模型不够聪明,而是它缺少一套可调用的、经过验证的“动作集合”。

这里说的“动作”,不只是函数调用(Function Calling)。函数调用只是模型输出一个JSON,告诉你要调哪个函数、传什么参数。但一个真正的“技能”至少要包含三层:

  • 触发条件:什么场景下该用这个技能,模型怎么知道该选它。
  • 执行逻辑:技能内部具体干什么,是调API、跑SQL、操作浏览器,还是组合多个工具。
  • 校验与兜底:执行结果怎么判断成没成功,失败了怎么办,要不要重试。

很多团队做Agent,第一步就是用LangChain把所有工具一股脑塞给模型,然后发现模型频繁选错工具、传错参数、甚至在不需要工具时强行调用工具。这不是模型的问题,是你根本没有做“技能封装”。agent-skills这个项目的核心,就是把零散的工具调用,升级成结构化的、可被模型精准理解和可靠执行的技能单元。

1.2 技能库的定位:不是提示词集合,也不是插件市场

有朋友第一次看到“agent-skills”这个名字,以为是又一个提示词模板仓库,或者像Chrome插件商店那样的东西。这两种理解都偏了。

提示词集合解决的是“怎么说”的问题,它只能影响模型的输出风格和格式。技能库解决的是“怎么做”的问题,它是一个可执行的、带完整逻辑和方法论的能力单元。举个例子:一个“网页内容抓取”技能,不是给模型一段“请你抓取网页”的提示词,而是要求有实际的抓取代码、有请求重试机制、有内容清洗逻辑、有解析失败时的降级方案。模型负责决定“什么时候用”和“怎么组合用”,技能本身负责“怎么精准完成”。

插件市场的逻辑是给用户提供一堆独立功能,由用户自己挑选、自己安装。技能库的逻辑是给Agent提供一套可编排的能力积木,由模型根据任务目标,自主判断该调哪个、不该调哪个,甚至组合多个技能完成复杂任务。这就引出一个关键问题:技能的“可被模型理解”和“可被模型编排”是两件事,很多项目连第一件都没做好,就急着去做第二件。

1.3 与LangChain工具、Claude Skills、OpenAI Actions的差异对比

为了避免大家混淆,我直接拿现在市面上常见的几个方案做个横向对比。LangChain的工具概念是最宽松的,你几乎可以把任何函数注册成工具,但它对工具的描述、参数校验和执行约束没有强制规范,导致模型误用概率偏高。Claude的Skills走的是自然语言技能描述的路子,强调用文档化的方式让模型理解技能,但在细致的执行编排上偏弱。OpenAI的Actions强依赖OpenAPI规范,适合HTTP接口,但灵活性一般,而且生态绑定比较紧。

agent-skills的设计思路是:它不依赖任何特定模型或框架,技能描述用结构化的YAML/JSON承载,执行逻辑用标准Python异步函数实现,中间通过一个轻量的注册中心管理。你在里面写的技能,既可以跑在Claude上,也可以跑在GPT上,甚至能接到本地开源模型上。这个“模型无关”的定位,是我最看重的——你会发现,模型更新换代太快了,但技能库一旦沉淀下来,是可以跨模型复用的资产。

2. 核心设计思路:技能系统怎么搭才不翻车

2.1 技能描述的结构化是命门

很多人在设计技能时,第一个犯的错就是把技能描述写得像产品说明书,又长又模糊。这里必须明确一个原则:技能描述不是给人看的,是给模型看的。模型的工具选择逻辑,本质上是在当前对话上下文中,把任务目标和每个技能的描述做语义匹配。如果你的描述不够结构化,模型就可能把“发送邮件”和“发送消息”搞混,或在不需要文件操作时选中一个文件操作技能。

我在agent-skills里,把每个技能的描述拆成四个强制字段:

  • name:技能的唯一标识,短小清晰,比如“fetch_webpage”。
  • description:两到三句话讲清楚技能用途、适用场景、不适用场景。
  • parameters:JSON Schema格式的参数定义,精确到每个字段的类型、是否必填、取值范围、默认值。
  • tags:一组标签,用于快速检索和过滤,比如“网络”“数据提取”“实时信息”。

这里有个细节值得强调:description里一定要写“什么时候不该用”。比如网页抓取技能,就要明确“如果目标网站需要登录认证,本技能不适用”。这个负面约束看起来没必要,实测能显著减少模型乱点鸳鸯谱的概率。模型在不确定时,倾向于“试一试”,一个清晰的不适用边界,比一堆正向描述管用得多。

2.2 技能调用的可观测性设计

技能系统做得越深入,你会越发现可观测性有多重要。模型调了一个技能,结果对不对、耗时多久、中间经历了什么,这些信息如果完全黑盒,后面排查问题会痛不欲生。我见过不止一个团队,Agent在生产环境里出了问题,第一反应是“是不是提示词该改改了”,结果查了半天发现是技能内部的第三方服务响应格式变了。

agent-skills在每个技能执行时,强制注入一个Trace上下文,自动记录以下几类数据:

  • 入参快照:模型传入了什么参数,完整保留原始JSON。
  • 执行日志:技能内部关键步骤的日志,按结构化JSON输出,带时间戳。
  • 工具调用链:技能内部如果又调了其他技能或外部API,形成调用链路,方便定位耗时瓶颈。
  • 结果摘要:成功时的关键结果摘要,以及失败时的错误类型、错误消息。

这些Trace数据统一打到本地JSONL文件,或者接外部的可观测性系统。我自己是在本地用SQLite存一份,再通过一个简单的Web界面查看。前期你不一定需要上Langfuse这类的重型方案,但至少要把Trace结构设计好,后面想接入也就是改个输出端的事情,不用重构核心逻辑。

2.3 为什么要把“失败重试”当成一等公民

写Agent的人都有这种体验:模型把参数配错了、外部接口网络超时了、上游服务数据格式变了,技能第一次执行经常失败。很多早期项目在技能里完全没做重试,一失败就直接报错,让模型自己“看着办”。结果模型只能反复调用同一个技能无限重试,陷入死循环,既浪费Token又把上下文搞得一团糟。

我在agent-skills里做了一个强制约定:每个技能都必须声明自己的重试策略。这不是说每个技能都要盲目重试,而是把“是否重试”“怎么重试”显式地写出来,而不是靠运气。常用策略有这么几类:

  • 快速失败:参数明显错误、客户端问题,直接抛错,不重试。
  • 指数退避重试:网络超时、服务端5xx错误,按“1秒、2秒、4秒、8秒”退避,最多4次。
  • 降级策略:主路径失败后,自动切换到备用方案,比如A搜索源失败就切B搜索源。
  • 重试上限后上报:超过重试次数,把原始错误、重试日志、上下文完整返回给Agent,让它决定下一步。

有一个经验数据:做了这种显式重试策略之后,我们项目里Agent任务的单次成功率大概提升了30%到40%。代价仅仅是多了一点代码量,非常划算。

3. 落地实操:手写一套自己的agent-skills框架

3.1 基础环境与目录设计

接下来进入实战环节。我不建议你直接照抄我的代码,而是跟着思路把框架搭起来,然后填自己的技能。整个项目我用纯Python实现,依赖只用了Pydantic做参数校验,AsyncIO做并发控制,其他都是标准库。

目录结构我是这样设计的:

agent-skills/ ├── skills/ │ ├── __init__.py │ ├── registry.py │ ├── base.py │ └── builtin/ │ ├── fetch_webpage.py │ ├── sql_query.py │ └── report_generator.py ├── traces/ │ └── trace_middleware.py ├── config/ │ └── settings.yaml └── main.py

这里有个关键设计决策:每个技能是一个独立的Python文件,文件内部包含完整的描述元数据、参数Schema和执行函数。这样做的理由很简单——随着技能越来越多,你不可能维护一个巨大的、把所有技能堆在一起的文件。独立文件的好处是每个技能可以单独测试、单独review、甚至单独复用。我在实际项目里,技能的增删改,都是通过Git提交来管理的,每个技能文件就是一个自然的代码评审单元。

3.2 技能注册与加载机制

技能注册机制是整个框架的核心。我用了装饰器模式,让注册动作和技能定义放在同一处,避免出现“定义在A文件、注册在B文件”这种割裂结构。基础类长这样:

from dataclasses import dataclass, field from typing import Any, Callable, Dict, Optional import inspect @dataclass class SkillDefinition: name: str description: str parameters: Dict[str, Any] tags: list[str] = field(default_factory=list) retry_policy: Dict[str, Any] = field(default_factory=dict) class Skill: def __init__(self, definition: SkillDefinition, handler: Callable): self.definition = definition self.handler = handler  async def execute(self, **kwargs) -> Any: return await self.handler(**kwargs)

注册中心做成一个单例,维护一个技能字典:

class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] = {}  def register(self, definition: SkillDefinition): def decorator(func: Callable): self._skills[definition.name] = Skill(definition, func) return func return decorator  async def execute_skill(self, name: str, **kwargs): if name not in self._skills: raise KeyError(f"Skill not found: {name}") skill = self._skills[name] # 这里统一做参数校验、重试、追踪 ... registry = SkillRegistry()

用装饰器注册一个技能,代码是这样:

@registry.register(SkillDefinition( name="fetch_webpage", description="抓取指定网页的正文内容,返回清洗后的纯文本。适用于公开可访问的网页,不适用于需登录的页面。", parameters={ "type": "object", "properties": { "url": {"type": "string", "description": "完整的网页URL"}, "max_chars": {"type": "integer", "description": "最多返回多少个字符", "default": 5000} }, "required": ["url"] }, tags=["网络", "抓取"], retry_policy={"max_retries": 3, "backoff": "exponential"} )) async def fetch_webpage(url: str, max_chars: int = 5000): ...

你可能想问,为什么不直接用LangChain的@tool装饰器?原因前面提过:LangChain对技能描述、参数Schema、重试策略、Trace都没有强约束,而我这套框架从一开始就把这些作为一等公民。如果你只是随便写几个工具做Demo,用LangChain没问题,但要做生产级技能库,强约束带来的规范价值会逐渐显现。

3.3 一个真实技能的实现流程

我拿项目里最常用的一个技能“数据表格查询助手”来演示完整流程。这个技能的作用是:用户用自然语言描述想查什么数据,技能把自然语言转成SQL,然后在预置的SQLite数据库上执行并返回结果。

第一步:定义参数Schema。这一步最关键的是把“自然语言查询”和“可选的表名列表”分开,因为技能内部要把自然语言转SQL,最好让模型先知道库里有哪些表:

SKILL_QUERY_DATABASE = SkillDefinition( name="query_database", description="查询业务数据库并返回结果。适用于需要对结构化数据做筛选、聚合、排序等操作的场景。如果用户想查的数据不在available_tables表名列表中,不要使用本技能。", parameters={ "type": "object", "properties": { "natural_language_query": { "type": "string", "description": "用户的自然语言查询描述,例如:最近7天每天的订单数量" }, "available_tables": { "type": "array", "items": {"type": "string"}, "description": "本次查询涉及的表名列表,限制模型只能访问这些表" } }, "required": ["natural_language_query"] }, tags=["数据库", "SQL"], retry_policy={"max_retries": 2, "backoff": "fixed", "interval": 1} )

第二步:写执行函数。执行函数里要处理的是:拼接一个更详细的提示词,让LLM根据表结构生成SQL,然后在受控的数据库连接上执行。这里有个安全细节:绝对不能让模型生成的SQL直接在生产库上执行,而是要在只读副本或者事务里执行,超时时间控制在5秒内。

第三步:结果映射。数据库查询结果是二维表,要把列名、行数据、行数、耗时都返回,方便Agent判断结果是否合理。如果查询结果为空,要明确返回“结果为空”,而不是抛一个没头没尾的异常。

这个技能上线后,我们的Agent在处理“上季度各区域销售额排行”这类问题时,成功率从裸调模型时的不到50%提升到了接近85%。核心原因很简单:裸调时模型要自己猜表结构、自己拼SQL,经常把表名写错;有了技能封装,表结构信息通过参数Schema强制约束,模型只需要负责“描述意图”,SQL生成和执行交给技能内部更专注的流程来处理。

3.4 从单技能到多技能编排

单个技能会写了之后,真正的挑战是从“会单个技能”到“会编排技能”。agent-skills在上一层做了个“编排技能”(Orchestrator Skill),它本身也是一个技能,但它允许模型声明“我需要依次调用了哪些技能、各自传入什么参数、如何处理中间结果”。

SKILL_ORCHESTRATE = SkillDefinition( name="orchestrate", description="将多个技能按顺序组合,完成一个复杂的任务。当任务需要查询数据、分析内容、生成报告等多个步骤时,使用本技能统一编排。", parameters={ "type": "object", "properties": { "steps": { "type": "array", "items": { "type": "object", "properties": { "skill": {"type": "string"}, "args": {"type": "object"}, "output_key": {"type": "string"} }, "required": ["skill", "args"] }, "description": "有序步骤列表" } }, "required": ["steps"] }, tags=["编排", "组合"], retry_policy={"max_retries": 0} )

一个典型的编排示例是“查询上季度销售数据,做成分析报告”。模型会调用orchestrate,声明步骤1调用query_database获取原始数据,步骤2把数据和报告模板传给report_generator,生成最终报告。这种显式编排和让模型自由发挥的区别在哪?在于每一个步骤的输入输出都被记录,Agent可以随时回溯,如果最终报告质量不行,它能定位到是数据查询错了,还是报告生成阶段的提示词没写好。

有一点必须提醒:编排技能容易极致灵活之后换来混乱。我建议给每个子技能的“输入约束”和“输出契约”都钉死,比如query_database的输出必须是“表格式+概要统计”,report_generator的输入必须是“结构化数据+模板ID”。这样编排才能稳定。

4. 常见问题与排查技巧实录

4.1 模型“装傻”不调用技能怎么办

这是被问得最多的一个问题:模型明明有工具,但就是不调,或者该调A技能时调成了B技能。我排查过几十个这种案例,总结下来,九成不是模型问题,而是技能描述和上下文管理出了问题。

排查顺序我建议是这样的:

  1. 先查描述:把技能的description单独拿出来,让一个陌生的初级工程师看一遍,看他能不能准确说出“这个技能什么时候该用、什么时候不该用”。如果他说不清楚,模型大概率也说不清楚。这是最快、成本最低的排查办法。

  2. 再查上下文:如果你的系统提示词里塞了几万字的历史对话记录,模型对“当下该用什么工具”的注意力会被稀释。我做过一个对比实验:同样的任务,上下文从2万字符压缩到5000字符后,工具调用准确率提升了近两成。压缩的核心是把历史消息摘要化:过去的工具调用记录、任务中间结果,压缩成几十个字的摘要,而不是把原文原封不动地继续往后面堆。

  3. 最后查参数Schema:很多模型在参数多、嵌套深的时候会传错。一个经验值:单个技能的参数建议控制在3到6个,类型尽量用简单的string、number、boolean,少用深层嵌套对象。实在要传复杂结构,先让它传JSON字符串,技能内部再解析。

4.2 技能越加越多,上下文被撑爆

技能库从几个涨到几十个之后,必定遇到一个现实问题:模型每次请求都要把全部技能定义塞进上下文,Token消耗直线上升,而且技能多了之后,模型选择负担反而变大,准确率下降。这时候就要做“动态技能检索”,而不是“全量技能注入”。

我用的方案很朴素但有效:给技能库加一层向量检索引擎。先把每个技能的description、tags、参数说明转成向量,存到本地的向量数据库里。每次任务进来,先用用户的输入去检索,只把最相关的5到8个技能注入到上下文,其他的根本不进Prompt。

这套方案跑起来之后,Token成本大概降了60%,工具选择的准确率也提高了大约15%。原理很简单:模型在一个小范围内的精确匹配,永远比大范围内的模糊匹配强。有人可能会想,直接用LangChain里的工具路由行不行?也行,但那套方案不够透明,你自己完全可控的做法就是上面这层“检出-注入-执行”的链路。

4.3 技能间的隐式冲突与优先级问题

技能库多了,你还会遇到一个“内卷”现象:两个技能功能重叠,模型拿不准选哪个。比如你有“fetch_webpage”(抓网页正文)和“fetch_webpage_v2”(抓网页并自动提取结构化信息),新任务来了,模型可能随机选一个,行为自然不稳定。

我的解决方法是显式的“技能淘汰”机制。每个技能上线前,都要做一次“重复度审计”:

  • 如果新技能和已有技能功能重复度超过70%,不新增,优先改进旧技能。
  • 如果旧技能确实被新技能完全覆盖,并且运行一段时间没有报错,就把旧技能从注册中心移除,而不是保留存根。
  • 每次技能更新,把“废弃技能”和“替代技能”的映射关系写进变更日志,Agent在遇到查询时会优先使用推荐版本。

这个原则帮我控制着技能库的规模,项目运行到三个月时,有效技能数量从42个控制到了28个,但任务整体成功率反而更高了。技能数量从来都不是越多越好,而是越精准越好。

4.4 实测数据与效果对比

最后分享一组我在一个文档处理Agent上的实测数据。这个Agent的核心技能包括:文档解析、表格抽取、图片文字识别、摘要生成、报告排版。在未使用agent-skills框架、纯靠提示词调用各种基础工具时,任务成功率约58%,平均耗时约15秒,Token消耗约1.8万/任务。

换成agent-skills框架并完成技能封装后,同一批测试任务成功率提升至87%,平均耗时降到9秒(因为技能内部做了并发和缓存),Token消耗降到1.1万/任务。最明显的提升其实不是速度,而是行为稳定性——Agent不再频繁出现“调用错了工具”“参数传偏了”“中途脚本报错后不知道怎么办”这类问题,因为它每一个技能的边界、重试逻辑、失败兜底都被显式定义好了。

这些数据不一定有普适性,但它能说明一个问题:Agent系统的演进核心,不在模型层,而在技能层。模型负责策略判断,技能负责稳定执行,两者通过规范的结构化描述衔接,整个系统才能从Demo走向生产。

我个人在实际操作中的体会是:做Agent技能库,本质上是在给模型造一套“可被信任的双手”。每次从零搭一个技能时,想想它将来面对的是几十种奇怪的输入、随时可能挂掉的外部依赖,你就明白前面说的描述结构化、重试策略、可观测性这些为什么缺一不可了。如果你刚起步,不用一上来就追求复杂的编排,先老老实实把三五个核心技能写得足够稳,再逐步扩展。这比一开始就想着“让模型自主搞定一切”靠谱得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询