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_file 和 search——没有调用 patch_file 或 write_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 | 无匹配 |
|---|---|---|---|
| 匹配方式 | 显式前缀 | "审查" 关键词 | — |
| 工具调用数 | 15 | 6 | 2 |
| 模型调用数 | 16 | 5 | 3 |
| 创建计划 | 是(4 步) | 否 | 否 |
| 使用工具 | glob + read + run_command | read + search | glob |
| 修改文件 | 尝试但达到上限 | 否(只报告) | 否 |
同样的工具集,同样的 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 具备可扩展的生命周期能力。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。