用 Rust 构建了一个支持多线程并发 AI 对话的终端应用,顺手把 God Object 拆成了 5 个子系统。这篇聊聊过程中的技术决策和踩坑记录。

这个版本做了什么
IfAI 是一个基于 Tauri 2.0 + Rust 的开源 AI 代码编辑器,提供终端 TUI 模式。v0.4.6 是一个功能与架构双升级的版本,核心亮点:
三大新功能:多线程并发对话、多行输入、线程管理命令
四阶段架构重构:App 27 字段 → 14 字段,声明式路由表替代 238 行 if-else
862 个测试:100% 通过,含 14 轮上下文断链 E2E 测试和并发隔离测试
亮点一:多线程并发对话
这是用户感知最强的功能。在此之前,AI 终端助手都是单线程的------你在问 A 模型一个问题,必须等它回答完,才能切到 B 模型问另一个问题。
v0.4.6 实现了 per-thread 并发对话隔离:
less
┌─ main ────────────────────┐ ┌─ thread-1 ──────────────┐
│ AI: 正在分析代码... │ │ AI: 正在生成测试用例... │
│ > _ │ │ (后台运行中) │
│ [streaming] [queue: 0] │ │ [streaming] [queue: 1] │
└───────────────────────────┘ └──────────────────────────┘
Alt+Left → 切换到 thread-1
每个线程拥有独立的:会话历史、streaming buffer、工具审批队列。切换线程时,当前线程的 streaming 在后台继续运行,不会中断。
并发隔离的技术挑战 在于 Rust 的所有权模型。多个线程共享同一个 App 状态,用 Arc<Mutex> 做三阶段锁:
rust
// 三阶段锁策略
// 1. Session 级:长持有,存储会话上下文
let session = Arc::Mutex::new(HashMap::<ThreadId, Session>::new());
// 2. Request 级:请求期间持有,管理 streaming 状态
let request = session.lock().unwrap()
.get(&tid).unwrap().new_request(prompt).await?;
// 3. Stream 级:流式输出期间持有,管理 buffer
stream_states.insert(tid, StreamState::new());
关键决策是 streaming buffer 的 per-thread 隔离 。早期版本用一个全局 buffer,线程 A 的 AI 输出会混入线程 B 的显示。修复方案是每个线程独立的 HashMap<ThreadId, String>。
亮点二:声明式路由表
这是一个工程层面的改进,但对可维护性影响巨大。
handle_single_key_event 是 TUI 的核心函数,负责处理所有键盘输入。v0.4.5 时它是 238 行的 if-else 链:
rust
// 旧代码(简化)
fn handle_single_key_event(key: KeyEvent) -> StreamingControl {
if key == Ctrl+C {
// 中断 streaming
} else if key == Ctrl+D && !is_diff_mode() && !is_overlay_mode() {
// 进入 diff 模式
} else if key == Alt+Left && !is_busy() {
// 上一个线程
} else if key == Alt+Right && !is_busy() {
// 下一个线程
}
// ... 还有 20+ 个分支
}
问题很明显:每新增一个快捷键,都要在 if-else 链中找位置插入,还要确保 guard clause 不遗漏。重构后变成数据驱动:
rust
const NORMAL_BINDINGS: &[RouteBinding] = &[
RouteBinding { key: Char('d'), mods: CTRL, action: EnterDiff },
RouteBinding { key: Char('o'), mods: CTRL, action: EnterOverlay },
RouteBinding { key: Char('t'), mods: CTRL, action: CreateThread },
RouteBinding { key: Left, mods: ALT, action: PrevThread },
RouteBinding { key: Right, mods: ALT, action: NextThread },
RouteBinding { key: Esc, mods: NONE, action: ReturnToParent },
RouteBinding { key: PageUp, mods: NONE, action: ScrollUp(5) },
RouteBinding { key: PageDown,mods: NONE, action: ScrollDown(5) },
];
fn route_normal_key(key: KeyEvent) -> Option<RouteAction> {
NORMAL_BINDINGS.iter()
.find(|b| b.matches(&key))
.map(|b| b.action)
}
新增快捷键 = 加一行数据,不改控制流。 238 行降到 158 行。
亮点三:Mode enum 替代布尔标志
旧代码用 5 个布尔变量表示 UI 模式:
rust
diff_mode: bool,
overlay.is_some(): bool,
search_mode: bool,
is_approving(): bool,
active_thread_mode: bool,
问题是这些模式本应互斥------不可能同时处于 Diff 和 Search 模式。但布尔变量不提供这个保证,只能靠 11 处手动 guard clause 维护:
rust
// 散落在代码中的 11 处手动互斥检查
if !app.is_overlay_mode() && !app.is_diff_mode()
&& !app.is_searching() && !app.is_approving() {
// 正常模式
}
替换为一个 enum:
rust
enum Mode { Normal, Diff, Overlay, Search, Approving, ThreadPicker }
// 一行搞定,编译器保证互斥
if matches!(app.mode, Mode::Normal) { ... }
有趣的是,这个改动在重构过程中暴露了 2 个隐藏 bug:Diff 模式下 Ctrl+O 被错误放行(应该被 Diff 拦截)、Overlay 模式下 Ctrl+D 被错误放行。布尔标志模式下这两个 bug 被手动的 guard clause 掩盖了,Mode enum 让它们无处遁形。
亮点四:TDD 发现真实 Bug
整个重构采用 TDD 流程:先写测试(RED),再实现(GREEN),最后重构。
32 个新测试覆盖了:
- 模式契约测试(10 个):enter Diff → assert Mode::Diff → exit → assert Mode::Normal
- Guard 行为测试(13 个):模拟 Streaming 状态下按 Ctrl+D,验证被正确拦截
- 路由契约测试(11 个):模拟 Alt+Left 按键,验证线程切换发生
- 状态契约测试(6 个):验证 cleanup 后无残留 busy 状态
其中最硬核的是 14 轮上下文断链 E2E 测试------让 AI 生成一个完整的 2048 游戏,验证超长对话(14 轮工具调用)不会丢失上下文。
Windows 兼容踩坑

重构完成后,Windows 用户报告 Alt+方向键无法切换线程。
根因是 crossterm 在 Windows 和 Unix 上使用不同的键盘事件解析路径。Windows 终端在 Alt+方向键时可能附带额外的修饰符位:
rust
// Windows 上 Alt+Left 可能是:
key.code = Left
key.modifiers = ALT | SHIFT // 额外的 SHIFT 位!
// 精确匹配失败:ALT != ALT | SHIFT
修复方案:仅在 Windows 上使用宽松匹配:
rust
#[cfg(target_os = "windows")]
{
if self.modifiers.contains(KeyModifiers::ALT) {
return key_event.modifiers.contains(KeyModifiers::ALT);
}
}
self.modifiers == key_event.modifiers // macOS/Linux 保持精确匹配
数据总览
| 指标 | v0.4.5 | v0.4.6 |
|---|---|---|
| App 字段数 | 27 | 14 |
| handle_single_key_event | 238 行 | 158 行 |
| guard clause | 11 处手动互斥 | 0(类型系统保证) |
| cleanup 路径 | 5 处分散 | 1 个统一入口 |
| 测试用例 | 830 | 862 |
| 文件变更 | - | 54 个,+14,920 行 |
| 发现的真实 bug | - | 4 个(重构过程暴露) |
快速体验
bash
# 安装
brew install peterfei/ifai/ifai
# 启动
ifai
# Ctrl+T 创建新线程,Alt+Left/Right 切换
# Shift+Enter 多行输入
# /thread list 列出所有线程
GitHub: peterfei/ifai
IfAI 是基于 Tauri 2.0 + React 19 + Rust 构建的开源 AI 代码编辑器,MIT 协议,欢迎 Star 和 PR。