Files
DevFlow/docs/03-模块文档/df-ai-AI集成模块-2026-06-12.md

265 lines
14 KiB
Markdown
Raw Permalink 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.
# df-ai — AI 集成模块
> 创建: 2026-06-10 | 最后更新: 2026-06-14
---
## 概述
`df-ai` 是 DevFlow 的 AI 核心层,提供 LLM Provider 抽象、双协议实现OpenAI 兼容 + Anthropic、流式 SSE 解析、工具调用、上下文窗口管理和 embedding 支持。
---
## 当前状态
| 能力 | 状态 |
|------|------|
| LlmProvider trait | ✅ |
| OpenAI 兼容 Provider流式/非流式/embed| ✅ |
| Anthropic Provider流式| ✅ Sprint 8 |
| ContextManager分组滑窗| ✅ Sprint 11 |
| embed() 向量生成 | ✅ Sprint 15 |
| AiToolRegistry基础设施| ✅ Sprint 519 工具注册在 commands/ai/tool_registry.rs|
| coordinator | ⬜ 空壳B 路线待填)|
---
## 文件结构
```
crates/df-ai/src/
├── lib.rs — 公共导出 + build_provider 工厂(按协议选 OpenAICompatProvider / AnthropicCompatProvider
├── provider.rs — LlmProvider trait含 embed 默认实现 + endpoint 默认实现)
├── openai_compat.rs — OpenAI 兼容实现chat + embed
├── anthropic_compat.rs — Anthropic Messages API 实现Sprint 8
├── context.rs — ContextManager 分组滑动窗口Sprint 11
├── ai_tools.rs — AiToolRegistry + AiTool + RiskLevel仅基础设施具体工具定义在 src-tauri/commands/ai/tool_registry.rs
└── coordinator.rs — AgentCoordinator 空壳B 路线,有意保留勿删)
```
> 注:历史文档曾列 `router.rs`ModelRouter与 `stream.rs`StreamCollector两个文件实际均不存在已删见全量核对报告 §3
---
## LlmProvider Trait
```rust
pub type StreamResult = Pin<Box<dyn Stream<Item = anyhow::Result<StreamChunk>> + Send>>;
#[async_trait]
pub trait LlmProvider: Send + Sync {
// 非流式完整响应(用于标题生成/知识提炼)
async fn complete(&self, request: CompletionRequest)
-> anyhow::Result<CompletionResponse>;
// 流式 SSE主对话
async fn stream(&self, request: CompletionRequest)
-> anyhow::Result<StreamResult>;
// 向量生成(默认 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;
// 实际请求端点(默认回落 name()provider 覆盖返真实 URL供 401/网络错误诊断)
fn endpoint(&self) -> String {
self.name().to_string()
}
}
```
> 注:历史文档曾列 `supported_features() -> ProviderFeatures`(已删),当前 trait 末项为 `endpoint()`。
---
## 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避免误砍流式长任务|
| complete 单请求超时 | ✅ FR-R42026-06-14 commit 36d68dd`complete()` 同步路径用 `RequestBuilder::timeout(Duration::from_secs(60))` 设 60s 单请求超时;**仅作用于 complete不影响 stream 流式路径**stream 仍靠上层 idle 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`)。
### tool_use_id 兜底AC1/AC22026-06-14 commit 36d68dd
GLM 端对 `tool_use_id``None`/空串的 `tool_result` 块会返 500 卡死会话。两路兜底:
| 路径 | 入站/出站 | 兜底 |
|------|----------|------|
| 出站(请求构造)| tool_result 块 | `tool_call_id` 为 None/空时**跳过该块** + `tracing::warn!`(不发出无效块)|
| 入站SSE 流式)| tool_use 块 | 缺 id 时填占位 id `tool_missing_{idx}`idx 为 content_block index+ `tracing::warn!`同步路径complete缺 id 时跳过 |
> 两者均 warn 留痕,不静默吞掉,便于事后排查 provider 返回异常 tool_use 的场景。
---
## 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` | 原子更新工具结果内容(审批通过/拒绝时回填),返回是否找到并替换。✅ FR-D42026-06-14 commit 4a95f6a由正向 `position` 遍历改为反向 `rposition`(审批替换命中最近一条同 id 工具结果;该调用属审批低频路径,不引入索引)|
---
## AI 工具注册
> 归属:`ai_tools.rs` 仅提供基础设施(`RiskLevel` / `AiTool` / `AiToolRegistry`);工具的具体定义与注册在 `src-tauri/src/commands/ai/tool_registry.rs::build_ai_tool_registry`handler 即唯一执行路径schema+risk+实现同源)。
22 个内置工具,按风险分级(核对 `build_ai_tool_registry`2026-06-15
| 风险 | 数量 | 工具 |
|------|------|------|
| Low自动执行8| 8 | list_projects / list_tasks / list_ideas / list_trash / read_file / list_directory / file_info / search_files |
| Medium需审批9| 9 | update_project / create_project / bind_directory / create_task / update_task / create_idea / write_file / patch_file / append_file |
| High需审批5| 5 | delete_task / delete_project / restore_project / purge_project / run_command |
> 原文档写 "21 个Low 8 + Medium 8 + High 5",遗漏了 **patch_file**Sprint 后增补的 Medium 风险局部文件编辑工具)。实际为 **22 个Low 8 + Medium 9 + High 5**。
工具执行结果写 `ai_tool_executions` 表(审计日志)。
### run_commandSprint 21方案 A「直接暴露 Shell」
让 AI 形成「写(write_file)→跑(run_command)→看 stdout→改」闭环。handler 复用 `df_execute::shell::execute`跨平台Windows `cmd /C` / Unix `sh -c`,已封装 `tokio::time::timeout` + `kill_on_drop` 防僵尸进程)。
| 维度 | 设计 |
|------|------|
| 风险 | **High**——强制人工审批,审批卡显示 `command`+`working_dir`(复用 ToolCard `toolArgsEntries`,零前端改动) |
| 参数 | `command`(必填) / `working_dir`(可选,默认 workspace_root) / `timeout_secs`(可选,默认 60) |
| working_dir 校验 | 走 `validate_path` 黑名单(`..` + `.ssh`/`.aws`/`windows`/`system32` 等),**不走** `resolve_workspace_path` 越界校验——High risk 靠人审兜底,放开目录才能在用户任意项目跑命令 |
| 输出截断 | stdout/stderr 各 10KB尾部保留报错堆栈在末尾超出设 `truncated: true``truncate_output` 辅助函数char 边界安全截) |
| 安全边界 | A 方案=最高风险,唯一防线=人审+黑名单。未做命令黑名单(`rm -rf`/`format`)、网络外传检测、资源限制——属 B(ScriptNode)/C(Docker)/D(MCP) 方案领域 |
---
## 知识库集成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计划-2026-06-12.md)。
---
## 相关文档
- [df-storage 存储层](./df-storage-存储层-2026-06-12.md)
- [df-workflow 工作流引擎](./df-workflow-工作流引擎-2026-06-12.md)
- [df-nodes 节点集合](./df-nodes-节点集合-2026-06-12.md)