前四章已经完成 error-log-analysis Skill。现在准备把它交给团队使用。
这时真正的问题不再是“我的电脑上能不能跑”,而是:
- 修改 Description 后,会不会不再触发?
- 模型是否按顺序读取日志、运行脚本、输出报告?
- 脚本报错或用户中断时,会不会继续做危险操作?
- 团队成员拿到的是不是同一个版本?
- 如果市场里的 Skill 不可信,桌面 Agent 应该怎样保护用户?
本章建立一条从测试到发布、再到治理的完整链路。
1. 先把“Skill 测试”拆成四层
flowchart LR
A["加载测试<br/>文件能否成为 Skill"] --> B["展开测试<br/>正文是否进入消息"]
B --> C["流程测试<br/>Tool 顺序与失败"]
C --> D["模型评测<br/>语义触发准确率"]
四层回答不同问题:
| 层级 | 要验证什么 | 是否需要真实模型 |
|---|---|---|
| 加载 | Frontmatter、路径、冲突、可见性 | 否 |
| 显式展开 | AgentSession.prompt("/skill:name …") 或底层 AgentHarness.skill() 生成的消息 |
否 |
| Agent 流程 | Tool Call、Tool Result、错误和中断 | 否,Faux Provider 即可 |
| 自动匹配质量 | 真实模型是否读了正确 Skill | 是,属于模型评测 |
不要用一个端到端用例包办全部层级。失败时你会不知道是加载错、Prompt 错、Tool 错,还是 模型选择不稳定。
2. Faux Provider 是什么
Pi 仓库的测试 Harness 调用 registerFauxProvider() 注册一个假的模型提供方,再用
setResponses() 放入预定响应:
const harness = await createHarness({ resourceLoader, tools });
harness.setResponses([
fauxAssistantMessage(
fauxToolCall("read", { path: skillPath }),
{ stopReason: "toolUse" },
),
fauxAssistantMessage(
fauxToolCall("analyze_log", { path: logPath }),
{ stopReason: "toolUse" },
),
fauxAssistantMessage("发现 timeout 签名重复出现 12 次。"),
]);
await harness.session.prompt("分析这份日志");
它不会联网,也不消耗模型 Token。真正的 Agent Loop、Message、Tool 执行和 Event 仍然运行, 只有“模型下一步说什么”由测试预先决定。
sequenceDiagram
participant T as 测试
participant S as AgentSession
participant F as Faux Provider
participant X as Tool
T->>F: setResponses(预定步骤)
T->>S: prompt()
S->>F: 模型请求
F-->>S: 预定 Tool Call
S->>X: execute()
X-->>S: Tool Result
S->>F: 下一次模型请求
F-->>S: 预定回答
3. Pi 源码里的测试 Harness 做了什么
packages/coding-agent/test/suite/harness.ts 不是公开产品 API,而是 Pi 仓库自己的测试
基础设施。createHarness() 会:
- 创建临时目录;
- 注册 Faux Provider 和 Faux Model;
- 创建内存
SessionManager、SettingsManager、AuthStorage; - 创建真正的
Agent与AgentSession; - 注入测试 Resource Loader、Tool 和 Extension;
- 订阅全部
AgentSessionEvent; - 在
cleanup()中释放 Session、注销 Provider、删除临时目录。
因此下面的代码用于解释 Pi 自身怎样测试。若桌面 Agent 自己封装了 Runtime,应建立等价
Harness,而不是从已发布包中假定能导入这个仓库内部文件。这里的 suite Harness 只有
session、Faux Response 和 Event 等测试能力;它不是 packages/agent 中公开的
AgentHarness,也没有 skill() Method。
4. 第一组测试:Skill 能否正确加载
这一层直接测试 loadSkillsFromDir():
const result = loadSkillsFromDir({
dir: skillDir,
source: "test",
});
expect(result.skills).toHaveLength(1);
expect(result.skills[0].name).toBe("error-log-analysis");
expect(result.diagnostics).toEqual([]);
至少覆盖:
- 合法
SKILL.md; - 缺失 Description;
- 非法 YAML;
- Name 与目录名不一致;
disable-model-invocation;- 嵌套目录;
- 两个相同 Name 的 Skill。
Pi 的现有 skills.test.ts 就以 Fixture 测这些边界。发生同名冲突时,先进入 Map 的 Skill
保留,后来的被跳过并产生诊断。因此测试不仅要断言数量,还要断言赢家的 sourceInfo。
5. 第二组测试:显式调用一定展开正文
显式调用适合做稳定回归:
let modelSaw = "";
harness.setResponses([
(context) => {
const user = context.messages.find((message) => message.role === "user");
modelSaw = user ? getMessageText(user) : "";
return fauxAssistantMessage("ok");
},
]);
await harness.session.prompt(
"/skill:error-log-analysis 分析 server.log",
);
expect(modelSaw).toContain(
'<skill name="error-log-analysis" location="',
);
expect(modelSaw).toContain("summarize-log.mjs");
expect(modelSaw).toContain("分析 server.log");
这个形状来自 Pi 自身的 agent-session-prompt.test.ts。它精确验证:
- Skill 按 Name 找到;
- Frontmatter 后的正文被包装;
- 用户附加参数保留;
- 展开发生在模型请求之前。
6. 怎样测试“正确触发”
这里必须先说清 Faux 的边界。
如果测试预先返回 read(SKILL.md),它证明的是:
当模型选择这个 Skill 时,Pi 能正确执行读取和后续流程。
它不能证明:
某个真实模型看到这句话时一定会选择这个 Skill。
所以“正确触发”分为两种测试:
确定性框架测试
捕获第一次模型请求,断言 System Prompt 中存在:
<name>error-log-analysis</name>
<description>...</description>
<location>...</location>
再让 Faux 返回 Read Tool Call,断言读取的绝对路径正是该 Skill。
真实模型评测
准备固定用例集:
[
{
"input": "分析 server.log 中反复超时的原因",
"expectedSkill": "error-log-analysis"
},
{
"input": "把这段英文翻译成中文",
"expectedSkill": null
}
]
在明确选择的模型和版本上重复运行,统计选择率。这个结果属于模型行为,不应写成 Pi Core 的保证。
7. 怎样测试“错误触发”
错误触发有两类:
- False Positive:普通任务误读了日志 Skill;
- Wrong Skill:日志任务读了另一个相似 Skill。
Faux 测试应先保证目录没有布线错误:
expect(systemPrompt).toContain("<name>error-log-analysis</name>");
expect(systemPrompt).not.toContain("<name>disabled-skill</name>");
真实模型评测再使用“相邻但不应触发”的输入:
- “解释 HTTP 504 的一般含义”;
- “生成一份空白事故报告模板”;
- “把日志文件重命名”;
- “分析一段没有日志的 TypeScript 报错描述”。
不要只测试正例。Description 越宽泛,False Positive 越容易被正例测试掩盖。
8. 测试 Tool 调用顺序
为 error-log-analysis 规定:
read SKILL.md
→ read 日志
→ 运行 summarize-log.mjs
→ read 报告格式
→ 输出结论
测试订阅 tool_execution_start:
const started: string[] = [];
harness.session.subscribe((event) => {
if (event.type === "tool_execution_start") {
started.push(event.toolName);
}
});
await harness.session.prompt("分析日志");
expect(started).toEqual([
"read",
"read",
"bash",
"read",
]);
如果同一次模型响应发出多个 Tool Call,Pi Core 可以并行执行它们。需要严格顺序的步骤, Skill 应明确要求等待前一步结果,并让模型分多个 Turn 发调用;不能仅靠数组排列暗示依赖。
9. 不只检查 Tool Name
只断言 ["read", "bash"] 太弱,还要检查:
- Argument 是否是预期绝对路径;
- Script 是否使用固定入口;
- Tool Result 是否被下一次模型请求看到;
- 最终回答是否引用了 Result 中的证据;
- 未授权目录有没有被访问。
桌面 Agent 可在测试 Tool 中记录:
const calls: Array<{ name: string; args: unknown }> = [];
然后对 Arguments 做结构化断言。安全测试尤其不能只看最终自然语言回答。
10. 测试脚本失败
让内置 Bash 以非零状态退出,或让自定义 Tool throw,再给 Faux 一个最终响应。断言:
tool_execution_end标记错误;- Error Tool Result 进入后续 Context;
- 后续没有执行会修改文件的 Tool;
- 最终回答明确告诉用户失败,不能伪造报告。
如果自定义 Tool 只是成功返回一个带 error 字段的普通对象,Core 仍把它当成功 Result;
那是在测试“错误数据”,不是 isError=true 的 Tool Error。要形成 Tool Error,需要执行
抛错、参数校验/Tool 查找失败,或由相应 Hook 明确改写错误状态。
Skill 没有统一的 skill_error 事件。脚本失败仍是普通 Tool Error,所以测试要监听 Tool
事件和消息,而不是等待不存在的 Skill 生命周期。
11. 测试文件读取失败
需要分别测试两条路径:
| 路径 | 失败发生处 | 应观察什么 |
|---|---|---|
| 自动发现后读取 | Read Tool | Tool Error / Result |
/skill:name 展开 |
AgentSession 同步读取 | skill_expansion Error,原文本继续 |
这两个错误通道不同。若桌面 UI 把它们都展示为“Skill 启动失败”,可以统一用户体验,但测试 仍应保留底层原因。
12. 测试用户中断
构造一个等待 AbortSignal 的慢 Tool:
execute: async (_id, _params, signal) => {
await waitUntilAborted(signal);
throw new Error("aborted");
}
当收到 tool_execution_start 后调用 Session 的中断能力,并断言:
- Slow Tool 收到 Abort Signal;
- Agent Run 结束;
- 下一 Turn 才会产生的后续危险 Tool 没有启动;
- UI 不再显示“仍在运行”;
- 临时文件得到清理。
不要只在 Tool 尚未开始时取消;最容易泄漏资源的是执行中断。默认 Parallel 的同一批 Tool Call 可能已经同时启动,Abort 不会追溯撤销已发生的副作用。若要断言“同批后续 Tool 未 启动”,测试必须明确使用 Sequential Execution;更简单的做法是把危险 Tool 放在下一次 Faux Response。
13. 一张最小测试矩阵
| 用例 | 加载 | Prompt/展开 | Tool | 最终结果 |
|---|---|---|---|---|
| 正确显式调用 | 1 个 Skill | 有 Skill Block | 按顺序 | 有证据 |
| 正确自动路径 | 目录可见 | 有 Metadata | Read 正确位置 | 完成 |
| 不相关任务 | 目录可见 | 有 Metadata | 不读 Skill | 普通回答 |
| 隐藏 Skill | 已加载 | 目录无 Metadata | 无自动读取 | 可显式调用 |
| 脚本失败 | 已加载 | 正常 | Error | 停止并说明 |
| 用户中断 | 已加载 | 正常 | 收到 Abort | 无后续调用 |
| 同名冲突 | 2 个来源 | 只见赢家 | 不读输家 | 有诊断 |
这张表也适合成为桌面 Agent 的发布门禁。
14. 项目级与用户级 Skill
coding-agent 的默认发现入口包括:
项目:<cwd>/.pi/skills
项目:cwd 及祖先的 .agents/skills(到 Git Root 或 Filesystem Root,受 Trust 约束)
用户:~/.pi/agent/skills
用户:~/.agents/skills
.pi/skills 与 .agents/skills 的扫描细节和显式路径例外已在第 18 章展开;这里按治理范围
把它们都归为项目级或用户级。
项目目录适合:
- 与仓库代码、日志格式和团队流程绑定;
- 随代码审查;
- 在 CI 中测试;
- 团队共享同一版本。
用户目录适合:
- 个人工作习惯;
- 跨项目通用能力;
- 不应提交到业务仓库的个人配置。
项目设置中的 Package 会在项目被 Trust 后补装。Trust 不是 Skill 内容审计的替代品;它只 是宿主决定是否接受这个项目环境的一道边界。
15. 把 Skill 放进 Pi Package
最小包:
my-log-skills/
├── package.json
└── skills/
└── error-log-analysis/
├── SKILL.md
├── scripts/
├── references/
└── assets/
显式 Manifest:
{
"name": "@team/pi-log-skills",
"version": "1.2.0",
"keywords": ["pi-package"],
"pi": {
"skills": ["./skills"]
}
}
没有 pi Manifest 时,Package Manager 也会按约定扫描顶层 skills/。Manifest 更适合
公开包,因为资源边界更明确,还能使用 Glob 和排除规则。
16. 三种 Package 来源
npm:@team/pi-log-skills@1.2.0
git:github.com/team/pi-log-skills@v1.2.0
./local/pi-log-skills
- npm 带版本的 Spec 被 Pin,更新 Package 时跳过;
- Git 的 Tag 或 Commit 是固定 Ref;Update 会校准到该 Ref,不会自动漂到更新 Ref;
- Local Path 不复制文件,Settings 中的相对路径以该 Settings File 为基准解析。
想让团队复现,优先使用精确 npm 版本或 Git Commit/Tag。开发中的本地路径适合快速迭代, 不等于可复现发布。
17. 用户范围与项目范围安装
默认 pi install 写用户 Settings;-l 写项目 Settings:
用户:~/.pi/agent/settings.json
项目:.pi/settings.json
相应安装位置也不同:
| 来源 | 用户范围 | 项目范围 |
|---|---|---|
| npm | ~/.pi/agent/npm/ |
.pi/npm/ |
| Git | ~/.pi/agent/git/ |
.pi/git/ |
同一个 Package 同时出现在两个范围时,项目条目通常胜出;如果项目条目
autoload: false,它会作为对用户条目的差量配置。身份依据是 npm Package Name、去掉 Ref
的 Git Repository URL,或解析后的 Local Absolute Path。
18. 更新策略
发布前至少保存:
- Package Version;
- Git Commit;
- 每个 Skill 的 Name 与内容 Hash;
- 测试结果;
- 权限清单;
- Changelog。
更新流程建议:
flowchart LR
A["发现新版本"] --> B["下载到隔离区"]
B --> C["Diff Skill/脚本/依赖"]
C --> D["自动测试"]
D --> E{"权限扩大?"}
E -->|是| F["重新请求用户批准"]
E -->|否| G["灰度启用"]
F --> G
G --> H["保留可回退版本"]
流程图是桌面 Marketplace 的产品治理建议,不是 Pi Core 已内置的完整更新工作流。
19. 同名冲突怎样治理
Pi 的 Skill 合并语义是“先加载者保留”,并发出 Name Collision Diagnostic。不要把加载 顺序当作隐式版本选择器。
治理规则应当是:
- Name 带清楚的 Domain;
- 启动时把 Collision Diagnostic 提升到可见警告;
- 测试断言实际赢家的 SourceInfo;
- Marketplace 安装前显示将被覆盖或被跳过的资源;
- 用户明确选择后再调整启用范围或 Package Filter。
Pi Package Filter 可以用 skills: [] 禁用某 Package 的全部 Skill,或通过 Pattern、
+path、-path 缩小资源集合。
20. 不可信 Skill 的真实风险
Skill 是 Markdown,但不代表它只是文档。它可以指示模型:
- 读取私密文件;
- 把内容发送到外部服务;
- 执行随包携带的二进制或脚本;
- 修改、删除项目文件;
- 忽略宿主安全规则;
- 读取外部内容后继续接受 Prompt Injection。
Pi 文档明确警告:Package 拥有完整系统访问能力;Extension 能执行任意代码,Skill 能指示 模型执行包括启动程序在内的操作。安装前必须审查源码。
21. 脚本和外部资源
审查 Skill 时沿引用链继续:
SKILL.md
├── scripts/*.mjs
├── references/*.md
├── assets/*
├── package.json dependencies
└── 外部 URL / 下载内容
重点检查:
- Script 是否读取 Environment Secret;
- 是否访问网络、上传文件;
- 是否使用未固定 Dependency;
- 是否拼接未经处理的 Shell Argument;
- 是否写入 Skill Directory 以外;
- 外部文档是否会被当成可信指令;
- 下载结果是否验证来源与 Hash。
“SKILL.md 看起来安全”不能推出它引用的脚本和依赖安全。
22. 桌面 Agent 的执行保护
Marketplace 不能只靠审核页面。Runtime 还应提供:
- 最小 Tool 集;
- 文件系统 Allowlist;
- 网络 Domain Allowlist;
- 敏感 Tool 的逐次审批;
- 参数预览;
- Timeout 与 Abort;
- Output Size 限制;
- Tool Call 审计日志;
- Secret Redaction;
- 临时工作目录。
这些保护主要由桌面宿主的 Tool Policy 与 Extension 实现。Skill 自己写“请先询问用户”只是 软约束,不能代替宿主强制审批。
23. Marketplace 最小数据模型
Pi 源码提供 Package 安装、来源与资源加载机制;下面是桌面 Marketplace 在其上需要增加的 产品层:
interface MarketplaceSkillRecord {
packageSource: string;
resolvedVersion: string;
contentHash: string;
publisher: string;
requestedCapabilities: string[];
reviewStatus: "unreviewed" | "reviewed" | "blocked";
testReportId: string;
}
还应记录:
- 发布者身份和签名;
- Skill、Script、Dependency 的 Diff;
- 支持的 Pi Version;
- 自动与显式调用策略;
- 已知风险和撤回状态;
- 用户安装时批准的能力。
这些字段不是当前 Pi Package Manifest 的内置 Schema,不能把产品建议伪装成 Core API。
24. 安装与更新时的用户体验
安装前展示:
来源:npm:@team/pi-log-skills@1.2.0
包含:2 Skills、1 Script、0 Extensions
请求:读取项目日志;执行 node 脚本
网络:不需要
冲突:error-log-analysis 与用户级 Skill 同名
更新时展示“新增能力”,而不是只展示 Version Number。若新版本从“只读日志”变成“执行 Shell 并访问网络”,必须重新批准。
Pi Core 不会替桌面产品推导完整 Capability Manifest;Marketplace 需要静态扫描、人工声明 与运行时监测共同完成。
25. 从开发到发布的完整路径
flowchart TD
A["编写 SKILL.md"] --> B["加载与 Frontmatter 测试"]
B --> C["显式展开测试"]
C --> D["Faux Tool 流程测试"]
D --> E["真实模型触发评测"]
E --> F["安全审查脚本与依赖"]
F --> G["打包并固定版本"]
G --> H["隔离安装与冲突检查"]
H --> I["用户批准能力"]
I --> J["灰度发布与审计"]
J --> K["更新或回退"]
前四步找功能问题,中间三步建立供应链边界,最后三步处理真实使用中的风险。
26. 常见误区
“Faux 测试通过,说明模型一定会触发”
错误。Faux 的选择由测试预定。真实模型触发要另做评测。
“Skill 是 Markdown,所以安装没有风险”
错误。模型可以依据 Markdown 调用高权限 Tool,Skill 还可以携带脚本和外部引用。
“有项目 Trust 就不用审查 Package”
错误。Trust 决定是否接受项目环境,不证明每个 Package 内容可信。
“同名 Skill 会自动选最新版”
错误。Skill 合并是先到先得并产生诊断,不按 SemVer 选择。
“Git Tag 会被 Update 自动推进”
错误。固定 Ref 只会被校准到配置的 Ref;要换 Ref,需要更新安装来源。
“Marketplace 审核过就不需要 Runtime Policy”
错误。静态审核不能覆盖运行时 Argument、外部内容和用户数据。
27. 本章小结
- Skill 测试应拆成加载、展开、流程和真实模型评测四层;
- Faux Provider 让真正的 Agent Loop 在无网络条件下执行预定模型响应;
- Faux 能验证目录、Tool 和错误布线,不能证明语义触发准确率;
- Tool 顺序、Argument、Result、错误和 Abort 都要单独断言;
- 项目级 Skill 适合团队流程,用户级 Skill 适合个人跨项目能力;
- Pi Package 可从 npm、Git 或 Local Path 分发,并支持用户与项目范围;
- 固定 npm Version 或 Git Ref 才能建立可复现基线;
- 同名 Skill 先到先得,必须展示 Diagnostic,不能暗中依赖顺序;
- 第三方 Skill、Script、Dependency 和外部内容都属于供应链风险;
- Marketplace 治理是桌面宿主在 Pi Package 机制上增加的产品层;
- 安全最终依赖宿主 Tool Policy、审批、隔离、审计和可回退更新。
28. 自测
- Skill 的四层测试分别回答什么问题?
- 为什么 Faux Provider 不能证明真实模型会正确触发?
- 怎样验证
/skill:name在模型请求前已经展开? - Tool 顺序测试为什么还要检查 Argument 和 Result?
- 自动读取失败与显式展开失败分别走什么通道?
- 中断测试至少要断言哪四件事?
- 项目级与用户级 Skill 各适合什么场景?
- npm Pin、Git Ref 和 Local Path 的更新语义有什么不同?
- 同名 Skill 冲突时 Pi 怎样选,桌面产品又该怎样展示?
- 为什么 Marketplace 审核不能代替 Runtime Tool Policy?