提到“agent-skills”,你可能以为又是一个把大模型包装成“全能助手”的营销词。实际上我前几个月接手了一个内部项目,要给基于大语言模型的智能体补上“干活能力”,绕了一大圈才发现,绝大多数团队卡住的根本不是模型推理,而是不知道如何把一次性脚本沉淀成可复用、可组合、可进化的技能模块。这篇就把我当时的设计思路、代码骨架、踩坑记录和后来总结的优化手段一并写出来,希望能给你省下几晚上的摸索时间。
1. Agent技能库的诞生背景:不是所有能力都该写死在提示词里
1.1 第一次让Agent干活时的失控现场
最初我搭的智能体很简单:用户问一句,模型答一句。后来业务方提出要让它“自动收集竞品信息”,我想也没想,就往系统提示词里塞了一大段“你要先访问网站,再提取价格,然后导出表格”的操作步骤。结果上线第一天就翻车——模型一本正经地编造了三个不存在的行业报告来源,还把它们标成了“已核实”。
问题的根源在于,我把“知道怎么做”和“真正能做”混为一谈了。大模型擅长的是语言生成,它可以把流程描述得像模像样,但一旦涉及真实的工具调用、数据处理、异常恢复,就必须依赖外部代码来兜底。这就像你给实习生一本操作手册,他照样可能摔碎烧杯,但如果你给他一套带防护的自动化装置,至少不会把实验室点了。
1.2 重新思考“技能”在智能体体系里的位置
后来我开始研究市面上的智能体框架,发现大家都提到了一个词:tool,也就是工具。但工具的粒度太粗了。一个“搜索API”是工具,可真正干活的时候,你需要的是“先搜索A关键词,再筛选近一周的结果,然后去重,最后整理成表格”这样一套完整动作,这套动作本身就是一个技能。
于是我把项目定名为agent-skills,核心思路是把一切可重复执行的流程封装成独立技能。每个技能有三个特征:输入输出有清晰契约、内部逻辑可被Agent理解、执行结果可被追踪。这样一来,Agent只需要负责规划“该用哪个技能、按什么顺序调用”,而技能内部的具体操作由代码保证稳定性。
这里的关键认知转变是:技能不是给模型背的提示词,而是给模型按的“按钮”。模型只负责按按钮,按钮内部怎么运作,与模型无关。这样既降低了模型的幻觉风险,又让整个技能库像乐高积木一样可以任意拼装。
1.3 这套技能库到底适合哪些人
如果你只是做一个问答机器人,或者给内部员工做个文档摘要助手,那完全可以直接用现成的函数调用能力,没必要自己造轮子。但如果你面临以下三种情况,agent-skills的思路会很值得参考:
- 你手头有大量重复性、多步骤的业务操作,比如数据采集、报表生成、批量审核。
- 你的智能体需要接入多个内部系统,而这些系统的API风格差异极大。
- 你希望不同业务线能共享底层能力,而不是每个项目都从零写一遍工具调用。
换句话说,只要你的智能体开始“做事情”,而不仅仅是“说话”,你就需要一套技能管理体系。
2. agent-skills的核心设计:描述、注册与调用三者如何分工
2.1 给模型看的技能描述要刻意“反模糊”
很多人在写技能描述的时候会陷入两种极端:要么一句话带过,要么写一篇五百字小作文。我的经验是,技能描述本质上是一份“API文档”,但它面向的不是人类程序员,而是大模型。因此它需要具备三个要素:触发条件、前置依赖、输出格式承诺。
最初我的技能描述是“根据用户问题查询数据库并返回结果”,模型经常困惑于该在什么时候调用它。后来我改成“当用户询问采购金额、订单数量、库存批次等业务数据时,调用此技能;若涉及历史趋势对比,先调用时间序列归一化技能;返回结果一律为包含字段名的JSON数组”。模型调用命中率显著上升,因为描述里已经包含了决策规则。
更反直觉的一点是,描述里要明确写“不做什么”。例如某个技能只负责数据清洗,我就必须在描述里标注“本技能不会生成统计报表,如需报表请调用report_builder技能”。这听起来啰嗦,但非常有效,它能减少模型在边界任务上的自由发挥。
2.2 技能注册表:一堆函数的动态编排中心
技能库的核心是一个注册表,它保存着每个技能的名称、版本、描述、入参Schema、出参Schema、依赖关系和健康状态。我用Python字典加JSON Schema来管理,没有引入太重的外部服务,因为技能数量一开始不会超过五十个,一个注册表进程就够用了。
注册表的妙处在于,它让技能发现和技能调用解耦。模型看到的是一份面向人类可读的技能清单,真正执行时则由调度器通过注册表查询对应函数。举个例子,同样的“数据透视”技能,我可以注册成v1.0和v2.0两个版本,默认走v2.0,出问题可以一键回滚到v1.0,模型和调用方几乎无感知。
我还为每个技能添加了“调用成本”字段,比如预估消耗多少token、耗时多少毫秒。虽然暂时没用它做负载均衡,但至少可以帮助定位性能瓶颈,也能避免模型在一个简单问题上反复调用昂贵技能。
2.3 技能调用的上下文传递机制
刚开始我犯过一个低级的错误:每次调用技能都重新构造参数,导致同一个用户会话里出现了严重的上下文断层。后来我引入了“技能会话上下文”对象,它会在一次任务执行期间持续存在,保存用户意图、原始信息、中间结果和全局配置。
这个上下文对象有四个核心组成部分:
- user_goal:用户原始目标的持久化语义表示;
- artifacts:各技能产出的中间数据对象;
- execution_trace:每一步调用的输入输出摘要;
- constraints:权限、超时时间、重试次数等边界条件。
有了这个机制,智能体在处理复杂任务时就表现出类似人类的“工作记忆”。我们可以让一个技能读取另一个技能刚刚生成的脏数据,还能在模型规划出错时,依照execution_trace回溯并重新调整调用顺序。这一步,让整条技能链的稳定性上了一个台阶。
3. 从零搭建一个最小可用的agent-skills框架
3.1 环境准备与核心依赖
我建议你一开始不要引入任何Agent框架,只靠Python标准库加一个轻量HTTP服务框架就够了,这样能逼自己把每个细节都过一遍。我自己的实验环境是Python 3.10,加上FastAPI和Pydantic,没有用LangChain之类的重型库。
你说得没错,不引入框架意味着要自己处理模型调用、上下文存储、工具调度等琐碎工作。但恰恰是这个过程,让我彻底理解了技能库的边界到底该划在哪里。等框架跑通后再去看别人的上层封装,你会有一种“原来那些抽象都是在解决这两个痛点”的通透感。
我用SQLite做技能的注册存储,每个技能一行记录,字段包括:name、description、input_schema、output_schema、handler_path、status、created_at。在开发阶段,这样的设计允许我直接打开数据库改描述,实时生效,不用重启服务。
3.2 技能函数的定义与注册代码详解
一个技能handler就是一个普通的Python函数,它接收统一的上下文参数,返回一个可行的数据结构。我给它起的规范名称是skill_handler,每个handler都必须实现execute(ctx)接口,ctx里面包含了上面提到的上下文对象。
来看一个具体的技能实现,它负责把一段原始文本按句子切分并去重。为了让Agent能正确调用它,我在函数头写了一段非常明确的描述性文字,然后用一个装饰器把它注册进技能表:
from pydantic import BaseModel, Field from typing import Any class TextSplitInput(BaseModel): text: str = Field(description="原始文本内容") min_length: int = Field(default=10, description="保留句子的最小长度") class TextSplitOutput(BaseModel): sentences: list[str] dropped_count: int @skill_register( name="text_split_dedupe", description="将长文本按句号切分并去重。仅当用户需要从大段文本中提取独立观点时使用。不负责摘要生成。", version="1.0", input_schema=TextSplitInput, output_schema=TextSplitOutput ) def text_split_dedupe(ctx): text = ctx.params.text min_len = ctx.params.min_length raw_sentences = [s.strip() for s in text.replace("\n", " ").split("。") if s.strip()] seen = set() result = [] for s in raw_sentences: if s not in seen and len(s) >= min_len: seen.add(s) result.append(s) return {"sentences": result, "dropped_count": len(raw_sentences) - len(result)}这里的skill_register装饰器会做三件事:校验输入Schema、把handler对象存入全局技能表、往日志里写入一条注册记录。对于没有用过的读者,我可以简化地认为,它就是给这个函数发了一张“员工卡”。
3.3 调度器怎么把Agent的意图转成一次真正的调用
下一步是写一个调度器,它负责让大模型决定调用哪个技能,并处理调用过程中产生的异常。我用的是OpenAI风格的函数调用接口,也就是把技能清单铺给模型,模型返回一个结构化的调用请求,然后由调度器执行真实函数。
调度器内部逻辑不算复杂,但有几个位置必须处理得足够细致。首先是参数合法性校验,模型偶尔会生成溢出边界的数值,比如把min_length填成-5,Pydantic的校验会在函数执行前直接拦截。其次,一个技能可能执行几十秒,我需要支持超时中断,防止技能A卡死导致整个任务链瘫痪。
我摘一段调度器的核心分支代码:
def run_skill(skill_name: str, params: dict, ctx: Any): skill = registry.get(skill_name) if not skill: return {"error": f"Skill {skill_name} not found"} validated = skill.input_schema(**params) # 校验失败会抛异常 result = skill.handler(ctx.replace_params(validated)) return {"status": "ok", "data": result}这里的异常会被统一捕获,然后编成一条“调用失败原因”附加到上下文对象里,反馈给模型,让模型有机会选择替代技能。这一步极其重要,否则模型会误以为上一次调用成功了,然后往下产生一堆无意义的连锁调用。
3.4 给技能库加一份“可观测性”日志
调试技能调用依赖链时,我意识到一个问题:没有日志,技能库就是一坨神秘的黑盒。于是我在调度器里加了结构化日志,记录每次技能调用的前因后果:调用前,模型给出的意图摘要和参数;调用后,返回结果里被截断的前200个字符;如果失败,记录异常堆栈的关键行。
日志统一写到JSON文件里,后续可以灌入任意分析工具。每隔一段时间,我会根据趋势找出最常用的技能和最常失败的技能,然后针对性地优化描述文案或内部实现。一个意外的收获是,这些日志还成了我写“技能使用指南”的第一手材料,比看代码直观多了。
4. 技能库落地时最容易踩的坑,我帮你提前排一遍
4.1 把技能描述写成功能清单,模型完全没有调用欲望
这是最容易犯的错。你以为自己在帮模型,实际上你在给它制造阅读负担。原本我的某个技能描述是“计算两个日期间的差值、工作日天数、自然日天数,返回日历天数和可用工时”,半年后我重新审视,发现模型几乎调用不中它。后来我把描述改成:“当用户询问‘请假几天’、‘项目何时截止’、‘还剩多少工作日’时使用;若用户只问单一日期,无对比场景,不要调用”。
改完之后,调用率直接翻了一倍。这说明一个道理:技能描述的任务不是详述功能,而是做意图路由。模型是根据“用户此刻想干什么”来找技能的,它并不真的关心你的实现细节。所以描述里应该多写触发场景、排除场景,少写逻辑过程。
4.2 技能间依赖处理过于粗暴,出了问题整条链路全崩
技能库规模大了以后,必然存在技能A先产出数据、技能B再消费数据的依赖关系。我初期没有建立依赖声明,只在调度逻辑里手动拼接顺序,结果某次数据源升级后,技能B接收了结构完全不同的中间结果,直接抛KeyError,任务链当场断掉。
后来我在技能注册表里增加depends_on字段,声明一个技能执行前需要哪个技能先跑完。调度器据此自动插入前置调用,不用模型操心。同时我会在技能输出里保留schema_version,让下游技能能迅速判断数据格式是否兼容。这个机制让整个调用链的抗变化能力强了很多。
4.3 上下文无限膨胀,最后一口气把记忆体挤爆
Agent执行一个复杂任务时,技能之间传递的中间结果会被全部累积到上下文对象里,看起来大而全,实际上非常致命。有一次我让Agent处理一个包含两千行商品记录的表格,每个技能都把原始表格保存到上下文,导致后续调用时token开销爆炸,响应慢得没法用。
我吸取教训后,规定了上下文对象里的artifacts只保留轻量级元数据,比如行数、列名、统计摘要,完整文件一律写到临时目录并返回路径。这样既不影响技能间传递信息,又控制了内存和token成本。你如果也想做类似项目,建议一开始就明确“上下文只存指针,不存实体”。
4.4 盲目给Agent加“全知”权限,结果技能库变成提款机
考虑到安全,你肯定不希望Agent在用户请求的诱导下调用一个“发送邮件”技能,给收件人列表里的所有人都发一遍垃圾邮件。所以我在每个技能注册时加入了permission_level权限等级,分为:只读、内网输出、外部提交、不可逆操作四级。调度器在调用前会检查当前用户会话的权限层,低于技能要求时直接拒绝,并把拒绝理由返回给模型。
这个设计的代价是初始化时要花不少时间给每个技能定义权限等级,但收益极其明显:即使模型被恶意Prompt绕开了规划限制,它也跳不出代码层的权限校验。任何技能库,没有权限分级的隔离,都不配称为生产级。
5. 进阶玩法:从“单技能调用”到“技能链编排”
5.1 技能链:让Agent自动发现可组合路径
基础技能库一旦跑通,你会很快遇到新的需求:某个任务不是单一技能能解决的,而是需要两三个技能串联。比如“分析本周销售趋势”可能需要:先调用“数据拉取技能”,再调用“异常值清洗技能”,最后调用“图表生成技能”。
我实现了一个轻量级的技能链编排模块,它允许用户或模型指定一个大致的执行序列,技能链管理器负责按顺序调用并统一收集结果。与硬编码流程不同的是,这个链里的每个环节仍然独立注册,且中间可以插入“条件分支”、“循环重试”等自定义节点。我把这个模式称为“技能管道”,它在不牺牲模块化的前提下,解决了复杂任务的组合问题。
5.2 技能版本管理与A/B测试的工程化
技能库会一直演化。这次改一处逻辑,下次调一段描述,你都应该把旧版本保留下来。我用Git管理每个技能handler的源码,同时在注册表里维护一个活跃版本号。发布新版时,只需要修改版本号并重新加载注册表,不用重启整个服务。
我还尝试过给同一个技能注册两个不同的handler版本,然后按用户会话比例做A/B测试,用日志中的成功率和耗时来决定保留哪个版本。这个操作成本很低,但能带来实打实的质量提升。毕竟,你对技能的描述和实现好不好,终究要通过真实调用数据来证明。
5.3 把技能库回馈给业务方:从个人工具到团队资产
当技能库稳定运行一段时间后,我开始整理“技能卡片”,每一张卡片包含技能名、触发样例、入参出参说明、常见问题。这份文档比代码注释重要得多。业务方甚至不用理解模型原理,只要读出对应的技能卡片,就能向Agent提出合理的任务请求。
这个项目最后沉淀的,不只是一个技能注册表和几十个函数,而是一套关于智能体能力的组织方法论。团队再遇到新的自动化需求时,不再陷入“这个能不能让AI做”的讨论,而是直接问“这属于哪个技能的变体,需要注册一个新技能还是扩展现有技能的Schema”。这才是Agent技能体系真正进入良性迭代的标志。
我在带这个项目的过程中最深的一个体会是:再聪明的模型,也需要一块稳固的“知识飞轮”来帮它稳稳落地到业务上。技能库的本质,就是把不可控的模型行为,逐渐收敛成可预期、可维护、可组合的执行单元。你当然可以不搞一套完整的agent-skills工程,但只要你的智能体开始接触真实任务,早点想清楚“技能从哪来、谁能调用、怎么进化”这三个问题,一定不会亏。