Skills)
0x00 概要OpenClaw 应该有40万行代码阅读理解起来难度过大因此本系列通过Nanobot来学习 OpenClaw 的特色。Nanobot是由香港大学数据科学实验室(HKUDS)开源的超轻量级个人 AI 助手框架定位为Ultra-Lightweight OpenClaw。非常适合学习Agent架构。Skill技能是将特定领域的专业知识、工作流程和最佳实践打包成可复用的指令模块嵌入到 AI 的上下文中使 AI 在处理相关任务时能够自动激活并运用这些专业能力。简单来说Skill 就是一个教 AI 怎么干活的 技能手册。通过将领域知识、标准流程与执行工具封装为可插拔的模块Skill让智能体首次实现了专业能力的沉淀、共享与持续优化从而真正从“自动化执行”迈向“专业化思考与行动”。Skill 的本质是把「隐性知识」变成「显性规程」。注本系列借鉴的文章过多可能在参考文献中有遗漏的文章如果有还请大家指出。0x01 原理本部分很多内容Function Calling的本质LLM要实现工具调用实际上最需要的内容只有函数名、参数要求以及一个description。然而在Function Calling的框架下用户面临几个问题如何融入到现有的系统中。如何在尽可能的准确的前提下能让用户能用文字而非代码指导模型按照特定的流程和规则执行任务所有知识都写入prompt会造成上下文爆炸。1.2 解决方案上述问题的解决方案就是MCP和Skills。MCP和Skills都是基于Function Calling的它们只是在Function Calling这个基础能力之上的不同应用方式。MCP让AI“能做到”执行工具读写数据。Skill教AI“怎么做”是给AI的操作手册告诉模型一步一步 How to do包括流程、标准、策略脚本代码Skills 不是 “训练进模型”而是写进 System Prompt构建上下文时自动读进去LLM 靠阅读理解就会用。1.2.1 如何融入到现有的系统中MCP是一种用于规范大模型与外部能力交互方式的协议。如果说 Tools 解决的是“模型如何调用一个函数”那么 MCP 解决的是“模型如何与一个长期存在、可复用的能力服务交互”。MCP的核心是解决与既有系统的接驳问题MCP的价值在于它提供了一套标准化的接驳协议让不同的工具和数据源能够以统一的方式被LLM使用。本质上MCP更偏重是一套接驳标准只是在Function Calling的基础上增加了一层JSON-RPC协议转换而不是唯一的接驳方式。或者说MCP 更像是 APIAgent 只关心该 API 提交什么「参数」、得到什么「结果」。1.2.2 如何指导模型按照特定的流程和规则执行任务Skills提供一个方式让用户可以用文字定义指令、脚本和资源形成可复用的任务流程。Skills则实际上是一个sub-agent的包装Skills只是在Function Calling之上的一个巧妙应用把加载文档这个操作封装成一个函数让模型在需要时自动去查找和加载固定的skills文档。Skills 让用户可以用文字来写流程替代了过去在MCP调用的函数里用代码写的一套串接各种API的逻辑流程。这种方式可以增强流程的环境适应性——因为模型可以根据实际情况动态调整策略处理不确定性理解自然语言意图。Skills能支持更复杂的流程定义——通过SKILL.md文档告知LLM如何组合多个接口调用所以长流程任务的成功率会更高。1.2.3 如何解决上下文爆炸可以使用双层加载范式。第一层: 系统提示中放技能名称 (低成本)。第二层: tool_result 中按需放完整内容。System prompt (Layer 1 -- always present): -------------------------------------- | You are a coding agent. | | Skills available: | | - git: Git workflow helpers | ~100 tokens/skill | - test: Testing best practices | -------------------------------------- When model calls load_skill(git): -------------------------------------- | tool_result (Layer 2 -- on demand): | | skill namegit | | Full git workflow instructions... | ~2000 tokens | Step 1: ... | | /skill | --------------------------------------1.3 Skills大模型的经验库Skill 可复用的工作方法就是把专家经验固化下来让每次执行都按最佳实践来即把人类经验性的任务变成 AI 可复用的模块。Skills是项目保持精简的扩展机制。不是把所有可能的功能都塞进核心代码库而是把新能力定义为 SKILL.md 文件——用 Markdown 写的指令文档教智能体如何使用特定工具或执行特定工作流。Skill 的本质是Prompt Tools。Prompt: 告诉 LLM “我是谁”、“我能做什么”、“什么时候用我”。Tools: 具体的函数实现通常是 CLI 命令或 API 调用。tool 是能力skill 是方法Tools 是能力的低层接口APItool 更像“手和脚”是 agent 真正可以直接调用的操作能力tool 解决的是“能不能做”。Skills 是任务的高级模板SOP Toolsskill 更像“作战手册”或者“标准流程”。所以skill 解决的是应该怎么做按什么步骤做什么时候用这套流程最合适。特性Function CallingSkills定位原子能力任务模块构成仅包含函数定义JSON Schema包含 Prompt 代码 流程定义关注点我可以调用这个 API我知道如何完成这项工作上下文无状态通常一次性调用有状态通过 Prompt 引导多步推理复用性代码级复用业务逻辑级复用1.3.1 Skill的核心原理Skill的核心在于“模块化封装”与“渐进式披露”。它的出现本质上是工程化手段的伟大创新其核心目标是更高效地释放和驾驭大模型已有的潜力。它并非因为大模型突然变得“更聪明”而是它将完成特定专业任务所需的所有要素知识、流程、工具打包成一个独立模块助力大模型从漫无边际的“流淌”转向目标明确、步骤清晰的“专业执行”。Skill的关键创新在于运行机制智能体在初始时仅预载轻量的技能索引目录。当识别到具体任务需求后才动态加载相应技能的详细指令与资源。这种“按需加载”模式实现了对上下文窗口的极致利用。累积能力Skill 把复杂任务沉淀成可调用、可复用、可组合的“技能模块Agent 的落地正在从单次问答转向可复用能力的积累。攻克专业化匮乏它将隐性的领域专家知识如财务分析SOP、代码审查清单显性化、结构化成为模型可理解和执行的具体指令让通用大模型获得了深度专业能力。它不是在给 AI 增加新的「能力」AI 本身已经很强而是在给 AI 增加领域知识和操作规程——告诉它在这个特定场景下应该怎么做用什么工具遵守什么约束。化解上下文爆炸相比传统上将全部知识塞进提示词或固化在冗长workflow中的方式Skill的“按需加载”机制在复杂任务中可降低40%-60%的上下文消耗。1.3.2 Skills 与架构选型我们思考下当 Single Agent 遇到知识瓶颈时除了拆分成 Multi-Agent还有没有更轻量的路径Skill 机制给出的答案是——回归 Single Agent 架构本体但赋予其极强的动态扩展能力。这种模式让单个 Agent 具备了局部专业化的能力它在宏观上保持统一的记忆和状态微观上却能像调用工具一样灵活掌握成千上万种垂直领域的专业知识。因此架构选择的原则可以明确为能用 Single Agent 解决的绝不上复杂架构。遇到知识瓶颈优先引入 Agent Skills 机制通过动态渐进式加载 Skills 来扩展能力边界。为什么 Skills 能保持上下文一致性我们从 Prompt 工程的角度拆解其机制层级特征作用System Prompt恒定不变核心系统指令人设身份、基础要求保持稳定确保模型认知的统一性User Prompt动态注入Skills 内容以用户输入或工具返回结果的形式渐进式披露这对模型而言意味着什么就像是用户在对话过程中不断提供新的参考资料Reference Material而不是强行改变它的人设。打个比方Multi-Agent 是让模型精神分裂成多个人格轮流登场而 Skills 是让同一个专家在需要时查阅不同的专业手册——人格不变知识动态扩展。Agent Skills 架构的三重收益1. 低成本的知识注入真正实现了将海量领域知识说明书化——模型按需阅读无需全量预加载。这比 Multi-Agent 更轻量也比 RAG 更精准RAG 是检索后被动接收Skills 是主动调用并明确用途。2. 全局上下文一致性由于始终由同一个主 Agent执行类似 Multi-Agent 里的 Orchestrator但没有路由层它完整知晓已执行的步骤已读取的 Agent Skills当前的任务状态这彻底消除了 Multi-Agent 中的信息割裂和重复劳动问题——没有路由错误的风险也没有子 Agent 间的上下文盲区。3. 规避 Context 爆炸通过读一点、做一点、再读一点的流式处理有效控制了瞬时上下文长度。相比 Multi-Agent 每个子节点都要维护独立 System Prompt 的冗余Skills 只在需要时加载用完即释放。本质洞察Skill 用文件系统的结构化能力替代了复杂的网络通信协议用渐进式的信息披露替代了暴力的全量注入。这不是简单的功能替代而是架构哲学的转变从用复杂度换能力走向用编排精度换效率。1.3.3 Skill的结构Skill 不是“一个统一目录里的插件列表”而是一套分层发现、按 workspace 和共享范围组织起来的工作流系统。Skill 通常包含三层结构三级加载机制元数据始终加载技能的描述、功能与接口定义。这些信息永远被加载在上下文约 100 词。AI 用这个判断是否触发 Skill。指令触发时加载结构化的专业操作步骤与判断逻辑。Skill 被触发后才加载建议 500 行。资源按需加载执行所需的数据、模板或工具链接。AI 判断需要时才加载无大小限制因为脚本可以直接执行。比如references/负责放详细说明scripts/负责放真正复用的脚本。所以 skill 不是“只有一个 markdown 文件”而是一组围绕某种任务组织起来的资源或者说能力即文件Capability as Files。复用 资源需求 Skill。下面展示了一个典型的Skill的目录结构通过元数据 → 指令 → 代码与资源这三层结构Skills 本质上只是一种文件结构约定一个 skill 不仅能被 Claude 正确识别和触发还能真正完成从“理解需求”到“执行任务”的完整闭环。some-repository/ ├── AGENTS.md # Permanent repository guidelines (recommended) └── .agents/ └── skills/ ├── rot13-encryption/ # AgentSkills standard (progressive disclosure) │ ├── SKILL.md │ ├── scripts/ │ │ └── rot13.sh │ └── references/ │ └── README.md ├── another-agentskill/ # AgentSkills standard (progressive disclosure) │ ├── SKILL.md │ └── scripts/ │ └── placeholder.sh └── legacy_trigger_this.md # Legacy/OpenHands format (keyword-triggered)1.3.4 Skill vs Workflow vs Prompt与Workflow相比Skill代表了一种根本性的能力升级。其核心区别在于Workflow是预先编排好的、线性的、固定路径的执行流程图确保复杂任务能“稳定地做对”Skill是模块化的、可组合的、按需调用的专业化能力包旨在让智能体能够“专业地做对”。Skill以前者难以实现的知识封装与灵活调度开启了智能体专业化与规模化应用的新阶段。Skill所代表的“能力模块化”与“知识工程化”方向无疑是构建下一代专业化、高可靠智能体的关键路径。Skill 与 Prompt 对比如下维度PromptSkill触发手动粘贴自动匹配能力仅文本文档脚本模板复用低高上下文每次完整加载渐进式加载1.3.5 场景Skill 适合解决的三类问题AI 没有的领域知识重复性工作流需要确定性执行的操作与直接提示词相比SKill强在可复用性和一致性不适合用 Skill 的情况一次性任务你只用这个工作流一次写 Skill 的时间比直接做还长高度个性化每次需求都完全不同没有可以固化的模式AI 已经很擅长通用编码、写作、翻译等AI 本身就有很强的默认行为加了 Skill 反而可能画蛇添足未来很多软件的核心资产可能是一组高质量、可调用、可审计、可复用的 Agent Skills。Agent 的竞争未来很可能会从“谁的模型分数更高”逐渐转向谁更擅长把能力沉淀成技能谁更擅长把技能组织成工作流谁更擅长让这些流程在真实业务里稳定运行。0x02 SkillsLoaderSkillsLoader是 Nanobot 框架中负责智能体技能Skills管理的核心组件核心职责是加载、管理、校验智能体的技能文件以 SKILL.md 为载体为智能体提供可用技能的检索、加载、校验能力是 Agent 具备工具使用 / 任务执行能力的基础支撑模块。SkillsLoader扫描插件ContextBuilder把插件文档塞进上下文LLM照着插件文档做任务2.1 业界范式我们引用 openhands 的文档来看看Skills管理系统的范式。The Skill system has four primary responsibilities:Context Injection- Add specialized prompts to agent context based on triggersTrigger Evaluation- Determine when skills should activate (always, keyword, task)MCP Integration- Load MCP tools associated with repository skillsThird-Party Support- Parse.cursorrules,agents.md, and other skill formats其架构图如下2.2 通用工作流程我们来看看 Skill 的通用工作流程。LLM 使用 Skills 的方式 阅读文档 调用工具。即人负责说清目标和约束Skill 封装实现标准代码/流程Agent 负责理解意图并调度执行。LLM不需要理解具体有哪些工具以及工具的用法是什么只需要在需要使用某个工具时查看工具说明书再把工具拿出来用。Agent 的行为是Progressive Disclosure渐进式批露初始化这样只看 Skill 的 name 和 description, 扫一眼这个工具能干啥Agent 看技能摘要确定 skill 和任务相匹配发现需要用 → 调用read_file读SKILL.md按文档里的步骤执行具体工作流程是这样的初始化阶段用户用文字定义指令、脚本和资源打包成Skills包含SKILL.md和可选的脚本、参考资料等。LLM 在启动时会读取所有Skills的元数据名称和描述这些元数据被加载到模型的上下文中系统提示词Agent 在每次对话开始时就知道自己拥有哪些 Skill但不知道具体内容。用于后续的意图匹配和技能触发判断。发现阶段当用户发起请求时LLM 会根据请求内容对比已加载的Skills元数据判断是否需要使用某个Skill。这个判断过程本质上就是LLM根据上下文做决策跟Function Calling中判断是否需要调用工具是一样的。需要注意两个要点Agent 只在觉得自己搞不定时才找 Skill。简单的一步操作比如读一下这个文件即使 Description 完美匹配也可能不触发因为 Agent 判断自己直接就能完成。Agent 天生偏向欠触发under-triggering。Description 要写得主动一点把边界往外推。加载阶段Function Calling如果 LLM 判断需要某个Skill当用户的请求与某个 Skill 的 description 匹配它会通过Function Calling机制调用一个专门的加载工具类似load_skill(skill_name)将对应的SKILL.md文档内容读取并加载到当前上下文中。这一步完全依赖Function Calling的能力。执行阶段继续使用Function CallingSKILL.md的内容包含指令、流程、示例等被加入到上下文后LLM 按照文档中定义的指令执行任务。如果SKILL.md中定义了需要执行脚本比如scripts/rotate_pdf.pyLLM 还是会通过Function Calling调用执行脚本的函数。如果需要加载参考资料同样是通过Function Calling调用读取文件的函数。可以看到整个Skills的运行过程从加载文档、执行脚本到读取资源每一步都离不开Function Calling。Skills并没有创造新的能力它只是把Function Calling这个基础能力组织成了一个更易用的形式让用户可以用文字定义流程让LLM 自动发现和加载相关知识。 这也意味着 Skill 的激活本身会消耗 1-2 步工具调用。所以 description 写得准不准直接影响 Token 消耗和响应速度误触发意味着浪费漏触发意味着能力缺失。用户消息 ↓ Agent 判断是否需调用某 Skill基于请求内容匹配已加载的 skill_name 与 description ↓ 若需要则通过 Function Calling 调用 load_skill(skill_name) ↓ 将对应 SKILL.md 的内容注入当前上下文作为执行指令依据SOP ↓ Agent 依照 SKILL.md 中定义的流程执行任务 ↓ 在执行过程中按需通过 Function Calling • 调用 bash 执行附带脚本 • 调用 read_file 读取所需资源文件 ↓ 整合执行结果2.3 Nanobot Skill 管理系统核心特色Nanobot Skill 管理系统的核心特色如下双源优先级加载支持「工作区技能workspace」和「内置技能builtin」双来源工作区技能优先级更高避免内置技能被覆盖需求校验机制自动校验技能依赖CLI 工具、环境变量可过滤掉依赖不满足的不可用技能结构化技能管理提供技能列表查询、单技能加载、技能摘要构建XML 格式、常驻技能筛选等能力适配 Agent 上下文加载、渐进式技能使用的需求2.4 总体流程SkillsLoader 相关的总体流程如下。用户输入“每小时检查一次Github星数” ↓ LLM识别上下文 ↓ SkillsLoader.get_always_skills() → 获取激活的技能 ↓ SkillsLoader.load_skills_for_context() → 加载cron技能上下文 ↓ SkillsLoader.bulid_system_prompt() → 构建系统提示 ↓ SkillsLoader._run_agent_loop() → 运行代理循环 ↓ LLMProvider.chat() → LL处理请求 ↓ LLM识别为cron工具调用 ↓ ToolRegistry.execute() → 执行工具 ↓ CronTool.execute() → Cron工具执行 ↓ CronService.add_job() → 添加定时任务 ↓ 任务存储到cron.json文件2.5 Skills.mdSkills 本质 可插拔的插件文档。以nanobot\skills\memory\SKILL.md 为例。上面的三根短横线部分相当于 Skill 的「身份证」name 是它的唯一标识起个简单好记的英文名字就行description 则决定什么时候会触发这个 Skill描述这个 Skills 是做什么的、遇到什么样的用户请求应该用它、提醒读者描述越具体越容易在正确场景被调用。description 包含三层信息这个 Skill 干什么用这个 Skill 的核心价值是什么、核心能力是什么具体包含哪些能力、什么时候触发用户说什么话、做什么操作时应该触发。description 始终存在于 Agent 的系统提示词中作用类似索引当用户输入到来时Agent 拿请求和所有 Skill 的 description 做匹配命中了才加载对应 Skill 的完整内容。这个设计意味着你可以同时挂载几十个 Skill而激活判断的成本只是几十行短文本的比对不需要把所有 Skill 的完整指令都塞进上下文。下面就是 Skill 的正文部分--- name: memory description: Two-layer memory system with grep-based recall. always: true --- # Memory ## Structure - memory/MEMORY.md — Long-term facts (preferences, project context, relationships). Always loaded into your context. - memory/HISTORY.md — Append-only event log. NOT loaded into context. Search it with grep. ## Search Past Events bash grep -i keyword memory/HISTORY.md Use the exec tool to run grep. Combine patterns: grep -iE meeting|deadline memory/HISTORY.md ## When to Update MEMORY.md Write important facts immediately using edit_file or write_file: - User preferences (I prefer dark mode) - Project context (The API uses OAuth2) - Relationships (Alice is the project lead) ## Auto-consolidation Old conversations are automatically summarized and appended to HISTORY.md when the session grows large. Long-term facts are extracted to MEMORY.md. You dont need to manage this.2.6 图例2.6.1 Skills 核心逻辑关系图2.6.2 Skills 核心流程流程图核心依赖关系ContextBuilder 依赖 SkillsLoader 提供技能相关内容是「技能消费方」SkillsLoader 是「技能管理核心」负责加载、校验、格式化技能对接 workspace/builtin 两类技能存储Skill 实体由 SKILL.md 文件定义包含内容 元数据frontmatter元数据决定技能的可用性 / 常驻属性。核心流程闭环初始化路径配置 → 目录校验技能加载清单遍历 → 依赖校验 → 内容加载 → 格式化上下文构建Identity → Bootstrap → Memory → 常驻技能 → 技能摘要 → 最终 Prompt。关键设计优先级workspace 技能 builtin 技能避免同名冲突可用性通过 bins/env 依赖校验过滤不可用技能摘要中明确标注缺失依赖轻量化技能摘要仅返回元数据完整内容需 agent 调用 read_file 工具加载符合渐进式设计常驻技能标记 alwaystrue 的技能直接嵌入上下文提升高频技能调用效率。2.6.3 SkillsLoader 技能元数据解析逻辑2.7 代码class SkillsLoader: Loader for agent skills. Skills are markdown files (SKILL.md) that teach the agent how to use specific tools or perform certain tasks. def __init__(self, workspace: Path, builtin_skills_dir: Path | None None): # 初始化工作区路径用户自定义技能的根目录 self.workspace workspace # 定义工作区技能目录workspace/skills self.workspace_skills workspace / skills # 定义内置技能目录优先使用传入值无则使用默认内置目录 self.builtin_skills builtin_skills_dir or BUILTIN_SKILLS_DIR def list_skills(self, filter_unavailable: bool True) - list[dict[str, str]]: List all available skills. Args: filter_unavailable: If True, filter out skills with unmet requirements. Returns: List of skill info dicts with name, path, source. # 初始化技能列表用于存储所有符合条件的技能信息 skills [] # 第一步加载工作区技能优先级最高 if self.workspace_skills.exists(): # 遍历工作区技能目录下的所有子目录每个子目录对应一个技能 for skill_dir in self.workspace_skills.iterdir(): # 仅处理目录类型排除文件 if skill_dir.is_dir(): # 拼接技能文件路径每个技能目录下必须有SKILL.md skill_file skill_dir / SKILL.md # 仅添加存在SKILL.md文件的技能 if skill_file.exists(): skills.append({ name: skill_dir.name, # 技能名称目录名 path: str(skill_file), # 技能文件的绝对路径 source: workspace # 技能来源工作区 }) # 第二步加载内置技能仅添加未被工作区技能覆盖的 if self.builtin_skills and self.builtin_skills.exists(): # 遍历内置技能目录下的所有子目录 for skill_dir in self.builtin_skills.iterdir(): if skill_dir.is_dir(): skill_file skill_dir / SKILL.md # 条件1. SKILL.md存在 2. 技能名称未在工作区技能中出现避免覆盖 if skill_file.exists() and not any(s[name] skill_dir.name for s in skills): skills.append({ name: skill_dir.name, path: str(skill_file), source: builtin # 技能来源内置 }) # 第三步根据需求过滤不可用技能 if filter_unavailable: # 仅保留满足依赖要求的技能_check_requirements校验依赖 return [s for s in skills if self._check_requirements(self._get_skill_meta(s[name]))] # 不过滤时返回所有技能包含依赖不满足的 return skills def load_skill(self, name: str) - str | None: Load a skill by name. Args: name: Skill name (directory name). Returns: Skill content or None if not found. # 第一步优先从工作区加载技能优先级更高 workspace_skill self.workspace_skills / name / SKILL.md if workspace_skill.exists(): # 读取技能文件内容UTF-8编码保证中文等字符正常 return workspace_skill.read_text(encodingutf-8) # 第二步工作区不存在时从内置目录加载 if self.builtin_skills: builtin_skill self.builtin_skills / name / SKILL.md if builtin_skill.exists(): return builtin_skill.read_text(encodingutf-8) # 技能不存在时返回None return None def load_skills_for_context(self, skill_names: list[str]) - str: Load specific skills for inclusion in agent context. Args: skill_names: List of skill names to load. Returns: Formatted skills content. # 初始化技能内容片段列表用于拼接最终上下文 parts [] # 遍历需要加载的技能名称列表 for name in skill_names: # 加载单个技能的内容 content self.load_skill(name) # 仅处理存在的技能 if content: # 剥离技能文件的YAML前置元数据仅保留核心描述 content self._strip_frontmatter(content) # 按固定格式拼接技能内容便于Agent识别 parts.append(f### Skill: {name}\n\n{content}) # 拼接所有技能内容用分隔线---分隔不同技能无技能时返回空字符串 return \n\n---\n\n.join(parts) if parts else def build_skills_summary(self) - str: Build a summary of all skills (name, description, path, availability). This is used for progressive loading - the agent can read the full skill content using read_file when needed. Returns: XML-formatted skills summary. # 获取所有技能不过滤不可用的 all_skills self.list_skills(filter_unavailableFalse) # 无技能时返回空字符串 if not all_skills: return # 定义XML转义函数避免特殊字符//破坏XML格式 def escape_xml(s: str) - str: return s.replace(, amp;).replace(, lt;).replace(, gt;) # 初始化XML内容行列表构建标准XML格式的技能摘要 lines [skills] # 遍历每个技能生成XML节点 for s in all_skills: # 转义技能名称避免特殊字符 name escape_xml(s[name]) # 技能文件路径 path s[path] # 转义技能描述 desc escape_xml(self._get_skill_description(s[name])) # 获取技能元数据包含依赖要求 skill_meta self._get_skill_meta(s[name]) # 校验技能是否可用依赖是否满足 available self._check_requirements(skill_meta) # 添加单个技能的XML节点available属性标记是否可用 lines.append(f skill available\{str(available).lower()}\) lines.append(f name{name}/name) # 技能名称 lines.append(f description{desc}/description) # 技能描述 lines.append(f location{path}/location) # 技能文件路径 # 对不可用的技能添加缺失的依赖信息 if not available: missing self._get_missing_requirements(skill_meta) if missing: lines.append(f requires{escape_xml(missing)}/requires) # 闭合单个技能节点 lines.append(f /skill) # 闭合根节点 lines.append(/skills) # 拼接所有行生成完整的XML字符串 return \n.join(lines) def _get_missing_requirements(self, skill_meta: dict) - str: Get a description of missing requirements. # 初始化缺失依赖列表 missing [] # 获取技能元数据中的依赖要求requires字段 requires skill_meta.get(requires, {}) # 检查CLI工具依赖遍历需要的CLI工具判断是否存在 for b in requires.get(bins, []): if not shutil.which(b): # shutil.which检查系统是否安装该CLI工具 missing.append(fCLI: {b}) # 检查环境变量依赖遍历需要的环境变量判断是否存在 for env in requires.get(env, []): if not os.environ.get(env): # os.environ.get检查环境变量是否定义 missing.append(fENV: {env}) # 拼接缺失依赖为字符串逗号分隔 return , .join(missing) def _get_skill_description(self, name: str) - str: Get the description of a skill from its frontmatter. # 获取技能的元数据包含description字段 meta self.get_skill_metadata(name) # 有元数据且包含description时返回描述否则返回技能名称兜底 if meta and meta.get(description): return meta[description] return name # Fallback to skill name def _strip_frontmatter(self, content: str) - str: Remove YAML frontmatter from markdown content. # 检查内容是否以YAML前置分隔符---开头 if content.startswith(---): # 正则匹配从开头的---开始到下一个---结束包含换行 # re.DOTALL使.匹配换行符确保完整匹配多行YAML match re.match(r^---\n.*?\n---\n, content, re.DOTALL) if match: # 截取分隔符之后的内容并去除首尾空白 return content[match.end():].strip() # 无YAML前置时直接返回原内容 return content def _parse_nanobot_metadata(self, raw: str) - dict: Parse skill metadata JSON from frontmatter (supports nanobot and openclaw keys). try: # 将原始字符串解析为JSON字典 data json.loads(raw) # 优先取nanobot字段无则取openclaw字段均无则返回空字典 return data.get(nanobot, data.get(openclaw, {})) if isinstance(data, dict) else {} except (json.JSONDecodeError, TypeError): # 解析失败非JSON/类型错误时返回空字典 return {} def _check_requirements(self, skill_meta: dict) - bool: Check if skill requirements are met (bins, env vars). # 获取技能的依赖要求 requires skill_meta.get(requires, {}) # 检查所有CLI工具依赖只要有一个不存在返回False for b in requires.get(bins, []): if not shutil.which(b): return False # 检查所有环境变量依赖只要有一个未定义返回False for env in requires.get(env, []): if not os.environ.get(env): return False # 所有依赖都满足时返回True return True def _get_skill_meta(self, name: str) - dict: Get nanobot metadata for a skill (cached in frontmatter). # 获取技能的原始元数据YAML前置 meta self.get_skill_metadata(name) or {} # 解析并返回nanobot/openclaw格式的元数据 return self._parse_nanobot_metadata(meta.get(metadata, )) def get_always_skills(self) - list[str]: Get skills marked as alwaystrue that meet requirements. # 初始化常驻技能列表alwaystrue的技能会默认加载到Agent上下文 result [] # 遍历所有可用技能已过滤依赖不满足的 for s in self.list_skills(filter_unavailableTrue): # 获取技能的原始元数据 meta self.get_skill_metadata(s[name]) or {} # 解析nanobot/openclaw格式的元数据 skill_meta self._parse_nanobot_metadata(meta.get(metadata, )) # 判断是否标记为常驻1. skill_meta中的always 2. 原始meta中的always兼容两种格式 if skill_meta.get(always) or meta.get(always): result.append(s[name]) # 返回所有常驻技能名称 return result def get_skill_metadata(self, name: str) - dict | None: Get metadata from a skills frontmatter. Args: name: Skill name. Returns: Metadata dict or None. # 加载技能的完整内容 content self.load_skill(name) # 技能不存在时返回None if not content: return None # 检查是否有YAML前置元数据以---开头 if content.startswith(---): # 正则匹配YAML前置内容---之间的部分 match re.match(r^---\n(.*?)\n---, content, re.DOTALL) if match: # 初始化元数据字典 metadata {} # 按行解析YAML内容简易解析仅处理key: value格式 for line in match.group(1).split(\n): if : in line: # 分割键值对仅分割第一个冒号避免值中包含冒号 key, value line.split(:, 1) # 清理键和值的空白、引号存入字典 metadata[key.strip()] value.strip().strip(\) # 返回解析后的元数据