# 13. 与 AI Agent 的交互体验
# 13.1 原理
Agent UX 的目标是让执行过程可见、可控、可信。用户不应只看到“转圈”,也不应被每个底层事件淹没。
建议采用四层信息:
- 任务层:当前目标、计划、完成度、子任务状态。
- 动作层:正在读取/搜索/运行/修改什么。
- 证据层:diff、命令输出、引用、测试结果。
- 控制层:取消、暂停、批准、修改参数、重试、接管。
交互状态必须与 Runtime 状态一一对应,不能只靠前端猜测。流式事件至少应有稳定 event id、turn id、call id、sequence、timestamp、status;断线重连按 cursor 补发。
# 13.2 源码对照
Claude Code 的流式工具执行器将 progress 独立存储并立即 yield,最终结果只发一次;Session 状态明确区分 idle/running/requires_action,审批时可以让远端 UI 精确显示阻塞原因:StreamingToolExecutor.ts:408、sessionState.ts:1。
Codex 在 Turn 开始事件中携带 turn id、trace id、上下文窗口和协作模式;运行中的用户输入进入队列,在适当 Step 被 drain,而不是破坏当前采样:regular.rs:48、turn.rs:264。
# 13.3 前端状态伪代码
reduce(uiState, event): // 用单向 Reducer 将协议事件投影为前端状态
assert event.sequence > uiState.lastSequence // 拒绝旧序号事件覆盖更新后的 UI 状态
switch event.type: // 按稳定事件类型执行确定性状态迁移
TURN_STARTED: createTurn(event.turnId) // 创建新的 Turn 视图并进入运行态
PLAN_UPDATED: replacePlan(event.version, event.steps) // 按版本原子替换计划,避免局部乱序更新
TOOL_STARTED: calls[event.callId] = RUNNING // 将指定 ToolCall 标记为执行中
TOOL_PROGRESS: appendBoundedProgress(event.callId, event.delta) // 追加有大小上限的临时进度信息
APPROVAL_REQUIRED: calls[event.callId] = WAITING_USER // 显示工具正在等待用户审批而非系统卡死
TOOL_ENDED: calls[event.callId] = event.status // 使用成功、失败或取消终态结束工具视图
PATCH_READY: showDiff(event.patchId) // 按执行产物 ID 加载并展示待审查的代码差异
TURN_ENDED: finalizeTurn(event) // 固化 Turn 结果并关闭仍存在的临时 UI 状态
# 13.4 面试题
问:如何避免流式 UI 因乱序事件显示错误?
使用每线程/每 Turn 单调 sequence;Reducer 幂等;terminal 状态不可被旧 progress 回退;断线后从 cursor 拉取;大输出只存摘要,详细内容按需加载。
# 13.5 原理深化:Agent UX 是 Runtime 状态机的只读投影
可靠 UI 不自己推断 Agent 状态,而是消费 Runtime 发出的领域事件。ToolCall started/progress/completed、Approval required/resolved、Turn interrupted/completed 都应来自同一个状态迁移点;前端只做幂等 Reducer。若 UI 通过“最后一条文本包含 finished”猜结束,就无法处理 Stop Hook 拒绝、后台工具仍运行或断线重放。
Claude Code 把工具 progress 与最终结果分开,且用 yielded 保证终态只发一次;Codex Session 则由 submission loop 管理输入,运行期间到达的用户消息在安全边界被 drain,而不是直接修改正在采样的 Prompt。两种实现共同体现了 UI 控制与执行一致性的原则:取消、追加指令和审批都是提交给 Runtime 的命令,Runtime 接受后再发确认事件;按钮点击本身不等于状态已经改变。
断线恢复需要事件游标和快照协同:客户端先获取某个事件序号对应的状态快照,再重放之后的事件;一旦进入终态就不能回退,旧进度事件不能把 completed 改回 running。大日志与代码差异使用执行产物 ID 延迟加载,事件流只传摘要和引用。这样网络重连、多个客户端观察和远程 Runtime 都能共享同一事实,而不会各自维护一套无法核对的“前端状态”。