1239 字
约 4 分钟
1
面向长任务的 TUI Agent 交互设计

面向长任务的 TUI Agent 交互设计

  1. 要解决的问题模型请求、文件处理和 Spark Job 都可能阻塞数秒甚至更久。如果在 Textual 事件循环中同步执行,界面会冻结;如果后台线程直接修改 Widget,又会产生线程安全问题。设计目标是让 UI 保持响应,同时让执行逻辑与界面解耦。2. 组件与线程边界后台线程不能直接操作...

字数: 1142 | 语雀原文


1. 要解决的问题

模型请求、文件处理和 Spark Job 都可能阻塞数秒甚至更久。如果在 Textual 事件循环中同步执行,界面会冻结;如果后台线程直接修改 Widget,又会产生线程安全问题。设计目标是让 UI 保持响应,同时让执行逻辑与界面解耦。

2. 组件与线程边界

Diagram 后台线程不能直接操作 Textual Widget。所有 UI 变更都通过 call_from_thread() 切回主线程,这是整个设计最关键的线程边界。

3. 一次输入的完整交互

Diagram 输入先上屏、再启动后端,因此用户能立即确认请求已接收。response_widget 在主线程创建,再作为引用传给 worker;worker 只通过调度回主线程更新它。

4. 统一流接口与三种语义

WorkbenchService.stream_input() 把不同后端统一成 Iterable[str],但三种路径的“流式”含义不同。

Diagram 因此当前 TUI 的视觉效果都是逐步出现,但只有模型网关路径可能是真正的生成中流式;Agent 不会在每个 Skill 开始或 artifact 创建时实时发事件。

5. 状态栏与模型降级

状态栏每次更新三个区域:runtime status、model name、累计 used token。ModelRouter 在主 provider 异常时保存 _last_error,fallback 成功后请求整体不会进入 ERROR,但 _append_model_warning() 会追加一条 warning,说明主模型失败和实际降级来源。

Diagram

6. 当前并发行为

run_worker(..., exclusive=False) 允许多个输入并发执行。这不会自动保证业务隔离:

  • 每个请求有独立 response widget 和局部 buffer,因此文本一般不会互相覆盖。
  • WorkbenchService、模型 gateway 和 AgentRuntime 是共享实例。
  • SparkToolExecutor._run_results 是共享可变字典,依赖不同 run_id 隔离。
  • SQLite 支持基础串行写入,但高并发锁争用没有治理。
  • Spark、模型 API 和 Docker 容器没有统一并发配额。 本项目定位为本地单用户工作台,不能据此声称支持多租户高并发。

7. 渲染与背压难点

累积 buffer 的复制成本

每个 chunk 都执行 buffer += chunk,然后把完整 buffer 传给 Widget 更新。长文本下可能产生重复字符串复制和重复渲染。可以按时间窗口合并 chunk,例如每 30–50ms 刷新一次。

sleep(0.01) 不是背压

它只控制视觉节奏。真正背压需要有界队列:生产者队列满时等待或合并普通 token;状态变更、错误、artifact 等关键事件不能丢弃。

取消需要贯穿所有层

Textual worker 取消不等于 Spark Job 取消。取消信号需要经过 UI → WorkbenchService → AgentRuntime → JobOrchestrator → Docker/Livy/Spark,并处理取消与成功回调竞态。

更合理的事件协议

当前所有响应都是字符串。生产化可定义:

RunEvent = TokenDelta | StepStarted | StepCompleted | ArtifactCreated |
           JobStatusChanged | WarningRaised | RunFailed | RunCompleted

TUI、SSE 和 WebSocket 只负责消费同一事件流。这样才能显示真实步骤进度、结构化错误和可点击 artifact,而不是解析字符串。

8. 异常路径

  • _stream_response() 捕获任意异常,切换 ERROR 并展示异常类型。
  • _format_error() 当前提示检查 master-model 三个配置槽位,适合模型错误,但对文件或 Spark 错误不够精确。
  • 主模型失败、fallback 成功不进入 ERROR,而是 READY + warning。
  • 空流会显示“没有生成响应”,避免留下永久空白气泡。
  • 当前没有超时,阻塞后端可能让 worker 长时间停留在 THINKING。

9. 源码索引

  • sparkos/interfaces/tui/app.py:Textual 事件、worker 和 UI 更新。
  • sparkos/application/workbench.py:统一流接口。
  • sparkos/infrastructure/llm/model_router.py:真实模型流和 fallback。
  • sparkos/application/agent_runtime.py:同步执行后格式化。
  • sparkos/interfaces/terminal/chat.py:同一 Service 的终端适配。
  • tests/test_workbench.py:聊天与文件任务 chunk 测试。

10. 如何向面试官概括

我把 Textual 主线程和阻塞后端严格分开:worker 执行 Service,所有 Widget 更新通过 call_from_thread 回主线程。统一 Iterable 接口隐藏了后端差异,但我也明确区分了模型真流式、文件切块和 Agent 执行后分段输出。生产化重点是结构化 RunEvent、背压、取消和任务隔离。

面向长任务的 TUI Agent 交互设计
http://www.clxhxhhr.top/posts/785/
作者
clxstart
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。