Skip to content

08. Skills 系统

本章目标

随着我们希望助理完成不同类型的任务(例如写作、总结、翻译),如果我们为每一种场景都写一段超长的系统指令,并把它们一次性全部塞入 System Prompt,这会迅速耗尽我们的上下文窗口,导致每次调用的延迟与 Token 成本剧增。

本章的目标是:实现一个最小可用的 Skills(技能)系统结构,演示如何利用类似 Pi 的 Lazy Skills(惰性加载技能) 思想来精简提示词:

  • 系统初始化时,在主 Prompt 中仅放置技能的名称与一句话简短简介。
  • 引导大模型认识自己的“技能边界”。
  • 演示在基础架构上如何为以后“当用户需要时,动态拉取并注入完整指令(Lazy Loading)”做好设计预留。

核心设计与数据结构

packages/core/src/types/index.ts 中,我们为技能声明了如下简易契约:

ts
/**
 * 技能定义(演示用)
 */
export interface SkillDefinition {
  name: string;
  description: string; // 显示在系统提示词中的一句话大纲介绍
  /**
   * 异步加载函数:用于演示后续如何按需加载完整的长篇指令集
   */
  load: () => Promise<SkillContent> | SkillContent;
  tags?: string[];
}

export interface SkillContent {
  instructions: string; // 技能的完整 System Prompt
  tools?: string[];     // 该技能推荐使用的工具名称列表
  examples?: string[];  // Few-Shot 示例
}

1. 实现 SkillRegistry 注册表

我们编写 packages/core/src/skills/registry.ts,统一收集和管理当前 Demo 启用的技能清单:

ts
import type { SkillDefinition, SkillContent } from "../types/index.js";

export class SkillRegistry {
  private skills = new Map<string, SkillDefinition>();

  register(skill: SkillDefinition) {
    if (this.skills.has(skill.name)) {
      throw new Error(`Skill already registered: ${skill.name}`);
    }
    this.skills.set(skill.name, skill);
  }

  get(name: string): SkillDefinition | undefined {
    return this.skills.get(name);
  }

  list(): SkillDefinition[] {
    return Array.from(this.skills.values());
  }

  /**
   * 将当前所有可用技能的名称与描述,序列化为系统 Prompt 所需的简短大纲列表
   */
  getPromptDescriptions(): string {
    if (this.skills.size === 0) return "";

    const lines = this.list().map(
      (s) => `- ${s.name}: ${s.description}`
    );
    return `你可以使用以下技能(需要时再深入使用):\n${lines.join("\n")}`;
  }

  async loadContent(name: string): Promise<SkillContent | null> {
    const skill = this.skills.get(name);
    if (!skill) return null;
    return await skill.load();
  }
}

2. 注入 Agent 上下文

我们把可用技能的简短大纲拼入 Agent.ts 拼装 System Prompt 的过程中:

ts
// packages/core/src/agent/agent.ts -> run()

if (this.skills) {
  const skillDesc = this.skills.getPromptDescriptions();
  if (skillDesc) {
    systemParts.push(
      `【当前可用技能列表】\n${skillDesc}\n\n请严格基于以上列表回答关于你具备哪些技能的问题。`
    );
  } else {
    systemParts.push(`【当前可用技能列表】\n(空)`);
  }
}

示例技能编写

我们在 packages/core/src/skills/examples/ 中模拟了两个简易的技能配置文件:

1. 写作技能:writing.ts

ts
import type { SkillDefinition } from "../../types/index.js";

export const writingSkill: SkillDefinition = {
  name: "writing",
  description: "帮助用户进行写作、润色、改写和结构化表达",
  tags: ["writing", "content"],
  load: () => ({
    instructions: `
你现在处于「写作助手」模式。请遵循以下原则:
1. 保持用户的原意,只优化表达。
2. 语言简洁有力。
`.trim(),
  }),
};

2. 摘要技能:summary.ts

ts
import type { SkillDefinition } from "../../types/index.js";

export const summarySkill: SkillDefinition = {
  name: "summary",
  description: "帮助用户总结长文、提取要点并生成大纲摘要",
  tags: ["summary", "writing"],
  load: () => ({
    instructions: `
你现在处于「总结助手」模式。请遵循以下原则:
1. 先给出核心结论。
2. 列表给出关键要点。
`.trim(),
  }),
};

测试验证与局限说明

在 CLI 客户端 scripts/chat.ts 中注册并传入 Agent:

ts
const skills = new SkillRegistry();
skills.register(writingSkill);
skills.register(summarySkill);

const agent = new Agent({ llm, tools, memory, skills });

演示效果

当我们在终端里问大模型你会什么时:

你: “你支持哪些技能?” hachimi: 我目前被配置了以下两个技能:

  • writing: 帮助用户进行写作、润色、改写和结构化表达
  • summary: 帮助用户总结长文、提取要点并生成大纲摘要

请问需要使用哪一项服务?

L1 阶段的局限

必须指出的是,本阶段我们仅仅实现了“大纲级系统提示词注入与能力对齐”。以下两个进阶能力在 L1 阶段并未编写,仅留作思考:

  • 动态加载(Dynamic Load):即当用户表示“帮我重写这篇文章”时,程序如何在后台监听到意图,并动态调用 load() 将完整 instructions 追加至对话历史中。
  • 技能与工具链联动:技能目前并未主动接管或限制工具的调用范围。

这些更为复杂的策略,将在以后的新合集中去实现。


本章总结

本章我们:

  • 讲解了 Lazy Skills 的核心价值,防止提示词爆仓。
  • 实现了一个最简的 SkillRegistry 注册模块。
  • 演练了如何向大模型声明其当下的技能范围,使其回复更加克制和准确。