Agent技能封装实战:手写agent-skills框架,让大模型从会聊天到会干活

发布时间:2026/10/7 2:10:14
Agent技能封装实战:手写agent-skills框架,让大模型从会聊天到会干活
这段时间一直在折腾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两到三句话讲清楚技能用途、适用场景、不适用场景。parametersJSON 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_factorylist) retry_policy: Dict[str, Any] field(default_factorydict) 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(fSkill not found: {name}) skill self._skills[name] # 这里统一做参数校验、重试、追踪 ... registry SkillRegistry()用装饰器注册一个技能代码是这样registry.register(SkillDefinition( namefetch_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( namequery_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( nameorchestrate, 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技能。我排查过几十个这种案例总结下来九成不是模型问题而是技能描述和上下文管理出了问题。排查顺序我建议是这样的先查描述把技能的description单独拿出来让一个陌生的初级工程师看一遍看他能不能准确说出“这个技能什么时候该用、什么时候不该用”。如果他说不清楚模型大概率也说不清楚。这是最快、成本最低的排查办法。再查上下文如果你的系统提示词里塞了几万字的历史对话记录模型对“当下该用什么工具”的注意力会被稀释。我做过一个对比实验同样的任务上下文从2万字符压缩到5000字符后工具调用准确率提升了近两成。压缩的核心是把历史消息摘要化过去的工具调用记录、任务中间结果压缩成几十个字的摘要而不是把原文原封不动地继续往后面堆。最后查参数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技能库本质上是在给模型造一套“可被信任的双手”。每次从零搭一个技能时想想它将来面对的是几十种奇怪的输入、随时可能挂掉的外部依赖你就明白前面说的描述结构化、重试策略、可观测性这些为什么缺一不可了。如果你刚起步不用一上来就追求复杂的编排先老老实实把三五个核心技能写得足够稳再逐步扩展。这比一开始就想着“让模型自主搞定一切”靠谱得多。