# 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 内 / 工具执行前）。停止后**保留已生成文本**——用户主动停止 ≠ 丢弃成果。

---

## 四、12 个 AI 工具

### 工具清单

工具定义在 `tool_registry.rs::build_ai_tool_registry`，编译期硬编码（无运行时动态注册）。

| 风险 | 工具 | 说明 |
|------|------|------|
| **Low**（自动执行） | `list_projects` | 列出项目（排软删，截断 50 条） |
| **Low** | `list_tasks` | 列出任务（可按 project_id 筛选） |
| **Low** | `list_ideas` | 列出灵感 |
| **Low** | `read_file` | 读文件（offset/limit 分页，1MB 上限） |
| **Low** | `list_directory` | 列目录（噪音剪枝 + 1000 条上限） |
| **Medium**（需审批） | `create_project` | 创建项目（可选 path/stack 一步绑定） |
| **Medium** | `update_project` | 改字段（复用 CRUD 白名单校验） |
| **Medium** | `create_task` | 创建任务 |
| **Medium** | `create_idea` | 创建灵感 |
| **Medium** | `write_file` | 写文件（自动建父目录，1MB 上限） |
| **Medium** | `bind_directory` | 项目绑定代码目录 + 探测技术栈 |
| **High**（需审批） | `delete_project` | 删项目（软删进回收站） |
| **High** | `run_workflow` | 运行工作流（当前返回提示，未实装） |

### 工具三要素同源

每个工具一次 `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 + 结果。

### 已知限制

- **截断 50 条无翻页**：三个 list 工具 `truncate(50)` 硬截断，无 offset/limit，AI 不知道有数据被漏掉（待改进）
- **"能写不能跑"**：AI 有 `write_file` 但无 `run_command`，无法形成"写→跑→改"闭环（待改进）
- **工具集封闭**：编译期硬编码，无 MCP 客户端，AI 运行时不能新增/修改工具

---

## 五、审批门控机制

### 流程

```
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) — 踩坑/约定/技巧
