WWW.YOUINFO.SITE
标签聚合 AG

/tag/AG

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 17:55:32+08:00 · tech

agent 是智能体的意思,什么是智能体呢,为啥不叫AI了啊,也不叫大模型了,其实这并不是孤立的概念,AI中文就是人工智能,英文全称:Artificial Intelligence,其实就是计算机科学的一个分支,用来研究开发模拟,延伸人的理论方法技术和应用研究。大模型是ai具象化的技术产品,大模型还分了LLM语言大模型、VLM视觉大模型、MLLM多模态、技术上又出现了很多细节,比如混合专家模型-MOE。 MCP 是定的ai识别的上下文协议,用来,调用外部的服务器,返回固定内容信息的一个规则,大家都用这个规则,不就方便了ai调用外部工具获取信息了。方便打通不同企业数据库和ai的交互。 tools 就是工具的意思,这里和mcp紧密相连,tools泛指一类工具,遵循的上下文协议也未必是mcp。方便ai通过这个工具进行获取信息。 plugin是插件的意思,就是个扩展包,这不是ai独有的概念,浏览器有插件,任何应用都可能有插件,一个插件里面东西就多了,可以包含skill,agents,hooks,mcp severs等内容。 prompt是提示词的意思,大模型学的东西多了,大模型要在知识汪洋中预测你想要的下一个词,简直不要太难,那么就帮她缩小范围降低幻觉,那就是定人物,定任务范围,定目标,这样将结合以上的信息,进行数据处理,就大大降低了,大模型胡说八道的可能性。大模型本身就是个统计学问题,根本不具备任何智慧,和反思能力,并非动态进化的,而是提前通过人类社会无数的现有文档,向量化,然后通过多维向量的训练出来的,一个具备无数维度的数学矩阵,通过通过上下文的切割成token又称词元,一个词元就是一个数字,多个词元就组成了一个数学矩阵,将这个数学矩阵扔到transform架构的数学矩阵中。我也不知道是不是百亿参数是不是也决定了词元的数量呢,会影响回应呢? workflow就是工作流,针对一项工作设计的工作流程,使其完成特定的任务,取代繁重的工作。 hook钩子的意思,什么是钩子啊就是,当执行到特定情况或者涉及特殊判断的时候就会触发的程序,相当于一个钩子,勾住了你的工作流,在特定情况下触发,进而保证进程的稳定和顺利。 skill技能的意思,技能可以是一个md说明的工作文档,也可以是md说明文档加一些小程序、或者一些模板的综合体,目标就是让大模型能按你的md说明文档进行工作。 harness就是一个工作台,工作台上啥也有,自由搭配,想用啥就用啥,比如有plugin、tools、prompt、workflow、hook、skill、和设定好的agent。 AI / 人工智能 └── 大模型 / LLM / VLM / MLLM └── Agentic System / 智能体系统 ├── Prompt:给模型的指令 ├── Context:当前任务上下文 ├── Memory:可长期保存或检索的历史信息 ├── Tools:模型可调用的外部能力 │ └── MCP:连接 tools / resources / prompts 的标准协议之一 ├── Workflow:预设流程 ├── Hook:生命周期触发器 ├── Skill:可复用能力包 ├── Plugin:可安装扩展包 └── Harness:运行框架 / 执行外壳 agent 是配置了 instructions、tools,以及可选运行行为的 LLM MCP Server 可以向 AI 应用暴露 resources、prompts 和 tools。这样不同 AI 应用和不同外部系统之间就不用每次都重新写一套私有接口。 Tool:一个具体能力 MCP Tool:通过 MCP 协议暴露出来的 tool MCP Server:把一组 tools / resources / prompts 提供给 AI 应用 Agent:根据任务需要决定是否调用这些工具 plugin 可能包含 tools、skills、agents、hooks、MCP servers 等内容。简单说,plugin 是“打包和分发能力”的方式。 prompt 帮模型缩小范围,降低幻觉。这个是对的。OpenAI 文档也把 prompt engineering 描述为编写有效指令,让模型更稳定地产生符合要求的内容。 大模型本质上是通过大量数据训练出来的神经网络,它没有人类意义上的主观意识,也不会在普通对话中自动修改自己的模型参数。它的回答来自当前输入、上下文、训练得到的参数,以及推理时的生成过程。我们看到的“推理”“反思”“自我检查”,更多是模型在特定提示、上下文或工具流程下表现出来的能力,而不是人类式的自我意识。 Token:文本被切分后的处理单位。 Token ID:token 被映射成的数字编号。 Embedding:token ID 进入模型后对应的向量表示。 Parameter:模型训练出来的权重和偏置。 Context window:一次输入/输出能处理的 token 上限。 Training tokens:训练时看过的数据 token 数量。 Vocabulary size:分词器支持的 token 种类数量。 文本会先被 tokenizer 切成 token,再映射成 token ID。模型会把 token ID 转成向量表示,也就是 embedding,然后送入 Transformer 网络中计算。Transformer 通过注意力机制和多层神经网络,结合上下文预测后续 token。参数量指的是模型内部训练出来的权重数量,和输入 token 数不是同一个概念。 Workflow 是预先设计好的流程。它强调“步骤固定、路径清楚、可控性强”。比如先读订单,再判断退款规则,再调用退款接口,再发送通知。workflow 里可以用大模型,也可以不用大模型。它和 agent 的区别是:workflow 的路径主要由人或程序提前写好;agent 的路径更多由模型根据目标和中间结果动态决定。 Anthropic 对这个区别说得很清楚:workflows 是 LLM 和工具通过预定义代码路径编排;agents 则是 LLM 动态决定自己的流程和工具使用。 这个方向对。Anthropic 的 Agent Skills 文档也把 skill 描述为模块化能力包,包含 instructions、metadata 和可选资源,比如 scripts、templates,Claude 会在相关任务中自动使用。 另一个官方指南也说 Skills 可以是由 instructions、scripts、resources 组成的文件夹 Context:这次对话/这次任务临时放进来的信息。 Memory:跨会话保存、以后还能拿出来用的信息。 Context 是模型当前这次任务能看到的信息,比如用户问题、系统指令、聊天历史、检索到的文档、工具返回结果等。Memory 是被长期保存、之后还能被取出来的信息,比如用户偏好、项目背景、历史决策、常用规则等。Memory 不是模型参数本身发生了变化,而是系统把相关历史信息保存下来,在需要时重新塞回 context。 5 个帖子 - 4 位参与者 阅读完整话题