# 指令从哪来 · AGENTS.md、系统提示词与协作模式

> 请求里的“系统提示词”只有基础指令一段，线程创建时定下，此后不变；开发者指令、权限说明、协作模式放在 developer 消息里，AGENTS.md 与环境信息放在 user 消息里，组织托管的开发者指令单独成条。AGENTS.md 由全局、线程与项目三部分拼成，全局部分每次请求前都重读，项目部分按执行环境与信任级别缓存。

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/instructions
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

# 指令从哪来 · AGENTS.md、系统提示词与协作模式

[上下文与历史](https://daiw.net/manual/codex-source/context-history)讲了注入的上下文怎么“只追加”，这一篇换个角度：一次请求里的各种指令分别从哪里来、落在请求的哪个位置、什么时候会重新加载。

## 用户看到的样子

能影响指令的入口不少：项目与 `CODEX_HOME` 里的 `AGENTS.md`（发现规则、大小上限、备用文件名见 [AGENTS.md 项目指令](https://daiw.net/manual/codex/agents-md)），配置里的 `developer_instructions`、官方明确不推荐的 `model_instructions_file`，组织在 `requirements.toml` 里下发的 `additional_developer_instructions`，以及 `/plan` 或 `Shift+Tab` 切换的协作模式。通过 app-server 起线程时，`thread/start` 也能直接带 `baseInstructions` 与 `developerInstructions`。

## 一次请求里的指令布局

```mermaid
flowchart TB
  I[instructions 字段<br/>基础指令]
  subgraph IN[input 数组 · 开头是初始上下文]
    D1[developer 消息<br/>开发者指令 · 权限说明 · 协作模式 · 应用与插件说明]
    D2[单独成条的 developer 消息<br/>多 agent 模式等]
    U1[user 消息<br/>AGENTS.md · 环境上下文]
    D3[developer 消息<br/>托管的开发者指令]
    H[对话本身<br/>用户消息 · 回复 · 工具调用与输出 · 后来追加的差异]
  end
  I --> D1 --> D2 --> U1 --> D3 --> H
```

这张图对应 `build_initial_context_with_world_state`（`codex-rs/core/src/session/mod.rs:4246`）拼出的初始上下文。要点是：真正意义上的“系统提示词”只有基础指令这一段；其余指令都是历史开头的普通消息，按 developer 与 user 两种角色分组。对启用了 Responses Lite 的模型（随附目录里除 `gpt-5.5` 外都是），连基础指令也不放 `instructions` 字段，而是作为一条 developer 消息插到输入最前面（见[模型客户端](https://daiw.net/manual/codex-source/model-client)）。

## 基础指令：线程创建时定下

```rust
        // Resolve base instructions for the session. Priority order:
        // 1. config.base_instructions override
        // 2. conversation history => session_meta.base_instructions
        // 3. rendered instructions_template for current model
        // ...
        let base_instructions = config
            .base_instructions
            .clone()
            .or_else(|| conversation_history.get_base_instructions().map(|s| s.text))
            .unwrap_or_else(|| render_model_instructions(&model_info));
```

（`codex-rs/core/src/session/mod.rs:707`）

三个来源按优先级排：

1. **显式覆盖**：`thread/start` 的 `baseInstructions`，或配置里的 `model_instructions_file`（读文件）与顶层 `instructions`，来源会被标记为自定义。
2. **会话元数据**：恢复或分叉的线程沿用当初写进元数据的那一份，即使期间模型目录更新了。
3. **模型目录**：`render_model_instructions` 取模型元数据里的 `model_messages.instructions_template`。随附目录里每个模型都自带一份，长度在一万七千到两万一千多字符之间；目录里查不到的模型用回退元数据，模板是 `codex-rs/models-manager/prompt.md`（开头写着“You are a coding agent running in the Codex CLI”）。

模型目录还能影响模板本身：`personality = "none"` 会删掉模板里的 `# Personality` 一节。发请求时还有一步只作用于请求副本的加工：计划工具 `update_plan` 默认关闭（`tools.update_plan.enabled`），基础指令来自模型目录时，模板里讲它的几节会被 `without_update_plan_instructions` 去掉，但存下来、会被分叉继承的原文不变。基础指令一旦定下就不再改，中途换模型靠追加 `<model_switch>` 消息表达，原因见上一篇。

## AGENTS.md：三部分拼成一条 user 消息

`LoadedAgentsMd`（`codex-rs/core/src/agents_md.rs:301`）由三部分按顺序拼接：

- **全局**：`CodexHomeUserInstructionsProvider`（`codex-rs/codex-home/src/instructions/mod.rs`）在 `CODEX_HOME` 下先找 `AGENTS.override.md` 再找 `AGENTS.md`，取第一个去掉首尾空白后非空的文件。
- **线程级**：宿主通过扩展接口提供的指令，超过 10,000 个估算 token 直接拒绝，不做截断。
- **项目**：对每个执行环境，从当前目录往上找项目根，再从根走回当前目录，每一级按候选文件名找第一个存在的文件。

候选文件名的顺序是：

```rust
fn candidate_filenames<'a>(config: &'a Config, cwd: &PathUri) -> Vec<&'a str> {
    let mut names: Vec<&str> = Vec::with_capacity(2 + config.project_doc_fallback_filenames.len());
    names.push(LOCAL_AGENTS_MD_FILENAME);
    names.push(DEFAULT_AGENTS_MD_FILENAME);
    for candidate in &config.project_doc_fallback_filenames {
        let candidate = candidate.as_str();
        if candidate.is_empty() {
            continue;
        }
        // Use the executor's path convention, not the host's: resolving a Windows
        // network path can send ambient credentials even during metadata probes.
        if matches!(candidate, "." | "..")
            || candidate.contains(['/', '\0'])
            || cwd.infer_path_convention() == Some(PathConvention::Windows)
                && candidate.contains(['\\', ':'])
        {
            tracing::warn!("ignoring project_doc_fallback_filenames entry that is not a filename");
            continue;
        }
        if !names.contains(&candidate) {
            names.push(candidate);
        }
    }
    names
}
```

（`codex-rs/core/src/agents_md.rs:272`）

项目根由 `project_root_markers` 判定，默认是 `.git`；读这个配置时特意跳过了项目级配置层，项目自己的配置文件改不了“哪里算根”。项目部分受 `project_doc_max_bytes` 约束（默认 32 KiB），预算跨所有执行环境累计，超出的文件被截断；未受信任的项目不加载任何项目级文件。拼接时，从全局或线程部分过渡到项目部分，中间插一行 `--- project-doc ---`；多个执行环境各自贡献了文件时，每组前面标上 “for `<环境 ID>` with root …”。

拼好的文本装进 `UserInstructions` 片段，角色是 user，渲染成 `# AGENTS.md instructions for <目录>` 开头、正文包在 `<INSTRUCTIONS>` 标签里的一条消息，和环境上下文合在同一条 user 消息里。

## 什么时候重新读

`AgentsMdManager::refresh` 不止在会话启动时调用：每次捕获 step（也就是每次采样请求之前）都会调用一次。

```rust
        let (mut instructions, cached, refresh_repository) = {
            let mut state = self.state.lock().await;
            let refresh_repository = state.cache.selections.as_ref() != Some(&selections)
                || state.cache.active_project_trust_level != active_project_trust_level;
            if refresh_repository {
                // Tightened read permissions must not leave inaccessible instructions visible,
                // even if discovery fails or the caller cancels the refresh.
                state.cache = AgentsMdCache::default();
            }
            (
                state.instructions.clone(),
                state.cache.loaded.clone(),
                refresh_repository,
            )
        };
        // ...
        if let Some(provider) = &instructions.user_provider {
            let loaded = provider.load_user_instructions().await;
            instructions.user = normalize_instructions(loaded.instructions);
            warnings.extend(loaded.warnings);
        }
```

（`codex-rs/core/src/agents_md_manager.rs:84`）

项目部分只在执行环境的选择（包括工作目录）或项目信任级别变了时才重新扫描，否则用缓存；全局部分由提供者负责，`CodexHomeUserInstructionsProvider` 每次都重新读文件。读到的内容一变，世界状态的 AGENTS.md 分区就追加一条替换声明（见上一篇）。子代理不自己读全局文件，而是继承父线程已经生效的那一份。

<Callout type="warn">
  《Codex 中文手册》依据官方文档写着 AGENTS.md “每次运行构建一次”。按 0.158.0 的代码，根线程在每次请求前都会重读 `CODEX_HOME` 下的全局文件，改动会在下一次请求时以一条替换声明的形式生效；项目里的文件则要等工作目录、执行环境或信任级别变化后才会重新扫描。
</Callout>

## 开发者指令与托管指令

`developer_instructions`（配置或 `thread/start` 的 `developerInstructions`）渲染成 `DeveloperInstructions` 片段，放在初始上下文那条 developer 消息里，没有标记。唯一的例外是 Guardian 审查子代理：它的开发者指令被包成 `GuardianPolicy`，单独成条，便于审计。

组织托管的 `additional_developer_instructions` 来自 `requirements.toml`，由世界状态里的 `ManagedDeveloperInstructionsState` 负责，渲染成 `<managed_developer_instructions>` 包裹的独立 developer 消息，排在初始上下文的最后。它有 10,000 个估算 token 的上限（`MAX_MANAGED_DEVELOPER_INSTRUCTIONS_TOKENS`），超了在加载配置时就报错；中途变更同样追加一条替换声明。

## 协作模式的指令

协作模式只有两种：`Default` 与 `Plan`。它们的说明文字在 `codex-collaboration-mode-templates` crate 的 `default.md` 与 `plan.md` 里，由 `builtin_collaboration_mode_presets`（`codex-rs/models-manager/src/collaboration_mode_presets.rs`）组装成预设：Plan 预设把推理强度设为 `medium`，开发者指令设为 `plan.md` 的全文。app-server 收到不带说明文字的协作模式时，会用预设补上。

世界状态里的 `CollaborationModeState` 决定最终渲染什么：模型目录若为该模式提供了覆盖文本，优先用目录里的（随附目录中 GPT-6 系列覆盖了 Default 模式的说明），否则用协作模式设置里带的说明；计划工具关闭时，还会从内置文本里去掉讲 `update_plan` 的段落。结果放进 `<collaboration_mode>` 包裹的 developer 片段。切换模式时同样只追加：Default 模式的说明第一句就是 “You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.”。Plan 模式在一轮里具体改变了什么，见[任务类型与 Plan 模式](https://daiw.net/manual/codex-source/tasks-and-plan-mode)。

## 和《从 LLM 到 Coding Agent》对照

[系统提示与上下文注入](https://daiw.net/manual/llm-to-agent/context-injection)把 prompt 分成系统提示、环境信息、项目记忆、动态附件几层，并建议“整场会话不变”的环境信息可以拼进系统提示。Codex 更激进：系统提示只放基础指令，环境、权限、AGENTS.md 全部作为历史开头的消息注入，之后的变化再以消息追加，系统提示这一段因此几乎永远不变。对照 [OpenCode 的文件工具](https://daiw.net/manual/opencode/file-tools)：OpenCode 在 read 工具读到某个文件时，才把沿途目录里尚未加载的 `AGENTS.md` 附在输出末尾；Codex 只加载项目根到当前目录这一条路径上的文件，更深子目录里的 `AGENTS.md` 不会被自动带上。

---

上一篇：[上下文压缩 · 窗口快满时怎么办](https://daiw.net/manual/codex-source/compaction) · 下一篇：[模型目录与提供方 · 一个 CLI 接多家后端](https://daiw.net/manual/codex-source/models-and-providers)
