Files
DevFlow/docs/02-架构设计/F-07-df-ai-core-trait下沉设计-2026-06-14.md
绝尘 04032a2a8d 重构: 文档汇总+进度看板+孤儿任务清理脚本+gitignore 噪音排除
- docs/02 架构设计: 新增 aichat审查/异步审批构想/流式渲染调研/generating状态机/密钥迁移健壮性/工作流脚本执行边界/条件表达式引擎/F-07 trait下沉/Agent架构说明/任务推进构想/功能创意池;更新功能决策记录+归档/对抗论证/文档记录规范/经验记录
- docs/03 模块文档: 新增 AI对话引擎/DAG引擎详解;更新 df-knowledge/df-nodes/df-storage/df-workflow/df-ai
- docs/05 代码审查: 新增 全栈审查/全局review/架构审查/近期改动审查/工作区多角度走查/自研memo流式渲染审查
- docs/09 问题排查: 新增 aichat-apikey-401
- docs/INDEX+README 索引同步;docs/todo 待办看板(2026-06-15 汇总)
- PROGRESS.md Sprint 22-25;URGENT.md 加急清单快照(5 项 P0 已全修)
- scripts/cleanup_orphan_tasks.{py,sh} 孤儿任务清理工具
- .gitignore 补 *.broken.bak + tmp/ 噪音排除
2026-06-15 05:14:21 +08:00

15 KiB
Raw Blame History

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-ideasadversarial.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-coreContextManagerTokenEstimatorAiToolRegistry 等留在 df-ai

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

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>>

// 改造后
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.rsAdversarialEngine 跟随同一模式。
  2. Option 天然表达降级None 时走启发式,Some 时走 LLM + 失败降级。类型系统层面清晰表达"有无 LLM"两种模式,与决策 3 的降级策略无缝配合。
  3. 批量评估友好evaluate_idea IPC 批量评估 N 个灵感时,构造 1 次 engineevaluate N 次provider 只注入一次。参数注入方式每次调用都要传。
  4. 未来扩展空间 — 后续如需给对抗评估加配置(温度、模型偏好、最大 token构造注入只需加字段参数注入则签名越来越长。

改造影响面

  • adversarial.rsstruct 加字段,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 字段标记评估来源。

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

论据

  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>,注入到 AdversarialEnginedf-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 的配置)。

论据

  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 traitcomplete / 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

相关文档