前端 + AI 进阶 Day 9: 多轮对话状态管理
前端 + AI 进阶学习路线|Week 9-10:对话式界面设计
Day 9:多轮对话状态管理
学习时间:2026年1月2日(星期五)
关键词:多会话管理、上下文记忆、本地持久化、会话切换、localStorage
📁 项目文件结构
day09-multi-session-chat/
├── src/
│ ├── components/
│ │ ├── SessionSidebar.jsx # 会话列表侧边栏
│ │ ├── ChatWindow.jsx # 聊天主窗口(复用 Day 4 流式组件)
│ │ └── NewSessionButton.jsx # 新建会话按钮
│ ├── hooks/
│ │ └── useSessionManager.js # 会话状态管理 Hook
│ ├── lib/
│ │ └── aiClient.js # 复用 Day 4 的 AI 客户端
│ └── App.jsx # 主应用集成
└── public/
✅ 本日核心:支持多个独立 AI 对话会话,且关闭页面后不丢失
🎯 今日学习目标
- 实现 多会话创建与切换(每个会话独立上下文)
- 本地持久化 所有会话历史(使用
localStorage) - 支持 会话重命名 / 删除
- 构建专业级 AI 聊天工作台基础
💡 为什么需要多会话管理?
在真实使用中,用户常需:
- 同时处理 多个任务(如“写代码” + “查资料”)
- 隔离上下文(避免不同话题混淆)
- 保留历史(关闭浏览器后仍可继续对话)
✅ 单一会话体验简陋,多会话 + 持久化 是专业 AI 应用的标配
📚 核心设计思路
| 功能 | 实现方式 |
|---|---|
| 会话数据结构 | { id, title, messages: [{role, content}], createdAt } |
| 状态管理 | React Context + useState + useEffect |
| 本地持久化 | localStorage + JSON 序列化(带错误处理) |
| 默认会话 | 首次访问自动创建“新会话” |
⚠️ 注意:
localStorage有 5-10MB 限制,需定期清理或分页
🔧 动手实践:构建多会话聊天工作台
步骤 1:创建项目并安装依赖
npx create-react-app day09-multi-session-chat
cd day09-multi-session-chat
npm install react-virtual react-markdown remark-gfm rehype-highlight rehype-katex katex @microsoft/fetch-event-source
💡 复用 Day 4 的
aiClient.js和流式组件
步骤 2:创建会话管理 Hook
// src/hooks/useSessionManager.js
import { useState, useEffect } from 'react';
const SESSIONS_KEY = 'ai_chat_sessions';
const CURRENT_SESSION_KEY = 'ai_current_session_id';
// 生成会话 ID
const generateSessionId = () => Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
// 默认欢迎消息
const createNewSession = (title = '新会话') => ({
id: generateSessionId(),
title,
messages: [
{ id: 'welcome', role: 'assistant', content: '你好!我是你的 AI 助手,有什么可以帮你的吗?' }
],
createdAt: Date.now(),
});
export const useSessionManager = () => {
const [sessions, setSessions] = useState(() => {
// 从 localStorage 读取
const saved = localStorage.getItem(SESSIONS_KEY);
if (saved) {
try {
return JSON.parse(saved);
} catch (e) {
console.error('Failed to parse sessions from localStorage');
}
}
// 首次访问,创建默认会话
const defaultSession = createNewSession();
return [defaultSession];
});
const [currentSessionId, setCurrentSessionId] = useState(() => {
const savedId = localStorage.getItem(CURRENT_SESSION_KEY);
if (savedId && sessions.find(s => s.id === savedId)) {
return savedId;
}
return sessions[0]?.id || null;
});
// 持久化 sessions
useEffect(() => {
try {
localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions));
} catch (e) {
console.error('Failed to save sessions to localStorage', e);
}
}, [sessions]);
// 持久化 currentSessionId
useEffect(() => {
if (currentSessionId) {
localStorage.setItem(CURRENT_SESSION_KEY, currentSessionId);
}
}, [currentSessionId]);
// 创建新会话
const createSession = (title) => {
const newSession = createNewSession(title);
setSessions(prev => [newSession, ...prev]);
setCurrentSessionId(newSession.id);
return newSession.id;
};
// 切换会话
const switchSession = (id) => {
if (sessions.find(s => s.id === id)) {
setCurrentSessionId(id);
}
};
// 更新会话消息
const updateSessionMessages = (sessionId, newMessages) => {
setSessions(prev =>
prev.map(session =>
session.id === sessionId
? { ...session, messages: newMessages }
: session
)
);
};
// 更新会话标题
const updateSessionTitle = (sessionId, title) => {
setSessions(prev =>
prev.map(session =>
session.id === sessionId
? { ...session, title }
: session
)
);
};
// 删除会话
const deleteSession = (sessionId) => {
setSessions(prev => {
const filtered = prev.filter(s => s.id !== sessionId);
// 如果删除的是当前会话,切换到第一个
if (sessionId === currentSessionId && filtered.length > 0) {
setCurrentSessionId(filtered[0].id);
}
return filtered;
});
};
const currentSession = sessions.find(s => s.id === currentSessionId) || null;
return {
sessions,
currentSession,
createSession,
switchSession,
updateSessionMessages,
updateSessionTitle,
deleteSession,
};
};
步骤 3:创建会话侧边栏组件
// src/components/SessionSidebar.jsx
import { useState } from 'react';
const SessionSidebar = ({
sessions,
currentSessionId,
onSwitchSession,
onCreateSession,
onUpdateTitle,
onDeleteSession
}) => {
return (
<div style={{ width: '240px', borderRight: '1px solid #e8e8e8', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '16px', paddingBottom: '8px' }}>
<button
onClick={() => onCreateSession(prompt('会话标题:', '新会话') || '新会话')}
style={{
width: '100%',
padding: '8px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
fontWeight: '500',
}}
>
➕ 新建会话
</button>
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
{sessions.map((session) => (
<SessionItem
key={session.id}
session={session}
isActive={session.id === currentSessionId}
onSwitch={() => onSwitchSession(session.id)}
onUpdateTitle={(title) => onUpdateTitle(session.id, title)}
onDelete={() => {
if (window.confirm(`确定删除会话 "${session.title}"?`)) {
onDeleteSession(session.id);
}
}}
/>
))}
</div>
</div>
);
};
// 单个会话项
const SessionItem = ({ session, isActive, onSwitch, onUpdateTitle, onDelete }) => {
const [isEditing, setIsEditing] = useState(false);
const [editTitle, setEditTitle] = useState(session.title);
const handleTitleBlur = () => {
if (editTitle.trim() && editTitle !== session.title) {
onUpdateTitle(editTitle.trim());
}
setIsEditing(false);
};
return (
<div
style={{
padding: '10px 16px',
cursor: 'pointer',
backgroundColor: isActive ? '#e6f7ff' : 'transparent',
borderLeft: isActive ? '3px solid #1890ff' : 'none',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
onClick={onSwitch}
>
{isEditing ? (
<input
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
onBlur={handleTitleBlur}
onKeyDown={(e) => e.key === 'Enter' && handleTitleBlur()}
autoFocus
style={{
flex: 1,
border: '1px solid #d9d9d9',
borderRadius: '3px',
padding: '2px 4px',
fontSize: '14px'
}}
/>
) : (
<div
style={{
flex: 1,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontSize: '14px'
}}
onDoubleClick={() => setIsEditing(true)}
title={session.title}
>
{session.title}
</div>
)}
<button
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
style={{
background: 'none',
border: 'none',
color: '#ff4d4f',
cursor: 'pointer',
fontSize: '16px',
marginLeft: '8px',
}}
>
×
</button>
</div>
);
};
export default SessionSidebar;
步骤 4:集成主应用(App.jsx)
// src/App.jsx
import 'katex/dist/katex.min.css';
import { useSessionManager } from './hooks/useSessionManager';
import SessionSidebar from './components/SessionSidebar';
import ChatWindow from './components/ChatWindow'; // 复用 Day 4 的流式聊天组件
function App() {
const {
sessions,
currentSession,
createSession,
switchSession,
updateSessionMessages,
updateSessionTitle,
deleteSession,
} = useSessionManager();
if (!currentSession) {
return <div style={{ padding: '20px' }}>加载中...</div>;
}
return (
<div style={{ display: 'flex', height: '100vh', fontFamily: 'Inter, -apple-system, sans-serif' }}>
<SessionSidebar
sessions={sessions}
currentSessionId={currentSession.id}
onSwitchSession={switchSession}
onCreateSession={createSession}
onUpdateTitle={updateSessionTitle}
onDeleteSession={deleteSession}
/>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{
padding: '16px 24px',
borderBottom: '1px solid #e8e8e8',
backgroundColor: '#fff',
fontSize: '16px',
fontWeight: '600'
}}>
{currentSession.title}
</div>
<ChatWindow
session={currentSession}
onUpdateMessages={(newMessages) =>
updateSessionMessages(currentSession.id, newMessages)
}
/>
</div>
</div>
);
}
export default App;
💡 ChatWindow.jsx 可直接复用 Day 4 的
StreamingChat.jsx,仅需调整messages和onSend逻辑以适配会话管理。
✅ 效果验证
- ✅ 首次打开 → 自动创建“新会话”
- ✅ 点击“新建会话” → 创建新会话并切换
- ✅ 双击会话标题 → 可编辑重命名
- ✅ 点击 × → 删除会话(确认弹窗)
- ✅ 刷新页面 → 所有会话和当前状态保留
- ✅ 不同会话消息完全隔离
🤔 思考与延伸
-
存储优化:
localStorage有大小限制,如何处理大量会话?
→ 可按时间归档,或切换到 IndexedDB -
同步支持:如何实现多设备同步?
→ 集成 Firebase 或自建后端 API -
会话搜索:如何快速查找历史会话?
→ 在侧边栏增加搜索框,按标题/内容过滤
💡 为 Week 2 做准备:此会话管理逻辑可抽象为
useAISessionHook,纳入ai-frontend-kit
📅 明日预告
Day 10:富交互消息
- 在消息流中嵌入 按钮 / 卡片 / 表单
- 支持用户点击按钮触发 AI 动作
- 构建 AI 原生交互范式
✍️ 小结
今天,我们让 AI 聊天从“单线对话”升级为“多任务工作台”!通过多会话管理与本地持久化,用户可同时处理不同任务,且历史永不丢失。会话隔离与持久化,是专业 AI 应用的基石。
💬 实践提示:如果遇到
localStorage满的错误,可在useEffect中添加清理逻辑(如保留最近 50 个会话)。欢迎分享你的多会话管理设计!

浙公网安备 33010602011771号