03 · 技能匹配机制
两种触发方式:显式前缀(/debug)和关键词自动匹配。显式优先,自动兜底。
两种触发方式
用户怎么告诉 agent "我要用哪个 skill"?两种方式:
显式选择: 用户在输入前加 skill 名称作为前缀。例如 /debug 为什么测试失败了。
自动匹配: 系统分析用户输入中的关键词,自动选择最匹配的 skill。例如用户输入"帮我修复这个 bug",系统自动匹配到 debug skill。
匹配优先级:显式 > 自动 > 无 skill。
SkillRegistry 实现
在 src/skills/registry.ts 中实现注册和匹配:
export class SkillRegistry {
private skills = new Map<string, Skill>();
/** 根据用户输入匹配 skill */
match(input: string): SkillMatch | null {
const trimmed = input.trim();
/** 1. 检查显式前缀 */
if (trimmed.startsWith("/")) {
const spaceIndex = trimmed.indexOf(" ");
const skillName = spaceIndex === -1
? trimmed.slice(1)
: trimmed.slice(1, spaceIndex);
const skill = this.skills.get(skillName);
if (skill) {
return { skill, source: "explicit" };
}
}
/** 2. 关键词匹配:统计每个 skill 的匹配数,选最多的 */
const lowerInput = trimmed.toLowerCase();
let bestMatch: { skill: Skill; count: number } | null = null;
for (const skill of this.skills.values()) {
let count = 0;
for (const keyword of skill.keywords) {
if (lowerInput.includes(keyword.toLowerCase())) {
count++;
}
}
if (count > 0 && (!bestMatch || count > bestMatch.count)) {
bestMatch = { skill, count };
}
}
if (bestMatch) {
return { skill: bestMatch.skill, source: "auto" };
}
return null;
}
}显式匹配
用户输入 /debug 测试失败了,解析过程:
- 检测到以
/开头 - 提取 skill 名称:
debug - 在 registry 中查找 → 找到 debug skill
- 返回
{ skill: debugSkill, source: "explicit" }
显式匹配的优势是零歧义——用户明确说了要哪个 skill,系统不需要猜测。
如果用户输入 /unknown test,registry 中没有 "unknown" 这个 skill,显式匹配失败。此时不会回退到自动匹配——用户说了用哪个,如果不存在就直接告知,不要自作主张换一个。
自动匹配
用户输入 帮我修复这个 bug,解析过程:
- 不以
/开头,跳过显式匹配 - 遍历所有 skill,检查关键词匹配:
- debug skill: "bug" 匹配 → count = 1
- review skill: 无匹配 → count = 0
- refactor skill: 无匹配 → count = 0
- debug skill 匹配数最多 → 返回
{ skill: debugSkill, source: "auto" }
多个 skill 都匹配时,选关键词匹配数最多的。 如果用户输入"帮我修复这个 bug 并做 code review",debug 匹配 "bug"(1 个),review 匹配 "review"(1 个),两者平局。这种情况下返回第一个达到最高匹配数的 skill(Map 的迭代顺序即注册顺序)。
剥离 skill 前缀
显式选择 skill 后,需要把前缀从用户输入中剥离,只保留纯任务描述传给模型:
stripSkillPrefix(input: string): string {
const trimmed = input.trim();
if (trimmed.startsWith("/")) {
const spaceIndex = trimmed.indexOf(" ");
if (spaceIndex !== -1) {
const skillName = trimmed.slice(1, spaceIndex);
if (this.skills.has(skillName)) {
return trimmed.slice(spaceIndex + 1).trim();
}
}
}
return input;
}/debug 为什么测试失败了 → 为什么测试失败了。剥离后的纯任务描述作为 state.task 传给 agent。模型不需要看到 /debug 前缀——它已经通过 skill 的 instructions 知道自己在 debug 模式。
在 main.ts 中的集成
main.ts 中,每一轮用户输入都会尝试匹配 skill:
const skillMatch = skillRegistry.match(input);
const taskInput = skillMatch
? skillRegistry.stripSkillPrefix(input)
: input;
if (skillMatch) {
console.log(`使用技能: ${skillMatch.skill.name} (${skillMatch.source === "explicit" ? "显式选择" : "自动匹配"})`);
}
const result = await runAgent(state, model, tools, {
// ...
skill: skillMatch?.skill,
});用户还输入 /skills 可以查看所有可用技能:
if (input.trim() === "/skills") {
console.log(skillRegistry.formatHelpText() + "\n");
continue;
}formatHelpText() 输出类似:
可用技能:
/debug - 定位和修复 bug
/review - 代码审查
/refactor - 代码重构
用法: /技能名 任务描述
示例: /debug 为什么测试失败了下一节讲解 skill 注入到 agent 循环的方式。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。