Files
DevFlow/docs/03-模块文档/AI对话引擎-2026-06-14.md
绝尘 ff3f153d45 修复: 安全加固+DRY 收敛+文档同步+测试补齐
安全:
- ScriptNode 默认黑名单兜底(rm/del/format/shutdown/mkfs/dd)
- bind_directory 分段 .. 检测替代 contains 子串(对齐 tool_registry)
- ai_providers 白名单移除 api_key(防 update_field 旁路写明文)

DRY:
- useAiEvents 抽 cleanupTerminatedConversation 统一三分支收尾
- 新增 useStoreAction 工具,4 个 store 替换 38 处 try/catch 样板

文档:
- df-core → df-types 批量替换(ARCHITECTURE/PROGRESS/SQLite-CRUD)
- INDEX 补齐 9 漏列文档(单对话并行多轮/跑题试验/工程系统设计等)
- Agent架构说明 死链修复(../构想审查/)
- AI对话引擎工具清单改为数量+按风险分组(不再用固定数字)
- ARCH 状态标签 设计阶段 → Phase 2 验证

测试:
- df-relay 新增 registry_test: ConnRegistry 路由 + RelayState + 16 项单测
2026-06-29 21:57:07 +08:00

305 lines
15 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.
# AI 对话引擎Agentic Loop
> 创建2026-06-14 | 来源:基于 `src-tauri/src/commands/ai/` 实际代码核对编写
> 关联模块文档:[df-ai-AI集成模块](./df-ai-AI集成模块-2026-06-12.md)crate 层Provider / ContextManager / 工具基础设施)
> 关联架构文档:[Agent架构说明-2026-06-14.md](../02-架构设计/专项设计/Agent架构说明-2026-06-14.md)(能力边界盘点)
---
## 一、概述
DevFlow 的 AI 对话不是简单的"发消息→收回复",而是一个 **ReAct 循环**Reason + ActLLM 流式回复 → 调用工具 → 执行工具 → 结果回传 LLM → 循环,直到 LLM 不再需要工具(返回纯文本)或达到最大轮次。
核心代码位于 `src-tauri/src/commands/ai/`11 个子模块协作:
```
commands/ai/
├── mod.rs — 模块入口 + glob 重导出 + AiSession 定义
├── commands.rs — 17 个 IPC 命令send/approve/reject/clear/switch…
├── agentic.rs — ReAct 循环主体run_agentic_loop / try_continue_agent_loop
├── stream_recv.rs — 流式接收idle timeout / 断连检测 / 停止信号)
├── conversation.rs — 持久化 + Token 累加器 + 截断函数
├── audit.rs — 工具执行审计process_tool_calls / audit_finalize / build_approval_reason
├── tool_registry.rs — 12 个 AI 工具注册 + 路径校验
├── prompt.rs — system prompt 构建 + provider 获取
├── skills.rs — 技能联想SKILL.md frontmatter 解析)
├── title.rs — 对话标题自动生成
└── knowledge_inject.rs — 知识注入 + 知识提炼
```
---
## 二、ReAct 循环详解
### 核心流程
```
用户发消息
┌─ for iteration in 0..MAX_AGENT_ITERATIONS(10) ──────────────┐
│ │
│ 1. 用户停止? → 收尾退出 │
│ │
│ 2. 构建请求消息 │
│ ├─ system prompt技能 + 知识注入) │
│ ├─ 历史消息(超预算时裁剪,保护工具三元组 + 最近 6 条) │
│ └─ 工具定义12 个内置工具) │
│ │
│ 3. LLM 并发限流(全局 3 / 单对话 2 双层 Semaphore
│ │
│ 4. stream_llm流式接收
│ ├─ 逐 chunk 推送 AiTextDelta 到前端 │
│ ├─ 累积 tool_calls按 index 排序) │
│ ├─ idle 120s timeout → 判定断连 │
│ └─ 流尽未收 finished → 丢弃残缺 │
│ │
│ 5. 有 tool_calls? │
│ ├─ 无 → 最终文本break正常结束
│ └─ 有 → process_tool_calls │
│ ├─ Low 风险 → 自动执行 │
│ └─ Medium/High → 进 pending_approvals暂停循环 │
│ │
│ 6. 全自动完成 → 继续下一轮 │
│ 有 pending → returngenerating 保持 true等审批恢复
│ │
└─ 达 10 轮 → 正常结束 ────────────────────────────────────────┘
收尾(后台 spawn不阻塞 Completed 事件)
├─ save_conversation消息 + token 累加落库)
├─ maybe_spawn_extraction知识提炼
└─ ensure_conversation_title标题生成
```
### 退出条件
| 条件 | 处理 |
|------|------|
| LLM 只返回文本(无 tool_calls | 正常结束emit `AiCompleted` |
| 有工具待审批 | 暂停循环,`generating` 保持 `true`,等 `ai_approve``try_continue_agent_loop` 恢复 |
| 达 `MAX_AGENT_ITERATIONS`(10) | 正常结束 |
| 用户请求停止 | 已生成文本入库后退出emit `AiCompleted` |
| 流式错误idle timeout / 断连) | emit `AiError``generating = false`,退出 |
### 审批恢复
当用户审批通过最后一个 pending 工具后,`try_continue_agent_loop` 检测到 `generating && pending_approvals.is_empty()`spawn 新的 `run_agentic_loop` 恢复循环。恢复前 emit `AiAgentRound` 通知前端新建 assistant 消息(审批结果不应追加到发起工具调用的旧消息)。
---
## 三、三重可靠性保险
| 保险层 | 机制 | 防什么 | 代码位置 |
|--------|------|--------|---------|
| **连接层** | `connect_timeout(30s)`,不设总 timeout | 连不上无限 hang总 timeout 会误砍流式长任务(流式可持续数分钟) | `openai_compat.rs` / `anthropic_compat.rs` |
| **流式层** | 每个 chunk 间 idle timeout 120s | 连上后中途静默无限 hang | `stream_recv.rs` |
| **前端层** | 60s streaming watchdog | 后端任何路径漏发收尾事件 → 前端永久 `streaming=true` 卡死 | `stores/ai.ts``useAiEvents` composable |
**审批等待时 watchdog 暂停**(不计超时),避免用户思考时间被误判。
### 断连丢弃残缺
维护 `finished_received` 标志;流尽未收到 finished 信号 → emit `AiError` 并**丢弃残缺响应,不当完整入库**。防脏历史污染对话记录。
### 停止生成保留文本
`AiSession.stop_flag: Arc<AtomicBool>` + 多检查点响应(循环顶 / stream 内 / 工具执行前)。停止后**保留已生成文本**——用户主动停止 ≠ 丢弃成果。
---
## 四、AI 工具清单
### 工具注册
工具定义在 `tool_registry.rs::build_ai_tool_registry`**编译期硬编码**(无运行时动态注册)。工具统计截至 2026-06-29后续增删见该函数 `registry.register` 调用点)。
### 按风险等级分组
**Low自动执行** — 只读查询类,不触发审批。包含:`list_projects`/`get_project_count``list_tasks`/`get_task_count``list_task_links`/`get_task_tree``list_ideas``list_trash``read_file``read_symbol`(AST符号解析)、`list_directory``file_info``grep`(跨文件内容搜索)、`search_files``list_project_services``list_project_modules``get_project_timeline``git_status`/`git_diff`/`git_log`(只读)。
**Medium需审批** — 写入类,需人工批准。包含:`create_project`/`update_project`/`bind_directory``create_task`/`update_task`/`delete_task``advance_task`/`move_task_queue`/`update_content``create_task_link`/`remove_task_link``create_idea``add_project_service``write_file`/`patch_file`/`append_file``git_commit`/`git_branch``http_request`(SSRF防护含DNS rebinding检查)。
**High需审批 + 默认谨慎)** — 高风险,默认自动执行模式会全部拒绝。包含:`delete_project`/`restore_project`/`purge_project``delete_file`/`rename_file``run_command`(默认超时 60s)、`run_workflow`(联动任务推进)、`git_merge`(冲突返回冲突文件列表)。
### 工具三要素同源
每个工具一次 `registry.register` 同时定义:`name + description + schema + RiskLevel + handler 闭包`。handler 即唯一执行路径schema+risk+实现同源,消除双轨。
### 路径安全
- `validate_path`:禁 `..` 路径遍历、禁 `.ssh/.aws/.gnupg/AppData/ProgramData/Windows/System32` 等敏感目录
- `resolve_workspace_path`:双层校验(词法 `starts_with` + `canonicalize` 解析 symlink锚定 `workspace_root`
### 审计
每次工具调用写 `ai_tool_executions`V9 建),`audit_finalize` 落盘 executed/rejected + 结果。
### 已知限制
- **工具集封闭**:编译期硬编码,无 MCP 客户端AI 运行时不能新增/修改工具
- **AI 不能造工具**AI 能 `write_file` 写脚本但不会变成可调用工具(要重编译)
- **agent ↔ workflow 仍未完全打通**`run_workflow` 现已能联动任务推进,但 ScriptNode 能力未暴露给 agent loop
---
## 五、审批门控机制
### 流程
```
AI 要执行 create_projectMedium 风险)
process_tool_calls → 不自动执行,写入 ai_tool_executionsstatus=pending
emit AiApprovalRequired → 前端 ToolCard 显示审批按钮
├─ 用户点「同意」→ ai_approve → 执行工具 → try_continue_agent_loop 恢复
└─ 用户点「拒绝」→ ai_reject → 跳过执行 → 恢复循环
```
### 持久化
pending 审批写入 DB`ai_tool_executions``status='pending'`),重启后 `restore_pending_approvals` 从 DB 恢复。`ai_conversation_switch``retain` 保其他对话的 pending不清空全局 HashMap
### 审批卡片信息
`build_approval_reason``audit.rs`)为 9 种工具拼接 reason 含项目名(`resolve_project_label` 查项目名,查不到 fallback「(项目已不存在, id=xxx)」)。前端 `ToolCard.vue``id`/`project_id` 字段特化回显项目名。
### 与工作流审批的区别
| | AI 工具审批 | 工作流审批 |
|---|---|---|
| 触发 | AI 对话中调 Medium/High 工具 | DAG 执行到 HumanNode |
| 事件 | `AiApprovalRequired` | `HumanApprovalRequest` / `HumanApprovalResponse` |
| 恢复 | `ai_approve``try_continue_agent_loop` | `approve_human_approval` IPC → EventBus broadcast |
| 通道 | 独立链路 | 独立链路EventBus broadcast |
两条审批链路完全独立,不共享事件类型或通道。
---
## 六、上下文窗口管理ContextManager
> 实现在 `crates/df-ai/src/context.rs`,解决长对话 token 暴涨导致 `context_length_exceeded` 死锁。
### 分组滑动窗口
```
消息历史:[旧] U₁ A₁(tool) T(result) A₁' U₂ A₂(tool) T(result) A₂' U₃ A₃ [新]
↑ ↑
可淘汰区 保护区最后6条
淘汰单位 = 工具调用三元组(原子性同进同出):
Assistant(tool_calls) + Tool(result)* + Assistant(紧随文本)
```
- **保护区**`PROTECT_COUNT = 6`(≈ 最近 2 个完整用户轮次),永不裁
- **裁剪只影响发送视图**`build_for_request` 返回裁剪版给 LLM`all_messages_clone` 返回全量给持久化
- **Token 估算零依赖**`chars × 0.35`(保守 ±15%),不引入 tiktoken-rs5MB BPE 数据文件对 Tauri 打包不友好)
- **预算公式**`(max_tokens 128k output_reserve 8192) × safety_ratio 0.85 ≈ 101K tokens`
### 关键方法
| 方法 | 用途 |
|------|------|
| `push(message)` | 追加消息并计 token、更新缓存push 不裁剪,裁剪统一在 build_for_request |
| `build_for_request(sys_tokens)` | 返回裁剪后的消息列表 + 是否发生裁剪 |
| `all_messages_clone()` | 返回全量消息(持久化 / 标题生成) |
| `restore_from_messages(msgs)` | 切换对话时重建缓存 |
| `replace_tool_result_content(tool_call_id, new_content)` | 审批通过/拒绝时回填工具结果(反向 rposition 命中最近一条) |
---
## 七、Token 累加
### 两协议语义差异
| | OpenAI | Anthropic |
|---|---|---|
| usage 时机 | 末 chunk 一次性给全量 | message_startinput+ message_deltaoutput |
| output_tokens | 最终值 | **累计值**(非增量),直接覆盖不累加 |
### 跨 loop 实例累加
审批暂停→恢复 spawn 全新 `run_agentic_loop` 实例,新 loop 局部累加器从 0 起。`save_conversation` 的 upsert 路径 token **读旧值叠加**(非覆盖),保证跨 loop 实例的对话总用量正确。
```
loop 实例 1prompt=500, completion=200 → save(旧值 None + 500/200)
│ 审批暂停
loop 实例 2prompt=800, completion=300 → save(旧值 500/200 + 800/300 = 1300/500)
```
---
## 八、LLM 并发控制
`LlmConcurrency``state.rs`):双层 Semaphore运行时可调。
| Semaphore | 默认 permits | 限流对象 |
|-----------|------------|---------|
| global | 3 | 全部 LLM 调用stream_llm / 标题 / 提炼) |
| per_conv | 2 | 单对话并发 |
- permit 仅覆盖 `stream_llm` 调用本身;工具执行(`process_tool_calls`)是本地操作无 RPM 成本permit 在 stream 后立即释放
- Semaphore 重建用「软收敛」策略(替换内层 Arc旧 permit 不受影响)
---
## 九、知识库集成
### 知识注入(对话开始时)
`build_knowledge_context(state, query, config) -> String`
- `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提炼失败仅 warn 不阻断
- 取最后 6 条 user/assistant 消息 → LLM JSON 输出
- parse 失败整批丢弃;成功逐条写 `candidate`(待人工审核)
`maybe_spawn_extraction(...)`agentic loop 两处正常退出路径max iterations / 无工具调用 break统一调用。
---
## 十、切对话不中断路由
Sprint 8 实现:生成中可切换对话不打断。
- 后端给所有 event 加 `conversation_id` + spawn 前快照 conv_id
- 前端按 id 路由:后台对话事件不污染当前视图
- `switchConversation``_latestSwitchId` 丢弃过期响应
---
## 十一、Agent 能力边界(四个"无"
当前系统的硬边界,做设计时不能假设超出这些能力:
| 能力 | 现状 | 影响 |
|------|------|------|
| **配置/调用外部工具** | ❌ 无 MCP 客户端,工具全编译期硬编码 | 工具集封闭 |
| **自造/迭代工具** | ❌ AI 运行时不能新增/修改工具 | 不能为特定任务临时造工具 |
| **执行类工具** | ❌ 无 `run_command`/`run_script` | AI 能写代码但不能运行验证("能写不能跑" |
| **agent ↔ workflow 打通** | ❌ `run_workflow` 工具是空壳 | AI 不能在对话中触发工作流 |
详见 [Agent架构说明-2026-06-14.md](../02-架构设计/专项设计/Agent架构说明-2026-06-14.md)。
---
## 相关文档
- [df-ai AI 集成模块](./df-ai-AI集成模块-2026-06-12.md) — crate 层Provider / ContextManager / 工具基础设施)
- [Agent架构说明](../02-架构设计/专项设计/Agent架构说明-2026-06-14.md) — 能力边界盘点
- [df-workflow 工作流引擎](./df-workflow-工作流引擎-2026-06-12.md) — DAG 引擎
- [DAG 引擎详解](./DAG引擎详解-2026-06-14.md) — 基于代码的引擎详解
- [经验记录](../02-架构设计/滚动规范/经验记录-2026-06-14.md) — 踩坑/约定/技巧