Files
DevFlow/docs/02-架构设计/专项设计/generating状态机加固-2026-06-15.md
绝尘 998a2f243d 文档: 架构方案文档(意图识别论证+多主题愿景/论证+文档物理分类+边界清晰化)
squash合并:
- 意图识别层论证(8维度+10业界佐证)
- 多主题上下文管理愿景+并存论证+补充论证(多轮agentic)
- 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化)
- 前端架构技术债清单归档
2026-06-19 15:04:04 +08:00

285 lines
13 KiB
Markdown
Raw 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 生成状态机加固generating 生命周期)
> 创建: 2026-06-15 | 来源: /review generating 状态机专项审查 | 关联 bug: 用户报障"创建不了新对话"
---
## 1. 背景
用户报障AI 面板"创建不了新对话了",前端报 `生成中无法新建对话,请先停止或等待完成`,但前端实际无内容生成。
排查定位:后端 `AiSession.generating` 标志卡在 `true`,与实际无生成状态不符。`ai_conversation_create`commands.rs:451硬拦 `generating=true` → 死锁,用户永远新建不了对话,只能重启 app。
`/review` 对 generating 状态机commands.rs 对话/审批/stop + agentic.rs loop/try_continue + stream_recv.rs stream_llm + mod.rs AiSession做专项审查产出本文档。
---
## 2. 现状:三标志组合表达隐式状态机
### 2.1 状态标志
| 标志 | 类型 | 位置 | 作用 |
|------|------|------|------|
| `generating` | `bool` | `Mutex<AiSession>` 内 | 是否有 agentic loop 活跃 |
| `pending_approvals` | `HashMap` | `Mutex<AiSession>` 内 | 待审批工具调用 |
| `stop_flag` | `Arc<AtomicBool>` | 跨锁共享 | ai_chat_stop 置位loop/stream 读取 |
### 2.2 隐式表达的 4 态
| 状态 | 标志组合 | 语义 |
|------|---------|------|
| **Idle** | `generating=false` | 空闲,可接新请求 |
| **Streaming** | `generating=true && pending=空` | agentic loop 流式生成中 |
| **AwaitingApproval** | `generating=true && pending非空` | loop 暂停等用户审批 |
| **Stopping** | `stop_flag=true`(瞬态) | 用户点了停止loop 检查点退出中 |
### 2.3 两个散布问题(根因)
**写侧散布(复位)**`generating=false` 手动散落在 6 个 return 点agentic.rs:53/94/144/190/264/318。任一新增 return 漏写、或 spawn task panic/abort → 所有复位点跳过 → `generating` 永久 true。
**读侧散布(判别)**:状态判别靠标志组合反推:
- `try_continue`agentic.rs:283`should_continue = generating && !pending`
- `ai_chat_stop`commands.rs:250`pending非空` 分流流式/审批态
- `ai_conversation_create`commands.rs:451`generating` 硬拦
判别逻辑散落三处,无单一真相源,新增分支易漏。
---
## 3. 状态机决策:轻量状态机,不引入框架
### 3.1 决策结论
**不引入独立状态机框架**enum 字段替换 bool + 转换守卫 + codegen 库)。采用**轻量状态机**
- **写侧收敛** = RAII guardDrop 兜底复位),对应审查项 ①
- **读侧收敛** = `session.state()` enum 视图方法(只读,不改字段),对应审查项 ⑤
- **stop_flag 保留** `Arc<AtomicBool>`(跨锁信号,状态机管不了)
### 3.2 多角度论证
| 角度 | 独立状态机框架 | 轻量方案(guard+视图) | 判 |
|------|--------------|-------------------|-----|
| **状态复杂度** | 4 态 6 边转换,简单 | 同 | 不足以 justify 框架 |
| **stop_flag 约束** | enum 不能跨锁stop_flag 仍须 AtomicBool 独存 | stop_flag 保留enum 只读视图 | 框架无法替代跨锁信号 |
| **根因对症** | 复位散布=写侧 / 判别散布=读侧 | guard 治写 + 视图治读,**精准对症** | 轻量直达根因 |
| **风格契合** | enum 字段+转换函数+非法检测=过度封装 | 增量加 guard+方法,做减法 | 轻量契合"可读性>抽象性" |
| **迁移成本** | 改 AiSession 字段+所有读写点+测试,大改 | 增量,①⑤已在审查清单 | 轻量零额外工作 |
### 3.3 关键洞察
① RAII guard写收敛+ ⑤ enum 视图(读收敛)= 状态机读写两端都收敛,**功能上等价于状态机,但不引入 enum 字段/转换守卫/框架**。这是务实路线,符合作减法风格。
`stop_flag` 是跨锁信号ai_chat_stop 在 IPC 线程置位loop/stream 在 agentic task 读),必须保持 `Arc<AtomicBool>` 独立——这是状态机 enum 字段(锁内)无法替代的并发要求。
---
## 4. 改造设计
### 4.1 ① RAII guard — 写侧收敛P0 根治)
新增 guard struct`run_agentic_loop` 入口创建,函数退出(含 panic/abort/正常 return时 Drop 兜底复位。
```rust
struct GeneratingGuard {
session: Arc<Mutex<AiSession>>,
app: AppHandle,
conv_id: String,
}
impl Drop for GeneratingGuard {
fn drop(&mut self) {
let app = self.app.clone();
let conv_id = self.conv_id.clone();
let session = self.session.clone();
tauri::async_runtime::spawn(async move {
let mut s = session.lock().await;
if s.generating { // 仅在仍 true 时复位(避免重复 emit
s.generating = false;
drop(s);
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0,
conversation_id: Some(conv_id),
});
}
});
}
}
```
`run_agentic_loop` 入口:`let _guard = GeneratingGuard { ... };`,函数内所有手动 `session.generating = false` 删除。
**注意**Drop 是同步的不能再 `.await`,故 spawn 异步复位。存在极小窗口 generating 仍 true由 ② 软复位兜底。
### 4.2 ⑤ session.state() 视图 — 读侧收敛P1
只读视图方法,不改 AiSession 字段,收敛三标志组合判别:
```rust
enum SessionState { Idle, Streaming, AwaitingApproval }
impl AiSession {
fn state(&self) -> SessionState {
if !self.generating { return SessionState::Idle; }
if self.pending_approvals.is_empty() { SessionState::Streaming }
else { SessionState::AwaitingApproval }
}
}
```
改造判别点:
- `try_continue``should_continue = matches!(session.state(), SessionState::Streaming)`
- `ai_chat_stop`:分流用 `matches!(session.state(), SessionState::AwaitingApproval)`
- `ai_conversation_create`:见 4.3
### 4.3 ② newConversation 软复位 — 死锁解除P0
`ai_conversation_create`commands.rs:451硬拦改软复位。`generating=true` 时强制复位(等同 `ai_chat_stop` 审批态分支 commands.rs:252再新建
```rust
if session.generating {
session.generating = false;
session.pending_approvals.clear();
session.stop_flag.store(true, Ordering::SeqCst);
let conv_id = session.active_conversation_id.clone();
drop(session);
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted { ... conversation_id: conv_id });
session = state.ai_session.lock().await;
}
session.active_conversation_id = Some(id.clone());
// ...
```
**含 ④ 策略统一**:软复位后 newConversation 与 switchConversation:518readonly 放行)形成一致心智——「新建=放弃当前生成 / 切换=只读接管」。
需加 `app: AppHandle` 参数tauri 注入,前端无感)。
### 4.4 ③ loop 对话一致性校验 — 竞态防护P0
② 软复位后旧 loop 退出时 `session.messages.push(旧 assistant)`agentic.rs:171/173污染新对话。loop 每次 push 前校验:
```rust
let mut session = session_arc.lock().await;
if session.active_conversation_id.as_deref() != Some(&conv_id) {
return; // 对话已切走,丢弃本轮,不 pushguard ① 兜底复位)
}
if has_tool_calls { /* push */ }
```
同样校验建议加在 `save_conversation` 调用前agentic.rs:90/186/212/254防旧 loop 写库污染。
### 4.5 ⑥ stop 流式态兜底 — 治 loop 已死P1
`ai_chat_stop`commands.rs:261流式态分支仅置 stop_flag依赖 loop 自复位。若 loop 已 panic/abortstop_flag 无人读generating 永不复位。复用 ② 抽出的复位 helper 兜底。
### 4.6 ⑦ stop_notify 即时打断P1可选
`stop_flag`AtomicBool无 async 通知能力,靠 30s 心跳 tick 间接唤醒 select!。换 `tokio::sync::Notify` 即时打断。`stop_flag` 保留作循环顶快检(冗余双保险)。
---
## 5. 状态转换图
```
ai_chat_send
┌─────────────────┐
│ Streaming │◄────────────────┐
│ generating=true │ │
│ pending=空 │ │
└────────┬────────┘ │
│ │
┌──────────┼──────────┐ │
│ │ │ │
有待审批 无工具调用 stop_flag ai_approve
│ (收敛) (用户停) (处理完)
▼ │ │ │
┌──────────────┐ │ ▼ │
│AwaitingApproval│ │ ┌─────────┐ │
│ generating=true │ │ │ Stopping│ │
│ pending非空 │ │ │stop=true│ │
└──────┬───────┘ │ └────┬────┘ │
│ │ │ │
ai_approve │ loop 检查点 │
(处理完) │ 退出+复位 │
│ │ │ │
└───────────┼──────────┼───────────────┘
▼ ▼
┌────────────────────┐
│ Idle │
│ generating=false │
└────────────────────┘
```
**复位保障**:所有退出路径(实线)由 ① guard 的 Drop 兜底异常路径panic/abort虚线同样由 Drop 覆盖。② 软复位作为 newConversation 入口的二级防线。
---
## 6. 实施计划
### P0用户 bug 根治组合,必须同批)
| # | 项 | 文件 | 依赖 |
|---|---|------|------|
| B-260615-09 | ① RAII guard 收尾 | agentic.rs | 无(上游) |
| B-260615-10 | ② newConversation 软复位(含④策略统一)| commands.rs:451 | 必须配 B-11 |
| B-260615-11 | ③ loop 对话一致性校验 | agentic.rs:158 | B-10 配套 |
**依赖关系**
- ②③ 强耦合(②放开 newConversation 触发③的竞态,单独做②会引入数据污染)→ **捆绑实施**
- ① 是②③的上游根治(①修了仍需②③,因 panic 兜底不可能 100% 覆盖 OS 级 abort**三者同批**
### P1
| # | 项 | 文件 |
|---|---|------|
| B-260615-12 | ⑤ session.state() enum 视图(轻量状态机读侧)| mod.rs:97 |
| B-260615-13 | ⑥ stop 流式态兜底(复用②复位 helper| commands.rs:261 |
| B-260615-14 | ⑦ stop_flag 换 Notify 即时打断 | stream_recv.rs:148 |
### P2
| # | 项 | 文件 |
|---|---|------|
| B-260615-15 | ⑧ heartbeat interval 提到 loop 外 | stream_recv.rs:138 |
| B-260615-16 | ⑨ MAX_AGENT_ITERATIONS 配置化 | agentic.rs:28 |
| B-260615-17 | ⑩ key_len 复用 build_provider_for 结果FR-S1 相关)| agentic.rs:67 |
| B-260615-18 | ⑪ pending_approvals 单例+conv_id 路由加注释 | mod.rs:107 |
---
## 7. 验证
### P0 验证(用户 bug 复现 + 根治)
1. **卡死复现**:构造 panic 场景(如临时在 run_agentic_loop 内 panic→ 验证 generating 卡 true → newConversation 被拦
2. **① guard 兜底**:同上 panic 场景 → guard Drop 复位 generating → newConversation 正常
3. **② 软复位**:手动置 generating=true模拟卡死→ newConversation 强制复位成功新建
4. **③ 一致性**:② 软复位后旧 loop 退出 → 验证不 push 到新对话 messages
### 回归
- 正常收敛(无工具调用)→ break → guard 复位 ✅
- 审批等待 → pending 非空 → guard 不复位(设计)→ ai_approve → try_continue 续 ✅
- stop 流式态 → stop_flag → loop 退出 → guard 复位 ✅
- stop 审批态 → 直接清 pending + 复位 ✅
---
## 8. 取舍 / 未做
| 项 | 决策 | 理由 |
|----|------|------|
| 独立状态机框架 | **不做** | 过度封装,轻量方案(guard+视图)功能等价 |
| enum 字段替换 bool | **不做** | 改字段+所有读写点,大改;视图方法够用 |
| stop_flag 转 enum | **不做** | 跨锁信号必须 AtomicBoolenum 锁内无法替代 |
| panic 100% 兜底 | **不可能** | OS 级 abortOOM killguard 抓不到,②软复位兜底 |
| 命令黑名单/资源限制 | **不做**run_command 安全边界)| 属 B/C/D 方案领域A 方案靠人审 |
---
## 相关文档
- [df-ai AI 集成模块](../../03-模块文档/df-ai-AI集成模块-2026-06-12.md)
- [aichat 审查报告-2026-06-14](../构想审查/aichat审查报告-2026-06-14.md)
- [流式 Markdown 渲染调研-2026-06-15](../构想审查/aichat流式Markdown渲染调研-2026-06-15.md)