LLM大模型对话框实践:大文件的分片上传

项目场景:

基于上篇文章的埋点上报文章中,哈喽开提同学针对监控到的不同的数据类型,做了全面可靠的上报方案。

近日,做了一个LLM大模型组件对话框的一个项目,即为用户实现便捷直观的AI交互效果。此时哈喽开提同学联想到了之前做的上报策略,发现上传也可以采取上报的某些策略思想完善 。细想,如果只是调用API做会话效果,再做个对话框页面对于整个项目来说还是远远不够的。侧重于如何给用户带来最好的体验,本次哈喽开提同学针对组件对话的输入 做了大文件分片上传的性能优化。

在LLM对话框的输入能力的设计中,针对用户的多种形式输入,如文本,图片,PDF等文件,在上传过程中,可能会带来以下问题:

  • 网络不稳定导致的上传失败

  • 服务器对文件大小有限制

  • 上传失败后可能还需要上传整个文件,文件过大带来的加载时间太慢等

  • 分片上传的话如果分片过多,一片一片上传岂不是还是无法优化性能

根据以上LLM输入中带来的上传问题,大文件的分片上传是前端需要将大文件分割成多个小片段,将分片固定一个大小,每个分片单独上传到服务器,可能按顺序上传,也可能并行上传,有效解决了大文件上传的稳定性,效率和服务器限制问题。

通过本文章你会学到什么?

  1. 分片上传的设计结构
  2. 上报和上传的思想重叠性
  3. 分片上传的代码逻辑实现

实现思路:

  1. 分片上传的核心流程:
  1. 核心思想:

哈喽开提同学定义了一个 FileUploader 的组件,引入多个自定义钩子如 useUploader 和 useFileInfo,实现了大文件分片上传的核心流程。其中,代码结构大致分为状态管理,文件处理,worker 的处理,上传逻辑和 UI 渲染几部分。其中涉及到了用加密算法返回文件等复杂的计算,为了不阻塞主线程的工作,哈喽开提同学使用 Web Worker,即开辟一个独立于所有线程的协程处理这种复杂运算,处理完之后将结果返回给主线程。

那我们如何用 Web Worker 这个 API 实现这个分片上传的,这其中涉及到了多方面,包括:

  • 文件分片

  • 计算哈希(用作后端返回的 ID 标识)

  • 使用 Web Worker

  • 断点续传

  • 进度跟踪

  • 错误处理

  1. 项目结构:

展开实现:

  1. 前端选择文件,后端返回一个id:
scss 复制代码
const fileInfo = useFileInfo(file);
... ...
const handleStartUpload = async () => {
    if (!file) {
      setError("请选择要上传的文件");
      return;
    }

    try {
      setUploading(true); //上传状态
      setError(null);    //错误信息
      resetProgress(); //上传进度

      // 测试服务器连接
      await testBackendConnection();

      // 计算文件哈希
      setHashingProgress(0);
      const fileHash = await calculateMD5();

      // 处理文件名
      const sanitizedFileName = sanitizeFileName(file.name);

      // 检查文件是否已存在
      const fileExistsResult = await checkFileExists(fileHash);

      if (fileExistsResult.exists && fileExistsResult.fileUrl) {
        setUploadedFileUrl(fileExistsResult.fileUrl);
        setUploading(false);
        return;
      }

      // 使用文件哈希作为文件ID的一部分,确保断点续传能找到之前的上传记录
      let fileId = `${fileHash.substring(0, 10)}-${Date.now()}`;
      
      ... ...
      
    } catch (error: unknown) {
      handleUploadError(error);
    } finally {
      setUploading(false);
    }
  };
  1. 分片与哈希计算:
typescript 复制代码
import { UploadConfig } from "../types/upload";

//分片配置
export const uploadConfig: UploadConfig = {
  CHUNK_SIZE: 2 * 1024 * 1024, // 2MB chunks for better performance
  HASH_CHUNK_SIZE: 5 * 1024 * 1024, // 5MB chunks for hashing
  API_BASE_URL: "http://localhost:8080", // 直接连接到后端服务
};


const calculateMD5 = async (): Promise<string> => {
    return new Promise((resolve, reject) => {
      if (!file) {
        reject(new Error("No file selected"));
        return;
      }

      const worker = getHashWorker(); //懒加载创建进行计算处理的 worker 对象
      if (!worker) {
        reject(new Error("Hash worker not available"));
        return;
      }

      worker.onmessage = (e) => {
        const message = e.data as WorkerMessage; 


        //文件哈希计算流程
        switch (message.type) {
          case "progress":  //进度更新
            if ("progress" in message) {
              setHashingProgress(message.progress);
            }
            break;
          case "complete":  //结果
            setHashingProgress(100);
            if ("hash" in message && message.hash) {
              resolve(message.hash);
            } else {
              reject(
                new Error("Hash calculation completed but no hash returned")
              );
            }
            break;
          case "error":
            reject(new Error(message.error));
            break;
        }
      };

      worker.onerror = () => {
        reject(new Error("Hash calculation failed"));
      };

      //分片上传控制
      worker.postMessage({
        file,
        chunkSize: uploadConfig.HASH_CHUNK_SIZE,
      });
    });
  };
  1. 分片上传到后端:
typescript 复制代码
 // 将上传逻辑抽离为单独的函数,提高可读性
  const uploadFile = (
    fileId: string,
    fileName: string,
    fileHash: string,
    uploadedChunks: number[]
  ): Promise<void> => {
    return new Promise((resolve, reject) => {
      if (!file) {
        reject(new Error("No file selected"));
        return;
      }

      const worker = getUploadWorker();  //创建做上传处理的 worker 对象
      if (!worker) {
        reject(new Error("Upload worker not available"));
        return;
      }

      worker.onmessage = (e) => {
        const message = e.data as WorkerMessage;

        switch (message.type) {
          case "progress": //进度更新
            if (
              "bytesUploaded" in message &&
              "totalBytes" in message &&
              message.bytesUploaded !== undefined &&
              message.totalBytes !== undefined
            ) {
              updateProgress(message.bytesUploaded, message.totalBytes);
            }
            break;
          case "complete": //结果
            if ("fileUrl" in message && message.fileUrl) {
              setUploadedFileUrl(message.fileUrl);
            }
            resolve();
            break;
          case "error":
            reject(new Error(message.error));
            break;
        }
      };

      worker.onerror = () => {
        reject(new Error("上传失败:worker错误"));
      };

      worker.postMessage({  //上传Worker消息处理
        file,
        fileId,
        fileName,
        fileHash,
        chunkSize: uploadConfig.CHUNK_SIZE,
        uploadedChunks,
      });
    });
  };
  1. 合并:与后端联调,交给后端处理

如何实现的断点续传:

断点续传,是一种在文件传输过程中因意外中断(如网络断开,系统崩溃等)后,能够从中断电继续传输的技术。对于大文件上传尤为重要,可显著提升上传效率和用户体验。以下是核心实现机制及技术细节:

  1. 分片上传: 使用 File.slice() 将文件切割为多个 Blob 对象,每个分片携带序号(chunkIndex)。

    ini 复制代码
      const chunkSize = 5 * 1024 * 1024; // 5MB
      const chunks = [];
      for (let i = 0; i < totalChunks; i++) {
        const start = i * chunkSize;
        const end = Math.min(start + chunkSize, file.size);
        chunks.push(file.slice(start, end));
      }
  2. 唯一标识与状态追踪:

    1. 客户端: 通过文件内容哈希(MD5) 生成唯一标识,确保同一文件的多次上传可被识别。利用 localStorage 存储文件哈希等元数据
    2. 服务端: 根据已上传的分片序号,服务端为每个上传任务(uploadId)维护已上传分片列表
    typescript 复制代码
      const saveUploadStateToStorage = (fileHash: string, state: UploadState) => {
        localStorage.setItem(`upload_${fileHash}`, JSON.stringify(state));
      };
  3. 断点 恢复机制:

从本地存储获取上传状态,恢复上传

typescript 复制代码
// 保存上传状态到本地存储
  const saveUploadStateToStorage = (
    fileHash: string,
    state: {
      fileId: string;    //与服务器记录的分片序号有关
      fileName: string;
      fileSize: number;
      startTime: number;
    }
  ) => {
    if (typeof window !== "undefined") {
      try {
        //通过本地保存上传状态
        localStorage.setItem(`upload_${fileHash}`, JSON.stringify(state));
      } catch (e) {
        console.error("无法保存上传状态到本地存储:", e);
      }
    }
  };

  // 从本地存储获取上传状态
  const getUploadStateFromStorage = (fileHash: string) => {
    if (typeof window !== "undefined") {
      try {
       //恢复上传
        const state = localStorage.getItem(`upload_${fileHash}`);
        return state ? JSON.parse(state) : null;
      } catch (e) {
        console.error("无法从本地存储读取上传状态:", e);
        return null;
      }
    }
    return null;
  };

和之前的上报策略有什么思想重叠性?

核心思想 大文件的分片上传 埋点上报
数据分块 遇到大文件时,做分片处理 对于高频事件做批量上报
容错恢复 遇到网络中断,服务器大小限制等问题采取分片上传 遇到网络中断,页面突然关闭等问题的重新上报机制
上传策略 sendBeacon优先,sendBeacon,xhr,Img 三种不同策略的选择 根据进度跟踪做策略选择,三种状态:上传中,上传完毕,上传失败(由跟踪进度的状态维护)
存储方案 本地持久化,再重新上报 本地持久化,将没传完的分片继续上传

总结:

大文件的分片上传在 LLM 大模型输入中起到了至关重要的作用,通过Web Worker 的管理减少了内存占用,保证了主线程工作的稳定性。通过分片上传的思想,确保遇到意外事件时,再次上传不必上传整个文件,对于上传的传输速率和用户体验带来绝佳的效果。

相关推荐
jakeswang40 分钟前
查询条件与查询数据的ajax拼装
前端·ajax
samuel91841 分钟前
axios取消重复请求
前端·javascript·vue.js
三天不学习43 分钟前
JiebaAnalyzer 分词模式详解【搜索引擎系列教程】
前端·搜索引擎·jiebaanalyzer
滿1 小时前
Vue 3 中按照某个字段将数组分成多个数组
前端·javascript·vue.js
安分小尧1 小时前
[特殊字符] 使用 Handsontable 构建一个支持 Excel 公式计算的动态表格
前端·javascript·react.js·typescript·excel
好_快1 小时前
Lodash源码阅读-baseClone
前端·javascript·源码阅读
Double Point1 小时前
(三十一) Dart 中的网络请求教程:从知乎日报 API 获取数据
前端
excel1 小时前
webpack 核心编译器 十二 节
前端
好_快1 小时前
Lodash源码阅读-baseToString
前端·javascript·源码阅读