ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent Skills实战:构建可复用的LLM技能注册与调用框架

Agent Skills实战:构建可复用的LLM技能注册与调用框架 最近大模型圈子有一个很明显的变化大家不再只盯着“模型参数涨了多少”而是开始关心“Agent 到底能不能稳定干完一件完整的事”。而在 Agent 工程化这条路上Agent Skills 是我认为非常值得投入时间理解的一个方向。它不像新模型架构那样难啃也不像提示词工程那样玄学它更像一种工程组织方式把 Agent 需要的能力封装成可注册、可复用、可共享的“技能”。我第一次接触 Agent Skills 时最大的感受是这个概念终于把 Agent 开发从“写一大段 prompt 临时函数”往前推了一步。很多人会把 Agent Skills 和 Tool、Function Calling、MCP 搞混也有文章说 Agent Skills 就是“带提示词的函数”这话只对了一半。它真正的难点在于怎么设计一套技能规范让 LLM 知道什么时候该用哪个技能、技能输入输出长什么样、技能失败以后怎么处理。这篇文章我会从概念讲起然后直接进入代码实战手把手带你实现一个本地可运行的 Skills 注册与调用框架并给出两个完整示例。读完你至少能获得三样东西第一搞清楚 Agent Skills 和 Tool、MCP、Workflow 的边界第二理解技能描述Skill Description在 Agent 调用中的关键作用第三跑通一个最小可用的 Skills 工程并知道常见的坑在哪里。1. Agent Skills 到底解决了什么问题先回到一个真实的开发痛点。假设你想做一个“代码巡检 Agent”它能拉取代码、分析质量、输出报告。第一版你直接把所有逻辑写在一个 Python 文件里prompt 里塞满各种指令效果还行。但很快你会遇到三个问题每次要加一个新能力比如加一个安全扫描都要改主流程代码重新测试主文件越来越臃肿。prompt 和代码耦合在一起换一个模型同一个 prompt 效果可能完全不同排错非常痛苦。团队里另一个项目也想复用“代码质量分析”这个能力但你只能复制粘贴改一处崩三处。Agent Skills 的出发点就是把“Agent 能做什么事”拆成一个个独立的、可描述的、带实现代码的模块。每个技能包含两部分一份给人看也给模型看的技能说明一个可执行的实现。Agent 在运行时会根据用户请求动态决定要加载哪个技能、如何传参。这件事带给工程最大的变化是Agent 的能力边界从“写死在代码里”变成“运行时可发现”。你可以像管理依赖库一样管理 Agent 的能力添加、升级、替换某个技能不需要重写整个系统。尤其是大模型应用进入生产阶段后稳定性比花哨更重要。Skills 这种设计天然适合做隔离和灰度新增技能先在小范围 Agent 上验证稳定后再推广到所有 Agent。这一步的价值只有真正维护过一套线上 Agent 系统的人才会懂。2. Agent Skills 的核心概念与适用场景要理解 Agent Skills先建立一个最小模型一个 Skill技能本质上是一个“接口约定 文档 实现”的组合。接口约定这个技能接收什么参数返回什么结果。文档给 LLM 看的自然语言描述说明技能用途、使用场景、注意点。实现真正执行任务的代码可能是本地函数、HTTP 调用、SQL 查询、shell 命令等。这三个部分缺一不可。很多人在实现时只写代码和注释忘记写“给模型看的描述”结果 Agent 根本不知道什么时候应该调用这个技能这是最常见的设计失误。适用场景方面Agent Skills 特别适合以下四类需求任务边界清晰、需要重复执行的操作比如 URL 健康检查、文本摘要、日志格式转换。需要版本管理、团队共享的通用能力比如公司统一的脱敏工具、数据统计逻辑。希望快速扩展 Agent 能力又不想改主程序代码的场景。多 Agent 协作场景不同 Agent 复用同一套技能库。不适合的场景也要说清楚如果只是单个简单 API 封装用传统 Function Calling 就够了不必上 Skills 框架如果你的任务高度依赖运行时编排有复杂状态流转那可能更适合 Workflow 而不是 Skills。2.1 最容易混淆的三个概念Tool、MCP、Skill很多读者会把这三个概念混在一起。这里用一个表做对比概念回答的问题典型形态侧重点ToolAgent 能调用什么外部能力函数、API 封装可执行性MCPAgent 如何标准化连接外部工具和数据协议、服务端与客户端互操作性Agent SkillAgent 如何组织复用一项完整能力描述文档 实现代码 元信息可复用性与自主决策一句话总结Tool 是能力的执行单元MCP 是能力的传输协议Skill 是能力的封装与组织方式。它们不是替代关系而是可以组合使用。一个 Skill 内部的实现完全可以通过 MCP 去调用外部服务两者并不冲突。2.2 吴恩达教程带火的“技能化”思路Agent Skills 这个概念能快速出圈和吴恩达Andrew Ng在公开教程中的推广关系很大。在他发布的系列教程中他反复强调一个观点我们应该把 Agent 开发从“写一大段 prompt 临时函数”升级为“构建可复用的技能库”就像软件工程师维护函数库一样维护 Agent 的能力。这个观点的价值在于改变了开发者的心智模型不要每次都从零开始教模型做事而是把经常要做的事变成标准技能让模型在需要时“加载”它们。这种做法也让团队协作变得简单技能可以作为独立产物进行评审、测试和版本记录而不是散落在某个 Agent 的 prompt 里。3. 环境准备与最小项目结构这一节开始进入实战。为了让示例足够简单又能说明问题我们不依赖任何重型框架就用 Python 标准库加少量第三方包实现一个最小的 Skills 注册与调用框架。建议环境如下版本请以你本机实际环境为准Python 3.10 及以上。pip 或 uv 作为依赖管理工具。一个能运行 Python 的终端。网络环境可访问 PyPI。先创建项目目录结构如下agent-skills-demo/ ├── skills/ │ ├── url_health_check/ │ │ ├── SKILL.md │ │ └── impl.py │ └── log_summarizer/ │ ├── SKILL.md │ └── impl.py ├── skill_registry.py ├── demo.py └── README.md目录约定说明skills/目录存放所有技能一个技能一个子目录。SKILL.md是技能描述文件同时给人类和 LLM 阅读。impl.py是技能实现统一暴露一个run函数。skill_registry.py负责扫描并加载技能。demo.py是调用演示入口。这种“一个目录一个技能”的约定是从目前主流 Agent Skills 实现中总结出的通用思路。好处是技能可以独立打包、独立测试、方便后续演进为远程技能库。4. 核心流程拆解技能注册、加载与调用抛开具体框架一个技能系统跑起来需要经过四个阶段。4.1 注册把技能元信息登记到系统注册阶段要做的事是让系统知道存在一个技能并记录它的名称、描述、入口函数。在我们的实现里只要把技能目录放到skills/下并确保包含SKILL.md和impl.py就会被识别为可用技能。这是一种基于文件约定的自动注册优点是简单、直观、很容易接入 CI 检查。4.2 加载把声明和实现关联起来加载阶段需要做两件事第一解析SKILL.md中的元信息得到技能名称、描述、版本。 第二动态导入impl.py拿到真正的执行函数把两者绑定成一个Skill对象。这里要特别提醒动态加载代码本质上是在执行文件中的 Python 代码。在真实项目中技能可能来自第三方你必须确认技能来源可信或者在一个隔离环境比如 Docker 容器中运行避免恶意代码进入主进程。4.3 调用按参数执行并返回结果调用阶段最关键。调用方需要按技能声明的参数签名去传参并且对异常情况做兜底。一个好的技能实现应该做到参数缺失时报错清晰执行失败时返回结构化错误而不是直接抛出让主程序崩溃的异常。4.4 编排让 LLM 决定用哪个技能真正的 Agent 场景中调用哪个技能不是程序员在代码里写死的而是由 LLM 根据用户意图决定。常见做法是把技能列表的描述部分发给模型模型返回要调用的技能名和参数然后由代码侧执行并回填结果。这也是为什么SKILL.md中的描述质量直接决定 Agent 的调用准确率。5. 完整示例实现两个可用技能并组合调用现在进入代码环节。我会逐步给出完整代码复制到自己的项目中就能跑通。5.1 第一个技能URL 健康检查先创建skills/url_health_check/SKILL.md--- name: url_health_check description: 批量检查 URL 的可访问性返回每个 URL 的状态码、响应时间和是否正常。适用于接口巡检、可用性监控、部署后验证。 version: 1.0.0 author: dev-team parameters: - name: urls type: list[string] required: true description: 需要检查的 URL 列表 - name: timeout type: number required: false default: 5 description: 单次请求超时时间单位秒 returns: - name: results type: list[object] description: 每个 URL 的检查结果包含 url、status_code、elapsed_ms、ok 字段再创建skills/url_health_check/impl.py# 文件路径skills/url_health_check/impl.py import time from typing import Any, Dict, List import httpx def run(urls: List[str], timeout: float 5.0) - List[Dict[str, Any]]: 批量检查 URL 健康状态。 Args: urls: 目标 URL 列表。 timeout: 单次请求超时时间单位秒。 Returns: 包含 url、status_code、elapsed_ms、ok、error 字段的结果列表。 results [] with httpx.Client(timeouttimeout, follow_redirectsTrue) as client: for url in urls: start time.time() try: response client.get(url) results.append( { url: url, status_code: response.status_code, elapsed_ms: int((time.time() - start) * 1000), ok: response.status_code 400, } ) except Exception as exc: # 网络错误、超时等统一兜底 results.append( { url: url, status_code: None, elapsed_ms: int((time.time() - start) * 1000), ok: False, error: str(exc), } ) return results这段代码有两点值得注意。第一把网络异常兜在run函数内部不让异常直接冒泡到调用方。对于 Agent 场景来说技能返回结构化错误比抛异常更友好模型可以根据错误信息决定是重试还是给用户解释。第二函数签名和SKILL.md中的参数声明保持一致。未来如果接入 LLM 自动填参模型会依赖这份声明来生成参数两边一旦不一致就会出现“模型传了参数但函数不认”的经典问题。5.2 第二个技能日志摘要再创建skills/log_summarizer/SKILL.md--- name: log_summarizer description: 从多行日志文本中提取关键信息统计 ERROR、WARN、INFO 级别日志数量并返回每个级别的代表性片段。适用于日志巡检和问题定位。 version: 1.0.0 author: dev-team parameters: - name: log_text type: string required: true description: 原始日志文本按换行分隔 - name: top_k type: number required: false default: 3 description: 每个级别最多返回的代表性日志行数 returns: - name: summary type: object description: 包含统计结果和代表性日志片段的摘要对象再创建skills/log_summarizer/impl.py# 文件路径skills/log_summarizer/impl.py import re from collections import Counter from typing import Any, Dict def run(log_text: str, top_k: int 3) - Dict[str, Any]: 从日志文本中提取关键信息并生成摘要。 Args: log_text: 原始日志文本。 top_k: 每个级别返回的代表性日志行数。 Returns: 包含 total、level_counts、samples 的摘要对象。 lines [line.strip() for line in log_text.splitlines() if line.strip()] level_pattern re.compile(r\b(ERROR|WARN|INFO|DEBUG)\b) level_counts Counter() level_samples {} for line in lines: match level_pattern.search(line) level match.group(1) if match else UNKNOWN level_counts[level] 1 if level not in level_samples: level_samples[level] [] if len(level_samples[level]) top_k: level_samples[level].append(line) return { total: len(lines), level_counts: dict(level_counts), samples: level_samples, }这个技能演示了一个常见场景把非结构化的日志文本变成结构化摘要方便上层 Agent 进一步判断系统是否有异常。如果后面要接 LLM 生成报告模型拿到的就是干净的 JSON而不是原始日志。5.3 技能注册器自动扫描并加载技能创建根目录的skill_registry.py# 文件路径skill_registry.py import importlib.util from pathlib import Path from typing import Callable, Dict ALLOWED_KEYS {name, description, version} class Skill: 一个可复用的 Agent 技能。 def __init__( self, name: str, description: str, version: str, handler: Callable, ) - None: self.name name self.description description self.version version self.handler handler def invoke(self, *args, **kwargs): 调用技能实现并保留异常兜底。 try: return self.handler(*args, **kwargs) except Exception as exc: return {ok: False, error: f{self.name} 执行失败: {exc}} def describe(self) - str: 生成给 LLM 看的技能描述。 return ( f技能名称: {self.name}\n f技能版本: {self.version}\n f技能说明: {self.description} ) class SkillRegistry: 扫描 skills 目录并加载所有可用技能。 def __init__(self, skills_dir: str ./skills) - None: self.skills_dir Path(skills_dir) self.skills: Dict[str, Skill] {} def load_all(self) - Dict[str, Skill]: if not self.skills_dir.exists(): raise FileNotFoundError(f技能目录不存在: {self.skills_dir}) for skill_dir in self.skills_dir.iterdir(): if not skill_dir.is_dir(): continue manifest_path skill_dir / SKILL.md impl_path skill_dir / impl.py if not manifest_path.exists() or not impl_path.exists(): print(f[警告] 跳过 {skill_dir.name}缺少 SKILL.md 或 impl.py) continue metadata self._parse_manifest(manifest_path) module self._load_impl(impl_path, metadata[name]) if module is None or not hasattr(module, run): print(f[警告] 跳过 {skill_dir.name}impl.py 未提供 run 函数) continue self.skills[metadata[name]] Skill( namemetadata[name], descriptionmetadata[description], versionmetadata[version], handlermodule.run, ) return self.skills def list_skills(self) - None: for name, skill in self.skills.items(): print(f- {name} v{skill.version}: {skill.description}) def _parse_manifest(self, path: Path) - Dict[str, str]: 从 SKILL.md 中解析元信息只保留 name、description、version。 metadata: Dict[str, str] {} text path.read_text(encodingutf-8) for line in text.splitlines(): stripped line.strip() if not stripped or stripped.startswith(#) or stripped.startswith(-): continue if : in stripped: key, value stripped.split(:, 1) key key.strip() if key in ALLOWED_KEYS: metadata[key] value.strip() return metadata def _load_impl(self, path: Path, module_name: str): 动态加载技能的 Python 实现文件。 spec importlib.util.spec_from_file_location(fskill_{module_name}, path) if spec is None or spec.loader is None: return None module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module这里的关键是_parse_manifest和_load_impl两个方法。前者把SKILL.md的元信息解析成字典后者用 Python 的importlib动态加载技能实现。这套机制保证了“新增一个技能目录系统就能自动识别”不需要改注册器代码。_parse_manifest只保留name、description、version三个字段刻意忽略参数细节。这是为了让教程示例保持简单真实项目中参数部分通常用 JSON Schema 完整解析方便做参数校验。5.4 演示入口组合调用两个技能创建根目录的demo.py# 文件路径demo.py from skill_registry import SkillRegistry SAMPLE_LOG 2026-01-05 10:00:01 INFO 订单服务启动成功 2026-01-05 10:00:03 INFO 数据库连接池初始化完成 2026-01-05 10:00:05 ERROR 库存扣减失败: 库存不足, order_id10086 2026-01-05 10:00:06 WARN 重试机制触发: 第 1 次重试 2026-01-05 10:00:08 ERROR 支付回调超时: transaction_idTX20260105001 2026-01-05 10:00:10 INFO 健康检查通过 def main() - None: registry SkillRegistry() registry.load_all() print( * 50) print(已加载的技能列表:) registry.list_skills() print( * 50) if url_health_check in registry.skills: print(调用 url_health_check 技能:) results registry.skills[url_health_check].invoke( urls[ https://www.csdn.net, https://www.baidu.com, https://not-exists-example.com, ], timeout5, ) for result in results: print(result) print( * 50) if log_summarizer in registry.skills: print(调用 log_summarizer 技能:) summary registry.skills[log_summarizer].invoke( log_textSAMPLE_LOG, top_k2 ) print(summary) if __name__ __main__: main()这个文件演示了两件事一是查看系统当前加载了哪些技能二是分别调用技能并打印结果。注意调用顺序先load_all()再调用如果技能没加载成功registry.skills里就找不到对应名字。6. 运行验证与结果分析先安装依赖并运行pip install httpx python demo.py预期输出格式如下具体耗时会因网络环境不同而变动 已加载的技能列表: - url_health_check v1.0.0: 批量检查 URL 的可访问性返回每个 URL 的状态码、响应时间和是否正常。 - log_summarizer v1.0.0: 从多行日志文本中提取关键信息统计 ERROR、WARN、INFO 级别日志数量。 调用 url_health_check 技能: {url: https://www.csdn.net, status_code: 200, elapsed_ms: 142, ok: True} {url: https://www.baidu.com, status_code: 200, elapsed_ms: 89, ok: True} {url: https://not-exists-example.com, status_code: None, elapsed_ms: 5002, ok: False, error: ...} 调用 log_summarizer 技能: {total: 6, level_counts: {INFO: 3, ERROR: 2, WARN: 1}, samples: {...}}判断成功的标准有三个。第一技能列表能完整打印出两个技能说明自动注册和加载链路没问题。第二URL 检查技能对正常域名返回 200对不存在的域名返回结构化错误说明异常兜底生效。第三日志摘要技能正确统计了各级别日志数量说明参数解析和实现逻辑正确。如果输出为空优先检查两个位置当前工作目录是不是agent-skills-demo以及skills/子目录的文件名是否和示例完全一致。这两个问题占了新手踩坑的一半以上。7. 常见问题与排查方法在实现 Agent Skills 的过程中下面几个问题出现频率很高。问题现象可能原因排查方式解决方案技能目录加载不出来当前工作目录不对或技能目录命名不规范在代码中打印Path.cwd()确认目录在项目根目录运行python demo.py动态加载报错ModuleNotFoundError技能实现依赖了未安装的第三方库查看完整异常堆栈在项目级统一安装依赖或为技能声明依赖清单Agent 从不调用某个技能SKILL.md描述不够清晰与用户意图匹配度低把技能描述单独发给模型看模型能否判断何时调用重写描述加入使用场景和输入输出示例技能参数传错调用方未按SKILL.md参数约定传参对比参数名和类型为技能补充参数校验或在 impl.py 开头加类型检查技能执行抛异常导致主程序崩溃实现中没有兜底异常在注册器的invoke入口统一捕获使用Skill.invoke的统一 try-except 兜底多个技能之间状态互相污染全局变量或共享缓存导致状态串扰检查 impl.py 中是否有模块级可变对象将状态限制在函数内部技能保持无状态这里重点提醒一条SKILL.md并不是可有可无的文档它直接决定了 LLM 的调用准确率。很多团队把精力全部花在实现代码上最后发现 Agent 根本不会触发技能问题往往就出在描述写得像功能清单没有说明“什么时候用”“输入长什么样”“失败怎么办”。8. 最佳实践与工程建议8.1 技能设计原则单一职责一个技能只做一件事。如果一个技能里既要做 URL 检查又要做日志摘要就应该拆开。输入输出结构化尽量使用 JSON 友好的数据结构和明确字段方便上层 Agent 解析。无状态优先技能内部不要依赖模块级全局状态保持可重复执行。描述要面向模型写技能描述时把自己想象成第一次看到这段文字的 LLM问一句“我知道什么时候调它吗”。8.2 工程落地建议在团队项目里引入 Agent Skills 时建议按下面的节奏推进第一阶段只实现 2 到 3 个高频技能跑通注册、加载、调用全链路。第二阶段引入版本号技能升级走类似依赖库的流程记录变更。第三阶段在技能实现外层加日志和指标统计调用次数、成功率、平均耗时。第四阶段如果技能越来越多考虑把技能仓库单独拆出来做成团队内部可检索的技能市场。8.3 安全边界动态加载技能代码是高权限操作。代码来自不可信来源时必须做隔离。推荐做法确认技能来源可信之后再做动态加载。在 Docker 容器中运行技能限制网络和文件系统访问。对技能能访问的 API Key、数据库凭据做最小权限管理。对技能输出做校验防止数据泄漏或格式污染。9. 下一步学习方向到这里你已经拥有一个可以运行的 Agent Skills 最小框架。但距离生产级应用还有几个值得继续深入的方向。第一把技能列表接入 LLM。写一个函数把registry.list_skills()的完整描述发给模型让模型返回{skill: url_health_check, arguments: {...}}结构然后代码侧执行并回填结果这就是一个最简 ReAct Agent 的雏形。第二研究更完整的技能实现。社区里已经有很多 Agent Skills 相关框架核心思路都包含“技能清单 实现 描述”但更完善的实现还支持参数 JSON Schema 校验、依赖声明、远程技能仓库等能力。读这些框架源码时重点关注它们的技能加载协议和描述模板。第三把技能和模型评估结合。不要只关心技能功能是否正确还要关心“模型是否在正确时机调用技能”。建议构造一组包含正例和负例的测试用例用它们衡量技能描述的清晰度。Agent Skills 的核心不是代码写得有多高级而是它逼着你用工程化的方式思考 Agent 的能力设计。先把一套小框架跑起来再去吸收更复杂的方案你会很快发现好技能和坏技能的差别往往不在实现逻辑而在描述是否让模型“看得懂、用得对”。
返回列表