用 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 测试和并发隔离测试

打开网易新闻 查看精彩图片

IfAI v0.4.6 发布:多线程并发对话 + Rust TUI 架构重构实战

亮点一:多线程并发对话

这是用户感知最强的功能。在此之前,AI 终端助手都是单线程的——你在问 A 模型一个问题,必须等它回答完,才能切到 B 模型问另一个问题。

v0.4.6 实现了 per-thread 并发对话隔离:

┌─ main ────────────────────┐   ┌─ thread-1 ──────────────┐│ AI: 正在分析代码...        │   │ AI: 正在生成测试用例...   ││ > _                      │   │ (后台运行中)              ││ [streaming] [queue: 0]   │   │ [streaming] [queue: 1]   │└───────────────────────────┘   └──────────────────────────┘Alt+Left → 切换到 thread-1

每个线程拥有独立的:会话历史、streaming buffer、工具审批队列。切换线程时,当前线程的 streaming 在后台继续运行,不会中断。

并发隔离的技术挑战在于 Rust 的所有权模型。多个线程共享同一个 App 状态,用 Arc 做三阶段锁:

// 三阶段锁策略// 1. Session 级:长持有,存储会话上下文let session = Arc::Mutex::new(HashMap::::new());// 2. Request 级:请求期间持有,管理 streaming 状态let request = session.lock().unwrap().get(&tid).unwrap().new_request(prompt).await?;// 3. Stream 级:流式输出期间持有,管理 bufferstream_states.insert(tid, StreamState::new());

关键决策是streaming buffer 的 per-thread 隔离。早期版本用一个全局 buffer,线程 A 的 AI 输出会混入线程 B 的显示。修复方案是每个线程独立的 HashMap。

亮点二:声明式路由表

这是一个工程层面的改进,但对可维护性影响巨大。

handle_single_key_event 是 TUI 的核心函数,负责处理所有键盘输入。v0.4.5 时它是 238 行的 if-else 链:

// 旧代码(简化)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 不遗漏。重构后变成数据驱动:

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 {NORMAL_BINDINGS.iter().find(|b| b.matches(&key)).map(|b| b.action)}

新增快捷键 = 加一行数据,不改控制流。238 行降到 158 行。

亮点三:Mode enum 替代布尔标志

旧代码用 5 个布尔变量表示 UI 模式:

diff_mode: bool,overlay.is_some(): bool,search_mode: bool,is_approving(): bool,active_thread_mode: bool,

问题是这些模式本应互斥——不可能同时处于 Diff 和 Search 模式。但布尔变量不提供这个保证,只能靠 11 处手动 guard clause 维护:

// 散落在代码中的 11 处手动互斥检查if !app.is_overlay_mode() && !app.is_diff_mode()&& !app.is_searching() && !app.is_approving() {// 正常模式}

替换为一个 enum:

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下 开10个窗体仅占用70m内存

重构完成后,Windows 用户报告 Alt+方向键无法切换线程。

根因是 crossterm 在 Windows 和 Unix 上使用不同的键盘事件解析路径。Windows 终端在 Alt+方向键时可能附带额外的修饰符位:

// Windows 上 Alt+Left 可能是:key.code = Leftkey.modifiers = ALT | SHIFT  // 额外的 SHIFT 位!// 精确匹配失败:ALT != ALT | SHIFT

修复方案:仅在 Windows 上使用宽松匹配:

#[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 个(重构过程暴露)

快速体验

# 安装brew install  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。