第 7 章 · Skills 与能力模块化

05 · 实战:技能匹配效果

用同一个任务分别触发 debug、review 技能,展示 skill 如何改变 agent 的工作方式。

同一个 agent,不同的工作方式

前三节实现了 Skill 类型、内置技能和 SkillRegistry 匹配器。这一节用真实任务展示 skill 系统的效果——同一个 agent,匹配到不同的 skill 后,会展现出截然不同的工作方式。

Skill 不改变 agent 的工具和循环逻辑,只改变系统提示词。但这足够让模型用完全不同的策略来使用同样的工具。

显式匹配:/debug 前缀

用户输入以 /debug 开头,触发显式匹配:

> /debug search 工具返回了空结果

匹配过程:

registry.match("/debug search 工具返回了空结果")

// 第一步:检查显式前缀
trimmed.startsWith("/") === true
skillName = "debug"
this.skills.get("debug") → debugSkill 存在

// 返回
{ skill: debugSkill, source: "explicit" }

source 为 "explicit",说明是用户主动选择的。此时系统提示词末尾追加了 debug skill 的 instructions,模型按照"理解问题→定位根因→验证假设→修复问题→确认修复"的步骤工作。

真实运行结果:

  使用技能: debug (显式选择)

>> 开始处理: search 工具返回了空结果
  ... 思考中
  > create_todos
  + create_todos: 已创建 4 个步骤
  计划更新:
    [ ] #1 复现问题:查看 search 工具实现并测试
    [ ] #2 定位根因:分析 search 工具代码逻辑
    [ ] #3 实施修复:修改代码解决问题
    [ ] #4 验证修复:测试 search 工具是否正常
  >  执行操作
  > update_todo
  + update_todo: 步骤 #1 "复现问题" → running
  计划更新:
    [>] #1 复现问题:查看 search 工具实现并测试
    [ ] #2 定位根因:分析 search 工具代码逻辑
    [ ] #3 实施修复:修改代码解决问题
    [ ] #4 验证修复:测试 search 工具是否正常
  >  执行操作
  > glob: src/tools/**
  > read_file: src/tools/search.ts
  > read_file: src/types.ts
  > read_file: test/tools/search.test.ts
  > read_file: src/agent.ts
  > run_command: rg --line-number --max-count 20 ...
  > run_command: rg "workingDir" src/agent.ts 2>&1
  > run_command: pnpm test test/tools/search.test.ts 2>&1
  >  执行操作
  > update_todo
  + update_todo: 步骤 #1 "复现问题" → completed
  计划更新:
    [x] #1 复现问题:查看 search 工具实现并测试
    [ ] #2 定位根因:分析 search 工具代码逻辑
    [ ] #3 实施修复:修改代码解决问题
    [ ] #4 验证修复:测试 search 工具是否正常
  >  执行操作
  = 完成 [16次模型调用, 15次工具调用 (达到迭代上限)]

[模型调用: 16次 | 工具调用: 15次]

debug skill 下的 agent 严格按照"复现→定位→修复→验证"的步骤推进。它创建了 4 步计划,标记第一步为 running,读取源码和测试文件,运行实际命令复现问题,然后标记第一步为 completed。由于达到最大迭代次数(15 次),agent 没能走完全部 4 步——但它的行为模式清晰地体现了调试技能的引导。

自动匹配:关键词触发

用户不输入前缀,但描述中包含关键词:

> 帮我审查一下 src/tools/search.ts 的代码质量

匹配过程:

registry.match("帮我审查一下 src/tools/search.ts 的代码质量")

// 第一步:不以 / 开头,跳过显式匹配
// 第二步:关键词匹配
debugSkill.keywords  → 匹配数: 0
reviewSkill.keywords → 匹配数: 1 ("审查")
refactorSkill.keywords → 匹配数: 0

// 返回
{ skill: reviewSkill, source: "auto" }

真实运行结果:

  使用技能: review (自动匹配)

>> 开始处理: 帮我审查一下 src/tools/search.ts 的代码质量
  ... 思考中
  > read_file: src/tools/search.ts
  > read_file: src/types.ts
  > read_file: test/tools/search.test.ts
  > search: searchTool
  > read_file: src/tools/index.ts
  > read_file: src/agent.ts
  >  执行操作
  = 完成 [5次模型调用, 6次工具调用]

review skill 的指令包含"只报告问题,不修改代码"。agent 确实只使用了 read_filesearch——没有调用 patch_filewrite_file。它主动读取了源码、类型定义、测试文件、调用上下文(agent.ts),然后输出了完整的审查报告:

## Code Review: src/tools/search.ts

### ⚠️ Warning — 命令注入风险
第 44 行的 escapeShell 只转义了双引号,但 shell 元字符如 $、`、\ 等也能被利用

### ⚠️ Warning — maxResults 未限制上限
如果模型传入极大值(如 999999),会导致 rg 执行时间很长

### 💡 Suggestion — formatMatchLine 正则不够健壮
当文件路径中包含 : 时会解析错误

### 💡 Suggestion — 缺少对 rg 是否安装的检测
### 💡 Suggestion — 测试覆盖不够完整

[模型调用: 5次 | 工具调用: 6次]

无匹配时的行为

如果输入不匹配任何 skill:

> 这个项目有多少个工具文件?
无技能匹配

>> 开始处理: 这个项目有多少个工具文件?
  > glob: src/tools/*.ts
  + glob: src/tools/create-todos.ts
  >  执行操作
  = 完成 [3次模型调用, 2次工具调用]

这个项目共有 11 个工具文件(含 index.ts 入口)
[模型调用: 3次 | 工具调用: 2次]

没有 skill 匹配时,agent 使用默认系统提示词——简单直接,2 次工具调用就完成了。

三种模式的行为对比

维度/debug/review无匹配
匹配方式显式前缀"审查" 关键词
工具调用数1562
模型调用数1653
创建计划是(4 步)
使用工具glob + read + run_commandread + searchglob
修改文件尝试但达到上限否(只报告)

同样的工具集,同样的 ReAct 循环,但模型在 skill 指令的引导下选择了完全不同的工具组合和工作流程。

前缀剥离

当 skill 匹配成功后,main.ts 会把前缀从用户输入中剥离:

const taskInput = skillMatch
  ? skillRegistry.stripSkillPrefix(input)
  : input;

// "/debug 为什么搜索返回空结果" → "为什么搜索返回空结果"
// "帮我审查一下代码" → "帮我审查一下代码"(无前缀,原样返回)

剥离后的 taskInput 作为 agent 的任务描述传入 runAgent。这样模型看到的是纯任务描述,不会被 /debug 这种前缀干扰。

这一章做了什么

回顾第 7 章的成果:

  • Skill 类型:name、description、keywords、instructions 四个字段
  • 三个内置技能:debug(调试)、review(审查)、refactor(重构),各有独立的指令
  • SkillRegistry:显式匹配(前缀)+ 自动匹配(关键词),优先级明确
  • 前缀剥离/debug 任务 变成纯任务描述传入 agent
  • 系统提示词注入:skill.instructions 追加到 systemParts 数组

agent 从第 6 章的"通用助手"进化为"按任务切换模式的专业工具"。下一章会实现 hook 系统,让 agent 具备可扩展的生命周期能力。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page