第 05 章:系统提示词与上下文文件

前面四章的系统提示词只有一句 "You are a coding assistant…"。这一章我们认真写它——不是写长,而是写准。pi 的系统提示词加工具定义合计不到 1000 token,却在 Terminal-Bench 上打出了第一梯队的成绩(论证见设计思想:极简核心)。这一章把 pi 的提示词"抄"过来,逐句理解,再加上 pi 的第二个上下文武器:AGENTS.md 分层加载

pi 的系统提示词,逐句拆解

以下是 mini-pi 采用的全文(对照 pi system-prompt.ts 的默认版本,去掉 pi 专属的文档指引段):

You are an expert coding assistant operating inside mini-pi, a coding agent harness.
You help users by reading files, executing commands, editing code, and writing new files.

Available tools:
- read: Read file contents
- bash: Execute bash commands
- edit: Make surgical edits to files
- write: Create or overwrite files

Guidelines:
- Use bash for file operations like ls, rg, find
- Use read to examine files before editing
- Use edit for precise changes (old text must match exactly)
- Use write only for new files or complete rewrites
- Be concise in your responses
- Show file paths clearly when working with files

每一句都有用:

  • "operating inside … a coding agent harness":一句话锚定角色和运行环境。前沿模型被 RL 训练得"天生知道 coding agent 是什么",不需要上万 token 去教。
  • Available tools 清单:与真正的工具 schema 互为补充——schema 管"怎么调",这里管"什么场合用哪个"。
  • "Use bash for file operations like ls, rg, find":因为我们故意不提供 ls/grep/find 工具,必须告诉模型去哪找这些能力。
  • "Use read to examine files before editing":配合 edit 的精确匹配——不先读,oldText 一定对不上。
  • "Use write only for new files or complete rewrites":约束 token 使用方式,防止模型动不动全量重写。

注意整段提示词里没有任何"工作流程"层面的规定(什么时候该 plan、怎么组织 commit、代码风格……)。这类内容 pi 认为不属于核心,应该由用户通过 AGENTS.md 自行注入——这就是我们接下来要做的。

AGENTS.md:项目级上下文的分层加载

pi 启动时按 全局 → 父目录逐级 → 当前项目 的顺序收集 AGENTS.md(或 CLAUDE.md),全部拼接进系统提示词。这样:

  • 个人偏好(如"永远用中文回答")放 ~/.pi/agent/AGENTS.md,全局生效;
  • 项目约定(技术栈、命令、禁区)放项目根的 AGENTS.md,跟着仓库走;
  • 单仓多项目(monorepo)时,父级目录的约定自然级联。

pi 用 <project_context> 标签把这段内容和核心提示词隔开,并在末尾附上当前工作目录。mini-pi 照搬这个结构:

src/main.ts(新增)
const BASE_SYSTEM_PROMPT = `You are an expert coding assistant operating inside mini-pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.

Available tools:
- read: Read file contents
- bash: Execute bash commands
- edit: Make surgical edits to files
- write: Create or overwrite files

Guidelines:
- Use bash for file operations like ls, rg, find
- Use read to examine files before editing
- Use edit for precise changes (old text must match exactly)
- Use write only for new files or complete rewrites
- Be concise in your responses
- Show file paths clearly when working with files`;

/** 加载 AGENTS.md:~/.mini-pi/AGENTS.md(全局)+ 从根目录一路向下到 cwd 的每一级 */
function loadContextFiles(): { path: string; content: string }[] {
  const files: { path: string; content: string }[] = [];
  const globalFile = join(homedir(), ".mini-pi", "AGENTS.md");
  if (existsSync(globalFile)) files.push({ path: globalFile, content: readFileSync(globalFile, "utf8") });

  const dirs: string[] = [];
  let dir = process.cwd();
  while (true) {
    dirs.push(dir);
    const parent = dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  for (const d of dirs.reverse()) {
    const p = join(d, "AGENTS.md");
    if (existsSync(p)) files.push({ path: p, content: readFileSync(p, "utf8") });
  }
  return files;
}

function buildSystemPrompt(): string {
  let prompt = BASE_SYSTEM_PROMPT;
  const contextFiles = loadContextFiles();
  if (contextFiles.length > 0) {
    prompt += "\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n";
    for (const f of contextFiles) {
      prompt += `<project_instructions path="${f.path}">\n${f.content}\n</project_instructions>\n\n`;
    }
    prompt += "</project_context>\n";
  }
  prompt += `\nCurrent working directory: ${process.cwd()}`;
  return prompt;
}

(join/homedir/existsSync/readFileSync 等 import 见本章完整代码,或对照最终版 code/mini-pi-vanilla/src/main.ts。)

然后把系统提示词接进 runAgent——注意它不作为普通消息存进历史,而是每次调用时在边界上拼进 wire messages:

src/main.ts(runAgent 开头)
async function runAgent(systemPrompt: string, session: Session, messages: Message[], signal: AbortSignal): Promise<void> {
  while (true) {
    const wire: Message[] = [{ role: "system", content: systemPrompt }, ...messages];
    // … streamChat(wire, signal, onText)
  }
}

"系统提示词在调用边界注入,而不是塞进消息历史"是个有意的选择:提示词是当前配置,历史是事实记录,两者分开,换提示词不用改历史。pi 的 Context { systemPrompt, messages, tools } 也是同样的切分。

关于 system role 的兼容性

一些新型号(如 OpenAI 的推理模型)期望 developer role 而不是 system,某些推理引擎又不认识 developer。pi-ai 用 compat.supportsDeveloperRole 之类的开关逐 provider 处理这类怪癖(见设计思想:LLM 抽象层)。mini-pi 用 system,在主流 OpenAI 兼容端点上都没问题。

验收

在项目根放一个 AGENTS.md:

AGENTS.md
# 项目约定

- 所有回复使用中文。
- 本项目使用 pnpm,不要使用 npm 命令。
- 修改 src/ 下的文件前先列出不涉及的测试文件。

重启 mini-pi,问 "这个项目用什么包管理器?"——它会按 AGENTS.md 回答 pnpm。再观察它的行为:pnpm 出现在它构造的 bash 命令里。你没有写一行代码,就改变了 agent 的行为——这就是 pi 语境下的"上下文工程":把定制放到用户可控的文件里,而不是塞进 harness 的代码里。

本章要点

  • 系统提示词的任务是"锚定角色 + 工具分工 + 少量行为约束",不是百科全书;前沿模型不需要被教怎么做 agent。
  • AGENTS.md 分层加载把定制权交给用户和项目,是 pi "adapt pi to your workflows" 哲学的直接体现。
  • 系统提示词在调用边界注入,与消息历史分离。
  • pi 还支持 .pi/SYSTEM.md 整体替换系统提示词、APPEND_SYSTEM.md 追加——mini-pi 留作练习。

下一章:第 06 章:会话持久化 —— JSONL 树