squash合并: - 意图识别层论证(8维度+10业界佐证) - 多主题上下文管理愿景+并存论证+补充论证(多轮agentic) - 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化) - 前端架构技术债清单归档
295 lines
15 KiB
Markdown
295 lines
15 KiB
Markdown
# 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) (灵感模块条目)
|