安全: - 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 项单测
305 lines
15 KiB
Markdown
305 lines
15 KiB
Markdown
# 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 + Act):LLM 流式回复 → 调用工具 → 执行工具 → 结果回传 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 → return(generating 保持 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_project(Medium 风险)
|
||
│
|
||
▼
|
||
process_tool_calls → 不自动执行,写入 ai_tool_executions(status=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-rs(5MB 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_start(input)+ message_delta(output) |
|
||
| output_tokens | 最终值 | **累计值**(非增量),直接覆盖不累加 |
|
||
|
||
### 跨 loop 实例累加
|
||
|
||
审批暂停→恢复 spawn 全新 `run_agentic_loop` 实例,新 loop 局部累加器从 0 起。`save_conversation` 的 upsert 路径 token **读旧值叠加**(非覆盖),保证跨 loop 实例的对话总用量正确。
|
||
|
||
```
|
||
loop 实例 1:prompt=500, completion=200 → save(旧值 None + 500/200)
|
||
│ 审批暂停
|
||
▼
|
||
loop 实例 2:prompt=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-3(LIKE 或 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) — 踩坑/约定/技巧
|