# F-260614-07：df-ai-core trait 下沉拆 crate — 增强设计

> 将 `LlmProvider` trait + AI 数据类型从 `df-ai` 拆出，下沉到新轻量 crate `df-ai-core`，确立全局 AI 接入标准。本文档为功能决策记录同名条目的详细设计展开。
>
> 创建：2026-06-14 | 状态：📐 设计定稿待实施 | 前置：无 | 解锁：F-260614-03（对抗评估接 LLM）

## 1. 背景与问题

### 1.1 核心矛盾

`df-ideas` 的 `adversarial.rs` 对抗评估系统当前是纯启发式实现（基于评分生成正反方论点），需要接入 LLM 让论点由 AI 生成。但 `df-ideas` 不应直接依赖 `df-ai`——那会引入 reqwest/futures/eventsource-stream 等重 HTTP 依赖到灵感模块，违反 crate 职责分层。

### 1.2 当前依赖关系

```
df-core（基础类型，零 AI 依赖）
  ├── df-ai（LLM Provider、上下文管理、流式、工具注册）
  │     └── 依赖 reqwest、eventsource-stream 等 HTTP 库
  ├── df-ideas（灵感捕获、评分、对抗评估）
  │     └── ❌ 当前不依赖 df-ai，adversarial.rs 全是硬编码启发式
  └── df-nodes（工作流节点）
        └── ✅ 已依赖 df-ai（AiNode 直接调 build_provider）
```

### 1.3 df-ai 现有模块分析

| 模块 | 行数 | 依赖 | 性质 |
|------|------|------|------|
| `provider.rs` | 221 | serde, async-trait, futures | 纯 trait + 数据结构，**零 IO** |
| `context.rs` | 511 | provider.rs | 消息管理 + token 裁剪，纯内存逻辑 |
| `ai_tools.rs` | 179 | provider.rs | 工具注册表，纯内存逻辑 |
| `stream.rs` | 45 | provider.rs | StreamCollector，纯内存逻辑 |
| `router.rs` | 51 | serde | 模型路由，纯逻辑（当前 TODO 占位） |
| `openai_compat.rs` | ~700 | reqwest, eventsource-stream | **HTTP 实现** |
| `anthropic_compat.rs` | ~800 | reqwest, eventsource-stream | **HTTP 实现** |
| `coordinator.rs` | 30 | — | B 路线占位空壳 |

## 2. 决策（4 项，均已定稿）

### 决策 1：df-ai-core 拆分边界 — 仅 trait + 数据结构

**方案选定**：仅将 `LlmProvider` trait 和请求/响应数据结构下沉到 `df-ai-core`。`ContextManager`、`TokenEstimator`、`AiToolRegistry` 等留在 `df-ai`。

| 放入 df-ai-core | 留在 df-ai |
|---|---|
| `LlmProvider` trait | `OpenAICompatProvider` / `AnthropicCompatProvider`（impl） |
| `ChatMessage` / `MessageRole` | `build_provider()` 工厂函数 |
| `CompletionRequest` / `CompletionResponse` | `ContextManager`（上下文裁剪） |
| `ToolDefinition` / `ToolCall` / `ToolCallDelta` | `AiToolRegistry`（工具注册） |
| `StreamChunk` / `TokenUsage` / `StreamResult` | `StreamCollector`（流式收集） |
| `ProviderFeatures` | `ModelRouter`（模型路由） |
| `ToolCallFunction` / `ToolFunction` | `AgentCoordinator`（B 路线占位） |

**df-ai-core 依赖**：serde, async-trait, futures（极轻，零 HTTP）

**论据**：

1. **df-ideas 实际需求极窄** — `adversarial.rs` 接 LLM 只需 `provider.complete()` 一次调用（1 条 system + 1 条 user），不需要流式、上下文裁剪、工具调用。下沉 ContextManager 是无收益的耦合。
2. **ContextManager 与 AI Chat 强绑定** — 它的 `build_eviction_units` 保护工具调用三元组、PROTECT_COUNT 保留最近 6 条消息，这些都是 agentic loop 的概念。下沉会让 df-ideas 无意中依赖它不需要的概念。
3. **变更频率差异** — trait 定义（provider.rs 结构体部分）自创建以来几乎没变；ContextManager 在 Sprint 8-18 多次迭代裁剪策略。下沉变更频繁的代码违反接口隔离原则。

**备选方案（否决）**：额外下沉 ContextManager — 无消费方需要，徒增耦合。

---

### 决策 2：provider 注入方式 — 构造注入 `Engine::new(Option<Arc<dyn LlmProvider>>>)`

**方案选定**：构造注入。`AdversarialEngine` 从无状态静态结构改为持有 `Option<Arc<dyn LlmProvider>>`。

```rust
// 改造后
pub struct AdversarialEngine {
    provider: Option<Arc<dyn LlmProvider>>,
}

impl AdversarialEngine {
    /// 注入 LLM provider 构造
    pub fn new(provider: Arc<dyn LlmProvider>) -> Self { ... }
    /// 纯启发式模式（无 LLM）
    pub fn heuristic() -> Self { Self { provider: None } }
    /// 执行对抗评估
    pub async fn evaluate(&self, idea: &Idea) -> Result<AdversarialEval> { ... }
}
```

**论据**：

1. **与现有代码风格一致** — `IdeaPromoter::new(policy)` 已是构造注入模式（`df-ideas/src/promotion.rs`），`AdversarialEngine` 跟随同一模式。
2. **Option 天然表达降级** — `None` 时走启发式，`Some` 时走 LLM + 失败降级。类型系统层面清晰表达"有无 LLM"两种模式，与决策 3 的降级策略无缝配合。
3. **批量评估友好** — `evaluate_idea` IPC 批量评估 N 个灵感时，构造 1 次 engine，evaluate N 次，provider 只注入一次。参数注入方式每次调用都要传。
4. **未来扩展空间** — 后续如需给对抗评估加配置（温度、模型偏好、最大 token），构造注入只需加字段；参数注入则签名越来越长。

**改造影响面**：
- `adversarial.rs`：struct 加字段，`evaluate` 改 `&self`，内部 6 个 `Self::method()` 改 `self.method()`
- `idea.rs`（调用方）：`AdversarialEngine::evaluate(&idea)` → 先构造再 evaluate
- 7 个单元测试：改为 `AdversarialEngine::heuristic().evaluate(&idea)` 或加辅助函数

**备选方案（否决）**：参数注入 `evaluate(&idea, &dyn LlmProvider)` — 与 IdeaPromoter 模式不一致，批量评估重复传参。

---

### 决策 3：LLM 失败降级策略 — 自动降级到启发式 + warn 日志 + 评估来源标记

**方案选定**：LLM 调用失败/超时/格式异常时，自动降级到启发式评估，`tracing::warn!` 记录失败原因。返回结果中新增 `evaluated_by` 字段标记评估来源。

```rust
pub async fn evaluate(&self, idea: &Idea) -> Result<AdversarialEval> {
    match &self.provider {
        Some(p) => match self.evaluate_with_llm(idea, p).await {
            Ok(mut eval) => {
                eval.evaluated_by = EvaluatedBy::Llm;
                Ok(eval)
            }
            Err(e) => {
                tracing::warn!("LLM 对抗评估失败, 降级到启发式: {e}");
                let mut eval = self.evaluate_heuristic(idea);
                eval.evaluated_by = EvaluatedBy::HeuristicFallback;
                Ok(eval)
            }
        },
        None => {
            let mut eval = self.evaluate_heuristic(idea);
            eval.evaluated_by = EvaluatedBy::Heuristic;
            Ok(eval)
        }
    }
}
```

**新增数据结构**：

```rust
/// 评估来源标记
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum EvaluatedBy {
    /// LLM 深度评估
    Llm,
    /// 启发式评估（无 LLM 配置时的默认模式）
    Heuristic,
    /// 启发式降级（LLM 调用失败后 fallback）
    HeuristicFallback,
}
```

`AdversarialEval` 新增字段：`pub evaluated_by: EvaluatedBy`

**论据**：

1. **启发式不是残次品** — 当前启发式是一个完整的评估系统：正方/反方论点基于真实评分数据生成，有区分度（confidence 区间设计合理），7 个单元测试覆盖高/中/低分路径，与 promotion 系统联动。降级是"降级到够用"而非"降级到垃圾"。
2. **保证前端结构完整** — `evaluate_idea` IPC 的调用方（前端 Ideas.vue）期望拿到完整的 `AdversarialEval` 结构。降级保证结构完整返回，前端不会 crash。报错则前端需额外处理错误态。
3. **批量评估容错** — 批量评估 50 个灵感时，第 3 个 LLM 失败不影响其余 47 个。报错中断会导致前面的评估结果全部丢失。
4. **透明化** — `EvaluatedBy` 标记让前端可显示"AI 深度评估"或"快速评估"标签，避免用户误判评估深度。三种状态（Llm / Heuristic / HeuristicFallback）精确区分"主动选择启发式"与"被动降级"。

**降级场景分析**：

| 失败场景 | 发生概率 | 降级影响 | 报错影响 |
|---------|---------|---------|---------|
| 网络超时（reqwest 无超时，FR-R4 已记） | 高 | 基于评分的评估，论点稍模板化 | 功能完全不可用 |
| API Key 无效/额度用尽 | 中 | 同上 | 同上 |
| LLM 返回 JSON 解析失败 | 中 | 同上 | 同上 |
| 批量评估中部分失败 | 中 | 失败的启发式兜底，成功的保留 LLM 质量 | 全部中断 |

**备选方案（否决）**：直接报错中断 — 启发式已足够稳定，中断用户体验不可接受。

---

### 决策 4：provider 构造归属 — 应用层（src-tauri）构造并注入

**方案选定**：provider 的构造（`build_provider`）仍由 `src-tauri` 应用层完成，从 DB 读取 provider 配置后构造 `Box<dyn LlmProvider>`，注入到 `AdversarialEngine`。`df-ideas` 只依赖 `df-ai-core` 的 trait，不负责构造。

**改造后调用链路**：

```
src-tauri/src/commands/idea.rs::evaluate_idea()
  → 从 state.ai_providers (AiProviderRepo) 读 DB 配置（复用 AI Chat 已有逻辑）
  → df_ai::build_provider(protocol, base_url, api_key, model) 构造 Box<dyn LlmProvider>
  → AdversarialEngine::new(Arc::from(provider))
  → engine.evaluate(&idea)
```

**应用层改造（idea.rs）**：

```rust
// 改造前：
let eval = df_ideas::adversarial::AdversarialEngine::evaluate(&idea).await?;

// 改造后：
let provider = build_default_provider(&state).await;  // 从 DB 读配置 + build_provider
let engine = match provider {
    Some(p) => df_ideas::adversarial::AdversarialEngine::new(p),
    None => df_ideas::adversarial::AdversarialEngine::heuristic(),
};
let eval = engine.evaluate(&idea).await?;
```

其中 `build_default_provider` 复用 AI Chat 已有的 provider 选择逻辑（从 `ai_providers` 表取 `is_default=true` 的配置）。

**论据**：

1. **df-ideas 依赖 df-ai 违背任务初衷** — 本任务的存在原因就是不让 df-ideas 依赖 df-ai。让 df-ideas 内部构造 provider 需要传入 base_url/api_key/model/protocol，等于强制依赖 df-ai 的 `build_provider` + HTTP 实现。
2. **配置访问权属于应用层** — provider 配置（api_key、base_url）存在 SQLite，通过 `AiProviderRepo` 访问，是 `AppState` 的字段。df-ideas 作为领域 crate 不应知道数据库。
3. **与 AI Chat 构造路径统一** — AI Chat 也是应用层从 DB 读配置后 `build_provider`，统一构造路径避免分裂。
4. **可测试性** — 测试时传 mock provider 构造 engine，不需要真实配置。

**备选方案（否决）**：df-ideas 内部自行构造 — 直接违背任务前提（df-ideas 不依赖 df-ai），引入循环依赖风险。

## 3. 实施清单

### 3.1 新建 df-ai-core crate

| 文件 | 内容 |
|------|------|
| `crates/df-ai-core/Cargo.toml` | 依赖 serde, async-trait, futures（极轻） |
| `crates/df-ai-core/src/lib.rs` | `pub mod provider;` + re-export |
| `crates/df-ai-core/src/provider.rs` | 从 `df-ai/src/provider.rs` 迁移：`LlmProvider` trait + 全部数据结构 |

迁移内容清单（从 df-ai/src/provider.rs）：
- `LlmProvider` trait（含 `complete` / `stream` / `embed` / `name` / `supported_features`）
- `CompletionRequest` / `CompletionResponse`
- `ChatMessage` / `MessageRole`
- `ToolDefinition` / `ToolFunction`
- `ToolCall` / `ToolCallFunction` / `ToolCallDelta`
- `TokenUsage` / `ProviderFeatures` / `StreamChunk`
- `StreamResult` 类型别名

### 3.2 df-ai 改造

| 改动 | 详情 |
|------|------|
| `Cargo.toml` | 加 `df-ai-core = { path = "../df-ai-core" }` 依赖 |
| `src/provider.rs` | 改为 `pub use df_ai_core::provider::*;`（re-export 保持外部兼容） |
| `src/lib.rs` | 加 `pub use df_ai_core;`（可选，供直接引用） |
| 其他模块 | 零改动 — `use crate::provider::ChatMessage` 等路径通过 re-export 仍然有效 |

### 3.3 df-ideas 改造

| 改动 | 详情 |
|------|------|
| `Cargo.toml` | 加 `df-ai-core = { path = "../df-ai-core" }` + `async-trait`（如需） |
| `src/adversarial.rs` | struct 加 `provider: Option<Arc<dyn LlmProvider>>` 字段；加 `new()` / `heuristic()` 构造方法；`evaluate()` 改 `&self`；加 `evaluate_with_llm()` / `evaluate_heuristic()` 内部方法；加 `EvaluatedBy` 枚举 + `AdversarialEval.evaluated_by` 字段 |
| `src/lib.rs` | 无改动 |
| 单元测试 | 7 个测试改为 `AdversarialEngine::heuristic().evaluate(&idea)` |

### 3.4 src-tauri 改造

| 改动 | 详情 |
|------|------|
| `commands/idea.rs::evaluate_idea()` | 从 DB 读默认 provider 配置 → `build_provider()` → `AdversarialEngine::new(Arc::from(provider))` → `engine.evaluate(&idea)` |
| 新增辅助函数 | `build_default_provider(state) -> Option<Box<dyn LlmProvider>>`（复用 AI Chat provider 选择逻辑） |
| `Cargo.toml` | 无改动（已依赖 df-ai + df-ideas） |

### 3.5 df-nodes 改造

| 改动 | 详情 |
|------|------|
| 无实质改动 | df-ai re-export 后 `use df_ai::provider::LlmProvider` 仍可用 |

### 3.6 workspace Cargo.toml

| 改动 | 详情 |
|------|------|
| 无改动 | `members = ["crates/*", "src-tauri"]` 自动包含新 crate |

## 4. 风险评估

| 风险项 | 等级 | 缓解措施 |
|--------|------|----------|
| re-export 路径断裂 | 低 | `pub use df_ai_core::provider::*` 保持 `df_ai::provider::LlmProvider` 路径不变，编译器验证 |
| df-ai-core 依赖膨胀 | 低 | 仅 serde + async-trait + futures，与 df-core 同级别 |
| 循环依赖 | 无 | df-ai-core（叶）← df-ai（impl）/ df-ideas（use trait），src-tauri 装配，无环 |
| 测试回归 | 低 | 7 个 adversarial 单测改为 heuristic() 构造，逻辑不变 |

## 5. 决策总结

| # | 决策项 | 选定方案 | 核心论据 | 置信度 |
|---|--------|---------|----------|--------|
| 1 | df-ai-core 拆分边界 | 仅 trait + 数据结构 | df-ideas 只需 complete() 一次调用；ContextManager 与 agentic loop 强绑定，变更频繁 | ⭐⭐⭐⭐⭐ |
| 2 | provider 注入方式 | 构造注入 `Engine::new(Option<Arc<dyn LlmProvider>>)` | 与 IdeaPromoter 模式一致；Option 天然表达降级；批量评估友好 | ⭐⭐⭐⭐⭐ |
| 3 | LLM 失败降级策略 | 自动降级 + warn + EvaluatedBy 标记 | 启发式是完整系统非残次品；保证前端结构完整；需补充评估来源标记 | ⭐⭐⭐⭐ |
| 4 | provider 构造归属 | 应用层构造注入 | 方案 B 直接违背任务初衷；配置访问权属于应用层；与 AI Chat 构造路径统一 | ⭐⭐⭐⭐⭐ |

## 6. 解锁关系

```
F-260614-07（本任务：df-ai-core trait 下沉）
  └── 解锁 F-260614-03（灵感对抗评估接 LLM）
        └── 解锁后续：scoring.rs 语义级评分接 LLM
```

---

**相关文档**：
- [功能决策记录 — AI trait 下沉条目](./功能决策记录-2026-06-14.md) （决策记录主文档）
- [功能决策记录 — 对抗评估启发式 fallback 待接 LLM](./功能决策记录-2026-06-14.md) （灵感模块条目）
