squash合并: - 意图识别层论证(8维度+10业界佐证) - 多主题上下文管理愿景+并存论证+补充论证(多轮agentic) - 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化) - 前端架构技术债清单归档
15 KiB
F-260614-07:df-ai-core trait 下沉拆 crate — 增强设计
将
LlmProvidertrait + AI 数据类型从df-ai拆出,下沉到新轻量 cratedf-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)
论据:
- df-ideas 实际需求极窄 —
adversarial.rs接 LLM 只需provider.complete()一次调用(1 条 system + 1 条 user),不需要流式、上下文裁剪、工具调用。下沉 ContextManager 是无收益的耦合。 - ContextManager 与 AI Chat 强绑定 — 它的
build_eviction_units保护工具调用三元组、PROTECT_COUNT 保留最近 6 条消息,这些都是 agentic loop 的概念。下沉会让 df-ideas 无意中依赖它不需要的概念。 - 变更频率差异 — trait 定义(provider.rs 结构体部分)自创建以来几乎没变;ContextManager 在 Sprint 8-18 多次迭代裁剪策略。下沉变更频繁的代码违反接口隔离原则。
备选方案(否决):额外下沉 ContextManager — 无消费方需要,徒增耦合。
决策 2:provider 注入方式 — 构造注入 Engine::new(Option<Arc<dyn LlmProvider>>>)
方案选定:构造注入。AdversarialEngine 从无状态静态结构改为持有 Option<Arc<dyn LlmProvider>>。
// 改造后
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> { ... }
}
论据:
- 与现有代码风格一致 —
IdeaPromoter::new(policy)已是构造注入模式(df-ideas/src/promotion.rs),AdversarialEngine跟随同一模式。 - Option 天然表达降级 —
None时走启发式,Some时走 LLM + 失败降级。类型系统层面清晰表达"有无 LLM"两种模式,与决策 3 的降级策略无缝配合。 - 批量评估友好 —
evaluate_ideaIPC 批量评估 N 个灵感时,构造 1 次 engine,evaluate N 次,provider 只注入一次。参数注入方式每次调用都要传。 - 未来扩展空间 — 后续如需给对抗评估加配置(温度、模型偏好、最大 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 字段标记评估来源。
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)
}
}
}
新增数据结构:
/// 评估来源标记
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum EvaluatedBy {
/// LLM 深度评估
Llm,
/// 启发式评估(无 LLM 配置时的默认模式)
Heuristic,
/// 启发式降级(LLM 调用失败后 fallback)
HeuristicFallback,
}
AdversarialEval 新增字段:pub evaluated_by: EvaluatedBy
论据:
- 启发式不是残次品 — 当前启发式是一个完整的评估系统:正方/反方论点基于真实评分数据生成,有区分度(confidence 区间设计合理),7 个单元测试覆盖高/中/低分路径,与 promotion 系统联动。降级是"降级到够用"而非"降级到垃圾"。
- 保证前端结构完整 —
evaluate_ideaIPC 的调用方(前端 Ideas.vue)期望拿到完整的AdversarialEval结构。降级保证结构完整返回,前端不会 crash。报错则前端需额外处理错误态。 - 批量评估容错 — 批量评估 50 个灵感时,第 3 个 LLM 失败不影响其余 47 个。报错中断会导致前面的评估结果全部丢失。
- 透明化 —
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):
// 改造前:
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 的配置)。
论据:
- df-ideas 依赖 df-ai 违背任务初衷 — 本任务的存在原因就是不让 df-ideas 依赖 df-ai。让 df-ideas 内部构造 provider 需要传入 base_url/api_key/model/protocol,等于强制依赖 df-ai 的
build_provider+ HTTP 实现。 - 配置访问权属于应用层 — provider 配置(api_key、base_url)存在 SQLite,通过
AiProviderRepo访问,是AppState的字段。df-ideas 作为领域 crate 不应知道数据库。 - 与 AI Chat 构造路径统一 — AI Chat 也是应用层从 DB 读配置后
build_provider,统一构造路径避免分裂。 - 可测试性 — 测试时传 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):
LlmProvidertrait(含complete/stream/embed/name/supported_features)CompletionRequest/CompletionResponseChatMessage/MessageRoleToolDefinition/ToolFunctionToolCall/ToolCallFunction/ToolCallDeltaTokenUsage/ProviderFeatures/StreamChunkStreamResult类型别名
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 下沉条目 (决策记录主文档)
- 功能决策记录 — 对抗评估启发式 fallback 待接 LLM (灵感模块条目)