PDF格式,简历内容控制在两页了,基本都是AI出来的。 RAG和Agent都是从GitHub上找的开源的,让大模型分析技术栈写的。 提到的技术栈能说出来个7788。就是不知道还不会不会问Java八股文 2 个帖子 - 2 位参与者 阅读完整话题
本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 前言 本教程的环境基于 jdk8 + langchain4j 0.35 教程源码放在这里了: github.com GitHub - worenbudaoni/rag-study-helper: 一个学习检索增强生成的全流程助手 一个学习检索增强生成的全流程助手 文章内容 因为内容比较多,我会从下面三个文章进行讲解,后续发布后会贴出来,这节讲: 接入飞书WIKI文档 RAG实现全流程 : 【开源、教程】RAG全流程实现(java+完整代码):第一弹 接入飞书WIKI文档 : 【开源、教程】RAG全流程实现(java+完整代码):第二弹 接口限流:令牌桶 + AOP 强烈建议先看完第一弹,不然后面代码有可能看不懂 实现逻辑 后面会有图文讲解的,这里就相当于大概介绍一下,看个大概就好,有不了解的不要先去搜,我后文都会讲,如果讲漏了麻烦评论一下,我改正 飞书开发者平台 : 1、去飞书开发者平台创建一个应用 2、给应用赋予权限(权限管理 菜单) 3、给应用赋予机器人能力(添加应用能力 菜单) 4、发布(版本管理与发布 菜单) 5、获取应用的app-id、app-secret(凭证和基础信息 菜单) 飞书app : 1、创建一个群聊 2、把机器人给拉进去 3、点击左边菜单的更多找到知识库,新建知识库(下面统一称为 WIKI) 4、添加群聊(机器人)为管理员:点 WIKI 进去会打开一个网站,左下角有个设置点进去,在成员设置->角色与权限->管理员,添加管理员,搜索我们刚才创建的有机器人的群聊并添加 5、在页面的连接处找到space-id,如: https://kcnvw23rzo5r.feishu.cn/wiki/settings/666666(666666就是我们要的space-id) 项目 : 1、创建一个job,用来定时获取文档(下面为job启动后的流程) 2、通过app-id、app-secret获取tenant_access_token和expire 3、通过tenant_access_token和space-id获取文档信息(名字、更新时间、documentToken 等等) 4、通过documentToken去获取文档的内容(字符串) 5、走 【开源、教程】RAG全流程实现(java+完整代码):第一弹 的入库流程 一、飞书开发者平台 app-id、app-secret 是啥 app-id :应用的唯一标识 app-secret :应用的密钥,在创建应用时由平台生成,可用于获取app_access_token 1、飞书开放平台创建企业应用 开发者后台 - 飞书开放平台 这里注意的是创建好应用后需要审核、启用,所以企业级的应用权限在领导手上,我们可以创建一个个人版的飞书账号来做实验 2、给应用赋予权限 (权限管理菜单) 直接复制我的也行 { "scopes": { "tenant": [ "bitable:app:readonly", "docx:document:readonly", "drive:drive:readonly", "drive:file:readonly", "wiki:wiki:readonly" ], "user": [] } } 3、给应用赋予机器人能力 (添加应用能力菜单) 4、发布 (版本管理与发布菜单) 创建好后发布就行 5、获取应用的app-id、app-secret(凭证和基础信息菜单) 二、飞书app space-id是啥 就是飞书知识库(WIKI)所对应的空间ID,我们找到这个空间就可以找到下面的文档 打个比方就是图书馆的书架,书架有个唯一标识(小说),我们根据这个书架ID(小说)去找下面所有的书 1、创建一个群聊 2、把机器人给拉进去 3、点击左边菜单的更多找到知识库,新建知识库(下面统一称为 WIKI) 4、添加群聊(机器人)为管理员 点 WIKI 进去会打开一个网站,左下角有个设置点进去,在成员设置->角色与权限->管理员,添加管理员,搜索我们刚才创建的有机器人的群聊并添加 5、在页面的连接处找到space-id 如: https://kcnvw23rzo5r.feishu.cn/wiki/settings/666666(666666就是我们要的space-id) 三、项目 这里就不按照上面实现逻辑写的走了,我就按照代码里的讲解 1、job总览(步骤拆解在后面) FeishuSyncService.java // 没想加重框架,如果用xxl-job什么的,自己搬一下就行了 @Scheduled(cron = "${app.feishu.cron}") public void syncWiki() { log.info("Starting Feishu wiki sync for space: {}", spaceId); try { // 获取全部文档信息(通过app-id、app-secret、space-id) // 这里逻辑后面会讲 List<WikiNode> nodes = feishuClient.getWikiNodeTree(spaceId); log.info("Found {} nodes in wiki", nodes.size()); // 成功数量,跳过数量(如果数据存在了关系型数据库且没有更新就跳过),失败数量 int synced = 0, skipped = 0, failed = 0; for (WikiNode node : nodes) { // 后缀 String objType = node.getObjType(); // 文档令牌 用来获取 文档内容 String nodeToken = node.getNodeToken(); // 更新时间判断是否需要跳过 long updateTime = node.getUpdateTime(); // 是否入库 Documents doc = documentsMapper.selectOne( Wrappers.<Documents>lambdaQuery() .eq(Documents::getFeishuNodeToken, nodeToken) ); // 文档是否更新是否需要跳过,这个更新时间不在where条件里面是因为后续要继续用到这个数据 if (doc != null && doc.getFeishuUpdateTime() != null && doc.getFeishuUpdateTime() == updateTime) { skipped++; continue; } try { // 文档内容 String content; // 获取文件名 String fileName; switch (objType) { case "doc": case "docx": // 获取文档内容 // 这里逻辑就不讲了,我后面扔给飞书的文档,照着对接或者看我源码就好 content = feishuClient.getDocumentContent(node.getObjToken()); fileName = node.getNodeTitle() + "_文档"; break; case "sheet": content = feishuClient.getSheetContent(node.getObjToken()); fileName = node.getNodeTitle() + "_表格"; break; case "bitable": content = feishuClient.getBitableContent(node.getObjToken()); fileName = node.getNodeTitle() + "_多维表格"; break; default: skipped++; continue; } // 如果是更新,先删旧向量和映射记录 if (doc != null) { // 查询旧文档相关的向量映射 List<DocumentChunks> oldChunks = documentChunksMapper.selectList( Wrappers.<DocumentChunks>lambdaQuery() .eq(DocumentChunks::getDocumentId, doc.getId()) ); // 有两张表 // 第一张为文档库:记录文档标题、更新时间、创建人等信息 // 第二张为分片库:记录向量数据库插入后的向量ID // 向量ID List<String> vectorIds = oldChunks.stream() .map(DocumentChunks::getVectorId) .collect(Collectors.toList()); // 删除向量 embeddingStore.removeAll(vectorIds); // 删除映射记录 documentChunksMapper.delete( Wrappers.<DocumentChunks>lambdaQuery() .eq(DocumentChunks::getDocumentId, doc.getId()) ); // 删除文档 documentsMapper.deleteById(doc.getId()); } // RAG 入库流程 (第一篇文章中亦有记载(跟第一章代码有些许出入,看完第一章后,直接看源码更佳)) ingestionService.ingestFeishuDocument(fileName, content, nodeToken, updateTime, objType); synced++; log.info(" Synced: {} ({})", node.getNodeTitle(), nodeToken); } catch (Exception e) { log.error(" Failed to sync node: {} ({})", node.getNodeTitle(), nodeToken, e); failed++; } } // 清理远程已删除的文档 // 这里的逻辑是 // 第一次job执行:查询飞书wiki给了 A、B、C 三个文档入库 // 后面有人在wiki中删了 C 文档 // 第二次job执行:只有查询出 A、B 两个文档 // 这时就要去数据库中和向量库中删除多余的 C 文档 List<String> remoteTokens = nodes.stream() .map(WikiNode::getNodeToken) .collect(Collectors.toList()); if (!remoteTokens.isEmpty()) { // MySQL 查出本地多出的记录,只遍历需要删除的 List<Documents> toRemove = documentsMapper.selectList( Wrappers.<Documents>lambdaQuery() .isNotNull(Documents::getFeishuNodeToken) .notIn(Documents::getFeishuNodeToken, remoteTokens) ); for (Documents removed : toRemove) { log.info("Document removed remotely, cleaning up: {} ({})", removed.getDocumentName(), removed.getFeishuNodeToken()); List<DocumentChunks> chunks = documentChunksMapper.selectList( Wrappers.<DocumentChunks>lambdaQuery() .eq(DocumentChunks::getDocumentId, removed.getId()) ); List<String> vectorIds = chunks.stream() .map(DocumentChunks::getVectorId) .collect(Collectors.toList()); // 向量数据库 删 embeddingStore.removeAll(vectorIds); // 关系型数据库 分片库 删 documentChunksMapper.delete( Wrappers.<DocumentChunks>lambdaQuery() .eq(DocumentChunks::getDocumentId, removed.getId()) ); // 关系型数据库 文档库 删 documentsMapper.deleteById(removed.getId()); } } log.info("Feishu wiki sync complete: synced={}, skipped={}, failed={}", synced, skipped, failed); } catch (Exception e) { log.error("Feishu wiki sync failed", e); } } 2、递归获取知识库所有文档节点 FeishuClient.java 这里其实没什么特别好讲的点,就是参考飞书文档,然后请求并解析 我在源码里也标记了文档的地址,所以这里放一个总体的查询地址 开发文档 - 飞书开放平台 /** * 获取 tenant_access_token(内部自动缓存和刷新) */ public synchronized String getAccessToken() throws IOException { if (cachedToken != null && System.currentTimeMillis() < tokenExpireAt) { return cachedToken; } String json = "{\"app_id\":\"" + appId + "\",\"app_secret\":\"" + appSecret + "\"}"; // https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal Request request = new Request.Builder() .url(baseUrl + "/open-apis/auth/v3/tenant_access_token/internal") .post(RequestBody.create(JSON, json)) .build(); try (Response resp = httpClient.newCall(request).execute()) { JsonNode body = objectMapper.readTree(resp.body().string()); if (body.get("code").asInt() != 0) { throw new IOException("Failed to get access token: " + body); } cachedToken = body.get("tenant_access_token").asText(); // tenant_access_token 的最大有效期是 2 小时 // 7200 是秒 int expire = body.get("expire").asInt(7200); // 防御性编程 免得刚好过期 由于网络延时 造成接口调用失败 tokenExpireAt = System.currentTimeMillis() + (expire - 60) * 1000L; return cachedToken; } } /** * 递归获取知识库所有文档节点。 */ public List<WikiNode> getWikiNodeTree(String spaceId) throws IOException { List<WikiNode> allNodes = new ArrayList<>(); collectNodes(spaceId, null, allNodes); return allNodes; } private void collectNodes(String spaceId, String parentNodeToken, List<WikiNode> result) throws IOException { List<WikiNode> currentLevelNodes = new ArrayList<>(); String pageToken = null; do { // https://open.feishu.cn/document/server-docs/docs/wiki-v2/space-node/create StringBuilder url = new StringBuilder(baseUrl + "/open-apis/wiki/v2/spaces/" + spaceId + "/nodes"); if (parentNodeToken != null) { url.append("/").append(parentNodeToken).append("/children"); } url.append("?page_size=50"); if (pageToken != null) { url.append("&page_token=").append(pageToken); } Request request = new Request.Builder() .url(url.toString()) .header("Authorization", "Bearer " + getAccessToken()) .get() .build(); try (Response resp = httpClient.newCall(request).execute()) { JsonNode body = objectMapper.readTree(resp.body().string()); if (body.get("code").asInt() != 0) { log.error("Wiki API error for URL [{}]: {}", url, body); break; } JsonNode items = body.path("data").path("items"); for (JsonNode item : items) { WikiNode node = new WikiNode(); // 节点token node.setNodeToken(item.path("node_token").asText()); // 对应文档类型的token,可根据 obj_type 判断属于哪种文档类型。 node.setObjToken(item.path("obj_token").asText()); // 文档类型,对于快捷方式,该字段是对应的实体的obj_type。 // 可选值有: // doc:旧版文档 sheet:表格 mindnote:思维导图 bitable:多维表格 file:文件 docx:新版文档 slides:幻灯片 node.setObjType(item.path("obj_type").asText()); // 文档标题 node.setNodeTitle(item.path("title").asText()); node.setParentNodeToken(parentNodeToken); // 是否有子节点 node.setHasChild(item.path("has_child").asBoolean(false)); // 文档最近编辑时间 String editTime = item.path("obj_edit_time").asText(); node.setUpdateTime(Long.parseLong(editTime.isEmpty() ? "0" : editTime)); currentLevelNodes.add(node); } pageToken = body.path("data").path("page_token").asText(null); } } while (pageToken != null && !pageToken.isEmpty()); // Add all nodes from this level, then recurse into children result.addAll(currentLevelNodes); for (WikiNode node : currentLevelNodes) { if (node.isHasChild()) { collectNodes(spaceId, node.getNodeToken(), result); } } } 四、测试 1、导入文档 还是拿这个 补鸡稻 作为测试案例 2、配置 app-id、app-secret、space-id和sync-enable 我这里通过 jvm 运行参数注入,免得又把 apikey 给上传到 github 了 sync-enable 记得为 true,不然不注册 spring bean 3、运行时发现文档可以查询到,并且入库了 4、提问 5、测试结束,完结撒花 2 个帖子 - 2 位参与者 阅读完整话题
先免费开个 Pro 额度玩了后选择 overages 一路猛干! 8 个帖子 - 7 位参与者 阅读完整话题
本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 前言 本教程的环境基于 jdk8 + langchain4j 0.35 教程源码放在这里了: github.com GitHub - worenbudaoni/rag-study-helper: 一个学习检索增强生成的全流程助手 一个学习检索增强生成的全流程助手 文章内容 因为内容比较多,我会从下面三个文章进行讲解,后续发布后会贴出来,这节讲: RAG实现全流程 RAG实现全流程 接入飞书WIKI文档 接口限流:令牌桶 + AOP RAG实现全流程 RAG、Embeding、Reranker、向量、向量数据库 是什么 本文主要讲RAG流程+代码,所以在讲完之后我会把这些列出来完整讲一下,这里就用大白话一笔带过了 RAG (检索增强生成(Retrieval Augmented Generation))是一种技术,它先从一个知识库中检索出相关的信息片段,再把这些信息“喂”给大语言模型,让它基于这些事实来生成更准确的答案。 Embedding (文本嵌入模型)嵌入是将文本、图像等数据转换为固定长度的向量表示,使得语义相似的内容在向量空间中距离更近,是检索步骤的关键技术。 Reranker (重排序模型)是对初步检索结果进行二次精排,根据与查询的相关性重新打分排序,以提升最终召回结果的质量。 向量 是将文本、图像等数据转换为一串数值数组(如 [0.1, 0.5, -0.2, ...] ),用来表示其语义或特征。相似的内容在向量空间中距离更近。 向量数据库 是专门存储和检索向量的数据库,支持高效的相似性搜索(如余弦相似度、欧氏距离),常用于 RAG 等场景中快速找到最相关的内容。 RAG 通用实现思路(图文) 图(配合下文观看更佳) 文 入库流程:文件->分割器->Embedding->向量数据库(入库) 文件 :(word、pdf、ppt、md、excel、图片)通过各种手段(POI、OCR)转成 字符串 分割器 :一个文件可能有几万几十万的字符,我们提问的知识可能只是其中的一个片段,如果把整个文件转向量存储起来,通过我们的提问从向量数据库找到了这个几万字符的文档,把这几万的字符喂给LLM耗token不说,无关的知识可能混淆内容,为了 精简内容、提取精华 ,我们就要用到分割器,分割器的功能就是通过段落、句子,把一长段字符串拆解成 文本段 。分割器的实现方式有很多,没有那种是银弹,只有合适才最重要。 Embedding(文本嵌入模型) :这个也是属于语言模型的一种,但它不擅长生成文本,而擅长理解语义并把字符串(入库流程中就是把分割器分出来的多个字符串)转为 向量 向量数据库 :我们把 文本段 和 向量 (如果要做权限管控,支持添加Metadata)都存进向量数据库中(PS:后续的提问流程就是通过问题转成向量去向量数据库通过余弦相似度(后面我会讲)得到我们需要的文本段) 提问流程:问题->问题重写->Embedding->向量数据库(匹配出库)->相关性筛选->rerank->prompt优化(文本段+问题)->LLM回答 问题 :就是我们的提问 问题重写 :在 RAG 场景下,你通过 《如何学习JAVA》 这个文档去检索,你第一次问:“java 要学什么框架”,embedding 根据 “java 要学什么框架” 转成向量去向量数据库检索到了相应的内容再放进 prompt 喂给 LLM,LLM 根据文档说:“springboot”,你第二次问:“他有什么好处”,我们可以一眼就看出这里的他指的是 springboot ,但是 embedding 模型不知道,embedding 只是把你输入的 “他有什么好处” 转换成向量去向量数据库查询,所以查出来的根本就不是你想要的文档内容,这时 LLM 就不会根据文档去生成你想要的内容了,问题重写就是根据你的上下文让LLM重写你的问题,使 embedding 生成的向量更能在向量数据库检索到更准确的文档内容,当然每次提问都要重写肯定费时又费token,所以我们要判断什么情况下重写问题能得到更好的效果,我的重新逻辑就是提问小于5个字且包含他、她、它、上述等关键词时进行重写 Embedding(文本嵌入模型) :这个也是属于语言模型的一种,但它不擅长生成文本,而擅长理解语义并把字符串(提问流程中就是把问题)转为 向量 向量数据库 :我们根据 向量 (如果有权限管控,支持过滤Metadata)去数据库中找到对应的 文本段 (选取 top 20,当然多少都可以自定义) 相关性筛选 :人和香蕉的DNA都有50%以上的相似度,所以我们要筛选掉相关的数据库,余弦相似度的取值范围是 [-1, 1], 越接近 1 相似度越高 ,反之越低,这里我们就要设置一个阈值(我设置的为0.77),抛弃掉相似度低的数据 rerank :就是把你的 问题 和从向量数据库得到的 文本段 对比,把最先关的文档排前面(检索召回 top 20,但真正有价值的可能只有其中 3-5 条,通过 rerank 可以让上下文质量更高,回答更准,还省 token) prompt优化(文本段+问题) :这里就是根据你的需求优化关键词了,比如你是金融公司要精切的答案,就prompt添加 “参考文档(文本段) 严格基于参考文档回答,不要使用你自己的知识 回答下面问题(问题)” 等等 LLM回答 :目前市面上的AI相关功能,什么Agent、CLI、Cursor等等都是基于LLM来实现的,RAG也不例外 RAG 代码实践 代码已经全部开源,并且代码里面有很多的注解,这里就讲重要的代码片段 一、配置 LangChain4j + Embedding 模型 + 向量数据库:LangChain4jConfig.java LangChain4j、Embedding :选用支持 Openai API 的模型,直接替换配置就可以了 Reranker :重定向这个模型,langchain没有支持 Openai API ,所以后续我们根据模型平台的接口文档去手搓一个使用,当然 LangChain4j、Embedding 这些都能手搓,但别人已经把轮子创建好了,就不要再重复造了 向量数据库 :这里我放了三套配置供大家筛选,只要合适自己的需求就好,没必要什么最好就上什么,成本摆在那的,但生产不要用 InMemory ,就丢失数据这一条就是不能接受的 1、 InMemory :纯内存单机库,好处是不依赖第三方组件,坏处是程序退出即丢失数据(生产不要用) 2、 Chroma :嵌入式本地库,零配置,部署极简,支持百万级别向量存储,单节点架构 3、 Milvus :分布式云原生库,高并发、低延迟,支持十亿级向量,分布式架构,支持水平扩展与分片 /** * LangChain4j 配置 */ @Configuration public class LangChain4jConfig { @Value("${langchain4j.open-ai.chat-model.api-key}") private String chatApiKey; @Value("${langchain4j.open-ai.chat-model.base-url}") private String chatBaseUrl; @Value("${langchain4j.open-ai.chat-model.model-name}") private String chatModelName; @Value("${langchain4j.open-ai.chat-model.temperature}") private Double temperature; @Value("${langchain4j.open-ai.embedding-model.api-key}") private String embeddingApiKey; @Value("${langchain4j.open-ai.embedding-model.base-url}") private String embeddingBaseUrl; @Value("${langchain4j.open-ai.embedding-model.model-name}") private String embeddingModelName; // ── InMemory (可以自己玩,生产不要用) ── // 没有配置的时候默认使用 InMemory 但生产环境不建议用这个配置,可以删掉,开发环境可以自己玩 @Bean @ConditionalOnProperty(name = "vector.store.type", havingValue = "in-memory", matchIfMissing = true) public EmbeddingStore<TextSegment> inMemoryEmbeddingStore() { return new InMemoryEmbeddingStore<>(); } // ── Chroma (中小型项目首选) ── @Value("${chroma.host:localhost}") private String chromaHost; @Value("${chroma.port:8000}") private Integer chromaPort; @Value("${chroma.collection-name:rag_study_helper}") private String chromaCollectionName; @Bean @ConditionalOnProperty(name = "vector.store.type", havingValue = "chroma") @Lazy public EmbeddingStore<TextSegment> chromaEmbeddingStore() { return ChromaEmbeddingStore.builder() .baseUrl("http://" + chromaHost + ":" + chromaPort) .collectionName(chromaCollectionName) .build(); } // ── Milvus (大型项目首选) ── @Value("${milvus.host:localhost}") private String milvusHost; @Value("${milvus.port:19530}") private Integer milvusPort; @Value("${milvus.collection-name:rag_study_helper}") private String milvusCollectionName; @Value("${milvus.dimension:2048}") private Integer milvusDimension; @Bean @ConditionalOnProperty(name = "vector.store.type", havingValue = "milvus") @Lazy public MilvusEmbeddingStore milvusEmbeddingStore() { return MilvusEmbeddingStore.builder() .host(milvusHost) .port(milvusPort) .collectionName(milvusCollectionName) .dimension(milvusDimension) .build(); } // LLM模型选择(要选择适配 OpenAI API 的模型) @Bean public OpenAiChatModel chatModel() { return OpenAiChatModel.builder() .apiKey(chatApiKey) .baseUrl(chatBaseUrl) .modelName(chatModelName) .temperature(temperature) .timeout(Duration.ofSeconds(60)) // 本地的计数器,用来知道当前对话有多长,跟模型实际输出无关 .tokenizer(new OpenAiTokenizer()) .build(); } // LLM流式模型(要选择适配 OpenAI API 的模型) @Bean public OpenAiStreamingChatModel streamingChatModel() { return OpenAiStreamingChatModel.builder() .apiKey(chatApiKey) .baseUrl(chatBaseUrl) .modelName(chatModelName) .temperature(temperature) .timeout(Duration.ofSeconds(60)) .tokenizer(new OpenAiTokenizer()) .build(); } // 向量嵌入模型(要选择适配 OpenAI API 的模型) @Bean public OpenAiEmbeddingModel embeddingModel() { return OpenAiEmbeddingModel.builder() .apiKey(embeddingApiKey) .baseUrl(embeddingBaseUrl) .modelName(embeddingModelName) .timeout(Duration.ofSeconds(60)) .build(); } } 二、入库流程:文件解析->分割器->Embedding->向量数据库(入库) 1、文件判重 + 解析文件 + 入库: DocumentIngestionService.java public DocumentInfo ingestDocument(String fileName, InputStream inputStream) throws IOException { // 通过 文件流 + hash 来判断文件是否重复 byte[] content = IOUtils.toByteArray(inputStream); String hash = sha256(content); log.info("Ingesting document: {}, hash={}", fileName, hash); // 检查文件是否已经入库 Documents existing = documentsMapper.selectOne( Wrappers.<Documents>lambdaQuery().eq(Documents::getContentHash, hash) ); // 文档存在就返回 if (existing != null) { log.info("Document already ingested: {} (hash={})", existing.getDocumentName(), hash); // 这个类不用管,给前端返回的实体类而已,不重要 return new DocumentInfo(existing.getId(), existing.getDocumentName(), existing.getChunkCount()); } // 解析文档 把文件转为 Document 类(LangChain4j 包下面的文档类) // parseDocument 个是通用方法,根据文件后缀去解析,下面的 parseWord 方法就是 parseDocument 方法解析到 .docx 文件后进行解析,就看下面的 parseWord 方法即可,不看也行 Document document = parseDocument(fileName, new ByteArrayInputStream(content)); // 入库(向量数据库 + 关系型数据库) return processAndSave(document, fileName, "UPLOAD", hash, (long) content.length, null, null, null, "upload"); } 2、文件解析: DocumentIngestionService.java 这个不用过多关注,网上解析文档一搜一大堆 private Document parseWord(InputStream inputStream) throws IOException { StringBuilder text = new StringBuilder(); try (XWPFDocument doc = new XWPFDocument(inputStream)) { for (XWPFParagraph para : doc.getParagraphs()) { text.append(para.getText()).append("\n"); } for (XWPFTable table : doc.getTables()) { for (XWPFTableRow row : table.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { text.append(cell.getText()).append(" | "); } text.append("\n"); } text.append("\n"); } } return Document.from(text.toString()); } 3、入库:分割器、Embedding、向量数据库: DocumentIngestionService.java private DocumentInfo processAndSave(Document document, String fileName, String source, String contentHash, Long fileSize, String feishuNodeToken, String feishuObjType, Long feishuUpdateTime, String creator) throws IOException { // 按 token 分割 OpenAiChatModelName.GPT_3_5_TURBO 为默认值,不加这个也行,这里写进入只是展示 Tokenizer tokenizer = new OpenAiTokenizer(OpenAiChatModelName.GPT_3_5_TURBO); String prefix = "[来源:" + fileName + "]\n"; // 计算文件名占多少token,后面会讲为什么需要计算 int prefixTokenCount = tokenizer.estimateTokenCountInText(prefix); // 占用token比较高的文件名就记一下日志 if (prefixTokenCount > 200) { log.warn("文件名前缀占用 token 过多: {} tokens, fileName={}", prefixTokenCount, fileName); } // 我用的 Embeding 模型(bge-large-zh-v1.5)解析单条字符串的上限为 512 token (虽然 bge-large-zh-v1.5 上限低,但他在硅基流动上是免费的,而且能力也不错) // 如果切换模型后,那么向量数据库中记录的数据都不可用了,要注意哦 int maxSegmentSize = Math.max(50, 512 - prefixTokenCount); // 段落间重叠token数 int maxOverlap = 51; // 基于 token 的分割器,层级降级(官方推荐) // 整体分段 token 数 <= 512,段落间重叠token数 占整体的 10%-20% ,就是说文本段最少占 380 的 token 量(文件名比较小的情况下),就是说一个分片差不多 250 ~ 300 个汉字 DocumentSplitter splitter = DocumentSplitters.recursive( // maxSegmentSize: 每个分段最大token数 maxSegmentSize, // maxOverlap: 段落间重叠token数 maxOverlap, // separator 优先级 tokenizer ); // 分割器分隔文档 List<TextSegment> segments = splitter.split(document); // 给文本段添加 文件名 前缀 segments.replaceAll(textSegment -> TextSegment.from( prefix + textSegment.text())); List<Embedding> allEmbeddings = new ArrayList<>(); // 记录嵌入成功的文本段,保证与 allEmbeddings 一一对应,避免失败时错位 List<TextSegment> successSegments = new ArrayList<>(); // 一次 http 请求 10 条,避免反复建立连接增大开销 int batchSize = 10; for (int i = 0; i < segments.size(); i += batchSize) { int end = Math.min(i + batchSize, segments.size()); List<TextSegment> batch = segments.subList(i, end); try { List<Embedding> embeddings = embeddingModel.embedAll(batch).content(); allEmbeddings.addAll(embeddings); successSegments.addAll(batch); log.info(" Embedded batch {}-{}/{}", i, end, segments.size()); } catch (Exception e) { log.warn(" Batch {}-{} failed, trying one-by-one", i, end); for (TextSegment seg : batch) { try { allEmbeddings.add(embeddingModel.embed(seg.text()).content()); successSegments.add(seg); } catch (Exception e2) { log.warn(" Skipping chunk: {}", seg.text().substring(0, Math.min(50, seg.text().length()))); } } } } // 向量数据库添加数据,返回 向量 ID 用于关系型数据库保存 List<String> vectorIds = embeddingStore.addAll(allEmbeddings, successSegments); // 文档类型 String docType = "unknown"; int dotIdx = fileName.lastIndexOf('.'); if (dotIdx > 0) { docType = fileName.substring(dotIdx + 1).toLowerCase(); } // 文档入库 Documents docRecord = new Documents(); docRecord.setDocumentName(fileName); docRecord.setDocumentType(docType); docRecord.setSource(source); docRecord.setContentHash(contentHash); docRecord.setFileSize(fileSize != null ? fileSize : 0L); docRecord.setChunkCount(successSegments.size()); docRecord.setFeishuNodeToken(feishuNodeToken); docRecord.setFeishuObjType(feishuObjType); docRecord.setFeishuUpdateTime(feishuUpdateTime); docRecord.setCreator(creator); documentsMapper.insert(docRecord); // 向量分片入库 for (int i = 0; i < successSegments.size(); i++) { DocumentChunks chunk = new DocumentChunks(); chunk.setDocumentId(docRecord.getId()); chunk.setVectorId(vectorIds.get(i)); chunk.setChunkIndex(i); chunk.setChunkText(successSegments.get(i).text()); documentChunksMapper.insert(chunk); } log.info("Ingested {} with {} chunks, documentId={}", fileName, successSegments.size(), docRecord.getId()); return new DocumentInfo(docRecord.getId(), fileName, successSegments.size()); } 三、提问流程:问题->问题重写->Embedding->向量数据库(匹配出库)->相关性筛选->rerank->prompt优化(文本段+问题)->LLM回答 1、 RagQueryService.java 整个提问的主流程,我浓缩进了一个方法,除了 rerank 的实现(也会贴出来),可以直接打开我的项目在根据下文的提示来追踪,项目内没有文章详细 // sessionId:会话 ID,用来查找上下文,question 用户的问题 // callback:控制层返回也是用的 SSE 格式,所以需要通过 langchain4j 的匿名内部类来传消息 public void streamAnswer(String sessionId, String question, StreamingResponseHandler<AiMessage> callback) { // 获取历史上下文 List<ChatMessage> history = conversationStore.getHistory(sessionId); // 提问小于 5 个字,进行问题重写,为了使 RAG 检索更准确 // 比如 RAG 场景下 你通过 《如何学习JAVA》 这个文档去检索(如果不了解向量数据库和embedding就先别管,只看问题) // 你第一次问:"java 要学什么框架" // embedding 根据 "java 要学什么框架" 转成向量去检索到了相应的内容再放进 prompt 喂给 LLM // LLM 根据文档说:"springboot" // 你第二次问:"他有什么好处" // 我们可以一眼就看出这里的他指的是 springboot ,但是 embedding 模型不知道 // embedding 只是把你输入的 "他有什么好处" 转换成向量去向量数据库查询,所以查出来的根本就不是你想要的文档内容 // 这时 LLM 就不会根据文档去生成你想要的内容了 String searchQuery = queryRewriteService.rewrite(question, history, 5); if (!searchQuery.equals(question)) { log.info("Search query rewritten: \"{}\" → \"{}\"", question, searchQuery); } // embedding(向量嵌入模型)根据你的问题转换成向量 Embedding questionEmbedding = embeddingModel.embed(searchQuery).content(); // 查询 向量数据库 找出前 20 个最相似的向量 EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() .queryEmbedding(questionEmbedding) .maxResults(20) .build(); EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest); // 查出来的向量结果 List<EmbeddingMatch<TextSegment>> matches = searchResult.matches(); for (EmbeddingMatch<TextSegment> m : matches) { log.info(" score={} content={}", m.score(), m.embedded().text().substring(0, Math.min(80, m.embedded().text().length()))); } if (!matches.isEmpty()) { DoubleSummaryStatistics stats = matches.stream() .mapToDouble(EmbeddingMatch::score).summaryStatistics(); long above90 = matches.stream().filter(m -> m.score() >= 0.90).count(); long above80 = matches.stream().filter(m -> m.score() >= 0.80 && m.score() < 0.90).count(); long above70 = matches.stream().filter(m -> m.score() >= 0.70 && m.score() < 0.80).count(); long below70 = matches.stream().filter(m -> m.score() < 0.70).count(); log.info("Score distribution: max={} min={} avg={} | ≥0.90={} 0.80-0.89={} 0.70-0.79={} <0.70={}", stats.getMax(), stats.getMin(), stats.getAverage(), above90, above80, above70, below70); } // 这里的效果 = searchRequest.minScore(scoreThreshold) 但这个阈值跟模型文档问题都有关系,没有什么标准的值,所以为了方便后续调试和检验适合自己项目的阈值,这里有必要打印一下日志,并且追踪日志进行观测 // 设置一个阈值,低于这个阈值的向量被过滤掉 // 就是说你问 "java 是什么" 检索结果里是 "怎么做红烧肉" 这种跟 question 余弦相似度很低的 那么把这个检索丢给 LLM 有什么用呢 double threshold = scoreThreshold; List<TextSegment> relevant = matches.stream() .filter(m -> m.score() >= threshold) .map(EmbeddingMatch::embedded) .collect(Collectors.toList()); log.info("After filtering question={} score>={} chunks={}", question, threshold, relevant.size()); // 检索召回 top 20,但真正有价值的可能只有其中 3-5 条,rerank 就是把最有用的排到最前面 // 通过 rerank 找到最相关的 5 条(这里的 5 可以自定义) // 效果就是喂给 LLM 的上下文质量更高,回答更准,还省 token if (!relevant.isEmpty()) { relevant = rerankService.rerank(searchQuery, relevant, 5); } // 自定义 prompt 模板,如果检索结果为空,则使用普通对话模式 String prompt; if (relevant.isEmpty()) { log.info("No relevant docs found, using normal chat mode"); prompt = "你是一个智能助手。请回答用户的问题。\n\n" + "## 问题\n" + question; } else { String context = relevant.stream() .map(TextSegment::text) .collect(Collectors.joining("\n\n---\n\n")); prompt = "## 角色\n" + "你是一个基于内部文档的数据分析助手。\n\n" + "## 参考文档\n" + context + "\n\n" + "## 约束\n" + "- 回答时请标注信息来源,格式:根据 [来源:文件名] 的记载/显示...\n" + "- 严格基于参考文档回答,不要使用你自己的知识\n" + "- 如果参考文档中没有相关信息:\n" + " - 完全不相关:回复\"根据文档内容,没有找到相关信息\"\n" + " - 部分相关:说明文档中涉及了什么,明确指出未涉及的部分\n" + "- 回答时引用具体的行/数据来支撑你的结论\n" + "- 用中文回答\n\n" + "## 问题\n" + question; } // 使用 langChain 调用适配 OpenAI API 的模型生成答案 StringBuilder fullAnswer = new StringBuilder(); streamingChatModel.generate(prompt, new StreamingResponseHandler<AiMessage>() { @Override public void onNext(String token) { fullAnswer.append(token); callback.onNext(token); } @Override public void onComplete(Response<AiMessage> response) { TokenUsage usage = response.tokenUsage(); if (usage != null) { log.info("Token 用量 - 输入: {}, 输出: {}, 总和: {}", usage.inputTokenCount(), usage.outputTokenCount(), usage.totalTokenCount()); } // 储存上下文 conversationStore.addTurn(sessionId, question, fullAnswer.toString()); callback.onComplete(response); } @Override public void onError(Throwable error) { callback.onError(error); } }); } 2、 RerankService.java 这个也没什么讲的,只是按照官方api请求并获取罢了: 创建重排序请求 /** * 重排序 * 就是把你查的内容和从向量数据库得到的文档分片对比,把最先关的文档排前面 */ public List<TextSegment> rerank(String query, List<TextSegment> documents, int topN) { if (documents.isEmpty()) { return documents; } Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", modelName); requestBody.put("query", query); requestBody.put("documents", documents.stream() .map(TextSegment::text) .collect(Collectors.toList())); requestBody.put("top_n", topN); MediaType JSON = MediaType.parse("application/json; charset=utf-8"); try { String json = objectMapper.writeValueAsString(requestBody); String url = baseUrl + "/rerank"; Request request = new Request.Builder() .url(url) .post(RequestBody.create(json, JSON)) .header("Authorization", "Bearer " + apiKey) .build(); log.info("Calling SiliconFlow Rerank: {} documents, query=\"{}\"", documents.size(), truncate(query, 50)); try (Response response = httpClient.newCall(request).execute()) { String body = response.body() != null ? response.body().string() : ""; if (response.isSuccessful() && !body.isEmpty()) { return parseAndReorder(body, documents); } } } catch (Exception e) { log.warn("Rerank API call failed, falling back to original order: {}", e.getMessage()); } return documents; } 测试 RAG 项目 java环境:只需要填入 app.rag.chat-api-key 和 app.rag.embedding-api-key 即可运行 docker环境: .env.example 文件去掉 .example 填入 APP_RAG_CHAT_API_KEY 和 APP_RAG_EMBEDDING_API_KEY 运行命令 docker compose up -d 即可,要使用 Chroma 或者 Milvus 请运行对应的 docker compose 文件 一、乱生成一个不存在的文档 二、上传文档走入库流程 三、提问 四、测试完成,撒花 向量、模型、分割器选择 大语言模型的选择支持 OpenAI API 的模型就行了,项目中用的就是 deepseek 官方的模型,关于 LLM 的选择区别我就不讲了,什么Qwen、MIMO、MiniMax、GLM啥的都可以 一、向量数据库 我的项目实现了前三个数据库的配置,需要注意的是使用Milvus的时候需要填写向量维度 milvus.dimension: 1024 这个值需要和 embeding 嵌入模型 的维度所匹配(维度的意思下文介绍 embeding 模型选择 有写) 数据库 类型 适合场景 LangChain4j In-Memory 索引库 / 内存库 Java 技术栈的 RAG 原型验证;中小型项目中追求极致低延迟的检索场景。(生产不要用) Chroma 开源 个人开发者、原型验证(PoC)、小型项目 Milvus 开源 超大规模、企业级生产系统;有专业运维团队 Pinecone 商业 追求极简运维、快速上线的商业项目 Qdrant 开源 高并发、低延迟检索;自托管且注重性价比 Weaviate 开源 知识库、智能问答等需结合语义与结构化关系 Pgvector 扩展 数据已在PG的轻量级RAG或MVP项目 Elasticsearch 混合引擎 已有ES技术栈,需要“文本+向量”一体化搜索 腾讯云VectorDB 商业 企业级RAG、智能客服;尤其适合腾讯云生态的企业 二、Embedding(文本嵌入模型) 维度 :根据输入得到的信息密度,比如(只是个例子,不是真实模型生成的):“我是MT” 转成 1维 的向量就是 [0.233] 二维就是 [0.520,0.666] 通常维度越高,信息容量越大,查找的内容就更精确 最大上下文 :就是把文本处理成向量最大的 token 数量,超出后一般操作就是截断,造成的结构就是你文章 800 字 最大上下文 500 字,截取了前 500 字转成向量,前面500字没有 java 相关内容,但后面 300 字有,这样你查 java 相关内容时,就截取不到这个分片了(上下文是指的字符转成的token数,这里为了方便大家理解我换成汉字来解说) 我项目用的是 BAAI/bge-large-zh-v1.5 1024维 512最大token ,嵌入模型不能随意切换,切换模型后向量输出都不一样了,那么从向量数据库查的东西都是乱的,数据就报废了 模型 厂商 维度 最大上下文 中文语义能力 部署成本 适用场景 Youtu-Embedding 腾讯优图 ~2048 ~512-8k C-MTEB榜首 (77.46) 开源 , 2B参数 企业级RAG,高精度通用任务 Qwen3-Embedding (2B/8B) 阿里Qwen 2048 ~8k-32k 专为表征设计,同尺寸SOTA 开源 先进LLM-Backbone向量化 BAAI/bge-large-zh-v1.5 智源研究院 1024 512 tokens C-MTEB榜首,中文RAG领先 开源免费 通用中文语义搜索/问答 GLM-Embedding 智谱AI 1024 8k tokens 中文RAG召回率领先 (83.5%) 商业API (~0.5元/1M tokens) 商业应用,极致中文精度 Conan-Embedding-V2 腾讯 ~1536 32k tokens MTEB中英SOTA,支持跨语言检索 开源 , 1.4B参数 长文档处理,中英混合场景 BAAI/bge-small-zh-v1.5 智源研究院 512 512 tokens 轻量高效 开源免费 边缘设备,对延迟敏感的应用 M3E-large Moka AI 1024 512 tokens 中文社区积累深厚 开源免费 社区支持好,快速原型验证 text-embedding-3-large OpenAI 3072 8k tokens 对非英文内容表现一般 商业API (~$0.13/1M tokens) 国际化应用,生态完善 三、Reranker(重排序模型) 我项目用的是 BAAI/bge-reranker-v2-m3 这个其实没啥好讲的,就是把问题和文本切片让模型排个序,随时都可以切换模型,需要注意的就是这个最大上下文,但一般来说重排序的模型都比嵌入模型大 模型 最大上下文 开发方 / 类型 适合场景 BAAI/bge-reranker-v2-m3 8192 tokens 智源研究院 / 开源 各类RAG与搜索场景的“万金油”首选,尤其是中文环境和需要自托管、注重性价比和响应速度的应用 BAAI/bge-reranker-v2-m3-4B 8192 tokens 智源研究院 / 开源 对排序精度有极致要求,且具备较强算力资源的专业RAG系统 Qwen3-Reranker-8B 32768 tokens 阿里通义千问 / 开源 多语言、跨语种或代码检索,预算充足、追求世界级顶级精度的应用 Cohere Rerank (v4) 32768 tokens Cohere / 商业API 追求极致便捷性和性能,不想投入运维精力的团队或有明确预算的商业项目 Jina Reranker v3 131072 tokens Jina AI / 开源(非商业) 需要处理超长文档(如整本书、长对话)的非商业研究或多模态RAG原型 Voyage rerank-2.5 32768 tokens Voyage AI / 商业API 企业级高精度搜索,特别是对召回质量要求苛刻、且预算充足的项目 mxbai-rerank-large-v2 未明确(通常≥512) mixedbread-ai / 开源+API 注重开源商业许可的稳定性,希望保留自托管或API两种灵活方案的项目 Zerank 2 8192 tokens (推测) Agentset / 商业API 毫秒级响应场景(如实时聊天检索),且能接受牺牲一点精度 MiniLM / DistilBERT-based 512 tokens (典型) 微软等 / 开源 资源极度受限的环境,如边缘设备或小型服务器 四、分割器(Document Splitters) 分割器这个就用我项目里的就行了,参数可以自己调一调 分割器 核心思路 主要适用场景 DocumentSplitters.recursive (python中是RecursiveCharacterTextSplitter,这两个是一样的) 递归尝试不同分隔符 (段落→换行→句子→单词→字符),直到块大小符合要求 。 通用首选 。TXT/Word/PDF等 任意格式文本的首选 。 DocumentByParagraphSplitter 按 段落 切分(连续两个或以上换行符 \n\n ) 。 网页、博客、Markdown格式规整,结构清晰的内容。 DocumentBySentenceSplitter 利用NLP库检测 句子边界 。 新闻、小说、聊天记录等自然语言文本,对语义连贯性要求高的任务。 DocumentByWordSplitter 按 单词 切分(至少一个空格 " " ) 。 英文文本、数据清洗、长单词序列。 DocumentByCharacterSplitter 按 字符 暴力切分 。 作为其他分割器的 底层保底策略 ;资源极度受限的环境。 DocumentByLineSplitter 按 换行符 \n 切分 。 日志文件、CSV数据、代码行等每行独立成块的内容。 DocumentByRegexSplitter 按 自定义正则表达式 切分 。 按特定模式(如日期、章节号、XML标签、JSON块等)分割的复杂文档。 后话 我做这个项目的目的是为了方便后续再有 RAG 的需求时可以直接复用代码,所以我做了一个通用的企业级 RAG 的案例,但既然是通用的,所以很多需要定制化的需求我没有加上,看完整篇文章后,需要定制化需求和细节优化,可以看这里 一、Embedding模型选择:选择维数多且支持最大上下文大的模型 二、Reranker模型选择:选择支持最大上下文大的模型 三、权限管控:在项目中搜 todo 权限 有注释的代码用 1、使用RBAC模型,文档分配给角色,角色分配给用户 2、入库流程:公用文档存入 Metadata 字段 public ,私有文档 Metadata 字段添加关系型数据库的文档 ID 3、提问流程:用户搜索时查询到自己权限的文档 ID,查询向量数据库时添加 Metadata 过滤字段(public、文档) 四、向量数据库Milvus: 1、注意dimension字段要和embeding维度对应 2、可以使用混合搜索,通过 稠密向量 + 稀疏向量 进行匹配得到结果更精准 五、上下文入关系型库: 1、redis过期事件监听(整条存,由于我的项目redis存上下文只存最新的10条对话,所以如果要用这个方案得自己改一下) 2、每次LLM生成完之后异步添加(单条存,通过sessionId+userId查询)(推荐) 六、文件解析:通过 OCR 将扫描版 PDF 转为文本(图片同理) 10 个帖子 - 10 位参与者 阅读完整话题
视频在此网盘,可在线播放 storage.to 关公面前耍大刀.mp4 - storage.to Send files up to 25GB. No signup. No intrusive ads. No tracking. Fast, private file sharing powered by Cloudflare. 就随便说几句话就生成了凑合能看的 ,真厉害,gptai啥时候能搞视频阿? 4 个帖子 - 3 位参与者 阅读完整话题
求助,有没有订阅式的embedding模型,或者便宜的按量付费的 如题,我们公司部门内搭建RAG,需要用到embedding 和reranker ,但是我看了阿里和字节,都是按量付费的,我也不知道目前按量付费扛得住不。目前是20人左右使用。我之前用用自己的免费的额度,但是禁不住几次就没了。像广大的大佬们,求助 有么有订阅式的或者便宜的靠谱的向量模型推荐,麻烦各位不吝赐教。 12 个帖子 - 11 位参与者 阅读完整话题
现在有的Java岗位,要求掌握rag、react、function calling、prompt engineering 还有必要面试传统Java岗吗(只会AICoding,不要求agent开发相关技术) 5 个帖子 - 4 位参与者 阅读完整话题
用户提出的问题有很多多余内容,在做向量检索的时候很影响召回效率,请问一下各位大佬在把问题丢给知识库前怎么对问题进行转写?是直接扔给大模型让它把问题转成需要了解的知识点还是有什么更好的方法? 2 个帖子 - 2 位参与者 阅读完整话题
配置如下: Memory:7777 MB CPU:4 Core(s) Storage:47 GB Traffic:2447 GB 适当溢价,有出的佬请留言
需要做一个 jsa工作安全风险分析助手 的rag 已经有的材料: 数百份 jsa工作安全风险分析.xsxl 的excel文件 需要达成的目的: 可以稳定的询问出已经存在的文档内容 当询问内容不存在时需要根据已有内容给出建议 需要按模板将建议再转换为 excel 文件, 然后再回到知识库中 需要根据用户会话区分, 不同用户有自己的上下文隔离 目前方案和打算: 使用dify的chatflow 计划流程为: 用户询问->意图识别->问题整理->rag检索->大模型总结->格式化输出 知识库使用父子分块 疑问: 父子切块如何切召回更合理? 目前按自己的理解写了脚本切块, 但是感觉怪怪的, 有没有佬救一下. 这是excel文件截图, 我将人名马赛克了 (点击了解更多详细信息) 这是我做的父子切块md文件, 大概思路就是按步骤切子分块 (点击了解更多详细信息) 这是dify中知识库内显示的切块 (点击了解更多详细信息) 是继续选择dify还是别的技术栈, 似乎chatflow的边界很难控制, 控制多了像人机, 控制少了不稳定. 这种rag怎么做基准测试? 有没有推荐的测试方案哇 计划流程是对的吗? 我也是到处找资料搞出来的 其实意图识别和问题整理我想要加入大模型来介入, 但是大模型如何多轮重入和工作流怎么画一点头绪没有 2 个帖子 - 2 位参与者 阅读完整话题
IT之家 6 月 4 日消息,Paraglacial 开发、THQ Nordic 发行的第一人称动作角色扮演游戏《Fatekeeper》现已开启抢先体验, 本作在 Steam 国区定价为 58 元,首发 8 折后 46.4 元 ,IT之家附 Steam 商品页( https://store.steampowered.com/app/2186990/Fatekeeper/ ),截至发稿,游戏好评率 75%“多半好评”。 据介绍,本作以紧凑的线性叙事作为主线,同时也为探索玩家探索世界留足了空间,设计了各个古战场、地下洞窟、森林、崩毁圣所等各种场景供玩家探索。 同时,游戏提供了多种变化多端、独一无二的深度成长机制,玩家需要在战斗流派、基础属性到法术学派之间做出各种重要的取舍,来精心培养自己的角色。 游戏图赏:
IT之家 6 月 3 日消息,在 5 月 30 日于深圳举办的瑞莎 & 高通联合开发者活动日上,瑞莎(Radxa)正式展出了多款基于高通平台的新品,还展示了与飞牛联合打造的 NAS 存储设备。 其中,最受关注的莫过于首次公开亮相的 Dragon Q8B 高性能迷你主板。这款产品搭载了高通骁龙 8cx Gen3 计算平台,配备双 2.5GbE 网口 + 双 NVMe 扩展。 这款产品目前已经在官网开启预购,4GB 型号首发价 799 元,8GB 型号 1199 元,16GB 型号 2099 元,32GB 型号 3999 元,7 月 31 日起上市。 Dragon Q8B 采用 100mm×75mm 的紧凑版型设计,搭载了 5nm 制程的高通骁龙 8cx Gen3(SC8280XP),配备八核 Kryo CPU,包含 4 个最高 3.0 GHz 的大核和 4 个最高 2.4GHz 的小核,集成 Adreno 690 GPU,AI 算力最高达 29 TOPS 以上。图形方面支持最高 4K@120 视频解码和 4K@60 视频编码,内存最高可配置 32GB LPDDR4X,总线位宽 128 位,数据传输速率达 4266 MT/s。 扩展性是这款主板的一大亮点。Dragon Q8B 配备了两个 M Key 的 M.2 2280 插槽,分别支持 PCIe 3.0 x4 和 PCIe 3.0 x2,可直接接入两块 NVMe SSD。存储方面,它还提供了 UFS 3.1 模块接口和 microSD 卡槽。 网络配置上,Dragon Q8B 板载双 2.5GbE RJ45 以太网口,并预留了 E Key 的 M.2 2230 插槽用于 Wi-Fi / 蓝牙模组扩展(高配还可选 Wi-Fi 7 模组)。 接口方面,它提供了两个 USB 3.2 Gen 2-C 接口(支持 DP 1.4b Alt Mode,可实现 4K@120 显示输出)、两个 USB 3.2 Gen 2-A 接口、两个 USB 2.0-A 接口,以及标准的 HDMI 2.1 接口。 此外,它还保留了 40 针彩色 GPIO 排针,支持 UART、I2C、SPI 等常用协议,以及 PCIe FPC 连接器、音频接口、RTC 电池接口、风扇接口等开发调试所需的接口。供电方面采用 USB-C PD 接口,官方建议使用 60W 电源。 操作系统方面,除了支持瑞莎官方 Radxa OS、Ubuntu、Armbian、Arch Linux 等 Linux 发行版外,由于骁龙 8cx Gen3 本身为 Windows PC 设计,该主板也支持 Windows 11 on Arm。 在本次开发者活动日上,瑞莎同时展示了另一款单板计算机 Dragon Q5E,第四季度上市。 该产品采用 Dragonwing QCS6690 八核 Kryo SoC,同样为 4nm 制程工艺,AI 算力约为 6 TOPS,最高支持 16GB LPDDR5 内存,配备双 2.5GbE RJ45 网口(其中一个可选 PoE 供电),板型更为小巧,尺寸仅 65mm×56mm,支持基于 Debian 的 Radxa OS 和 Ubuntu 操作系统。 ▲ 飞牛 NAS 瑞莎还预告了两款与飞牛合作开发的高通平台 NAS 系统,均支持飞牛 fnOS 系统,但技术细节仍未公布: DragonStation 定位高性能全闪存存储,提供 6 个 M.2 NVMe SSD 盘位,支持 10GbE 网络,据称可配合内置 AI 加速卡本地运行最高 120B 参数的大语言模型,10 月上市; DragonBay 则是一款面向主流用户的 4 盘位 NAS,面向大容量存储、媒体库备份及多用户文件协作等场景,9 月上市。 IT之家注意到,在展会现场,深度操作系统(deepin)也展示了其旗舰开发板 Dragon Q8B 的系统适配成果。据介绍,深度开发者仅用一个下午就完成了 deepin 25.1 对 Dragon Q8B 的系统启动与初步适配。 瑞莎在此次开发者活动日上还预告了 2026 年的整体产品规划。根据 CNX Software 报道,瑞莎总计将推出 22 款基于高通平台的硬件产品,涵盖迷你主板、核心板、Cluster 计算集群等多种形态。 与此同时,瑞莎还在现场展示了灵控远程控制器(Radxa Linkr)、灵控智能体调试器等配套工具,旨在为开发者和行业客户提供从计算、连接到远程管理与调试的一体化平台能力。 相关阅读: 《 瑞莎预热高通平台飞牛 NAS:6 盘位全闪高性能 / 4 盘位主流双选择 》
最近和学校打交道,博士生说他们的很多横向,实际上发现微调后的效果用户根本感知不出来,只能通过一些数据集测试准确率反应效果,实际体验还不如挂个知识库RAG来的快又好。在开源大模型能力愈发强大的当下,微调还有价值吗? 2 个帖子 - 2 位参与者 阅读完整话题
希望是带有RAG、langchain(langgraph)等技术栈和知识点的开源项目 想学习一下,感谢各位佬友 1 个帖子 - 1 位参与者 阅读完整话题
本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 大家好,我最近开源了一个项目:RedForge AI。 RedForge AI 是一个面向 LLM 应用、RAG 系统、AI Agent、工具调用和 Memory 的红队评估框架。 我做它的原因是:现在很多 LLM 安全测试还停留在手搓 jailbreak prompt 的阶段,测试结果很难复现,也很难交给开发修复。RedForge 想解决的是"可授权、可限定范围、可记录证据、可复现、可生成报告"的问题。 它不是 C2,不是通用 Web 扫描器,也不是所谓"一键证明模型安全"的工具。它更像是一个面向 AI 应用安全评估的 campaign runner。 git clone https://github.com/Aimer-zero/redforge-ai.git cd redforge-ai make setup make demo open "$(uv run redforge latest-report --path-only --format html)" 目前支持的方向包括: prompt injection / jailbreak 基础用例 RAG / tool use / memory / agent trust boundary 的评估框架 local demo target custom HTTP target OpenAI-compatible API target CLI 和 FastAPI service mode public plugin interface / attack pack / target adapter 我希望它不是一个"payload 仓库",而是一个更偏工程化的 LLM 安全评估工具:可以在本地跑、可以接进 CI、可以把结果交给开发和安全团队复盘。 项目还在 early preview,API / schema / CLI 都可能继续变化。目前最希望大家帮忙的是: 跑一下 demo,看看安装和报告体验有没有问题; 反馈你们常见的 LLM / RAG / Agent API 形态; 贡献安全、通用、可公开的 baseline attack cases; 帮忙改进 HTML report 和 CI 集成; 对架构和边界设计拍砖。 项目地址: github.com GitHub - Aimer-zero/redforge-ai: Open-core AI red teaming and offensive AI security... Open-core AI red teaming and offensive AI security evaluation platform. 欢迎试用、提 issue、star 或直接吐槽。 为什么有这个想法? 最近几年AI爆发式的增长 各种各样的模型和Agent应用 安全方面就体现不是很好 于是就有了这个想法。 另外项目现在有一部分实验性内容还没有放进公开仓库,主要原因是还没整理干净,里面有不少和具体场景、内部测试环境绑定的东西。 我的想法是:公开仓库先把 open-core 做扎实 第一次开源 欢迎大家提出建议 也欢迎大佬批评指教 1 个帖子 - 1 位参与者 阅读完整话题
IT之家 6 月 1 日消息,Qualcomm(高通)总裁兼首席执行官 Cristiano Amon(克里斯蒂亚诺 · 安蒙)今日在台北发布其 COMPUTEX 2026 主题演讲, 宣布推出数据中心品牌 Dragonfly ,更多细节将在企业本月 24 日的年度投资者日中揭晓。 Dragonfly 预计将包含高通的数据中心 CPU、AI ASIC 产品 ,与外部伙伴合作的芯片设计服务项目应该也会算在其中。而 Dragonfly 将与客户端的 Snapdragon(骁龙)、AIoT 的 Dragonwing(跃龙)一道形成高通新世代的品牌组合。 安蒙在其演讲中表示,到 2030 年 AI Token(词元)需求将达到 401.48×10 16 ;智能体将随用户而移动。
本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 最近在学习rag相关的知识,系统性的梳理了几篇文章,想着做个项目实践一下。正好在公司负责了一堆后台项目,就想着能不能搭个知识库平台,把这些后台系统的文档都作为一个知识库管理起来,省得每个项目查个什么东西还得去翻各种文档。过程中正好可以学习下rag相关的切分策略和检索召回逻辑。 所以,下面的这个项目就诞生了。 项目地址 github.com GitHub - helloHupc/rag-knowledge-system: 通用知识库管理平台,用于自建 RAG 知识库场景。支持多种文档解析、灵活切分策略、混合检索,可对接... 通用知识库管理平台,用于自建 RAG 知识库场景。支持多种文档解析、灵活切分策略、混合检索,可对接 Dify、自建页面、HTTP 调用等多种前端入口。 项目定位 这个项目的愿景是可以用于企业内部自建知识库,但是企业内部的具体情况各不相同,所以这个项目主要做的事情就是以不同知识库管理各种文档,包括PDF,word,Excel,图片等。然后支持多种的切分策略,包括固定长度切分、按结构切分、表格感知切分、父子分层切分、语义切分、聊天记录切分。 重点讲讲聊天记录和表格切分。 聊天记录策略是专门为公司内部沟通场景做的,因为平时会经常和业务人员沟通各种功能和数据的修改和检查,有时候找问题就需要翻之前的聊天记录,就想着把这个也做成知识库,到时候都直接放到知识库里直接就搜出来了。 核心就是把聊天记录按话题切分,通过正则识别消息边界(说话人 + 时间戳),解析每条消息的发言者、时间、内容。 按时间间隔(默认 60 分钟)自动识别话题切换,同一个话题的消息尽量放在一个 chunk 里。 格式化后统一 embedding,保证检索时能按时间、参与者、话题精准命中。 表格切分就是专门处理 Excel、CSV 这类结构化表格文档。 这核心逻辑是按行分批切分:表格数据按 table_rows_per_chunk (默认 20 行)和 max_chars (默认 1200 字符)两个维度控制,哪个先触发就切一批。 每个 chunk 带表头:每批数据前面都会重复表头行,保证 chunk 独立可读,不用依赖上一个 chunk 的表头。 这里还特殊处理了一种FAQ类型的表格,而且有多级合并的情况。 如果检测到表头包含"一级/二级/三级(/四级)"这种层级分类字段,会走专门的 FAQ 渲染模式。 这个是按分类层级分组,同组的行尽量放在一个 chunk 里。渲染成 问题:xxx、一级分类:xxx、答案:xxx 的结构化文本,比原始表格更适合 LLM 理解和检索命中。 处理完每个chunk就类似下面这样: 后台管理页面 下面是项目后台管理页面的部分截图。 切分配置页面可以实施预览各种文档或者自定义文字,在不同切分策略下的效果。 项目里的文档上传和解析切分都归结为“任务”。任务都是异步处理 图片识别 图片的识别是用OCR和多模态大模型的混合方案。 OCR目前支持百度的paddleOCR(每天有2000的额度)。如果配置了多模态LLM ,会用LLM生成一个图片的语义描述,涵盖图表趋势、流程步骤、关键文字等。然后和OCR识别的文字一起作为文字内容embedding到向量数据库中。 所有的元数据和chunk后的数据都会在postgresql中存一份。如果查询的时候命中了某个chunk,则会关联到某个文档信息,如果是图片则会直接返回一个图片的URL,前端可以直接展示出来。 检索 这是一个检索测试页面,可以选择不同的知识库进行测试,并且支持纯向量检索和混合检索(就是融合了BM25的多路召回),也支持 Rerank 二次排序(接入外部 Rerank 模型)。 还支持把不同的知识库一键生成可以用于dify的外部知识库。直接选择知识库然后配置到dify的外部知识库就可以了。 我自己试着把这套系统在公司的几个项目中跑了一下,简单搭个页面或者在dify中搭个智能客服然后再加上外部知识库,能在后台系统中给内部用户用于检索系统功能性的问题。 需要完善的地方: 一个是日志系统需要根据自己的需求完善一下,一般是推荐放到系统侧,能和系统的用户和信息融合到一起,这个平台可以单独作为知识库的服务。 第二个就是权限,这个系统目前没有任何权限设置。倒是预留了 PermissionChecker 权限插件,可以通过自定义接口来对接企业内部的权限,来控制知识库的权限。毕竟这些都是企业内部的核心资料。 我这边主要是自己负责好多公司后台项目,我都是用自己整理的一些文档和说明。所以没有做什么权限控制,如果是公司用权限还是要做更精细的控制。 以上就是项目的一个大概介绍,如果也有探索rag知识库落地场景的佬可以互相交流一下。 1 个帖子 - 1 位参与者 阅读完整话题
之前看见很多 codex 的学习教程,里面给的配置都有 disable_response_storage = true 这一项,我也一直都是这么配的。今天突发奇想准备去看看 codex 源码,发现这个配置项早在去年9月份就已经移除了,配了等于没配,有强迫症的佬友们可以去掉这一项。 具体的 Pull Request: Never store requests · Pull Request #3212 · openai/codex 3 个帖子 - 2 位参与者 阅读完整话题
求佬! 咱谁有华为存储题库HCIA-Storage H13-611 最近准备考一下。请问有没有已经考过的佬友分享一下最新版的H13-611题库呀,非常感谢! 1 个帖子 - 1 位参与者 阅读完整话题
想请教一下各位佬:工业生产企业内部知识库 / RAG 系统,应该怎么选型和落地? 目前我在尝试给自己公司搭建一套内部知识库,希望把过去积累的各种业务资料、产品资料、型号参数、性能数据、适用场景、报价/询价记录、历史项目经验等内容,更系统地沉淀下来,方便后续检索和复用。 核心目标主要有两个: 第一个是提高业务资料的检索效率 过去很多信息散落在excel、pdf、图片、聊天记录、邮件或者老员工经验里。新人入职后,想快速理解产品、型号、性能、适用范围,经常需要到处问人或者翻资料,效率比较低。 第二个是让ai能辅助回答业务问题 比如用户输入一个比较模糊的需求,ai能根据内部知识库里的产品资料、型号参数、历史案例等信息,帮忙推荐可能合适的产品方向、型号、供应商信息,或者提示需要进一步确认哪些参数。 我们公司本身是做工业生产相关业务的,所以数据特点大概是: 产品型号多; 参数、性能、适用范围比较重要; 很多资料是半结构化/非结构化的,比如 PDF、Excel、图片、说明书、报价单等; 有些信息需要长期沉淀,最好后续能持续补充、维护和更新; 希望新人也能通过自然语言查询快速上手业务。 目前我比较纠结的是技术方向和工具选型。 我看了一些传统RAG方案,比如Dify这类,也看到不少反馈说实际落地后查询准确率并不稳定,尤其是遇到工业产品这种参数型、型号型、强业务语境的数据时,可能会出现召回不准、答非所问、幻觉、引用不清楚等问题。 另外市面上也有一些云厂商方案,比如阿里百炼之类的,号称低代码搭建知识库、接入大模型比较简单。但我不太确定这类平台在真实业务场景里的效果如何,尤其是面对比较复杂的企业内部产品库和历史业务数据时,后续可控性、迁移成本、准确率优化空间怎么样。 顺带问个蠢问题,比如notebookllm、ima这种,虽然定位是个人知识库,但是拓展到企业会有什么问题吗? 感觉这个方向相比大模型本身热度低很多,但对企业实际业务应该挺有价值的。真心求教,感谢各位! 15 个帖子 - 11 位参与者 阅读完整话题