enhance(docs): 更新 system prompt 双重表达冲突推演结论为方案 D

This commit is contained in:
徐涛
2026-06-22 06:34:11 +08:00
parent 80d44ee687
commit 3ddd0b6d80
3 changed files with 56 additions and 26 deletions
+1 -1
View File
@@ -108,6 +108,6 @@ pub type Message = OpenaiChatMessage;
| 6 | Anthropic 流式状态机设计 | [9d-provider-implementations.md](9d-provider-implementations.md#52-anthropicprovidermessages-api) | ✅ 已推演(轻量分发器:3 状态 + 7 种事件映射 + 零 index 映射) |
| 7 | OpenAI Response API 完整映射表 | [9d-provider-implementations.md](9d-provider-implementations.md#53-openai-response-api草案) | 低 |
| 8 | DeepSeek/Qwen Provider 落地策略 | [9d-provider-implementations.md](9d-provider-implementations.md#54-deepseek--qwen-等兼容-provider-的落地策略) | 低 |
| 9 | system prompt 双重表达冲突 | [9e-llm-cycle-and-upstream.md](9e-llm-cycle-and-upstream.md#62-build_request--新签名) | **高** |
| 9 | system prompt 双重表达冲突 | [9e-llm-cycle-and-upstream.md](9e-llm-cycle-and-upstream.md#62-build_request--新签名) | ✅ 已推演(方案 D:移除 system 字段,IR 只留一个入口,Provider 层负责映射) |
| 10 | compact 在 IR 上的改法与 token 估算 | [9e-llm-cycle-and-upstream.md](9e-llm-cycle-and-upstream.md#66-compact-逻辑调整) | 中 |
| 11 | Thinking signature 端到端传递 | [9f-edge-cases.md](9f-edge-cases.md#92-thinking-的端到端流程) | ✅ 已推演(方案 CMessageComplete 兜底 + finalize 回填) |
+2 -1
View File
@@ -237,7 +237,8 @@ pub struct MessageRequest {
// ════════════════════════════════════════════
pub model: String,
pub messages: Vec<Message>,
pub system: Option<String>, // 顶层 system prompt
// 没有 `system` 字段——系统提示统一通过 `Message::System { content }`
// 在 `messages` 中表达。各 Provider 在 IR→原生映射层自行处理差异。
pub tools: Vec<ToolDefinition>,
pub tool_choice: ToolChoice,
pub max_tokens: Option<u32>,
+53 -24
View File
@@ -30,11 +30,15 @@ pub struct LlmCycle {
```rust
fn build_request(&self, tools: &[ToolDefinition]) -> MessageRequest {
let mut messages = self.messages.clone();
if let Some(sys_prompt) = &self.system_prompt
&& !messages.iter().any(|m| matches!(m, Message::System { .. }))
{
if let Some(sys_prompt) = &self.system_prompt {
// `system_prompt` 是权威来源:显式设置时替换 messages 中的任何 System
messages.retain(|m| !matches!(m, Message::System { .. }));
messages.insert(0, Message::system(sys_prompt));
}
// 如果 `system_prompt` 为 Nonemessages 中的 System 保持原样,
// 由各 Provider 在 IR→原生映射层各自处理
MessageRequest {
model: self.config.model.clone(),
messages,
@@ -47,29 +51,54 @@ fn build_request(&self, tools: &[ToolDefinition]) -> MessageRequest {
}
```
> **🔄 待深入推演:system prompt 的双重表达冲突**
> `MessageRequest` 同时存在两个 system prompt 入口:
> 1. `messages` 中的 `Message::System { ... }`
> 2. 顶层字段 `system: Option<String>`
> **✅ 推演结论(2026-06-22):方案 D —— 移除 `system` 字段,IR 中只留一个入口**
>
> 问题在于:
> - **Anthropic** 要求 system prompt 只能用顶层 `system` 参数,`messages` 中不能包含 system role
> - **OpenAI** 没有顶层 `system` 参数,system prompt 放在 messages 中
> - 当前 `build_request` 的逻辑是:将 `self.system_prompt` 插入到 `messages` 开头(作为 `Message::System`),
> 但**不**填充 `request.system`。这对 OpenAI 是正确的,但对 Anthropic 是错的——AnthropicProvider
> 需要反向提取:遍历 messages 找出 System 消息,移到 `request.system`,再从 messages 中移除。
> **决策:** 从 `MessageRequest` 中移除 `system: Option<String>` 字段,统一通过 `messages: Vec<Message>`
> 中的 `Message::System { content }` 表达系统提示。各 Provider 在 IR→原生 映射层自行处理差异。
>
> **需要推演**
> 1. **方案 A(当前方案):** `build_request` 始终把 system prompt 放进 messagesAnthropicProvider 内部
> 做反向提取。问题:当 LlmCycle 的消息历史中已有一个 `Message::System``self.system_prompt` 也被设置时
> 两个 source 哪个优先?当前代码只检查 `messages` 中是否已有 System 来决定是否插入,但 LlmCycle 的
> `with_system_prompt` 设置的 prompt 可能与消息历史中已有的 System 内容不同。
> 2. **方案 B** 统一通过顶层 `request.system` 传递,OpenaiProvider 内部将 `request.system` 转为
> System message 插入 messages 开头(与 AnthropicProvider 方向相反)。缺点:OpenAI 用户习惯
> 在 messages 中直接放 system message,可能不设置顶层 system。
> 3. **方案 C** 约定 `messages` 中的 System 和顶层 `system` 互斥——`build_request` 检查两者,
> 同时存在时报错或合并(顶层 system 覆盖 messages 中的 System)。
> 优先级:高(Phase 2-3 实现 AnthropicProvider 前必须解决)
> **理由**
> 1. **与整套 IR 设计的理念一致**——IR 只描述"有什么",不关心"怎么传"。ToolResult 嵌套约束、
> Thinking 端到端、StreamEvent 汇总等问题的解决方向都是"Provider 层负责格式差异"
> system prompt 的双重入口是同一个问题,应用同样的原则。
> 2. **消除歧义的最佳方式是砍掉一个入口**——两个入口导致的"谁优先"问题在类型层面就解决了,
> 不需要运行时规则。
> 3. **每个 Provider 做自己的转换本来就是 Provider 层的职责**——OpenaiProvider 几乎零成本
> System 直接序列化为 role=`system`),AnthropicProvider 做提取+移除(约 10 行代码),
> DeepSeek/Qwen 同 OpenAI。没有 Provider 需要额外做反向工作。
>
> **具体做法:**
>
> **① `MessageRequest`[9b-ir-type-system.md](9b-ir-type-system.md#36-messagerequest--统一请求)**
> 删除 `system: Option<String>` 字段。`messages: Vec<Message>` 是系统提示的唯一载体。
>
> **② `LlmCycle::build_request`(本节上方代码)**
> 增加"替换"语义:`self.system_prompt` 设置时,先 `retain` 移除 messages 中已有的所有
> `Message::System`,再插入新的。确保 `system_prompt` 作为权威来源。
>
> **③ `OpenaiProvider::ir_to_native`**
> 零改动。`Message::System { content }` 直接映射为 `role: "system"`(或 `role: "developer"`)。
>
> **④ `AnthropicProvider::ir_to_native`**[9d-provider-implementations.md](9d-provider-implementations.md)
> 新增提取逻辑:
> ```rust
> // 1. 遍历 messages,收集所有 System 的纯文本内容
> // 2. 若有多个 System,合并为一个字符串(Anthropic 只接受一个)
> // 3. 设置 Anthropic 请求的顶层 `system` 参数
> // 4. 从 messages 中移除所有 System 消息
> // 5. 非文本 ContentBlock 静默丢弃 + warn! log
> ```
>
> **边界情况处理:**
> | `self.system_prompt` | messages 中已有的 System | 结果 |
> |---|---|---|
> | `None` | 无 System | messages 不变 |
> | `None` | `System("B")` | 保留,Provider 层处理 |
> | `Some("A")` | 无 System | 插入 System("A") |
> | `Some("A")` | `System("B")` | **移除 B,插入 A**(显式设置优先) |
> | `Some("A")` | 多个 System("B1"), ("B2") | **移除所有,插入 A** |
>
> **何时实现:** Phase 2-3 实现 AnthropicProvider 时同步完成。
> **影响范围:** `MessageRequest` 删除一个字段 + `build_request` 增 2 行 `retain` + AnthropicProvider 增约 10 行提取逻辑。
主要变化:
- 返回类型 `MessageRequest`(非 `ChatRequest`