这个是一个基于私有知识库的智能对话平台,允许用户上传文档构建专属知识库,并通过自然语言交互的方式查询和获取知识。它结合了大语言模型和向量检索技术,让用户通过对话的形式与自己的知识库进行高效交互
应用场景
个人用户场景:
- 学习助手:学生可以上传课堂笔记、教材,构建个人知识库
- 研究工具:实验室拟整理论文资料进行跨文献知识连接与发现
- 创作辅助:作家内容创作者管理素材,获取灵感和参考
企业用户:
- 企业知识管理:整合公司制度、流程文档、技术文档等内部知识
- 新员工培训:加速新员工学习曲线,快速掌握公司业务知识
- 技术支持:技术团队快速解锁产品文档、AP文档故障处理方案
- 客服服务:教师整理教学资料,为学生提供专业个人辅导
专业领域场景:
- 法律咨询:律师整理法规,判列文档,辅助法律分析
- 医疗参考:医生整理医学文件、诊疗指南、临床辅助决策
- 教育培训:教师整理教学资料,为学生提供个性化辅导
这个项目解决了哪些痛点的?
从技术的角度来看,专业的企业知识管理存在很多重点,最明显的就是信息孤岛问题。各个部门的文档分散在不同的系统中,有的在文件,有的在共享文件夹,还有的在各种云盘中。员工想要找一个资料得在好几个地方翻来翻去效率低。
而我们这个系统可以解决这个问题,它不仅支持各种各样的文档形式,还用Apache Tika这个强大的文档解析库来提取文本内容,更重要的是,他引入了组织标签的概念,让不同部门的文档可以有序管理,保证权限隔离
@Query("SELECT u FROM User u WHERE u.orgTags LIKE %:orgTag%")
大文件上传,传统的上传方式经常出现网络不稳定,就上传失败,得重来。而我们这个项目使用了分片上传技术,把大文件切成一小块一小块的上传
public void uploadChunk(String fileMd5, int chunkIndex, long totalSize, String fileName,
MultipartFile file, String orgTag, boolean isPublic, String userId) throws IOException {
logger.info("[uploadChunk] 开始处理分片上传请求 => fileMd5: {}, chunkIndex: {}, totalSize: {}, fileName: {}",
fileMd5, chunkIndex, totalSize, fileName);
// 检查分片是否已上传
if (isChunkUploaded(fileMd5, chunkIndex)) {
logger.info("分片已存在,跳过上传 => fileMd5: {}, chunkIndex: {}", fileMd5, chunkIndex);
return;
}
// 上传分片到MinIO
String chunkPath = String.format("%s/chunk_%d", fileMd5, chunkIndex);
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(chunkPath)
.stream(file.getInputStream(), file.getSize(), -1)
.build());
// 在Redis中标记分片已上传
markChunkUploaded(fileMd5, chunkIndex);
}
我们使用了redis的BitMap来记录哪些上分片已经上传,即使网络中断也能从断点续传,不用从头开始
传统关键词检索的时候经常出现这种情况:我们明明知道这个文档有过相关的内容,但是又是搜索不出来。因为你用的词和文档里面的不一样。而这个项目我们采用了混合检索的方式,把ES的全文检索和向量语义搜索结合起来,这样即使我们使用的词不完全匹配,系统也能理解我们的意图,找到相关文档
/**
* 执行向量化操作
* @param fileMd5 文件指纹
* @param userId 上传用户ID
* @param orgTag 组织标签
* @param isPublic 是否公开
*/
public void vectorize(String fileMd5, String userId, String orgTag, boolean isPublic) {
// 获取文件分块内容
List<TextChunk> chunks = fetchTextChunks(fileMd5);
if (chunks == null || chunks.isEmpty()) {
return;
}
// 提取文本内容
List<String> texts = chunks.stream()
.map(TextChunk::getContent)
.toList();
// 调用外部模型生成向量
List<float[]> vectors = embeddingClient.embed(texts);
// 构建 Elasticsearch 文档并存储
List<EsDocument> esDocuments = IntStream.range(0, chunks.size())
.mapToObj(i -> new EsDocument(
UUID.randomUUID().toString(),
fileMd5,
chunks.get(i).getChunkId(),
chunks.get(i).getContent(),
vectors.get(i),
"deepseek-embed", // 更新为 DeepSeek 的模型版本
userId,
orgTag,
isPublic
))
.toList();
elasticsearchService.bulkIndex(esDocuments); // 批量存储到 Elasticsearch
}
public void processMessage(String userId, String userMessage, WebSocketSession session) {
logger.info("开始处理消息,用户ID: {}, 会话ID: {}", userId, session.getId());
try {
// 1. 获取或创建会话 ID
String conversationId = getOrCreateConversationId(userId);
logger.info("会话ID: {}, 用户ID: {}", conversationId, userId);
// 为当前会话创建响应构建器
responseBuilders.put(session.getId(), new StringBuilder());
// 创建一个CompletableFuture来跟踪响应完成状态
CompletableFuture<String> responseFuture = new CompletableFuture<>();
responseFutures.put(session.getId(), responseFuture);
// 2. 获取对话历史
List<Map<String, String>> history = getConversationHistory(conversationId);
logger.debug("获取到 {} 条历史对话", history.size());
// 3. 执行带权限过滤的混合搜索
List<SearchResult> searchResults = searchService.searchWithPermission(userMessage, userId, 5);
logger.debug("搜索结果数量: {}", searchResults.size());
// 4. 构建上下文
String context = buildContext(searchResults, session.getId());
// 5. 调用 DeepSeek API 并处理流式响应
logger.info("调用DeepSeek API生成回复");
deepSeekClient.streamResponse(userMessage, context, history,
chunk -> {
// 累积响应内容
StringBuilder responseBuilder = responseBuilders.get(session.getId());
if (responseBuilder != null) {
responseBuilder.append(chunk);
}
sendResponseChunk(session, chunk);
},
error -> {
// 处理错误并完成future
handleError(session, error);
// 发送响应完成通知(错误情况)
sendCompletionNotification(session);
responseFuture.completeExceptionally(error);
// 清理会话响应构建器
responseBuilders.remove(session.getId());
responseFutures.remove(session.getId());
});
项目包含的业务模块
- 用户管理模块
- 文档上传与解析模块
- 知识库检索模块
- 聊天助手模块
- 聊天记录模块
- 文档管理与组织模块
疑问解答
Embedding
它是一种将离散数据转换为低维向量的技术,通过神经网络学习,将高维数据映射到低维连续空间,同时保留数据间的语义关系的信息技术
- 解决维度灾难问题,减少计算和存储成本
- 使计算机能够理解文本语义,相似文本在下载空间中相近
- 知识语义相关计算、搜索推荐等应用功能
BitMap是什么?
其是一种数据结构,其基本的思想是用bit作为标记某个元素对应的值采用空间换时间的方法实现高效存储和查询
核心特点:
- 每个元素仅有一个bit特标示位,相比于传统存储方式能极大节省内存空间
- 适用于海量数据去重问题,如上一行数据的排查
典型应用:
- 数据库,索引如oracle的位图索引
- 用户标签系统如存储10亿用户的ID
- 文件上传状态记录如redis中的bitmap标记分片上传状态
- 图像处理作为图像格式存在
如果在高并发场景下发现服务器频繁Full GC随导致系统延迟飙升,你会从哪方面排查?
在高并发场景下排查频繁Full GC导致延迟飙升,我会从以下几个方向系统排查:
- 堆内存配置优化:
-
- 检查堆内存分配比例,确保-Xms和-Xmx设置相同,避免动态扩容
- 优化新生代与老年代比例(-XX:NewRatio),通常2:3较合适
- 调整Eden与Survivor区比例(-XX:SurvivorRatio),推荐6-8
- 限制元空间大小,防止Metaspace泄漏
- GC算法选择与调优:
-
- 从默认Parallel Scavenge+Serial Old切换到G1垃圾收集器
- 考虑使用JDK 11+的ZGC,暂停时间可控制在10ms以内
- 禁用UseAdaptiveSizePolicy,避免JVM自适应调整引发Full GC
- 添加GC日志参数(-XX:PrintGCDetails)进行详细分析
- 对象生命周期管理:
-
- 分析对象是否过早晋升到老年代
- 检查大对象直接分配问题
- 查找内存泄漏点,特别是静态变量和缓存
- 使用对象池技术减少对象创建
- 系统资源监控:
-
- 监控CPU、内存、SWAP使用情况
- 分析Full GC触发时系统资源变化
- 使用jstat、jmap、jstack等工具分析JVM状态
- 代码层面优化:
-
- 检查HashMap等集合类使用是否合理
- 减少循环内对象创建
- 预分配slice/map等集合容量
- 避免对象逃逸到堆
这个项目为什么用websocket而不用用 SSE?
双向通信能力
- WebSocket:支持双向实时通信(客户端↔服务端)。
-
- 用户可能需要实时发送中断指令、调整提问或触发新操作(如"重新回答""切换模型"),而AI的流式响应需即时反馈。
- SSE仅支持服务端单向推送,无法满足用户主动交互的需求。
- SSE:仅支持服务端→客户端单向推送,无法处理用户实时输入。
连接稳定性与状态管理
- WebSocket:
-
- 长连接更稳定,适合高频交互场景(如对话中频繁发送/接收消息)。
- 连接状态可被程序主动管理(如心跳检测、异常重连)。
- SSE:
-
- 依赖HTTP长连接,但网络波动易导致连接中断,且重连机制较弱。
- 无原生连接状态管理,需额外实现复杂逻辑。
协议效率与性能
- WebSocket:
-
- 基于 TCP 的二进制协议,数据帧开销小(仅2字节头部),适合传输大量实时数据(如长文本流)。
- 传输效率高,延迟低,尤其适合低延迟交互场景。
- SSE:
-
- 基于 HTTP,需携带HTTP头部(如
Content-Type: text/event-stream),数据量较大时效率较低。 - 文本格式(如
data: {...})需额外解析,性能略逊。
- 基于 HTTP,需携带HTTP头部(如
流式响应的实时性
- WebSocket:
-
- 支持真正的逐块流式输出(AI生成内容时立即推送,无需等待完整响应)。
- 用户可实时看到内容生成过程,体验接近ChatGPT。
- SSE:
-
- 虽然也能流式推送,但依赖HTTP分块传输(
Transfer-Encoding: chunked),在复杂网络环境下可能存在延迟或丢包。
- 虽然也能流式推送,但依赖HTTP分块传输(
扩展性与协议灵活性
- WebSocket:
-
- 协议独立于HTTP,可承载任意类型数据(文本、二进制),便于未来扩展(如语音、视频交互)。
- 支持自定义子协议(如
chat、file-transfer),适配不同业务场景。
- SSE:
-
- 仅支持文本格式,扩展性有限,难以支持复杂交互需求。
为何不选SSE?
虽然SSE实现简单(原生浏览器API支持),且适合单向数据推送(如股票行情、日志流),但派聪明的核心场景是双向对话:
- 用户需实时提问、修改问题、中断回答。
- AI需即时响应并流式输出答案。 WebSocket的双向性、低延迟和稳定性完美匹配这一需求,而SSE单向推送的特性无法满足交互式对话的完整流程。
总结
|-------|--------------|---------------|
| 特性 | WebSocket | SSE |
| 通信方向 | 双向(客户端↔服务端) | 单向(服务端→客户端) |
| 协议效率 | 高(二进制协议,低开销) | 中(HTTP头部开销大) |
| 连接稳定性 | 强(长连接,可主动管理) | 弱(依赖HTTP,易断线) |
| 实时性 | 极高(逐块推送,延迟低) | 较高(依赖HTTP分块) |
| 扩展性 | 强(支持任意数据类型) | 有限(仅文本) |
在派聪明中,WebSocket 是实现"实时对话+流式响应"的最优解,兼顾了交互性、性能和用户体验,而SSE更适合单向数据广播场景。