Files
DevFlow/docs/02-架构设计/已编号方案/F-07-df-ai-core-trait下沉设计-2026-06-14.md
绝尘 998a2f243d 文档: 架构方案文档(意图识别论证+多主题愿景/论证+文档物理分类+边界清晰化)
squash合并:
- 意图识别层论证(8维度+10业界佐证)
- 多主题上下文管理愿景+并存论证+补充论证(多轮agentic)
- 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化)
- 前端架构技术债清单归档
2026-06-19 15:04:04 +08:00

295 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# F-260614-07df-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-aiLLM Provider、上下文管理、流式、工具注册
│ └── 依赖 reqwest、eventsource-stream 等 HTTP 库
├── df-ideas灵感捕获、评分、对抗评估
│ └── ❌ 当前不依赖 df-aiadversarial.rs 全是硬编码启发式
└── df-nodes工作流节点
└── ✅ 已依赖 df-aiAiNode 直接调 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 项,均已定稿)
### 决策 1df-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 — 无消费方需要,徒增耦合。
---
### 决策 2provider 注入方式 — 构造注入 `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 次 engineevaluate 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 模式不一致,批量评估重复传参。
---
### 决策 3LLM 失败降级策略 — 自动降级到启发式 + 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 质量 | 全部中断 |
**备选方案(否决)**:直接报错中断 — 启发式已足够稳定,中断用户体验不可接受。
---
### 决策 4provider 构造归属 — 应用层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-aiimpl/ df-ideasuse traitsrc-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) (灵感模块条目)