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 时自动执行环境可用性检查,涉及三类条件:
- 二进制依赖(bins):通过
shutil.which(item)检查 CLI 工具是否在 PATH 中可用。例如tavily_search要求node可用。 - 环境变量(env):通过
os.environ.get(item)检查。例如tavily_search要求TAVILY_API_KEY已设置。 - 操作系统(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 如何发现和路由技能。接下来可以深入以下主题:
- Skill 定义与目录:SKILL.md 解析、行为树技能与 Prompt 技能 — 了解 SkillLoader 如何解析 SKILL.md 的 frontmatter 和正文
- 工具与技能节点:ToolExecutor、SkillNode 与约束联动 — 深入 SkillNode 的执行流水线和工具解析
- ReAct 节点工厂:LLM 调用 → 工具执行 → 完成的循环回路 — 理解 prompt 技能在 ReAct 循环中如何被执行
- 上下文构建器:消息过滤、Token 预算控制与多源 Prompt 装配 — 了解技能摘要如何注入 LLM 上下文