WWW.YOUINFO.SITE
标签聚合 申明

/tag/申明

LinuxDo 最新话题 · 2026-06-11 22:42:04+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 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 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-11 22:32:57+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 最近做了一个自己在用的 YouTube 下载插件,开源出来。 项目地址: github.com GitHub - fengjunda888/youtube-download-extension: YouTube 视频下载 Chrome 扩展,基于… YouTube 视频下载 Chrome 扩展,基于 yt-dlp,支持合集、多任务、画质选择和下载进度 这个插件的特点: 基于 yt-dlp Chrome 插件形式 支持 Windows 和 macOS 支持先解析链接,再选择下载 支持单个视频、合集、多选、全选 支持画质选择 支持查看下载进度和任务状态 目前的使用方式是: 在 Chrome 加载 extension 目录 本地安装 Native Host 配好 yt-dlp 在插件里粘贴 YouTube 链接,先解析,再选择下载 Windows 安装比较直接: Install-NativeHost.bat macOS 也支持: Install-NativeHost.sh 说明一下: 这个工具不会绕过 YouTube 权限限制,会员、私有、区域限制之类的视频还是取决于 yt-dlp 和账号/网络环境。 欢迎试用,也欢迎提 issue / PR。 希望能给个star! 谢谢! LINUX DO 版主 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-11 19:19:29+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 GitHub - Hittopu/okfa-one-keyboard-for-all: okfa - one keyboard for all · GitHub 可以实现mac/windows的键盘一键切换成另外一台windows的键盘,可以实现在工位偶尔在另一台电脑上输入nvidia-smi的方便操作,也间接的实现了(mac键盘打瓦) 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-11 10:20:14+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 CainFlow 项目背景 没找到比较好用的调用API进行生图的节点式的工具,很多在线的画布都不支持设置API,还有一些个人制作的软件感觉操作手感怪怪的 有一些是体积大,有一些是卡顿,索性使唤AI开发了一个。 项目简介 CainFlow 是一款受 ComfyUI 启发的轻量级节点式的调用API的绘图工具。前端用的js和css,后端用python 作用: 支持google和openai两种API格式的图片生成或者对话 有图片对比 良好的历史记录功能 有自动重试功能 完成后可以有音效提示 节点式操作 可以搭建属于自己的工作流 有一定的视频生成能力(测试的比较少) 好看的界面,粉色主题 本地保存工作流与历史数据,默认不依赖云端托管 内置工作流管理、媒体恢复、下载、更新和日志能力 提供 Windows 和macos两个版本 项目本身还有很多可以打磨和优化的空间,欢迎感兴趣的大佬一起探讨、指正。 项目开源地址: GitHub - RingoCaviar/CainFlow: 节点式简易本地API请求画图工具 · GitHub Readme已添加友链 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-11 09:45:28+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 不知道各位佬有没有听过这样一个概念 上班“ 摸鱼时间 ”换来的钱 才是“ 赚 ”的 剩余的全是等价交换。 于是我就想,怎么才能记录记录我“ 赚 ”的钱呢? Codex启动~ 经过多轮友好协商讨论,于是乎: GitHub - ChengX3/MoYuMei: MoYuMei 一款macOS上的摸鱼统计工具 每秒计算薪资 按照摸鱼/搬砖状态计算到底今天“赚”多少钱 · GitHub 展示区域: 看完图图后简单说一些有哪些设计在里面吧 1.想测试?但懒得设置摸鱼/搬砖软件? 简单!内置了一些常见的软件名单 可以在设置页面一键复用(会根据你本地电脑是否安装 才会真正导入) 2.主界面的FUCK是什么意思?你骂我? 真不是啊!这个FUCK按钮是加班模式的开启,毕竟谁加班不想说上一句FUCK!呢? 我甚至还贴心的加入了 无偿 模式 3.原理是什么?老东西都交出来! 交!原理就是监控台前应用是什么,然后记录时间。剩下的就是算账啦 ------------人格分割线----------------- 好了不皮了,做这个其实很简单都是codex在做,我只需要说出我的想法以及协助他测试。相信大家也可以在AI的浪潮下多多验证自己的想法,比较现在做一款软件/网页/app门槛很低,说不定你随手Vibe的项目就是某些人需要的呢? PS:本项目仅限macOS用户测试使用,并非多端项目,请见谅 ** 学AI,上L站!** 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 23:45:36+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 胖乖生活第三方安卓客户端 github.com GitHub - Night-stars-1/wash 通过在 GitHub 上创建帐户来为 Night-stars-1/wash 开发做出贡献。 因为胖乖生活APP的广告太多了,所以开发了一个第三方客户端 测试了洗衣机开源正常使用,其他设备理论上也可以通过扫码使用 功能 支持短信登录 纯净无广告 支持扫码查看设备 支持查看历史订单 截图 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 22:16:08+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 Github地址: https://github.com/ali156666/notchdeck 各位尊敬的Macbook机主们,你们的Macbook肯定都有一个不可消除的刘海,而且也没有灵动岛,显得这个刘海毫无用处,于是我就vide codeing了一个灵动岛。 功能如下: Agent助手:这是一个拥有长期记忆,skills,自进化,MCP的智能体,另外你可以从微信,桌面,访达中将文件拖到灵动岛中交给Agent处理 剪贴板:Mac系统并不像windows自带剪贴板,于是我就将它做到了岛里,支持文字,图片,文件。 音乐播放器:目前仅支持Apple Music,这个功能用过nookx的都知道是要付费的,所以我开源了,在悬屿最小化的时候会有歌词显示(国内的音乐平台没有暴露相关的端口,所以做不了) 快捷启动:你可以通过这个快捷启动你常用的App,这个功能nookx也是付费的 系统面板:这个你可以看清你的macbook 配置 内存占用 天气 日程 日历 以上就是这个项目的主要功能,如果对你有用的话,请帮忙转发和点一个star 2 个帖子 - 2 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 21:43:21+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 一个手机给电脑打字的小工具,使用 Rust 语言开发,AGPL 许可证。 github.com GitHub - WantenMN/remote-input: Type on your phone, paste into your desktop Type on your phone, paste into your desktop 程序启动后会在局域网内启动一个 HTTPS 服务,手机连上同一 WiFi 就能在浏览器里访问,会有一个输入框,输入完文字,点发送,内容就会自动粘贴到电脑当前光标所在的位置。 想法来源于一个月前(实际上很早之前就有这个想法,之前用 Python 做过一版,不太满意): 手机语音识别结果实时输出到电脑上 开发调优 豆包输入法的语音识别效果我认为是最好的,但现在豆包输入法目前只支持 Mac,没有 Windows 和 Linux。豆包 AI 的语音输入不太好用,并且需要电脑上有麦克风,不如手机灵敏。 所以我有了一个想法,开发两个配套的程序,一个在手机上,另一个在电脑上,它们通过局域网连接通信。 大概流程 手机端:一个输入框,默认激活输入法。启动语言识别功能,直接说话 电脑端:后台程序,接收到手机端发来的文… 此外,对于语音输入的看法,这个主题的观点和我的挺相近的,贴在这供参考: 强烈推荐语音输入,几乎可以告别打字了,无论是否Vibe Coding 搞七捻三 随着大模型的发展,现在语音输入非常成熟了,尤其在Mac端,Windows也没问题。 废话少说,直接说结论。如果不想花钱,就上豆包语音输入法,Mac端、iOS端和安卓端都有。 它的效果就是,识别率高,能给你正常的加上标点符号。中英文混合输入没问题,微信输入法就做不到这一点。 如果你更期待的是结构化的输入,也就是你输入一段零散的话,想到什么说什么,然后输入法把你说的话给你整合出来,基本上保持原意… 5 个帖子 - 3 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 20:57:05+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 多账号 / sms注册 / OTP接收 / 消息接收 目前大致这个效果吧,欢迎 PR / issue wa-app 开源地址 5 个帖子 - 4 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 20:24:09+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 / 我的开源项目完整开源,无未开源部分: 是 / 我的开源项目已链接认可 LINUX DO 社区: 是 / 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 / 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 / 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 此项目是一个手机微信配合agent的使用案例。通过安卓状态栏抓取微信消息,然后将消息转发到agent,agent回复后再通过手机打开对话框,使用自建输入法传输文字或者图片信息。这种方案的优势在于免hook的rpa,但是比传统的rpa更快更稳定。将微信设置里的回车发送打开,全程只需要一个输入框坐标即可完成信息发送。包括信息隔离,会话接续等功能就不一一介绍了。项目地址:GitHub - c1422113471-cpu/hermes-wechat-relay: Profile-local Hermes Agent platform plugin for WeChat relay bridges · GitHub 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 20:23:41+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 TLDR 上传论文PDF/BibTeX,自动提取并检测引用条目是否正确,是不是大模型生成的幻觉引用。 介绍 大模型在论文写作中的应用越来越广泛,但有很多论文投稿后因为引用条目中存在由大模型生成的幻觉引用而被直接拒稿。我曾经在投稿之前一条一条地手动搜索,检查自己的参考文献部分写得是否真实,这样机械重复的事情应该交给Agent来干。 所以我开发了ValiRef。只需要上传论文的PDF,ValiRef会自动提取里面的参考文献部分,并交给多个并行的Agent进行验证。不仅可以检测引用的论文是否存在,还可以验证论文实际上的内容和文中引用时的描述是否一致。 项目链接 github.com GitHub - Gianthard-cyh/ValiRef: detect hallucinated citations in academic papers. detect hallucinated citations in academic papers. 在线体验 http://valiref.com/ 演示截图 5 个帖子 - 3 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 19:57:22+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 Github sms注册/OTP/消息接收/多账号 大致这个效果吧,欢迎PR/issue,如果对你有用的话,帮我点个star吧~ 9 个帖子 - 8 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 17:16:45+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 项目介绍: 一个部署在Cloudflare Worker的定时邮件提醒应用,支持 Resend API 或 SMTP 邮箱发送邮件。由于最近弄了gg卡需要半年保号但是可能忘记就顺手用codex搓了一个这个。支持单次/每天/每周/每月提醒,并且加入重要提醒(需要确认才会关闭提醒否则一直重发) 项目地址: yuluo688/auto-send-email 图片预览: 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 16:44:52+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: (点击了解更多详细信息) 继上午话题 【claude Fable5 测试】速搓一个体积云效果,请打分 后,我想再优化一下 添加了丁达尔效应云层动态流动、柔和地面云影等优化 以上均为claude-fable5实现,只是在实际效果上,我自己加了略微的调整把控整体方向 整个项目也已开源,有兴趣佬友可自行拉源码玩 github.com GitHub - 123164867376464646/tyndall-clouds: Rust + wgpu 实时体积云渲染:纯着色器程序化生成,丁达尔光柱、云层动态翻腾、egui... Rust + wgpu 实时体积云渲染:纯着色器程序化生成,丁达尔光柱、云层动态翻腾、egui 实时调参 这次的结果我评价是 顶级 欢迎各位佬给出评价,最好是附有评价的理由 云层效果投票 夯 顶级 人上人 NPC 拉完了 点击以查看投票。 Fable5投票 夯 顶级 人上人 NPC 拉完了 点击以查看投票。 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 15:42:48+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 《我们走过的地图》是一款为情侣设计的旅行回忆微信小程序。作品以地图为核心载体,将旅行足迹、照片、时间线、行程规划与 AI 能力相结合,记录两个人共同走过的城市、留下的故事以及未来想去的地方。 用户不仅可以回顾过去的旅程,还能借助 AI 自动生成旅行总结、回忆文案和个性化旅行攻略,让旅行记忆从简单记录升级为更有温度的情感表达。 本作品在开发过程中使用 Unity2.ai API 进行创意构思、功能设计、代码开发与内容生成支持,探索 AI 技术在旅行记录与情感陪伴场景中的创新应用。 InfFlow/map-of-cn-mp: 微信小程序版中国足迹/情侣回忆地图,含 PHP + MySQL 后端示例 2 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 15:07:28+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 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 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 12:18:20+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 主要是通过 引用+@ 实现的 使用说明 访问任意 Discourse 论坛帖子 在 Boost 气泡旁会出现 回复图标 点击图标即可引用内容并 @用户回复 项目地址 github.com GitHub - chenhuawang-04/DiscourseReply: 回复boost的消息 回复boost的消息 14 个帖子 - 6 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 11:22:08+08:00 · tech

本帖使用社区公益推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的项目是免费使用的,无收费(变相收费、赞助)部分: 是 我的帖子已经打上 公益推广 标签: 是 我的项目属于个人项目,与公司或商业机构无关: 是 我的项目不存在QQ、TG等群组引流: 是 我的项目不存在非运营必要的网站引流: 是 我的项目不存在为他人推广、AFF: 是 我的项目无关联的商业项目: 是 我的站点存在登录,并已接入 LINUX DO Connect: 否 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 github.com GitHub - Leeero/melisle: 听见每一份热爱 听见每一份热爱 之前用音流播放音乐服的歌的时候,经常会遇到断流卡顿的问题,陆陆续续又试了棉花这些,总觉得少点意思,索性就自己vibe coding了一个播放器。 目前已经支持emby和navidrome,因为我没有其他音乐源,所以暂时没有适配其他源,如果有佬友希望能适配其他源的话,欢迎提供一个测试地址,我来适配。 目前已经真机测试通过:macOS和Android,iOS模拟器测试通过,因为没有Windows设备,所以暂时没有真机测试过。 因为项目是一个人vibe的,所以肯定还会有各种没发现的问题,欢迎大家在尝鲜的过程中反馈给我,或者直接在GitHub上提issue。 4 个帖子 - 3 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 09:55:29+08:00 · tech

本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 给ccs vibe coding了一个webui,已经fork了提了pr,方便佬友们自己局域网远程切换或编辑自己的ccs配置 我的fork: GitHub - szh1118/cc-switch: A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Gemini CLI & Hermes Agent. Only official website: ccswitch.io · GitHub 我的pr: feat: Add LAN WebUI support for remote browser control - Pull Request #3972 - farion1231/cc-switch - GitHub 有需要的佬友可以进我的fork拉取main分支,然后让cc或其他LLM帮忙部署,效果如下图(和官方ui没区别) 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 07:03:28+08:00 · tech

本帖使用社区公益推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的项目是免费使用的,无收费(变相收费、赞助)部分: 是 我的帖子已经打上 公益推广 标签: 是 我的项目属于个人项目,与公司或商业机构无关: 是 我的项目不存在QQ、TG等群组引流: 是 我的项目不存在非运营必要的网站引流: 是 我的项目不存在为他人推广、AFF: 是 我的项目无关联的商业项目: 是 我的站点存在登录,并已接入 LINUX DO Connect: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 先祝各位佬友高考金榜题名!CHY公益站开放无注册码注册一天,开始时间:现在 《壬寅夏赠高考诸生》 【现代】CHY 十载萤窗映雪深,今朝试剑白云岑。 乘风莫道鲲鹏晚,自有天光照汝心。 44 个帖子 - 42 位参与者 阅读完整话题