从零开始拆解Pi系列——(6)skills 机制

一、引言:什么是 skill

前五篇文章讲了 pi agent 的核心骨架:调 LLM、流式事件、循环、工具、hook。这些机制让 agent 能干活、能扩展。但还有一种扩展方式不需要写代码、不需要注册工具、不需要实现接口------只需要写一个 Markdown 文件。

这就是 skill。

skill 是"按需加载的指令包"。它是一个目录,里面放着一个 SKILL.md 文件和可选的参考文档、脚本。agent 启动时只加载 skill 的名字和描述(约 100 token),写进 system prompt。当用户的任务匹配某个 skill 的描述时,LLM 自己用 read 工具读取 SKILL.md 全文,获取详细指令。如果指令里引用了更多文件,LLM 再按需读取。

这解决了一个核心问题:agent 的能力越多,system prompt 越长。如果所有领域知识都塞进 system prompt------代码审查流程、Git 工作流、部署规范、安全检查清单------token 浪费严重,LLM 注意力被分散。skill 把这些知识拆成独立包,只在需要时加载,不占常驻 context。

和前几种扩展方式的区别:

扩展方式 需要写代码? 加载时机 改 agent loop?
工具(AgentTool) 是(实现 execute) 始终传给 LLM 不改
hook(beforeToolCall 等) 是(实现回调) 固定点调用 不改
skill 否(只写 Markdown) 按需 不改

skill 是最轻量的扩展方式------不改源码、不写代码、不注册工具,仅通过 Markdown 文件就能扩展 agent 的能力边界。

二、skill 文件格式

一个 skill 是一个目录,核心是 SKILL.md,可选附带参考文档和脚本:

bash 复制代码
.pi/skills/
└── my-skill/                     # 目录名 = skill name(必须和 frontmatter 的 name 一致)
    ├── SKILL.md                   # 必需------frontmatter + 指令正文
    ├── references/                # 可选------详细参考文档
    │   ├── api-reference.md       # LLM 按需读取
    │   └── examples.md
    └── scripts/                   # 可选------可执行脚本
        └── validate.py            # LLM 通过 bash 执行,只有输出进 context

SKILL.md 结构

SKILL.md 由两部分组成------YAML frontmatter 和 Markdown 正文:

markdown 复制代码
---
name: my-skill-name
description: Brief description of what this skill does and when to use it
---

# My Skill Name

## Instructions
[Clear, step-by-step guidance for the agent to follow]

## Examples
[Concrete examples of using this skill]

必填字段

name

  • 最多 64 个字符
  • 只能包含小写字母、数字和连字符
  • 不能以连字符开头或结尾,不能有连续连字符
  • 必须和 SKILL.md 所在目录名一致(pi 的 loadSkillFromFile 会校验)

description

  • 不能为空
  • 最多 1024 个字符
  • 必须同时说明"做什么"和"什么时候用"------这是 LLM 判断是否触发该 skill 的唯一依据

description 的写法是 skill 能否被正确触发的关键。好的 description 应该像这样:

makefile 复制代码
description: 当用户要求代码审查时,按此 skill 的流程检查命名规范、错误处理、测试覆盖率。适用于 PR review、代码走查、质量检查。

而不是这样:

makefile 复制代码
description: 代码审查工具

前者同时说明了"做什么"(检查命名/错误处理/测试覆盖率)和"什么时候用"(PR review / 代码走查 / 质量检查),LLM 能精准匹配。后者太模糊,LLM 可能在不该触发时触发,或该触发时没触发。

可选字段

disable-model-invocation :设为 true 时,该 skill 的描述不注入 system prompt------LLM 不会自动发现它。只能通过代码显式调用。适用于内部 skill 或实验性 skill。

正文写法

正文是 LLM 触发 skill 后读到的指令。写法建议:

  • 步骤化:用有序列表写明工作流程,LLM 按步骤执行
  • 引用外部文件 :复杂内容拆到 references/ 下,正文里写"For advanced usage, see REFERENCE.md"
  • 控制长度:正文建议 <5k token(约 3000 字中文)。超长内容拆到 references/ 里按需加载

完整的 skill 编写规范(字段约束、description 写法、正文结构、最佳实践),请参考 Claude 官方 Skill 编写指南

三、加载与注册

skill 的加载分两个阶段:发现(遍历目录找 SKILL.md)和注册(存入 resources 供后续注入 system prompt)。

1. 发现:loadSkills

pi 的 loadSkillsagent/src/harness/skills.ts:49)负责从目录树中发现所有 skill:

typescript 复制代码
// skills.ts:49-75
export async function loadSkills(
  env: ExecutionEnv,
  dirs: string | string[],
): Promise<{ skills: Skill[]; diagnostics: SkillDiagnostic[] }> {
  const skills: Skill[] = [];
  const diagnostics: SkillDiagnostic[] = [];
  for (const dir of Array.isArray(dirs) ? dirs : [dirs]) {
    const rootInfoResult = await env.fileInfo(dir);
    if (!rootInfoResult.ok) {
      if (rootInfoResult.error.code !== "not_found") {
        diagnostics.push({ type: "warning", code: "file_info_failed", ... });
      }
      continue;                    // 目录不存在就跳过------不报错
    }
    const result = await loadSkillsFromDirInternal(env, rootInfoResult.value.path, true, ignore(), rootInfoResult.value.path);
    skills.push(...result.skills);
    diagnostics.push(...result.diagnostics);
  }
  return { skills, diagnostics };
}

核心逻辑在 loadSkillsFromDirInternalskills.ts:103-175)------递归遍历目录:

typescript 复制代码
// skills.ts:103-175(简化)
async function loadSkillsFromDirInternal(env, dir, includeRootFiles, ignoreMatcher, rootDir) {
  // 1. 先找当前目录的 SKILL.md------找到了就加载,不递归子目录
  const entries = await env.listDir(dir);
  for (const entry of entries) {
    if (entry.name !== "SKILL.md") continue;
    const result = await loadSkillFromFile(env, entry.path);
    if (result.skill) skills.push(result.skill);
    return { skills, diagnostics };    // 有 SKILL.md 的目录就是一个 skill,不再往下找
  }

  // 2. 没有 SKILL.md------递归子目录
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
    if (entry.name.startsWith(".") || entry.name === "node_modules") continue;

    // 检查 ignore 文件(.gitignore / .ignore / .fdignore)
    if (ignoreMatcher.ignores(entry.path)) continue;

    if (entry.kind === "directory") {
      const result = await loadSkillsFromDirInternal(env, entry.path, false, ignoreMatcher, rootDir);
      skills.push(...result.skills);
    }
  }
  return { skills, diagnostics };
}

两个关键设计:

"有 SKILL.md 就停" :遍历到一个目录时,先检查它有没有 SKILL.md。有就加载它、不再递归子目录。没有才继续往下找。这保证一个 skill 目录的子目录不会被误当成独立 skill。

ignore 文件支持 :遍历时读取 .gitignore / .ignore / .fdignore,匹配的路径跳过。这让用户能用 .gitignore 控制哪些 skill 被加载。

2. 解析:loadSkillFromFile

找到 SKILL.md 后,loadSkillFromFileskills.ts:233-279)解析内容:

typescript 复制代码
// skills.ts:233-279(简化)
async function loadSkillFromFile(env, filePath): Promise<{ skill: Skill | null; diagnostics: SkillDiagnostic[] }> {
  const rawContent = await env.readTextFile(filePath);

  // 解析 frontmatter(YAML)+ body(Markdown)
  const { frontmatter, body } = parseFrontmatter(rawContent);

  // name:优先用 frontmatter 的,没有就用目录名
  const skillDir = dirname(filePath);
  const parentDirName = basename(skillDir);
  const name = frontmatter.name || parentDirName;

  // 校验 name
  for (const error of validateName(name, parentDirName)) {
    diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath });
  }

  // 校验 description
  const description = frontmatter.description;
  for (const error of validateDescription(description)) {
    diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath });
  }

  // description 为空 → 不加载这个 skill(但不报错,只记 warning)
  if (!description || description.trim() === "") {
    return { skill: null, diagnostics };
  }

  return {
    skill: {
      name,                           // skill 名
      description,                    // 注入 system prompt 的描述
      content: body,                  // SKILL.md 正文(触发后 LLM 读到的指令)
      filePath,                       // 文件路径(LLM 用 read 工具读全文时用)
      disableModelInvocation: frontmatter["disable-model-invocation"] === true,
    },
    diagnostics,
  };
}

校验规则(skills.ts:281-301):

  • name 校验:必须和目录名一致、≤64 字符、只能小写字母+数字+连字符、不能开头/结尾连字符、不能连续连字符
  • description 校验:不能为空、≤1024 字符

校验失败不抛异常------只记 SkillDiagnostic warning,跳过该 skill。其他 skill 继续加载。这是容错设计------一个坏 skill 不影响整个 agent 启动。

3. 注册:ResourceLoader

pi coding-agent 的 ResourceLoadercoding-agent/src/core/resource-loader.ts:505-526)调 loadSkills 并存储结果:

typescript 复制代码
// resource-loader.ts:505-526(简化)
private updateSkillsFromPaths(skillPaths: string[]) {
  const skillsResult = loadSkills({
    cwd: this.cwd,
    agentDir: this.agentDir,
    skillPaths,
  });

  this.skills = skillsResult.skills.map((skill) => ({
    ...skill,
    sourceInfo: this.findSourceInfoForPath(skill.filePath, ...),
  }));
  this.skillDiagnostics = skillsResult.diagnostics;
}

getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] } {
  return { skills: this.skills, diagnostics: this.skillDiagnostics };
}

ResourceLoader 是 pi coding-agent 的资源管理器------启动时加载 skills、prompts、themes、extensions。加载完的 skills 存在 this.skills 里,通过 getSkills() 暴露。

AgentHarness(或 AgentSession)在构造 createTurnState 时从 resources.skills 读取 skills 列表,传给 formatSkillsForSystemPrompt(第 4 章讲)。没有独立的"注册"步骤------加载完就存着,构造 system prompt 时直接用。

四、注入 system prompt

skill 加载完后,它的 name + description 被格式化成 XML 块注入 system prompt。这是 LLM"发现"skill 的唯一途径。

1. formatSkillsForPrompt

pi coding-agent 的 formatSkillsForPromptcoding-agent/src/core/skills.ts:335-361)负责把 skills 列表格式化成 system prompt 片段:

typescript 复制代码
// skills.ts:335-361
export function formatSkillsForPrompt(skills: Skill[]): string {
  const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
  if (visibleSkills.length === 0) return "";

  const lines = [
    "\n\nThe following skills provide specialized instructions for specific tasks.",
    "Use the read tool to load a skill's file when the task matches its description.",
    "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
    "",
    "<available_skills>",
  ];

  for (const skill of visibleSkills) {
    lines.push("  <skill>");
    lines.push(`    <name>${escapeXml(skill.name)}</name>`);
    lines.push(`    <description>${escapeXml(skill.description)}</description>`);
    lines.push(`    <location>${escapeXml(skill.filePath)}</location>`);
    lines.push("  </skill>");
  }

  lines.push("</available_skills>");
  return lines.join("\n");
}

输出长这样:

xml 复制代码
The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.
When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.

<available_skills>
  <skill>
    <name>code-review</name>
    <description>当用户要求代码审查时,按此 skill 的流程检查命名规范、错误处理、测试覆盖率。</description>
    <location>/path/to/.pi/skills/code-review/SKILL.md</location>
  </skill>
  <skill>
    <name>deploy-guide</name>
    <description>部署到生产环境时,按此 skill 的检查清单验证配置、数据库迁移、回滚方案。</description>
    <location>/path/to/.pi/skills/deploy-guide/SKILL.md</location>
  </skill>
</available_skills>

2. 三个关键字段

每个 skill 在 system prompt 里有三个字段:

  • name:skill 名,LLM 用它引用 skill
  • description:触发依据------LLM 拿用户的任务和这段描述做语义匹配,决定要不要读这个 skill
  • locationSKILL.md 的文件路径------LLM 决定读取时用 read 工具打开这个路径

注意:正文(content)不在 system prompt 里formatSkillsForPrompt 只输出 name + description + location,不输出 body。这就是"按需加载"------正文要等 LLM 主动 read 才进 context。

3. 两行关键指令

注入的 XML 块开头有两行指令:

vbnet 复制代码
The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.

第一行告诉 LLM "下面这些是 skill"。 第二行告诉 LLM "任务匹配 description 时,用 read 工具读 SKILL.md 全文"------这是触发机制的关键。LLM 看到这段指令后,知道"如果用户任务匹配某个 skill 的 description,我应该 read 它的 SKILL.md"。

第三行处理相对路径------skill 正文里引用 references/xxx.md 时,LLM 要把它解析成相对于 SKILL.md 所在目录的绝对路径再 read。

4. disableModelInvocation 的作用

typescript 复制代码
const visibleSkills = skills.filter((s) => !s.disableModelInvocation);

disableModelInvocation: true 的 skill 被过滤掉 ------不注入 system prompt,LLM 看不到它。这种 skill 只能通过代码显式调用(AgentHarness 有 API 可以手动触发 skill),适用于:

  • 内部 skill:只在特定流程里用,不希望 LLM 随意触发
  • 实验性 skill:还没写好 description,不想让 LLM 误触发
  • 安全敏感 skill:如部署 skill,只允许特定条件触发

5. 注入时机

formatSkillsForPromptbuildSystemPromptcoding-agent/src/core/system-prompt.ts:73, 165)里被调用:

typescript 复制代码
// system-prompt.ts:70-73
if (hasRead && skills.length > 0) {
  prompt += formatSkillsForPrompt(skills);
}

两个条件:

  • hasRead:agent 有 read 工具。没有 read 工具的 agent 不能触发 skill(因为触发要 read SKILL.md),注入了也没用。
  • skills.length > 0:有至少一个可见 skill。

注入发生在 buildSystemPrompt 构造 system prompt 时------文章 5 讲过,prepareNextTurn 每轮都会调 createTurnState,如果 systemPrompt 是函数(pi coding-agent 就是),每轮都重新构造。这意味着运行中新增的 skill 会在下一轮被注入------动态加载 skill 不需要重启 agent。

6. system prompt 对比

触发前(agent 启动时,skill 只有元数据在 system prompt 里):

xml 复制代码
You are an expert coding assistant operating inside pi...

Available tools:
- read: Read file contents
- bash: Execute commands
- edit: Edit files
- write: Write files

Guidelines:
- Be concise in your responses
- Show file paths clearly when working with files

The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.
When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.

<available_skills>
  <skill>
    <name>code-review</name>
    <description>当用户要求代码审查时,按此 skill 的流程检查命名规范、错误处理、测试覆盖率。</description>
    <location>/project/.pi/skills/code-review/SKILL.md</location>
  </skill>
</available_skills>

Current date: 2026-08-27
Current working directory: /project

此时 LLM 只知道有 code-review 这个 skill,但不知道里面的指令。token 成本:约 100 token。

用户说"帮我 review 这个 PR" → LLM 判断任务匹配 code-review 的 description → 调 read 工具读 SKILL.md

markdown 复制代码
[tool_call] read({ path: "/project/.pi/skills/code-review/SKILL.md" })

[tool_result] # 代码审查流程

## 步骤
1. 先用 bash 执行 `git diff main...HEAD` 获取完整 diff
2. 检查命名规范:
   - 变量名使用 camelCase
   - 常量使用 UPPER_SNAKE_CASE
   - 文件名使用 kebab-case
3. 检查错误处理:
   - async 函数有没有 try-catch
   - 外部调用有没有超时处理
4. 检查测试覆盖率:
   - 新增函数有没有对应测试
   - 边界条件有没有覆盖
5. 输出审查报告,按严重程度分级:blocker / warning / suggestion

For detailed checklist, see [references/checklist.md](references/checklist.md).

SKILL.md 正文进入 context(约 500 token)。LLM 按正文里的步骤执行------先 git diff,再逐项检查。

如果 LLM 需要更详细的检查清单,再 read references/checklist.md

css 复制代码
[tool_call] read({ path: "/project/.pi/skills/code-review/references/checklist.md" })

token 成本对比

阶段 进 context 的内容 token
启动 name + description + location ~100
触发 + SKILL.md 正文 ~600
深入 + references/checklist.md ~800
总计 ~1500

如果不用 skill,把全部内容塞进 system prompt:每次调用都带 ~1500 token,即使任务和代码审查无关。10 个 skill 就是 15000 token 的浪费。skill 的按需加载让这 1500 token 只在真正需要时消耗。

五、三级加载机制

前四章分别讲了 skill 的格式、加载、注入。本章把它们串起来,看 skill 的内容如何分三级逐步进入 context------这就是 Claude 文档所说的"progressive disclosure"(渐进式披露)。

流程图

flowchart TD A([agent 启动]) --> B[&#34;loadSkills 遍历 .pi/skills/<br/>解析 SKILL.md frontmatter&#34;] B --> C[&#34;formatSkillsForPrompt<br/>name + description + location&#34;] C --> D[&#34;注入 system prompt<br/>每轮都带,~100 token/skill&#34;] D --> E{用户任务匹配<br/>某 skill 的 description?} E -->|否| F([LLM 正常回复,不触发 skill]) E -->|是| G[&#34;LLM 调 read 工具<br/>read(SKILL.md)&#34;] G --> H[&#34;SKILL.md 正文进 context<br/>~5k token&#34;] H --> I{正文引用了<br/>references/ 或 scripts/?} I -->|否| J([LLM 按正文指令执行]) I -->|是| K[&#34;LLM 调 read / bash<br/>read references/xxx.md<br/>或 bash scripts/validate.py&#34;] K --> L[&#34;参考文档进 context<br/>或脚本输出进 context&#34;] L --> J

三级加载

级别 加载时机 内容 token 成本 谁触发
1. 元数据 agent 启动时 name + description + location,注入 system prompt ~100 token/skill 框架(loadSkills + formatSkillsForPrompt
2. 指令 LLM 判断任务匹配后 SKILL.md 正文,LLM 用 read 工具读取 <5k token LLM(主动调 read)
3. 资源 指令引用时 references/ 下的文档 / scripts/ 下的脚本 按需 LLM(按正文引用调 read / bash)

第 1 级:元数据(始终加载)

agent 启动时 loadSkills 遍历 .pi/skills/,解析每个 SKILL.md 的 frontmatter,提取 name + descriptionformatSkillsForPrompt 把它们格式化成 <available_skills> XML 块注入 system prompt。

这一级始终加载------不管用户任务是什么,每个可见 skill 的 name + description 都在 system prompt 里。成本约 100 token/skill,10 个 skill 只占 1000 token。

这一级的作用是"让 LLM 知道有哪些 skill 存在"------但只看到名字和描述,不知道具体指令。

第 2 级:指令(触发时加载)

当用户任务匹配某个 skill 的 description 时,LLM 自己判断"我应该读这个 skill"。它调 read 工具读 SKILL.md 全文,正文进入 context。

这一级只在触发时加载------没有匹配的 skill 永远不读,不占 token。成本约 <5k token/skill,只在需要时消耗。

这一级的作用是"让 LLM 获取具体指令"------正文里有步骤、工作流、最佳实践、对外部文件的引用。

第 3 级:资源(按需加载)

正文里可能引用 references/xxx.mdscripts/validate.py。LLM 按需读取:

  • 参考文档 :LLM 调 read 工具读 references/ 下的文件,内容进 context
  • 脚本 :LLM 调 bash 工具执行 scripts/ 下的脚本,只有输出进 context,脚本代码本身不进 context

这一级只在被引用时加载------没被正文引用的文件永远不读。一个 skill 可以带几十个参考文件,但只用到的才进 context。

这一级的作用是"让 LLM 获取详细信息"------API 文档、检查清单、示例数据,或通过脚本执行确定性操作。

为什么分三级

核心问题是context window 是稀缺资源。假设 agent 有 10 个 skill,每个 skill 的正文 + 参考文件共 5000 token:

方式 常驻 token 触发后 token
全塞 system prompt 10 × 5000 = 50000 50000
三级加载 10 × 100 = 1000 1000 + 5000(触发的 1 个)= 6000

三级加载让常驻成本从 50000 降到 1000,触发后只多用 5000。LLM 的注意力不被无关 skill 干扰,token 预算留给真正的对话和工具结果。

和工具的对比

skill 的三级加载和工具调用有相似之处------都是 LLM 主动调 read 工具读文件。但机制不同:

skill 工具
谁决定读什么 LLM 看 system prompt 里的 description 判断 LLM 看 system prompt 里的 tool description 判断
读什么 固定路径(SKILL.mdlocation LLM 自己生成参数(如 read({ path: "..." })
读完后做什么 按正文指令执行工作流 用读到的内容回复用户
目的 注入领域知识 / 工作流 获取实时信息

skill 是"框架引导 LLM 去读特定文件",工具是"LLM 自主决定读什么"。skill 在 system prompt 里告诉 LLM"有这些指令包,匹配就读",工具在 system prompt 里告诉 LLM"你可以读任意文件"。

六、Q&A

Q1:在一个 skill 被加载后,是否可以继续加载其他 skill?

可以。skill 之间没有互斥。

LLM 在执行一个 skill 的指令过程中,可以同时触发另一个 skill。假设 agent 有 code-reviewtest-coverage 两个 skill,用户说"review 这个 PR,顺便看看测试覆盖率够不够"。LLM 可以:

  1. 先 read code-review/SKILL.md → 按流程执行
  2. 执行到"检查测试覆盖率"步骤时,发现 test-coverage skill 匹配
  3. 再 read test-coverage/SKILL.md → 按它的流程生成覆盖率报告
  4. 两个 skill 的指令同时在 context 里,LLM 综合两者执行

没有限制------不限制同时加载几个 skill、不限制加载顺序、不限制 skill 之间引用。唯一的约束是 context window------每个 skill 的正文 + 参考文件都占 token。但这是 LLM 自己的判断------如果它觉得不需要某个 skill 了,后续轮次不会重新 read。

和工具调用一样------LLM 可以在一轮里调多个工具,也可以在多轮里调不同工具。skill 的"加载"本质就是一次 read 工具调用,没有特殊的加载状态管理。

Q2:触发多个 skill 后,加载顺序是什么样的?

没有框架控制的顺序------完全由 LLM 自己决定。

skill 的加载本质是 LLM 调 read 工具。LLM 在一轮回复里可以产出多个 toolCall,顺序就是 LLM 生成的顺序。框架不干预。几种情况:

串行:LLM 先 read skill A → 拿到正文 → 下一轮按 A 的指令执行 → 执行过程中发现需要 skill B → read skill B。这是最常见的------LLM 读完一个 skill 后先理解内容,再决定要不要读下一个。

一轮同时 read 多个 :LLM 在一轮里同时产出两个 toolCall,executeToolCalls 按顺序执行,两个 skill 的正文在同一轮回填。但这种情况少见------LLM 通常不会一次赌多个 skill 都相关。

跨轮 read :turn 1 read skill A,turn 3 read skill B。skill 正文在 read 的那一轮进 context,之后一直在 context.messages 历史里,不需要重新 read。

关键点:顺序是 LLM 的语义判断,不是框架的调度。框架只负责"LLM 说 read,就执行 read"。

Q3:<available_skills> 始终存在 system prompt 里吗?会被更新吗?

是的,<available_skills> 始终在 system prompt 里,并且每轮更新。

pi 的 systemPrompt 可以是函数,prepareNextTurn 每轮都调 createTurnStatebuildSystemPrompt()formatSkillsForPrompt(skills)。所以每轮的 system prompt 都是重新构造的,包含最新的 skill 列表。

什么时候会变:

场景 system prompt 里的 skills 变化
运行中新增 skill 文件 下一轮 <available_skills> 多一条
运行中删除 skill 文件 下一轮 <available_skills> 少一条
skill 的 description 被修改 下一轮 description 文本更新
setTools 切换到 read-only 模式(无 read 工具) 下一轮 <available_skills> 整块消失
skill 的 disableModelInvocation 被改 下一轮该 skill 出现/消失

但已读的 skill 正文不会消失------<available_skills> 是元数据,在 system prompt 里每轮重建。而 LLM 之前 read 进来的 SKILL.md 正文在 context.messages 历史里,不会因为 system prompt 重建而丢失。

Q4:哪些 skill 会出现在 <available_skills> 里?

一个字段决定------disableModelInvocation

typescript 复制代码
const visibleSkills = skills.filter((s) => !s.disableModelInvocation);

展示的条件(缺一不可):skill 文件合法、description 非空、disableModelInvocation 不是 true、agent 有 read 工具。

不展示的情况:

情况 原因
disableModelInvocation: true 被代码显式隐藏,只能通过 API 手动触发
description 为空 loadSkillFromFile 返回 skill: null,根本没进列表
name 校验失败 同上------只记 warning,不加载
目录被 .gitignore 匹配 遍历时跳过
没有 read 工具 buildSystemPrompthasRead 为 false,整块不注入

Q5:如果 skill 太多,会产生什么负面影响?

三个层面的问题,但 pi 的设计已经把影响压到最低。

token 成本------线性增长:每个可见 skill 占约 100 token。100 个 skill = 10000 token 常驻。和"全塞正文"比(100 × 5000 = 50 万 token,直接爆 context),10000 token 可控。

LLM 注意力分散------真正的风险:system prompt 里 100 个 description,LLM 每轮扫一遍判断匹配。描述越多,匹配精度越低------可能误触发(读了不相关的 skill,浪费 token)、漏触发(真正相关的被淹没)、决策延迟。这个问题没有框架层面的解决------pi 不做 skill 匹配过滤,完全交给 LLM 语义判断。

加载性能------影响小loadSkills 启动时遍历目录、读文件、解析 YAML。100 个 skill = 100 次文件读取,但这是一次性成本。

实际建议:

skill 数量 影响 建议
<20 无感 随便加
20-50 token 可感知,注意力略有分散 定期清理不用的 skill
50-100 token 占用明显,误触发风险上升 disableModelInvocation 隐藏不常用的
>100 严重影响 拆分到不同项目 / 按场景动态切换

一句话:token 成本是线性可预测的,注意力分散才是真正的风险。

七、下一章预告

下一篇文章将进入 pi 的 Extension API------.pi/extensions/ 目录下的扩展如何被加载、扩展如何注册自定义工具和 hook handler、扩展的生命周期管理,以及 Extension API 如何让第三方在不 fork pi 源码的前提下,完整地改变 agent 的工具集、行为和 UI 渲染。这是 pi "扩展无需 fork" 理念的最终落地机制------skill 注入知识,hook 拦截行为,extension 定义一切。

相关推荐
tachibana220 分钟前
Agent 的长短期记忆系统
人工智能·ai·大模型·llm·agent
prog_610333 分钟前
【笔记】cllama:搜罗万象当LLM api server测qwen3.8:flash-next
笔记·大语言模型·agent·vibe-coding·qwen3.8-flash
特立独行的猫a1 小时前
仓颉语言原生 Coding Agent:cjh · 仓颉语言实现的 Harness
ai·agent·harmonyos·仓颉·cangjie·harness
沉默王二2 小时前
豆包工作Agent正式发布,直接给到夯。
人工智能·面试·agent
苏灿烤鱼2 小时前
AI总改崩你的代码?4.6万星GitNexus架构深拆
typescript·开源·agent
leeyi3 小时前
Eino + DeepFlux 实战全景复盘:100 篇,从 5 分钟 Demo 到企业级平台(第100篇-E86)
llm·aigc·agent
怕浪猫10 小时前
设计你自己的可替换能力:三段式 Seam 实战,从需求到上线
agent·产品
腾讯云大数据11 小时前
DataBuddy数据语义驱动的企业Agent Runtime实践
大数据·人工智能·腾讯云·agent
plainGeekDev14 小时前
Agent高级编排模式
agent·ai编程·claude