其他流式输出方案:Fetch API方案
流式输出三层架构深度解析

架构层次与职责分工
第一层:EventSourceWrapper(连接管理层)
核心职责:基础设施管理
- 连接生命周期管理 :负责 SSE 连接的建立、维护、断开和
重连
- 底层协议封装:处理 EventSource API 的原生事件(onopen、onmessage、onerror)
- 配置动态更新:支持运行时修改连接参数和回调函数
- 单例模式设计 :
确保全局唯一连接
,避免资源浪费 - 错误恢复机制 :
自动重连和异常处理
第二层:MessageService(业务逻辑层)
核心职责:消息处理与状态管理
- 消息生命周期管理:处理用户消息发送、AI响应接收、流式更新
状态机控制
:管理消息状态(streaming、completed、error、stopped)- 内容
增量更新
:实现流式内容的实时拼接和渲染 - 业务逻辑封装 :处理
重试、停止、错误恢复
等业务场景 - 依赖注入设计:通过 handler 接口与 UI 层解耦
第三层:AIDialogContent(UI展示层)
核心职责:用户交互与视图渲染
- 实时UI更新:响应流式数据变化,动态渲染消息内容
- 用户交互控制:提供发送、停止、重试、复制、插入等操作
- 多模态内容支持:处理文本、图片、PDF等不同类型内容
- 状态反馈:显示加载状态、错误提示、操作结果
架构设计优势
1. 解耦与复用
typescript
// 依赖注入实现解耦
interface MessageServiceHandler {
setHistoryContent: (updater: (prev: ChatMessage[]) => ChatMessage[]) => void;
onMessage: (type: 'success' | 'error' | 'warning', content: string) => void;
onStreamStart: () => void;
onStreamCompleteOrError: () => void;
}
- EventSourceWrapper 作为基础设施层,可被多个业务场景复用
- MessageService 通过接口注入,与具体 UI 实现解耦
- AIDialogContent 只关注展示逻辑,不涉及连接管理
2. 单一职责原则
- 每层只负责自己的核心功能,职责边界清晰
- 修改某一层不会影响其他层的实现
- 便于单元测试和功能扩展
3. 可维护性与扩展性
- 新增消息类型只需修改 MessageService 层
- 更换 SSE 实现只需修改 EventSourceWrapper 层
- UI 改版只需修改 AIDialogContent 层
面试介绍话术
"我设计了一套三层架构来处理 AI 对话的流式输出:
底层 EventSourceWrapper 负责 SSE 连接的全生命周期管理,采用单例模式确保资源高效利用,并提供自动重连和错误恢复机制。
中间层 MessageService 作为业务逻辑中间件,通过依赖注入模式与 UI 层解耦,负责消息状态管理、流式内容的增量更新和业务流程控制。
上层 AIDialogContent 专注于用户交互和视图渲染,响应流式数据变化实现实时 UI 更新。
这种架构设计实现了关注点分离,每层职责单一且边界清晰,不仅提升了代码的可维护性和可测试性,还支持各层独立演进和复用。比如 EventSourceWrapper 可以复用到其他需要 SSE 的场景,MessageService 可以适配不同的 UI 框架。"
技术亮点总结
- 架构分层:三层职责清晰,符合软件工程最佳实践
- 设计模式:单例模式 + 依赖注入 + 状态机模式
- 性能优化:增量更新 + 连接复用 + 内存管理
- 用户体验:实时响应 + 优雅降级 + 错误恢复
- 工程化:高内聚低耦合 + 易测试 + 可扩展
需要详细分析项目中用户手动结束对话和连接错误重连的实现机制,查看相关代码逻辑。
toolName: view_files
status: success
filePath: c:\Users\111\Desktop\FE-Jing\website\Back-Management\src\app\blog\edit\components\AIDialogContent\AIDialogContent.tsx
查看 handleStop 函数的实现,了解用户手动停止对话的逻辑。
toolName: view_files
status: success
filePath: c:\Users\111\Desktop\FE-Jing\website\Back-Management\src\app\blog\edit\components\AIDialogContent\AIDialogContent.tsx
查看 MessageService 中的 stop 方法实现,了解停止流的具体逻辑。
toolName: view_files
status: success
filePath: c:\Users\111\Desktop\FE-Jing\website\Back-Management\src\lib\utils\MessageService.ts
查看 EventSourceWrapper 中的错误处理和重连机制。
toolName: view_files
status: success
filePath: c:\Users\111\Desktop\FE-Jing\website\Back-Management\src\lib\utils\EventSourceWrapper.ts
查看 EventSourceWrapper 中的 close 方法和错误处理的完整实现。
toolName: view_files
status: success
filePath: c:\Users\111\Desktop\FE-Jing\website\Back-Management\src\lib\utils\EventSourceWrapper.ts
用户手动结束对话与连接错误重连机制详解
基于项目的三层架构,我来详细解释用户手动结束对话和连接错误重连的实现机制:
1. 用户手动结束对话机制
UI层 (AIDialogContent)
typescript
const handleStop = useCallback(() => {
if (loading) {
messageService.stop();
}
}, [loading, messageService]);
- 触发条件:用户点击停止按钮且当前正在加载状态
- 设计亮点 :只有在
loading
状态下才允许停止,避免无效操作
业务逻辑层 (MessageService)
typescript
public stop() {
if (this.esInstance.isActiveConnection) {
this.isStoppedManually = true; // 标记手动停止
this.esInstance.close(); // 关闭连接
this.handler.onMessage('warning', 'AI响应已停止。');
this.markBotMessageAsFinished(true, 'stopped');
this.handler.setLastSuccessfulUserQuery(null);
}
}
- 核心逻辑 :
- 设置
isStoppedManually = true
标志位,区分手动停止和错误中断 - 调用底层连接关闭方法
- 更新消息状态为 'stopped'
- 清理用户查询记录
- 设置
连接层 (EventSourceWrapper)
typescript
close(): void {
if (this.eventSource) {
// 清理事件监听器,防止内存泄漏
this.eventSource.onmessage = null;
this.eventSource.onerror = null;
this.eventSource.onopen = null;
this.eventSource.close(); // 关闭 SSE 连接
this.eventSource = null;
this.isActive = false; // 标记连接状态
}
}
2. 连接错误重连机制
错误检测与分类
typescript
this.eventSource.onerror = (event) => {
console.error('EventSourceWrapper: SSE connection error:', event);
this.close(); // 确保连接关闭
let errorMsg = 'SSE connection failed.';
// 根据不同错误类型提供具体错误信息
if (event instanceof MessageEvent && typeof event.data === 'string') {
if (event.data === 'Network Error') {
errorMsg = '网络错误,请检查网络连接后重试。';
}
} else if (this.eventSource?.readyState === EventSource.CONNECTING) {
errorMsg = 'Failed to connect to SSE endpoint.';
} else if (this.eventSource?.readyState === EventSource.CLOSED) {
errorMsg = 'SSE connection closed unexpectedly.';
}
};
错误处理策略
typescript
private handleError(error: Error) {
if (!this.isStoppedManually) {
// 非手动停止的错误,提示用户重试
this.handler.onMessage('error', `与AI服务的连接中断:${error.message}`);
this.markBotMessageAsFinished(false, 'error', '请求错误请重试');
} else {
// 手动停止,正常结束
this.markBotMessageAsFinished(true, 'stopped');
}
this.handler.setLastSuccessfulUserQuery(null);
}
3. 重连机制设计
连接状态管理
typescript
connect(): Promise<void> {
if (this.isActive) {
console.warn('connect() called while already active. Closing previous connection.');
this.close(); // 关闭现有连接
}
this.isActive = true; // 防止并发连接
// ... 连接逻辑
}
自动重连触发
- 用户重试:错误提示中包含"请重试",引导用户手动重新发送
- 连接检查 :通过
isActiveConnection
、isConnected
、isConnecting
状态判断 - 防重复连接:连接前先关闭现有连接,避免资源浪费
4. 技术亮点总结
状态管理精确性
- 手动停止标志 :
isStoppedManually
区分用户主动停止和系统错误 - 连接状态追踪 :多维度状态监控(
isActive
、readyState
) - 消息状态更新:精确标记消息状态('stopped'、'error'、'completed')
资源管理优化
- 内存泄漏防护:关闭连接前清理所有事件监听器
- 并发控制:防止多个连接同时建立
- 异步处理:Promise 包装连接过程,便于错误处理
用户体验设计
- 友好错误提示:根据错误类型提供具体的中文提示
- 状态反馈:实时更新 UI 状态(loading、streaming)
- 操作引导:错误时提示用户"请重试",而非自动重连
这种设计既保证了系统的稳定性,又为用户提供了良好的交互体验,体现了前端架构设计中的最佳实践。
用户询问如何记忆之前的对话数据,我需要查看项目中对话历史的存储和管理机制。
toolName: search_codebase
status: success
query: 对话历史存储机制,包括 localStorage、sessionStorage、数据库存储、聊天记录持久化相关的代码实现
搜索结果显示了一些文件,但没有直接找到对话历史存储的相关代码。我需要查看 MessageService 和相关组件中是否有对话历史的持久化存储机制。
toolName: search_codebase
status: success
query: 聊天历史记录存储,包括 aiMessages、chatHistory、localStorage、sessionStorage、数据库存储聊天记录的实现
搜索结果仍然没有找到对话历史存储的具体实现。我需要查看 AIDialogContent 组件中的 aiMessages 状态管理,以及是否有本地存储或持久化机制。
toolName: view_files
status: success
filePath: c:\Users\111\Desktop\FE-Jing\website\Back-Management\src\app\blog\edit\components\AIDialogContent\AIDialogContent.tsx
我看到 aiMessages 状态只是存储在组件的 useState 中,没有持久化。让我搜索是否有任何本地存储或数据库相关的聊天记录存储实现。
toolName: search_by_regex
status: success
query: localStorage|sessionStorage|IndexedDB|chatHistory|conversationHistory
搜索结果为空,说明项目中目前没有实现对话历史的持久化存储。让我查看是否有数据库相关的聊天记录存储API。
toolName: search_by_regex
status: success
query: chat|conversation|message.*history|history.*chat
现在我明确了项目的对话历史存储现状。让我查看数据库相关的API,看是否有聊天记录存储的后端实现。
toolName: view_folder
status: success
c:\Users\111\Desktop\FE-Jing\website\Back-Management\src\app\api
我需要查看数据库配置文件,了解项目的数据库结构,看是否有聊天记录相关的表设计。
toolName: view_files
status: success
filePath: c:\Users\111\Desktop\FE-Jing\website\Back-Management\src\lib\db.ts
对话历史记忆机制分析
当前对话存储现状
1. 临时内存存储
typescript
const [aiMessages, setAiMessages] = useState<ChatMessage[]>([]);
- 存储方式 :仅使用 React 的
useState
存储在组件状态中 - 生命周期:页面刷新或关闭后对话历史完全丢失
- 作用域:仅限当前会话,无法跨页面或跨会话访问
2. 消息结构设计
typescript
interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
status?: 'completed' | 'error' | 'stopped';
isStreaming?: boolean;
originalQuery?: string;
}
- 优点:消息结构设计合理,包含必要的元数据
- 缺点:缺少时间戳、会话ID等持久化必需字段
对话历史持久化解决方案
方案一:前端本地存储(简单快速)
1. localStorage 实现
typescript
// 保存对话历史
const saveConversationHistory = (messages: ChatMessage[]) => {
const conversationData = {
messages,
timestamp: new Date().toISOString(),
articleId: articleId || 'new'
};
localStorage.setItem(`ai-conversation-${articleId}`, JSON.stringify(conversationData));
};
// 加载对话历史
const loadConversationHistory = (): ChatMessage[] => {
const saved = localStorage.getItem(`ai-conversation-${articleId}`);
return saved ? JSON.parse(saved).messages : [];
};
2. 在 AIDialogContent 中集成
typescript
// 组件初始化时加载历史
useEffect(() => {
const savedMessages = loadConversationHistory();
if (savedMessages.length > 0) {
setAiMessages(savedMessages);
}
}, [articleId]);
// 消息更新时自动保存
useEffect(() => {
if (aiMessages.length > 0) {
saveConversationHistory(aiMessages);
}
}, [aiMessages]);
方案二:数据库持久化(推荐生产环境)
1. 数据库表设计
sql
CREATE TABLE ai_conversations (
id INT PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(255) NOT NULL,
article_id INT NULL,
user_id VARCHAR(255) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE ai_messages (
id INT PRIMARY KEY AUTO_INCREMENT,
conversation_id INT NOT NULL,
message_id VARCHAR(255) UNIQUE NOT NULL,
role ENUM('user', 'assistant') NOT NULL,
content TEXT NOT NULL,
status ENUM('completed', 'error', 'stopped') DEFAULT 'completed',
original_query TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id)
);
2. API 接口设计
typescript
// /api/conversations/route.ts
export async function POST(request: Request) {
const { articleId, sessionId } = await request.json();
// 创建新对话会话
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const conversationId = searchParams.get('id');
// 获取对话历史
}
// /api/conversations/[id]/messages/route.ts
export async function POST(request: Request) {
// 保存新消息
}
3. 前端集成
typescript
const messageService = useRef(new MessageService({
setHistoryContent: async (updater) => {
const newMessages = updater(aiMessages);
setAiMessages(newMessages);
// 自动保存到数据库
await saveMessagesToDatabase(newMessages);
},
// ... 其他配置
})).current;
方案三:混合存储策略(最佳实践)
1. 多层存储架构
- 内存层:当前会话的实时数据
- 本地存储层:临时缓存,防止意外刷新丢失
- 数据库层:长期持久化存储
2. 智能同步机制
typescript
class ConversationManager {
private syncToLocal(messages: ChatMessage[]) {
// 立即同步到 localStorage
}
private async syncToDatabase(messages: ChatMessage[]) {
// 批量同步到数据库(防抖处理)
}
private async loadFromBestSource(): Promise<ChatMessage[]> {
// 优先从数据库加载,fallback 到本地存储
}
}
实现建议
立即可实施(方案一)
- 添加 localStorage 支持:快速实现基本的对话记忆功能
- 会话隔离:按文章ID或会话ID分别存储对话历史
- 数据清理:定期清理过期的本地存储数据
长期规划(方案二/三)
- 数据库设计:建立完整的对话存储表结构
- 用户系统集成:关联用户账户,支持跨设备同步
- 对话管理功能:提供对话列表、搜索、导出等功能
技术要点总结
- 当前状态:对话历史仅存在于内存中,页面刷新即丢失
- 核心问题:缺少持久化存储机制
- 推荐方案:localStorage + 数据库的混合存储策略
- 实现优先级:先实现本地存储,再逐步完善数据库持久化
这样的设计既能快速解决当前的对话记忆问题,又为未来的功能扩展奠定了良好的基础。