包含关键字 typecho 的文章

 Copy a File to Multiple Directories in Linux

要将文件复制到 Linux 中的多个目录,可以使用 cpxargs 命令。所有目标目录都将作为标准输入管道连接到 xargs 命令,示例如下:

echo dir1 dir2 dir3 | xargs -n 1 cp -v file.txt

这将复制文件 file.txt 到 dir1,dir2 和 dir3 目录。

或者,使用 for 循环将文件复制到多个目录,示例如下:

for dir in dir1 dir2 dir3; do
    cp file.txt $dir
done

也可以使用 find 命令将文件复制到多个目录,示例如下:

find dir1 dir2 dir3 -type d -exec cp file.txt {} \;

注意: 确保您具有将文件复制到目标目录的必要权限。

我的开源项目

酷瓜云课堂-开源知识付费解决方案

在 2025 年 6 月发布Jakarta EE 11之后,Jakarta EE 12 的开发工作一直在顺利进行,这个版本预计将提供改进的集成,并与前一个版本保持一致。

 

Jakarta EE 12 的四个里程碑版本中的第二个计划在 2026 年第一季度发布。在本文中,我们将讨论新特性和能力,这些将提供一致性和配置,改善 Jakarta EE 开发者体验。此外,我们将突出该平台中发现的一些初始项目。

 

为什么这个版本对开发者和架构师很重要

 

当我们谈论 Java 平台本身时,Jakarta EE 12 Milestone 2 重塑了平台,直接影响了 Java 生态系统,而不仅仅是个别规范。

 

Jakarta EE 12 带来了对数据的新视角,将查询、数据访问、配置和一致性视为平台一级的关注点。因此,Jakarta EE 12 的主题是健壮性和灵活性。

 

Jakarta EE 是 Quarkus 和 Spring 等 Java 框架的基础。对于开发者来说,这一发展转化为更清晰的 API、更少的跨层重复,以及无论使用哪个框架都感觉熟悉的概念。

 

对于架构师来说,Jakarta EE(以前称为 Java EE)一直提供稳定性和可移植性,但 Jakarta EE 12 增加了架构的相关性。

 

该平台现在明确支持多语言持久性、现代 Java 基线和新兴关注点,如基于智能体的 AI 集成,而不会迫使框架陷入非自然的抽象。这种方法创造了更具适应性的架构,此外,多语言持久性,允许架构师为正确的场景选择合适的数据库。

 

了解 Jakarta EE 12 发布过程

在深入探讨 Jakarta EE 12 Milestone 2 的新内容之前,简要解释开发阶段,包括测试、社区反馈和两个修订里程碑是很重要的。与任何软件开发过程中的典型情况一样,这些规范在最终投票(在这种情况下,由 Jakarta EE 指导委员会对过程进行投票)之前可能会发生变化或延迟。如图 1 所示,这是目前作为 Jakarta EE 12 平台一部分的规范集:

 Jakarta EE 平台中的规范

 

需要注意的是,Jakarta MVC 3.1 和 Jakarta NoSQL 1.1 规范目前正在考虑是否包含在 Jakarta EE 12 平台中。

 

在 Jakarta EE 11 中,我们看到了 Java 17 作为基准支持 Java 21 的影响。这种包含允许你在 Jakarta 持久性规范中使用 Java 记录作为嵌入式类和 ID,以及在 Jakarta 并发规范中使用虚拟线程。对于 Jakarta EE 12,基线将是支持 Java 25 的 Java 21。在平台层面,这个版本反映了从已弃用的 SecurityManager 的明确转变,对遗留 API 和模糊规范语言的更广泛清理,以及为现代 Web 协议如 HTTP/3 做准备。

 

由于OSSRH和 Maven Central 的后续新过渡协议的结束,一些规范无法提供里程碑 1 版本。因此,这些规范将直接进入里程碑 2,如本仪表板所示。

 

我们将重点关注四个可以提供里程碑2版本的规范:

 

  • Jakarta Query为持久层提供了一种通用的面向对象语言。

  • Jakarta Data支持对存储库的动态查询,并与 Jakarta Query 集成。

  • Jakarta Persistence支持 Java 21 中引入的 SequencedCollection 接口,以及与 Jakarta Query 的集成。

  • Jakarta NoSQL引入了带有 Java 记录的投影和与 Jakarta Query 的集成。

 

除了强大和灵活的新主题,我们可以将 Jakarta EE 12 称为数据时代,它包括 Jakarta EE 生态系统中的多语言持久性,使 Jakarta EE 能够“说”SQL 和 NoSQL,可能还包括 Jakarta NoSQL 1.1。

 

Jakarta Query

作为新批准包含在 Jakarta EE 12 平台和 Web 配置文件中的规范,Jakarta Query 1.0,初始版本,为持久层提供了一种 Java 查询语言。我们的目标是从 Jakarta Persistence 中提取 Jakarta 持久化查询语言(JPQL)和从 Jakarta Data 中提取 Jakarta 数据查询语言(JDQL),并将其集中到一个基于两种语言的单一规范中:

 

  • Jakarta 通用查询语言(JCQL),基本语言,你可以执行基本查询操作,这些操作可以与 Jakarta Data 和 Jakarta NoSQL 规范一起使用。

  • Jakarta 持久性查询语言(JPQL),一种扩展了核心语言的关系和实体特性的语言,用于 Jakarta 持久性规范和其他基于 SQL 的技术。

 

由于这是一个新规范,我们仍在进行一些需要改进的工作,包括规范及其组件中使用的术语。我们有的是定义一种由两个配置文件或语言组成的语言的规范。

 

核心语言是完整的 Jakarta 查询语言的一个子集,专注于可移植性操作,如选择、限制、排序和简单的投影。为了说明,考虑下面房间文档的 JSON 表示:

 

{  "id": "R-101", "type": "DELUXE","status": "AVAILABLE",  "number": 42 }
复制代码

 

使用核心语言,一个查询可能会检索所有可用的豪华房间,并按它们的号码排序:

 

FROM Room WHERE type = 'DELUXE' AND status = 'AVAILABLE' ORDER BY number
复制代码

 

持久性语言是一种关系查询语言,它引入了面向 SQL 的结构,如连接、分组和批量更新或删除。这些在关系上下文中特别有用。例如,假设有一个嵌入了房间列表的酒店文档。

 

有了持久性语言,一个查询可以计算每个酒店的已入住客房数,只返回客房数大于 10 的客房:

 

SELECT h.name, count(r) FROM Hotel h JOIN h.rooms r WHERE r.status = 'OCCUPIED' GROUP BY h.name HAVING count(r) > 10 ORDER BY count(r) DESC
复制代码

 

该规范的主要目标是作为持久性的参考语言。因此,它将被用于 Jakarta NoSQL、Jakarta Persistence 和 Jakarta Data。

 

Jakarta Data

Jakarta Data 1.0 是 Jakarta EE 11 中流行的新规范。该规范的最新版本Jakarta Data 1.1简化了 Java 和持久层之间的集成,允许你通过统一的接口同时使用 NoSQL 和关系数据库。这个新版本引入了三个新特性。

 

第一个特性允许你在存储库中执行动态查询。你可以使用 fluent API 创建搜索,给定可以包含在存储库中的 Restriction 属性。

 

@Repository public interface Products{ List<Product> findAll(Restriction<Product> restriction);  }
复制代码

 

这个版本包含元模型上的更多功能,包括搜索的 fluent API 功能。因此,我们可以在仓库中组合限制:

 

@Inject private Products products;List<Product> found = products.findAll(    Restrict.all(        _Product.type.equalTo(ProductType.PHYSICAL),        _Product.price.greaterThan(10.00f),        _Product.name.contains("Jakarta")    ) );
复制代码

 

第二个特性是使用@Is注解改进的搜索。在 Jakarta Data 1.0 中,有按等值条件查询的选项。但在新版本中,你可以使用@Is注解或使用新的 Constraint 接口的实例:

 

List<Product> pricedBelow(@By(_Product.PRICE) @Is(LessThan.class) float max);  @Find Page<Product> search(  @By(_Product.NAME) @s(Like.class) String pattern,  PageRequest pagination,  Order<Product>; order);    @Find         List<Product> inSomeOtherRegionThan(     @By(_Country.REGION) NotEqualTo<Region> exclude);
复制代码

 

Jakarta 查询将用核心语言替换 Jakarta Data 中现在已废弃的 JDQL。目标是保持兼容性,这样就不会对 Jakarta EE 12 中的用户造成影响。

 

@Repository public interface BookRepository extends BasicRepository<Book, UUID>{   // 查找书名与特定模式匹配的图书@Query("WHERE title LIKE :titlePattern")  List<Book> booksMatchingTitle(String titlePattern);    // 按特定作者选择书籍,并按书名排序  @Query("WHERE author.name = :author ORDER BY title")    List<Book> findByAuthorSortedByTitle(String author); }
复制代码

 

Jakarta Data 最初默认使用无状态仓库。在这种方法中,每个操作都是一次处理一个,没有上下文或记忆在调用之间传递,保持简单、可预测,并清楚地表明每个事务的开始和结束。

 

在最新版本中,Jakarta Data 现在也支持有状态存储库。有了这种支持,你就可以使用持久化上下文,就像在 Jakarta 持久化规范中一样。有状态存储库让你管理实体的完整生命周期,比如保存、更新、刷新、分离和删除。你还可以利用延迟同步和延迟加载等特性。

 

@Repository public interface Products extends DataRepository<Product, String>{    @Persist        void add(Product product);        @Merge        Product merge(Product product);        @Remove        void remove(Product product);        @Refresh        void reload(Product product);        @Detach        void detach(Product product);     }
复制代码

 

Jakarta NoSQL

Jakarta NoSQL 1.1,这个规范的最新版本,促进了 NoSQL 和 Java 在企业 Java 中的轻松集成。这个版本的亮点是支持通过 Jakarta query 的核心语言特性的查询语言。这个版本将由与 Jakarta Persistence 类似的术语驱动,使 Java 开发者更容易在企业应用中使用 NoSQL 数据库。

 

Jakarta NoSQL 提供两个特性。第一个是新的 Query 接口,它提供了与 Jakarta Persistence 中已经存在的对应物类似的结构。这个接口将作为核心语言和 Jakarta NoSQL 规范之间的桥梁。Query 接口允许你动态设置参数,并返回单个结果作为ListStreamOptional

 

List<Car> cars = template.query("FROM Car WHERE type = :type")                           .bind("type", CarType.SPORT)                         .result();
复制代码

 

新的TypedQuery接口允许你在查询中定义实体并将其作为实体本身返回,或者将其作为记录类定义的投影的新结构返回。

 

@Projection(from = Car.class)public record BudgetCar(String name, double price) {  }  List<BudgetCar> cheapCars = template    .typedQuery("WHERE price < 100", BudgetCar.class)    .result();
复制代码

 

Jakarta Persistence

Jakarta Persistence 4.0,即该规范的最新版本,新增的一个特性是将 JPQL 转移到 Jakarta Query。Jakarta Persistence 是一个面向数据的规范,它仍将像以前一样使用 JPQL。这种语言将保持向后兼容性。因此,它不会影响可能仍在使用旧版本 JPQL 的 Java 开发人员。

 

Jakarta Persistence 还支持 Java 21,为新结构提供支持:

 

@Entity @Table(name = "orders") public class Order{  @Id @GeneratedValue(strategy = GenerationType.UUID)   private UUID id;   @Column(nullable = false)    private String customer;    @Column(nullable = false)    private Instant createdAt = Instant.now();    @ElementCollection @CollectionTable(name = "order_lines", joinColumns = @JoinColumn(name = "order_id")) @Column(name = "item")    private SequencedCollection<String> items = new LinkedHashSet<>();    }
复制代码

 

有一些关于在 Jakarta Data 中为静态查询添加注释的讨论。目标是提供更多的功能和选项来运行结合 Jakarta 数据和 Jakarta 持久性的查询。

 

@Repository interface Library{ @StaticQuery("where title like :pattern") @ReadQueryOptions(cacheStoreMode=BYPASS)  List<Book> books(String pattern);  }
复制代码

 

你可以在 IBM 的 Red Hat 高级杰出工程师Gavin King博客文章中了解更多关于 Jakarta Persistence 4.0 的信息。

 

Jakarta 代理式人工智能

在 2025 年 11 月初,一项新的专注于采用人工智能的规范通过了创建审查。Jakarta Agentic AI规范提供了一套供应商中立的 API,旨在简化、标准化和简化在 Jakarta EE 运行时构建、部署和运营 AI 智能体的过程。

 

通过提供统一的方法,开发人员可以跨不同环境以更高的效率和一致性创建智能解决方案。

 

这些 API 将旨在促进互操作性和可靠性,使组织能够加速创新,同时保持灵活性和与行业最佳实践的一致性。

 

这项新规范的范围包括:

 

  • 为在 Jakarta EE 运行时内运行的 AI 代理建立标准化的使用模式和生命周期,以促进互操作性和一致性。

  • 提供访问基础 AI 能力的简化外观,例如大语言模型(LLM),而不是标准化 LLM 本身。API 提供直接、可插拔和可配置的集成,与现有的 LLM API(如 LangChain4j 和 Spring AI)集成,类似于 Jakarta Persistence 如何解包对底层非标准 API 的访问。

  • API 预计将包括一个流畅的 Java API,用于定义代理工作流,而不是 XML。这些工作流将在运行时动态,包含灵活的适应性,而不是依赖于部署时的静态定义。此外,可能还有通过 YAML 和 XML 等格式的可插拔性支持。

  • 与重要的 Jakarta EE 规范建立集成。这些集成包括 Jakarta Validation、Jakarta RESTful Web Services、Jakarta JSON Binding、Jakarta Persistence、Jakarta Data、Jakarta Transactions、Jakarta NoSQL、Jakarta Concurrency、Jakarta Security 和 Jakarta Messaging。

  • 该项目在可行的地方利用 Jakarta Config,并允许实现使用 MicroProfile Config。

  • 实现还可能提供与 OpenTelemetry 的集成,以增强可观测性。

 

Jakarta Agentic AI 1.0,初始版本,有意最小化,以促进早期采用,促进社区参与,并提高对 Jakarta Agentic AI 倡议的认识。未来的增强将根据行业趋势和持续的用户和贡献者反馈进行指导,以使规范保持相关性和前瞻性。

 

这个版本集中在基础编程模型和最佳实践上,并引入了一个轻量级的外观,用于集成 LLM。后续版本预计将扩展程序生命周期管理,并为更高级的代理编排提供全面的工作流 API。

 

规范正在开发中,因此你可以通过参与规范过程并提供反馈和报告错误,通过电子邮件列表或直接在每个规范的 GitHub 存储库中,为最后阶段做出贡献。如果你想直接为代码做出贡献,但不知道如何做到这一点,还有Jakarta EE社区导师计划,你可以在那里学习如何直接为 Java 企业平台的代码做出贡献。

 

Jakarta EE 12 采用时间线

 

与任何软件开发过程一样,里程碑版本不打算在生产代码中使用;相反,它允许开发人员进行实验、测试和提供反馈。里程碑版本的目标与 OpenJDK 中的孵化和预览功能类似。

 

Jakarta EE 12 通过保持向后兼容性来兑现其主要承诺,这将使已经使用 Jakarta 持久性和 Jakarta 数据的组织受益。包括统一查询语言和更清晰的有状态与无状态存储库模型之间的界限在内的最新改进,将提供永久性的解决方案,以消除所有不确定领域。这些变化专注于通过使旧代码中发现的奇怪行为案例可见来增加透明度。当前阶段要求架构师和开发人员停止解决紧急问题,以便他们可以学习新定义将如何改变他们现有的工作方法。

 

要使 Jakarta EE 12 的采用变得广泛,工具将是决定性因素,特别是对于 Jakarta 查询和基于存储库的数据访问。当前开发阶段仍需要额外的工作来完成诸如 IDE 集成、查询验证、重构和运行时诊断等功能。当实现最终确定时,工具生态系统将发挥其全部能力。

 

结论

Jakarta EE 12 里程碑 2 定义了迈向最新企业级 Java 平台版本的第一步。通过开放和透明的贡献过程提供的机会,允许整个社区对规范提供反馈。Jakarta 查询的引入将数十年的查询演变,从 JPQL 到 JDQL,整合为一种单一的、可扩展的语言,连接了关系型和非关系型数据库的世界。Jakarta 数据、Jakarta 持久性和 Jakarta NoSQL 现在共享一个一致的查询基础,可以减少碎片化并改善整个生态系统中的开发人员体验。

 

以 Java 21 作为新的基线,Jakarta EE 继续其拥抱现代 Java 语言特性的传统,提供改进的性能、更清晰的语法和长期的可维护性。虽然这个里程碑标志着 Jakarta EE 12 旅程的开始,但它已经表明了一个明确的方向:规范之间的更紧密集成、提高开发人员生产力,以及与核心 Java 平台的更强大的对齐。

 

第三和第四个里程碑将完善理念,稳定 API,并增加兼容性。

 

原文链接:

https://www.infoq.com/articles/jakartaee-12-milestone-2/

优步将其内部搜索索引系统迁移到了OpenSearch,引入了面向大规模流式数据的拉取式(pull‑based)数据摄入框架。此次改造的目标是提升实时索引工作负载的可靠性、回压(backpressure)处理能力与故障恢复能力。此前,不断演进的产品需求暴露出很多问题,例如,维护自研搜索平台的成本与复杂度持续攀升,还面临着模式演进、相关性调优以及多区域一致性等方面的挑战。

 

优步的搜索基础设施支撑行程发现、配送选择和基于位置的查询,以近实时方式处理持续的事件流。他们自研的搜索平台原本基于推送式(push‑based)数据摄入,也就是上游服务直接向集群写入数据。这种方式在小规模场景下效果不错,但在流量突增与故障场景下表现乏力,会导致写入丢失、重试逻辑复杂等问题。

 

拉取式数据摄入将责任转移到OpenSearch集群本身。分片会从KafkaKinesis等持久化流中主动拉取数据,这些消息队列充当缓冲层,实现可控速率、内置回压与可重放恢复。优步的工程师表示,该方案在流量峰值期间显著减少了索引失败,并简化了运维恢复流程。此前会压垮分片队列的突发流量,现在可被每个分片的有界队列平滑吸收,提升了吞吐量与稳定性。

在流量突增期间,推送式和拉取式摄入表现对比(图片来源:优步的技术博客

 

拉取式管道由多个交互组件构成。事件会被写入 Kafka 或 Kinesis 主题,每个分片映射到一个流分区以支持确定性重放;流消费者将消息拉取到阻塞队列,实现消费与处理解耦,并支持并行写入;消息由独立线程完成校验、转换与索引请求准备,再交给摄入引擎;引擎直接写入 Lucene,绕过事务日志(translog),同时跟踪已处理的偏移量以支持确定性恢复。

拉取式摄入的流式索引架构(图片来源:优步的技术博客

 

据优步工程师介绍,拉取式摄入还提供细粒度的运维控制能力。外部的版本机制确保乱序消息不会覆盖更新数据,至少一次投递能够保证一致性;运维人员可配置失败策略:丢弃策略下消息会直接丢弃,阻塞策略下则会无限重试;通过 API 可暂停、恢复或重置到指定偏移量,帮助团队在故障后快速处理积压。

 

优步支持两种摄入模式。段复制模式仅在主分片上摄入数据,副本从主分片拉取完整段,这可以降低 CPU 开销,但存在轻微可见性延迟。全活模式会在所有分片副本上同时摄入,实现近乎实时可见,但计算成本更高。

 

拉取式摄入是优步高可用、多区域搜索架构的核心。每个区域的 OpenSearch 集群从全局聚合的 Kafka 主题消费数据,构建完整、最新的索引。该设计保证了冗余性、全局一致性与无缝故障转移,让全球用户都能获得一致的搜索视图,同时保持高可用。

拉取式的索引模型(图片来源:优步的技术博客

 

优步正逐步将所有搜索场景迁移到 OpenSearch 的拉取式摄入架构,向云原生、可扩展架构演进,并持续优化平台和回馈 OpenSearch 社区。

 

原文链接:

Uber Moves In-House Search Indexing to Pull-Based Ingestion in OpenSearch

今天真是大快人心,爽了一把。

本来说好今年年夜饭我来做,菜单也写好了。我想着今天才腊月二十六,有些要炸的东西也没必要提前准备好,昨晚就出去和同学打麻将打到一点才回。早上七点多我爸就经典霹雳哐啷地喊我吃早饭,还说今天我姐他们就要过来吃年夜饭(因为才结婚两年,大年初一他们要去男方家( 1000 多公里))我说我昨天去他们家吃饭不是告诉我后天才来吗?然后又睡了一会儿

后来八点多我起床了,我爸就一顿嘲讽 “年夜饭什么都不准备的” “看你做什么出来” “实在不行你炒两个菜意思一下就行了” 给我恶心坏了,我又不是说不弄,小年开始算起到过年也就 6 天,工期一下子少一半还嘲讽我完不成,这不是找茬是什么?我没管他就在厨房备菜了,他就在客厅坐着说我东西放得乱,其实就是前两天收拾行李把两瓶维生素放在餐桌上,然后吵了两句他就搁客厅沙发坐着,一脸不爽的。我也没管继续跟在厨房备菜,一上午我妈弄弄餐桌布置,我哥在厨房帮我打下手,弄到最后的时候我妈发现我没弄鱼(我想带鱼不也是鱼吗🤣)弄鱼的时候我爸过来指导了一下,我也不知道是因为客人来了还是因为怕我煎不好。反正最后十一点多弄好了一桌菜,除了个别卤菜是买的和排骨汤是早上起来他们炖的以外都是我弄的(猪皮冻得到大家一致好评;中间那个其实是三鲜锅仔,只不过我的蛋皮放多了,下面有肉丸之类的,上次兄弟们就吐槽我做菜怎么没有肉;)

IMG_0950.jpeg

我爸和大家经常吐槽的那种家长一模一样,npd 那种,窝里狠,对外唯唯诺诺,在我看来这种人就是蠢,为人处事是一点不会。从小到大我都怕过年,每年过年就是吵架,我跟我爸吵,我妈跟我爸吵,我哥跟我爸吵,那么问题在谁呢?好难猜啊!他总是看家里不顺眼,这里他觉得做的不好,那里他觉得做的不好,想方设法地说家里人的错,真是给我恶心坏了。

最后提前祝大家新年快乐,家庭和睦

(做虎皮凤爪的时候顺手写的)
IMG_0949.jpeg

检索是 RAG 系统的搜索引擎,分块则是这个搜索引擎的基础。分块太长、太短、有噪声、切错了位置——随便犯哪个错LLM 都会有问题。行业里有句话流传很广:"分块决定了 RAG 质量的 70%。"

这个说法不夸张:好的分块让检索器拿到完整、有上下文、真正相关的信息;差的分块把文档打成碎片,上下文断裂,LLM 只能靠"编"来填补空白。

什么是分块?

RAG 的起点是文档收集与摄取:把所有原始材料(文档、文章、知识库条目)汇聚到一起。在进入检索环节之前,这些文档要经过文本分块处理也就是切分成更小的、有意义的片段。

每个分块应当是连贯且自包含的,这样检索器才能在面对查询时快速定位、排序,并返回最相关的信息。

分块就是在生成 Embedding 之前,把大段文本拆成更小语义单元的过程。检索器真正搜索的对象而不是整篇文档就是这些分块。

分块做得好,文档中的内容就能被干净地捕获,上下文得以保留LLM 能做出有意义的推理。分块做得差,语义被割裂检索充满噪声。向量存储、Embedding 模型、Reranker——这些统统排在分块之后,分块才是真正的起点。

固定大小分块

这是最简单的方式。按预设的字符数或 Token 数直接切分,比如每 500 个 Token 一块完全不管句子和段落的边界在哪。

速度快,行为可预测,处理大规模、结构混乱的数据集时很实用。但缺点也很明显——语义经常被拦腰切断。一个句子在这个分块里开了头,到下一个分块才结束,Embedding 的语义表达力就会打折扣。

实践中一般会在相邻分块之间设置一定的重叠来缓解这个问题:

 from langchain.text_splitter import RecursiveCharacterTextSplitter  

splitter = RecursiveCharacterTextSplitter(  
    chunk_size=500,  
    chunk_overlap=50  
)  

 chunks = splitter.split_text(long_text)

切分文本时,连续的分块之间通常会加入一小段重叠区域来维持上下文的连贯。所谓重叠,就是前一个分块的尾部几句话,在下一个分块的开头再出现一次。

这么做是为了防止跨越分块边界的关键信息丢失。没有重叠的话,检索器可能只拿到部分内容LLM 因此漏掉了关键上下文,给出残缺甚至误导性的回答。重叠量一般控制在分块长度的 10% 到 20%,在冗余和效率之间找一个平衡点。

固定大小分块适合的场景包括日志文件、邮件、代码仓库,以及结构参差不齐的大型语料库。

基于句子的分块

这种方式按完整句子来划分文本,而不是按任意长度一刀切。每个分块至少包含一个或多个完整的句子,语法完整,语义连贯。

好处是每个分块都是一个有意义的思想单元。检索器向 LLM 返回的信息更精确、更易理解,碎片化回答的风险降低不少。实际使用中通常也会搭配小幅重叠,进一步保证分块之间的衔接。

基于段落的分块

以完整段落为单位切分,不再拘泥于单个句子或固定 Token 数。这种方式天然保留了文档的结构和行文节奏,检索器更容易抓到完整的想法。

每个分块往往对应一个独立的主题或子主题,LLM 处理起来更从容,也更容易给出准确的回答。对长篇文档、研究论文、综述类文章来说,段落级分块效果不错。和句子级分块一样,也可以加重叠来保持连贯。

语义分块

语义分块的切入点不是长度,而是语义本身。它利用 Embedding 或相似度分数来识别文本中天然的断裂点——主题切换、上下文转折、章节边界。

产出的分块语义清晰度更高,边界和语义对齐,检索质量有明显提升,尤其在知识库、技术文档、结构化文章这类内容上效果突出。代价是计算开销更大而且分块长度不一致,后续处理需要额外考虑。

 from langchain_experimental.text_splitter import SemanticChunker  
 from sentence_transformers import SentenceTransformer  
   
 model = SentenceTransformer("all-MiniLM-L6-v2")  
 chunker = SemanticChunker(model, breakpoint_threshold=0.4)  
   
 chunks = chunker.split_text(long_text)

如果文档质量高、主题流转有明确脉络,语义分块往往是精度最高的选择。

递归分割

递归分割是固定大小和语义分块之间的一个折中方案。核心思路是优先尊重文档结构,只有在必要时才进一步拆分。

具体做法是先尝试按标题切分。如果某个章节还是太长,就按段落切。段落还不够就按句子。句子仍然超限,最后才按字符兜底。这样得到的分块既保有语义完整性,尺寸也在可控范围内。

 recursive_splitter = RecursiveCharacterTextSplitter(  
     separators=["\n## ", "\n### ", "\n", ". ", ""],  
     chunk_size=600,  
     chunk_overlap=80  
 )  
   
 chunks = recursive_splitter.split_text(long_doc)

开发者文档、技术手册、学术论文、研究报告——凡是层级结构明确的内容,递归分割都很适合。

滑动窗口分块

有些文本的语义天然是跨句分布的。法律合同、科学论文、长段论证,一个完整的意思可能横跨好几个句子。滑动窗口就是为这种场景设计的。

它不生成彼此独立的分块,而是创建相互重叠的窗口。比如窗口大小 400 Token,每次滑动 200 Token,这样相邻的分块之间有一半的内容是共享的,语义在边界处不会断裂。

上下文保持得很好,但分块数量会膨胀,存储和检索的成本都会上升。

法律 RAG、金融分析、医学文献检索、合规审查——这些领域用滑动窗口的比较多。

层次化分块

层次化分块是一个多层级的架构:小分块负责细粒度精确检索,中等分块支撑平衡的推理,大分块维持全局上下文。

检索时,系统先用小分块锁定精确位置,再把关联的大分块拉进来补充完整上下文。这种组合能有效压制幻觉,提升推理的深度。

企业级 RAG 系统和 LlamaIndex 这类多粒度检索框架,背后都有层次化分块的影子。

实践中常见的分块失误

多数 RAG 项目翻车根源都是分块层面的问题。分块过大模型被不相关的细节淹没。分块过小语义丧失殆尽。句子被拦腰切断、不相关的段落被混到一个分块里,Embedding 质量直接垮掉。没有重叠,上下文断裂。没有元数据,检索器找不到方向。还有一个常见错误——所有类型的文档套用同一种分块策略。

分块没有万能方案。政策文件和教科书不一样,通话记录和研究论文不一样。策略必须跟着文档类型和检索任务走。

总结

分块不是一个可有可无的预处理环节,它是 RAG 管道的脊梁。好的分块是一个有意义的、自包含的知识单元;差的分块是一个孤零零的碎片,把 LLM 带向歧途。

检索是引擎分块是燃料。燃料的质量决定了整个系统是输出干净、可靠的结果,还是不断产出噪声和幻觉。LLM 本身再好,也救不了烂分块。

https://avoid.overfit.cn/post/e6520bd283254415ae61cfa28fb2ef32

作者:Abinaya Subramaniam

68gpTV7pqSasNZdsjdaXTa4kVAASR2oI.webp

🏮 过年啦!今天吃了啥?

春节的味道,从来不只有一种模样——

可能是妈妈包的饺子,可能是爸爸炖的红烧肉;
可能是闺蜜约的下午茶,也可能是深夜那碗热腾腾的泡面;
可能是值班时叫的外卖,可能是简单对付的一餐。

不挑丰盛,只等你回复分享。

无论你此刻身在何处、吃着什么,你的"春节味道"都值得被看见。

📸 活动规则超简单:

拍下你春节期间的任意一餐
年夜饭可以,外卖盒饭可以,简单速食也可以——
早餐 | 午餐 | 下午茶 | 晚餐 | 夜宵 | 快餐……统统都算!

🎁 参与奖励:

晒出你的一餐,即可获得 200 枚金币!
每个用户多次晒出照片,可能大概也许会多次奖励snicker

📅 活动时间:

春节期间(除夕至正月初三)

📲 参与方式:

拍摄你的美食照片(不挑场景,不挑档次,真实就好)
直接在本帖子回复即可!
金币为手动送出,到账时间最晚不超过正月初五!

一个人值班?叫份外卖,我们陪你吃。
简单凑合一顿?那也是你的春节味道。
快来晒出你的年味儿吧!🍜🥟🍱

p.s. 😐 嗯,其实早发也行,哈哈 😂 我在线可能大概也许就直接给了,就是这么的不讲道理。哈哈 😂

在京东自营给乡下老家买点了过年用的饮料,调料啥的。没有往常的隔日达,年底了货多,人忙我理解。第三日了,老妈等了一天快递,到 20 点了,打电话说放驿站了,让理解理解。我理解不了,如果做不到,我选的地址无货我都能接受。

还有我妈给我炫耀说她学会了多多买菜,晚上 10 点前下单,隔天早上 9 点就能送到村里。





过年九宫格抽奖系统,带后台版
一款专为节日 / 活动设计的九宫格转盘抽奖程序
炫酷九宫格 + 流畅绕圈动画(渐快渐慢)
支持自定义奖品图片上传、位置、中奖概率
IP 限制抽奖次数(后台可调 1 次 / 多次 / 无限)
前端毛玻璃风格 + 手机完美适配
后台支持一键编辑奖品、设置中奖位置、查看中奖记录
简单部署,适合春节、双 11、店庆等活动快速上线
源码截图:

image

下载地址: https://yuncv.lanzouw.com/i8dQp3ifqu9c

开源这个项目的初衷是希望能帮到同样在处理地址、邮编、行政区划逻辑转换中受苦的 dev
如果你由于业务需要发现其中的数据有误,或者有更好的优化建议,欢迎来提 Issue 或 PR !

ScreenShot_2026-02-13_161742_225.png

一、 为什么要做这个项目?

1. 开发时邮编获取的困局

调研了下市面上的邮编数据 API, 通过地址信息获取邮编需要付费限免次数有限
通过 AI 获取的邮编数据幻觉严重,实测多个模型都无法返回准确邮编
有很多网页提供三级联动查询区域邮编,但无法通过 api 接入项目
有一些开源数据但内行政区划老旧不准确,获取邮编还需要手动拆分输入地址的省市区来进行匹配,不准还麻烦

2. 地图 API 的“断层”

在使用地图 SDK 时,最常用的流程是:
输入地址 -> 地图地理编码 -> 行政代码(Adcode) / 经纬度 / 格式化后的地址等
然而,邮编作为一个相对传统的字段,在现代地图服务中的权重在降低。即使拿到了详细地址以及行政区域编码(Adcode),也无法获取到对应的邮编。

3. 数据的时效性问题

邮编数据,要么是 2018 年之前的陈旧版本,不少是付费下载的。
行政区划数据,随着近年来国内行政区划的频繁调整(撤县设区、新区合并),最新对应数据变得非常稀缺。

注意:自 2024 年 10 月起,国家统计局继续公开《关于统计上划分城乡的规定》《统计用区划代码和城乡划分代码编制规则》等统计标准方法,不再公开具体相关代码


二、 解决方案

通过建立行政区划代码( Adcode )对应邮政编码( Zipcode )的关联数据来方便实现查询,将数据导出为 JSON ,各种语言都可以基于此数据方便开发

  • 最新数据:参考了《2023 年中华人民共和国县以上行政区划代码》与 高德 API 返回区域代码 手动校对邮编。 高德全国邮政编码查询
  • Adcode 关联:不仅是名字匹配,且是基于标准的行政区划代码 Adcode 匹配,极速方便。
  • 极小体积:经过数据去重与结构优化,JSON 压缩后极小,加载无压力。
  • 全环境支持:支持纯 JSON 导入、ESM 模块加载、甚至浏览器 CDN 零配置调用。


logo

本文展示了如何通过自托管的 OpenClaw Skills,对 Home Lab(家庭服务器群)与智能家居进行真 AI 智能化控制。而不受限于小米米家等平台的各种智能限制。通过将家庭服务器群环境、监控系统以及几种 API 封装为结构化的 Skills,AI Agent 可以完成服务器故障排查、以有趣的智能家居灯光方式可视化运维操作,并通过智能音箱汇总报播网站实时新闻。


背景

这些年,我一直在运行和维护自己的 Home Lab(家庭服务器群) 和一套小型智能家居系统。随着设备和服务越来越多,维护和管理也变得越来越复杂。

同时,我也逐渐意识到,传统的“智能家居”其实并不真正智能。运营方出于各种原因,没有把最近的 AI 科技更新到智能家居平台上。很多问题的存在,往往只是因为我懒得去做自动化。如今,现代 AI 技术或许可以帮助解决这些问题。

我的 Home Lab 已经运行了 5 年多,承载着多个 Linux 主机上的各种服务。同时,我还运行了一套 Home Assistant 实例,用来管理家中的 IoT 智能家居设备。

在可观测性方面,我部署了 Prometheus 和 Grafana,并为硬件和软件异常配置了告警机制。

但问题是:当手机收到告警后,我仍然需要手动调查和排查问题。如果我不在家,用手机远程排障就会变得低效又麻烦。

这正是 OpenClaw 发挥作用的地方。理想情况下,我只需要给它一些目标,它就应该能替我完成后续操作。

作为 Home Lab 和智能家居的管理员,OpenClaw 需要具备以下三类“技能 (Skills) ”:


使用场景

1. Home Lab 故障排查与根因分析

查找某台服务器上 CPU 占用最高的服务,并通过智能音箱播报结果。

当收到“CPU 使用率过高”或“温度过高”告警时,这种方式尤其方便。

image.png
(配图:root cause 分析示意图)


2. Home Lab “氛围灯光秀大师”

重启某一台服务器。重启开始时,立刻调暗书房灯光;当机器启动完成、所有服务初始化完毕后,再把灯光恢复。

这样一来,当我人在书房时,就可以通过灯光变化 “可视化” 服务器的启动进度。

image.png
(配图:重启灯光联动示意图)


3. AI 驱动的智能家居控制

将 Hacker News (https://news.ycombinator.com/) 的内容摘要和翻译为中文,并通过智能音箱朗读出来。

当我在晨起或做家务时,也可以通过智能音箱播报了解最新任意网站的动态摘要。

use-case-speak-news.png
(配图:语音播报示意图)


Skills 设计

为了实现这些功能,我首先需要向 OpenClaw 介绍我的 Home Lab 环境,包括:

  • 所有 Linux 主机列表
  • 每台机器上运行的服务
  • 监控系统细节
  • Agent 必须遵守的运维规则

我不希望把这些信息直接塞进普通的 LLM 上下文里——那样会迅速消耗上下文窗口,并浪费大量 token。

Anthropic 的 Skills 规范提供了一个清晰的解决方案:我可以将 Home Lab 环境封装成一个 Skill,让 OpenClaw 在需要时按需加载。

注意:以下 Skill 代码片段为了便于阅读,做了简化处理。

Home Lab 环境 Skill

核心思想:

  • 通过 Bash 和 SSH 控制 Home Lab 机器
  • 所有 Linux 主机使用同一个用户名
  • SSH 无需密码登录
  • Prometheus 运行在 192.168.1.74:9090
  • 优先从 Prometheus 查询指标,而非 SSH 到机器
  • 仅在无法获取数据时才通过 SSH 获取实时信息
  • 某些关键机器(如网关)默认只读访问
  • 执行任何会修改系统状态的命令前必须请求确认

特别强调了两条运维规范:

  1. 执行前必须先解释行动计划
  2. 所有可能改变系统状态的操作必须征求用户确认

这相当于为 AI Agent 定义了运维行为准则(Operational Guardrails)。

路径:

~/openclaw/workspace/skills/home-lab/SKILL.md

Skill 片段:

---
name: home-lab
description: Control the home lab where you(personal assistant running inside OpenClaw) are running on
---

# Home Lab

Control home lab Linux machines via bash and SSH commands.

## Conduct of Code

Always provide a clear, high-level explanation of your action plan before executing the first step. Let the user understand and confirm the plan before proceeding.

Always request confirmation before executing any command that may change system state, such as rebooting the OS, modifying configuration files, or restarting services. Clearly explain what the command does and why it is necessary before asking for confirmation.

## Home Lab Environment

All Linux machines use the same username: `mark`. SSH login does not require a password.

### Linux Machines

- 192.168.1.58 : Raspberry Pi, always on
- 192.168.1.68 : Raspberry Pi, always on
- 192.168.1.108 : Raspberry Pi, always on
- 192.168.1.14 : X86_64 server

### Observability and Monitoring

A Prometheus instance runs on 192.168.1.74:9090. Metrics are collected by Node Exporter on each machine.

When users request metrics (temperature, CPU usage, memory usage, disk usage), first query Prometheus. Only execute remote SSH commands if the required metrics cannot be found in Prometheus.

If you are unfamiliar with the Prometheus API, load the `prometheus-api` Skill.

Note that Prometheus metrics may not always be real-time. If real-time data is required, execute a remote SSH command.

### Services running on the machines

#### 192.168.1.68
This machine acts as the main Internet gateway of the home lab. 

Access this machine in read-only mode. Do not change anything without explicit confirmation. 

##### Home Assistant
The home automation system. Access by http://192.168.1.68:8123

##### OpenClaw
An OpenClaw instance runs at http://127.0.0.1:18789. It may be you or may be another AI assistant.

#### 192.168.1.74

##### Prometheus

Base URL: `http://192.168.1.74:9090`  

All machines are monitored via Node exporter, with metrics collected by this Prometheus server. 

## Remote wake up

Wake up 192.168.1.14 using:
```bash
ssh mark@192.168.1.108 o
```

Home Assistant Skill

基于 Home Assistant API 文档,我创建了一个 webhook,用于向小米智能音箱发送通知。

然后在 Skill 文档中告诉 Agent 如何调用该 API,例如:

curl -s -X POST $HA_URL/api/webhook/ai-speak \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $HA_TOKEN" \
  -d '{"msg": "需要播报的内容"}'

此外,还封装了:

  • 列出智能设备
  • 控制开关
  • 控制灯光亮度

这些都通过标准的 Home Assistant REST API 实现。

---
name: homeassistant
description: Control Home Assistant - smart plugs, lights, scenes, automations.
homepage: https://www.home-assistant.io/
metadata: {"clawdis":{"emoji":"🏠","requires":{"bins":["curl"],"env":["HA_TOKEN"]},"primaryEnv":"HA_TOKEN"}}
---

# Home Assistant

Control smart home devices via Home Assistant API.

## Setup

Set environment variables:
- `HA_URL`: Your Home Assistant URL (e.g., `http://192.168.1.100:8123`)
- `HA_TOKEN`: Long-lived access token (create in HA → Profile → Long-Lived Access Tokens)

## Quick Commands

### Smart Speaker Integration 🔊 
There is a smart speaker you can control. You can control it to speech any text to the user. You can use it to send notification to user at home.

```bash
curl -s -X POST $HA_URL/api/webhook/ai-speak -H "Content-Type: application/json" -H "Authorization: Bearer $HA_TOKEN" -d '{"msg": "The message you want to speech to user"}'
```

### List entities by domain
```bash
curl -s "$HA_URL/api/states" -H "Authorization: Bearer $HA_TOKEN" | \
  jq -r '.[] | select(.entity_id | startswith("switch.")) | .entity_id'
```

### Turn on/off
```bash
# Turn on
curl -s -X POST "$HA_URL/api/services/switch/turn_on" \
  -H "Authorization: Bearer $HA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "switch.office_lamp"}'
```

### Control lights
```bash
# Turn on with brightness
curl -s -X POST "$HA_URL/api/services/light/turn_on" \
  -H "Authorization: Bearer $HA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "light.living_room", "brightness_pct": 80}'
```

Prometheus API Skill

基于 Prometheus API 文档封装。

这样 Agent 可以:

  • 查询 CPU 使用率
  • 查询设备温度
  • 查询内存、磁盘使用情况
  • 结合 Alertmanager 做进一步自动化处理

未来使用场景

主动型 Agent

下一步,我希望 Agent 不只是“被动响应”,而是主动监控并处理 Home Lab 和智能家居状态。

用户只需要用自然语言描述期望行为,Agent 可以自动转换为:

  • 监控规则
  • 告警规则
  • 自动化规则

例如:

通过集成 OpenClaw Webhook 到 Prometheus Alertmanager,实现:

  • 自动触发故障排查
  • 自动调查分析
  • 自动进行根因定位

也就是说,当告警触发时,Agent 不只是发通知,而是直接开始分析问题。

管理我的家居物品

用自然语言管理我的 Homebox,这是一个自托管的家庭物品管理系统。例如,当我购买新设备时,我可以告诉 Agent: “我买了一部新的 iPhone 15 Pro Max,请将其添加到我的家庭物品清单中”。


总结

通过将 Home Lab 环境知识、Prometheus 监控系统和 Home Assistant API 封装为结构化 Skills,OpenClaw 可以将传统“手动运维 + 被动智能家居”升级为“AI 编排驱动”的自动化系统。

它不仅可以:

  • 分析服务器故障
  • 有趣化运维过程
  • 语音播报任意网站信息

更重要的是,它为“主动式 AI 运维与智能家居控制”提供了一个可扩展的架构基础。

安全风险警告

赋予人工智能 Agent 在您的 Home Lab 中执行命令并控制智能家居设备的权限会带来重大的安全风险。攻击者可能利用该 Agent 获取未经授权的访问权限、造成损害或窃取敏感信息。请仅从可信来源下载和安装任何 Skill 。安装任何 Skill 之前,务必先查看其代码。考虑在权限受限的沙箱环境中运行 Agent,以降低潜在风险。

本文发布于我的博客:https://blog.mygraphql.com/zh/posts/ai/ai-personal-assistant/openclaw-as-home-lab-admin/

image
保存过通知 Webhook URL 之后,如果想要关闭通知,删除 url 保存报错

前端用 antd 替换了,界面有点像 UniFi Dream Router

官方下载页支持直接下载升级包


新增应用市场


4.0 升级注意事项
欢迎您使用全新版本的 IKOS!

本次升级为您带来了更强大的性能和更稳定的体验。

为了确保系统兼容性与稳定性,以下需要您关注并调整。

请仔细阅读以下内容



升级降级

强烈建议您使用 3.7.21 版本进行升级。

其他的更低版本,有较小概率出现不可预知风险。

4.0 版本不支持降级至 3.0 版本,升级前请妥善备份好相关配置&文件。



应用协议

应用协议库已重新分类,采用更科学的分类体系,提升识别准确率。

但可能会影响协议相关的分流/管控规则。

需要您关注:

1. 检查现有 [协议分流] 中的应用协议选择;

2. 检查现有 [应用协议控制] 中的应用协议选择;

3. 验证关键应用的识别准确性。

新老协议转换详情:3.0→4.0 协议库对照表



智能流控

我们对智能流控-自定义场景中的协议场景做了重新的分类,场景更多元且合理,识别更加准确。

由于发生了结构的变化,此处无法兼容。

若您当前是自定义模式,升级后,系统将以自定义模式的默认优先级运行。

需要您关注:

1. 智能流控的自定义场景下的协议分类;

2. 协议分类的流控优先级数值



QQ 黑名单

因 QQ 通信协议频繁更新且加密机制日益复杂,同时根据我们近期的调研,该功能的使用率显著偏低;

为优化服务体验并集中资源维护更广泛使用的功能,我们将于近期下架路由设备中的“QQ 黑白名单”功能。

由此带来的不便,我们深表歉意,感谢您一直以来的支持与理解。



Docker

4.0_32 位系统不支持 docker&应用市场。

软路由用户建议使用 64 位系统进行刷机。



企业微信/钉钉-应用绑定

因微信与钉钉平台已关闭设备绑定接口的相关业务,导致新设备无法成功接入。

为保障服务的一致性与稳定性,避免功能差异带来的体验困扰,我们决定在 4.0 版本中正式下架“企业微信绑定”与“钉钉绑定”功能。

若您十分依赖此两项功能,请暂时勿升级

感谢您的支持与理解。



其他

1. 备份路由配置文件,并保存至 PC 本地;

2. 备份路由重要磁盘文件,并保存至 PC 本地。

以前我觉得开源模型大多是玩具,真要干活、写复杂逻辑,还得老老实实给闭源大厂交 API 的保护费。GLM-5 的发布,不是一次简单的版本号 +1,而是直接把开源模型从玩具拉到了员工的级别。

image.png

跑完测试后,我发现,以前得雇人或者自己熬夜干的活,现在这个模型真的能接手了。

为什么说 GLM-5 让我的认知崩塌了?

参数量 744B(激活 40B),预训练数据 28.5T tokens,AI一般都有2个问题,脑子不够用和记性太差,这也是用 AI 开发的痛点,而 GLM-5就没有这个烦恼。

1. 它不再是小镇做题家

以前评测模型,大家喜欢看它做奥数题。但说实话,谁家好人在工作中用到奥数呀,我它帮我规划任务。

这次 GLM-5 在 Vending Bench 2 上的表现就很厉害了,要知道这个测试很变态的,要求模型在模拟环境里经营一家自动售货机公司,周期长达一年。

  • 大多数开源模型:落地成盒,开局就寄,根本搞不清库存和资金流。
  • GLM-5:不仅活下来了,最后账户余额还剩 4,432 美元

image.png

这个成绩在开源界是断层第一,直逼闭源的 Claude Opus 4.5。就是说,如果你把 GLM-5 接入业务流,它真的具备长期规划和资源管理的能力。

2. 从生成文字到交付工作

大家以前用模型最烦的是什么?它给你吐出一堆 Markdown 格式的文本,你还得自己复制粘贴去排版。

GLM-5 这次最让我惊喜的是它对办公场景的理解。它能把那些复杂的推理结果,直接生成 .docx、.pdf 甚至 .xlsx 文件

  • 写 PRD?它直接给你一个格式完美的 Word 文档。
  • 做财报分析?它直接扔给你一个带公式的 Excel 表格。

这才是真正的生产力工具。它省掉那些最没技术含量的格式调整和文档整理时间。

3. 技术上的降本增效

我也好奇,这么大的参数量,跑起来会不会慢得像蜗牛?

GLM-5 用了 DeepSeek Sparse Attention (DSA) 的技术,让模型只关注该关注的信息,把算力用在刀刃上。再加上 slime 的强化学习架构,解决了大模型越训越傻的问题。

image.png

所以它的逻辑密度高,废话少。

image.png

本地部署

说到这,很多人可能跃跃欲试想在本地跑一下。毕竟是开源模型,数据握在自己手里才踏实。

GLM-5 这种量级的模型,对 Python 环境、依赖库有要求。我之前为了跑一个大模型,光是解决 Python 依赖冲突就花了一整天,最后心态崩了模型还没跑起来。

所以这次为了不重蹈覆辙,直接上 ServBay。如果以前你觉得这种工具是给新手用的,那就错了,这是给想省时间的人用的。

你想跑 GLM-5,得装特定版本的 Python,还得配 vLLM 或者 SGLang,原生环境里搞,很容易把之前的项目环境搞挂。用 ServBay,点击下载Python, 它直接给我弄了一个隔离的、干净的 Python 3.10+ 环境。

image.png

就这么简单。在这个干净的沙盒里,再运行安装命令:

pip install -U vllm --pre --index-url https://pypi.org/simple --extra-index-url https://wheels.vllm.ai/nightly

没有报错,没有红字,一次通过。

这一步省下来的时间,足够我把 GLM-5 的 API 文档看两遍了。

最后

如果你看看未来的工作方式长什么样,可以试试 GLM-5。

它不是那种让你“哇”一声然后就关掉的玩具,它是那种你用了一次,就会把招聘助手的计划推迟的工具。

值得试试。

Cloudflare 推出了开源的Moltworker,支持在 Cloudflare 开发者平台上运行 Moltbot(一款可自托管的个人 AI 智能体),从而不再需要专用的本地硬件。Moltbot 最初名为 Clawdbot,设计定位是通过聊天应用提供个人助手服务,可集成 AI 模型、浏览器与第三方工具,且全程由用户自主掌控。

 

Moltworker 将 Moltbot 适配到了 Cloudflare Workers 中,在架构上组合了入口 Worker 与隔离的沙箱(Sandbox)容器。Worker 充当 API 路由与管理层,而 Moltbot 运行时及其各类集成则在沙箱内执行。包括对话记忆与会话数据在内的持久化状态会存储在 Cloudflare R2 中,解决了容器本身无状态、生命周期短的问题。

 

该实现充分利用了 Cloudflare Workers 近期在 Node.js 兼容性上的增强。Cloudflare 表示,对 Node API 更完善的原生支持减少了对变通方案的需求,让更多 npm 包可以不经修改直接运行。尽管 Moltbot 当前主要在容器中运行,但 Cloudflare 认为,这种更强的兼容性未来将让更多智能体逻辑可以更贴近边缘节点执行。

 

Moltworker 集成了多项 Cloudflare 服务,复刻并扩展了本地版 Moltbot 的体验。AI 请求会经由 Cloudflare AI Gateway 进行路由,支持多模型厂商、集中式可观测性与多种配置选项;浏览器自动化任务通过 Cloudflare Browser Rendering 来处理,使 Moltbot 可以控制无头 Chromium 实例完成页面导航、表单填写与内容抓取,而无需在容器内直接运行浏览器;API 与管理后台的身份认证则通过 Cloudflare Zero Trust Access 实现。

该项目在早期用户中引发了褒贬不一的反响。有人认为,基于 Cloudflare 托管的方案显著降低了使用门槛。Peter Choi 在评论 中指出,在 Cloudflare 上运行 Moltbot 有望大幅提升普及度,但同时质疑这种迁移是否会改变项目最初强调完全本地控制的核心吸引力。

 

另一些用户则强调其运维优势,有的用户这样说到

我一直在 VPS 上实现自托管,虽然能用,但管理服务器本身很麻烦。这个方案看起来是“部署后无需维护”的版本,很好奇状态在多次 Worker 调用之间是如何持久化的。

 

Cloudflare 已经在 GitHub 上开源了 Moltworker,并将其定位为概念验证(proof of concept),而非正式支持的产品。官方表示,该项目旨在展示其开发者平台(结合了 Workers、Sandboxes、AI Gateway、Browser Rendering 与存储服务)如何在边缘环境安全地、规模化地运行 AI 智能体。

 

原文链接:

Cloudflare Demonstrates Moltworker, Bringing Self-Hosted AI Agents to the Edge

11.jpg
本次更新围绕"记忆系统工程化"和"Agent 能力结构化"两条主线,对云服务和开源项目做了系统升级。核心改进集中在多视角记忆、记忆版本管理、检索召回质量、Skills 本地化,以及若干生产环境的稳定性优化。

本次发布亮点

22.png

1. 多视角记忆:让每个 Agent 拥有“自己的记忆世界”

传统 Agent 架构里,记忆通常是全局共享的,所有 Agent 共享同一份"客观记忆池"。这在多角色系统、多 Agent 协作场景中容易导致行为冲突和角色混淆。

我们上线了多视角记忆(Multi-Perspective Memory),为每个 Agent 引入"主观视角"的记忆结构。同一事实可以在不同 Agent 那里形成不同视角的记忆表达和认知结构。每个 Agent 的记忆体拥有自己的"主观视角",适合需要角色化、队伍化的 AI 游戏或多 Agent 协作场景,比如组队游戏、角色化陪伴类应用。

帮助系统在多 Agent 协作时避免"一刀切"的全局记忆,便于实现个性化行为和角色差异化决策。记忆不再是单一全局视图,而是 Agent 级别的认知世界模型。

33.png

2. 多视角 AI 小游戏 Demo:多 Agent 记忆协作的真实形态

基于多视角记忆的小游戏"冲顶鳌太线"已经上线,提供组队冲顶玩法的示例。Demo 以组队协作为核心场景,多 Agent 各自拥有独立视角记忆,同时参与协作任务目标,形成"个体认知 + 团队目标"的复合结构。

该 Demo 以组队协作为核心场景:

  • 多 Agent 各自拥有独立视角记忆
  • 同时参与协作任务目标
  • 形成“个体认知 + 团队目标”的复合结构

3. 检索记忆(search/memory)能力增强:更准 + 更省 Token

3.1 关键词召回 + 语义相似度混合排序

在事实记忆检索链路中新增 ​关键词召回机制​,并与原有语义相似度检索进行混合排序:

  • 提升召回覆盖率;
  • 提升召回准确率;
  • 避免单一语义相似导致的语义漂移问题。

实测效果:

  • LongMemEval 提升 1.8%;
  • Locomo 提升 0.72%。

该能力默认开启,开发者无需额外配置。

3.2 消耗 Token 更少的记忆召回策略(相关性精筛)

新增 relativity(相关性阈值)memory_limit_number/top_k 等参数,允许开发者按阈值只返回高相关性的记忆,从而显著降低注入 prompt 的 token 消耗,控制成本并提高上下文质量。

为解决记忆注入导致的 Token 消耗问题,search/memory 接口新增 ​相关性精筛机制​:

  • relativity:相关性阈值(0\~1);
  • memory_limit_number / top_k:召回数量上限。

系统只返回:

  • 相关性 ≥ 阈值;
  • 数量 ≤ 上限 的记忆集合。

这使 MemOS 的记忆注入从“暴力拼接”升级为:

精准召回 + 强相关过滤 + Token 成本可控

📌 当前 relativity 仅对 事实记忆、偏好记忆 生效。

示例(云服务)​:

data = {
  "user_id": "memos_user_123",
  "query": "为我规划5天的成都游。",
  "relativity": 0.8, # 只返回相关性 >= 0.8 的记忆
  "memory_limit_number": 9 # 最多返回 9 条
}

示例(开源)​:

{
  "user_id": "memos_user_123",
  "readable_cube_ids": ["memos_user_123_cube"],
  "query": "为我规划5天的成都游。",
  "relativity": 0.8,
  "top_k": 9
}

注意:relativity 当前仅对事实记忆偏好记忆生效。

4. Skills 能力工程化升级 + MindDock 插件接入

4.1 Skills 本地化存储机制

Skills 文件支持​本地保存​,系统会为本地 Skills 自动生成专属访问 URL,LLM 可通过接口远程加载并运行 Skills,支持私有化部署与企业级管理。

这使 Skills 从"运行态能力"升级为可管理、可分发、可治理的能力资产。

开源项目中,Skills 文件现已支持​本地保存​:

  • 系统自动生成专属访问 URL
  • 大模型可通过接口远程加载 Skills
  • 支持私有化部署与企业级管理

4.2 Skills 生成质量优化

系统现在可以基于用户历史消息生成更完整、更结构化的 Skills 描述,使技能从"零散规则"升级为结构化能力模块。

配置本地储存(开源)(简要步骤):

Step 1: 添加环境变量到项目根目录的.env 文件

SKILLS_REPO_BACKEND=LOCAL
SKILLS_LOCAL_DIR=/tmp/upload_skill_memory/ # 最终存储位置
SKILLS_LOCAL_TMP_DIR=/tmp/skill_memory/ # 生成时的临时位置
SKILLS_LLM=gpt-4o

Step 2: 启动本地服务

uvicorn memos.api.server_api:app --host 0.0.0.0 --port 8001 --workers 1

4.3 MindDock 插件能力接入

插件 MindDock 现已支持:

  • 在 ChatGPT;
  • 千问;
  • 等多平台聊天环境中。

并支持实时注入 Skills,使 Skills 成为​跨平台通用能力层​,而非单一模型绑定能力。

44.png

  1. MCP 删除记忆路径增强:删除不再是“弱操作”

为更好支持用户删除记忆的意图识别与落地,MCP 处理逻辑更新为:

  • 在识别到删除意图后,调用 deleteMemory 接口直接删除对应记忆;
  • 同时调用 addFeedback 接口以记录用户反馈并更新相关记忆项,确保删除操作更可靠且可审计。

从“模糊删除”升级为​双通道强语义删除机制​,确保用户对记忆控制权的完整性与可靠性。

55.png

6. 记忆调度模块化重构:工程级稳定性升级

记忆调度任务处理器实现模块化重构并集中统一管理。

重构内容包括:

  • 将检索流程拆分为:search enhancererank filter 四阶段;
  • 新增 search_service 统一 API 与 Scheduler 的文本检索实现;
  • 修复 Redis Streams 调度消息序列化问题,补齐 mem_read/pref_add processoruser_context 传递。

我们提升了调度的可靠性、可观测性与可扩展性,便于在高并发场景下稳定运行。

66.png

7. 文档记忆双轨检索:记忆 + 原文 + 上下文协同

新能力​:

  • 支持​原文片段(RawFileMemory)与记忆(SummaryMemory)混合检索​,并可按需同时召回原文上下文以增强长文本语义连贯性;
  • search_memory_type 支持三种模式:All(原文 + 记忆混合)、AllSummaryMemory(仅记忆)、RawFileMemory(仅原文片段);
  • neighbor_discovery 配置用于是否召回原文分片的上下文。

现在,文档记忆同时具备:

  • 语义抽象能力;
  • 原文可追溯性;
  • 上下文连贯性。

开源示例​:

data{
  "user_id": "testfile", 
  "readable_cube_ids": ["testfile_cube"],
  "query": "minddock 适配什么浏览器",
  "search_memory_type": "AllSummaryMemory", # 三种检索模式 All | AllSummaryMemory | RawFileMemory
  "neighbor_discovery": "true", # 若想召回原文上下文则置为 True
}

检索到的结果中:memory_type 新增 RawFileMemory(记忆原文片段)。

8. 记忆过滤器(Filter)支持秒级时间精度

filter 字段现在支持秒级别时间范围过滤(例如 "create_time": "2026-02-12 10:00:00"),适用于检索/获取记忆与对话接口的精确时窗筛选,提高审计与时效性控制的能力。

示例​:

"filter" : {
  "and": [
    {"create_time": {"gt": "2026-02-01 10:00:00"}},
    {"create_time": {"lt": "2026-02-12 10:00:00"}}
  ]
}

9. 对话接口(Chat)稳定性与能力增强

  • 修复了 qwen3-32b 回答失败的问题,恢复模型可用性;
  • 对话接口现支持 relativity 字段,允许开发者在对话阶段控制召回记忆的相关性阈值,从源头减少低价值上下文注入。

对话系统在稳定性与成本控制层面同步升级。

10. 开源社区(CHANGELOG 摘要)

新增 / 新功能

  • 记忆检索优化(关键词检索 + 语义混合);
  • 文档记忆双轨检索:原文 + 记忆协同检索;
  • 文档记忆上下文唤醒(分片上下文);
  • relativity 精筛字段(0\~1);
  • MindDock 与云服务 Skill 支持;
  • MCP 删除意图触发 deleteMemoryaddFeedback
  • Chat 接口可传 relativity

改进

  • 检索 pipeline 重构(Search → Enhance → Rerank → Filter);
  • 调度任务处理器模块化与 Redis Streams 修复;
  • Skills 本地化存储与 URL 发布;
  • Skills 生成质量提升。

修复

  • Playground 使用体验问题修复;
  • 偏好记忆阈值字段使用错误修复;
  • 修复 get_memory 在复杂 filter 情形下的调用失败或卡顿问题;
  • 修复 Chat 接口 qwen3-32b 回答失败,兼容 LLM 的 enable thinking 参数。

关于 MemOS

MemOS 为 AGI 构建统一的记忆管理平台,让智能系统如大脑般拥有灵活、可迁移、可共享的长期记忆和即时记忆。

作为记忆张量首次提出“记忆调度”架构的 AI 记忆操作系统,我们希望通过 MemOS 全面重构模型记忆资源的生命周期管理,为智能系统提供高效且灵活的记忆管理能力。本次更新围绕"记忆系统工程化"和"Agent 能力结构化"两条主线,对云服务和开源项目做了系统升级。核心改进集中在多视角记忆、记忆版本管理、检索召回质量、Skills 本地化,以及若干生产环境的稳定性优化。
77.jpg

如果说 DeepSeek 让 AI 学会了说人话,那 GLM-Image 就是专治 AI 画图「听不懂人话」的老毛病——毕竟,谁还没被那些鬼画符文字气笑过呢?

原先的扩散模型手艺好,但耳朵背。现在的 AI 画图工具,像极了手艺精湛却耳背的 Tony 老师——你说招牌写开业大吉,他画出一串连考古学家都破译不了的符号。扩散模型训练稳定、泛化强,但面对复杂指令和知识密集型场景,总在信息表达和语义对齐上掉链子。

GLM-Image 的解法很务实:让专业的模块干专业的事。90 亿参数的自回归模块(基于 GLM-4-9B-0414)当阅读理解冠军,生成携带语义信号的视觉词元;70 亿参数的扩散解码器(沿袭 CogView4 架构)当像素级工匠,还原高频细节。文科生写剧本、理科生做特效,分工明确才能出大片。

除文本生成图像外,GLM-Image 还支持图像编辑、风格迁移、身份保持、多主体一致性。更关键的是,它终于能正确渲染中文了!通过集成 Glyph-byT5 进行字符级编码,开业大吉不会再变成开壶大古,海报设计师总算可以松口气了。

开源,为了好用而不只是能用,由智谱华章以开源形式发布的 GLM-Image 打破「高性能=闭源收费」的潜规则。160 亿总参数对开发者友好,自回归懂语义 + 扩散雕细节的混合架构,或将成为下一代模型的标配。

毕竟,我们要的不是抽卡式的运气游戏,而是能听懂复杂需求的靠谱搭档。当 AI 海报终于出现正确的汉字,记得感谢这个双脑协作的聪明架构——从耳背 Tony 到贴心设计师,GLM-Image 真的下了功夫。

教程链接: https://go.openbayes.com/cZzpu

使用云平台: OpenBayes

http://openbayes.com/console/signup?r=sony_0m6v

首先点击「公共教程」,找到「GLM-Image:首个全流程国产芯片训练模型」,单击打开。


页面跳转后,点击右上角「克隆」,将该教程克隆至自己的容器中。


在当前页面中看到的算力资源均可以在平台一键选择使用。平台会默认选配好原教程所使用的算力资源、镜像版本,不需要再进行手动选择。点击「继续执行」,等待分配资源。

若显示「Bad Gateway」,这表示模型正在加载中,请等待约 2-3 分钟后刷新页面即可。

使用步骤如下:

  1. 页面跳转后,点击左侧 README 页面,进入后点击上方「运行」。

  1. 点击运行后等待加载模型与初始化

  1. 待运行完成,即可点击右侧 API 地址跳转至 demo 页面。

  1. 打开后上传你想要的图片或文字,点击运行

  1. 成图展示


教程链接: https://go.openbayes.com/cZzpu

在广告投放系统中,IP地理定位往往是最基础的数据能力之一。但在实际业务中,很多团队为了节省成本,会优先采用免费IP库进行地域定向,那么免费IP库是否真的适用于广告投放?

本文基于本人自测数据,数据测试时间为2025.9月,从准确率与误差距离两个维度进行分析,得出相关结论。

一、为什么IP地址准确度对于广告系统更为重要?

其实很好理解,在程序化广告系统中,IP定位通常会对广告系统中的定向精准投放、用户来源定向、本地生活推送广告匹配、区域黑名单过渡、投放报表分析等多多项业务有所影响,所以一旦IP定位精度出现过多误差,将会直接影响以下对于广告系统的相关维度:

CPM浪费; ROI下降;广告主投诉; 数据分析偏差;合规风险

换句话说,IP数据质量不是“辅助数据”,而是投放链路中的基础设施。

二、测试设计说明

1.样本数据

构建100,000条真实IP→地理位置映射样本,覆盖全球主要国家、多个省/州、城市级别、IPv4与IPv6以真实定位数据作为基准进行比对。

2.测试对象(免费库)

本次选取两款常见免费IP数据库:

产品类型更新频率
DB-IP Free社区免费版月度
GeoLite 2社区免费版月度

三、实测结果

1️国家级准确率

产品国家准确率
DB-IP Free97.8%
GeoLite 299.1%

结论:国家级定位表现尚可,误差比例较低。

2️省/州级准确率

产品省级准确率
DB-IP Free86.5%
GeoLite 290.7%

结论:开始出现10%左右误差,已不适合做精细区域策略。

3️城市级准确率

产品城市准确率平均误差距离
DB-IP Free63.5%52.7km
GeoLite 270.1%38.9km

关键结论:

城市级误差明显

平均误差40–50公里

移动网络与云出口IP误差更大

在本地商户广告、区域商圈广告场景下,这种误差对于广告投放来说是不可接受的。
【工具对比】1免费IP库用于广告投放是否可靠?误差率实测报告.jpg

四、免费IP库为何不适合广告投放?

1 更新周期滞后

IP地址段变化频繁,而免费库多为月度更新,存在明显延迟。

2️ 数据采集维度有限

免费库通常基于公开数据与社区样本,缺乏商业级验证机制。

3️ 城市级定位天然误差

ISP出口聚合导致城市归属偏移,免费库难以持续修正。

4️ 无SLA保障

广告系统属于实时高并发场景,而免费库没有服务稳定性保障,没有数据纠错通道也没有相关的技术支持。

五、广告投放真实影响评估

我们在模拟广告投放场景中进行ROI偏移测算:

-城市级定向误差导致约8%–15%投放浪费
-本地商户广告转化率下降10%以上
-数据报表城市分布偏差显著

在流量规模较大的广告平台中,这种误差会被放大。

六、结论:免费库不适合用于广告投放

可以明确得出结论:

免费IP库可以用于测试环境、非核心统计分析,但不适合用于正式广告投放系统。

尤其是在以下场景:
城市级精准广告/本地生活服务投放/高预算品牌定向广告/跨境合规广告免费库的误差率与数据滞后,都会直接影响商业结果。

七、建议:采用专业IP地址库

对于广告系统,建议采用专业商业IP地址库,例如:

  • IP数据云-特点:高频更新,城市级精度优化,支持IPv4/IPv6,适合国内外广告系统接入
  • DB-IP商业版-特点:数据覆盖全面,稳定性较好,海外业务适用性强
  • IPnews-特点:强调实时数据更新,精度与稳定性均优于免费版本,适合对实时性要求高的投放系统

相比免费库,专业IP数据库通常具备:更高城市级准确率,更小平均误差距离,更高更新频率,SLA保障,数据纠错机制。

八、工程实践建议

建议广告系统采用分层策略:

1.生产环境使用商业IP库
1.关键流量实时API校验
1.对高价值流量进行多源交叉验证

在广告系统中,IP数据属于“基础数据能力”,节省IP成本,往往会放大广告成本。

2026 年,智能体将在企业级应用中取得哪些实质性突破?点击下载《2026 年 AI 与数据发展预测》白皮书,获悉专家一手前瞻,抢先拥抱新的工作方式!

语义层并非新兴概念。早在二十一世纪初我便开始使用该技术,并在多项分析解决方案(尤其是企业数据仓库)中主导其设计、配置与实施。

我曾使用多种语义层工具,包括 IBM Cognos Framework Manager 及微软数据平台的各类语义层功能。

事实上,几乎所有“商业智能(BI)+自助服务”场景都涉及语义层的应用。

近年来,由于智能体 AI 发展的需求,语义层正迎来复兴浪潮。因此,本文将围绕 Snowflake 平台语境,提出 7 项利用语义层实现 AI 就绪的战略建议。

注:本文全篇手动撰写,内容基于我的行业经验与专业知识、相关研究,以及对客户需求与行业趋势的观察。

利用语义层提升 Snowflake 平台上 Cortex AI Analyst 的 AI 提示准确性。如图所示,本例中的语义层集成位于顶部:VWS_CUSTOMER_DETAILS。图片来源:Dan Galavan

要点一:理解语义层的传统作用

首先需要明确基本概念。

根据《牛津英语词典》的定义:“语义学是语言学和逻辑学中研究意义的分支。

此外,维基百科将语义层定义为:

“……将复杂数据映射为熟悉的业务术语,例如产品、客户或收入。”

语义层能够为数据提供易于业务理解的、一致的解释视角,即一种抽象的“逻辑”视图。

 

数据抽象逻辑视图示例。图片来源:Dan Galavan

图中展示了一个抽象逻辑视图的示例,该视图在清晰描述数据的同时保持了更贴近业务用户需求的设计。

语义层可用于定义和配置以下内容:

  • 标准化定义与含义

  • 数据对象间的关系

  • 指标

语义层过去乃至现在仍广泛应用于商业智能场景。这对数据治理具有重要价值,有助于提升业务逻辑的一致性、可复用性及可维护性。

要点二:了解人工智能为何推动语义层“复兴”

通过语义层提升 Snowflake 平台上 Cortex AI Analyst 的 AI 提示词准确性。图中顶部的 VWS_CUSTOMER_DETAILS 即为此类语义层集成示例。

核心观点:语义层有助于提升 Agentic AI 文本转 SQL 的准确性。

换言之,当我们用 AI 增强商业智能分析时,若采用自然语言+智能体与数据进行交互,则必须将自然语言转换为底层数据仓库的查询语言(通常为 SQL)。

语义层可将大型语言模型处理(常称为“推理”)与基于规则的定义相结合。规则定义承载了业务理解与业务逻辑。

语义层有助于管控不准确性风险(亦称“幻觉”),从而提升可信人工智能的可靠性。

要点三:数据建模是这一领域的关键技能

如前所述,语义层是数据的“逻辑”或抽象视图。因此,在使用语义层时,我们无需深入了解数据存储库的内部工作机制。

但在设计语义层时,必须有人完成以下任务:

  • 确定使用哪些源数据库表及其他对象;

  • 梳理这些对象之间的关联关系;

  • 对语义层对象进行命名,确保其符合业务通俗性;

  • 制定相应指标逻辑;

  • 确认业务定义;

  • 简而言之,有效建立从物理数据库到语义层的映射关系。

物理数据模型示意图。图片来源:Dan Galavan

实现上述目标的前提是理解数据。从实施层面来看,数据建模角色最适合承担此项工作,同时需要结合业务用户等领域专家的意见进行完善。

要点四:AI 增强型语义层设计与人机协同机制

Human in the Loop:评估来自 Snowflake AI Cortex Analyst 的 AI 生成语义层建议。 图片来源:Dan Galavan

尽管人工智能能够提出建议并增强语义层设计,但从质量保证的角度来看,用户验证至关重要。若我们采用基于 AI 的语义层配置建议,则必须建立相应的“人机协同”审批流程,例如涉及:

  • 确定概念同义词;

  • 业务友好的描述定义;

  • 正确的提示词与查询组合。

以 Snowflake 云语义视图为例,其 Cortex AI 服务能够为语义层设计提供有益建议。这些建议可能包括描述、指标及查询语句。然而,必须由具备相应领域知识的系统用户对这些建议进行评估。

 

Snowflake Cortex Analyst 语义视图已验证查询。 图片来源:Dan Galavan

一个典型产出是已验证查询。在此场景中,系统会监测自然语言提示词及其对应查询,以识别潜在可复用的模式。一旦发现候选查询,系统将自动向用户推送建议。用户可选择接受、编辑或拒绝该条目。

要点五:技术栈兼容性

如同技术生态系统的任何组成部分,语义层的兼容性始终是需要考量的关键因素。因此,务必确保您的技术栈能够满足实际需求。

以 Snowflake 平台为例,其功能支持包括:

  • 配置语义视图时提供多种接口与选项,例如 SQL、用户界面、REST API 及 YAML 格式,并集成了智能体 AI 功能;

  • 支持第三方商业智能工具,如 Sigma、Omni、Honeydew 和 Hex;

  • 兼容第三方设计与交付工具,例如采用 dbt 进行集成测试,且其路线图中已纳入 SqlDBM 的设计功能。

要点六:语义层开放标准即将到来

该标准源于开放语义交换规范(OSI)。图片来源:Snowflake

开放标准不仅能降低供应商锁定风险,还有助于推动技术普及。当前,语义层开放标准——开放语义交换规范(OSI)正在制定中。

该标准主要关注以下三个核心领域:

  • 标准化:提供统一的定义语言;

  • 互操作性:促进数据交换;

  • 可扩展性:支持模型随数据需求演进而灵活适配。

该项目已获得众多机构的共同推进,参与方涵盖从 Blackrock 到 AWS 再到 Salesforce 等各类组织。

从数据与人工智能战略视角来看,这一开放标准将带来显著价值,特别是在 AI/BI 工具及相关互操作性方面。

要点七:面向语义层与智能体人工智能的治理方法

若无法将语义层开放给智能体人工智能(Agentic AI),则前述所有工作均无法实现。

再次以 Snowflake 平台为例,我们可以配置 AI 智能体,使其调用 Cortex Analyst 语义视图。这将为智能体提供我们在前文讨论的各个领域的明确指引,例如:

  • 数据含义;

  • 指标定义;

  • 针对智能体提示的已验证预制查询语句。

语义视图还可应用于 BI 工具、Notebook 环境或 AI/MCP 集成中。

利用 Snowflake Intelligence 功能,通过智能体人工智能+ Cortex Analyst 语义视图提交自然语言提示。图片来源:Dan Galavan

此外,Snowflake Intelligence 功能可结合前述 AI 智能体与语义视图对数据进行查询。我们能够通过提交自然语言提示生成图表,并识别数据趋势。另一优势在于,Snowflake 的生成式人工智能能力与平台整体的数据治理功能紧密结合。

我曾在生成式人工智能方兴未艾的 2023 年 Snowflake 全球峰会上就此话题发表过演讲。

结论

正如我们所看到的,语义层在与商业智能(BI)工作负载相关的领域虽已存在多年,但当前正迎来一场智能体人工智能(Agentic AI)的复兴。这场复兴的驱动力,源于确保“与数据对话”时实现可信人工智能的迫切需求。换句话说,这正是我们在“以 AI 增强 BI”的过程中所要应对的挑战。

然而,在此过程中,务必牢记以下几点:

数据建模在此背景下为何扮演关键角色

  • AI 增强如何助力语义层的设计

  • Human in the Loop 的重要性

  • 即将发布的开放语义交换规范

  • 采用治理化方法运用智能体人工智能(本文以 Snowflake Intelligence 平台的语义视图为例进行说明)

  • 综合考虑以上要素,将有助于有效推进数据与人工智能战略的落地。

我还有许多相关的实践建议希望分享,并期待能尽快抽时间撰写成文。敬请持续关注!

在此,预祝各位在语义层与智能体人工智能的探索之旅中一切顺利。如有任何疑问,欢迎随时联系

© Dan Galavan 2026

Dan Galavan 是一名独立的数据治理与数据管理顾问,拥有 27 年的客户咨询与数据解决方案交付领导经验。他自 2019 年起开始使用 Snowflake 数据平台,持有都柏林大学学院颁发的“商业高级人工智能”文凭,并已获得 Snowflake Snowpro 高级架构师认证。

原文地址:https://medium.com/snowflake/snowflake-in-a-nutshell-7-strategic-ai-semantic-layer-tips-78422f7e0bdc

点击链接立即报名注册:Ascent - Snowflake Platform Training - China更多 Snowflake 精彩活动请关注专区

 

概要

该方案是一个典型的用空间(Redis缓存)和异步化换取时间(响应速度)和系统稳定性(数据库抗压) 的架构设计。它非常适合点赞这类写多读多、对实时一致性要求稍低的业务场景。

前言

学习java过程中的心得,如有错误请提醒作者纠正,感谢不尽!!!

如果有更好的实现,欢迎分享!!!

当前实现优点

  1. 高性能与低延迟

    • 写操作快:用户点赞/拉踩请求直接操作内存数据库Redis,响应速度极快,用户体验好。
    • 数据库在定时任务触发前压力小:高频的写操作被Redis承接,避免了直接冲击MySQL
  2. 数据一致性保障

    • 原子性操作:使用Lua脚本在Redis内完成状态切换(点赞->拉踩->无状态),保证了“一个评论同一时刻只能有一种状态”的业务逻辑的原子性,防止并发请求导致的数据错乱。
    • 分布式锁:对单个用户的操作加锁(RLock),防止同一用户极短时间内的重复提交造成缓存数据问题。
  3. 批量处理效率高

    • 异步落库:定时任务将缓存中的大量变更集中起来,通过批量插入/更新ON DUPLICATE KEY UPDATE)和批量更新统计CASE WHEN)的方式与数据库交互,极大地减少了网络I/O和SQL执行次数,数据库处理效率高。

当前实现存在问题

  1. 定时任务引发的数据库峰值:

    1. 可通过使用消息队列削峰填谷
    2. 将定时任务从处理全部数据改为处理部分数据,定时任务的周期调低

主要实现细节

Redis + Redis分布式锁 + 原子操作 + 异步落库 + SpringBoot定时任务

流程图

请求方法设计

请求URL/api/article/v1/{commentId}/vote

请求方法:PUT

请求参数:type(Integer);(type=0表示修改成无状态,type=1表示修改成点赞状态,type=-1表示修改成拉踩状态)

URL示例:/api/article/v1/{commentId}/vote?type = 1

Redis表设计

下述三表都用来记录用户对评论的状态(点赞、拉踩、无状态)

articles:comments:likes:users:{userId}

{userId}为动态键名

Redis Set数据结构;

存储的值:{commentId}

articles:comments:dislikes:users:{userId}

{userId}为动态键名

Redis Set数据结构;

存储的值:{commentId}

articles:comments:stateless:users:{userId}

{userId}为动态键名

Redis Set数据结构;

存储的值:{commentId}

MySQL表设计

该博客聚焦实现点赞/拉踩功能,涉及x_article_comments表不多,故不做x_article_comments表的字段介绍

  • x_article_comments_votes

    • 联合主键(comment_id, user_id)
    • vote字段表用户对评论的状态,1代表点赞,-1代表拉踩,0代表无状态,即不处于点赞状态或拉踩状态
-- 文章评论表
CREATE TABLE x_article_comments (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  article_id BIGINT UNSIGNED NOT NULL,            -- 关联的文章
  parent_id BIGINT UNSIGNED NULL,               -- NULL 表示顶级评论;否则是回复
  root_id BIGINT UNSIGNED NULL,                 -- 同一Thread的Root评论 id(便于查询整个Thread)
  user_id BIGINT UNSIGNED NOT NULL,             -- 评论作者
  content TEXT NOT NULL,
  status TINYINT DEFAULT 1,            -- 1=显示,0=已删除/隐藏,2=待审核 等
  like_count INT DEFAULT 0,
  dislike_count INT DEFAULT 0,
  reply_count INT DEFAULT 0, -- 该层孩子的数量
  reply_descendant_count INT DEFAULT 0, -- 该层后代的数量
  version INT DEFAULT 0,               -- 乐观锁(更新时校验)
  create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
  update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (id)
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_unicode_ci;

-- 点赞/踩表(每个用户对某条评论的动作)
CREATE TABLE x_article_comments_votes (
  comment_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  vote TINYINT NOT NULL, -- 1=like, -1=dislike, 0=none
  create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
  update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (comment_id, user_id)
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_unicode_ci;

后端实现细节

处理用户发起的点赞/拉踩/无状态请求

Controller层

校验参数

先校验当前用户是否登录,userReadApi.getCurrentUserId()是我封装好的方法,用于获取发起当前请求的用户ID

        // 获取当前用户ID
        Long userId = userReadApi.getCurrentUserId();
        if (userId == null) {
            return AjaxResult.error(HttpStatus.UNAUTHORIZED, "用户未登录或获取用户ID失败");
        }
完整代码
    /**
     * 点赞/拉踩/无状态评论
     */
    @PutMapping("/{commentId}/vote")
    public AjaxResult voteComment(
            @PathVariable("commentId") Long commentId,
            @RequestParam("type") Integer type
    ) {
        log.info("进入请求 /api/article/v1/{}/vote -> 点赞/取消点赞评论 type={}", commentId, type);

        // 获取当前用户ID
        Long userId = userReadApi.getCurrentUserId();
        if (userId == null) {
            return AjaxResult.error(HttpStatus.UNAUTHORIZED, "用户未登录或获取用户ID失败");
        }


        articleService.voteComment(userId, commentId, type);

        log.info("返回结果 /api/article/v1/{}/vote -> OK", commentId);
        return AjaxResult.success();
    }

Service层

校验参数
        // 1. 参数校验
        if (commentId == null || type == null) {
            throw new ClientException(HttpStatus.BAD_REQUEST, "参数不完整");
        }
        if (type != ArticleCommentVoteConstants.LIKE && type != ArticleCommentVoteConstants.DISLIKE && type != ArticleCommentVoteConstants.STATELESS) {
            throw new ClientException(HttpStatus.BAD_REQUEST, "投票类型非法");
        }

        // 2. 校验评论是否存在
        ArticleComment comment = articleCommentMapper.selectArticleCommentById(commentId);
        if (comment == null) {
            throw new ClientException(HttpStatus.NOT_FOUND, "评论不存在");
        }
分布式锁 + lua脚本

使用了redisson实现加锁,并使用了lua脚本保证原子性,从而防止用户频繁发起请求导致在redis缓存的数据出现错误的情况

分布式锁相关代码
        // 5. 尝试获取分布式锁
        RLock lock = redisson.getLock(RedisKeyConstants.ARTICLES_COMMENTS_VOTES_LOCK_KEY + userId);
        boolean isLocked = false;
        try {
            isLocked = lock.tryLock(0, RedisKeyConstants.ARTICLES_COMMENTS_VOTES_LOCK_KEY_EXPIRE_TIME, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new ClientException(HttpStatus.SERVICE_UNAVAILABLE, "系统繁忙,请稍后再试");
            } else {
                // 获取到锁,执行投票逻辑,此处省略
                // ...
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new ClientException(HttpStatus.ERROR, "系统繁忙,请稍后再试");
        } finally {
            if (isLocked && lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.info("用户 {} 的锁已释放", userId);
            }
        }
lua脚本相关代码

预先准备好redis键和lua脚本

        // 3. 构建 Redis 键
        String likeKey = RedisKeyConstants.ARTICLES_COMMENTS_LIKES_USERS_KEY + userId;
        String dislikeKey = RedisKeyConstants.ARTICLES_COMMENTS_DISLIKES_USERS_KEY + userId;
        String statelessKey = RedisKeyConstants.ARTICLES_COMMENTS_STATELESS_USERS_KEY + userId;
        // 4. 根据投票类型处理逻辑
        DefaultRedisScript<Long> deleteScript = new DefaultRedisScript<>();
        deleteScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(RedisLuaConstants.ARTICLE_COMMENTS_VOTE_LUA_SCRIPT_PATH)));
        deleteScript.setResultType(Long.class);
        Long execute = null;

lua脚本文件

lua脚本能够保证原子性,若用户频繁发起请求也能保证以下的要求

三个键对应的set集合里的值应保持互斥,只允许一个值如3只能出现在一个键,例如:共有三个键like:{userId}、dislike:{userId}、stateless:{userId},键like现在持有3,用户对commentId = 3发起了点赞/拉踩/无状态请求,修改为拉踩,此时要去除like:{userId}键和stateless:{userId}键的set集合中值为3的元素,执行完后只有键dislike:{userId}存在值3

-- Lua 脚本:处理评论投票逻辑
local mainKey = KEYS[1]       -- 进行add的键
local srem1Key = KEYS[2]    -- 进行srem的键
local srem2Key = KEYS[3]  -- 进行srem的键
local commentId = ARGV[1]     -- 评论 ID

-- 添加到main集合
redis.call('SADD', mainKey, commentId)

-- 从两个集合中移除
redis.call('SREM', srem1Key, commentId)
redis.call('SREM', srem2Key, commentId)

-- 返回操作结果(可选)
return 1  -- 表示成功执行

获取到锁后,为实现存进redis,执行的操作如下

                switch (type) {
                    case ArticleCommentVoteConstants.LIKE:
                        // 4. 处理点赞逻辑
                        execute = stringRedisTemplate.execute(deleteScript, Arrays.asList(likeKey, dislikeKey, statelessKey), commentId.toString());
                        if (execute == null) {
                            throw new ClientException(HttpStatus.SERVICE_UNAVAILABLE, "处理点踩逻辑失败");
                        }
                        break;
                    case ArticleCommentVoteConstants.DISLIKE:
                        // 5. 处理点踩逻辑
                        execute = stringRedisTemplate.execute(deleteScript, Arrays.asList(dislikeKey, likeKey, statelessKey), commentId.toString());
                        if (execute == null) {
                            throw new ClientException(HttpStatus.SERVICE_UNAVAILABLE, "处理点踩逻辑失败");
                        }
                        break;
                    case ArticleCommentVoteConstants.STATELESS:
                        execute = stringRedisTemplate.execute(deleteScript, Arrays.asList(statelessKey, dislikeKey, likeKey), commentId.toString());
                        if (execute == null) {
                            throw new ClientException(HttpStatus.ERROR, "处理无状态逻辑失败");
                        }
                        break;
                    default:
                        throw new ClientException(HttpStatus.BAD_REQUEST, "未知的投票类型");
                }
完整代码
    @Override
    public void voteComment(Long userId, Long commentId, Integer type) {
        // 1. 参数校验
        if (commentId == null || type == null) {
            throw new ClientException(HttpStatus.BAD_REQUEST, "参数不完整");
        }
        if (type != ArticleCommentVoteConstants.LIKE && type != ArticleCommentVoteConstants.DISLIKE && type != ArticleCommentVoteConstants.STATELESS) {
            throw new ClientException(HttpStatus.BAD_REQUEST, "投票类型非法");
        }

        // 2. 校验评论是否存在
        ArticleComment comment = articleCommentMapper.selectArticleCommentById(commentId);
        if (comment == null) {
            throw new ClientException(HttpStatus.NOT_FOUND, "评论不存在");
        }

        // 3. 构建 Redis 键
        String likeKey = RedisKeyConstants.ARTICLES_COMMENTS_LIKES_USERS_KEY + userId;
        String dislikeKey = RedisKeyConstants.ARTICLES_COMMENTS_DISLIKES_USERS_KEY + userId;
        String statelessKey = RedisKeyConstants.ARTICLES_COMMENTS_STATELESS_USERS_KEY + userId;
        // 4. 根据投票类型处理逻辑
        DefaultRedisScript<Long> deleteScript = new DefaultRedisScript<>();
        deleteScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(RedisLuaConstants.ARTICLE_COMMENTS_VOTE_LUA_SCRIPT_PATH)));
        deleteScript.setResultType(Long.class);
        Long execute = null;
        // 5. 尝试获取分布式锁
        RLock lock = redisson.getLock(RedisKeyConstants.ARTICLES_COMMENTS_VOTES_LOCK_KEY + userId);
        boolean isLocked = false;
        try {
            isLocked = lock.tryLock(0, RedisKeyConstants.ARTICLES_COMMENTS_VOTES_LOCK_KEY_EXPIRE_TIME, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new ClientException(HttpStatus.SERVICE_UNAVAILABLE, "系统繁忙,请稍后再试");
            } else {
                // 获取到锁,执行投票逻辑
                switch (type) {
                    case ArticleCommentVoteConstants.LIKE:
                        // 4. 处理点赞逻辑
                        execute = stringRedisTemplate.execute(deleteScript, Arrays.asList(likeKey, dislikeKey, statelessKey), commentId.toString());
                        if (execute == null) {
                            throw new ClientException(HttpStatus.SERVICE_UNAVAILABLE, "处理点踩逻辑失败");
                        }
                        break;
                    case ArticleCommentVoteConstants.DISLIKE:
                        // 5. 处理点踩逻辑
                        execute = stringRedisTemplate.execute(deleteScript, Arrays.asList(dislikeKey, likeKey, statelessKey), commentId.toString());
                        if (execute == null) {
                            throw new ClientException(HttpStatus.SERVICE_UNAVAILABLE, "处理点踩逻辑失败");
                        }
                        break;
                    case ArticleCommentVoteConstants.STATELESS:
                        execute = stringRedisTemplate.execute(deleteScript, Arrays.asList(statelessKey, dislikeKey, likeKey), commentId.toString());
                        if (execute == null) {
                            throw new ClientException(HttpStatus.ERROR, "处理无状态逻辑失败");
                        }
                        break;
                    default:
                        throw new ClientException(HttpStatus.BAD_REQUEST, "未知的投票类型");
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new ClientException(HttpStatus.ERROR, "系统繁忙,请稍后再试");
        } finally {
            if (isLocked && lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.info("用户 {} 的锁已释放", userId);
            }
        }
    }

定时任务

定时任务要实现落库到MySQL中,并且清空redis对应的缓存

创建定时任务

@Component
@Slf4j
public class VoteSyncScheduler {

    private final ArticleService articleService;
    public VoteSyncScheduler(ArticleService articleService) {
        this.articleService = articleService;
    }

    @Scheduled(fixedRate = 5 * 60 * 1000) // 每五分钟执行一次
    public void syncVote() {
        log.info("开始执行点赞同步任务...");
        Boolean result = articleService.syncVote();
        log.info("点赞同步任务执行完成。");
    }
}

syncVote实现

MySQL涉及操作多张表,所以需要在方法上加上注解@Transactional

参考流程图

  1. 我们首先要从redis中获取数据,并将数据封装到两个集合中。
  2. 落库到MySQL
  3. 根据x_article_comments_votes表,通过SQL语句统计出like_count, dislike_count
  4. 将统计出的数据更新到x_article_comments
  5. 清除已在该定时任务处理完的redis缓存数据
变量说明
  • List<ArticleCommentVote> votes = new ArrayList<>(); // 保存所有投票数据
  • Set<Long> TotalcommentId = new HashSet<>(); // 保存所有评论id
  • likesUserIdToCommentIdsMap、dislikesUserIdToCommentIdsMap、statelessUserIdToCommentIdsMap——存储的值为经过逻辑后键中已被处理的值,在最后一步清除redis缓存发挥作用
从redis中获取数据,并将数据封装到两个集合中
        Set<String> userLikesKeys = stringRedisTemplate.keys(RedisKeyConstants.ARTICLES_COMMENTS_LIKES_USERS_KEY + "*");
        Set<String> userDislikesKeys = stringRedisTemplate.keys(RedisKeyConstants.ARTICLES_COMMENTS_DISLIKES_USERS_KEY + "*");
        Set<String> userStatelessKeys = stringRedisTemplate.keys(RedisKeyConstants.ARTICLES_COMMENTS_STATELESS_USERS_KEY + "*");
        if (userLikesKeys.isEmpty() && userDislikesKeys.isEmpty() && userStatelessKeys.isEmpty()){
            log.info("没有需要同步的数据");
            return true;
        }
        List<ArticleCommentVote> votes = new ArrayList<>();      // 保存所有投票数据
        Set<Long> TotalcommentId = new HashSet<>();     // 保存所有评论id
        Map<Long, Set<String>> likesUserIdToCommentIdsMap = new HashMap<>();
        Map<Long, Set<String>> dislikesUserIdToCommentIdsMap = new HashMap<>();
        Map<Long, Set<String>> statelessUserIdToCommentIdsMap = new HashMap<>();
        for (String userLikesKey : userLikesKeys) {
            String userId = userLikesKey.replace(RedisKeyConstants.ARTICLES_COMMENTS_LIKES_USERS_KEY, "");
            Set<String> commentIds = stringRedisTemplate.opsForSet().members(userLikesKey);
            for (String commentId : commentIds) {
                votes.add(new ArticleCommentVote(Long.parseLong(commentId), Long.parseLong(userId), ArticleCommentVoteConstants.LIKE, null, null));
                TotalcommentId.add(Long.parseLong(commentId));
                // 更新或新增likesUserIdToCommentIdsMap键值对,值的set集合新增commentId
                likesUserIdToCommentIdsMap.computeIfAbsent(Long.parseLong(userId), k -> new HashSet<>()).add(commentId);
            }
        }
        for (String userDislikesKey : userDislikesKeys) {
            String userId = userDislikesKey.replace(RedisKeyConstants.ARTICLES_COMMENTS_DISLIKES_USERS_KEY, "");
            Set<String> commentIds = stringRedisTemplate.opsForSet().members(userDislikesKey);
            for (String commentId : commentIds) {
                votes.add(new ArticleCommentVote(Long.parseLong(commentId), Long.parseLong(userId), ArticleCommentVoteConstants.DISLIKE, null, null));
                TotalcommentId.add(Long.parseLong(commentId));
                // 更新或新增dislikesUserIdToCommentIdsMap键值对,值的set集合新增commentId
                dislikesUserIdToCommentIdsMap.computeIfAbsent(Long.parseLong(userId), k -> new HashSet<>()).add(commentId);
            }
        }
        for (String userStatelessKey : userStatelessKeys) {
            String userId = userStatelessKey.replace(RedisKeyConstants.ARTICLES_COMMENTS_STATELESS_USERS_KEY, "");
            Set<String> commentIds = stringRedisTemplate.opsForSet().members(userStatelessKey);
            for (String commentId : commentIds) {
                votes.add(new ArticleCommentVote(Long.parseLong(commentId), Long.parseLong(userId), ArticleCommentVoteConstants.STATELESS, null, null));
                TotalcommentId.add(Long.parseLong(commentId));
                // 添加到statelessUserIdToCommentIdsMap键值对,值的set集合新增commentId
                statelessUserIdToCommentIdsMap.computeIfAbsent(Long.parseLong(userId), k -> new HashSet<>()).add(commentId);
            }
        }
落库到MySQL
Service层相关代码
        // 根据votes执行落库逻辑(包含插入/更新)
        if (!votes.isEmpty()){
            int i = articleCommentVoteMapper.batchInsertOrUpdate(votes);
            if (i < 0){
                throw new ClientException(HttpStatus.ERROR, "批量插入或更新数据失败");
            }
            log.info("批量插入或更新数据成功,数量为:{}", i);
        } else {
            log.info("没有需要落库的数据");
            return true;
        }
SQL语句相关实现
    <!-- 原有方法 -->
    <insert id="batchInsertOrUpdate" parameterType="java.util.List">
        INSERT INTO x_article_comments_votes (
            comment_id,
            user_id,
            vote,
            create_time,
            update_time
        )
        VALUES
        <foreach collection="list" item="item" separator=",">
            (
                #{item.commentId},
                #{item.userId},
                #{item.vote},
                NOW(),
                NOW()
            )
        </foreach>
        ON DUPLICATE KEY UPDATE
            vote = VALUES(vote),
            update_time = VALUES(update_time)
    </insert>
根据x_article_comments_votes表,通过SQL语句统计出like_count, dislike_count
Service层相关代码
        List<UpdateCommentVoteCountDTO> updateCommentVoteCountDTOList = getCommentLikeCountAndDislikeCountByListOfCommentId(TotalcommentId);
SQL语句相关实现
<!-- 新增方法: 聚合查询点赞/点踩数量 -->
<select id="selectCommentLikeCountAndDislikeCountByListOfCommentId" resultType="com.anon.spaceblogserver.modules.article.POJO.DTO.UpdateCommentVoteCountDTO">
    SELECT
        comment_id AS commentId,
        SUM(CASE WHEN vote = 1 THEN 1 ELSE 0 END) AS likeCount,
        SUM(CASE WHEN vote = -1 THEN 1 ELSE 0 END) AS dislikeCount
    FROM x_article_comments_votes
    WHERE comment_id IN
    <foreach collection="commentIds" item="commentId" open="(" separator="," close=")">
        #{commentId}
    </foreach>
    GROUP BY comment_id
</select>
将统计出的数据批量更新到x_article_comments
Service层相关代码
int updated = batchUpdateCommentLikeCountAndDislikeCount(updateCommentVoteCountDTOList, MySQLBatchSizeConstants.DEFAULT_UPDATE_BATCH_SIZE);
if (updated < 0){
    throw new ClientException(HttpStatus.ERROR, "批量更新数据失败");
}
log.info("批量更新数据成功,更新数量为:{}", updated);
限制单条SQL语句长度,批量更新操作具体实现

因为SQL语句采用了case when减少网络往返,为限制SQL语句长度防止溢出以及影响性能,采用的策略如下

若检测到参数updateCommentVoteCountDTOList超过指定个数,将会拆分成多条SQL语句与MySQL数据库进行交互

/**
 * 批量更新评论的点赞数和点踩数
 * @param updateCommentVoteCountDTOList     需要更新的评论点赞数和点踩数列表
 * @param batchSize                   批次大小,建议根据实际情况调整,过大可能导致单次更新过慢,过小可能导致更新次数过多
 * @return      成功更新的记录数
 */
public int batchUpdateCommentLikeCountAndDislikeCount(List<UpdateCommentVoteCountDTO> updateCommentVoteCountDTOList, Integer batchSize) {
    int totalUpdated = 0;
    for (int i = 0; i < updateCommentVoteCountDTOList.size(); i += batchSize) {
        // 截取当前批次的数据
        List<UpdateCommentVoteCountDTO> batch = updateCommentVoteCountDTOList.subList(
                i,
                Math.min(i + batchSize, updateCommentVoteCountDTOList.size())
        );
        // 执行当前批次的更新
        int updated = articleCommentMapper.batchUpdateCommentLikeCountAndDislikeCount(batch);
        if (updated < 0) {
            log.error("批量更新数据失败,当前批次更新数量: {}", updated);
            return -1;
        }
        totalUpdated += updated;

        log.info("批量更新评论点赞/点踩数,当前批次更新数量: {}, 累计更新数量: {}", updated, totalUpdated);
    }
    return totalUpdated;
}
SQL语句相关实现
<update id="batchUpdateCommentLikeCountAndDislikeCount">
    UPDATE x_article_comments
    SET like_count = CASE id
    <foreach collection="list" item="item">
        WHEN #{item.commentId} THEN #{item.likeCount}
    </foreach>
    END,
    dislike_count = CASE id
    <foreach collection="list" item="item">
        WHEN #{item.commentId} THEN #{item.dislikeCount}
    </foreach>
    END,
    update_time = NOW()
    WHERE id IN
    <foreach collection="list" item="item" open="(" separator="," close=")">
        #{item.commentId}
    </foreach>
</update>
清除已在该定时任务处理完的redis缓存数据

前面提到的变量——likesUserIdToCommentIdsMap、dislikesUserIdToCommentIdsMap、statelessUserIdToCommentIdsMap。存储的值为经过逻辑后键中已被处理的值,在最后一步清除redis缓存发挥作用

上述变量类型形式为Map<Long, Set<String>>,满足了userId -> 评论id集合,这可以精准且方便的清除已处理后的redis缓存数据

构建上述三个Map集合的过程在第一步操作中经历三个for循环已经构建完毕

该操作同样用到lua脚本保证原子性

lua脚本

该脚本实现安全批量的移除key中要删除的元素列表为什么不直接把键删除的原因 -> 假如在该定时任务执行过程中以及该脚本执行之前用户又发起点赞/拉踩/无状态请求,若与定时任务触发后从redis查到的用户对评论的状态相等,则会移除,这并没有什么问题,若不相等,不会移除新请求中用户设置的点赞/拉踩/无状态,留到下一次定时任务触发后处理

-- 安全批量删除,包含key存在性检查
-- KEYS[1]: Set的key
-- ARGV[1..n]: 要删除的元素列表
--
local key = KEYS[1]
local key_type = redis.call('TYPE', key).ok

-- 检查key是否存在且类型为set
if key_type == 'none' then
    return 0
elseif key_type ~= 'set' then
    return redis.error_reply('WRONGTYPE Operation against a key holding the wrong kind of value')
end

-- 兼容 Lua 5.1 和 Lua 5.2+
local unpack = unpack or table.unpack

local result = 0

-- 如果 ARGV 不为空,则执行批量删除
if #ARGV > 0 then
    result = redis.call('SREM', key, unpack(ARGV))
end

return result
Service层相关代码
// 清空Redis中的数据
DefaultRedisScript<Long> deleteScript = new DefaultRedisScript<>();
deleteScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(RedisLuaConstants.BATCH_REMOVE_MEMBERS_FROM_SET_LUA_SCRIPT_PATH)));
deleteScript.setResultType(Long.class);
likesUserIdToCommentIdsMap.forEach((userId, commentIdSet) -> {
    Long result = stringRedisTemplate.execute(deleteScript, List.of(RedisKeyConstants.ARTICLES_COMMENTS_LIKES_USERS_KEY + userId.toString()), commentIdSet.toArray());
    if (result == null || result < 0){
        log.error("批量删除用户 {} 缓存点赞的评论失败,欲删除的commentId -> {}", userId, commentIdSet);
    }
});
dislikesUserIdToCommentIdsMap.forEach((userId, commentIdSet) -> {
    Long result = stringRedisTemplate.execute(deleteScript, List.of(RedisKeyConstants.ARTICLES_COMMENTS_DISLIKES_USERS_KEY + userId.toString()), commentIdSet.toArray());
    if (result == null || result < 0){
        log.error("批量删除用户 {} 缓存点踩的评论失败,欲删除的commentId -> {}", userId, commentIdSet);
    }
});
statelessUserIdToCommentIdsMap.forEach((userId, commentIdSet) -> {
    Long result = stringRedisTemplate.execute(deleteScript, List.of(RedisKeyConstants.ARTICLES_COMMENTS_STATELESS_USERS_KEY + userId.toString()), commentIdSet.toArray());
    if (result == null || result < 0){
        log.error("批量删除用户 {} 缓存无状态的评论失败,欲删除的commentId -> {}", userId, commentIdSet);
    }
});

SyncVote完整代码

@Transactional
@Override
public Boolean syncVote() {
    //TODO 发散思维,该段逻辑似乎在收藏、投币等功能有相似之处,该段之所以有点复杂是因为有三种状态————点赞、点踩、无状态,且三者之间是互斥的,但前面的收藏、投币功能没有这个问题,并且好像只有两个状态,日后可以抽象出一个通用方法来处理这类功能,减少代码重复度
    Set<String> userLikesKeys = stringRedisTemplate.keys(RedisKeyConstants.ARTICLES_COMMENTS_LIKES_USERS_KEY + "*");
    Set<String> userDislikesKeys = stringRedisTemplate.keys(RedisKeyConstants.ARTICLES_COMMENTS_DISLIKES_USERS_KEY + "*");
    Set<String> userStatelessKeys = stringRedisTemplate.keys(RedisKeyConstants.ARTICLES_COMMENTS_STATELESS_USERS_KEY + "*");
    if (userLikesKeys.isEmpty() && userDislikesKeys.isEmpty() && userStatelessKeys.isEmpty()){
        log.info("没有需要同步的数据");
        return true;
    }
    List<ArticleCommentVote> votes = new ArrayList<>();      // 保存所有投票数据
    Set<Long> TotalcommentId = new HashSet<>();     // 保存所有评论id
    Map<Long, Set<String>> likesUserIdToCommentIdsMap = new HashMap<>();
    Map<Long, Set<String>> dislikesUserIdToCommentIdsMap = new HashMap<>();
    Map<Long, Set<String>> statelessUserIdToCommentIdsMap = new HashMap<>();
    for (String userLikesKey : userLikesKeys) {
        String userId = userLikesKey.replace(RedisKeyConstants.ARTICLES_COMMENTS_LIKES_USERS_KEY, "");
        Set<String> commentIds = stringRedisTemplate.opsForSet().members(userLikesKey);
        for (String commentId : commentIds) {
            votes.add(new ArticleCommentVote(Long.parseLong(commentId), Long.parseLong(userId), ArticleCommentVoteConstants.LIKE, null, null));
            TotalcommentId.add(Long.parseLong(commentId));
            // 更新或新增likesUserIdToCommentIdsMap键值对,值的set集合新增commentId
            likesUserIdToCommentIdsMap.computeIfAbsent(Long.parseLong(userId), k -> new HashSet<>()).add(commentId);
        }
    }
    for (String userDislikesKey : userDislikesKeys) {
        String userId = userDislikesKey.replace(RedisKeyConstants.ARTICLES_COMMENTS_DISLIKES_USERS_KEY, "");
        Set<String> commentIds = stringRedisTemplate.opsForSet().members(userDislikesKey);
        for (String commentId : commentIds) {
            votes.add(new ArticleCommentVote(Long.parseLong(commentId), Long.parseLong(userId), ArticleCommentVoteConstants.DISLIKE, null, null));
            TotalcommentId.add(Long.parseLong(commentId));
            // 更新或新增dislikesUserIdToCommentIdsMap键值对,值的set集合新增commentId
            dislikesUserIdToCommentIdsMap.computeIfAbsent(Long.parseLong(userId), k -> new HashSet<>()).add(commentId);
        }
    }
    for (String userStatelessKey : userStatelessKeys) {
        String userId = userStatelessKey.replace(RedisKeyConstants.ARTICLES_COMMENTS_STATELESS_USERS_KEY, "");
        Set<String> commentIds = stringRedisTemplate.opsForSet().members(userStatelessKey);
        for (String commentId : commentIds) {
            votes.add(new ArticleCommentVote(Long.parseLong(commentId), Long.parseLong(userId), ArticleCommentVoteConstants.STATELESS, null, null));
            TotalcommentId.add(Long.parseLong(commentId));
            // 添加到statelessUserIdToCommentIdsMap键值对,值的set集合新增commentId
            statelessUserIdToCommentIdsMap.computeIfAbsent(Long.parseLong(userId), k -> new HashSet<>()).add(commentId);
        }
    }
    // 根据votes执行落库逻辑(包含插入/更新)
    if (!votes.isEmpty()){
        int i = articleCommentVoteMapper.batchInsertOrUpdate(votes);
        if (i < 0){
            throw new ClientException(HttpStatus.ERROR, "批量插入或更新数据失败");
        }
        log.info("批量插入或更新数据成功,数量为:{}", i);
    } else {
        log.info("没有需要落库的数据");
        return true;
    }
    // 根据TotalcommentId集合中的commentId,利用聚合函数获取对应表中对应评论的likeCount和dislikeCount,封装成List<updateCommentVoteCountDTO> updateCommentVoteCountDTOList
    List<UpdateCommentVoteCountDTO> updateCommentVoteCountDTOList = getCommentLikeCountAndDislikeCountByListOfCommentId(TotalcommentId);
    // 根据updateCommentVoteCountDTOList执行批量更新的逻辑,作用的表为x_article_comments,使用case when then语法更新likeCount和dislikeCount
    int updated = batchUpdateCommentLikeCountAndDislikeCount(updateCommentVoteCountDTOList, MySQLBatchSizeConstants.DEFAULT_UPDATE_BATCH_SIZE);
    if (updated < 0){
        throw new ClientException(HttpStatus.ERROR, "批量更新数据失败");
    }
    log.info("批量更新数据成功,更新数量为:{}", updated);
    // 清空Redis中的数据
    DefaultRedisScript<Long> deleteScript = new DefaultRedisScript<>();
    deleteScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(RedisLuaConstants.BATCH_REMOVE_MEMBERS_FROM_SET_LUA_SCRIPT_PATH)));
    deleteScript.setResultType(Long.class);
    likesUserIdToCommentIdsMap.forEach((userId, commentIdSet) -> {
        Long result = stringRedisTemplate.execute(deleteScript, List.of(RedisKeyConstants.ARTICLES_COMMENTS_LIKES_USERS_KEY + userId.toString()), commentIdSet.toArray());
        if (result == null || result < 0){
            log.error("批量删除用户 {} 缓存点赞的评论失败,欲删除的commentId -> {}", userId, commentIdSet);
        }
    });
    dislikesUserIdToCommentIdsMap.forEach((userId, commentIdSet) -> {
        Long result = stringRedisTemplate.execute(deleteScript, List.of(RedisKeyConstants.ARTICLES_COMMENTS_DISLIKES_USERS_KEY + userId.toString()), commentIdSet.toArray());
        if (result == null || result < 0){
            log.error("批量删除用户 {} 缓存点踩的评论失败,欲删除的commentId -> {}", userId, commentIdSet);
        }
    });
    statelessUserIdToCommentIdsMap.forEach((userId, commentIdSet) -> {
        Long result = stringRedisTemplate.execute(deleteScript, List.of(RedisKeyConstants.ARTICLES_COMMENTS_STATELESS_USERS_KEY + userId.toString()), commentIdSet.toArray());
        if (result == null || result < 0){
            log.error("批量删除用户 {} 缓存无状态的评论失败,欲删除的commentId -> {}", userId, commentIdSet);
        }
    });
    return true;
}