返回课程首页

19

Skill 是如何被触发和执行的

理解显式调用、自动匹配与渐进式加载。

源码基线:Pi v0.82.0 · commit 518855d

上一章已经让 error-log-analysis 成功出现在 Resource Loader 中。现在用户只输入:

请分析这份服务日志为什么反复超时。

桌面 Agent 没有收到 Skill 名称。它能否自己选中正确能力?选中后又是谁“执行” Skill?

答案先分成两类语义:

  • 自动匹配:模型看目录、决定读取;
  • 显式调用:应用明确指定 Skill。

显式调用又有两套公开接法:通用 AgentHarness.skill() 与 coding-agent AgentSession/skill:name 展开。它们的加载时机和失败语义不同。

1. Skill 没有独立执行器

先建立最重要的认识:

Skill 通常不是一个 execute() 函数

两层 Skill 都没有 execute 字段,但字段并不完全相同:

  • coding-agent 的 Skill 保存 namedescriptionfilePathbaseDirsourceInfo 和可见性标记,正文仍在文件中;
  • 通用 Harness 的 Skill 保存 namedescriptioncontentfilePath 和可见性 标记,没有内置的 baseDirsourceInfo 字段。
flowchart LR
    S["Skill 指令"] --> A["Agent Loop"]
    A --> R["read 正文/参考资料"]
    A --> T["调用已有 Tool"]
    T --> O["Tool Result"]
    O --> A
    A --> U["最终回答"]

所谓“执行 Skill”,实际是模型读懂工作流后,在普通 Agent Loop 中选择 Tool 并继续多轮推理。

2. coding-agent 自动匹配的前提

自动路径要求:

  1. Resource Loader 已加载 Skill;
  2. Skill 没有 disable-model-invocation: true
  3. read 是 Active Tool;
  4. Base System Prompt 已在资源变化后重建;
  5. before_agent_start 后的最终有效 System Prompt 仍保留这份目录。

满足时,System Prompt 才包含:

<skill>
  <name>error-log-analysis</name>
  <description>...</description>
  <location>/absolute/path/SKILL.md</location>
</skill>

Extension 可以在 Run 前替换 Base System Prompt,并移除 Skills Section。缺少任一条件, 模型就不能通过这个目录自动发现 Skill。

3. 通用 AgentHarness 怎样提供自动目录

通用 AgentHarness 的默认 System Prompt 只是:

You are a helpful assistant.

resources.skills 会提供给宿主的 systemPrompt 回调,但 Harness 不会自动把它们写入 Prompt。宿主要明确组合:

systemPrompt: ({ resources }) =>
  [
    "You are a desktop agent.",
    formatSkillsForSystemPrompt(resources.skills ?? []),
  ]
    .filter(Boolean)
    .join("\n\n")

宿主还要提供一个模型可调用、并且能读取 skill.filePath 的文件读取 Tool。 formatSkillsForSystemPrompt() 会过滤 disableModelInvocation,但它本身既不检查 Active Tool,也不要求 Tool 必须叫 read。这些是通用 Harness 交给宿主负责的能力边界。

目录进入 Prompt 以后,“是否匹配”仍然是模型行为,不是 Harness 内置 Router。

4. 模型怎样决定匹配

System Prompt 给模型的规则是:

Use the read tool to load a skill's file
when the task matches its description.

模型比较当前任务语义与 Description:

flowchart TD
    U["用户任务"] --> M["模型读取 available_skills"]
    M --> Q{"Description 匹配?"}
    Q -->|是| R["read location"]
    Q -->|否| N["不用该 Skill"]

Pi Core 没有额外的关键词打分器、Embedding Retriever 或确定性 Skill Router。决定来自模型 本身。

5. 为什么 Description 像路由表

模型在第一次选择时只看到 Name、Description 和 Location,还没看到正文。

# 路由信息不足
description: Helps with logs.
# 同时说明 What 与 When
description: Analyzes application error logs, groups repeated
  signatures, reconstructs timelines, and proposes evidence-backed
  next checks. Use when the user provides a log file or asks to
  diagnose runtime errors from logs.

任务对象、主要产出和触发语境越清楚,模型越容易正确选择。

6. 自动匹配不是保证

即使 Description 很好,模型也可能:

  • 没有读取 Skill;
  • 选错相似 Skill;
  • 同时读取多个 Skill;
  • 直接依据已有知识回答。

Pi 文档明确说明模型并不总会自动读取匹配 Skill。

所以产品不能把“目录已加载”展示成“工作流一定执行”。更准确的状态是:

Skill available
→ Skill selected
→ SKILL.md loaded
→ workflow followed

每一步都需要单独观察。

7. 自动路径的正文何时进入 Context

模型输出普通 Tool Call:

read({
  path: "/.../error-log-analysis/SKILL.md"
})

Read Tool 返回本次读取内容,形成 Tool Result。之后:

  • Tool Call 与 Result 进入 Transcript;
  • 下一次模型调用能看到这次 Tool Result;
  • Session 正常持久化这些消息;
  • Compaction 时它们可能被摘要或保留。

Pi 没有单独的“Active Skill Context”存储区。正文通过普通消息进入 Context。

coding-agent 内置 Read Tool 默认最多返回 2000 行或 50KB,以先触发者为准。小 Skill 通常 一次得到完整正文;更大的文件会附带下一次 offset,模型必须继续分页读取。不能把一次 Read Tool Call 等同于无条件获得完整 Skill。

8. 自动读取后的相对路径

System Prompt 还告诉模型:

Skill 引用的相对路径从 Skill Directory 解析,
Tool Command 使用 Absolute Path。

因此看到:

Read `references/report-format.md`.

模型应组合 dirname(SKILL.md) 与相对路径,并在 Tool Command 中使用绝对路径。

引用文件不会被 read SKILL.md 自动递归加载。模型要在工作流需要时再调用 Tool。

9. 显式调用绕过模型选择

用户输入:

/skill:error-log-analysis 分析 server.log

这条路径不要求模型先从 Description 判断。AgentSession._expandSkillCommand() 在请求模型 之前处理它。

sequenceDiagram
    participant U as 用户
    participant S as AgentSession
    participant F as SKILL.md
    participant A as Agent

    U->>S: /skill:name args
    S->>S: 按 name 查 Skill
    S->>F: readFileSync
    F-->>S: Frontmatter + Body
    S->>S: stripFrontmatter + 包装
    S->>A: Expanded User Message

显式调用确定的是“正文被展开”,并不保证模型随后每一步都严格遵守。

10. 通用 AgentHarness 的显式调用

构建桌面 Agent 若直接使用通用 Harness,可以调用:

await harness.skill(
  "error-log-analysis",
  "分析 /absolute/path/server.log",
);

通用 Harness 的 Skill Loader 在资源加载阶段已经把正文保存在 skill.contentharness.skill() 创建当前 Turn State,在 Resources 中按 Name 查找,再用 formatSkillInvocation() 把内存中的 Content 和 Additional Instructions 组成 User Message。它不会在每次调用时重新同步读取 SKILL.md

磁盘文件变化后应先走宿主资源 Reload,不能假设下一次 harness.skill() 自动看到新正文。 未知名称会抛出 AgentHarnessError("invalid_argument", "Unknown skill: ...")

11. 显式 Skill Block 的真实形状

源码构建:

<skill
  name="error-log-analysis"
  location="/absolute/path/SKILL.md">
References are relative to /absolute/path.

...去掉 Frontmatter 后的正文...
</skill>

分析 server.log

coding-agent 会先裁剪 /skill:name 后面的参数,再把它原样放在 Skill Block 后;通用 Harness 则把调用者传入的 additionalInstructions 原样追加,不替调用者裁剪。两者都不会 给路径参数补全,也不会自动解析成结构化 Schema。

12. 两套显式调用对照

行为 通用 AgentHarness.skill() coding-agent /skill:name
正文来源 加载阶段保存在 Skill.content 调用时 readFileSync(filePath)
调用形式 Method + Additional Instructions User Text Command
未知名称 invalid_argument 原文本继续
文件调用前被删除 仍用内存快照 skill_expansion Error,原文本继续
disableModelInvocation 仍可显式调用 仍可显式调用
忙碌时 Harness 要求 idle 普通 Prompt 可按 streamingBehavior 入队

两者都会构造相似的 <skill> User Message,但不能混用失败和刷新语义。

13. coding-agent 显式调用受加载结果约束

_expandSkillCommand() 只在 Resource Loader 的 Skills 数组中按 Name 查找。

未知 Skill

找不到时,原文本不变:

/skill:not-found ...

它随后会作为普通 User Text 发给模型,并不是一个 Core Error。

文件读取失败

Skill 已加载但文件后来被删除或不可读时:

  1. Extension Runner 收到 skill_expansion Error;
  2. 原始文本不变;
  3. Prompt 流程继续。

桌面宿主应显示错误事件,否则用户只会看到模型困惑地处理一条未展开命令。

14. disable-model-invocation 的真实作用

设置:

disable-model-invocation: true

只会让 formatSkillsForPrompt() 不把该 Skill 放入自动目录。

_expandSkillCommand() 查找的是全部已加载 Skills,没有再次检查这个字段。所以:

模型自动发现:不可见
用户 /skill:name:仍可展开
应用 harness.skill():仍可调用

这适合把启动权留给用户,但不是执行授权。

15. 普通 Prompt 的处理顺序

coding-agent 的普通 prompt() 大致顺序:

  1. 若是已注册 Extension Command,先执行并返回;
  2. 触发 input Extension Event;
  3. Handler 可以处理或转换文本;
  4. 展开 /skill:name
  5. 展开 Prompt Template;
  6. 若正在 Streaming,按 streamingBehavior 入 Steering/Follow-up 并立即返回;
  7. idle 时进行模型/API Preflight;
  8. idle 新 Run 才触发 before_agent_start
  9. 进入 Agent Loop。

所以 input Handler 看到的是 Skill 展开前文本,before_agent_start 看到的是展开后文本。 运行中排队的这条消息不会单独触发自己的 before_agent_start

16. Steering 与 Follow-up 也能显式调用 Skill

直接调用:

await session.steer(
  "/skill:error-log-analysis 改按日志工作流分析",
);

或:

await session.followUp(
  "/skill:error-log-analysis 完成后再检查另一份日志",
);

两者都会先展开 Skill Command 和 Prompt Template,再把展开后的 User Message 放入基础 Agent 队列。

它们会拒绝已注册的 Extension Command,因为 Extension Command 不能作为普通排队文本执行。 直接 session.steer() / followUp() 不触发 inputbefore_agent_start;它们从 Skill/Template 展开直接进入队列。

17. 引用脚本是怎样执行的

SKILL.md 写:

Run `node scripts/summarize-log.mjs <log-path>`.

这不会自动启动进程。模型需要:

  1. 从 Skill Base Directory 解析绝对 Script Path;
  2. 决定使用 bash Tool;
  3. 形成 Tool Call;
  4. 宿主执行权限/审批策略;
  5. Tool 返回 stdout、stderr 与 Exit;
  6. 模型解释结果。
flowchart LR
    I["Skill 中的脚本说明"] --> C["模型形成 bash Tool Call"]
    C --> P{"宿主允许?"}
    P -->|是| X["执行脚本"]
    P -->|否| D["Tool 拒绝结果"]
    X --> R["Tool Result"]
    R --> M["模型继续"]

Skill 不能绕过 Tool Policy。

18. Skill 执行失败怎样处理

失败位置不同,行为也不同:

失败点 进入哪里 典型恢复
自动 read SKILL.md 失败 普通 Tool Result 模型重试、换路径或询问用户
显式展开读文件失败 skill_expansion Error,原文本继续 UI 提示并 Reload
引用 Reference 失败 普通 Tool Result 模型说明缺失或选择替代
Script Exit 非零 Bash Tool Result 阅读 stderr、修正参数、停止
Tool 被 Policy 拒绝 Tool Result / 宿主错误契约 请求批准或换无副作用方案
模型没有自动选择 没有 Skill Tool Call 用户显式调用或改进 Description

没有统一的“Skill Failed Event”,因为大多数步骤就是普通 Tool/Agent 流程。

19. 多个 Skill 同时适用

例如用户说:

分析错误日志,然后把结果写成事故复盘文档。

可能同时匹配:

  • error-log-analysis
  • incident-report-writing

自动路径中,模型可以读取两份 Skill。Pi Core 没有内置 Skill 优先级字段、互斥锁、依赖图 或自动合并器。

Skill 作者应让 Description 尽量区分场景,并在正文中说明与其他能力的组合顺序。

20. 多 Skill 的冲突怎么处理

要区分两类冲突:

名称冲突

加载阶段已经 first-wins,模型只看到 Winner。

指令冲突

两份不同名称 Skill 都被读取后,正文可能要求不同输出格式或步骤。Pi 没有确定性规则引擎 解决它们。

宿主可采用:

  • 一个 Orchestrator Skill 明确组合顺序;
  • 在 System Prompt 中定义不可违反的全局边界;
  • 用 Extension 对高风险 Tool 做强制 Policy;
  • 让用户选择目标输出。

不要把安全冲突留给自然语言优先级猜测。

21. Skill 与多轮 Agent Loop

自动路径:

Turn 1:模型决定 read SKILL.md
Tool Result:正文
Turn 2:模型按步骤采样日志
Tool Result:日志片段
Turn 3:模型运行辅助脚本
Tool Result:统计
Turn 4:模型回读证据并输出

显式路径把正文直接放在最初 User Message 中,通常少一次 read SKILL.md Tool Call,但引用 文件和业务 Tool 仍需要后续 Turn。

Skill 不改变 Agent Loop 的 Tool Call、Steering、Follow-up 或 Event 规则。

22. Skill 会一直“激活”吗

Pi 没有 Active Skill Flag。

正文进入 Transcript 后,后续 Turn 可以继续看到并遵循它。但随着 Context Transform、 Compaction、Branch/Fork、新 Prompt 和新规则,可见内容会变化。Compaction Summary 是 有损的,不保证逐字保留 Skill。

长期必须遵守的安全规则不应只存在某次 Skill Body 中,应在 System Prompt、Project Context 或 Tool Policy 中重复落实。

23. Session 中怎样观察 Skill 使用

自动匹配可以观察:

  • Assistant 发出的 read Tool Call;
  • 对应 SKILL.md Path;
  • Tool Result 中的正文;
  • 后续 Tool 是否符合工作流。

显式调用可以观察:

  • User Message 已变成 <skill> Block + 参数;
  • 原始 /skill:name 字符串通常不会作为最终展开文本进入模型;
  • 出错时的 Extension Error。

桌面 UI 可显示:

已选择 Skill:error-log-analysis
来源:project
加载方式:automatic read
工作流文件:/absolute/path/SKILL.md

这需要宿主从 Message/Event 推导,不是 Pi 的独立 skill_start Event。

24. 怎样提高触发可靠性

按优先级:

  1. 写清 Description 的 What 与 When;
  2. 避免多个 Skill Description 大面积重叠;
  3. 保证 read Active 且 Prompt 已重建;
  4. 对用户明确选择提供 /skill:name
  5. 对产品固定流程,由宿主显式展开或用 Extension 编排;
  6. 用测试记录模型是否读取、读了哪份、后续是否遵循。

“再强调一句必须使用 Skill”不是唯一方案,先确认可见性和路由信息。

25. 一个完整例子

sequenceDiagram
    participant U as 用户
    participant P as System Prompt
    participant M as 模型
    participant R as read
    participant B as bash
    participant S as Session

    U->>M: 分析 server.log 的重复超时
    P-->>M: error-log-analysis 元数据
    M->>R: read /.../SKILL.md
    R-->>M: 本页工作流;必要时继续 offset
    M->>R: 读取日志头尾
    R-->>M: 原始证据
    M->>B: 运行 summarize-log.mjs
    B-->>M: 按文件统计
    M->>R: 回读关键时间段
    R-->>M: 关联证据
    M-->>U: 观察、假设、置信度、下一步
    M-->>S: Messages 与 Tool Results 正常持久化

这里没有隐藏的 Skill Runtime。每一步都能用之前章节的 Message、Tool、Event 和 Session 模型解释。

26. 常见误区

“Pi 用关键词搜索自动选 Skill”

错误。当前 Core 把目录交给模型判断。

“模型选中 Skill 后 Pi 自动运行整个目录”

错误。模型按正文逐步调用已有 Tool。

“显式调用会创建一个新的 System Message”

错误。它展开成普通 User Message 内容。

“引用文件会随 SKILL.md 自动加载”

错误。需要后续 Tool Call。

“disable-model-invocation 禁止用户调用”

错误。它只隐藏模型自动发现。

“读过一次后 Session 有永久 Active Skill”

错误。没有这个状态;正文只是 Context 中的消息内容。

“Skill 失败有统一 skill_error”

错误。读取、脚本、Tool Policy 和显式展开走各自错误通道。

27. 本章小结

  • Skill 没有统一执行器,模型在普通 Agent Loop 中读取指令并调用 Tool;
  • coding-agent 的自动匹配依赖 Loader、可见性、read Active、已重建 Base Prompt,以及 最终 Extension Prompt 仍保留目录;
  • 通用 Harness 不自动插入 Skill 目录;宿主要在 systemPrompt 回调中格式化目录,并提供 能读取 Skill 文件的 Tool;
  • 当前 Core 没有确定性的关键词、Embedding 或独立 Skill Router;
  • 自动正文通过 Read Tool Result 进入 Transcript,大文件需要 Offset 分页;
  • 通用 Harness 用已加载的 Skill.content 显式调用,未知名称抛 invalid_argument
  • coding-agent /skill:name 在模型请求前同步读取并展开成 User Message;
  • 未知 Skill 或读取失败会保留原文本,后者另发 skill_expansion Error;
  • disable-model-invocation 只影响自动目录;
  • Skill 引用和脚本需要模型继续调用 Tool,并受宿主 Policy 约束;
  • 多 Skill 的选择与指令冲突没有内置依赖/互斥系统;
  • Skill 正文跨多轮存在于 Context,但没有永久 Active Flag;
  • 桌面 UI 可从 Message/Event 推导 Skill 使用过程,而不是等待专用事件。

28. 自测

  1. 自动 Skill 匹配的五个前提是什么?
  2. 当前 Pi Core 是否有独立 Skill Router?
  3. 自动读取正文后,它以什么形式进入 Context?
  4. /skill:name 怎样变成模型输入?
  5. 未知 Skill 和文件读取失败分别怎样处理?
  6. disable-model-invocation 影响哪条路径?
  7. 两个不同名称 Skill 的指令冲突由谁处理?
  8. 为什么关键安全规则不能只写在 Skill Body 中?
  9. 通用 Harness 与 coding-agent 显式调用的正文来源和未知名称行为有何不同?
  10. 为什么只把 Skill 放进通用 Harness 的 resources.skills 还不能自动匹配?