# 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>>)` **方案选定**:构造注入。`AdversarialEngine` 从无状态静态结构改为持有 `Option>`。 ```rust // 改造后 pub struct AdversarialEngine { provider: Option>, } impl AdversarialEngine { /// 注入 LLM provider 构造 pub fn new(provider: Arc) -> Self { ... } /// 纯启发式模式(无 LLM) pub fn heuristic() -> Self { Self { provider: None } } /// 执行对抗评估 pub async fn evaluate(&self, idea: &Idea) -> Result { ... } } ``` **论据**: 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 { 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`,注入到 `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 → 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>` 字段;加 `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>`(复用 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>)` | 与 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) (灵感模块条目)