AI对话数据管理useChat 实现 SSE hook封装

useChat Hook

概述

useChat 是一个功能完整的 React Hook,用于构建支持流式响应的聊天应用。它提供了消息管理、流式数据处理、错误处理、重试机制、会话管理等核心功能。

核心架构

1. 类型系统

1.1 基础消息结构
typescript 复制代码
interface Message<TMetadata = Record<string, unknown>> {
  id: string;           // 唯一标识符
  role: "user" | "assistant";  // 消息角色
  content: string;      // 消息内容
  metadata?: TMetadata;  // 扩展元数据
}
1.2 配置选项
typescript 复制代码
interface ChatOptions<TMetadata> {
  sendFullHistory?: boolean;      // 是否发送完整历史
  maxHistoryLength?: number;      // 最大历史长度
  headers?: Record<string, string>; // 自定义请求头
  timeoutMs?: number;              // 超时时间
  maxRetries?: number;            // 最大重试次数
  retryDelayMs?: number;          // 重试延迟
  parseChunk?: (data: string) => any; // 数据解析函数
  onStatusChange?: (status: ChatStatus) => void; // 状态变更回调
  onChunkError?: (error: Error) => void; // 块错误处理
  sessionId?: string;             // 会话ID
  metadata?: TMetadata;           // 全局元数据
}
1.3 状态管理
typescript 复制代码
type ChatStatus = "ready" | "submitted" | "streaming" | "error";

interface ChatError {
  message: string;
  code: "TIMEOUT" | "ABORT" | "NETWORK" | "HTTP" | "PARSE" | "UNKNOWN";
  details?: unknown;
}

核心实现原理

2.1 流式数据处理架构

2.1.1 TransformStream 管道设计

useChat 采用现代 Web Streams API 构建了一个高效的数据处理管道:

scss 复制代码
ReadableStream (HTTP响应) 
    ↓
TextDecoderStream (二进制→文本)
    ↓
LineSplitterStream (按行分割)
    ↓
SSEParserStream (SSE解析)
    ↓
React State (消息更新)
2.1.2 LineSplitterStream 实现
typescript 复制代码
function createLineSplitterStream(): TransformStream<string, string> {
  let buffer = "";
  return new TransformStream<string, string>({
    transform(chunk, controller) {
      buffer += chunk;
      const lines = buffer.split(/\r?\n/);
      buffer = lines.pop() || "";
      for (const line of lines) {
        if (line) controller.enqueue(line);
      }
    },
    flush(controller) {
      if (buffer) controller.enqueue(buffer);
    },
  });
}

工作原理

  • 维护一个缓冲区累积接收到的文本块
  • 使用正则表达式 \r?\n 分割行
  • 保留不完整的最后一行到缓冲区
  • 流结束时推送剩余数据
2.1.3 SSEParserStream 实现
typescript 复制代码
function createSSEParserStream<TMetadata>(
  parseChunk: (data: string) => any,
  onChunkError?: (error: Error) => void
): TransformStream<string, string> {
  return new TransformStream<string, string>({
    async transform(line, controller) {
      const trimmedLine = line.trim();
      if (trimmedLine.startsWith("data:")) {
        const data = trimmedLine.slice(5).trim();
        if (data && data !== "[DONE]") {
          try {
            const content = parseChunk(data);
            if (content !== null) controller.enqueue(content);
          } catch (err) {
            onChunkError?.(err as Error);
          }
        }
      }
    },
  });
}

SSE格式解析

  • 识别 data: 前缀的行
  • 过滤空数据和 [DONE] 标记
  • 支持自定义解析函数
  • 错误隔离处理

2.2 状态管理机制

2.2.1 核心状态
typescript 复制代码
const [messages, setMessages] = useState<Message<TMetadata>[]>([]);
const [status, setStatus] = useState<ChatStatus>("ready");
const [error, setError] = useState<ChatError | null>(null);
const [currentSessionId, setCurrentSessionId] = useState<string | undefined>();
2.2.2 引用管理
typescript 复制代码
const abortControllerRef = useRef<AbortController | null>(null);
const lastRequestRef = useRef<{
  message: string;
  options: Partial<ChatOptions<TMetadata>>;
} | null>(null);

2.3 消息发送流程

2.3.1 完整生命周期
  1. 验证阶段:检查状态和消息有效性
  2. 状态更新:设置 "submitted" 状态
  3. 消息构建:创建用户消息和空助手消息
  4. 配置合并:合并全局配置和局部配置
  5. 历史处理:根据配置决定发送的历史消息
  6. 重试循环:支持指数退避重试
  7. 流式处理:通过管道处理响应数据
  8. 状态同步:实时更新消息内容和状态
2.3.2 流式数据处理流程
typescript 复制代码
// 1. 创建桥接流
const readableStream = new ReadableStream<Uint8Array>({
  start(controller) {
    const pump = async () => {
      try {
        const { done, value } = await reader.read();
        if (done) controller.close();
        else {
          controller.enqueue(value);
          pump();
        }
      } catch (err) {
        controller.error(err);
      }
    };
    pump();
  },
});

// 2. 构建处理管道
const pipeline = readableStream
  .pipeThrough(new TextDecoderStream())
  .pipeThrough(createLineSplitterStream())
  .pipeThrough(createSSEParserStream(...));

// 3. 消费处理结果
const processedReader = pipeline.getReader();
while (true) {
  const { done, value } = await processedReader.read();
  if (done) break;
  if (value) {
    setMessages(prev => prev.map(msg => 
      msg.id === assistantMessageId 
        ? { ...msg, content: msg.content + value }
        : msg
    ));
  }
}

2.4 错误处理机制

2.4.1 错误分类
  • TIMEOUT: 请求超时
  • ABORT: 用户主动取消
  • NETWORK: 网络错误
  • HTTP: HTTP状态错误
  • PARSE: 数据解析错误
  • UNKNOWN: 未知错误
2.4.2 重试策略
typescript 复制代码
let attempt = 0;
while (attempt < mergedOptions.maxRetries) {
  try {
    // 执行请求
    break; // 成功则退出循环
  } catch (err) {
    if (isAbortOrTimeout(err)) {
      // 不可重试错误,直接返回
      return;
    }
    attempt++;
    if (attempt < maxRetries) {
      await delay(retryDelayMs);
    }
  }
}

2.5 会话管理

2.5.1 会话ID处理
  • 支持通过 options.sessionId 初始化会话
  • 动态切换会话时自动清空消息
  • 支持通过 setMessages 更新会话ID
2.5.2 会话持久化
typescript 复制代码
useEffect(() => {
  if (options.sessionId !== currentSessionId) {
    clearMessages();
    setCurrentSessionId(options.sessionId);
  }
}, [options.sessionId]);

高级特性

3.1 元数据支持

通过 TypeScript 泛型支持任意类型的元数据:

typescript 复制代码
interface MyMetadata {
  confidence?: number;
  sources?: string[];
  timestamp?: Date;
}

const { sendMessage } = useChat<MyMetadata>('/api/chat', {
  metadata: { confidence: 0.95 }
});

3.2 自定义数据解析

支持自定义 parseChunk 函数处理不同格式的响应:

typescript 复制代码
const parseChunk = (data: string) => {
  const parsed = JSON.parse(data);
  if (parsed.type === 'delta') {
    return parsed.content;
  }
  return null;
};

3.3 生命周期钩子

  • onStatusChange: 状态变更通知
  • onChunkError: 块级错误处理

使用示例

4.1 基础用法

typescript 复制代码
import useChat from '@/hooks/use-chat';

function ChatComponent() {
  const { messages, status, error, sendMessage } = useChat('/api/chat');

  const handleSend = async (message: string) => {
    await sendMessage(message);
  };

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id} className={msg.role}>
          {msg.content}
        </div>
      ))}
    </div>
  );
}

4.2 高级配置

typescript 复制代码
const { messages, status, sendMessage, abortRequest } = useChat('/api/chat', {
  sendFullHistory: true,
  maxHistoryLength: 50,
  timeoutMs: 10000,
  maxRetries: 3,
  retryDelayMs: 1000,
  headers: { 'Authorization': 'Bearer token' },
  onStatusChange: (status) => console.log('Status:', status),
  parseChunk: (data) => {
    try {
      const parsed = JSON.parse(data);
      return parsed.choices[0]?.delta?.content || null;
    } catch {
      return null;
    }
  }
});

性能优化

5.1 内存管理

  • 自动清理超时的 AbortController
  • 组件卸载时自动中止请求
  • 使用 useCallback 优化重渲染

5.2 流式处理优势

  • 内存占用恒定,不受响应大小影响
  • 实时更新,无需等待完整响应
  • 支持任意大小的响应数据

错误调试

6.1 调试技巧

typescript 复制代码
const { error, status } = useChat('/api/chat', {
  onStatusChange: console.log,
  onChunkError: console.error,
});

// 错误处理
if (error) {
  console.error('Chat error:', error.code, error.message, error.details);
}

6.2 常见问题

  1. 网络超时 :调整 timeoutMs 配置
  2. CORS错误:检查服务器配置
  3. 解析错误 :验证 parseChunk 函数实现
  4. 状态异常 :检查 status 状态机

扩展指南

7.1 集成其他UI库

支持无缝集成各种UI组件库,只需处理返回的 messagesstatus

7.2 自定义存储

通过 setMessagesgetMessages 实现自定义存储:

typescript 复制代码
// 保存到localStorage
useEffect(() => {
  localStorage.setItem('chat', JSON.stringify(messages));
}, [messages]);

// 从localStorage恢复
const loadMessages = () => {
  const saved = localStorage.getItem('chat');
  if (saved) setMessages(JSON.parse(saved));
};

API 参考

useChat 参数

参数名 类型 默认值 描述
apiUrl string - 聊天API端点
options ChatOptions {} 配置选项

返回值

属性名 类型 描述
messages Message\[\] 当前消息列表
status ChatStatus 当前状态
error ChatError | null 错误信息
sendMessage function 发送消息函数
abortRequest function 中止当前请求
clearMessages function 清空消息列表
retry function 重试上次请求
setMessages function 设置消息列表
resetConversation function 重置会话
相关推荐
PHP实战开发录12 分钟前
PHP接口偶发变慢怎么查
前端·php·开发
向北丶41 分钟前
vite项目中配置alias别名
前端·vite
COOLMO研究AI1 小时前
Next.js App Router 中 sitemap、robots 与 canonical 的配置与排查
前端·web·js·网站优化
DyLatte1 小时前
AI 给了我 8 个优化方案,全都是对的,但没有一个有用
前端·后端·程序员
the局外人1 小时前
一座不够大的城市,装下了我毕业后的成长
前端·程序员·求职
涛涛ing2 小时前
周下载量1.1亿的Tailwind,为什么养不活自己?
前端
变与不变8062 小时前
JS事件机制精讲
前端·javascript
装备研究社2 小时前
Promise 的五个状态,90% 的人只说对了三个
前端
小聪7082 小时前
elpis-core 抽离 npm 包过程的难点和卡点
前端·架构
钱栈up3 小时前
坯料管理导入功能bug修复:只改前端一个文件就解决问题
前端·bug