Frontend Event Reducer
Frontend Event Reducer 是前端将 Agent SSE 事件转化为 UI 状态的纯函数。它是流式 AI 应用的状态管理核心,定义了每种事件类型如何更新消息列表和 UI 状态。
#type / concept
#status / evergreen
#tech / frontend
#tech / ai
[!info] related notes
- 所属 MOC: AI Agent Application MOC
- 上游: SSE Client, 事件契约
- 下游: Chat UI, Message State Management
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. 乐观更新
用户发送消息后立即添加到列表,不等后端确认。
常见坑
- Reducer 有副作用: Reducer 应该是纯函数,不要在里面发请求
- 不处理未知事件: 新增事件类型时 Reducer 应该忽略而不是报错
- 状态嵌套太深: 超过 3 层嵌套的状态应该拆分
- 不做事件去重: 同一个事件可能到达多次