6.7 系统提示词与 Prompt Templates
本页分析版本earendil-works/pi@16787ad2026-09-21本章解决什么问题:模型每一轮看到的那段「你是谁、你有什么工具、这个项目有什么规矩」的系统提示词(System Prompt),到底是由哪个函数、按什么顺序、从哪些文件拼出来的?
--system-prompt、AGENTS.md、SYSTEM.md各自作用在这条流水线的哪一段?以及:你在输入框里敲的/cl、/pr这类斜杠命令,是模型的能力还是客户端的字符串替换? 前置知识:3.2 消息、上下文与 token、3.7 Compaction、Extension 与 Skill、5.1 启动:pi 命令如何跑起来、6.4 工具系统:定义、校验与执行。 学习目标:读完后你能 ① 背出buildSystemPromptSections的八个段落及其顺序;② 说清三条自定义通道(CLI 参数 /SYSTEM.md/ 扩展运行时覆盖)分别在哪一行接入;③ 解释拼好的提示词怎样变成 transcript 里的一条 system 消息,以及为什么中途改了一段只需要补发那一段;④ 解释 prompt template 为什么「模型完全不知道它存在」,并写出$1、$@、${2:-默认值}各自的替换规则;⑤ 指出 coding-agent 与 pi-agent-core 在提示词这件事上的分工差异。
建立直觉:系统提示词是一次纯粹的字符串拼接
role: "system" 消息,发请求时再由各家 provider 放到 API 要求的位置。模型无法「回忆」它是怎么来的——对模型来说它只是一段先于所有消息出现的文字。 初学者容易把系统提示词想象成某种运行时状态:以为 Pi 会「注册」工具、「安装」技能(Skill),模型那边有一份对应的清单。事实要朴素得多:Pi 把这些信息全部渲染成文本,按名字分成若干段,每段用同名的 XML 标签包起来,最后连成一个字符串。工具清单是文本,技能目录是 XML 文本,AGENTS.md 的内容是被 <project_instructions> 包起来的文本。
为什么值得强调这一点?因为一旦接受「它只是字符串拼接」,后面所有问题都变成了可回答的:拼接顺序是什么?谁负责拼?拼完存在哪、什么时候重拼?没有它会怎样——模型不知道自己有 edit 工具能改文件,也不知道这个项目要求提交前跑 lint。
AgentSession 缓存的是组装提示词用的那份输入(_baseSystemPromptOptions,里面有 AGENTS.md 的内容、技能列表、工具说明等),只有 _rebuildSystemPrompt 会重建它,调用点只有三处:setActiveToolsByName(packages/coding-agent/src/core/agent-session.ts:1290)、_restoreToolsFromTranscript(:1461)和 extendResourcesFromExtensions(:2965)。AGENTS.md 的内容是 ResourceLoader 在加载阶段读进内存的,所以 pi 运行期间修改它,当前会话不会跟着变——要么重启,要么用 /reload 命令(packages/coding-agent/src/core/slash-commands.ts:42)走一遍资源重载。 最小示例:三十行复现这套拼接
脱离 Pi,把拼接规则写成一个小函数,放进 build-prompt.mjs:
function buildSections({ customPrompt, tools = {}, appendText, contextFiles = [], cwd }) {
const s = {};
if (customPrompt) {
s.preamble = customPrompt; // 有自定义提示词就整体替换,默认段落一段都不要
} else {
s.preamble = "You are an expert coding assistant.";
s.tools = Object.entries(tools).map(([n, d]) => `- ${n}: ${d}`).join("\n") || "(none)";
}
if (appendText) s.addendum = appendText;
if (contextFiles.length > 0) {
s.project_context = contextFiles
.map((f) => `<project_instructions path="${f.path}">\n${f.content}\n</project_instructions>`)
.join("\n\n");
}
s.cwd = cwd;
// preamble 原样保留,其余每段用同名标签包起来
return Object.fromEntries(
Object.entries(s).map(([name, text]) => [name, name === "preamble" ? text : `<${name}>\n${text}\n</${name}>`]),
);
}
const sections = buildSections({
tools: { read: "Read file contents", bash: "Execute bash commands" },
appendText: "Always answer in Chinese.",
contextFiles: [{ path: "/repo/AGENTS.md", content: "提交前跑 npm test。" }],
cwd: "/repo",
});
console.log(Object.keys(sections)); // [ 'preamble', 'tools', 'addendum', 'project_context', 'cwd' ]
console.log(Object.values(sections).join("\n\n"));node build-prompt.mjs 先打印段名,再打印一段完整提示词。这三十行里已经藏着 Pi 的三个关键设计:自定义提示词是「替换」而不是「插入」(customPrompt 分支里默认段落一段都不生成);追加段、项目上下文、工作目录这三节无论走哪条分支都会拼上;以及每一段都有名字——以后只有 project_context 变了,就可以只把这一段重新发给模型,而不用整篇重发。真实的 buildSystemPromptSections 就是这个骨架加上三个额外段落。
回到 Pi 源码:buildSystemPromptSections 的八个段落
整个系统提示词只有一个组装函数,输入是一个纯数据对象:
BuildSystemPromptOptions默认路径下,前四段依次写进一个以段名为键的对象:
// packages/coding-agent/src/core/system-prompt.ts:142-161(节选,长句用 … 截断)
const promptSections: Record<string, string> = {};
if (customPrompt) {
promptSections.preamble = customPrompt;
} else {
promptSections.preamble =
"You are an expert coding assistant operating inside pi, a coding agent harness. …";
const visibleTools = selectedTools.filter((name) => !!toolSnippets[name]);
const tools =
visibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets[name]}`).join("\n") : "(none)";
promptSections.tools = `${tools}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.`;
promptSections.rules = buildRules(selectedTools, toolGuidelines, promptGuidelines);
promptSections.docs = `Pi documentation (read only when the user asks about pi itself, …):
// …(省略 154-160:README / docs / examples 三个绝对路径与阅读指引)
}promptSections.preamble两个容易被忽略的细节(源码事实)。其一,工具不是只要启用就会出现在清单里:system-prompt.ts:148-150 先用 toolSnippets[name] 过滤一遍,只有调用方给了一行说明的工具才会被列出,一个都没有时清单是字面量 (none)。其二,rules 段是动态生成的(buildRules,system-prompt.ts:81-118):只有在有 bash(或 Windows 上的 powershell)而没有 grep/find/ls 时才加那条「用 bash 做文件操作」(:101-109);随后按选中工具的顺序并入每个工具自带的准则(toolGuidelines,:111-113),再并入调用方传入的 promptGuidelines(:114),最后固定追加两条通用条款(:115-116)。所有条目经 addRule 去掉首尾空白并去重。
后四段是无条件走到的尾部装配,装配完再统一加标签:
project_context技能段的那个前置条件值得多说一句。技能清单里给的是「名字 + 描述 + 文件绝对路径」(formatSkillsForPrompt,packages/coding-agent/src/core/skills.ts:355-383),正文要靠模型自己去读:有 read 工具就叫它用 read,只有 bash 时提示语改成「用 bash 读取技能文件」(skills.ts:364-366)。两个工具都被关掉时,把清单贴出来只会让模型看见一堆读不到的路径——干脆不贴。技能系统本身在 7.2 Skill 系统 详述,本章只需知道它是系统提示词的第七段。
项目上下文段收的是哪些文件?loadContextFileFromDir 在每个目录里按 AGENTS.override.md、AGENTS.md、AGENTS.MD、CLAUDE.md、CLAUDE.MD 的顺序找,每个目录只取第一个存在的(packages/coding-agent/src/core/resource-loader.ts:71-90)。所以同一目录下放一个 AGENTS.override.md,就能在不改动原 AGENTS.md 的前提下替换掉它;其他目录(全局 agentDir 与 cwd 的各级祖先目录)的上下文文件照常叠加(loadProjectContextFiles,resource-loader.ts:119-157)。
自定义提示词走的是一条短路径:
customPrompt也就是说,--system-prompt "你只回答是或否" 会让模型看不到系统提示词里的工具清单,但仍然看得到 AGENTS.md 和技能目录。这是很多人第一次用自定义提示词时踩的坑:以为是「换掉开头那句话」,实际是「换掉整个默认骨架」。
拼好之后:变成 transcript 里的一条 system 消息
buildSystemPromptSections 返回的是一个「段名 → 文本」的对象,不是字符串。它怎样到达模型?这里是理解 Pi 的关键一步:在 Pi 内部,系统提示词并不是请求旁边的一个独立字段,而是对话记录里的一条消息。pi-ai 为此定义了 SystemMessage:
SystemMessageAgentSession 每次发送用户消息前,都用当前 options 重新算一遍各段,再跟 transcript 里已经生效的各段逐一比较,只把变化的部分做成一条新的 system 消息:
_preparePromptAndToolLoadout求差本身只有十几行(diffSystemPromptSections,system-prompt.ts:204-216):新值与旧值不同的段放进补丁,旧有而新无的段记成 null。于是:
- 新会话的 transcript 里还没有任何 system 消息,第一次
prompt()求出的补丁就是全部八段,这条消息成了开头的系统提示词; - 之后提示词不变,补丁为空,不追加任何东西;
- 如果你
/reload之后AGENTS.md变了,下一次prompt()只会追加一条只含project_context段的 system 消息。
prompt() 把这条消息放在用户消息前面一起交给 Agent(agent-session.ts:1747-1749),它随后像普通消息一样落盘成会话里的一条 message entry(agent-session.ts:933-940)。Agent 运行中需要开启下一轮时,_installAgentNextTurnRefresh 注册的钩子会再做一次同样的求差(agent-session.ts:700-709),所以扩展在一轮中途改了工具,下一轮请求前也会补上对应的 system 消息。
模型最终看到什么,取决于它支不支持「对话中途的 system 消息」。支持的模型(模型元数据里 compat.supportsMidConvoSystemMessages 为真),后来的 system 消息会原样留在对话中间,以「Updated system prompt section …」的形式告诉模型哪一段变了(renderSystemMessageUpdate,packages/ai/src/utils/text.ts:28-40);不支持的,provider 调用 collapseSystemMessages 把所有 system 消息重放成一条开头消息、丢掉中间的那些(packages/ai/src/utils/transcript.ts:108-120)。以 Anthropic 为例,这条开头消息的文本最后放进请求的 system 字段(packages/ai/src/api/anthropic-messages.ts:1093-1100)。
还有一个只读的便捷入口:AgentSession.systemPrompt getter(agent-session.ts:1239-1241)调用 buildSystemPrompt,把各段用两个换行连成一整串(system-prompt.ts:195-197,拼法与 transcript 重放时的 getSystemMessageText 完全一致)。扩展的 ctx.getSystemPrompt() 看到的就是它。
谁在调用它
_rebuildSystemPrompt调用链是这样闭合的:setActiveToolsByName(agent-session.ts:1279)→ _rebuildSystemPrompt(:1290)→ 结果存进 _baseSystemPromptOptions;等下一次 prompt() 时再经 _preparePromptAndToolLoadout → buildSystemPromptSections 变成 system 消息。/reload 走的是另一条路但殊途同归:AgentSession.reload(:3291)→ _buildRuntime(:3236)→ _refreshToolRegistry(:3144)→ 末尾再调一次 setActiveToolsByName(:3234)。
还有一条运行时旁路:每次 prompt() 发消息前,Pi 会把当前的 _baseSystemPromptOptions 交给扩展的 before_agent_start 事件(agent-session.ts:1702-1714)。扩展有两种改法(packages/coding-agent/src/core/extensions/runner.ts:1312-1364):直接修改事件里的 systemPromptOptions(例如往 sections 里加一段、改 selectedTools),这些修改会照常参与上面的分段求差;或者返回一个 systemPrompt 字符串,它被记成 forceSystemPrompt(runner.ts:1346-1348)。强制文本不写进 transcript:transcript 里仍记录结构化的各段,发请求时由 _installAgentForcedPromptProjection 把所有 system 消息压成一条内容为强制文本的开头消息(agent-session.ts:1433-1448)。这些本轮改动在整个运行结束时清空(agent-session.ts:1485),下一次 prompt() 重新从基础值出发。扩展机制见 7.1 Extension 系统。
三条自定义通道
CLI 参数在 packages/coding-agent/src/cli/args.ts:110-114 解析:--system-prompt 只保留最后一个值,--append-system-prompt 可重复、累积成数组。它们经 main.ts:776-777 传给 ResourceLoader,最终在 reload 阶段落地:
systemPromptSource文件发现的顺序是「项目优先、全局兜底」:先看 cwd/.pi/SYSTEM.md(且项目必须已受信任),再看 agentDir/SYSTEM.md(resource-loader.ts:1027-1039);APPEND_SYSTEM.md 同理(:1041-1053)。
一个很实用的细节:CLI 参数的值既可以是文本,也可以是文件路径。
resolvePromptInput图 6.7-1 系统提示词的八段装配与三条自定义入口
主干是 buildSystemPromptSections 内部的执行顺序,从上到下就是最终文本的先后顺序(扩展经 sections 追加的具名段落排在段8之后,图中省略)。虚线是三条外部输入:SYSTEM.md 走 customPrompt 分支做整体替换,APPEND_SYSTEM.md 只影响第五段,扩展则作用在结果上——改 options 会参与求差,返回 systemPrompt 则在发请求时整体顶替。注意 CUS 与 D4 汇合到 T1——这说明后四段是两条分支的公共尾巴。
值得关注的是虚线的三个接入点深度不同:SYSTEM.md 改的是「有没有默认骨架」,APPEND_SYSTEM.md 改的是「骨架后面加什么」,扩展改的是「这一轮实际发出去的是什么」。想让模型永远说中文,用 APPEND_SYSTEM.md 一行就够;想彻底换掉 Pi 的人设,才需要 SYSTEM.md。
Prompt Template:模型不知道它存在
prompt template(提示词模板)是另一件事。系统提示词是给模型的背景,而模板是给用户的快捷方式:你敲 /cl,Pi 在把消息发出去之前,先把 /cl 替换成一整段事先写好的文字。模型收到的是展开后的文本,它既不知道有个叫 /cl 的命令,也不知道发生过替换。
模板就是一个 Markdown 文件。文件名去掉 .md 就是命令名:
loadTemplateFromFile官方文档说明模板可以来自五个地方:全局 ~/.pi/agent/prompts/*.md、项目 .pi/prompts/*.md(需项目受信任)、pi package 的 prompts/ 目录、settings 的 prompts 数组、以及 CLI 的 --prompt-template <path>(来源:packages/coding-agent/docs/prompt-templates.md:7-17)。同一份文档还写明目录扫描是非递归的(docs/prompt-templates.md:95),这一点与实现一致——loadTemplatesFromDir(prompt-templates.ts:159-198)只看一层 .md 文件。
从源码结构看,实际运行时这五个来源并不是由 loadPromptTemplates 自己去扫的:DefaultResourceLoader 调它时固定传 includeDefaults: false(resource-loader.ts:700-705),默认目录全部由 package-manager 汇总成一份路径清单后再传进来。loadPromptTemplates 里的 includeDefaults: true 分支(prompt-templates.ts:268-271)留给直接调用这个函数的 SDK 使用者。它的返回值是 { templates, diagnostics }:加载失败的文件不会中断整批加载,而是变成一条诊断(prompt-templates.ts:211-214)。
展开发生在发送之前:
expandPromptTemplatesubstituteArgs在 AgentSession.prompt() 里,展开是三步流水线的最后一步(agent-session.ts:1615-1651):先尝试扩展注册的命令(命中就直接执行,根本不发消息),中间经过扩展的 input 事件(可以改写或吞掉这条输入),再尝试 /skill:名字 展开,最后才是模板展开。调用方传 expandPromptTemplates: false 时,扩展命令和两种展开都会跳过,文本原样发出(:1611)。steer() 与 followUp() 都经 _queueUserInput 走同样的展开(:1841-1842),区别是它们遇到扩展命令会直接报错而不是执行(:1829-1831)。
图 6.7-2 一次斜杠命令输入的三级展开
从上到下是 prompt 方法里三个拦截点的先后顺序,关注三个「返回」箭头:任何一级命中都会短路后面的处理。最后一条 Note 是本节的要点——展开完全发生在客户端,模型侧没有任何痕迹。对应源码是 agent-session.ts:1619、:1649、:1650 这三行。
真实终端里的样子可以对照本书的采集素材(真实采集,research/cli-captures/pi-tui-main.txt:12-13):在 Pi 仓库里启动时,项目自带的模板显示在启动横幅里:
[Prompts]
/cl, /deslop, /is, /pr, /sa, /wr它们对应 _sources/pi/.pi/prompts/ 下的 cl.md、deslop.md、is.md、pr.md、sa.md、wr.md,横幅里显示的就是去掉 .md 的文件名。这一段由 interactive-mode.ts:1800-1817 渲染,模板同时被注册成自动补全项(interactive-mode.ts:741-745),argument-hint 会显示在描述前面。
Focus on: {{focus}}(来源:packages/coding-agent/README.md:360)。但源码的 substituteArgs 只认 $ 系列占位符,这种双花括号写法不会被替换,会原样发给模型。要占位请用 $1 或 ${1:-默认值}。这是一处官方说明与源码不一致的地方。 顺带把模板和技能的分工说清楚:模板是客户端替换,用户显式触发,模型看不到机制;技能是系统提示词里的目录,模型自己判断要不要用 read 工具去读正文。两者恰好是「谁来决定加载」这个问题的两个答案。
两个包的分工
packages/agent(包名 @earendil-works/pi-agent-core)里也有 system-prompt.ts 和 prompt-templates.ts,但它们不是被 coding-agent 复用的——在 packages/coding-agent/src 下搜 formatSkillsForSystemPrompt、formatPromptTemplateInvocation 没有任何命中。从源码结构看,这是两套平行实现,分工如下(均为源码事实):
| 关注点 | coding-agent(产品层) | pi-agent-core harness(库层) |
|---|---|---|
| 系统提示词组装 | buildSystemPromptSections 八段具名骨架,再按段求差写进 transcript | 没有组装函数;应用通过 AgentHarnessOptions.systemPrompt 给字符串或回调(agent-harness.ts:526),不给时就是空串(harness/runtime/drive/generation.ts:55-65) |
| 技能清单格式化 | formatSkillsForPrompt(skills.ts:355),第二句按可用工具是「用 read 工具加载」或「用 bash 加载」 | formatSkillsForSystemPrompt(harness/system-prompt.ts:3-25),第二句改为「读完整的技能文件」,且无前导空行;要不要放进提示词由应用自己决定 |
| 模板占位符 | 支持 ${N:-默认值} 等默认值语法 | substituteArgs(harness/prompt-templates.ts:252-265)只支持 $N、$@、$ARGUMENTS、${@:N}、${@:N:L} |
| 模板数据结构 | name/description/argumentHint/content/sourceInfo/filePath | 只有 name/description?/content(harness/types.ts:63-70) |
| 资源来源 | 自己扫目录、读 settings、判断项目信任 | 应用通过 setResources() 注入(agent-harness.ts:597),库不做任何发现 |
一种看法是:库层刻意不碰文件系统,才能同时服务 CLI、评测(evals)和第三方 SDK 使用者;代价是同类逻辑要写两遍,两边的行为差异(比如默认值语法)容易被误当成同一套。要用 harness 的模板能力,入口是会话通道(lane)上的 AgentLane.promptFromTemplate(接口见 harness/agent-harness.ts:553,实现见 harness/runtime/lane.ts:1154-1156):它按名字在已注入的资源里找模板,找不到返回 UnknownTemplate 错误,找到就用 formatPromptTemplateInvocation 展开成一条 user 消息(harness/runtime/lane.ts:543-557)。
实践任务
目标:不需要任何 API Key,端到端验证 prompt template 的三件事——一个 Markdown 文件如何变成命令、如何出现在启动清单与自动补全里、$1 与 ${2:-默认值} 到底被替换成了什么。
前提:Pi 仓库(本书为 _sources/pi),依赖已安装。全程只读:模板与脚本都写在系统临时目录,不修改仓库任何文件。
步骤 1 · 写一个最小模板(bash/zsh;heredoc 的 'EOF' 必须带引号,否则 shell 会先把 $1 吃掉。fish 用户请改用编辑器创建同样内容的文件):
mkdir -p /tmp/pi-prompts
cat > /tmp/pi-prompts/sum3.md <<'EOF'
---
description: Summarize a file in three bullets
argument-hint: <path>
---
Read the file $1 and summarize it in exactly three bullet points.
Focus: ${2:-overall structure}
EOF步骤 2 · 启动 pi 并观察启动清单(在仓库根目录执行):
env PI_CODING_AGENT_DIR=/tmp/pi-demo-agent PI_OFFLINE=1 \
./pi-test.sh --no-env -na --verbose --prompt-template /tmp/pi-prompts四个附加开关各有用处:PI_CODING_AGENT_DIR 把全局配置目录指到临时位置,不碰你自己的 ~/.pi/agent;PI_OFFLINE=1 关掉启动期网络操作;--no-env 是 pi-test.sh 自带的开关,清空所有 API Key 环境变量;-na 让本次运行忽略项目本地文件,从而跳过「是否信任此项目」的交互提问;--verbose 强制展开启动清单(默认是紧凑一行,也可以进界面后按 ctrl+o 展开)。
预期现象(本书在锁定 commit 上实测,终端 120x45):启动横幅里出现
[Prompts]
path
/sum3同一屏还会有 [Context] 和 [Skills] 两段,内容取决于你自己机器上的文件,与本任务无关。
步骤 3 · 看自动补全:在输入框里敲 /sum,下拉里出现(实测):
→ sum3 <path> — [t] Summarize a file in three bullets<path> 是 frontmatter 里的 argument-hint,[t] 是来源标记:通过 --prompt-template 临时加载的资源 scope 默认是 temporary(source-info.ts:36),既非 user 也非 project 时标记就是 t(interactive-mode.ts:631-636)。看完先按 ctrl+c 清空输入框,再按 ctrl+d 退出(默认键位见 packages/coding-agent/src/core/keybindings.ts:94-95)。
步骤 4 · 直接验证替换结果(不依赖终端,任何环境都能跑)。把下面内容存成 /tmp/expand-demo.mts(扩展名用 .mts,让 tsx 按 ES 模块处理,顶层 await 才能用):
const mod = await import(`${process.cwd()}/packages/coding-agent/src/core/prompt-templates.ts`);
const { loadPromptTemplates, expandPromptTemplate } = mod;
const { templates } = loadPromptTemplates({
cwd: process.cwd(), agentDir: "/nonexistent",
promptPaths: [process.argv[2]], includeDefaults: false,
});
for (const t of templates) console.log("name:", t.name, "| hint:", t.argumentHint, "| desc:", t.description);
console.log(JSON.stringify(expandPromptTemplate("/sum3 src/main.ts", templates)));
console.log(JSON.stringify(expandPromptTemplate("/sum3 src/main.ts error-handling", templates)));
console.log(JSON.stringify(expandPromptTemplate("/nope hello", templates)));在仓库根目录运行(必须在根目录,脚本用 process.cwd() 定位源码):
./node_modules/.bin/tsx --tsconfig ./tsconfig.json /tmp/expand-demo.mts /tmp/pi-promptsloadPromptTemplates 返回的是 { templates, diagnostics },所以第三行要解构出 templates。预期现象(本书实测输出,逐字如下):
name: sum3 | hint: <path> | desc: Summarize a file in three bullets
"Read the file src/main.ts and summarize it in exactly three bullet points.\nFocus: overall structure"
"Read the file src/main.ts and summarize it in exactly three bullet points.\nFocus: error-handling"
"/nope hello"步骤 5 · 跑一遍模板的单元测试:
npm test --workspace=@earendil-works/pi-coding-agent -- test/prompt-templates.test.ts预期现象:Tests 93 passed (93)(本书实测)。用例名直接对应本章结论,例如 substituteArgs - positional defaults、substituteArgs - array slicing、expandPromptTemplate > should split template arguments on unquoted newlines。
如何判断成功:① 步骤 2 的清单里看到 /sum3;② 步骤 4 的三行输出与上面完全一致——第一行证明第二个参数缺失时 ${2:-overall structure} 用了默认值,第二行证明给了参数就用参数,第三行证明不匹配任何模板名的文本原样返回;③ 你能解释为什么 /nope hello 没有报错而是被当成普通消息。
常见错误:① heredoc 写成不带引号的 <<EOF,$1 被 shell 替换成空串,模板里就少了占位符;② 把模板放进 /tmp/pi-prompts/sub/ 子目录——目录扫描非递归(prompt-templates.ts:159-198),找不到;③ 步骤 4 不在仓库根目录执行,process.cwd() 拼出的路径不存在,报模块找不到;④ 省掉 --verbose 时清单是紧凑的一行 /sum3,不分组,不是出错;⑤ 忘记 npm test 后面的 -- 分隔符,文件名参数到不了 vitest,会跑整个包的测试。
对应源码位置:packages/coding-agent/src/cli/args.ts:174-176(--prompt-template 解析)、packages/coding-agent/src/main.ts:715,769(传给 ResourceLoader)、packages/coding-agent/src/core/resource-loader.ts:695-705(updatePromptsFromPaths)、packages/coding-agent/src/core/prompt-templates.ts:105(loadTemplateFromFile)、:304(expandPromptTemplate)、:71(substituteArgs)、packages/coding-agent/src/modes/interactive/interactive-mode.ts:1800(启动清单)、:741(自动补全注册)。
本章小结
- 系统提示词是一次纯字符串拼接,唯一组装函数是
buildSystemPromptSections,八段具名、按顺序为:preamble(身份句)→tools→rules→docs→addendum(追加段)→project_context→skills→cwd,扩展还能用sections追加具名段落;除preamble外每段都包在同名标签里。 - 工具只有带一行 snippet 才会出现在清单里;技能段只有
read或bash工具启用时才拼上。每个目录的上下文文件按AGENTS.override.md→AGENTS.md→CLAUDE.md的优先级只取一个。 --system-prompt/SYSTEM.md是整体替换默认骨架(后四段仍在);--append-system-prompt/APPEND_SYSTEM.md是追加;扩展的before_agent_start可以修改这一轮的 options,或返回systemPrompt在发请求时整体顶替(不写进 transcript)。CLI 参数的值可以是文本,也可以是文件路径。- 系统提示词以 transcript 里的 system 消息存在:第一次
prompt()写入全部八段,之后每次只在某段变化时追加一条只含差异段落的 system 消息;不支持中途 system 消息的 provider 会把它们重放、压成一条开头消息。组装输入缓存在_baseSystemPromptOptions,只在工具集合变化、资源扩展或会话重载时重建。 - prompt template 是纯客户端展开:
/name args在prompt()里被替换成模板正文,模型对模板机制毫无感知;占位符支持$N、$@、$ARGUMENTS、${N:-默认值}、${@:N:L}等。目录扫描非递归,文件名即命令名。 - coding-agent 与 pi-agent-core harness 是平行实现而非复用;harness 不做资源发现,也不支持默认值语法。
- 关键术语:系统提示词(System Prompt)、system 消息(
SystemMessage)、具名段落(sections)、项目上下文文件(AGENTS.override.md / AGENTS.md / CLAUDE.md)、提示词模板(Prompt Template)、占位符替换(Argument Substitution)、来源标记(SourceInfo scope)。 - 关键源码索引:
packages/coding-agent/src/core/system-prompt.ts(BuildSystemPromptOptions:9、buildRules:81、buildSystemPromptSections:121、customPrompt 分支:143、工具过滤:148、尾部装配:163、buildSystemPromptState:186、buildSystemPrompt:195、diffSystemPromptSections:204)、packages/ai/src/types.ts:491(SystemMessage)、packages/ai/src/utils/transcript.ts(getCurrentSystemMessage:73、collapseSystemMessages:108)、packages/coding-agent/src/core/agent-session.ts(setActiveToolsByName:1279、_rebuildSystemPrompt:1371、_preparePromptAndToolLoadout:1407、_installAgentForcedPromptProjection:1433、prompt:1606、展开三步:1615-1651、before_agent_start:1702、_expandSkillCommand:1797)、packages/coding-agent/src/core/resource-loader.ts(resolvePromptInput:54、loadContextFileFromDir:71、reload 决策:526、updatePromptsFromPaths:695、discoverSystemPromptFile:1027、discoverAppendSystemPromptFile:1041)、packages/coding-agent/src/core/prompt-templates.ts(parseCommandArgs:25、substituteArgs:71、loadTemplateFromFile:105、loadTemplatesFromDir:159、loadPromptTemplates:222、expandPromptTemplate:304)、packages/coding-agent/src/core/skills.ts:355(formatSkillsForPrompt)、packages/coding-agent/src/modes/interactive/interactive-mode.ts:631,741,1800、packages/agent/src/harness/system-prompt.ts:3、packages/agent/src/harness/prompt-templates.ts:252,268、packages/agent/src/harness/agent-harness.ts:526,553、packages/agent/src/harness/runtime/lane.ts:1154;测试packages/coding-agent/test/prompt-templates.test.ts(93 passed)、packages/coding-agent/test/system-prompt.test.ts(16 passed)、packages/agent/test/harness/prompt-templates.test.ts(5 passed)。 - 自测问题:① 用
--system-prompt "只回答是或否"启动后,模型还看得到AGENTS.md的内容吗?看得到工具清单吗?分别在哪一行决定?② 一个工具被--tools启用了,却没出现在系统提示词的<tools>段里,最可能的原因是什么?③ 模板/x的正文里写$1,用户输入/x "a b" c,$1会被替换成什么?④ pi 正在运行时你改了AGENTS.md,要让当前会话看到新内容需要发生什么?只把 ResourceLoader 重新加载一遍够不够? - 下一章:6.8 配置系统——
settings.json的双层合并、项目信任,以及本章多次提到的agentDir到底怎么算出来。本章未展开的内容:技能的发现与加载规则(见 7.2 Skill 系统)、/skill:名字展开的完整格式、扩展如何注册斜杠命令(见 7.3 自定义工具与斜杠命令)。
✅ 自测问题参考答案先自己回答,再点开对照
AGENTS.md看得到,工具清单看不到。--system-prompt走的是system-prompt.ts:143的 customPrompt 分支,它把preamble换成你的文本,默认骨架里的tools、rules、docs三段一段都不生成;而后四段——addendum、project_context(AGENTS.md / CLAUDE.md)、skills、cwd——是在:163起的尾部装配里无条件拼上的,不受影响。所以这个开关能换掉「你是谁、怎么干活」,但换不掉「这个项目是什么」。- 最可能是这个工具没有写
promptSnippet。buildSystemPromptSections过滤工具清单时(system-prompt.ts:148)只收带一行 snippet 的工具——启用与「出现在提示词里」是两件事:没有 snippet 的工具照样能被模型调用(它的 name / description / parameters 由 Agent Loop 作为 system 消息里的toolsAdded声明,发请求时由 provider 放进 API 的工具字段),只是不会出现在系统提示词那份人类可读的清单里。 - 替换成
a b(不带引号的两个字,中间一个空格)。parseCommandArgs(prompt-templates.ts:25)按 shell 风格分词,引号内的空格不算分隔符,所以$1=a b、$2=c。 - 不够,而且中间还隔着一步。
AGENTS.md的内容先被 ResourceLoader 读进内存,再被_rebuildSystemPrompt抄进AgentSession._baseSystemPromptOptions;只有setActiveToolsByName(agent-session.ts:1290)、_restoreToolsFromTranscript(:1461)和extendResourcesFromExtensions(:2965)会触发这次重建。光把 ResourceLoader 重新加载一遍,options 缓存不会跟着变。要么重启 pi,要么用/reload——它走的是AgentSession.reload→_buildRuntime→_refreshToolRegistry→ 末尾再调一次setActiveToolsByName,这才顺带触发了重建。重建之后,下一次prompt()求差时发现project_context段变了,才会往 transcript 里追加一条只含这一段的 system 消息,模型从这时起看到新内容。