Agent Skills 这个词最近在 Agent 开发圈子里热度很高。很多人一听到它,就以为是给 Agent 加一堆工具函数,或者在提示词里塞几段固定指令;实际上,Agent Skills 的核心是把“某类任务的完整处理方式”封装成独立、可描述、可被 Agent 自动调度的模块。它对普通开发者最大的价值,不是让你写一个多复杂的框架,而是让 Agent 不再靠聊天式临场发挥,而是按能力模块去完成任务。下面按我实际踩过的路径,从基础概念讲起,逐步拆到技能封装、Agent 调度和代码落地,整个过程尽量保持能复现的状态,读完你可以用最小代码把自己的业务动作封装成技能。
1. 先搞清楚 Agent Skills 解决的问题,再谈封装和调度
1.1 为什么 Agent 会突然需要 Skills
普通 LLM 对话模式下,你让模型执行任务,它只能在上下文里“现场生成”回答。如果这个任务是“写一段固定格式的周报”“解析指定格式的日志”“把文本按规则转换成指定结构”,每次都让模型重新理解规则,结果很容易漂移。今天可能格式对,明天换个输入就变样。Skills 的出发点就是把这类重复、固定、可验证的任务提前定义好,模型不需要在每次对话里重新发明轮子,而是根据任务描述去调用已经封装好的能力。
另一个原因是工程化。一个 Agent 项目一旦要落地,必然涉及输入校验、错误处理、超时、日志和批量执行。如果所有逻辑都写在提示词里,这些工程能力几乎没法保证;但把能力封装成技能模块后,这些都可以用普通代码完成。很多人看完吴恩达的 Agent 课程,会对 skill 这个词印象很深。他的观点核心在于:与其让模型在推理时即兴组合,不如把高频操作预先封装成结构化步骤,让 Agent 在做规划时更稳。课程里更多是概念讲解,实际开发时,我们需要把概念翻译成代码模块。
1.2 Agent Skills、工具、插件、工作流的边界
很多人分不清这几个词。
工具(Tool)通常指模型可以调用的单个函数,比如查询天气、访问数据库。它粒度很小,一般只负责一次操作,没有运行状态,也不包含复杂流程。插件(Plugin)更多指能扩展平台功能的一组能力集合,比如 IDE 插件、浏览器插件,它可以带 UI 或协议接口。它在 Agent 领域也可以理解为工具集。工作流(Workflow)强调固定步骤和确定性流程,比如先翻译再总结再发邮件,一般不允许模型自由调整步骤顺序。
Agent Skills 更接近“可以交给 Agent 自主调用的专业能力包”。它比单个工具粒度大,往往包含完整的输入输出约定、内部步骤和错误处理,但它又不像工作流那样强制固定执行顺序。简单理解:Skills 是给模型的“专业方法”,不是“固定流水线”。这个边界很重要,因为一旦混在一起,设计出来的技能模块要么太细碎,要么太僵硬,Agent 调度时反而不知道该怎么选。
1.3 一套可复用的技能包含哪几部分
我在实际封装时,一般会把一套技能拆成五个部分:
- 能力清单:技能叫什么、负责什么、什么时候用、什么时候不用。
- 输入协议:调用方需要提供哪些参数,字段类型、必填项、约束条件。
- 执行实现:一段代码或一组提示词,负责真正完成工作。
- 输出协议:返回什么,结构如何,是否包含状态码、错误信息、运行耗时。
- 测试样例:一组典型输入和期望输出,用来验证封装没有坏。
这五样缺了后两样,短期能用,长期一定出问题。输出协议决定 Agent 下一步怎么接;测试样例决定技能改完以后怎么确认没改坏。很多人只关注第三步“实现”,把另外四样当成形式主义,结果技能一多就乱套。
2. 准备工作:先搭一个最小可运行的 Agent 骨架
2.1 本地环境需要准备什么
Agent Skills 本身不挑语言,Python 生态最方便。建议准备:
- Python 3.10 或以上版本,主要为了类型注解和更清晰的数据类语法。
- 一个可用的 LLM 接口,本地模型或云端 API 都可以。不同模型对 function calling 的支持不一样,第一次实验建议用支持结构化工具调用的模型。
- 基本的依赖:openai 或对应 SDK、pydantic 用于输入输出校验、标准 logging 做日志记录。
低配置机器要不要担心?如果是调用云端接口,对电脑要求很低;如果本地跑模型,显存至少要能装下模型权重加推理开销。7B 级别模型通常 8GB 显存起步能试,但并发和长上下文不要指望太多。这里给的是通用经验,实际要看模型大小、量化方式和推理框架。
2.2 项目结构怎么摆
第一次做,不要一开始就上框架。我的建议是保持最小目录结构:
agent_skills_demo/ ├── skills/ │ ├── __init__.py │ └── weekly_report.py ├── agent.py ├── registry.py └── main.pyskills 目录放技能实现,registry.py 负责注册所有技能,agent.py 写 Agent 主循环,main.py 跑测试。这个结构简单到不能再简单,但足够看清楚完整链路。为什么先这样摆?因为你要先验证“模型能发现技能、技能能执行、结果能回填”这三个环节。等链路通了,再上复杂框架,否则报错时你根本分不清是框架问题还是你的技能封装问题。
2.3 验证基线:什么算跑通
不要一上来就写复杂技能。先用一条最简单的技能验证,比如 get_current_time:输入为空,返回当前时间。
跑通标准有三条:
- Agent 在收到“现在几点了”这个问题时,会主动调用该技能,而不是自己编一个时间。
- 技能代码成功执行并返回结果。
- Agent 能基于技能返回结果组织最终回答。
三条都满足,你的 Agent 调度骨架就是完整的。之后再往里填复杂技能。这个顺序能节省大量排查时间,因为复杂技能一旦出问题,影响因素太多,你会分不清是描述问题、参数问题还是主循环问题。
3. 技能封装:从一段提示词到一个可调度模块
3.1 技能描述决定调度准确率
在技能注册表里,模型并不是靠阅读你的完整函数体来决定调用哪个技能,它只看能力索引:技能名、描述、参数说明。描述要写清楚“什么时候用、什么时候不用”。
我经常看到的反面写法是:“周报工具,用于处理文本。”这种描述太模糊,模型看到“处理文本”四个字,什么任务都敢往这里塞。更好的写法是:
“weekly_report_generator:根据用户提供的本周工作项目和下周计划,生成 Markdown 格式周报。当用户要求生成周报、写周总结、整理周工作内容时使用。不要用于日报或月报。”
描述里加入否定规则,调度准确率会明显提升。这是经验,不是理论。单一正面描述覆盖不了真实的语言变化,只有把“不要用”的情况也说清楚,模型才能减少误选。
3.2 输入输出协议要用 schema 约束
封装技能时,最怕的就是参数名没有约束。模型自由发挥字段,你的代码什么都拿不到。所以输入输出都要定义 schema。
from pydantic import BaseModel class WeeklyReportInput(BaseModel): user_name: str = "未填写" this_week_items: list[str] next_week_plan: list[str] class WeeklyReportOutput(BaseModel): success: bool report_text: str = "" error: str = ""必填项必须标清楚,可选项给默认值。输出里始终带 success 和 error 字段,这样 Agent 后续才能判断要不要重试。这个习惯比写任何注释都重要。因为一旦技能执行失败,上层只需要看 success 字段就能决定是终止任务、让模型重新填参数,还是直接上报错误。
3.3 代码型技能和提示词型技能怎么选
不是所有技能都要写成代码。对于需要完整逻辑、循环、格式化、文件读取的任务,用代码;对于需要模型创造力、语义理解、改写润色的任务,可以只用提示词模板。但即使是提示词型技能,也要有协议壳。也就是说,外部还是走输入输出校验,内部才交给模型自由发挥。这样上层调度逻辑不变,底层实现随便换。
这里要特别注意一个误区:提示词型技能不等于把提示词拼进系统提示词里。它应该是一个独立函数,接收参数,返回结构化结果。这样你才可能对它做单元测试,也才能在线上单独监控这个技能的成功率。
3.4 一个封装示例
下面是一个最简封装示例,不代表只能这么写,但链路是完整的:
# skills/weekly_report.py SAMPLE_SKILL = { "name": "weekly_report_generator", "description": "根据用户输入生成 Markdown 周报。适用于周总结、周报生成场景,不处理日报和月报。", "parameters": { "type": "object", "properties": { "user_name": {"type": "string"}, "this_week_items": {"type": "array", "items": {"type": "string"}}, "next_week_plan": {"type": "array", "items": {"type": "string"}} }, "required": ["this_week_items", "next_week_plan"] } } def execute_weekly_report(user_name, this_week_items, next_week_plan): try: lines = [f"# {user_name} 周报", "## 本周完成", ""] lines += [f"- {item}" for item in this_week_items] lines.append("") lines.append("## 下周计划") lines += [f"- {item}" for item in next_week_plan] return {"success": True, "report_text": "\n".join(lines), "error": ""} except Exception as e: return {"success": False, "report_text": "", "error": str(e)}这个技能很简单,但它示范了最重要的三个点:注册信息里有明确的调度描述、参数是结构化 schema、执行函数返回成功状态。以后再往里面加复杂逻辑,整体设计不用动。如果技能内部可能访问外部 API,建议在 executor 里加超时和重试逻辑,而不要放在 Agent 主循环里。
4. Agent 调度:模型如何知道该调哪个技能
4.1 能力枚举与路由机制
Agent 调度有两种主流做法。
一种是依赖模型原生的 function calling / tool calling。你把所有技能的能力清单交给模型,模型在生成回答时自动决定是否调用某技能,并把参数按 schema 填好。这种方式的优点是代码量小,模型理解能力强;缺点是模型可能会误选技能,或者填错参数。
另一种是自己写路由。比如用关键词规则、向量检索、小模型分类来决定调用哪个技能。这种方案更可控,但需要维护路由逻辑和测试集,成本和复杂度都更高。
我建议第一次从 function calling 开始。因为它最能体现“Agent 自主调度”的工作方式,代码量也小。等技能数量超过十几个、模型调度出现明显误选时,再引入独立路由层也不迟。判断标准很简单:如果你发现技能越多,误调用越多,那说明依赖模型枚举已经到瓶颈了。
4.2 调度参数怎么调:temperature、并发、最大迭代
有一组参数会直接影响调度效果:
| 参数 | 建议范围 | 作用 | 调节方向 |
|---|---|---|---|
| temperature | 0 到 0.3 | 控制模型在调度时的随机性 | 误填参数时调低 |
| max_iterations | 3 到 10 | 限制 Agent 最多调用几轮技能 | 死循环时调小 |
| max_parallel_tool_calls | 先关掉 | 是否并行执行多个独立技能 | 单任务稳定后再打开 |
| timeout | 10 到 60 秒 | 单次技能调用的超时上限 | 外部 API 场景必须配置 |
这里多说一句 temperature。很多人在写业务生成任务时习惯把 temperature 调高,让输出更有创造性。但在工具调度环节,我不建议这么做。温度高,模型可能“灵机一动”把技能名或参数填错。工具调用要的是确定性,不是创造力。
4.3 从单技能到多技能组合
Agent 的调度能力体现在组合上。比如用户说“把这份会议纪要整理成周报,并检查有没有错别字”。此时 Agent 可能需要调用 meeting_summary、weekly_report_generator、proofread 三个技能。
多技能组合时,最需要关注的是上下文传递。技能的输出必须能被下一个技能的输入 schema 接受。因此我建议所有技能输出统一为结构化对象,至少保留 success 和 data/error 两个字段。否则组合链条很容易在某个环节断掉。
如果发现某个技能经常在组合时被误调用,优先改技能描述,而不是改代码。描述里写清楚前置条件和典型时机,比调任何调度参数都有效。这也是 Agent Skills 里最反直觉的一点:很多“调度不稳定”的问题,根源不在调度逻辑,而在技能描述写得太含糊。
5. 代码实战:从零实现一个可调度的 Skills 模块
5.1 注册表:让技能能被统一发现
技能多了,不能每个都硬编码在主循环里。写一个简单注册表:
# registry.py SKILL_REGISTRY = {} def register_skill(meta, executor): SKILL_REGISTRY[meta["name"]] = {"meta": meta, "executor": executor} def get_skill_list(): # 返回给模型的能力清单,只需要 meta 部分 return [item["meta"] for item in SKILL_REGISTRY.values()] def execute_skill(name, **kwargs): if name not in SKILL_REGISTRY: return {"success": False, "error": f"skill {name} not found"} return SKILL_REGISTRY[name]["executor"](**kwargs)注册表的价值在于新增技能不需要改动 Agent 主循环。你只需写一个新技能文件,在入口处注册,模型下一次就能看到它。这就是“可扩展”最朴素的样子。实际项目里,可以再加一个 skills 目录扫描逻辑,自动注册所有技能文件,但那是优化,不是必需品。
5.2 Agent 主循环:模型返回工具调用后的消息回填
一个最简的 Agent 主循环大概是:
def run_agent(user_input, history=None): messages = (history or []) + [{"role": "user", "content": user_input}] for step in range(MAX_ITERATIONS): response = client.chat.completions.create( model=MODEL_NAME, messages=messages, tools=[{"type": "function", "function": meta} for meta in get_skill_list()], tool_choice="auto", temperature=0.2 ) choice = response.choices[0].message if not choice.tool_calls: return choice.content for call in choice.tool_calls: args = json.loads(call.function.arguments) result = execute_skill(call.function.name, **args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False) }) return "达到最大迭代次数,任务未完成"这段代码非常关键的地方在于:模型返回 tool_calls 后,必须把执行结果以 tool 角色回填给模型,否则模型不知道技能执行的结果,也就没法组织最终回答。很多初学者卡在这一步:技能明明执行了,但最终回答没有变化,基本都是忘记回填消息。还有一点要注意:每个工具调用都有独立的 tool_call_id,回填时必须一一对应,不能把所有结果塞到一条消息里。
5.3 解析模型返回时要处理的异常
模型返回的 arguments 是字符串,不是对象。必须先用 json.loads 解析。解析失败时不要直接崩溃,可以尝试返回一个带 error 信息的 tool 回填消息,让模型重新填写参数。
另外,模型填的参数可能缺少必填项。执行前做 schema 校验,比如用 pydantic 的 model_validate。校验失败时,将错误信息回传给模型,让它修正参数,而不是让技能函数收到 None 后报出莫名其妙的错误。这个“模型临时出错允许它自我修正”的机制,是 Agent 稳定运行的重要保障。但它必须配合最大迭代次数限制,否则模型会无限修正,请求量和耗时都会爆炸。
5.4 用三组测试样例验证调度
我第一次跑通后,一般会准备三组用例:
- 单个技能调用:问“请帮我把本周完成了 A、B、C,下周计划做 D 生成周报”。
- 不需要技能:问“你好”,期望不触发任何工具,直接回答。
- 需要调用但参数残缺:故意不提某个必填项,观察模型是追问还是补全。
第一组验证主链路,第二组验证误触发率,第三组验证参数容错。三组都符合预期,才算真正跑通,不是看到一次成功就收工。特别是第二组,很多人忽略。一个 Agent 如果用户随便说句话都去调用技能,说明能力枚举太激进,或者描述边界没写清楚。
6. 项目落地:批量任务、失败重试、日志和边界判断
6.1 从单条任务到批量任务
单条任务跑通后,离落地还差很远。批量场景要额外处理三件事。
第一是输入准备。不要把所有输入塞进同一个上下文反复跑,而是准备一个输入列表,每条任务独立调用 Agent。原因很简单:上下文会膨胀,耗时会飙升,某一个任务的错误还会影响后续所有任务。第二是输出命名。批量任务必须保证输出文件不互相覆盖。建议用任务 ID 或输入文件名作为前缀,再拼时间戳。第三是失败隔离。某条任务失败时,要记录错误并跳过,不能让整个批次中断。这就要求单任务必须有超时、有异常捕获、有结构化日志。
我的建议是不要一上来就开最大并发。先用 1 个并发跑完一个条数较少的样本,记录单条耗时,再逐步调到 2、4、8。每一步都看成功率、内存和 API 返回的限流情况。资源占用高不一定代表并发开得多,有时是上下文太长或日志打印太多。
6.2 输出质量和稳定性怎么判断
判断一个 Skills 项目能不能用,不能只看一次跑得漂不漂亮,要看几个指标:
- 调度准确率:任务涉及某个技能时,模型是否正确调用该技能。
- 参数正确率:模型填的参数是否都能通过 schema 校验。
- 任务完成率:技能执行后,Agent 最终回答是否存在、是否包含必要结果。
- 运行耗时:单条任务平均耗时、批量任务总耗时。
- 资源占用:峰值显存、内存、磁盘写入量。
这些指标在开发和测试阶段就要建立基线。比如十连跑成功率 90% 以上,平均耗时不超过某个阈值,才考虑接生产任务。没有基线,看到偶发报错就无从判断是改进了还是退步了。我自己一般会写一个简单的评测脚本,准备 10 到 20 条带标签的输入,每次改动代码后跑一遍,对比这几项指标的变化。
6.3 低资源配置和小模型环境下怎么取舍
如果用的是本地小模型,或者 API 的 function calling 不太稳定,要做几个降级动作:
- 减少技能数量,越少越不容易误选。
- 技能描述更短更明确,必要时在描述里写“如果输入不符合以下几点,不要调用”。
- 降低 max_iterations,避免模型反复尝试导致时间浪费。
- 把多技能组合拆成多个单技能 Agent,每个 Agent 只负责一种能力,再用一段简单脚本串联。
低配置能跑,不代表适合批量跑。你要清楚自己的瓶颈在哪里:是模型调度不准,还是技能内部计算太慢,还是外部 API 限流。定位不准,盲目升级硬件或堆并发都没用。如果模型本身对 function calling 支持一般,就老老实实走人工路由或规则路由,别硬撑着让模型自己决策。
6.4 哪些场景不适合用 Agent Skills
最后说边界。不是所有任务都适合做成 Skills。
- 临时性一次性任务,不值得封装,封装成本比直接写提示词高。
- 对输出格式有极其严格要求的生产系统,建议用确定性代码,而不是让模型自己决定调用顺序。
- 输入变化极大的开放任务,技能很难描述清楚调用条件,调度准确率会很低。
- 极低延迟场景,多一次模型调用就多一次延迟,单纯追求速度时,固定流程更合适。
Agent Skills 解决的是“有一定重复性、需要理解上下文、允许少量模型决策”的任务。它讲究的是把专业方法沉淀成能力,让 Agent 在合适时机调用。真正的项目落地,先看调度准确率和失败隔离,再看并发和耗时。
我在几次踩坑后最大的感受是:很多问题不是 Agent 不够聪明,而是技能描述写得含糊、输入协议不严格、错误信息没有回传给模型。你把这三件事做扎实,Agent Skills 的稳定性会提升一大截。先跑通单条,再加大并发;先记录日志,再调参数;先把一份技能跑稳,再扩展技能库。