指令优先级与拼装顺序
很多资料把 Codex 的指令描述成 system > developer > user 的一张表,但这会掩盖真正影响行为的三个问题:指令由谁产生、进入哪个 API 槽位、在首轮或后续回合的什么时候生效。本文以 Session 为入口,追踪 TurnContext、WorldState、ContextManager 和 Prompt,直到 Responses API 请求。
阅读前建议先看 ContextItem角色语义、ContextHistory读写 和 Context变更语义。本文不展开 AGENTS.md 的目录发现算法,也不讨论每一种插件的实现;重点是它们进入上下文后的所有权、角色、排序与更新边界。读完后,读者应能从一个模型可见文本反查其构造入口,并判断它属于基础 instructions、首轮消息还是稳态差分。
1. 两条指令通道
Prompt 把发送给模型的内容拆成 input 与 base_instructions。前者是 ResponseItem 历史,后者对应 Responses API 的 instructions 字段;因此不能把所有内容都称为 system prompt。
相关源码:
codex-rs/core/src/client_common.rs :: Promptcodex-rs/protocol/src/models.rs :: BaseInstructions
pub struct Prompt {
pub input: Vec<ResponseItem>,
pub(crate) tools: Vec<ToolSpec>,
pub(crate) parallel_tool_calls: bool,
pub base_instructions: BaseInstructions,
pub output_schema: Option<Value>,
pub output_schema_strict: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BaseInstructions {
pub text: String,
}Session::get_base_instructions 从 session configuration 读取基础文本;它不是 TurnContext.developer_instructions。后者会在初始上下文中变成 developer message,而基础文本由请求构造器放入 instructions。这个分离决定了“优先级”不能只按消息 role 推断。
2. 来源与所有者
TurnContext 只保存本回合可读取的快照。它同时携带 session 级 developer_instructions、协作模式专用 instructions、模型、环境、权限、扩展数据和 feature 配置。真正把这些字段转成可见文本的是 Session::build_world_state_for_step,所以字段存在不等于已经进入模型输入。
相关源码:
codex-rs/core/src/session/turn_context.rs :: TurnContextcodex-rs/core/src/session/world_state.rs :: build_world_state_for_step
pub(crate) developer_instructions: Option<String>,
pub(crate) collaboration_mode_developer_instructions: Option<String>,
pub(crate) personality: Option<Personality>,
pub(crate) environments: TurnEnvironmentSnapshot,
pub(crate) extension_data: Arc<ExtensionData>,World state 按 section 保存模型指令、人格、AGENTS、权限、协作模式、环境、插件和多 agent 状态。每个 section 自己决定快照、marker、role 和差分;因此不存在一个集中式的“低优先级覆盖高优先级”函数。覆盖关系更多表现为:后续 fragment 是否追加、是否独立成消息,以及旧 section 是否产生 replacement/removal 文本。
3. 首轮拼装
源码位置:codex-rs/core/src/session/mod.rs :: build_initial_context_with_world_state。
let mut developer_sections = Vec::<String>::with_capacity(8);
let mut contextual_user_sections = Vec::<String>::with_capacity(2);
let mut separate_developer_sections = Vec::<String>::new();
if !separate_guardian_developer_message
&& let Some(developer_instructions) = turn_context.developer_instructions.as_deref()
&& !developer_instructions.is_empty()
{
developer_sections.push(developer_instructions.to_string());
}
for fragment in world_state.render_full() {
match fragment.role() {
"developer" if fragment.markers().0 == ModelSwitchInstructions::type_markers().0 => {
developer_sections.insert(0, fragment.render());
}
"developer" if fragment.requires_separate_message() => {
separate_developer_sections.push(fragment.render());
}
"developer" => developer_sections.push(fragment.render()),
"user" => contextual_user_sections.push(fragment.render()),
_ => {}
}
}这段代码给出首轮顺序的关键不变量:普通 developer sections 先聚合;模型切换片段强制插入索引 0;要求独立消息的片段不与其他 developer 文本合并;user fragment 进入 contextual user 集合。推荐插件、扩展 thread context、turn context 和 token budget 在 world_state.render_full() 前后按各自入口追加,不能从 section 名字推断最终相邻位置。
最终输出依次是聚合 developer message、独立 developer messages、初始 multi-agent mode、聚合 contextual user message;Guardian reviewer 还会把 policy prompt 作为最后一个独立 developer message。Guardian 的特殊顺序是隔离安全边界,不是普通 developer 指令的通用优先级。
4. 聚合与隔离
源码位置:codex-rs/core/src/context_manager/updates.rs :: build_developer_update_item、build_contextual_user_message。
pub(crate) fn build_developer_update_item(text_sections: Vec<String>) -> Option<ResponseItem> {
build_text_message("developer", text_sections)
}
pub(crate) fn build_contextual_user_message(text_sections: Vec<String>) -> Option<ResponseItem> {
build_text_message("user", text_sections)
}
fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<ResponseItem> {
if text_sections.is_empty() {
return None;
}
let content = text_sections
.into_iter()
.map(|text| ContentItem::InputText { text })
.collect();
Some(ResponseItem::Message {
id: None,
role: role.to_string(),
content,
phase: None,
internal_chat_message_metadata_passthrough: None,
})
}聚合只合并同一 role、同一消息组的文本;requires_separate_message() 会打断合并。这样做不是排版偏好:独立消息让 Guardian policy、某些 extension fragment 或多 agent usage hint 保持单独的顶层边界,消费者可以按消息边界检查,而不必从一个巨大 developer bundle 中重新切分来源。
5. 请求槽位
源码位置:codex-rs/core/src/client.rs :: build_responses_request。
let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
let (instructions, tools) = if model_info.use_responses_lite {
let mut prefix = vec![ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools,
}];
if !prompt.base_instructions.text.is_empty() {
prefix.push(ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: prompt.base_instructions.text.clone(),
}],
phase: None,
});
}
input.splice(0..0, prefix);
(String::new(), None)
} else {
(prompt.base_instructions.text.clone(), Some(create_tools_raw_json_for_responses_api(&prompt.tools)?.into()))
};普通 Responses API 把 BaseInstructions.text 留在 instructions 字段;Responses Lite 没有同样的独立槽位,因此把基础文本前插成 developer item。于是同一份基础指令在两种 wire mode 中的字段位置不同,但其相对于历史 input 的“前置”语义保持一致。项目指令、环境和 AGENTS 不会因此自动升级为基础 instructions。
6. 稳态更新
源码位置:codex-rs/core/src/session/mod.rs :: record_context_updates_and_set_reference_context_item。
let reference_context_item = {
let state = self.state.lock().await;
state.reference_context_item()
};
let turn_context_item = turn_context.to_turn_context_item();
let turn_context_changed = reference_context_item.as_ref() != Some(&turn_context_item);
let should_inject_full_context = reference_context_item.is_none();
let (mut context_items, world_state_item) = if should_inject_full_context {
let items = self
.build_initial_context_with_world_state(turn_context, world_state.as_ref())
.await;
(items, Some(WorldStateItem::full(world_state.snapshot().into_value())))
} else {
let (fragments, item) = self.state.lock().await.history.update_world_state(&world_state);
(merge_contextual_fragments(fragments), item)
};
if !should_inject_full_context && turn_context_changed {
context_items.extend(self.build_turn_context_contribution_items(step_context).await);
}当前实现的稳态更新还有一个容易漏掉的顺序:先把 WorldState 变化写入内存基线,再决定是否需要 TurnContext contribution;只有已经生成并记录了 model-visible context items 后,才持久化 WorldStateItem 和 TurnContextItem。如果本回合只有 snapshot 变化而没有可见 diff,代码会更新 baseline, 但不会重复写入 TurnContextItem。
源码位置:codex-rs/core/src/session/mod.rs :: record_context_updates_and_set_reference_context_item
if !context_items.is_empty() {
self.record_conversation_items(turn_context, &context_items).await;
}
if let Some(world_state_item) = world_state_item {
self.persist_rollout_items(&[RolloutItem::WorldState(world_state_item)]).await;
}
if only_world_state_changed {
return Ok(world_state);
}
self.persist_rollout_items(&[RolloutItem::TurnContext(turn_context_item.clone())])
.await;因此“指令生效”至少有三种时刻:构造出的 ResponseItem 进入当前 prompt、写入 live history,或成为 rollout 中可供恢复使用的基线;它们不一定在同一时刻发生。
第一次没有 reference baseline 时注入完整上下文;之后只写入 WorldState diff,并在 TurnContext 变化时追加本回合 extension contribution。变更可能只影响内存快照而不产生 model-visible item;这种情况下不会制造重复的 TurnContext 记录,但仍会持久化新的 baseline,供 resume 和下一回合比较。
7. 测试边界
源码测试位于 codex-rs/core/src/session/tests.rs。build_initial_context_prepends_model_switch_message 设置旧模型,断言首项是 developer message 且含 <model_switch>;它证明前插位置,不证明 provider 最终如何解释该标记。
build_initial_context_includes_prompt_fragments_from_extensions 注入 thread extension state,断言 developer 文本出现;build_initial_context_omits_prompt_fragments_without_extension_state 使用同一 registry 但不提供 state,断言文本缺失。这两个测试共同证明 extension state 是注入门槛,而不是“注册了 contributor 就必然可见”。
多 agent 测试分别验证 root/subagent 使用不同 usage hint,并验证 feature 关闭或 hint 为空时不注入独立 developer message。它们证明选择条件和消息边界,不证明任意自定义 fragment 的排序,也不覆盖真实网络 provider 的 tokenization 或模型服从程度。
8. 读源码验证
可以用以下只读搜索复现本文主线:
rg -n "build_initial_context_with_world_state|record_context_updates_and_set_reference_context_item" \
codex-rs/core/src/session/mod.rs
rg -n "build_responses_request|base_instructions|use_responses_lite" \
codex-rs/core/src/client.rs codex-rs/core/src/client_common.rs阅读时先在 world_state.rs 标出 section 的添加顺序,再回到 mod.rs 看 role 分桶和独立消息分支,最后检查 client.rs 的两个 wire mode。若某段文本在历史中找不到,先检查它是否属于 BaseInstructions;若同一 section 在下一回合消失,检查其 render_diff 是否生成 removal fragment,而不要假设 ContextManager 丢失了历史。
