# 10 - 预算控制与性能策略

## 问题

不同 LLM 模型的上下文窗口差异巨大：

| 模型 | 上下文窗口 |
|------|-----------|
| GPT-4o | 128K tokens |
| Claude 3.5 Sonnet | 200K tokens |
| Qwen-2.5 72B | 128K tokens |
| 本地小模型 | 4K-32K tokens |

固定 token 预算（如原设计的"4000 tokens"）不适用于所有场景。

---

## 动态预算分配

### 总预算计算

```rust
pub struct MemoryBudget {
    pub total_tokens: usize,
    pub injection_tokens: usize,
    pub recall_tokens: usize,
    pub workspace_index_tokens: usize,
}

impl MemoryBudget {
    /// 根据模型上下文窗口动态计算
    pub fn from_model_context(model_context_window: usize) -> Self {
        // Memory 总预算 = 上下文窗口的 15%，上限 8000 tokens
        let total = (model_context_window as f32 * 0.15).min(8000.0) as usize;

        Self {
            total_tokens: total,
            injection_tokens: (total as f32 * 0.4) as usize,   // 40% → Injection
            recall_tokens: (total as f32 * 0.45) as usize,     // 45% → Recall
            workspace_index_tokens: (total as f32 * 0.15) as usize, // 15% → Index
        }
    }
}
```

### 各段预算示例

| 模型上下文 | 总预算 | Injection | Recall | Index |
|-----------|--------|-----------|--------|-------|
| 4K | 600 | 240 | 270 | 90 |
| 32K | 4800 | 1920 | 2160 | 720 |
| 128K | 8000* | 3200 | 3600 | 1200 |

*128K 以上模型也限制在 8000 tokens，避免 Memory 占用过多上下文。

---

## 检索延迟预算

双引擎召回的延迟分配：

| 阶段 | 延迟预算 | 说明 |
|------|---------|------|
| Query 分析 | < 1ms | 纯规则判定 |
| 锚点提取 | < 5ms | 正则 + 关键词提取 |
| Tantivy BM25 查询 | < 5ms | 本地索引，极快 |
| Qdrant 语义查询 | < 40ms | 嵌入式模式，含 embedding 计算 |
| 融合重排 | < 2ms | 纯计算 |
| **总计** | **< 55ms** | 不阻塞用户交互 |

### 降级策略

当 Qdrant 不可用（Phase B 阶段或 embedding 服务异常）时，自动降级为 Tantivy 单引擎模式：

```rust
pub async fn recall_with_fallback(
    &self,
    anchors: &RecallAnchors,
    intent: QueryIntent,
    limit: usize,
) -> Result<Vec<RecallHit>> {
    match self.dual_recall(anchors, intent, limit).await {
        Ok(results) => Ok(results),
        Err(e) => {
            tracing::warn!("双引擎召回失败，降级为 Tantivy 单引擎: {}", e);
            self.tantivy_only_recall(anchors, limit)
        }
    }
}
```

---

## 与 ContextOrganizer 的协调

当前 `ContextOrganizer` 负责 History 消息的 token 管理。Memory 预算需要与之协调：

```rust
// context/organizer.rs（修改后）
pub struct ContextBudget {
    pub model_context_window: usize,
    pub system_prompt_tokens: usize,
    pub memory_tokens: usize,         // ← 新增
    pub history_tokens: usize,
    pub tool_context_tokens: usize,
    pub reserve_tokens: usize,        // 留给模型输出
}

impl ContextBudget {
    pub fn calculate(model_context_window: usize) -> Self {
        let reserve = (model_context_window as f32 * 0.15) as usize;
        let memory = MemoryBudget::from_model_context(model_context_window).total_tokens;
        let system = 2000; // 固定
        let tool_context = 1000; // 固定

        let history = model_context_window - reserve - memory - system - tool_context;

        Self {
            model_context_window,
            system_prompt_tokens: system,
            memory_tokens: memory,
            history_tokens: history,
            tool_context_tokens: tool_context,
            reserve_tokens: reserve,
        }
    }
}
```

---

## 超限裁剪策略

当注入内容超过分配预算时的降级策略：

### Injection 超限

```rust
pub fn trim_injection(sections: Vec<String>, budget_tokens: usize) -> Vec<String> {
    let mut result = Vec::new();
    let mut remaining = budget_tokens;

    // 优先级：Session > Workspace > Profile
    for section in sections.into_iter().rev() {
        let tokens = estimate_tokens(&section);
        if tokens <= remaining {
            result.push(section);
            remaining -= tokens;
        } else {
            // 按行截断
            let trimmed = trim_to_tokens(&section, remaining);
            if !trimmed.is_empty() {
                result.push(trimmed);
            }
            break;
        }
    }

    result.reverse();
    result
}
```

### Recall 超限

```rust
pub fn trim_recall_results(hits: Vec<RecallHit>, budget_tokens: usize) -> Vec<RecallHit> {
    let mut result = Vec::new();
    let mut remaining = budget_tokens;

    // 按 relevance_score 降序，逐条加入
    for hit in hits {
        let tokens = estimate_tokens(&hit.summary);
        if tokens <= remaining {
            result.push(hit);
            remaining -= tokens;
        } else {
            break; // 预算用尽
        }
    }

    result
}
```

---

## Token 估算

使用简单的字符数估算，避免引入 tokenizer 依赖：

```rust
/// 简易 token 估算
/// 英文约 4 字符/token，中文约 1.5 字符/token
/// 取保守估计：中文 1.5 字符/token
pub fn estimate_tokens(text: &str) -> usize {
    let char_count = text.chars().count();
    let cjk_count = text.chars().filter(|c| is_cjk(*c)).count();
    let non_cjk_count = char_count - cjk_count;

    (cjk_count as f32 / 1.5 + non_cjk_count as f32 / 4.0).ceil() as usize
}

fn is_cjk(c: char) -> bool {
    matches!(c, '\u{4e00}'..='\u{9fff}'
        | '\u{3400}'..='\u{4dbf}'
        | '\u{f900}'..='\u{faff}')
}
```

---

## 性能约束

| 操作 | 延迟预算 | 说明 |
|------|---------|------|
| Injection 加载 | < 5ms | 3 个文件读取 |
| Token 估算 | < 1ms | 纯字符扫描 |
| 预算计算 | < 0.1ms | 算术运算 |
| 裁剪处理 | < 2ms | 字符串截断 |
| 总额外延迟 | < 10ms | 不影响用户感知 |

---

## 监控指标

Phase C+ 可考虑记录以下指标用于调优：

```rust
#[derive(Debug, Default)]
pub struct MemoryMetrics {
    pub injection_tokens_used: usize,
    pub recall_tokens_used: usize,
    pub recall_hits_count: usize,
    pub budget_utilization: f32,  // 实际使用 / 分配预算
}
```

当 `budget_utilization` 长期 < 0.3 时，说明 Memory 数据不足；长期 > 0.95 时，说明预算紧张需要调大。
