新增: Phase2 阶段收尾(Sprint 1-20)

重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录
功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore
修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸
文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新

详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
2026-06-14 14:08:20 +08:00
parent 98393b4908
commit cf017f81e2
167 changed files with 19549 additions and 6886 deletions

View File

@@ -1,298 +1,234 @@
# df-ai - AI 集成模块
# df-ai AI 集成模块
> Provider 抽象层与流式响应处理
> 创建: 2026-06-10 | 最后更新: 2026-06-13
## 📋 模块概览
---
`df-ai` 负责 AI 功能的核心集成,支持多个 AI Provider提供统一的接口和流式响应处理。
## 概述
### 主要特性
- 多 Provider 支持OpenAI、Anthropic、DeepSeek
- 流式响应处理
- 工具调用支持
- 错误处理和重试机制
`df-ai` 是 DevFlow 的 AI 核心层,提供 LLM Provider 抽象、双协议实现OpenAI 兼容 + Anthropic、流式 SSE 解析、工具调用、上下文窗口管理和 embedding 支持。
## 🏗️ 架构设计
---
### 核心组件
```rust
// Provider trait 定义
pub trait AIProvider: Send + Sync {
async fn chat_completion(&self, request: ChatRequest) -> Result<ChatResponse>;
async fn create_embedding(&self, text: &str) -> Result<Vec<f32>>;
}
## 当前状态
// 流式响应处理
pub struct StreamProcessor {
event_sender: mpsc::UnboundedSender<StreamEvent>,
}
| 能力 | 状态 |
|------|------|
| LlmProvider trait | ✅ |
| OpenAI 兼容 Provider流式/非流式/embed| ✅ |
| Anthropic Provider流式| ✅ Sprint 8 |
| ContextManager分组滑窗| ✅ Sprint 11 |
| embed() 向量生成 | ✅ Sprint 15 |
| AiToolRegistry基础设施| ✅ Sprint 512 工具注册在 commands/ai.rs|
| coordinator | ⬜ 空壳B 路线待填)|
// 统一的 AI 服务
pub struct AIService {
providers: HashMap<String, Box<dyn AIProvider>>,
default_provider: String,
}
---
## 文件结构
```
crates/df-ai/src/
├── lib.rs — 公共导出
├── provider.rs — LlmProvider trait含 embed 默认实现)
├── openai_compat.rs — OpenAI 兼容实现chat + embed
├── anthropic_compat.rs — Anthropic Messages API 实现Sprint 8
├── context.rs — ContextManager 分组滑动窗口Sprint 11
├── ai_tools.rs — AiToolRegistry + 12 工具定义
├── coordinator.rs — AgentCoordinator 空壳B 路线)
├── router.rs — ModelRouter 模型路由空壳route() 按 TaskType 选模型,当前全返回 default_model
└── stream.rs — StreamCollector 流式辅助(累积 chunk.delta 文本 + 跟踪 finished 标志)
```
### Provider 实现
- **OpenAIProvider**: OpenAI GPT 系列模型
- **AnthropicProvider**: Claude 系列模型
- **DeepSeekProvider**: DeepSeek 模型
## 🔧 使用方法
---
## LlmProvider Trait
### 基础聊天
```rust
use df_ai::AIService;
pub type StreamResult = Pin<Box<dyn Stream<Item = anyhow::Result<StreamChunk>> + Send>>;
let ai_service = AIService::new(config);
let request = ChatRequest {
model: "gpt-4".to_string(),
messages: vec![Message {
role: "user".to_string(),
content: "Hello, world!".to_string(),
}],
};
#[async_trait]
pub trait LlmProvider: Send + Sync {
// 非流式完整响应(用于标题生成/知识提炼)
async fn complete(&self, request: CompletionRequest)
-> anyhow::Result<CompletionResponse>;
let response = ai_service.chat_completion(request).await?;
```
// 流式 SSE主对话
async fn stream(&self, request: CompletionRequest)
-> anyhow::Result<StreamResult>;
### 流式响应
```rust
use df_ai::stream_chat;
let (mut receiver, mut stream) = stream_chat(&ai_service, request).await?;
while let Some(event) = receiver.recv().await {
match event {
StreamEvent::Content(chunk) => {
print!("{}", chunk);
}
StreamEvent::Done => {
println!("\n完成");
}
StreamEvent::Error(e) => {
eprintln!("错误: {}", e);
}
// 向量生成(默认 bailopenai_compat 覆盖实现)
async fn embed(&self, _model: &str, _texts: Vec<String>)
-> anyhow::Result<Vec<Vec<f32>>> {
anyhow::bail!("该 Provider 不支持 embedding({})", self.name())
}
// Provider 名称(必填)
fn name(&self) -> &str;
// 支持的特性必填streaming / function_calling / vision
fn supported_features(&self) -> ProviderFeatures;
}
```
### 工具调用
```rust
let request = ChatRequest {
model: "gpt-4".to_string(),
messages: vec![Message {
role: "user".to_string(),
content: "创建一个文件".to_string(),
}],
tools: vec![Tool {
r#type: "function".to_string(),
function: FunctionDef {
name: "create_file".to_string(),
description: "创建文件".to_string(),
parameters: Parameters {
r#type: "object".to_string(),
properties: serde_json::json!({
"path": {"type": "string"},
"content": {"type": "string"}
}),
required: vec!["path".to_string()],
},
},
}],
tool_choice: "auto".to_string(),
};
```
## ⚙️ 配置
### Provider 配置
```yaml
# config/ai.yaml
providers:
openai:
api_key: ${OPENAI_API_KEY}
base_url: "https://api.openai.com/v1"
model: "gpt-4"
max_tokens: 4000
temperature: 0.7
anthropic:
api_key: ${ANTHROPIC_API_KEY}
model: "claude-3-sonnet-20240229"
max_tokens: 4000
temperature: 0.7
deepseek:
api_key: ${DEEPSEEK_API_KEY}
model: "deepseek-chat"
max_tokens: 4000
temperature: 0.7
default_provider: "openai"
```
### 环境变量
```bash
export OPENAI_API_KEY="sk-your-key"
export ANTHROPIC_API_KEY="sk-ant-key"
export DEEPSEEK_API_KEY="your-key"
```
## 🔄 错误处理
### 错误类型
```rust
pub enum AIError {
APIError(String), // API 调用失败
Timeout, // 请求超时
RateLimit, // 达到速率限制
InvalidResponse, // 响应格式错误
ProviderNotFound, // Provider 不存在
ConfigurationError, // 配置错误
}
```
### 重试机制
```rust
let config = RetryConfig {
max_attempts: 3,
backoff: ExponentialBackoff::from_millis(1000),
retryable_errors: vec![
AIError::Timeout,
AIError::RateLimit,
],
};
let response = ai_service.chat_with_retry(request, &config).await?;
```
## 📊 性能优化
### 缓存机制
```rust
pub struct CachedAIService {
inner: AIService,
cache: Arc<Mutex<HashMap<String, ChatResponse>>>,
}
// 缓存键生成
fn cache_key(request: &ChatRequest) -> String {
format!("{:?}-{:?}", request.model, request.messages)
}
```
### 连接池
```rust
pub struct ConnectionPool {
connections: HashMap<String, Vec<Client>>,
max_connections: usize,
}
pub async fn get_client(&self, provider: &str) -> Result<Client> {
// 从连接池获取或创建新连接
}
```
## 🔍 监控与日志
### 请求追踪
```rust
pub struct RequestTracer {
request_id: String,
start_time: Instant,
metrics: RequestMetrics,
}
impl RequestTracer {
pub fn log_request(&self, provider: &str, duration: Duration) {
metrics.record_request(provider, duration);
}
}
```
### 指标收集
```rust
pub struct RequestMetrics {
total_requests: AtomicU64,
successful_requests: AtomicU64,
failed_requests: AtomicU64,
average_duration: AtomicDuration,
}
```
## 🧪 测试
### 单元测试
```rust
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_chat_completion() {
let ai_service = AIService::new(test_config());
let request = test_request();
let response = ai_service.chat_completion(request).await;
assert!(response.is_ok());
}
}
```
### 集成测试
```rust
#[tokio::test]
async fn test_multiple_providers() {
let providers = vec!["openai", "anthropic"];
for provider in providers {
let ai_service = AIService::new(config_for_provider(provider));
let response = test_chat(&ai_service).await;
assert!(response.is_ok(), "Provider {} failed", provider);
}
}
```
## 🚨 最佳实践
### 1. 错误处理
```rust
// ✅ 正确
match ai_service.chat_completion(request).await {
Ok(response) => handle_response(response),
Err(AIError::RateLimit) => wait_and_retry(),
Err(e) => log_error_and_notify(e),
}
// ❌ 错误 - 忽略错误
let _ = ai_service.chat_completion(request).await;
```
### 2. 资源管理
```rust
// ✅ 正确 - 使用连接池
let client = connection_pool.get_client("openai").await?;
// ❌ 错误 - 每次创建新连接
let client = Client::new(config);
```
### 3. 并发控制
```rust
// ✅ 正确 - 使用信号量
let semaphore = Arc::new(Semaphore::new(10));
let permit = semaphore.acquire().await?;
let response = ai_service.chat_completion(request).await;
// ❌ 错误 - 无限制并发
let handles: Vec<_> = requests.into_iter().map(|req| {
tokio::spawn(ai_service.chat_completion(req))
}).collect();
```
---
**相关文档**:
- [df-storage - 存储层](./df-storage-存储层.md)
- [df-workflow - 工作流引擎](./df-workflow-工作流引擎.md)
- [df-nodes - 节点集合](./df-nodes-节点集合.md)
## OpenAI 兼容 Provideropenai_compat.rs
支持 OpenAI / DeepSeek / GLM / 本地 Ollama 等任意 OpenAI 兼容端点。
| 特性 | 实现 |
|------|------|
| base_url 智能拼接 | `chat_url()` 三分支:已含 `/chat/completions` 直用;以 `/v<数字>` 结尾(如 `/api/paas/v4`,由 `ends_with_version` 判定)补 `/chat/completions`;仅域名(如 `api.openai.com`)补 `/v1/chat/completions` |
| 流式 | `stream: true` + SSE 逐 chunk 解析(`apply_openai_sse` 纯函数)|
| 工具调用 | tool_calls 按 index 排序(消 HashMap 迭代乱序)|
| usage 解析 | `stream_options: {include_usage: true}`,末 chunk 读累计值 |
| finish 判定 | 逐 chunk 判 `finish_reason``stop`/`tool_calls`/`length` 三者同等视为正常 finished`length` = max_tokens 截断,属正常终止而非断连)|
| embed | POST `/v1/embeddings`,响应按 index 排序返回 `Vec<Vec<f32>>` |
| connect timeout | `connect_timeout(30s)`,不设总 timeout避免误砍流式长任务|
> idle timeout120s与断连丢弃finished_received属上层 `stream_llm`src-tauri/commands/ai.rs的职责不在本 Provider 层。
SSE 解析抽成 `pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>) -> StreamChunk` 纯函数,与 HTTP/eventsource 解耦,便于单测(喂构造 data 字符串验证 `[DONE]`/usage 覆盖/tool_calls 等分支)。
---
## Anthropic Provideranthropic_compat.rsSprint 8
| 特性 | 实现 |
|------|------|
| 认证 | `x-api-key` + `anthropic-version: 2023-06-01` |
| 消息格式 | 顶层 `system` 字段,`max_tokens` 必填(请求缺省时兜底 `DEFAULT_MAX_TOKENS = 4096`|
| 流式 SSE | event 类型解析(全集见下)|
| 工具调用 | `content_block` type=tool_use |
| usage | `message_start` 初始化累加器(input_tokens)、`message_delta` 覆盖 completion(output_tokens 为累计值,非增量)、`message_stop``take()` 带出累积 usage |
| embed | 不支持Anthropic 无 embed API继承 trait 默认 Err |
SSE 事件全集(按 `type` 字段分发):
| event type | 处理 |
|------------|------|
| `message_start` | 用 `message.usage.input_tokens` 初始化累加器output 置 0|
| `message_delta` | `usage.output_tokens` 是累计值(非增量),直接覆盖 completion 并重算 total |
| `content_block_delta`(text_delta) | 文本增量 |
| `content_block_delta`(input_json_delta) | 工具入参增量(带 index|
| `content_block_start`(tool_use) | 工具块开始,带 id + name |
| `message_stop` | 返回 `finished=true` 终态 chunkusage 经 `take()` 带出 |
| `error` | 返回 `finished=true` 终态空 chunk不清空累加器|
| 其它content_block_stop / ping| 空 chunk |
SSE 解析抽成 `pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUsage>) -> StreamChunk` 纯函数,与 HTTP/eventsource 解耦,便于单测(喂构造 data 字符串验证事件分支与 usage 累积)。
支持 GLM 订阅端点(`https://open.bigmodel.cn/api/anthropic`)。
---
## ContextManager 分组滑动窗口context.rsSprint 11
解决长对话无限增长导致 `context_length_exceeded` 死锁。
### 核心设计
- **TokenEstimator**:字符粗估,零依赖,保守 ±15%。单条公式 = `content(chars×0.35 ceil)` + `per_message 4` + 每个 `tool_call(per_tool_call 30 + name/arguments 各按 chars×0.35 ceil)` + 有 `tool_call_id``+3`
- **ContextConfig**max_tokens 128k / output_reserve 8192 / safety 0.85;预算公式 `(max_tokens output_reserve) × safety_ratio`,由 `ContextConfig::budget_limit()` 提供(`ContextManager::budget_limit()` 仅转发它)
- **分组淘汰单元**工具调用三元组ToolCallHead + ToolResultTail* + 紧随文本 Assistant原子性同进同出防上下文语义断裂
- **保护区**`PROTECT_COUNT = 6`(编译期常量,≈ 最近 2 个完整用户轮次),最后 6 条消息永不裁
- **裁剪范围**:仅影响发送视图(`build_for_request`),全量历史(`all_messages_clone`)用于持久化
### 关键方法
| 方法 | 用途 |
|------|------|
| `push(message: ChatMessage)` | 追加消息并计 token、更新 `history_tokens` 缓存push 不裁剪,裁剪统一在 build_for_request|
| `clear()` | 清空消息历史并重置 token 计数 |
| `len() -> usize` | 历史消息条数 |
| `is_empty() -> bool` | 历史是否为空 |
| `history_tokens() -> u32` | 当前历史累计 token不含 system prompt|
| `budget_limit() -> u32` | 上下文预算上限,转发 `config.budget_limit()` = (max_tokens output_reserve) × safety_ratio |
| `iter() -> impl Iterator<Item = &ChatMessage>` | 只读迭代历史消息(如标题生成 filter|
| `build_for_request(sys_tokens) -> (Vec<ChatMessage>, bool)` | 返回裁剪后的消息列表 + 是否发生裁剪(用于 LLM 调用)|
| `all_messages_clone()` | 返回全量消息(用于 save_conversation / 标题生成)|
| `restore_from_messages(msgs)` | 切换对话时重建 ContextManager 缓存 |
| `replace_tool_result_content(tool_call_id, new_content) -> bool` | 原子更新工具结果内容(审批通过/拒绝时回填),返回是否找到并替换 |
---
## AI 工具注册
> 归属:`ai_tools.rs` 仅提供基础设施(`RiskLevel` / `AiTool` / `AiToolRegistry`12 个工具的具体定义与注册在 `src-tauri/src/commands/ai.rs::build_ai_tool_registry`handler 即唯一执行路径schema+risk+实现同源)。
12 个内置工具,按风险分级:
| 风险 | 工具 |
|------|------|
| Low自动执行| list_projects / list_tasks / list_ideas / read_file / list_directory |
| Medium需审批| update_project / create_project / create_task / create_idea / write_file |
| High需审批| delete_project / run_workflow |
工具执行结果写 `ai_tool_executions` 表(审计日志)。
---
## 知识库集成Sprint 15逻辑在 src-tauri/commands/ai.rs
### 知识注入
`build_knowledge_context(state, query, config) -> String`
- `config.auto_inject == false` → 立即返回空(零开销)
- `hybrid_search()` top-3LIKE 或 LIKE+向量混合)
- 每条命中调 `increment_reuse_count`fire-and-forget
- 格式化为 markdown注入 system prompt 头部
### 知识提炼
`extract_knowledge_from_conversation(db, conv_id, provider_cfg)`
- 后台 spawn`tauri::async_runtime::spawn`),提炼失败仅 warn 不阻断
- 取最后 6 条 user/assistant 消息 → LLM JSON 输出(强制 JSON schema
- parse 失败整批丢弃;成功则逐条写 candidate
`maybe_spawn_extraction(...)`agentic loop 两处正常退出路径统一调用。
### 向量 embeddingPhase 5.5
`generate_embedding(state, text, config) -> Option<Vec<f32>>`
- 截断 8000 字 → 找 `embedding_provider_id` 对应 provider → `embed()`
- 失败返回 None降级 LIKE
`hybrid_search(state, query, limit, config)`:三层降级链:
1. `vector_enabled == false` → 纯 LIKE
2. embed 调用失败 → 纯 LIKE
3. 正常 → 双信号合并排序(同时 LIKE+向量 cos≥0.3 > 仅 LIKE > 仅向量 cos≥0.3
---
## LLM 并发控制state.rsSprint 11 Part C
`LlmConcurrency`:双层 Semaphore运行时可调。
| Semaphore | 默认 permits | 限流对象 |
|-----------|------------|---------|
| global | 3 | 全部 LLM 调用stream_llm / 标题 / 提炼)|
| per_conv | 2 | 单对话并发 |
本地工具执行(`tools.execute`)不限流,无外部成本。
---
## 决策能力缺口B 路线,待立项)
| 能力 | 现状 | 需补 |
|------|------|------|
| Planning任务规划| 缺失 | 意图 → LLM 规划子任务 DAG |
| Coordinator多 agent| `coordinator.rs::run()` 全 TODO | 实现多 agent 协作拆 DAG |
| Conditions条件分支| 缺失df-ai 无实现,条件求值属 df-workflow crate| JSON Path / 比较 / and-or-not |
| Reflection自纠| 缺失 | 执行后自检 / 重试 |
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划.md)。
---
## 相关文档
- [df-storage 存储层](./df-storage-存储层.md)
- [df-workflow 工作流引擎](./df-workflow-工作流引擎.md)
- [df-nodes 节点集合](./df-nodes-节点集合.md)