跳转至

Skills catalog cn

SkillsCatalog 是 Jianmu 技能系统中负责发现、索引、合并与路由所有技能的中央注册表。它解决的核心问题是:当用户在自己的项目中定义了同名技能时,如何确保工作空间版本优先于框架内置版本——实现"零分叉自定义"(override without forking)。本文聚焦 SkillsCatalog 的层级覆盖机制、运行时解析链、可用性门控系统以及候选路由生成流程。

架构定位:SkillsCatalog 在技能系统中的角色

SkillsCatalog 处于技能加载流水线的最前端,向上对接 SkillSet(运行时技能选择门面)和 SkillNode(执行节点),向下驱动 SkillLoader(SKILL.md 解析器)与 SkillTreeLoader(行为树加载器)。它不负责解析文件内容本身,而是负责"发现哪些技能存在、来自哪里、应该用哪个版本、当前环境能否运行"这四个关键决策。

flowchart LR
    A[jianmu.yaml<br/>paths.skills_dir] --> B{SkillsCatalog<br/>resolve_runtime}
    C[PromptRuntimeContext<br/>skills_dir / skills_catalog] --> B
    D["cwd/skills/<br/>(convention)"] --> B
    B --> E["_collect_records()<br/>合并 workspace + builtin"]
    E --> F[list_skills / get_skill]
    E --> G[build_candidates]
    E --> H[build_skills_summary]
    F --> I[SkillSet.resolve]
    G --> J[SkillNode 路由选择]
    H --> K[LLM Prompt 上下文]

双源合并与覆盖语义

SkillsCatalog 管理两类技能来源,优先级通过 _collect_records() 中的字典覆盖实现——工作空间源先写入,内置源仅填充尚未存在的键:

来源 标签 (source) 典型路径 优先级
workspace "workspace" 用户在 jianmu.yaml 中配置的 paths.skills_dir,或 PromptRuntimeContext.skills_dir,或 ./skills 高(覆盖)
builtin "builtin" 框架打包目录 jianmu/skill/builtin/ 低(回退)

合并的核心逻辑位于 _collect_records():先遍历 workspace_skills_dir 下的一级子目录(每个子目录代表一个技能,其中包含 SKILL.md),将其注册到以技能名小写为键的字典中;再遍历 builtin_skills_dir,仅当键不存在时才补充写入。这意味着同名技能优先使用工作空间版本,源码标签也相应标记为 "workspace" 或 "builtin"。

具体的覆盖行为在测试中得到精确验证:当 workspace 和 builtin 同时存在名为 demo 的技能时,最终 records 列表长度为 1,且 source == "workspace",description 为工作空间版本的内容。

运行时解析链:从配置到 Catalog 实例

SkillsCatalog.resolve_runtime() 类方法实现了一条四级优先级链,从最明确的覆盖一路回退到约定位置:

flowchart TD
    A["resolve_runtime(PromptRuntimeContext)"] --> B{runtime_prompt.skills_catalog<br/>已存在?}
    B -->|是| C["直接返回该 catalog(最高优先级)"]
    B -->|否| D{runtime_prompt.skills_dir<br/>已配置?}
    D -->|是| E["创建 catalog(workspace_skills_dir=该路径)"]
    D -->|否| F["读取 get_config().paths.skills_dir<br/>(静态 jianmu.yaml 配置)"]
    F --> G{值存在?}
    G -->|是| E
    G -->|否| H["检查 cwd/skills/ 是否存在"]
    H --> I{存在?}
    I -->|是| E
    I -->|否| J["返回 None"]

这一设计允许在 Agent 运行时层面覆盖技能目录——例如通过 PromptRuntimeContext(skills_dir="/custom/path") 注入,使同一套 Agent 代码在不同部署环境中加载不同的技能集。

静态配置路径 paths.skills_dir 在项目的 jianmu.yaml 中声明,默认值为 jianmu/skill/builtin(即框架内置技能目录本身),通过配置加载器将相对路径解析为绝对路径。

SkillRecord:注册表条目的内部结构

SkillRecord 是 SkillsCatalog 内部使用的数据类,记录了每个被发现的技能的完整元信息。其字段覆盖身份、可用性、约束三个维度:

字段 类型 说明
name str 技能名,来自 SKILL.md 的 frontmatter
path str SKILL.md 的绝对路径
source str 来源标签:"workspace" 或 "builtin"
description str 简要描述
available bool 当前环境是否满足运行条件
always bool 是否始终注入 prompt 上下文
requires SkillRequirements 结构化需求声明(bins / env / os)
missing_bins list[str] 缺失的 CLI 二进制
missing_env list[str] 缺失的环境变量
unsupported_os list[str] 不支持的 OS 列表
raw_metadata dict 原始 frontmatter 字典

每条 SkillRecord 由 _build_record() 方法构造,它读取并解析 SKILL.md 的 YAML frontmatter,支持 python-frontmatter 库(优先)和内置正则回退解析两种方式。其中 metadata 字段支持内嵌 JSON 字符串(通过 _parse_metadata_field() 处理),这是兼容 OpenClaw 风格技能的关键——tavily_search 技能就使用了这种嵌套格式。

可用性门控:requires 的三重检查

SkillsCatalog 在构建 SkillRecord 时自动执行环境可用性检查,涉及三类条件:

  1. 二进制依赖(bins):通过 shutil.which(item) 检查 CLI 工具是否在 PATH 中可用。例如 tavily_search 要求 node 可用。
  2. 环境变量(env):通过 os.environ.get(item) 检查。例如 tavily_search 要求 TAVILY_API_KEY 已设置。
  3. 操作系统(os):支持规范化别名(macos/mac/osx → darwin;windows/win32/win → windows),与当前 OS 比对。

三重检查全通过,available 才为 True;任何一项不满足则标记为不可用,并在 missing_reasons_text() 中生成人类可读的原因描述。

下面的测试精确验证了门控行为:定义一个要求缺失 binary、缺失环境变量、和不匹配 OS 的技能,其 available 应为 False,且 missing_reasons_text() 应包含 CLI:、ENV: 和 OS: 前缀的原因字符串。

核心查询与路由 API

SkillsCatalog 对外暴露的查询接口围绕两个维度设计:按名称精确查找和按条件批量筛选。

方法 签名 用途
list_skills(filter_unavailable) → list[SkillRecord] 列出所有已知技能
get_skill(name, filter_unavailable) → SkillRecord \| None 按名称精确查找(大小写不敏感)
resolve_records(names, filter_unavailable) → list[SkillRecord] 按名称列表解析,保留请求顺序去重
select_records_for_context(include_unavailable_skills) → list[SkillRecord] 为 prompt 上下文选择技能记录
get_always_skills() → list[SkillRecord] 获取所有 always=true 且可用的技能

resolve_records() 的关键行为是保留请求顺序并自动去重——如果请求 ["beta", "alpha", "beta"],返回 ["beta", "alpha"]。

技能内容加载与 Prompt 渲染

SkillsCatalog 提供了两个面向 LLM 上下文构建的渲染方法:

build_skills_summary() 生成 XML 风格的技能目录摘要,直接注入 prompt:

<skills>
  <skill available="true">
    <name>web_research</name>
    <description>输入查询需求,调用联网搜索工具...</description>
    <location>/path/to/SKILL.md</location>
    <source>builtin</source>
  </skill>
  <skill available="false">
    <name>tavily_search</name>
    <description>Use Tavily web search...</description>
    <requires>CLI: node, ENV: TAVILY_API_KEY</requires>
  </skill>
</skills>

当 include_unavailable_reasons=True(默认)且技能不可用时,会额外输出 <requires> 标签说明缺失项。

load_skills_for_context() 则用于加载特定技能的完整 SKILL.md 正文(剥离 frontmatter 后),以 ### Skill: {name} 为标题拼接,适合在选定的技能上下文中内联全部指令。

SkillCandidate 与路由候选构建

SkillCandidate 是面向路由选择的数据类,它从 SkillRecord 扩展而来,增加了路由决策所需的字段:

附加字段 说明
execution 执行模式:"prompt" 或 "bt"
inputs / outputs JSON Schema 风格的输入/输出定义
required 必需的输出字段名列表
tools 技能声明的工具名列表
role 关联的 Swarm 角色名
outputs 对路由器和宿主暴露的结构化输出 schema

build_candidates() 方法支持三种输入组合:显式 SKILL.md 文件路径、技能目录扫描、以及按名称过滤。其内部使用 _infer_source() 判断每个候选的来源标签(workspace / builtin / explicit),通过路径相对化来判断文件属于哪个根目录。

build_routing_summary() 则将候选列表渲染为紧凑的路由摘要行:

- deep_research | execution=bt | description=Conduct interactive deep research... | inputs=(none) | outputs=summary, report_path, ...
- web_research | execution=bt | description=输入查询需求... | inputs=query | outputs=result

每种候选按 name | execution | description | inputs | outputs 格式输出一行,可选择性包含路径和缺失需求。

SkillSet:运行时技能选择门面

SkillSet 是 SkillsCatalog 的主要消费者,负责将目录查询结果转换为可执行的 Skill 对象列表。其 resolve() 类方法整合了三条输入通道:

flowchart TD
    A["SkillSet.resolve(...)"] --> B["_resolve_catalog()<br/>确定 SkillsCatalog 实例"]
    B --> C["_resolve_paths()<br/>确定 SKILL.md 文件路径列表"]
    C --> D["SkillLoader.load(path)<br/>逐文件解析为 Skill 对象"]
    D --> E["SkillSet(skills, catalog)"]

_resolve_paths() 中的路径解析顺序为:显式 skill_files 优先 → enabled_skills 按名查找 → 回退到 catalog 的全部可用技能。当 enabled_skills 指定了名称但 catalog 中找不到或技能不可用时,会抛出带有详细信息的 ValueError。

SkillSet 还提供按执行模式分区的方法:prompt_skills 属性返回非 BT 技能,bt_skills 返回 execution="bt" 的技能。SkillNode 会在加载后继续读取 BT 的 orchestration:单个 direct BT 进入 Direct 执行器,其他和多技能集合保留 ReAct 路径。

SkillNode 中的完整集成流程

SkillNode 通过 SkillSet.resolve() 加载技能,再根据集合和 orchestration 构建 Direct BT 或 ReAct 子树:

flowchart TD
    A["_compile_subtree()"] --> B["_load_skills() → SkillSet.resolve()"]
    B --> C{"单个 orchestration: direct BT?"}
    C -->|是| D["Direct BT executor → skill_result"]
    C -->|否| E["create_react_node()<br/>Prompt 上下文 + run_bt_skill"]

多技能场景仍由现有 ReAct 完成选择;选中 direct BT 后,其 skill_result 直接结束 SkillNode,不再触发外层总结 LLM。

内置技能一览

框架打包了 6 个内置技能,位于 jianmu/skill/builtin/,作为开箱即用的能力基线:

技能名 执行模式 关键特征
web_research BT 联网搜索 + 总结输出,通过行为树编排
deep_research BT 深度研究:大纲规划 → 逐项调查 → 报告生成
rock_paper_scissors BT 最小 direct BT Skill 示例,随机返回石头、剪刀或布
tavily_search Prompt Tavily API 搜索,always=true,要求 node + TAVILY_API_KEY
agent_memory Prompt 持久记忆系统,跨会话记忆事实和经验
skill_creator Prompt 技能创建向导,指导如何编写 SKILL.md

快照与缓存

snapshot() 方法为整个目录生成稳定的摘要摘要,包含每个技能的 name、path、source、available、always 状态及其文件的修改时间(mtime),对所有条目排序后计算 SHA1 哈希作为版本标识。这为上层消费者提供了基于内容的缓存失效判断依据——当任何技能文件的 mtime 变化时,版本哈希随之改变。

阅读指引

你已经理解了 SkillsCatalog 如何发现和路由技能。接下来可以深入以下主题: