返回课程首页

15

System Prompt 不是一段固定文字

理解动态 Prompt、项目上下文和资源合并。

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

现在桌面 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"]

这里至少有三个时间点:

  1. ResourceLoader.reload() 发现并读取资源;
  2. _rebuildSystemPrompt() 收集当前状态并调用 Builder;
  3. 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.mdCLAUDE.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

自动发现顺序体现了项目覆盖用户:

  1. 如果项目受信任且 <cwd>/.pi/SYSTEM.md 存在,使用项目文件;
  2. 否则尝试用户 Agent 目录下的 SYSTEM.md
  3. 都没有则使用内置 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 toolsGuidelines 主体;虽然 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

事件包含:

  • input Handler 转换、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,启用 readbashedit

构建过程是:

  1. Loader 解析设置和受信任状态;
  2. 加载用户级 Context;
  3. 从祖先到 cwd 加载项目 Context;
  4. 发现项目 Append Prompt;
  5. 发现并校验 Skill;
  6. Session 收集当前工具 Snippet 和 Guidelines;
  7. Builder 拼出 Base Prompt;
  8. 宿主绑定 Extension,资源发现可能扩充 Skills 并再次构建;
  9. 用户发送请求,输入先经过转换和命令/模板展开;
  10. before_agent_start Extension 按顺序处理;
  11. 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.md Project 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. 自测

  1. 为什么不能把 System Prompt 理解成一个固定文件?
  2. 用户级和项目级 Context Files 按什么顺序加载?
  3. SYSTEM.mdAPPEND_SYSTEM.md 的区别是什么?
  4. 自定义主体后,哪些内容仍会追加?
  5. 为什么启用 Tool 不代表它一定出现在文字版 Available Tools 中?
  6. 没有 read Tool 时,为什么不附加 Skills 目录?
  7. resources_discoverbefore_agent_start 分别怎样参与构建?
  8. 本次 Run 的 Extension Override 何时清除?