Frontend Event Reducer

Frontend Event Reducer 是前端将 Agent SSE 事件转化为 UI 状态的纯函数。它是流式 AI 应用的状态管理核心,定义了每种事件类型如何更新消息列表和 UI 状态。

#type / concept #status / evergreen #tech / frontend #tech / ai

[!info] related notes

Frontend Event Reducer

一句话定义

Frontend Event Reducer 是前端将 Agent SSE 事件转化为 UI 状态的纯函数。它接收当前状态和一个事件,返回新状态。它是流式 AI 应用的状态管理核心。

它解决什么问题

SSE 事件流产生多种事件(text_delta、tool_call_start、tool_call_result、error、done),每种事件需要以不同方式更新 UI 状态。如果没有统一的 Reducer:

  • 事件处理逻辑散落在各组件中
  • 状态更新不一致(有的地方更新了,有的没更新)
  • 难以测试和调试
  • 新增事件类型时需要修改多处代码

核心原理

Reducer 模式

// 状态定义
interface ChatState {
  messages: Message[];
  status: 'idle' | 'streaming' | 'waiting_approval' | 'error' | 'done';
  currentToolCalls: ToolCall[];
  error: string | null;
}

// Reducer 函数
function chatReducer(state: ChatState, event: AgentEvent): ChatState {
  switch (event.event) {
    case 'text_delta':
      return {
        ...state,
        messages: updateLastAssistantMessage(state.messages, msg => ({
          ...msg,
          content: msg.content + event.data.delta,
          status: 'streaming',
        })),
      };

    case 'tool_call_start':
      return {
        ...state,
        currentToolCalls: [...state.currentToolCalls, {
          id: event.data.tool_call_id,
          name: event.data.tool_name,
          arguments: event.data.arguments,
          status: 'running',
        }],
      };

    case 'tool_call_result':
      return {
        ...state,
        currentToolCalls: state.currentToolCalls.map(tc =>
          tc.id === event.data.tool_call_id
            ? { ...tc, status: 'done', result: event.data.result }
            : tc
        ),
      };

    case 'interrupt':
      return {
        ...state,
        status: 'waiting_approval',
        interrupt: event.data,
      };

    case 'error':
      return {
        ...state,
        status: 'error',
        error: event.data.message,
      };

    case 'done':
      return {
        ...state,
        status: 'done',
        messages: finalizeLastAssistantMessage(state.messages),
      };

    default:
      return state;
  }
}

与 React useReducer 集成

function Chat() {
  const [state, dispatch] = useReducer(chatReducer, {
    messages: [],
    status: 'idle',
    currentToolCalls: [],
    error: null,
  });

  // SSE 事件 → dispatch
  useEffect(() => {
    const es = new EventSource('/api/chat/stream');
    es.onmessage = (e) => {
      const event = JSON.parse(e.data);
      dispatch(event);
    };
    return () => es.close();
  }, []);

  return (
    <div>
      <MessageList messages={state.messages} />
      {state.status === 'streaming' && <StreamingIndicator />}
      {state.status === 'waiting_approval' && <ApprovalCard interrupt={state.interrupt} />}
      {state.status === 'error' && <ErrorMessage error={state.error} />}
    </div>
  );
}

典型工程实现

辅助函数

function updateLastAssistantMessage(
  messages: Message[],
  updater: (msg: Message) => Message
): Message[] {
  const lastIndex = messages.length - 1;
  const lastMsg = messages[lastIndex];
  if (lastMsg?.role !== 'assistant') {
    // 创建新的 assistant 消息
    return [...messages, {
      id: generateId(),
      role: 'assistant',
      content: '',
      status: 'streaming',
      ...updater({} as Message),
    }];
  }
  return [
    ...messages.slice(0, lastIndex),
    updater(lastMsg),
  ];
}

常见设计模式

1. 事件类型映射

把事件类型映射到处理函数,避免长 switch 语句。

2. 中间件模式

在 Reducer 前后添加日志、持久化等中间件。

3. 乐观更新

用户发送消息后立即添加到列表,不等后端确认。

常见坑

  1. Reducer 有副作用: Reducer 应该是纯函数,不要在里面发请求
  2. 不处理未知事件: 新增事件类型时 Reducer 应该忽略而不是报错
  3. 状态嵌套太深: 超过 3 层嵌套的状态应该拆分
  4. 不做事件去重: 同一个事件可能到达多次

参考资料

创建于 2026/6/30 更新于 2026/7/15