现在桌面 Agent 已经能对话、调用工具、保存 Session,也能在运行中接收新指令。接下来出现一个产品问题:
同一个 Agent 进入不同项目时,怎样自动遵守各自的规则?
在支付项目中,模型可能必须先跑安全检查;在文档项目中,它可能只能修改 Markdown;某位 用户又希望所有回答简洁。如果把这些规则写死成一个字符串,配置、工具和项目一变化, Prompt 就会过时。
Pi 的做法是:先加载资源,再根据当前工具和目录动态构建 Base System Prompt;每次 Agent Run 开始前,Extension 还有一次临时改写机会。
1. 先看最终流水线
flowchart LR
D["默认提示词或 SYSTEM.md"] --> B["buildSystemPrompt"]
A["APPEND_SYSTEM.md"] --> B
C["AGENTS.md / CLAUDE.md"] --> B
T["当前启用工具的说明与规则(默认主体)"] --> B
S["已发现 Skill 的目录"] --> B
W["当前工作目录"] --> B
B --> BASE["Base System Prompt"]
BASE --> E["before_agent_start Extensions"]
E --> RUN["本次 Run 的 System Prompt"]
这里至少有三个时间点:
ResourceLoader.reload()发现并读取资源;_rebuildSystemPrompt()收集当前状态并调用 Builder;before_agent_start在某一次 Run 前顺序处理 Extension 的修改。
因此“System Prompt 是什么”不能只看一个 Markdown 文件,要看整条构建链。
2. System Prompt 负责什么
Pi 默认 Prompt 主要声明:
- Agent 的角色;
- 当前可见工具的简短说明;
- 与当前工具匹配的操作准则;
- Pi 自身文档的位置;
- 项目级指令;
- 可发现的 Skills;
- 当前工作目录。
它提供长期、跨 Turn 的行为背景。普通 User Prompt 则表达这次要做什么。
System Prompt:你是谁、环境是什么、长期规则是什么
User Prompt:这一次用户要完成什么
3. Resource Loader 先准备原料
SDK 在没有收到自定义 Loader 时,会创建:
const resourceLoader =
new DefaultResourceLoader({
cwd,
agentDir,
settingsManager,
});
await resourceLoader.reload();
Loader 不只加载 Prompt,还统一管理:
- Extensions;
- Skills;
- Prompt Templates;
- Themes;
- Context Files;
SYSTEM.md;APPEND_SYSTEM.md。
这解释了它为什么叫 Resource Loader,而不是 Prompt Loader。System Prompt 只是这些资源的 一个消费者。
4. Context File 怎样发现
每个目录最多选择一个上下文文件,候选顺序是:
AGENTS.md
AGENTS.MD
CLAUDE.md
CLAUDE.MD
找到第一个存在且可读的文件就停止,不会把同一目录里的 AGENTS.md 和 CLAUDE.md
同时加入。
这是一条容易忽略的覆盖规则:同一目录中,AGENTS.md 优先。
5. Context Files 的完整加载顺序
loadProjectContextFiles() 先加载用户级 Agent 目录中的 Context File,然后从当前工作目录
向文件系统根逐级查找项目文件。
项目文件在结果中按“祖先到当前目录”排列:
用户级:<agentDir>/AGENTS.md
项目级:/AGENTS.md
项目级:/workspace/AGENTS.md
项目级:/workspace/app/AGENTS.md
项目级:/workspace/app/src/AGENTS.md
假设 cwd 是 /workspace/app/src,后四项就是从宽泛规则逐步走到具体规则。源码用
unshift() 构造这段顺序。
flowchart TD
G["用户级 Context"] --> R["文件系统较高层 Context"]
R --> P["项目根 Context"]
P --> C["cwd 附近 Context"]
C --> X["按此顺序写入 project_context"]
Builder 不解释“后面的规则一定覆盖前面”。它只是按顺序把带路径的内容交给模型。真正的 冲突处理仍依赖模型理解,所以项目规则应尽量避免互相矛盾。
这里还有一个重要的安全边界:loadProjectContextFiles() 本身不检查项目 Trust。
cwd 和祖先目录里的 AGENTS.md / CLAUDE.md 会被加载。Trust 检查约束的是后文所讲
项目 .pi/SYSTEM.md、.pi/APPEND_SYSTEM.md 等自动发现路径,不能把它泛化到所有
Context File。
6. Context Files 怎样进入 Prompt
每份文件会被包装为:
<project_context>
Project-specific instructions and guidelines:
<project_instructions path="/workspace/AGENTS.md">
...
</project_instructions>
</project_context>
路径很重要。它让模型知道一条规则来自用户级目录、项目根,还是更具体的子目录。
Context File 不是新的 User Message,也不会成为独立 Session Entry。它是 System Prompt 的一部分,每次模型请求都随当前 Prompt 状态发送。
7. 默认 Prompt 与 SYSTEM.md 的关系
没有自定义 System Prompt 时,Builder 使用 Pi 内置模板。
发现 SYSTEM.md 后,它作为 customPrompt:
if (customPrompt) {
let prompt = customPrompt;
// 再追加其他部分
}
也就是说,SYSTEM.md 替换内置主体,不是附加在默认主体之后。但以下内容仍会继续追加:
- Append System Prompt;
- Context Files;
- Skills 目录;
- Current Working Directory。
所以“自定义 System Prompt”也不等于“最终 Prompt 就只有这个文件”。
8. 用户级与项目级 SYSTEM.md
自动发现顺序体现了项目覆盖用户:
- 如果项目受信任且
<cwd>/.pi/SYSTEM.md存在,使用项目文件; - 否则尝试用户 Agent 目录下的
SYSTEM.md; - 都没有则使用内置 Prompt。
项目 SYSTEM.md 不是向上逐层搜索。源码只检查当前 cwd 下的 .pi/SYSTEM.md。
“项目受信任”也是安全边界:未受信任项目不能仅靠本地 .pi/SYSTEM.md 替换 Agent 的身份
和规则。
这个边界很窄:
- 它不阻止
AGENTS.md/CLAUDE.md进入 Project Context; - 宿主显式传入
systemPrompt或 Append Source 时,使用的是显式来源,不走自动发现判断。
因此桌面宿主仍要决定何时允许打开项目、读取 Context 和启用有副作用的 Tool。
9. APPEND_SYSTEM.md 不同在哪里
APPEND_SYSTEM.md 不替换主体,而是作为:
appendSystemPrompt
插在 Base Prompt 主体之后、Project Context 之前。
自动发现同样优先使用受信任项目的 .pi/APPEND_SYSTEM.md,否则使用用户级文件。自动发现
路径并不会把项目文件和用户文件同时合并;只有显式传入多个 Append Source 时,Loader
才会加载多项,AgentSession 用两个换行连接它们。
选择建议:
- 想完全控制角色和默认规则:
SYSTEM.md; - 只想增加少量约束而保留 Pi 默认说明:
APPEND_SYSTEM.md; - 想声明项目开发规范:
AGENTS.md。
10. 当前工具怎样影响默认 Prompt
AgentSession._rebuildSystemPrompt(toolNames) 只处理当前有效的工具名:
const validToolNames =
toolNames.filter(
(name) => toolRegistry.has(name),
);
然后从工具注册信息收集:
- 一行
promptSnippet; - 若干
promptGuidelines。
这些内容只在 Pi 的默认内置主体分支生成。默认 Prompt 的 Available tools 列表只展示有
Snippet 的启用工具。启用了工具却没有 Snippet,该工具仍可出现在模型请求的 Tool
Definitions 中,只是不会出现在这段文字列表里。
如果 SYSTEM.md 提供了 customPrompt,Builder 会跳过默认 Available tools 和
Guidelines 主体;虽然 AgentSession 仍收集并传入 Snippet/Guideline,custom 分支并不
使用它们。自定义主体的作者需要自行决定是否写工具说明。
因此要区分:
Tool Definition:机器可调用的 name、description、parameters
Prompt Snippet:System Prompt 中给模型看的简短目录
Prompt Guideline:与该工具配套的行为规则
11. 为什么 Tool 不只是 Prompt
工具的真实能力不由 System Prompt 创造。模型能否调用工具,取决于请求 Context 中的 结构化 Tool Definition。
Prompt 可以说“使用 read 查看文件”,但没有注册 read Tool,模型无法产生一个可执行的
真实调用。反过来,已注册 Tool 即使没有 Snippet,仍有结构化描述可供 Provider 使用。
所以:
- Prompt 负责解释行为和策略;
- Tool 负责提供可执行能力与参数契约。
12. Skills 为什么只放目录
Builder 会调用:
formatSkillsForPrompt(skills)
这里放入的是模型可见 Skill 的名称、描述和文件位置,以及按需读取的使用说明,而不是把
每份 SKILL.md 全文都塞进 System Prompt。设置了
disableModelInvocation: true 的 Skill 会被过滤,因此“已加载”不等于“一定出现在目录”。
好处是:
- Prompt 不会因 Skill 数量快速膨胀;
- 模型先依据描述选择相关 Skill;
- 需要时再用
read打开完整指令; - 专业流程可独立维护。
如果当前工具集合没有 read,Builder 不附加 Skills Section,因为模型知道路径却无法读取
内容没有意义。
13. Prompt、Tool、Skill 的职责边界
| 机制 | 解决的问题 | 是否直接执行 |
|---|---|---|
| System Prompt | 长期角色、环境、规则和资源导航 | 否 |
| Tool | 提供结构化的外部动作 | 是 |
| Skill | 告诉 Agent 某类任务应按什么流程完成 | Skill 本身不执行,通常会指导使用 Tool |
以“代码审查”为例:
System Prompt:遵守项目规则,修改前先阅读上下文
Skill:代码审查的检查步骤、证据要求和输出结构
Tool:read 读取代码,bash 运行检查,edit 修改文件
把所有审查细节都塞进 System Prompt,会让每次无关请求也支付 Context 成本;把审查流程 写成 Tool Description,又混淆了“能力”和“方法”。Skill 是更合适的复用层。
14. Builder 的精确拼接顺序
默认主体与自定义主体虽然不同,但后半段顺序一致:
默认内置主体 或 SYSTEM.md
→ appendSystemPrompt
→ <project_context>Context Files</project_context>
→ Skills Section(需要 read)
→ Current working directory
内置主体内部还包含:
角色
→ Available tools
→ Guidelines
→ Pi documentation
工具 Guidelines 会去重;空白规则会被忽略。最后固定加入:
- Be concise in your responses;
- Show file paths clearly when working with files。
15. 什么时候重新构建
System Prompt 不是每个 Token 都重新拼接。常见重建点包括:
AgentSession初始化工具注册完成后;setActiveToolsByName()改变启用工具;- Extension 在启动或 Reload 时发现额外资源;
- Resource Reload 后 Session 重新装配资源。
例如切换工具:
session.setActiveToolsByName([
"read",
"search",
]);
代码同时更新 agent.state.tools 并重新运行 Base Prompt Builder。使用默认主体时,工具
文字说明会随之更新;使用自定义 SYSTEM.md 时,Builder 不会改写作者写在其中的工具说明,
所以重建不保证自定义文字与真实工具集合自动同步。
16. Extension 如何发现 Prompt 原料
Extension 的 resources_discover 可以返回额外的:
- Skill Paths;
- Prompt Template Paths;
- Theme Paths。
这个启动链不是 createAgentSession() 单独完成的。桌面宿主还必须调用
session.bindExtensions(...);绑定后 Session 才发 session_start,运行
resources_discover,把路径交给 Resource Loader,并重新构建 Base System Prompt。
AgentSession.reload() 会先重载设置和 Resource Loader、重建 Extension Runtime。只有
保留了 UI Context、Command Context Actions、Shutdown Handler 或 Error Listener 中至少
一种 Binding 时,它随后才重新绑定、再次触发启动/发现链并重建 Prompt。空的
bindExtensions({}),或只绑定 mode / abortHandler,都不满足这个 hasBindings
判断。宿主不能笼统假设 Reload 一定运行 resources_discover。
注意:这个事件不直接返回一段 System Prompt。它扩展的是资源来源。
17. Extension 如何改写本次 Prompt
每次普通 Prompt 真正运行前,coding-agent 发出:
before_agent_start
事件包含:
inputHandler 转换、Skill Command 与 Prompt Template 展开后的 Prompt,以及图片;- 当前 Base System Prompt;
BuildSystemPromptOptions。
Handler 可以返回:
- 一条 Custom Message;
- 修改后的
systemPrompt; - 两者同时返回。
多个 Extension 按加载顺序执行。后一个 Handler 看到的是前一个 Handler 已修改过的
currentSystemPrompt,因此它们形成串行管道,而不是并行投票。
sequenceDiagram
participant B as Base Prompt
participant E1 as Extension A
participant E2 as Extension B
participant R as Agent Run
B->>E1: currentSystemPrompt
E1-->>E2: 修改版 A
E2-->>R: 修改版 B
18. 临时 Override 只属于一次 Run
before_agent_start 返回修改版后,AgentSession 设置:
this._systemPromptOverride = result.systemPrompt;
this.agent.state.systemPrompt = result.systemPrompt;
本次 Agent Run 结束时,finally 只清除 _systemPromptOverride 标记,并没有立即把
agent.state.systemPrompt 写回 Base。因此 idle 期间读取 session.systemPrompt,仍可能
看到上一轮的临时文本。下一次普通 Prompt 若 Extension 不再修改,preflight 分支才把
agent.state.systemPrompt 恢复为 Base;工具/资源重建也会更新它。
所以 Override 不会自动带入下一次正常 Run,但“Run 一结束 getter 就立刻恢复”并不成立。
这适合:
- 某一轮进入“只读分析模式”;
- 根据当前 User Prompt 临时增加格式要求;
- 为一次高风险操作加入额外检查。
不适合把长期配置只存在内存 Override 中。长期规则应进入 Resource Loader 的输入。
19. 一个桌面 Agent 的构建例子
假设项目中有:
~/.pi/agent/AGENTS.md
/workspace/AGENTS.md
/workspace/app/AGENTS.md
/workspace/app/.pi/APPEND_SYSTEM.md
/workspace/app/.pi/skills/security-review/SKILL.md
用户在 /workspace/app 打开桌面 Agent,启用 read、bash、edit。
构建过程是:
- Loader 解析设置和受信任状态;
- 加载用户级 Context;
- 从祖先到
cwd加载项目 Context; - 发现项目 Append Prompt;
- 发现并校验 Skill;
- Session 收集当前工具 Snippet 和 Guidelines;
- Builder 拼出 Base Prompt;
- 宿主绑定 Extension,资源发现可能扩充 Skills 并再次构建;
- 用户发送请求,输入先经过转换和命令/模板展开;
before_agent_startExtension 按顺序处理;- Agent 用本次最终 Prompt、Messages 和 Tool Definitions 请求模型。
flowchart TD
OPEN["打开项目"] --> LOAD["ResourceLoader.reload"]
LOAD --> BUILD["重建 Base Prompt"]
SEND["用户发送消息"] --> EXT["before_agent_start"]
BUILD --> EXT
EXT --> CONTEXT["System Prompt + Messages + Tools"]
CONTEXT --> MODEL["模型请求"]
20. 宿主实现建议
桌面产品至少应把这些状态分开:
type PromptState = {
basePrompt: string;
runOverride?: string;
contextFiles: Array<{
path: string;
content: string;
}>;
activeToolNames: string[];
skillNames: string[];
};
这不是 Pi 的原样类型,而是产品侧状态建议。调试页应能回答:
- 当前用了默认主体还是
SYSTEM.md? - 哪些 Context Files 被加载,顺序是什么?
- 哪些工具说明进入了 Prompt?
- 哪些 Skills 可发现?
- 哪个 Extension 改写了本次 Prompt?
不要只展示最终长字符串。没有来源信息,用户很难解释 Agent 为什么遵守或忽略某条规则。
21. 常见误区
“SYSTEM.md 就是最终 System Prompt”
错误。它替换主体,但 Append、Context、Skills 和 cwd 仍可能追加。
“所有祖先目录的 SYSTEM.md 都会合并”
错误。逐级发现的是 Context File;项目 SYSTEM.md 只检查 cwd/.pi/SYSTEM.md。
“同一目录的 AGENTS.md 和 CLAUDE.md 会同时加载”
错误。候选有优先顺序,每个目录最多选择一份。
“Prompt 写了工具名,工具就能执行”
错误。真实调用能力来自结构化 Tool Definition。
“Skill 全文常驻 System Prompt”
错误。默认只放发现目录,完整内容按需读取。
“Extension 改写一次后会永久覆盖”
错误。Override 标记在本次 Run 后清除,不会自动带入下一次正常 Run;但 idle 时 getter 仍可能暂时显示上一轮文本,直到下一次 preflight 或重建恢复 Base。
22. 本章小结
- Pi 的 System Prompt 是 Resource Loading、动态构建和 Run 前扩展共同产生的;
- 用户级 Context 先加载,项目 Context 按祖先到 cwd 排列;
- 每个目录只选一份 Context File,
AGENTS.md优先; - Trust 约束项目
.pi/SYSTEM.md/APPEND_SYSTEM.md的自动发现,但不约束AGENTS.md/CLAUDE.mdProject Context; - 默认 Builder 会加入工具说明和规则;自定义 SYSTEM 主体不会自动生成这两段;
- Builder 还会加入模型可见 Skills 目录和 cwd;
- Tool Definition 决定可执行能力,Prompt 解释策略,Skill封装专业流程;
- 启用工具或扩展资源变化时会重建 Base Prompt;
resources_discover依赖宿主先绑定 Extension;before_agent_start接收展开后的输入并串行改写本次 Prompt;Override 标记不跨 Run, 但 idle getter 可能暂时保留旧文本;- 桌面调试界面应显示 Prompt 各部分的来源,而不只是最终字符串。
23. 自测
- 为什么不能把 System Prompt 理解成一个固定文件?
- 用户级和项目级 Context Files 按什么顺序加载?
SYSTEM.md与APPEND_SYSTEM.md的区别是什么?- 自定义主体后,哪些内容仍会追加?
- 为什么启用 Tool 不代表它一定出现在文字版 Available Tools 中?
- 没有
readTool 时,为什么不附加 Skills 目录? resources_discover与before_agent_start分别怎样参与构建?- 本次 Run 的 Extension Override 何时清除?