包含关键字 typecho 的文章

在前一篇 《Flink SQL 窗口(Window)操作详解》 中,我们已经打好了时间与窗口的基础。
但在真实业务里,单条流上的聚合往往只是第一步,更常见的需求是把多条业务流关联起来一起看,例如:

  • 订单流 + 支付流:衡量下单到付款的转化效果
  • 浏览流 + 下单流:分析从曝光、点击到下单的完整漏斗
  • 用户行为流 + 用户画像维表:驱动推荐、风控等在线决策

这些需求背后的共性能力就是:双流 JOIN

本文以「订单流 + 支付流」为主线,从环境准备、建表、造数到 JOIN 查询,一步步带你搞懂 Flink SQL 中的双流 JOIN 思路与实践。

一、双流 JOIN 适用的典型场景

  • 订单与支付关联:找出已下单但未支付、支付失败等情况
  • 广告曝光与点击关联:计算点击率、转化路径
  • 日志与告警规则关联:实时检测异常行为

这些场景有两个共同特征:

  • 两条都是事实流(不断追加的新事件)
  • 需要在时间范围内去匹配事件(谁先发生、允许多长时间内匹配)

因此在流计算中做 JOIN,一定绕不开时间字段水位线(Watermark)

二、Flink 中常见的 JOIN 类型

在 Flink SQL 的流模式下,常见的双流关联方式有:

  • 普通 JOIN:基于等值条件 + 时间字段的 JOIN
  • Interval Join:基于「时间区间」的双流 JOIN
  • Temporal Join:一条流 + 维表(变更流)的时态关联

本篇主要聚焦前两种,更贴近「订单流 + 支付流」这样的事实双流场景。

三、准备示例数据表

安装 Kafka(环境前提)

在 WSL2 的 Ubuntu 环境中安装并启动 Kafka,请参考 《从零开始学Flink:数据源》

安装 Flink Kafka SQL Connector

需要把 Flink 的 Kafka SQL Connector JAR 包,放到 $FLINK_HOME/lib 目录下。

以本系列示例使用的 Flink 1.20.1 + Kafka 3.4.0-1.20 为例,可以这样操作:

  1. 确认你的 Flink 安装目录(假设为 /opt/flink):

    export FLINK_HOME=/opt/flink
  1. 下载 Kafka SQL Connector JAR 到 Flink 的 lib 目录:

    cd $FLINK_HOME/lib
    wget https://repo1.maven.org/maven2/org/apache/flink/flink-sql-connector-kafka/3.4.0-1.20/flink-sql-connector-kafka-3.4.0-1.20.jar

    如果你是 Windows + WSL2,可以在 WSL2 里执行同样的命令;或者用浏览器下载后手动拷贝到 lib 目录。

  2. 如果你使用的是独立集群或远程集群,需要重启 Flink 集群,让新 JAR 在 JobManager/TaskManager 上生效:

    cd $FLINK_HOME
    bin/stop-cluster.sh
    bin/start-cluster.sh

    如果只是本地直接运行 bin/sql-client.sh 启动内嵌 mini-cluster,则只需重启 SQL Client 即可。

  3. 启动 Flink SQL Client,然后执行本文后续的建表与查询示例:

    cd $FLINK_HOME
    bin/sql-client.sh

准备 Kafka 中的示例数据表

我们假设已经从 Kafka 中读取两条流:

  • orders:订单流
  • payments:支付流

并在建表时定义了事件时间和水位线:

CREATE TABLE orders (
  order_id     STRING,
  user_id      STRING,
  order_amount DECIMAL(10, 2),
  order_time   TIMESTAMP_LTZ(3),
  WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (
  'connector' = 'kafka',
  'topic' = 'orders',
  'properties.bootstrap.servers' = '127.0.0.1:9092',
  'properties.group.id' = 'flink-orders',
  'scan.startup.mode' = 'earliest-offset',
  'format' = 'json',
  'json.timestamp-format.standard' = 'ISO-8601'
);

CREATE TABLE payments (
  pay_id     STRING,
  order_id   STRING,
  pay_amount DECIMAL(10, 2),
  pay_time   TIMESTAMP_LTZ(3),
  WATERMARK FOR pay_time AS pay_time - INTERVAL '5' SECOND
) WITH (
  'connector' = 'kafka',
  'topic' = 'payments',
  'properties.bootstrap.servers' = '127.0.0.1:9092',
  'properties.group.id' = 'flink-payments',
  'scan.startup.mode' = 'earliest-offset',
  'format' = 'json',
  'json.timestamp-format.standard' = 'ISO-8601'
);

有了时间字段和水位线,Flink 才能在流模式下安全地做双流 JOIN,并在「时间窗」关闭后清理状态。

使用 Kafka Console Producer 造测试数据

上面的 DDL 建好了 orderspayments 两张表,对应的是 Kafka 中的两个 Topic。接下来我们用 Kafka 自带的命令行工具写入几条 JSON 测试数据。

假设你已经在 WSL2 的 Ubuntu 中启动好了 Kafka(包括 ZooKeeper 或 KRaft),进入 Kafka 安装目录,执行:

1. 往订单 Topic 写入数据

bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic orders

在命令行中输入几条 JSON 数据(按回车发送一条):

{"order_id":"o_1","user_id":"u_1","order_amount":100.00,"order_time":"2026-02-16T14:41:00Z"}
{"order_id":"o_2","user_id":"u_2","order_amount":200.00,"order_time":"2026-02-16T14:42:00Z"}
{"order_id":"o_3","user_id":"u_1","order_amount":150.00,"order_time":"2026-02-16T14:45:00Z"}

2. 往支付 Topic 写入数据

新开一个终端,同样进入 Kafka 安装目录,执行:

bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic payments

输入对应的支付 JSON 数据:

{"pay_id":"p_1","order_id":"o_1","pay_amount":100.00,"pay_time":"2026-02-16T14:41:00Z"}
{"pay_id":"p_2","order_id":"o_2","pay_amount":200.00,"pay_time":"2026-02-16T14:42:00Z"}

这里的字段名、时间格式都要和前面建表时定义的一致,这样 Flink 才能正确反序列化 JSON 并进行双流 JOIN。

四、基于时间条件的普通双流 JOIN

先来看最直观的一种写法:同时指定「关联键」和「时间范围」。

需求:统计订单在下单后 15 分钟内完成支付的记录。

SELECT
  o.order_id,
  o.user_id,
  o.order_amount,
  o.order_time,
  p.pay_id,
  p.pay_amount,
  p.pay_time
FROM orders AS o
JOIN payments AS p
ON o.order_id = p.order_id
AND p.pay_time BETWEEN o.order_time AND o.order_time + INTERVAL '15' MINUTE;

双流join

这里有几点非常关键:

  • o.order_id = p.order_id:以订单号作为两条流的业务主键
  • pay_time BETWEEN order_time AND order_time + INTERVAL '15' MINUTE:明确限定“下单后 15 分钟内支付”这类时间约束
  • 使用事件时间字段配合水位线,可以在保证计算正确性的前提下控制状态大小,并处理一定范围内的迟到数据

如果你希望保留那些下单了但超时未支付的记录,可以将上面的 JOIN 改为 LEFT JOIN,然后在下游以 p.pay_id IS NULL 作为“未支付/超时”的判断条件。

五、Interval Join:显式时间区间的双流 JOIN

普通 JOIN 中的时间条件本质上就是一种「区间约束」。
在 Flink Table API 中,有一个更明确的概念:Interval Join

等价的 Interval Join 写法大致如下(Table API 伪代码,仅作为概念理解):

SELECT
  o.order_id,
  o.order_time,
  p.pay_id,
  p.pay_time
FROM orders AS o
JOIN payments AS p
ON o.order_id = p.order_id
AND p.pay_time BETWEEN o.order_time AND o.order_time + INTERVAL '15' MINUTE;

无论是普通 JOIN 还是 Interval Join,本质上都是:

  • 以某个时间字段作为「对齐基准」
  • 设定一个前后允许的时间区间
  • 在这个区间内匹配到的记录会输出为 JOIN 结果

六、迟到数据与状态清理

在流式 JOIN 中,最容易被忽略但又非常重要的一点就是:状态会不断累积

Flink 会根据时间条件和水位线来决定:

  • 某条历史事件是否还有可能再匹配到另一条流的事件
  • 超出时间范围且水位线已推进时,可以安全地清理对应状态

设计双流 JOIN 时,建议考虑:

  • 时间窗口不要设置得过大,否则状态会膨胀
  • 根据业务的真实延迟来设置水位线与时间区间
  • 对于极端迟到的数据,是丢弃、旁路输出,还是通过补偿机制处理

七、一个完整的小结

通过本文,你需要记住下面几点:

  • 双流 JOIN 场景非常常见,本质是两条事实流在时间上的匹配
  • 流式 JOIN 一定要依赖事件时间 + 水位线来控制状态和迟到数据
  • 常见的方式包括基于时间条件的普通 JOIN 和 Interval JOIN
  • 设计时间区间时,要在「业务容忍度」和「资源消耗」之间做权衡

在下一篇中,我们可以继续围绕「实时数仓」或「维表时态 Join」展开,把事实流与维度数据关联起来,构建更真实的 Flink SQL 实战项目。


原文来自:http://blog.daimajiangxin.com.cn

在做 V2EX 图库的图片生成功能的过程中,我一直在用一组特定的提示词生成这样的图标:



可惜这个样式目前只能通过 API 调用 gpt-image-1 这个模型生成。

如果是在 ChatGPT 网站的聊天界面上,似乎已经没有办法再获得这样的样式。目前 ChatGPT 网站上背后用的应该是 gpt-image-1.5 ,同样的提示词进去,出来的结果很不一样。

最近两天因为之前在用的 gpt-image-1 服务的提供商遇到一些可用性问题,春节期间又不好摇人,所以搜索了一下站内,然后尝试接入了另外一位 V 友的服务。然后发现这件事情里水好深啊。

同样的 gpt-image-1 模型,但是会有多种不同的“分组”,计费最便宜的 1 倍,最贵的 8 倍。

结果有区别吗?居然是有的。只有最贵的 8 倍“优质官转”可以按照提示词正确生成透明背景,其他更便宜的都不行。

8 倍计费的“优质官转”:



1 倍计费的“默认”:



按照这个新服务的定价,如果必须透明背景,那么一张图的成本差不多是 1.3 元人民币。“默认”的话,差不多是 0.16 人民币一张图。



如果你在做支持 gpt-image-1 的中转服务,欢迎贴出你的地址,我打算每个服务都去充值试用一下。

V2EX 的图片生成功能是打算一直维护和更新下去的,多一些供应商会让这个服务更稳健。

SQL Server Management Studio (SSMS) 22.3.0 - 微软数据库管理工具

integrated environment for managing SQL Server & any Azure SQL infrastructure

请访问原文链接:https://sysin.org/blog/ssms/ 查看最新版。原创作品,转载请保留出处。

作者主页:sysin.org


笔者注:SQL Server 2014 及之前版本内置 SQL Server Management Studio (SSMS),SQL Server 2016 及以后版本需要独立安装。

针对未来就绪的基础结构进行优化

SQL Server Management Studio (SSMS) 是一种集成环境,用于管理从 SQL Server 到 Azure SQL 数据库的任何 SQL 基础结构。SSMS 提供用于配置、监视和管理 SQL Server 和数据库实例的工具 (sysin)。使用 SSMS 部署、监视和升级应用程序使用的数据层组件,以及生成查询和脚本。

使用 SSMS 在本地计算机或云端查询、设计和管理数据库及数据仓库,无论它们位于何处。

对于需要 SSMS 的跨平台助手来管理 SQL 及其他 Azure 数据库的客户,请使用 Azure Data Studio (将停用)。

有关此版本中新增功能的更多详细信息,包括重要安全更改,请参阅 SQL Server Management Studio (SSMS) 的发行说明

支持的 SQL 产品和服务

SQL Server Management Studio

此版本的 SSMS 适用于 SQL Server 2014(12.x)及更高版本。它为使用 Azure SQL 数据库、Azure Synapse Analytics 和 Microsoft Fabric 中的最新云功能提供了最重要的支持。

此外,SQL Server Management Studio 21 可与 SSMS 20.x、SSMS 19.x、SSMS 18.x、SSMS 17.x 和 SSMS 16.x 一起安装。

对于 SQL Server Integration Services(SSIS),SSMS 17.x 及更高版本不支持连接到旧版 SQL Server Integration Services 服务 (sysin)。若要连接到旧 Integration Services 的早期版本,请使用与 SQL Server 版本一致的 SSMS 版本。

SSMS version支持的最高 SQL Server 级别支持的旧版 SSIS 服务
16.xSQL Server 2016 (13.x)SQL Server 2016 (13.x)
17.xSQL Server 2017 (14.x)SQL Server 2017 (14.x)
18.xSQL Server 2019 (15.x)SQL Server 2019 (15.x)
19.x, 20.xSQL Server 2022 (16.x)SQL Server 2022 (16.x)
21.xSQL Server 2025 (17.x)SQL Server 2025 (17.x)
22.xSQL Server 2025 (17.x)SQL Server 2025 (17.x)

例如,使用 SSMS 19.x 或 20.x 连接到旧版 SQL Server 2022(16.x)Integration Services 服务。SSMS 21 和 SSMS 20.x(或更早版本)可以安装在同一台计算机上。自 SQL Server 2012(11.x)发布以来,建议使用 SSIS 目录数据库 SSISDB 来存储、管理、运行和监视 Integration Services 包。

系统要求

以下 64 位作系统支持 SQL Server Management Studio 22:

  • Windows 11 最低支持的 OS 版本或更高版本:家庭版、专业版、专业教育版、适用于工作站、企业和教育的专业版。
  • Windows 10 最低支持的 OS 版本或更高版本:家庭版、专业版、教育版和企业版。
  • Windows Server 2025:标准版和数据中心版。
  • Windows Server 2022:标准版和数据中心版。
  • Windows Server 2019:标准版和数据中心版。
  • Windows Server 2016:标准版和数据中心版。
  • 参看:Windows 下载汇总

新增功能

SQL Server Management Studio v22.3.0

  • Connection dialog:改进现代连接对话框的启动性能。
  • External Models:为 external models 新增模板支持。
  • GitHub Copilot in SSMS (Preview):新增对 database instructions 的支持。
  • Libraries:将 Microsoft.Data.SqlClient 更新至 6.1.3。
  • Libraries:将 MSODBCSQL 更新至 18.6.1.1。
  • Visual Studio:更新至 Visual Studio 18.3 [11506.43]。

有关此版本中新增功能的更多详细信息,请参阅 SQL Server Management Studio (SSMS) 发行说明

下载地址

Microsoft SQL Server Management Studio (SSMS) 22.x 简体中文 | 繁體中文 | English

  • 请访问:https://sysin.org/blog/ssms/
  • 此为离线三合一版本,通过对应的安装程序来安装对应界面语言的版本。

    • setup_zh_CN.exe - 简体中文
    • setup_zh_TW.exe - 繁體中文
    • setup_en_US.exe - English

相关产品:Microsoft SQL Server 下载汇总

更多:Windows 下载汇总

SQL Server Management Studio (SSMS) 22.3.0 - 微软数据库管理工具

integrated environment for managing SQL Server & any Azure SQL infrastructure

请访问原文链接:https://sysin.org/blog/ssms/ 查看最新版。原创作品,转载请保留出处。

作者主页:sysin.org


笔者注:SQL Server 2014 及之前版本内置 SQL Server Management Studio (SSMS),SQL Server 2016 及以后版本需要独立安装。

针对未来就绪的基础结构进行优化

SQL Server Management Studio (SSMS) 是一种集成环境,用于管理从 SQL Server 到 Azure SQL 数据库的任何 SQL 基础结构。SSMS 提供用于配置、监视和管理 SQL Server 和数据库实例的工具 (sysin)。使用 SSMS 部署、监视和升级应用程序使用的数据层组件,以及生成查询和脚本。

使用 SSMS 在本地计算机或云端查询、设计和管理数据库及数据仓库,无论它们位于何处。

对于需要 SSMS 的跨平台助手来管理 SQL 及其他 Azure 数据库的客户,请使用 Azure Data Studio (将停用)。

有关此版本中新增功能的更多详细信息,包括重要安全更改,请参阅 SQL Server Management Studio (SSMS) 的发行说明

支持的 SQL 产品和服务

SQL Server Management Studio

此版本的 SSMS 适用于 SQL Server 2014(12.x)及更高版本。它为使用 Azure SQL 数据库、Azure Synapse Analytics 和 Microsoft Fabric 中的最新云功能提供了最重要的支持。

此外,SQL Server Management Studio 21 可与 SSMS 20.x、SSMS 19.x、SSMS 18.x、SSMS 17.x 和 SSMS 16.x 一起安装。

对于 SQL Server Integration Services(SSIS),SSMS 17.x 及更高版本不支持连接到旧版 SQL Server Integration Services 服务 (sysin)。若要连接到旧 Integration Services 的早期版本,请使用与 SQL Server 版本一致的 SSMS 版本。

SSMS version支持的最高 SQL Server 级别支持的旧版 SSIS 服务
16.xSQL Server 2016 (13.x)SQL Server 2016 (13.x)
17.xSQL Server 2017 (14.x)SQL Server 2017 (14.x)
18.xSQL Server 2019 (15.x)SQL Server 2019 (15.x)
19.x, 20.xSQL Server 2022 (16.x)SQL Server 2022 (16.x)
21.xSQL Server 2025 (17.x)SQL Server 2025 (17.x)
22.xSQL Server 2025 (17.x)SQL Server 2025 (17.x)

例如,使用 SSMS 19.x 或 20.x 连接到旧版 SQL Server 2022(16.x)Integration Services 服务。SSMS 21 和 SSMS 20.x(或更早版本)可以安装在同一台计算机上。自 SQL Server 2012(11.x)发布以来,建议使用 SSIS 目录数据库 SSISDB 来存储、管理、运行和监视 Integration Services 包。

系统要求

以下 64 位作系统支持 SQL Server Management Studio 22:

  • Windows 11 最低支持的 OS 版本或更高版本:家庭版、专业版、专业教育版、适用于工作站、企业和教育的专业版。
  • Windows 10 最低支持的 OS 版本或更高版本:家庭版、专业版、教育版和企业版。
  • Windows Server 2025:标准版和数据中心版。
  • Windows Server 2022:标准版和数据中心版。
  • Windows Server 2019:标准版和数据中心版。
  • Windows Server 2016:标准版和数据中心版。
  • 参看:Windows 下载汇总

新增功能

SQL Server Management Studio v22.3.0

  • Connection dialog:改进现代连接对话框的启动性能。
  • External Models:为 external models 新增模板支持。
  • GitHub Copilot in SSMS (Preview):新增对 database instructions 的支持。
  • Libraries:将 Microsoft.Data.SqlClient 更新至 6.1.3。
  • Libraries:将 MSODBCSQL 更新至 18.6.1.1。
  • Visual Studio:更新至 Visual Studio 18.3 [11506.43]。

有关此版本中新增功能的更多详细信息,请参阅 SQL Server Management Studio (SSMS) 发行说明

下载地址

Microsoft SQL Server Management Studio (SSMS) 22.x 简体中文 | 繁體中文 | English

  • 请访问:https://sysin.org/blog/ssms/
  • 此为离线三合一版本,通过对应的安装程序来安装对应界面语言的版本。

    • setup_zh_CN.exe - 简体中文
    • setup_zh_TW.exe - 繁體中文
    • setup_en_US.exe - English

相关产品:Microsoft SQL Server 下载汇总

更多:Windows 下载汇总

SQL Server Management Studio (SSMS) 22.3.0 - 微软数据库管理工具

integrated environment for managing SQL Server & any Azure SQL infrastructure

请访问原文链接:https://sysin.org/blog/ssms/ 查看最新版。原创作品,转载请保留出处。

作者主页:sysin.org


笔者注:SQL Server 2014 及之前版本内置 SQL Server Management Studio (SSMS),SQL Server 2016 及以后版本需要独立安装。

针对未来就绪的基础结构进行优化

SQL Server Management Studio (SSMS) 是一种集成环境,用于管理从 SQL Server 到 Azure SQL 数据库的任何 SQL 基础结构。SSMS 提供用于配置、监视和管理 SQL Server 和数据库实例的工具 (sysin)。使用 SSMS 部署、监视和升级应用程序使用的数据层组件,以及生成查询和脚本。

使用 SSMS 在本地计算机或云端查询、设计和管理数据库及数据仓库,无论它们位于何处。

对于需要 SSMS 的跨平台助手来管理 SQL 及其他 Azure 数据库的客户,请使用 Azure Data Studio (将停用)。

有关此版本中新增功能的更多详细信息,包括重要安全更改,请参阅 SQL Server Management Studio (SSMS) 的发行说明

支持的 SQL 产品和服务

SQL Server Management Studio

此版本的 SSMS 适用于 SQL Server 2014(12.x)及更高版本。它为使用 Azure SQL 数据库、Azure Synapse Analytics 和 Microsoft Fabric 中的最新云功能提供了最重要的支持。

此外,SQL Server Management Studio 21 可与 SSMS 20.x、SSMS 19.x、SSMS 18.x、SSMS 17.x 和 SSMS 16.x 一起安装。

对于 SQL Server Integration Services(SSIS),SSMS 17.x 及更高版本不支持连接到旧版 SQL Server Integration Services 服务 (sysin)。若要连接到旧 Integration Services 的早期版本,请使用与 SQL Server 版本一致的 SSMS 版本。

SSMS version支持的最高 SQL Server 级别支持的旧版 SSIS 服务
16.xSQL Server 2016 (13.x)SQL Server 2016 (13.x)
17.xSQL Server 2017 (14.x)SQL Server 2017 (14.x)
18.xSQL Server 2019 (15.x)SQL Server 2019 (15.x)
19.x, 20.xSQL Server 2022 (16.x)SQL Server 2022 (16.x)
21.xSQL Server 2025 (17.x)SQL Server 2025 (17.x)
22.xSQL Server 2025 (17.x)SQL Server 2025 (17.x)

例如,使用 SSMS 19.x 或 20.x 连接到旧版 SQL Server 2022(16.x)Integration Services 服务。SSMS 21 和 SSMS 20.x(或更早版本)可以安装在同一台计算机上。自 SQL Server 2012(11.x)发布以来,建议使用 SSIS 目录数据库 SSISDB 来存储、管理、运行和监视 Integration Services 包。

系统要求

以下 64 位作系统支持 SQL Server Management Studio 22:

  • Windows 11 最低支持的 OS 版本或更高版本:家庭版、专业版、专业教育版、适用于工作站、企业和教育的专业版。
  • Windows 10 最低支持的 OS 版本或更高版本:家庭版、专业版、教育版和企业版。
  • Windows Server 2025:标准版和数据中心版。
  • Windows Server 2022:标准版和数据中心版。
  • Windows Server 2019:标准版和数据中心版。
  • Windows Server 2016:标准版和数据中心版。
  • 参看:Windows 下载汇总

新增功能

SQL Server Management Studio v22.3.0

  • Connection dialog:改进现代连接对话框的启动性能。
  • External Models:为 external models 新增模板支持。
  • GitHub Copilot in SSMS (Preview):新增对 database instructions 的支持。
  • Libraries:将 Microsoft.Data.SqlClient 更新至 6.1.3。
  • Libraries:将 MSODBCSQL 更新至 18.6.1.1。
  • Visual Studio:更新至 Visual Studio 18.3 [11506.43]。

有关此版本中新增功能的更多详细信息,请参阅 SQL Server Management Studio (SSMS) 发行说明

下载地址

Microsoft SQL Server Management Studio (SSMS) 22.x 简体中文 | 繁體中文 | English

  • 请访问:https://sysin.org/blog/ssms/
  • 此为离线三合一版本,通过对应的安装程序来安装对应界面语言的版本。

    • setup_zh_CN.exe - 简体中文
    • setup_zh_TW.exe - 繁體中文
    • setup_en_US.exe - English

相关产品:Microsoft SQL Server 下载汇总

更多:Windows 下载汇总




建立一个测试项目,这个项目使用我编写的类似于 OpenClaw 的 AI Assistant, 我的目的是做出一个当前适合于华人的 AI Assistant, 所以对于微信和浏览器等操作做了强化, 当前使用这个 AI Assistant 需要使用 Mac 电脑,对于 LLM 这个程序支持多种,但我推荐一个很便宜的选择 Z.ai 可以购买一年 $26 的 GLM, 可以使用 GLM 4.7 flash 这个模型,针对当前这个 AI Assistant 可能已经够用了

今日速览

  1. Base44 Backend Platform:AI 应用的后台管家,一键搞定全栈部署。
  2. Toolspend:SaaS 支出追踪神器,告别浪费,省钱省心。
  3. NVIDIA PersonaPlex:能聊能演的全双工 AI,声音角色随你定制。
  4. JDoodle.ai MCP:和 ChatGPT 聊天就能建网站,实时预览超方便。
  5. PenguinBot AI:24/7 智能员工,把对话变成实际工作。
  6. Agent Bar:菜单栏里的 Claude 小助手,语音交互实时追踪。
  7. Marketing Agents Squad:250+ 营销 AI 代理,秒出专业方案。
  8. Enough Cream:手机摄像头帮你泡咖啡,奶油浓度刚刚好。
  9. chowder.dev:一个 API 搞定 OpenClaw 实例,部署快如闪电。
  10. Meteorite:macOS 菜单栏记事本,轻量快捷不打扰。

深度阅读

1. Base44 Backend Platform

这款神器能帮你轻松构建带 AI 代理的应用,省去繁琐的后端设置,让开发更高效。

  • 一条命令部署全栈应用,无需手动配置
  • 专为 Claude Code 和 Cursor 优化,AI 代理用技能替代复杂 API
  • 经过数百万生产应用考验,稳定可靠
  • 独立服务上线,开箱即用

热度:🔺407

Base44 Backend Platform
访问官网 Product Hunt 详情


2. Toolspend

别再为 SaaS 订阅和闲置许可证烧钱了!Toolspend 是你的技术栈指挥中心,帮你全面掌控支出。

  • 深度分析工具使用情况和消费模式,识别浪费
  • 自动发现未充分利用的座位和重复工具
  • 续订前智能提醒,避免隐形费用
  • 自动化采购流程,让你专注产品构建

热度:🔺396

Toolspend
访问官网 Product Hunt 详情


3. NVIDIA PersonaPlex

这款全双工对话 AI 能自然交流,支持定制声音和角色,让互动更生动。

  • 可应对打断和旁白,保持角色一致性
  • 在对话动态和任务执行上优于现有系统
  • 声音和角色高度可定制,适应不同场景
  • 实现自然流畅的人机交互

热度:🔺252

NVIDIA PersonaPlex
访问官网 Product Hunt 详情


4. JDoodle.ai MCP

想建网站?直接和 ChatGPT 或 Claude 聊天就行!JDoodle.ai 让你实时创建和预览应用。

  • 通过对话构建前端、后端或全栈应用
  • 项目在 JDoodle.ai 内实时更新,提供即时预览
  • 一键生成在线链接,快速发布
  • 支持迭代和问题修复,聊天驱动开发
  • 可集成数据库,功能全面

热度:🔺191

JDoodle.ai MCP
访问官网 Product Hunt 详情


5. PenguinBot AI

你的智能员工全天候待命,把对话转化为实际行动,解放你的双手。

  • 管理电子邮件、安排任务、创建文档自动化
  • 自动运行工作流程,后台持续运转
  • 自主规划执行,安全可靠
  • 24/7 运行,让你专注核心事务

热度:🔺171

PenguinBot AI
访问官网 Product Hunt 详情


6. Agent Bar

把 Claude Code 装进菜单栏!这个小助手提供原生界面,让桌面操作更轻松。

  • 选择项目后通过语音交互,实时查看工具调用
  • 批准或自动批准操作,灵活控制
  • 追踪令牌费用,成本一目了然
  • 桌面集成,无需切换窗口

热度:🔺139

Agent Bar
访问官网 Product Hunt 详情


7. Marketing Agents Squad

250+ AI 营销代理随时待命,帮你分担日常营销工作,秒出专业方案。

  • 每个代理都懂营销背景,无需额外培训
  • 描述目标后几秒钟内生成专业输出
  • 覆盖多种营销场景,灵活选择
  • 就像拥有天才实习生团队,效率倍增

热度:🔺138

Marketing Agents Squad
访问官网 Product Hunt 详情


8. Enough Cream

泡咖啡时总加多奶油?这款应用用手机摄像头帮你搞定,每次都是完美浓度。

  • 实时分析咖啡颜色,对比保存的偏好
  • 智能提示停止倒奶油的时机
  • 确保每次咖啡浓稠度一致
  • 简单易用,提升日常体验

热度:🔺129

Enough Cream
访问官网 Product Hunt 详情


9. chowder.dev

一个 API 就能启动和管理 OpenClaw 实例,部署快得飞起,基础设施全包。

  • 几秒钟内启动完全独立的 Claw 实例
  • 连接到 11 个消息通道,扩展性强
  • 支持安装技能、管理身份验证和持久化记忆
  • 与 OpenAI 兼容的 API,从零部署不到一分钟

热度:🔺129

chowder.dev
访问官网 Product Hunt 详情


10. Meteorite

专为 macOS 设计的菜单栏记事本,轻量快捷,让你随时捕捉灵感不打断工作流。

  • 快速记录笔记、任务和代码片段
  • 原生开发,键盘操作为主,高效流畅
  • 轻量级设计,不占用系统资源
  • 简约界面,无干扰体验

热度:🔺123

Meteorite
访问官网 Product Hunt 详情

今天是大年初一,江湖十年给读者朋友们拜年了,祝大家新年快乐!

又是新的一年,想必大家都没看春晚吧 😄,今天继续一年一度的用 Go 语言实现春晚魔术。

废话不多说,咱们直接看原理。

<!-- more -->

魔术原理揭秘

这个魔术的数学原理其实很简单,基于一个简单的恒等式:

设目标时间为 T
设观众说的两个数为 A 和 B
魔术师计算的第三个数为 C = T - (A + B)

那么:A + B + C = A + B + [T - (A + B)] = T

这里的 T 就是最终结果:2162227

  • 观众 A 说:1106
  • 观众 B 说:88396
  • 观众“乱按”的计算器显示:2072725
  • 相加结果:1106 + 88396 + 2072725 = 2162227

这个数字代表:2 月 16 日 22 时 27 分

理解了吗?

这里其实有个障眼法,就是魔术师现场找了三个观众“乱按”了几下,但其实谁也不确定他们按的什么,对不对,其实他们按的数字根本就没用上,而是计算器里已经预先计算出了目标时间 T 与 A + B 总和的差值。

没错,魔术这种东西就是这么朴实无华。

Go 语言实现魔术

那么接下来,就用 Go 语言来实现一个简单的计算器程序,来还原这个魔术。

首先定义一个魔术计算器:

// MagicCalculator 魔术计算器
type MagicCalculator struct {
    targetTime int    // 目标时间转换的数字
    timestamp  string // 实际时间字符串
}

接下来实现一个魔术计算器的构造方法:

// NewMagicCalculator 创建一个魔术计算器实例
func NewMagicCalculator() *MagicCalculator {
    // 获取当前时间
    now := time.Now()

    // 生成类似 "2162227" 的时间数字
    // 格式: 月(1-2 位) + 日(2 位) + 小时(2 位) + 分钟(2 位)
    month := int(now.Month())
    day := now.Day()
    hour := now.Hour()
    minute := now.Minute()

    // 构建时间字符串和数字
    timestamp := fmt.Sprintf("%d%02d%02d%02d", month, day, hour, minute)

    // 转换为整数
    target, _ := strconv.ParseInt(timestamp, 10, 64)

    return &MagicCalculator{
        targetTime: int(target),
        timestamp:  timestamp,
    }
}

当前时间 now 就是用来计算目标时间的,可以根据需要设定,这里直接使用当前时间。

target 是格式为 2162227 的时间数字,也就是咱们原理解析中的目标时间 T。

定义一个 计算魔术数字 T -(A + B)的方法:

// GetMagicNumber 计算魔术数字
func (mc *MagicCalculator) GetMagicNumber(num1, num2 int) int {
    // 魔术公式: target - (num1 + num2)
    return mc.targetTime - (num1 + num2)
}

最后就是定义一个交互式函数,它实现了:

  • 计算目标时间 T
  • 接收用户输入的 A、B
  • 计算观众“乱按”的第三个数字

源码如下:

func InteractiveMagic() {
    fmt.Println("=== 交互式魔术体验 ===")
    fmt.Println("请按照提示输入数字,我会展示魔术的原理")

    mc := NewMagicCalculator()

    var num1, num2 int
    fmt.Print("请输入第一个数: ")
    fmt.Scan(&num1)
    fmt.Print("请输入第二个数: ")
    fmt.Scan(&num2)

    fmt.Printf("\n你输入的是: %d 和 %d\n", num1, num2)

    magicNum := mc.GetMagicNumber(num1, num2)
    fmt.Printf("魔术数字(第三个数)是: %d\n", magicNum)

    fmt.Printf("\n验证: %d + %d + %d = %d\n", num1, num2, magicNum, mc.targetTime)
    fmt.Printf("这个数字代表的时间是: %s\n", mc.timestamp)
}

程序 main 入口:

func main() {
    InteractiveMagic()
}

验证魔术:

# 运行程序
$ go run main.go
=== 交互式魔术体验 ===
请按照提示输入数字,我会展示魔术的原理
请输入第一个数: 1106
请输入第二个数: 88396

你输入的是: 1106 和 88396
魔术数字(第三个数)是: 2081398

验证: 1106 + 88396 + 2081398 = 2170900
这个数字代表的时间是: 2170900

通过 go run main.go 运行程序,接下来根据提示分别输入两个数字,这里以春晚观众说的两个数字(110688396)为例,然后计算观众“乱按”的第三个数字(2081398),最终得到的目标时间是 2170900

没错,我在 2 月 17 日 09 时 00 分运行的程序。

总结

今天依旧使用 Go 语言简单实现了魔术小程序,看个乐子,开心最重要。

如果你有兴趣,完全可以通过聊天的方式让大模型生成一个带有前端界面的魔术计算器程序,体验 vibe coding 的乐趣。

本文完整代码示例我放在了 GitHub 上,欢迎点击查看。

25 年我的文章里吐槽了春晚魔术“降本增效”,在此给刘谦老师道个歉,是我冒犯了🤣之前对魔术一无所知。

25 年看了老罗采访刘谦的视频,对刘谦大佬肃然起敬 respect 🫡。

image.png

延伸阅读

联系我

在编程中,我们经常要处理字符和字符串,为了方便操作字符和字符串,C语言标准库中提供了一系列库函数。

1 字符函数

1.1 字符分类函数

C语言中有一系列的函数是专门做字符分类的,也就是一个字符是属于什么类型的字符的。这些函数的使用都需要包含一个头文件ctype.h

例如islowerisupper等,以islower为例:

int islower (int c);

islower能够判断参数部分的c是否是小写字母。通过返回值来说明是否是小写字母,如果是小写字母就返回非0的整数,如果不是小写字母,则返回0。关于(int c),要注意到字符的本质是一个ASCII码值,这样的写法并没有问题。

1.2 字符转换函数

在之前,要一个字符大小写转换打印,用的是字符+/-32来实现的,例如:大写转小写就加上32、小写转大写就减去32(参见ASCII码相关知识)。现在,我们可以用以下库函数来更简洁的解决。

C语言提供了2个字符转换函数:

int tolower (int c);//将参数传进去的大写字母转小写
int toupper (int c);//将参数传进去的小写字母转大写

举个例子:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <ctype.h>
int main()
{
    char i = 'a';
    //i = i - 32;
    i = toupper(i);
    printf("%c\n", i);
    return 0;
}

2 字符串函数

1 strlen的使用和模拟实现

size_t strlen ( const char * str );
  • 字符串以'\0'作为结束标志,strlen函数返回的是在字符串中'\0'前面出现的字符个数(不包

'\0')。

  • 参数指向的字符串必须要以'\0'结束。
  • 函数的返回值为size_t,是无符号的(⭐)。
  • strlen的使用需要包含头文件string.h

strlen的模拟实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
#include <assert.h>

//模拟实现strlen
size_t my_strlen1(const char* str)
{
    assert(str != NULL);
    size_t n = 0;
    while (*str != '\0')
    {
        n++;
        str++;
    }
    return n;
}

size_t my_strlen2(const char* str)
{
    assert(str != NULL);
    char* p = str;
    while (*p)
    {
        p++;
    }
    return p - str;
}

size_t my_strlen3(const char* str)
{
    assert(str != NULL);
    if (*str)
    {
        return 1 + my_strlen3(str + 1);
    }
    else
    {
        return 0;
    }
}

int main()
{
    char arr[] = { "abcdef" };
    size_t r = strlen(arr);
    size_t r1 = my_strlen1(arr);
    size_t r2 = my_strlen2(arr);
    size_t r3 = my_strlen3(arr);
}

2 strcpy的使用和模拟实现

char * strcpy ( char * destination, const char * source );
  • 字符串复制。Copies the C string pointed by source into the array pointed by destination, including the terminating null character (and stopping at that point).
  • 源字符串必须以'\0'结束。
  • 会将源字符串中的'\0'拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可修改。

strcpy的模拟实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
#include <assert.h>

//模拟实现strcpy
//char* my_strcpy(char* des, const char* src)
//{
//    assert(des != NULL && src != NULL);
//    char* ret = des;
//    while (*src)
//    {
//        *des = *src;
//        des++;
//        src++;
//    }
//    *des = '\0';
//    return ret;
//}

char* my_strcpy(char* des, const char* src)
{
    assert(des != NULL && src != NULL);
    char* ret = des;
    while (*des++ = *src++)
    {
        ;
    }
    return ret;
}

int main()
{
    char arr1[20] = { "abcdef" };
    char arr2[20] = {"xxxxxxxxxxxxxxxx"};
    my_strcpy(arr2, arr1);
}

3 strcat的使用和模拟实现

char * strcat ( char * destination, const char * source );
  • 字符串拼接。Appends a copy of the source string to the destination string. The terminating null character in destination is overwritten by the first character of source, and a null-character is included at the end of the new string formed by the concatenation of both in destination.
  • 源字符串必须以'\0'结束。
  • 目标字符串中也得有'\0',否则没办法知道追加从哪里开始。
  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  • 目标空间必须可修改。
  • 将该函数用于字符串自己给自己追加是不合适的,其结果未定义。

strcat模拟实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strcat(char* des, const char* src)
{
    assert(des && src);
    char* ret = des;
    while (*des != '\0')
    {
        des++;
    }
    while (*des++ = *src++)
    {
        ;
    }
    return ret;
}

int main()
{
    char a[20] = { "Hello \0xxxxxxxxxxx" };//目标字符串要有足够位置
    char b[10] = { "world" };
    //strcat(a, b);
    my_strcat(a, b);
    printf("%s\n", a);
}

4 strcmp的使用和模拟实现

int strcmp ( const char * str1, const char * str2 );
  • 字符串比较。This function starts comparing the first character of each string. If they are equal to each other, it continues with the following pairs until the characters differ or until a terminating null-character is reached.
  • 判断两个字符串的大小:比较两个字符串中对应位置上字符ASCII码值的大小。
  • 标准规定的返回值要求如下:

strcmp模拟实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <assert.h>
int my_strcmp1(const char* str1, const char* str2)
{
    assert(str1 && str2);
    while (*str1 == *str2)
    {
        if (*str1 == '\0')
            return 0;
        str1++;
        str2++;
    }
    return (*str1 - *str2);
}

int my_strcmp2(const char* str1, const char* str2)
{
    assert(str1 && str2);
    while (*str1 == *str2)
    {
        if (*str1 == '\0')
            return 0;
        str1++;
        str2++;
    }
    if (*str1 > *str2)
        return 1;
    else
        return -1;
}

int main()
{
    char a1[] = { "abcdef" };
    char a2[] = { "abq" };
    int r = strcmp(a1, a2);
    int r1 = my_strcmp1(a1, a2);
    int r2 = my_strcmp2(a1, a2);
    return 0;
}

strcpystrcatstrcmp三个函数有对应的更安全的版本(要多传一个参数),它们分别是strncpystrncatstrncmp

5 strncpy函数

char * strncpy ( char * destination, const char * source, size_t num );
  • Copies the first num characters of source to destination. If the end of the source C string (which is signaled by a null-character) is found before num characters have been copied,destination is padded with zeros until a total of num characters have been written to it.
  • 拷贝num个字符从源字符串到目标空间。
  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

6 strncat函数

char * strncat ( char * destination, const char * source, size_t num );
  • Appends the first num characters of source to destination, plus a terminating null-character.(将source指向字符串的前num个字符追加到destination指向的字符串末尾,再追加一个\0字符)。
  • If the length of the C string in source is less than num, only the content up to the terminating null-character is copied.(如果source指向的字符串的长度小于num的时候,只会将字符串中到\0的内容追加到destination指向的字符串末尾)。

7 strncmp函数

int strncmp ( const char * str1, const char * str2, size_t num );
  • Compares up to num characters of the C string str1 to those of the C string str2.This function starts comparing the first character of each string. If they are equal to each other, it continues with the following pairs until the characters differ, until a terminating null-character is reached, or until num characters match in both strings, whichever happens first.
  • 比较str1str2的前num个字符,如果相等就继续往后比较,最多比较num个字母,如果提前发现不一样,就提前结束。返回值标准参照strcmp函数。

8 strstr的使用和模拟实现

const char * strstr ( const char * str1, const char * str2 );
  • Returns a pointer to the first occurrence of str2 in strl, ora null pointer if str2 is not part of strl.(函数返回字符串str2在字符串str1中第一次出现的位置)。
  • The matching process does not include the terminating null-characters, but it stops there.(字符串的比较匹配不包含\0字符,以\0作为结束标志)。
  • 返回值规定:A pointer to the first occurrence in str1 of the entire sequence of characters specified in str2, or a null pointer if the sequence is not present in str1.

strstr模拟实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strstr(const char* str1, const char* str2)
{
    assert(str1 && str2);
    const char* cur = str1;//记录比较的起始位置
    const char* s1 = NULL;//用s1在str1中遍历
    const char* s2 = NULL;//用s2在str2中遍历
    
    //特殊情况:str2是一个空字符串
    if (*str2 == '\0')
        return str1;

    while (*cur)
    {
        s1 = cur;
        s2 = str2;
        while (*s1 && *s2 && *s1 == *s2)
        {
            s1++;
            s2++;
        }
        if (*s2 == '\0')
            return (char*)cur;
        cur++;
    }
    return NULL;//找不到
}

int main()
{
    char a[] = { "abbbcdef" };
    char b[] = { "bbc" };
    //char* r = strstr(a, b);
    char* r = my_strstr(a, b);
    printf("%s\n", r);
}

示意图:

str1str2分别记录字符串a、b的起始位置;cur记录比较的起始位置;s1s2用于遍历,完成一次比较后分别靠curstr2复位。

另,字符串匹配算法——KMP算法。

9 strtok函数的使用

char * strtok ( char * str, const char * sep );
  • sep参数指向一个字符串,定义了用作分隔符的字符集合。
  • 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
  • strtok函数找到str中的下一个标记,并将其用\0结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以被strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
  • strtok函数的第一个参数不为NULL,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
  • strtok函数的第一个参数为NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  • 如果字符串中不存在更多的标记,则返回NULL指针。

举个例子:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>


int main()
{
    char arr[] = "zpw@bitedu.net.hehe";
    char buf[20] = {0};//可以修改
    strcpy(buf, arr);
    //zpw\0bitedu\0net
    const char* p = "@.";
    char* r = NULL;
    for (r = strtok(buf, p); r != NULL; r = strtok(NULL, p))
    {
        printf("%s\n", r);
    }

    //这样用是不合适的
    //char* r1 = strtok(buf, p);
    //printf("%s\n", r1);//zpw
    //r1 = strtok(NULL, p);
    //printf("%s\n", r1);//bitedu
    //r1 = strtok(NULL, p);
    //printf("%s\n", r1);//net
    return 0;
}

10 strerrorperror函数的使用

strerror函数可以把参数部分错误码对应的错误信息的字符串地址返回来。

char * strerror ( int errnum );

在不同的系统和C语言标准库的实现中都规定了一些错误码,一般是放在errno.h这个头文件中说明的,C语言程序启动的时候就会使用一个全局变量errno来记录程序的当前错误码,只不过程序启动的时候errno是0,表示没有错误,当我们在使用标准库中的函数的时候发生了某种错误,就会将对应的错误码,存放在errno中,而一个错误码的数字是整数很难理解是什么意思,所以每一个错误码都是有对应的错误信息的。strerror函数就可以将错误对应的错误信息字符串的地址返回。

除此之外,还有一个函数perror,它不需要传入错误码,会直接将错误信息打印出来。

void perror ( const char * str );

perror函数打印完参数部分的字符串后,再打印一个冒号和一个空格,再打印错误信息。

举个例子:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main()
{
    //打开文件
    FILE* pf = fopen("test.txt", "r");//以读的形式打开test.txt文件,如果文件不存在,则打开失败。
    if (pf == NULL)
    {
        //打开文件失败了
        perror("错误信息是");//: xxxx
        printf("错误信息是: %s\n", strerror(errno));
        return 1;
    }
    //往下走了
    //...
    return 0;
}

点赞 + 关注 + 收藏 = 学会了

整理了一个NAS小专栏,有兴趣的工友可以关注一下 👉 《NAS邪修》

Kavita 是免费开源的自托管数字图书馆平台,主打漫画与电子书一站式管理和阅读。在 NAS 可以用 Docker 轻松部署 Kavita,数据完全私有~

这次我用群晖的 NAS 演示一下如何部署 Kavita,其他品牌的 NAS 操作步骤也是差不多的。

打开“File Station”,在“docker”文件夹里创建一个“kavita”文件夹。

然后在“kavita”文件夹里再建3个文件夹:config、books、manga。

打开“Container Manager”,创建一个新项目。

  • 项目名称:kavita
  • 路径:/docker/kavita
  • 来源:创建 docker-compose.yml

代码如下:

services:
    kavita:
        image: jvmilazz0/kavita:latest
        container_name: kavita
        restart: always
        volumes:
            - /volume1/docker/kavita/manga:/manga    
            - /volume1/docker/kavita/books:/books    
            - /volume1/docker/kavita/config:/kavita/config     
        environment:
            - TZ=Asia/Shanghai
        ports:
            - 3456:5000

我给 Kavita 配置了一个 3456 端口,你可以设置其他数字。

点击“下一步”。

勾选“通过 Web Station 设置网页门户”。

打开“Web Station”,服务选择“kavita”,门户类型选择“基于端口”,端口可以自定义一个,我配置了 2334

等项目构建完成后,在浏览器输入 你NAS的IP:2334 就可以使用 Kavita 了。

第一件事是注册一个账号,填了什么自己要记得哈~

有了账号之后就可以登录了。

首次打开界面是全英的。

点击右上角的齿轮按钮,切换到“Preferences”,在“Locale”这项切换成“中文”。

然后刷新一下页面就变成中文了。

我用漫画举例,电子书的方法也是一样的。

打开“File Station”,找到“/docker/kavita/manga”,丢几部漫画进去。

回到 Kavita,在“服务器”这项,点击“资料库”,然后“添加资料库”。

名称可以自己输入,类型选择“漫画”,然后点击“下一步”。

文件夹这里,选择刚刚创建的“/manga”(文章开头创建的那个文件夹)。

封面可以跳过。

高级这里,文件类型全选上。

点击“强制扫描”后,回到首页就能看到这部漫画了。

看吧~


以上就是本文的全部内容啦,有疑问可以在评论区讨论~

想了解更多NAS玩法可以关注《NAS邪修》👏

点赞 + 关注 + 收藏 = 学会了

在2026年央视春晚中,魔法原子、宇树科技、银河通用、松延动力等多家机器人公司的产品登台表演,带来了特技展示、武术、小品等多样化节目。

2026年春晚机器人阵容与表演

  1. 魔法原子:作为本届春晚首家亮相的机器人企业,其人形机器人MagicBot Gen1在节目中亮相并向观众挥手致意,而MagicBot Z1则展示了“托马斯360°”特技动作,据公开资料这是业内同尺寸人形机器人首次完成该动作。

  1. 松延动力:该公司携多款机器人参与了小品《奶奶的最爱》,与知名艺术家蔡明、王天放合作演出。

    • 旗下“小顽童N2”机器人在舞台上接连完成了侧手翻、空翻等动作。
    • 首次公开亮相的消费级人形机器人“小布米”则呈现了高度拟人的流畅跑步姿态。

  1. 宇树科技:该公司机器人今年与河南塔沟武术学校共同带来了武术表演《武BOT》,展示了升级版的“赛博功夫”。

  1. 银河通用:该公司参与了微电影《我最难忘的今宵》,与沈腾、马丽等演员同台,其机器人被宣布为2026年春晚指定具身大模型机器人。

历年春晚机器人回顾

机器人登上春晚舞台的历史已超过十年,以下是部分代表性事件:

  • 2012年(龙年春晚):来自哈尔滨工业大学创新基地的多台小型机器人参与了儿童节目《机器人总动员》的表演。

  • 2016年(猴年春晚):优必选派出540台Alpha 1S机器人与歌手孙楠共同表演了歌曲《冲向巅峰》。

  • 2021年(牛年春晚):宇树科技的四足机器人(机器狗)和ABB的工业机器人(机械臂)参与了歌舞节目《牛起来》。

  • 2024年(龙年春晚):新松机器人派出8台工业机器人在表演中承担了作用。

  • 2025年(蛇年春晚):宇树科技的16台H1人形机器人与舞蹈演员共同表演了创意舞蹈《秧BOT》,引发了广泛关注。

机器人上春晚的意义与技术演进

机器人频繁亮相春晚,不仅是为舞台艺术注入硬核科技元素,更是中国机器人产业发展水平的集中展示。从早期的自动化控制、实现整齐划一的群舞,到基于深度学习、图像识别技术展现较稳定的力控能力,再到近年来结合人工智能大模型,使机器人迈向具身智能、自主学习的新阶段,春晚舞台见证了机器人技术的快速演进。同时,春晚作为拥有巨大流量的平台,既放大了公众对人形机器人的认知,也为本土品牌提供了面向全球的输出窗口,并对参与企业的系统稳定性、工程可靠性提出了高要求,推动了技术从实验室走向更广泛的应用。

用语言模型写代码、查数据库、跑自动化流程这些事情大家早就习以为常了。Vibe Coding 到今年二月刚好满一年,绝大多数人或多或少都在用它搞定代码库、写文档、处理各种杂活。但有一个问题始终是避免不了的:任务一多Agent 就开始丢三落四甚至开始一本正经地胡说八道。

MCP 让外部工具的接入变得很方便,Playwright、Supabase、Slack 这些都能挂上去,但代价是Context Rot [1]。简单说就是输入 Token 一多模型性能就会塌方式下降。

我们先看看上下文窗口里到底装了些什么。

Claude的内存结构拆解

拿 Claude 举例,它的上下文窗口大致是这么分配的:系统提示词占 1.4%系统工具(包括 MCP 工具)占 8.3%,Agent 上下文(技能、工具描述、对话历史)吃掉约 70%,用户实际能用的提示词空间反而很小。

Anthropic 的研究数据表示:真正用来放系统级指令的部分只有大约 10%,剩下全被对话历史、工具输出和各种中间结果给填满了。一旦膨胀到 200K Token 的量级模型根本分不清什么才是重点。

模型健忘、幻觉频发的根本原因就是 Context Rot。一个缓解思路是 Ralph Wiggum Loop,可以更合理地利用上下文窗口。

CodeAct

CodeAct[2] 是 2024 年的一篇论文,核心思想非常简单:既然语言模型天然擅长写代码为什么不直接让它用可执行代码来和外部世界交互?说白了就是用代码当动作空间。这个想法对任何写过程序的人来说都不陌生。

举个最简单的例子,假设要做一个学生数据库管理系统,现在需要查 2025 年入学的所有学生。如果让语言模型自己在上下文里逐条扫描记录来"推理",那既慢又不靠谱。但换个思路直接写一条 SQL:

 SELECT *  
 FROM students  
 WHERE enrollment_year = 2025;

跑一下就完事了。把检索的工作交给数据库引擎,这才是正常开发者的做法。

CodeAct 的逻辑完全一样,与其把海量数据塞进上下文让模型去"理解"(顺便制造 Context Rot),不如让模型写一段代码、执行它、拿到结果。

Image from [2] — CodeAct in multi-turn interaction framework

回到刚才的学生数据库场景,CodeAct 的工作流是这样的:先接收用户的自然语言查询——

 Find me the record of students who have been enrolled in the year 2025

然后通过一次 LM 调用理解意图,生成 Python 代码,在编译器里跑一遍,检查输出。结果满意就直接返回,不满意就继续迭代修正,直到拿到正确答案。

1、原子工具使用:CodeAct 匹配或超越 JSON/Text

第一组实验测的是最基础的场景:单个 API 调用。在 API-Bank 基准上作者对比了文本格式调用、JSON 格式调用和 CodeAct(Python 函数调用)三种方案。

即便在这种完全用不上控制流优势的简单场景下,CodeAct 在多数模型上的正确率都持平甚至更高。GPT-4、Claude 这些闭源模型在三种格式上都表现稳定,但开源模型从 CodeAct 中获益明显更大。合理的解释是:预训练阶段见过大量代码的模型,用代码表达动作比用 JSON 更自然、更顺手。

Image from [2] — Atomic API call correctness comparison.

2、复杂多工具任务:CodeAct 实现更高成功率

真正拉开差距的是多工具组合场景。M3ToolEval 基准包含 82 个人工精选的多工具任务,CodeAct 在这里的优势就很明显了——模型可以在一个代码块里组合多个工具、用循环和条件语句控制流程、存储中间变量、跨步骤复用输出。

数据上看,最佳模型的成功率绝对提升了 20.7%,交互轮次平均减少 2.1 轮。有意思的是,模型越强,从结构化动作空间里获得的收益就越大。

Image from [2] — Success rate comparison

Image from [2] — Full M3ToolEval results

3、多轮自调试

CodeAct 带来了一个很有意思的能力:自调试。因为动作本身就是代码,执行出错会产生 traceback,模型直接拿到结构化的错误反馈,下一轮就可以针对性地修复。

论文里展示了一个典型案例:CodeActAgent 先用 Pandas 下载数据,然后训练回归模型、可视化系数,中间碰到 matplotlib 报错就自己修,发现缺失值就自己处理。整个过程不是简单的工具调用,而是基于执行反馈的迭代推理。

Image from [2] — Multi-turn interaction example.

4、微调后的 CodeActAgent 进一步提升性能

作者构建了 CodeActInstruct 数据集(约 7k 条多轮轨迹),在此基础上微调出了 CodeActAgent。相比基础的 LLaMA-2 和 Mistral 有大幅提升,在 MINT 任务上表现突出,跟更大规模的模型相比也有竞争力。

比如 CodeActAgent(Mistral-7B),在同等规模的开源模型里排在前列,通用 Agent 任务得分明显提高,同时在 MMLU、GSM8K、HumanEval 等通用能力评测上也没有退化。

Image from [2] — CodeActAgent evaluation.

从实验数据整体来看,CodeAct 做到的不只是格式上的改进。它实质上重构了 Agent 的动作空间——模型获得了控制流、数据流、可复用变量和自动反馈循环。工具使用不再是一个接一个地调 API,而是变成了可编程的推理过程。交互步骤更少,任务成功率更高,特别是在需要组合多个工具的场景下。

实现

我先试了 langchain-codeact[3] 这个包,但坑不少,而且只兼容 Anthropic 的模型,所以干脆自己撸了一个小原型。

实验环境用的 Google Colab + OpenAI API。生产环境建议用隔离沙箱。

导入依赖

 import os  
import re  
import io  
import contextlib  
from openai import OpenAI  
from google.colab import userdata  

os.environ["OPENAI_API_KEY"] = userdata.get("OPENAI_API_KEY")  
 client = OpenAI()

两个工具函数:一个从 LM 输出里提取 Python 代码块,另一个用内置的 exec() 执行它。

 def extract_python_code(text: str):  
    pattern = r"```python(.*?)```"  
    match = re.search(pattern, text, re.S | re.I)  
    return match.group(1).strip() if match else None  

def run_python(code: str):  
    buf = io.StringIO()  
    try:  
        with contextlib.redirect_stdout(buf):  
            exec(code, {})  
        return buf.getvalue()  
    except Exception as e:  
         return f"Execution error: {e}"

CodeAct 循环,这里用 GPT-5-mini 作为底座模型,因为需要推理能力。

 SYSTEM_PROMPT="""  
You are a CodeAct agent.  
Always solve using Python code.  
Return ONLY a python code block.  
Do not explain in text.  
"""  

def codeact_run(user_problem, max_iters=3):  

    messages= [  
        {"role": "system", "content": SYSTEM_PROMPT},  
        {"role": "user", "content": user_problem}  
    ]  

    for step in range(max_iters):  

        resp=client.chat.completions.create(  
            model="gpt-5-mini",  
            messages=messages  
        )  

        text=resp.choices[0].message.content  

        print("\n===== MODEL OUTPUT =====\n")  
        print(text)  

        code=extract_python_code(text)  

        if not code:  
            print("\n No code block — retrying\n")  
            messages.append({  
                "role": "user",  
                "content": "Return python code block only."  
            })  
            continue  

        print("\n EXECUTING PYTHON:\n")  
        print(code)  

        output=run_python(code)  

        print("\n PYTHON OUTPUT:\n")  
        print(output)  

        returnoutput  

    print("Failed to obtain executable code.")  
     returnNone

整个流程就是:让 LM 理解问题、写 Python 代码,extract_python_code() 提取代码,run_python() 执行,拿到结果。

 problem="""  
A batter hits a baseball at 45.847 m/s at 23.474 degrees.  
Outfielder throws it back at 24.12 m/s at 39.12 degrees.  
Find final distance from batter.  
Assume no air resistance and g = 9.8.  
"""  

 codeact_run(problem)

输出:

    
===== MODEL OUTPUT =====  

import math

Given values

v1 = 45.847 # m/s, initial hit speed
theta1_deg = 23.474 # degrees
v2 = 24.12 # m/s, outfielder throw speed
theta2_deg = 39.12 # degrees
g = 9.8 # m/s^2

Convert to radians

theta1 = math.radians(theta1_deg)
theta2 = math.radians(theta2_deg)

Range of the initial hit (from batter, to the right is positive)

R1 = (v1*2) math.sin(2 * theta1) / g

Outfielder throws back toward the batter: horizontal velocity is negative

vx2 = -v2 * math.cos(theta2)
vy2 = v2 * math.sin(theta2)

Time of flight for the throw (starting and ending at ground level)

t2 = 2 * vy2 / g

Horizontal displacement of the throw relative to outfielder's position

dx2 = vx2 * t2

Final x-position relative to batter (batter at x=0)

x_final = R1 + dx2

Distance from batter (absolute)

distance_from_batter = abs(x_final)

print(f"Initial landing distance from batter (R1): {R1:.3f} m")
print(f"Horizontal displacement from outfielder's throw (dx2): {dx2:.3f} m")
print(f"Final position relative to batter (x_final): {x_final:.3f} m")
print(f"Final distance from batter: {distance_from_batter:.3f} m")


EXECUTING PYTHON:  

import math  

# Given values  
v1 = 45.847       # m/s, initial hit speed  
theta1_deg = 23.474  # degrees  
v2 = 24.12        # m/s, outfielder throw speed  
theta2_deg = 39.12   # degrees  
g = 9.8           # m/s^2  

# Convert to radians  
theta1 = math.radians(theta1_deg)  
theta2 = math.radians(theta2_deg)  

# Range of the initial hit (from batter, to the right is positive)  
R1 = (v1**2) * math.sin(2 * theta1) / g  

# Outfielder throws back toward the batter: horizontal velocity is negative  
vx2 = -v2 * math.cos(theta2)  
vy2 = v2 * math.sin(theta2)  

# Time of flight for the throw (starting and ending at ground level)  
t2 = 2 * vy2 / g  

# Horizontal displacement of the throw relative to outfielder's position  
dx2 = vx2 * t2  

# Final x-position relative to batter (batter at x=0)  
x_final = R1 + dx2  

# Distance from batter (absolute)  
distance_from_batter = abs(x_final)  

print(f"Initial landing distance from batter (R1): {R1:.3f} m")  
print(f"Horizontal displacement from outfielder's throw (dx2): {dx2:.3f} m")  
print(f"Final position relative to batter (x_final): {x_final:.3f} m")  
print(f"Final distance from batter: {distance_from_batter:.3f} m")  

✅ PYTHON OUTPUT:  

Initial landing distance from batter (R1): 156.731 m  
Horizontal displacement from outfielder's throw (dx2): -58.119 m  
Final position relative to batter (x_final): 98.612 m  
Final distance from batter: 98.612 m  

 'Initial landing distance from batter (R1): 156.731 m\nHorizontal displacement from outfielder\'s throw (dx2): -58.119 m\nFinal position relative to batter (x_final): 98.612 m\nFinal distance from batter: 98.612 m\n'

到这里可以看到 CodeAct 是怎么让模型动手干活的——写代码、执行、拿结果,LLM 有了"编程的手",不再只是被动回答问题。

但还有一个问题没解决。

模型能写代码了,可如果输入本身就极其庞大呢?几百页的报告、几个 G 的日志、整个代码仓库——单次前向传播根本消化不了这么多信息。

那如果代码不只是用来调 API、查数据库,而是用来组织模型自身的推理过程呢?

这就是递归语言模型(Recursive Language Models)要解决的事情。模型不再写代码去调用外部工具,而是写代码来调用自己——把大任务拆成小任务,分别处理,最后把结果拼起来。

CodeAct 是代码作为动作接口,RLM 是代码作为推理控制器。

RLM 递归语言模型


RLM [4] 由 Alex Zhang 和 Omar Khattab 在 2025 年 10 月提出。他们在论文中明确表示受到了 CodeAct 的启发,但认为 CodeAct 在面对超长上下文的推理任务时力不从心。

用伪代码描述 RLM 的工作方式:

 huge document  
 → split into sections  
 → model analyzes each section  
 → model summarizes  
 → model calls itself on summaries  
 → final answer

论文给出的正式定义是:"一种通用推理策略,将长提示词视为外部环境的一部分,允许 LLM 以编程方式检查、分解并递归地在提示词片段上调用自身。"

Image from [4]

上图来自 RLM 论文 [4],展示的是如何处理一整本书这样的超大上下文。RLM 不会把完整文本硬塞进模型(塞不进去),而是把提示词当作外部环境来对待。具体操作是:先把提示词作为变量加载到 REPL 环境里,然后用代码把它拆成可管理的小块。根语言模型(depth = 0)通过代码执行检查文本的不同部分,挑出跟任务相关的块,对这些块发起递归子调用(depth = 1)。每个子调用只看自己那一小段上下文,返回中间结果,最后由根模型把这些结果聚合起来生成最终响应。这套机制绕过了上下文窗口的硬限制,让模型可以通过结构化分解和受控递归处理任意长度的输入。

不同上下文长度下的性能表现

RLM 的核心主张是能把推理能力扩展到标准 LLM 固定上下文窗口之外。实验数据很支持这个论断。

在 S-NIAH(恒定复杂度)、OOLONG(线性复杂度)、OOLONG-Pairs(二次复杂度)三个基准上,基础语言模型的表现都是随输入长度增加快速崩塌的,任务复杂度越高崩得越厉害。RLM 则一路稳住了——哪怕输入规模到了百万 Token 量级,远远超过底层模型的原生窗口大小,表现依然稳健。

Image from [4] — the log-scale performance vs input length graph.

信息密集型推理任务的性能

差距最大的地方在信息密集型任务上。OOLONG 和 OOLONG-Pairs 要求模型聚合输入的几乎每个部分,做语义变换,根据成对关系构建输出——简单说就是不能跳过任何信息。

在 OOLONG-Pairs 上,基础模型的 F1 分数接近零,根本处理不了长上下文下的密集关系推理。RLM 却通过递归分解展示了涌现能力,GPT-5 的配置下拿到了 58% 的 F1。

这说明 RLM 做的不只是"看更多 Token"这么简单。它改变的是推理本身的执行方式。

Image from [4] — performance comparison table.

效率和成本分析

递归推理听起来开销应该很大,但实际数据却不是这样:RLM 的平均 API 成本跟基础模型差不多,有时候反而更低。

原因在于 RLM 不会获得整个上下文。它通过代码执行和针对性的子调用只探测相关部分,避免了全量上下文的输入,减少了无效 Token 的处理,计算资源只分配到真正需要的地方。虽然由于执行轨迹的不同,RLM 的成本方差会大一些,但中位成本通常跟摘要压缩之类的基线策略持平甚至更低。

好的推理性能靠的不是暴力堆算力,而是结构化分解和选择性上下文交互。

Image from [4]-cost distribution quartiles.

RLM 的核心优势不在于访问了更多 Token,而在于彻底改变了推理的计算结构。把提示词外化到环境中、允许递归子调用,推理就从被动地消耗 Token 变成了主动的信息检索和受控分解。Context Rot 被削弱了,推理时的信噪比上去了,任意长度的输入都可以通过可扩展的聚合来处理。本质上RLM 把长上下文推理从内存瓶颈问题转化成了一个编程式的搜索问题。

实现

我这里用一本 Arthur Conan Doyle 的《福尔摩斯探案集》(txt 格式)[5] 当输入。Grammerly 统计下来单词量超过 1M,字符数约 6.5M。

using Grammerly for word count

按 OpenAI[6] 的换算规则,常见英文文本中 1 个 Token 大约对应 4 个字符,也就是约 0.75 个单词。6.5M 字符对应大约 1.625M Token。

Image from [7]

OpenAI 当前最强模型的上下文窗口是 400,000 Token,我实验用的 GPT-4.1-mini 是 1,047,576 Token。但实际推理时用户拿不到全部窗口,系统提示词、工具描述之类的要占掉一大块,输入输出 Token 还得共享这个空间。就算假设能用满 1,047,576 的窗口,1,625,000 Token 的输入也放不进去。

所以问题很明确:这么大的上下文,怎么用 RLM 来处理?

实验任务是让模型"提取前 20 个最频繁出现的大写实体,并总结 3 个主要主题"。

安装依赖,用 pip 装 rlms[8] 包:

 !pip install -qU rlms

导入包,加载环境变量(Google Colab 的写法略有不同):

import os  
from rlm import RLM  
from rlm.logger import RLMLogger  
from google.colab import userdata [#google](#google) colab  
api_key = userdata.get('OPENAI_API_KEY')

加载文档,创建 RLMLogger 存日志:

with open("big.txt", "r", encoding="utf-8") as f:  
    large_document = f.read()  

print(f"Loaded document with {len(large_document)} characters.")  

logger = RLMLogger(log_dir="./logs")

初始化 RLM environment 设成 local, Python 内置的 exec() 来跑代码。也可以换成 docker、prime-sandbox、modal、daytona 之类的外部服务。

max-depth 设成 1,跟论文里一致,调用结构是这样的:

Root LM  
   ├── Sub LM call  
   ├── Sub LM call  
   └── Sub LM call

如果 max-depth = 2 就多一层嵌套:

Root LM  
   ├── Sub LM  
   │     ├── Sub-Sub LM  
   │     └── Sub-Sub LM  
   └── Sub LM  
         └── Sub-Sub LM

以此类推。

rlm = RLM(  
    backend="openai",  
    backend_kwargs={  
        "model_name": "gpt-4.1-mini",  # You can upgrade to stronger model  
        "api_key": api_key,  
    },  
    environment="local",  
    environment_kwargs={},  
    max_depth=1,  # Depth-1 recursion like in paper  
    logger=logger,  
    verbose=True,  
)  
prompt = f"""  
You are analyzing a large enterprise document.  

Return ONLY valid JSON.  

TASK:  
Extract the top 20 most frequent capitalized entities  
and summarize 3 major themes.  

Document:  
{large_document}  
"""  
result = rlm.completion(prompt)  

print("\n RLM Analysis Result:\n")  
print(result)

结果出来了:

 Loaded document with 6488666 characters.  
╭─ ◆ RLM ━ Recursive Language Model ──────────────────────────────────────────────────────────────────────────────╮  
│                                                                                                                 │  
│    Backend                           openai                     Environment                       local         │  
│    Model                             gpt-4.1-mini               Max Iterations                    30            │  
│    Max Depth                         1                                                                          │  
│                                                                                                                 │  
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯  
──────────────────────────────────────────────────  Iteration 1  ──────────────────────────────────────────────────  
╭─ ◇ LLM Response  (1.57s) ───────────────────────────────────────────────────────────────────────────────────────╮  
│ ```repl                                                                                                         │  
│ # First, let's see what the prompt or question is exactly, and inspect the beginning of the context to          │  
│ understand what type of information it contains.                                                                │  
│ print(context[:2000])  # print the first 2000 chars to get an idea of what the context looks like               │  
│ ```                                                                                                             │  
│                                                                                                                 │  
│ ~45 words                                                                                                       │  
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯  
╭─ ▸ Code Execution  (0.000s) ────────────────────────────────────────────────────────────────────────────────────╮  
│ Code:                                                                                                           │  
│ # First, let's see what the prompt or question is exactly, and inspect the beginning of the context to          │  
│ understand what type of information it contains.                                                                │  
│ print(context[:2000])  # print the first 2000 chars to get an idea of what the context looks like               │  
│                                                                                                                 │  
│ Output:                                                                                                         │  
│                                                                                                                 │  
│ You are analyzing a large enterprise document.                                                                  │  
│                                                                                                                 │  
│ Return ONLY valid JSON.                                                                                         │  
                                          |  
                                          |    (To be continue)  
                                          |     
─────────────────────────────────────────────────  Iteration 25  ──────────────────────────────────────────────────  
╭─ ◇ LLM Response  (2.98s) ───────────────────────────────────────────────────────────────────────────────────────╮  
│ FINAL({                                                                                                         │  
│   "top_20_most_frequent_capitalized_entities": [                                                                │  
│     ["Holmes", 278],                                                                                            │  
│     ["Watson", 114],                                                                                            │  
│     ["Mr", 92],                                                                                                 │  
│     ["Sherlock Holmes", 77],                                                                                    │  
│     ["Baker Street", 23],                                                                                       │  
│     ["McCarthy", 22],                                                                                           │  
│     ["Neville St. Clair", 22],                                                                                  │  
│     ["Irene", 20],                                                                                              │  
│     ["Adler", 18],                                                                                              │  
│     ["London", 18],                                                                                             │  
│     ["King", 17],                                                                                               │  
│     ["Hosmer Angel", 16],                                                                                       │  
│     ["Henry Baker", 14],                                                                                        │  
│     ["Lestrade", 12],                                                                                           │  
│     ["Bohemia", 11],                                                                                            │  
│     ["City", 11],                                                                                               │  
│     ["Mr. Windibank", 11],                                                                                      │  
│     ["K. K. K.", 11],                                                                                           │  
│     ["Mrs. St. Clair", 11],                                                                                     │  
│     ["Lascar", 11]                                                                                              │  
│   ],                                                                                                            │  
│   "top_3_major_themes_summary": [                                                                               │  
│     "Deduction and Observation",                                                                                │  
│     "Deception and Disguise",                                                                                   │  
│     "Mystery and Detection"                                                                                     │  
│   ]                                                                                                             │  
│ })                                                                                                              │  
│                                                                                                                 │  
│ ~68 words                                                                                                       │  
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯  
╭─ ★ Final Answer ────────────────────────────────────────────────────────────────────────────────────────────────╮  
│                                                                                                                 │  
│  {                                                                                                              │  
│    "top_20_most_frequent_capitalized_entities": [                                                               │  
│      ["Holmes", 278],                                                                                           │  
│      ["Watson", 114],                                                                                           │  
│      ["Mr", 92],                                                                                                │  
│      ["Sherlock Holmes", 77],                                                                                   │  
│      ["Baker Street", 23],                                                                                      │  
│      ["McCarthy", 22],                                                                                          │  
│      ["Neville St. Clair", 22],                                                                                 │  
│      ["Irene", 20],                                                                                             │  
│      ["Adler", 18],                                                                                             │  
│      ["London", 18],                                                                                            │  
│      ["King", 17],                                                                                              │  
│      ["Hosmer Angel", 16],                                                                                      │  
│      ["Henry Baker", 14],                                                                                       │  
│      ["Lestrade", 12],                                                                                          │  
│      ["Bohemia", 11],                                                                                           │  
│      ["City", 11],                                                                                              │  
│      ["Mr. Windibank", 11],                                                                                     │  
│      ["K. K. K.", 11],                                                                                          │  
│      ["Mrs. St. Clair", 11],                                                                                    │  
│      ["Lascar", 11]                                                                                             │  
│    ],                                                                                                           │  
│    "top_3_major_themes_summary": [                                                                              │  
│      "Deduction and Observation",                                                                               │  
│      "Deception and Disguise",                                                                                  │  
│      "Mystery and Detection"                                                                                    │  
│    ]                                                                                                            │  
│  }                                                                                                              │  
│                                                                                                                 │  
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯  
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════  
                                                Iterations       25                                                      
                                                Total Time       397.59s                                                 
                                                Input Tokens     2,026,303                                               
                                                Output Tokens    98,396                                                  
 ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════                                                                       

25 轮迭代,总耗时约 400 秒,处理了 2M 输入 Token(含子 LM 调用)输出约 98,000 Token。给了一个塞不进任何上下文窗口的巨大输入,模型还是给出了接近正确的结果。

Actual count vs RLM count

跟确定性正则匹配的计数比,RLM 的输出有轻微偏差。这可以理解因为RLM 做的是跨 chunk 的递归语义实体提取,不是严格的词法计数。聚合过程中会引入累计漂移,高频实体和变体形式尤其容易受影响。但关键是模型准确识别了语料库的主要叙事主题。RLM 追求的是结构化的语义理解而非 Token 粒度的精确计数。

DSPy 也把 RLM 集成进了自己的包里,可以直接用 dspy.RLM [10]。

总结:动作 vs 推理——选择正确的范式

大语言模型已经不只是文本生成器了。它们正在变成可编程的系统。CodeAct 和 RLM 是这条进化路径上两个方向不同但可以组合的范式。这两种方法都让 LLM 的推理过程变得透明可观察:中间步骤、执行轨迹、分解结构都暴露出来了,开发者不用再对着一个黑箱去猜模型在想什么。

CodeAct 把 LLM 变成了执行引擎。模型不再只是给你一个文本答案,而是写代码、跑代码、看结果,不满意就再来一轮。适合的场景包括工具调用与 API 编排、数据库查询与数据处理、流程自动化,以及需要通过执行来验证正确性的结构化问题求解。一句话概括:CodeAct 适合需要"动手做事"的任务。

RLM 走的是另一个方向。它不是让模型去操作外部世界,而是让模型用代码来组织自己的推理过程——递归分解大输入、通过受控子调用聚合结果。适用于超长文档处理、多文档推理、信息密集型的分析任务、跨大规模语料库的结构化聚合。RLM 解决的是推理规模的瓶颈。

CodeAct 是代码作为动作接口,RLM 是代码作为推理控制器。

两者不是互斥的。在生产系统里完全可以组合使用——RLM 负责在海量上下文中完成推理,CodeAct 负责把决策执行出去、跟外部系统交互。

这里真正的范式转移是:与其一味地扩大上下文窗口,不如去重构计算本身。无论是 CodeAct 的执行循环还是 RLM 的递归分解,LLM 系统的未来不在于能吃下多少 Token,而在于如何更聪明地控制推理和动作。

引用

https://avoid.overfit.cn/post/021ca9c0ed414fac82ab09532992b7df

by Shreyansh Jain

  1. 缘起

大家好,我是归序的独立开发者。
作为一个每天会议很多的开发者,我非常讨厌传统日历 App 的交互——点“+”号、滚轮选日期、手动填备注……这个过程太打断心流了。
虽然市面上有很多 AI 工具,但大多数都在“自作主张”地乱填我的时间表。
我想要一个“守边界”的 AI ,于是花了两三个月,开发了这款 App 。

  1. 核心特性
  • 真正的 NLP 输入
    支持复杂的自然语言。比如输入“下周三五晚上 8 点健身,直到月底”,它能精准识别循环规则,不用你滚轮选半天。

  • **流式交互 (Token-level Streaming)**:
    为了解决 LLM 的延迟感,前端做了字符级的流式渲染。你可以肉眼看到 AI 在“思考”并将时间、地点、备注填入卡片的过程(见下方 图)。

APP 截图

  • AI Proposes, You Decide
    这是 App 的核心逻辑。AI 生成提案卡片,你点 Confirm 才入库。拒绝 AI 幻觉污染你的日历。
  • 全链路闭环
    支持后台静默运行与冲突检测。比如早起推送到锁屏的“晨间简报”,提醒你今天的时间冲突。
  1. 技术栈

简单聊聊技术实现,欢迎同行交流:

  • 后端:使用 Go 重写了核心服务,配合 LangGraph-go 做 Agent 编排,为了保证高并发下的低延迟。
  • 前端Flutter 开发,为了双端一致性和高性能渲染(先上了 IOS
  • IOS ,安卓也在计划内)。
  • AI:针对意图识别做了大量 Prompt Engineering ,尽量减少 Token 消耗并提升准确率。

目前 v1.0 刚刚跑通,iOS TestFlight 开放中。

官网: http://universalai.bettercall.cn/
允许匿名访问,不用注册。

智能推送

APP 截图

对我来讲,我个人更希望实现的是,一个绝对安全且何时克制的管家,在正确的时间使用正确的方式通知我,甚至帮我安排日程

暂时没有付费计划,效果好未来可能会加入 pay as you go 。

但是无论如何 绝对绝对绝对 不会使用任何方式加入任何形式的广告或是推广。

希望 V 友用的开心,有问题随时留言

马年至,愿诸君:
乘骐骥以驰骋,前程万里;
驾扁舟而浮游,顺遂无虞。
椿萱并茂,棠棣同馨。

JzmnBPOIafk8vOUruJXXdKegSHkupEFR.webp