用Zustand管理AI多会话状态

先把结论摆这儿:做AI聊天类应用,要同时管「当前会话 / 历史会话列表 / 没发出去的草稿」这三摊状态,Zustand 比 Redux 省事太多。一个 store 文件 60 行搞定,不用 action / reducer / dispatch 那套八股,组件里 const messages = useChatStore(s => s.current.messages) 直接取,改的时候 set 一下完事。下面是我自己踩出来的写法。

我当时卡在哪

上个月我给一个内部小工具做前端,需求是个多轮对话的界面。一开始想都没想上了 Redux Toolkit------毕竟"状态多就上 Redux"是肌肉记忆嘛。

结果写到一半就烦了。一个发消息的动作,我要在 slice 里写 addMessage、setStreaming、appendChunk、finishStream 四个 reducer,再配 createAsyncThunk 处理流式返回,光样板代码就铺了一百多行。更难受的是草稿:用户在会话A输了一半切到会话B,回来得还在------这意味着草稿得按 sessionId 存,Redux 里又得多一个 slice、多一组 selector。那天下午我对着 extraReducers 发了会儿呆,心想这破玩意儿就一个聊天框,至于吗。

后来直接拆了换 Zustand。

store 怎么设计

核心就一个 store,三块状态:current(当前会话)、sessions(历史)、drafts(各会话草稿,按 id 存)。

typescript 复制代码
import { create } from 'zustand';

interface Msg { id: string; role: 'user' | 'ai'; content: string }
interface Session { id: string; title: string; messages: Msg[] }

interface ChatState {
  current: Session | null;
  sessions: Session[];              // 历史会话列表
  drafts: Record<string, string>;   // sessionId -> 没发出去的文字

  newSession: () => void;
  switchTo: (id: string) => void;
  pushMsg: (m: Msg) => void;
  appendChunk: (chunk: string) => void;  // 流式拼接
  saveDraft: (text: string) => void;
}

export const useChatStore = create<ChatState>((set, get) => ({
  current: null,
  sessions: [],
  drafts: {},

  newSession: () => {
    const s = { id: crypto.randomUUID(), title: '新对话', messages: [] };
    set(st => ({ current: s, sessions: [s, ...st.sessions] }));
  },

  switchTo: (id) => {
    const target = get().sessions.find(s => s.id === id);
    if (target) set({ current: target });
  },

  pushMsg: (m) => set(st => {
    if (!st.current) return st;
    st.current.messages.push(m);   // 配 immer 更稳,这里图省事直接推
    return { current: { ...st.current } };
  }),

  appendChunk: (chunk) => set(st => {
    const msgs = st.current!.messages;
    msgs[msgs.length - 1].content += chunk;
    return { current: { ...st.current! } };
  }),

  saveDraft: (text) =>
    set(st => ({ drafts: { ...st.drafts, [st.current!.id]: text } })),
}));

没有 provider,没有 dispatch。草稿那块我最满意------切会话只是换 current 的引用,drafts 里按 id 存的文字一直在,切回来 textarea 自动还原。

组件里怎么用

取值用选择器,只订自己关心的那片,别的状态变了不会让你重渲染:

ini 复制代码
function ChatInput() {
  const draft = useChatStore(s => s.current ? s.drafts[s.current.id] ?? '' : '');
  const saveDraft = useChatStore(s => s.saveDraft);

  return (
    <textarea
      value={draft}
      onChange={e => saveDraft(e.target.value)}
      placeholder="说点什么..."
    />
  );
}

历史列表侧边栏也是一行:

ini 复制代码
const sessions = useChatStore(s => s.sessions);
const switchTo  = useChatStore(s => s.switchTo);

想持久化?套个 persist 中间件,历史和草稿自动进 localStorage,刷新不丢。这个我后面加的,大概五分钟。

一点不吹的取舍

Zustand 不是万能。它太自由了------没有强制的目录结构,store 写大了一样会乱成一锅。我那个 store 后来膨胀到两百行也开始难受,得自己拆 slice、自己立规矩,Redux 那套约束没了,纪律得自己上。流式那段直接 push + mutate 也不够干净,严肃项目我会配上 immer。

另外说句题外话,真正让我把这个小工具跑起来的,反而不是前端这部分。

会话状态我半天就搞定了,卡我更久的是后面那个"答得像点样"的智能体本身------我懒得写后端编排,直接找了个零代码搭智能体的平台,拖一拖配一配,挂上模型、塞了份产品文档当知识库,半小时出了第一版。第一版巨干,问什么都背文档,后来调了调提示词和检索才像人话。前端这边我啥都没改,接口照接,它就只负责"对话内容"这摊脏活,挺好。

(模型这块我走的是讯飞的 MaaS,现成 API 调,没自己部署算力。)

所以状态这块,真没必要一上来就 Redux。你做AI应用,会话状态是怎么管的?评论区聊聊,尤其草稿那块有没有更顺的写法,我还在折腾。

相关推荐
骑着蜗牛撵大象3273 小时前
Spring AI + Qwen3.8 周报助手:从事项采集到人工确认卡的闭环实战
人工智能·spring·elasticsearch·自动化·大模型应用·qwen3.8·rag溯源
看浪的路人3 小时前
第6讲:Agent 安全体系——Prompt 注入防御、工具调用审计与输出合规
人工智能·算法
DogDaoDao3 小时前
当视频理解撞上大语言模型:一篇 37 页综述的全景拆解
人工智能·语言模型·音视频·知识图谱·视频
一次旅行3 小时前
2026.10.16 AI产业深度解读|AI独角兽估值遇冷;Meta重磅开放权重模型;硬件产能向AI算力倾斜
人工智能
paopaokaka_luck3 小时前
民宿管理系统 (AI民宿总结、协同过滤推荐算法、WebSocket 在线咨询、ECharts 经营分析、房态与结算联动、 民宿预订业务闭环)
java·人工智能·spring boot·websocket·数据分析·echarts·推荐算法
loulanyue_3 小时前
不训一个参数的准实时强化学习:JitRL与Logit闭式调控的第一性原理——读ICML 2026 Oral论文与《Lau博士的云组会》深度解析
人工智能
清如许.3 小时前
LLM 的分词(Tokenization)详解:文本如何变成 token
人工智能·大模型·llm
Pocker_Spades_A3 小时前
论文精读(十四):基于先验地图的视觉重定位方法综述:启元机器人“知道自己在哪“的底层能力
大数据·人工智能·机器人
昨日之日20064 小时前
FLUX 3 Image Klein:一键搞定文生图和智能编辑,支持精准布局,黑森林新开源
人工智能·计算机视觉·ai作画
Chengbei114 小时前
JsOwl|AI 赋能 JS 敏感信息扫描器,AST + 正则、source‑map 源码还原、登录态爬虫输出交互式 HTML 报告
开发语言·javascript·网络·人工智能·爬虫·网络安全·自动化