Agent Skills实战:从零构建可复用的AI Agent技能包
Agent Skills 是当前 AI Agent 工程化中绕不开的一个概念它解决的核心问题是一个 Agent 不能只靠提示词和模型记忆完成所有事。查询系统状态、抓取网页、运行数据分析、操作文件这些能力如果全部写进提示词既难维护又容易出错如果一上来就微调模型成本高、周期长、更新慢。Agent Skills 把某一类能力封装成独立的“技能包”由说明文档、脚本、依赖和触发条件组成Agent 在运行时按需发现、按说明调用。这篇文章用一个最小可运行的项目带读者完整走一遍技能包的创建、挂载、调用、验证和排错适合已经能调用大模型 API、准备做真实 Agent 应用的开发者。1. 先理解 Agent Skills 到底解决什么问题1.1 扩展 Agent 能力的几种主流方式大模型本身的知识和推理能力很强但它默认不掌握外部环境的状态。它不知道你机器上跑着哪些进程不能直接读你本地文件不能帮你执行一段会修改系统的代码。要让 Agent 具备这些能力工程上主要有四条路微调、Function Calling、MCP、Agent Skills。方式是否改模型能力更新成本适合场景主要问题微调是高需要数据和训练资源把领域知识内化到模型更新慢、成本高、能力固定Function Calling否中需要调整 API 定义单一、确定、参数化的接口复杂多步流程难以描述MCP否中高需要搭建 Server连接外部系统和数据源偏协议层缺少流程编排Agent Skills否低新增目录和文档多步骤、有判断、可复用的能力依赖模型指令遵循能力把这四类放在一起看微调改变的是模型本身Function Calling 改变的是模型和外部函数的交互方式MCP 改变的是工具接入的标准化方式Agent Skills 则站在更高一层解决“一段完整能力如何组织、如何被模型理解、如何被执行”的问题。1.2 Agent Skills 是独立方案还是叠加方案很多初学者把这三个词搞混。实际项目里它们不是二选一的关系Agent Skills 的内部完全可以用 Function Calling 或 MCP 来实现。一个技能包的脚本需要访问外部数据库时脚本内部通过 MCP 客户端连接数据源这是很常见的组合。Agent Skills 强调的不是传输层而是把能力组织成人类可读、模型可看的文档和代码让模型像人一样按手册操作。换句话说Function Calling 决定模型能不能产生一次调用MCP 决定调用如何跨进程协作Agent Skills 决定 Agent 如何知道在什么场景下用什么方式完成什么目标。1.3 什么场景值得引入 Agent Skills从工程角度看出现下面三个信号就该认真考虑技能包方案了同一能力被多个 Agent 或多次对话复用。技能只写一遍所有 Agent 共享。能力本身是多步骤流程。比如“抓取网页 - 提取正文 - 生成 Markdown 摘要”中间有判断和分支。能力需要快速迭代。新增一个技能就是新增一个目录不需要重新发布提示词或模型。反过来如果只是给模型加一个两行的计算接口直接 Function Calling 就够如果能力需要高频、实时连接外部 SaaS优先考虑 MCP 协议。Agent Skills 的价值在“流程、复用、维护”这三件事上体现得最明显。2. Agent Skills 的核心组成和调用机制2.1 一个技能包由哪些文件组成目前社区里最常见的技能包结构是一套目录约定agent-skills-demo/ └── skills/ └── process-inspector/ ├── SKILL.md ├── scripts/ │ └── inspect_process.py ├── references/ │ └── troubleshooting.md └── requirements.txt各文件的职责如下SKILL.md给模型看的操作手册同时包含技能元数据。scripts/真正执行的代码入口。一个技能可以有多个脚本但入口要明确。references/补充参考文档。模型执行时如果发现信息不够可以再读取这些文档。requirements.txt技能的 Python 依赖列表。这个结构不是必须一字不差但建议遵循。因为 Agent 在扫描技能时有统一约定读SKILL.md拿元数据进入scripts找入口安装依赖准备环境。约定越一致Agent 的可用性越稳定。2.2 SKILL.md 的 frontmatter 是技能的“门牌号”SKILL.md开头的 YAML frontmatter 是整个技能包最重要的部分。最小示例--- name: process_inspector description: 查询本机进程信息。当用户询问系统进程、CPU 占用、内存占用或怀疑某个程序没有启动时使用。用户询问天气、写诗、翻译时不要使用。 --- # Process Inspector 这个技能用于查询当前机器上正在运行的进程。name在整个技能集合里必须唯一它是 Agent 引用这个技能的 ID。description是模型做技能选择的唯一依据必须告诉模型两件事什么时候用、什么时候不要用。很多人的技能写完了却不被调用问题就出在 description 太笼统。2.3 调用机制模型先读手册再执行脚本一个完整的调用链路通常包含五步系统扫描 skills 目录汇总每个SKILL.md的 name 和 description得到技能清单。用户提问被送入模型模型在系统提示中看到技能清单。模型判断当前问题是否命中某个技能的触发条件。命中后系统执行技能脚本或者把完整SKILL.md注入上下文让模型按步骤操作。执行结果返回模型模型基于结果生成最终回答。第 4 步有两种常见模式。一种是文档注入式适用于纯提示词、不需要真实计算的技能另一种是子进程执行式适用于需要访问文件、网络、系统 API 的技能。本文的例子属于后者系统把参数传给 Python 脚本脚本把结果以 JSON 打回。3. 环境准备先搭一个能跑的最小工程3.1 需要准备的组件组件本教程使用说明操作系统Linux / macOSWindows 也可以但进程信息字段会有差异Python3.10运行 Agent 编排脚本和技能脚本LLM APIOpenAI 兼容的 Chat Completions也可以用本地模型服务依赖openai、psutil、pyyaml按技能实际需求增减如果本地没有现成 API可以用 Ollama 等本地模型服务只要它提供 OpenAI 兼容接口即可。下面的最小工程只依赖 openai模型名通过环境变量配置。3.2 创建项目骨架和虚拟环境mkdir -p agent-skills-demo/skills cd agent-skills-demo python -m venv .venv source .venv/bin/activate pip install --upgrade openai psutil pyyaml使用虚拟环境的原因是技能脚本可能依赖 psutil、requests 等第三方库如果不隔离依赖换一个技能就可能把系统环境搞乱。生产环境里还应该把依赖版本锁进requirements.txt而不是靠“当前环境里恰好装了”。3.3 验证依赖和 API 是否可用python -c import openai, psutil, yaml; print(dependency ok)设置 API Key 后用一小段代码验证连通性from openai import OpenAI client OpenAI() resp client.chat.completions.create( model你的模型名, messages[{role: user, content: ping}], ) print(resp.choices[0].message.content)看到正常文本输出就说明 API 可用。如果模型名不确定先去 API 服务商查模型列表。这里不要照抄某一个固定模型名不同账户可用的模型差异很大。注意环境验证这一步不能省。很多技能问题最终都回溯到了“API 都不通却在调 Agent”这一步。4. 从零实现第一个 Agent Skill4.1 先定义技能边界第一个技能选择“本机进程信息查询”原因有三个不需要网络、数据真实、结果容易验证。对新手来说这种技能最适合理解全链路。先定义清楚输入和输出边界输入过滤关键字 pattern、返回条数 limit、排序字段 sort。输出JSON 数组每个元素包含 pid、name、cmdline、cmd、cpu_percent、memory_percent。边界只读操作不 kill 进程、不修改进程。边界一定要先写清楚因为它会直接体现在SKILL.md里决定模型能不能安全、正确地使用这个技能。4.2 创建 SKILL.md--- name: process_inspector description: 查询本机运行中的进程支持按进程名或命令行关键字过滤返回 CPU 和内存占用。当用户问“有没有 python 进程”“谁占用了 CPU”“查一下进程状态”时使用。用户问天气、写代码、翻译时不要使用。 --- # Process Inspector 这个技能用于查看当前机器上正在运行的进程是一个只读技能不会修改或终止任何进程。 使用方法agent 需要读取进程信息时执行以下命令 python scripts/inspect_process.py --pattern 关键字 --limit 条数 --sort cpu|memory_percent 参数说明 - --pattern过滤关键字匹配进程名或命令行可为空。 - --limit最多返回多少条默认 20。 - --sort排序字段可选 cpu 或 memory_percent默认按 CPU 排序。 脚本输出 JSON 数组每个元素包含 pid、name、cmdline、cmd、cpu_percent、memory_percent 字段。如果出错会输出包含 error 字段的 JSON 对象。 注意事项 - 不要虚构不存在的进程和字段。 - 如果脚本输出 error直接把错误信息告诉用户。 - 如果进程数量很多优先展示占用 CPU 或内存最高的几条。在SKILL.md的正文里写清楚“脚本路径、参数、输出格式、注意事项”模型才能在执行时不猜测。真正生产级的技能doc 的质量直接影响调用成功率。4.3 编写核心脚本在skills/process-inspector/scripts/目录下创建inspect_process.py#!/usr/bin/env python3 查询本机进程信息输出 JSON。 用法 python inspect_process.py --pattern python --limit 5 --sort cpu import argparse import json import sys import time try: import psutil except ImportError: print(json.dumps({error: psutil 未安装}, ensure_asciiFalse)) sys.exit(1) def warmup(): psutil 的 cpu_percent 第一次读取返回 0先采样一次建立基线。 for proc in psutil.process_iter(): try: proc.cpu_percent() except (psutil.NoSuchProcess, psutil.AccessDenied): pass time.sleep(0.2) def collect(patternNone, limit20, sort_keycpu): warmup() rows [] attrs [pid, name, cmdline, cpu_percent, memory_percent] for proc in psutil.process_iter(attrs): try: info proc.info cmd .join(info[cmdline] or [info[name]]) info[cmd] cmd[:200] if pattern and pattern not in cmd and pattern not in info[name]: continue rows.append(info) except (psutil.NoSuchProcess, psutil.AccessDenied): continue rows.sort(keylambda x: x.get(sort_key) or 0, reverseTrue) return rows[:limit] def main(): parser argparse.ArgumentParser(description查询进程信息) parser.add_argument(--pattern, defaultNone, help按关键字过滤) parser.add_argument(--limit, typeint, default20, help最多返回条数) parser.add_argument(--sort, defaultcpu, choices[cpu, memory_percent], help排序字段) args parser.parse_args() try: result collect(args.pattern, args.limit, args.sort) print(json.dumps(result, ensure_asciiFalse, indent2)) except Exception as exc: # 技能必须保证输出可被解析异常也要以 JSON 返回 print(json.dumps({error: str(exc)}, ensure_asciiFalse)) sys.exit(1) if __name__ __main__: main()这里有两个设计点值得注意。第一脚本入口参数用 argparse 固定下来而不是让模型自由拼 shell 命令避免注入风险。第二所有异常都转换成 JSON 输出外部执行器拿到的一定是结构化数据不会是一段堆栈日志。4.4 先单独验证脚本cd agent-skills-demo/skills/process-inspector python scripts/inspect_process.py --pattern python --limit 5正常输出类似[ { pid: 31245, name: python, cmdline: [python, -m, http.server, 8080], cmd: python -m http.server 8080, cpu_percent: 0.6, memory_percent: 0.2 } ]这一步验证的是脚本本身。如果脚本这一步就报错后面 Agent 的问题排查会非常难受。脚本能独立跑通再进入 Agent 集成。4.5 编写最小 Agent 运行器在项目根目录创建agent.pyimport json import os import subprocess import sys from pathlib import Path from openai import OpenAI client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL, https://api.openai.com/v1), ) SKILLS_DIR Path(skills) def parse_front(text): if not text.startswith(---): return {} parts text.split(---, 2) if len(parts) 3: return {} meta {} for line in parts[1].strip().splitlines(): if : not in line: continue key, value line.split(:, 1) meta[key.strip()] value.strip().strip(\) return meta def load_manifest(): manifest [] for skill_dir in sorted(SKILLS_DIR.iterdir()): skill_md skill_dir / SKILL.md if not skill_md.exists(): continue meta parse_front(skill_md.read_text(encodingutf-8)) manifest.append({ name: meta.get(name, skill_dir.name), description: meta.get(description, ), path: str(skill_dir), }) return manifest def run_skill(skill_path, args): script Path(skill_path) / scripts / inspect_process.py proc subprocess.run( [sys.executable, str(script), *args], capture_outputTrue, textTrue, timeout30, ) if proc.returncode ! 0: return {error: proc.stderr.strip()} try: return json.loads(proc.stdout) except json.JSONDecodeError: return {error: skill output is not valid json, raw: proc.stdout[:500]} def ask_llm(messages): resp client.chat.completions.create( modelos.getenv(LLM_MODEL, gpt-4o-mini), messagesmessages, temperature0.2, ) return resp.choices[0].message.content or def main(): manifest load_manifest() manifest_text json.dumps(manifest, ensure_asciiFalse) user_input input(请输入你的问题) messages [ {role: system, content: ( 你是系统助手。下面列出当前可用的 Agent Skills\n f{manifest_text}\n 如果用户的问题需要用进程查询技能请回复\n SKILL_START process_inspector --pattern 关键字 --limit 5 SKILL_END\n 其中 --pattern 可以从用户问题中提取SKILL_START 和 SKILL_END 是调用分隔符。 )}, {role: user, content: user_input}, ] reply ask_llm(messages) if SKILL_START not in reply: print(模型没有触发技能直接回答) print(reply) return skill_cmd reply.split(SKILL_START)[1].split(SKILL_END)[0].strip().split() skill_name skill_cmd[0] args skill_cmd[1:] skill_path next(item[path] for item in manifest if item[name] skill_name) output run_skill(skill_path, args) messages.append({role: assistant, content: reply}) messages.append({role: user, content: ( f技能《{skill_name}》执行结果如下\n f{json.dumps(output, ensure_asciiFalse, indent2)}\n 请用中文整理成最终回答不要虚构数据。 )}) print(ask_llm(messages)) if __name__ __main__: main()这个运行器的设计意图很明确把技能选择交给模型把实际执行交给本地子进程。模型输出一段带分隔符的指令运行器解析后用sys.executable去跑技能的 Python 脚本再把结果作为新的上下文回传给模型生成最终回答。真实框架不会用SKILL_START/SKILL_END这种自定义分隔符而是用原生工具调用或结构化输出。但理解这个最小机制后再看任何框架的实现都会很快上手。5. 关键设计点和参数说明5.1 description 决定技能会不会被想起模型选择技能时通常只读 name 和 description不会把整个SKILL.md都读一遍。所以 description 是决定“技能会不会被想起”的第一要素。写法质量原因查询进程信息一般没有触发条件模型不确定什么时候使用当用户询问系统进程、CPU 占用、内存占用或怀疑某进程未启动时使用。用户问天气、翻译时不要使用推荐有触发条件也有排除条件实际项目里一个 Agent 可能挂了几十个技能。description 写得好模型才能快速命中写不好技能就是“存在但永远不被调用”。5.2 stdout 只输出机器可读结果日志走 stderr技能脚本的输出会被两层消费者读取一是运行器二是模型。运行器要用json.loads解析模型要根据结构化内容生成回答。任何一行多余的 print 日志混进 stdout都会导致 JSON 解析失败。正确做法stdout 只输出 JSON 结果或 JSON 错误对象。需要调试时用logging或print(..., filesys.stderr)写 stderr。脚本被单独运行时可以打印日志但被 Agent 调用时stdout 必须干净。5.3 执行参数、超时和重试技能的执行参数需要在运行器里统一控制参数推荐值含义设置过小的表现timeout30 秒技能单次执行上限Agent 误以为技能不可用retry2 次对瞬时失败的重试次数偶发失败直接报错max_output100 KB对 stdout 返回大小的限制长输出被截断max_turns5模型与技能之间的交互步数上限复杂任务陷入死循环在subprocess.run里设置timeout30并在脚本内部做好异常捕获是技能稳定性的底线。不要指望模型每次都生成正确的参数运行器的校验和兜底必不可少。5.4 依赖和运行环境隔离技能不是只写一个脚本就完了依赖要和技能打包管理。requirements.txt的版本建议锁死例如psutil5.9,7学习环境下直接pip install -r requirements.txt即可。生产环境不要在每次调用时现装依赖应该把依赖打进镜像或预制到运行环境中。否则一个技能首次调用会占掉十几秒甚至更久。6. 运行验证与结果分析6.1 端到端跑一次对话在项目根目录执行export LLM_API_KEYsk-xxxx python agent.py输入请输入你的问题帮我看看现在有没有 python 进程在跑底层会发生四件事load_manifest扫描到process_inspector技能把 name 和 description 放进了系统提示。模型判定问题命中技能输出类似SKILL_START process_inspector --pattern python --limit 5 SKILL_END的指令。运行器解析指令执行scripts/inspect_process.py --pattern python --limit 5。执行结果回传给模型生成最终回答。最终回答类似当前机器上检测到 2 个 python 相关进程 - pid 31245python -m http.server 8080CPU 占用约 0.6%内存占用约 0.2% - pid 28701python scripts/train.pyCPU 占用约 42.3%内存占用约 0.8% 两个进程内存占用均低于 1%。能观察到这个完整链路说明技能包从“文件结构”到“模型触发”再到“真实执行”都正常工作。6.2 验证顺序和判定标准调试技能时建议按下面的顺序逐层验证不要跳步检查项判定标准技能目录是否存在skill 文件夹在skills/下包含SKILL.mdfrontmatter 是否可解析load_manifest输出的清单里有该技能description 是否清晰模型能命中触发条件脚本单独执行是否正常返回合法 JSON包含预期字段运行器能否解析 SKILL_START日志能看到执行了对应脚本最终回答是否准确回答里的 pid、进程名与 JSON 数据一致每层通过后再进下一层。多数技能问题都出现在第二层或第三层而不是模型层。6.3 失败场景分析技能执行失败的常见返回{ error: psutil 未安装 }另一类返回是脚本超时subprocess.TimeoutExpired: Command ... timed out after 30 seconds看到这两类输出第一反应不是“模型不行”而是“执行环境有问题”。先单独跑一遍脚本确认依赖、路径、执行时间再回去调整运行器的超时参数。7. 常见问题排查7.1 Agent 完全不调用技能现象用户问“有没有 python 进程”模型却直接回答“我无法查看本机进程”。可能原因load_manifest返回空列表模型根本没看到技能。description 写得太笼统模型不确定该不该用。系统提示里没有说明调用协议模型不知道可以调技能。检查方式先打印 manifest再直接问模型“你能用哪些技能”。如果模型列不出来问题在清单加载或提示词如果能列出来但不调用问题在 description 的触发条件。解决方式把 description 改成“当用户询问……时使用”并在系统提示里增加明确的调用协议示例。7.2 技能没有出现在 manifest 中现象load_manifest结果缺少某个技能。可能原因SKILL.md文件名拼写错误比如skill.md。frontmatter 格式错误name或description缺冒号。文件编码不是 UTF-8中文 description 乱码导致解析失败。检查方式直接读取SKILL.md文件内容解析解析 frontmatter 部分。解决方式统一用 UTF-8 保存文件frontmatter 严格按 YAML 格式写必要时用yaml.safe_load替代手写解析。7.3 脚本返回内容无法被 JSON 解析现象运行器返回{error: skill output is not valid json}。可能原因脚本里把调试日志 print 到了 stdout。脚本抛异常堆栈信息直接打到 stdout。输出内容超过了解析上限。检查方式在运行器里把原始 stdout 输出保存到日志文件print(RAW STDOUT:, proc.stdout[:1000], filesys.stderr)解决方式日志改用 stderr异常统一包装成 JSON 错误对象脚本入口用sys.exit(1)标记失败。7.4 子进程找不到脚本或文件路径错误现象运行器报FileNotFoundError或脚本内报文件不存在。可能原因运行器不是从项目根目录启动相对路径解析错误。脚本内部用了./data/xxx但子进程工作目录不在技能目录。检查方式打印脚本绝对路径和当前工作目录。print(script:, script.resolve()) print(cwd:, Path.cwd())解决方式运行器内用Path.resolve()把脚本路径转成绝对路径脚本内部读取文件时使用Path(__file__).parent拼绝对路径。7.5 权限不足或进程偶发不可读现象psutil 抛AccessDenied或某些进程数据缺失。可能原因查询的是其他系统用户的进程。系统限制了进程信息读取权限。进程在遍历过程中刚好退出。检查方式把捕获到的异常输出到 stderr观察被跳过的进程。解决方式脚本内捕获psutil.NoSuchProcess和psutil.AccessDenied跳过不可读进程技能文档里说明哪些字段在受限情况下可能为空。不要在技能里用提权方式强行读取那样会引入安全风险。8. 生产环境最佳实践与扩展方向8.1 生产级技能检查清单检查项说明有明确的触发和排除条件description 能直接指导模型选型输出格式可机器解析stdout 只输出 JSON错误也是 JSON有超时和重试运行器统一控制脚本不无限阻塞依赖版本锁定requirements.txt 锁版本镜像内预制权限最小化技能只获取完成任务所需的最小权限有日志和监控记录技能命中、执行耗时、失败原因有回归测试保证脚本升级后行为不漂移技能上线前逐条过一遍比手动测试二十次更可靠。8.2 为技能写自动化测试技能脚本是可测的因为它就是普通 Python 程序。用 pytest 写一个基础验证import json import subprocess import sys from pathlib import Path SCRIPT Path(skills/process-inspector/scripts/inspect_process.py) def test_script_output_is_json_list(): result subprocess.run( [sys.executable, str(SCRIPT), --limit, 3], capture_outputTrue, textTrue, timeout10, ) assert result.returncode 0 data json.loads(result.stdout) assert isinstance(data, list) def test_script_filter_works(): result subprocess.run( [sys.executable, str(SCRIPT), --pattern, python, --limit, 5], capture_outputTrue, textTrue, timeout10, ) data json.loads(result.stdout) for row in data: assert python in row[cmd] or python in row[name]除了测试脚本本身还要做“黄金用例”测试把一组典型用户问题、期望触发技能、期望回答要点存成样例集每次修改提示词或技能文档后跑一遍确保模型行为没有退化。8.3 从学习到生产的能力扩展路线如果你是从零开始接触 Agent可以参考下面这个学习顺序不必追求几天速成但每个阶段都要有可运行的产物阶段目标可运行产物第一阶段掌握大模型 API 调用能完成一次多轮对话第二阶段理解工具调用模型能按 JSON Schema 调用函数第三阶段实现第一个技能能创建、加载、执行技能包第四阶段多技能管理Agent 能根据 description 选择不同技能第五阶段调试和日志能定位技能不调用、输出解析失败等问题第六阶段自动化测试有技能脚本测试和黄金用例集第七阶段部署监控技能运行效率、错误率可观测可回滚继续深入的方向包括把技能做多语言化不只写 Python还可以封装 shell、Node、Go 脚本把技能版本化为独立仓库用包管理工具分发和 MCP 配合用技能组织内部流程用 MCP 连接外部系统在沙箱或容器里执行技能隔离权限和资源。如果看过吴恩达关于 Agent 的公开课或教程会发现他反复强调规划、工具使用、记忆和反思这四个设计维度。Agent Skills 正是其中“工具使用”的落地形态把一个工具的输入、输出、边界和错误处理都写清楚Agent 才能稳定地使用它。动手写一个最小技能包再回到那套方法论对照理解会比只刷 PDF 教程有效得多。对新手最有价值的练习不是一上来设计复杂技能而是把本文的 process-inspector 完整跑通再改成你自己的场景比如查磁盘占用、查天气、读本地文件。跑通一个闭环之后Agent Skills 的整套机制才算真正长在了自己手上。