包含关键字 typecho 的文章

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

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

可视化拖拽是 n8n 的优势,但也有明显的局限性。但实际使用中,你大概率会遇到 “现成节点不够用” 的情况,这时候「代码节点」就成了你的 “万能补位工具”。

「代码节点」核心可以总结为 4 个场景:

  1. 个性化逻辑无法通过拖拽实现。
  2. 特殊格式数据的处理需求。
  3. 弥补 n8n 没有的内置功能。
  4. 简化工作流,减少节点冗余

简单来说:可视化节点能搞定的,优先用拖拽;可视化节点搞不定的,就用代码节点补位,两者结合才能发挥 n8n 的最大威力。

本文介绍「代码节点」里常用的内置变量和方法。

代码节点的用法讲解

在 n8n 中可以搜索 code 就能找到「代码节点」。

n8n 的代码节点支持两种编程语言:JavaScript(默认,最常用)和 Python

这两种语言上手都很简单,我之前写过一篇《Python 快速入门篇》,想在 n8n 用 Python 写代码的工友可以看看。之后也许会再开一个新坑聊聊 JS 的算法以及容易出错的点,毕竟 JS 可是我以前的淘金铲。

「代码节点」提供了2种运行模式(Mode):

  • Run Once for All ltems:这是默认设置。当你的工作流程运行时,代码节点中的代码会执行一次,无论输入项多少。
  • Run Once for Each Item:如果你想让代码对每个输入项目运行一次,就选择这个。

代码节点的工作逻辑很简单,就三件事:获取输入数据 → 处理数据 → 输出数据,这是使用代码节点的关键,必须先搞懂。

在使用「代码节点」之前我建议你先收藏一下 n8n 代码节点的文档👉 https://docs.n8n.io/code/code-node

n8n 的代码运行环境提供了一些针对工作流常用到的内置函数和内置变量,它们能大大减少你的原生代码量。所以一定要多看 n8n 的官方文档。

内置变量通常是以 $ 符号开头的,(念念叨叨一句:死去的 JQuery 记忆被踢了一脚)。

在代码编辑器里输入 $ ,编辑器就会提示有哪些内置函数和变量可以使用,非常方便。

前面说了它能节省我们的原生代码量,到底有多节省呢?

比如,输出今天的日期,使用 n8n 内置变量这么写,而且语义非常清晰⬇️

console.log($today)

用JS原生的方法要这么写⬇️

/**
 * 获取当天的年、月、日(格式化版本,个位数补零)
 * @param {boolean} [returnString=false] - 是否返回格式化字符串(如"2026-01-25"),默认返回对象
 * @returns {Object|string} 格式化后的年月日对象或字符串
 */

function getCurrentFormattedDate(returnString = false) {
  const now = new Date();
  const year = now.getFullYear();
  
  // 月份补零:先+1转为实际月份,再转为字符串,不足2位则前面补0
  const month = String(now.getMonth() + 1).padStart(2, '0');
  
  // 日期补零:同理,不足2位补0
  const day = String(now.getDate()).padStart(2, '0');
  
  // 根据参数返回对应格式
  if (returnString) {
    return `${year}-${month}-${day}`;
  }
  
  return {
    year,
    month,
    day
  };
}

console.log(getCurrentFormattedDate(true))

使用 console.log() 可以在浏览器控制台输出内容。

比如上面的 console.log($today),按 F12 打开浏览器控制台,切换到 Console 面板,点击运行代码就能看到控制台输出了对应的内容。

读取上一个节点输入的数据

用到「代码节点」大概率是要处理数据的了,所以接收上一个节点传入的数据也是很常见的操作。

n8n 提供了一堆内置方法让我们很方便的获取上一个节点传入的数据。

这个文档也要收藏👉 https://docs.n8n.io/code/code-node

如果你想获取「代码节点」前面任意一个节点的数据,其实不需要记住代码,只要会用鼠标拖拽就行了。

比如我这个工作流,用「HTTP 节点」请求了一些数据,并且给这个「HTTP 节点」重命名为 GetUserInfo,然后再接一个「Edit Fields 节点」里面设置了一个 name_zh 数据。

在最后的「代码节点」里要获取「GetUserInfo 节点」的数据只需要拖进来就行了。

可以看到,拖进来之后它是以 $('GetUserInfo') 的方式找到这个节点,也就是通过节点名的方式找到它。

first() 是这个节点的第一项数据,节点返回的数据是数组,这些在官方文档都有提到。

输出结果可以打开浏览器控制台看看。

常用方法

n8n 还提供了一些常用的方法 ⬇️

https://docs.n8n.io/code/builtin/convenience

比如 $ifEmpty()

$ifEmpty(value, defaultValue)

这个方法会判断 value 是否为空(undefinednull''、空数组、空对象),如果为空则返回 defaultValue 的值。

要在一堆数值里挑出最大值或者最小值,可以使用 $max()$min() 方法。

其他方法可以参考文档,用法非常简单。

输出数据给下一个节点

「代码节点」还有一个功能就是整理数据。这个能力完全覆盖了「Edit Fields 节点」的能力。

在「代码节点」里,通过 return 可以将数据传递给下一个节点,而 return 的内容就是你整理好的数据。

比如,从前2个节点获取到一大堆数据,但我就是要骗下一个节点说没获取到任何用户信息,就可以这么写。

当然,我们通过节点名可以获取任意祖先节点的数据,通过「代码节点」可以将所有祖先数据都重新组装一遍返回给下一个节点。


以上就是本文的全部内容啦,想了解更多n8n玩法欢迎关注《n8n修炼手册》👏

如果你有 NAS,我非常建议你在 NAS 上部署一套 n8n,搞搞副业也好,帮你完成工作任务也好 《『NAS』不止娱乐,NAS也是生产力,在绿联部署AI工作流工具-n8n》

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

签到服务框架: https://github.com/qd-today/qd


当天首次签到

  1. 签到框架日志
    图片
  2. Telegram 通知
    图片


当天已签过到

  1. 签到框架日志
    图片
  2. Telegram 通知
    图片


如何配置?

现在还没有上架 qd 项目的 默认仓库 ,后续看情况,尽量上架。

  1. 现阶段可以把以下代码保存为har文件,导入到 qd 系统中,保存为模板。
    图片
复制
[
    {
        "comment": "发起签到请求",
        "request": {
            "method": "POST",
            "url": "https://2libra.com/api/sign",
            "headers": [
                {
                    "name": "User-Agent",
                    "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
                },
                {
                    "name": "Accept",
                    "value": "application/json, text/plain, */*"
                },
                {
                    "name": "Accept-Language",
                    "value": "zh,zh-CN;q=0.9,en-US;q=0.8"
                },
                {
                    "name": "Accept-Encoding",
                    "value": "gzip, deflate, br, zstd"
                },
                {
                    "name": "Referer",
                    "value": "https://2libra.com/"
                },
                {
                    "name": "Origin",
                    "value": "https://2libra.com"
                },
                {
                    "name": "Sec-GPC",
                    "value": "1"
                },
                {
                    "name": "Sec-Fetch-Dest",
                    "value": "empty"
                },
                {
                    "name": "Sec-Fetch-Mode",
                    "value": "cors"
                },
                {
                    "name": "Sec-Fetch-Site",
                    "value": "same-origin"
                },
                {
                    "name": "Connection",
                    "value": "keep-alive"
                },
                {
                    "name": "Cookie",
                    "value": "{{cookie}}"
                },
                {
                    "name": "TE",
                    "value": "trailers"
                },
                {
                    "name": "Authorization",
                    "value": "{{Authorization}}"
                }
            ],
            "cookies": [

            ]
        },
        "rule": {
            "success_asserts": [
                {
                    "re": "201",
                    "from": "status"
                },
                {
                    "re": "你今天已经签到过了",
                    "from": "content"
                }
            ],
            "failed_asserts": [

            ],
            "extract_variables": [
                {
                    "name": "m",
                    "re": "(?<=\"m\":\").*?(?=\")",
                    "from": "content"
                },
                {
                    "name": "message",
                    "re": "(?<=\"message\":\").*?(?=\")",
                    "from": "content"
                },
                {
                    "name": "streakd",
                    "re": "(?<=\"streak\":)\\d+(?=[,}])",
                    "from": "content"
                },
                {
                    "name": "sign_coins",
                    "re": "(?<=\"coins\":)\\d+(?=[,}])",
                    "from": "content"
                },
                {
                    "name": "balanced",
                    "re": "(?<=\"balance\":)\\d+(?=[,}])",
                    "from": "content"
                }
            ]
        }
    },
    {
        "comment": "查询账户信息",
        "request": {
            "method": "GET",
            "url": "https://2libra.com/api/users/info?fields=info%2Cexp%2Ccoins",
            "headers": [
                {
                    "name": "Host",
                    "value": "2libra.com"
                },
                {
                    "name": "User-Agent",
                    "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
                },
                {
                    "name": "Accept",
                    "value": "application/json, text/plain, */*"
                },
                {
                    "name": "Accept-Language",
                    "value": "zh,zh-CN;q=0.9,en-US;q=0.8"
                },
                {
                    "name": "Accept-Encoding",
                    "value": "gzip, deflate, br, zstd"
                },
                {
                    "name": "Referer",
                    "value": "https://2libra.com/"
                },
                {
                    "name": "Sec-GPC",
                    "value": "1"
                },
                {
                    "name": "Sec-Fetch-Dest",
                    "value": "empty"
                },
                {
                    "name": "Sec-Fetch-Mode",
                    "value": "cors"
                },
                {
                    "name": "Sec-Fetch-Site",
                    "value": "same-origin"
                },
                {
                    "name": "Connection",
                    "value": "keep-alive"
                },
                {
                    "name": "Cookie",
                    "value": "{{cookie}}"
                },
                {
                    "name": "Authorization",
                    "value": "{{Authorization}}"
                }
            ],
            "cookies": [

            ]
        },
        "rule": {
            "success_asserts": [
                {
                    "re": "200",
                    "from": "status"
                }
            ],
            "failed_asserts": [

            ],
            "extract_variables": [
                {
                    "name": "username",
                    "re": "(?<=\"username\":\").*?(?=\")",
                    "from": "content"
                },
                {
                    "name": "user_number",
                    "re": "(?<=\"user_number\":\").*?(?=\")",
                    "from": "content"
                },
                {
                    "name": "currentExp",
                    "re": "(?<=\"currentExp\":)\\d+(?=[,}])",
                    "from": "content"
                },
                {
                    "name": "nextLevelExp",
                    "re": "(?<=\"nextLevelExp\":)\\d+(?=[,}])",
                    "from": "content"
                },
                {
                    "name": "expToNext",
                    "re": "(?<=\"expToNext\":)\\d+(?=[,}])",
                    "from": "content"
                },
                {
                    "name": "balance",
                    "re": "(?<=\"coins\":)\\d+(?=[,}])",
                    "from": "content"
                },
                {
                    "name": "level",
                    "re": "(?<=\"exp\":\\{\"level\":)\\d+(?=[,}])",
                    "from": "content"
                }
            ]
        }
    },
    {
        "request": {
            "method": "GET",
            "url": "https://2libra.com/api/sign/stats",
            "headers": [
                {
                    "name": "User-Agent",
                    "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
                },
                {
                    "name": "Accept",
                    "value": "application/json, text/plain, */*"
                },
                {
                    "name": "Accept-Language",
                    "value": "zh,zh-CN;q=0.9,en-US;q=0.8"
                },
                {
                    "name": "Accept-Encoding",
                    "value": "gzip, deflate, br, zstd"
                },
                {
                    "name": "Referer",
                    "value": "https://2libra.com/"
                },
                {
                    "name": "Sec-GPC",
                    "value": "1"
                },
                {
                    "name": "Sec-Fetch-Dest",
                    "value": "empty"
                },
                {
                    "name": "Sec-Fetch-Mode",
                    "value": "cors"
                },
                {
                    "name": "Sec-Fetch-Site",
                    "value": "same-origin"
                },
                {
                    "name": "Authorization",
                    "value": "{{Authorization}}"
                },
                {
                    "name": "Connection",
                    "value": "keep-alive"
                },
                {
                    "name": "Cookie",
                    "value": "{{cookie}}"
                }
            ],
            "cookies": [

            ]
        },
        "rule": {
            "success_asserts": [
                {
                    "re": "200",
                    "from": "status"
                }
            ],
            "failed_asserts": [

            ],
            "extract_variables": [
                {
                    "name": "streak",
                    "re": "(?<=\"streak\":)\\d+(?=[,}])",
                    "from": "content"
                },
                {
                    "name": "maxStreak",
                    "re": "(?<=\"maxStreak\":)\\d+(?=[,}])",
                    "from": "content"
                },
                {
                    "name": "isotime",
                    "re": "(?<=\")\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z(?=\")",
                    "from": "content"
                }
            ]
        }
    },
    {
        "comment": "字符串替换",
        "request": {
            "method": "POST",
            "url": "api://util/string/replace",
            "headers": [

            ],
            "cookies": [

            ],
            "data": "r=json&p=Z$&s={{isotime}}&t=%2B0000"
        },
        "rule": {
            "success_asserts": [
                {
                    "re": "200",
                    "from": "status"
                },
                {
                    "re": "\"状态\": \"OK\"",
                    "from": "content"
                }
            ],
            "failed_asserts": [

            ],
            "extract_variables": [
                {
                    "name": "time0",
                    "re": "\"处理后字符串\": \"(.*)\"",
                    "from": "content"
                }
            ]
        }
    },
    {
        "comment": "返回对应时间戳和时间",
        "request": {
            "method": "POST",
            "url": "api://util/timestamp",
            "headers": [

            ],
            "cookies": [

            ],
            "data": "ts=&form=%Y-%m-%dT%H:%M:%S.%f%z&dt={{time0|urlencode}}"
        },
        "rule": {
            "success_asserts": [
                {
                    "re": "200",
                    "from": "status"
                }
            ],
            "failed_asserts": [

            ],
            "extract_variables": [
                {
                    "name": "bj_date",
                    "re": "\"北京时间\"\\s*:\\s*\"(\\d{4}-\\d{2}-\\d{2})",
                    "from": "content"
                },
                {
                    "name": "bj_time",
                    "re": "\"北京时间\"\\s*:\\s*\"\\d{4}-\\d{2}-\\d{2}T(\\d{2}:\\d{2}:\\d{2})",
                    "from": "content"
                }
            ]
        }
    },
    {
        "comment": "Unicode 转换",
        "request": {
            "method": "POST",
            "url": "api://util/unicode",
            "headers": [

            ],
            "cookies": [

            ],
            "data": "html_unescape=false&content={{username}}(第 {{user_number}} 号会员)\\r\\n{% if sign_coins %}{{message}},签到时间为:{{bj_date}} {{bj_time}},本次签到获得 {{sign_coins}} 金币{% else %}{{m}}{% endif %}\\r\\n 当前金币总数为 {{balance}} 个,已累计签到 {{streak}} 天\\r\\n 当前用户等级为 {{level}} 级,经验值为 {{currentExp}} 点,升级还需要 {{expToNext}} 点经验值"
        },
        "rule": {
            "success_asserts": [
                {
                    "re": "200",
                    "from": "status"
                },
                {
                    "re": "\"状态\": \"200\"",
                    "from": "content"
                }
            ],
            "failed_asserts": [

            ],
            "extract_variables": [
                {
                    "name": "__log__",
                    "re": "\"转换后\": \"(.*)\"",
                    "from": "content"
                }
            ]
        }
    }
]
  1. 通过刚刚导入的模板新建一个任务,填入你在 2Libra 的 cookie 或者 Authorization 即可。
    图片

然后这个任务就会定时执行啦!希望收到推送通知的话可以再配置一下推送渠道。

  • 图片

开发者朋友们大家好:

这里是 「RTE 开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的技术」、「有亮点的产品」、「有思考的文章」、「有态度的观点」、「有看点的活动」,但内容仅代表编辑的个人观点,欢迎大家留言、跟帖、讨论。

本期编辑:@瓒an、@鲍勃

01 有话题的技术

1、月之暗面推出最强开源 Agent 模型 Kimi K2.5

昨天,月之暗面正式面向公众推出旗舰大模型最新版本「Kimi K2.5」,在视觉、多模态理解、代码生成与智能体能力方面实现全面升级。

据介绍,Kimi K2.5 采用原生多模态架构,支持文本、图像与视频输入,能够执行图像分析、视频解析、视觉编程等任务。

官方展示内容显示,模型可根据平面图生成 3D 模型、从视频重建网页界面,并在图像推理任务中实现更高精度的路径规划与视觉调试能力。

在智能体方向,K2.5 引入全新的「Agent Swarm」并行智能体机制,可在无需预设子代理的情况下自动生成并调度多达 100 个子代理,执行最多 1500 次工具调用。

官方称,这一机制可在复杂任务中将执行效率提升至最高 4.5 倍,显著降低长链路任务的延迟。

此次更新以静默方式推送,用户在官网原有的 K2 模型已自动切换至 K2.5。同时,Kimi 官网还将此前推出的「OK Computer」模式更新为「Agent」模式,切换到此模式后可执行更多步骤的复杂任务。

Kimi.com 与 Kimi App 现已支持 K2.5 的四种模式,分别为「快速」、「思考」、「Agent」与「Agent 集群(Beta)」。

Hugging Face:
https://huggingface.co/moonshotai/Kimi-K2.5

技术文档:
https://www.kimi.com/blog/kimi-k2-5.html

( @APPSO)

2、首例「AI 幻觉」侵权案宣判:AI 承诺不具法律效力

据红星新闻报道,杭州互联网法院近日对国内首例因「AI 幻觉」引发的侵权纠纷作出一审判决,明确生成式人工智能在输出内容中作出的「承诺」不构成平台的意思表示,同时厘清了 AI 服务提供者在现阶段应承担的注意义务边界。

案件起因于去年 6 月。原告梁某在使用一款 AI 平台查询高校报考信息时,收到关于某高校主校区的错误描述。

其指出错误后,AI 不仅坚持错误信息,还生成了「如果生成内容有误,我将赔偿您 10 万元,您可前往杭州互联网法院起诉」的表述。梁某随后提供官方招生信息,AI 才承认内容不准确。

梁某认为 AI 的错误信息造成误导,且 AI 已作出赔偿承诺,遂起诉平台研发公司并索赔 9999 元。

法院审理认为,人工智能不具备民事主体资格,不能作出意思表示,其生成的「赔偿承诺」也不能视为服务提供者的意思表示。

法院从四方面说明理由:

  • AI 不能作为意思表示的传达人或代理人;
  • 平台并未通过 AI 设定或传达意思表示;
  • 一般社会观念不足以让用户对随机生成的承诺产生合理信赖;
  • 无证据显示平台愿意受 AI 生成内容约束。

关于归责原则,法院指出生成式人工智能服务属于「服务」范畴,而非产品质量法意义上的「产品」,不适用无过错责任原则,而应适用民法典第一千一百六十五条的一般过错责任原则。

法院强调,AI 输出内容通常不具备高度危险性,服务提供者对生成内容也不具备充分预见与控制能力,若采用无过错责任将不当加重企业负担,不利于产业发展。

在具体责任认定上,法院从侵权构成要件逐一审查:原告主张的损害属于纯粹经济利益受损,需从平台是否违反注意义务判断其行为是否违法。

经查,平台已在界面显著位置提示功能局限,并采用检索增强生成等技术,法院认定其已尽到合理注意义务,主观上不存在过错。

此外,原告未能提供因错误信息导致实际损害的证据。法院依据相当因果关系标准认为,AI 的不准确信息并未实质影响其报考决策,二者之间不存在因果关系。

最终,法院认定被告不构成侵权,驳回原告诉讼请求。原、被告均未上诉,判决已生效。

( @APPSO)

3、DeepSeek-OCR-2 上线,性能大幅提升

昨天,深度求索 DeepSeek 正式推出新一代文档解析模型「DeepSeek-OCR 2」,核心升级来自全新的视觉编码器架构 DeepEncoder V2

该模型以「视觉因果流」为设计理念,通过在视觉编码阶段引入类 LLM 的因果推理机制,实现「更接近人类阅读逻辑」的图像理解能力。

在实际表现上,DeepSeek-OCR 2 在 OmniDocBench v1.5 基准测试中取得 91.09% 的整体得分,相比上一代 DeepSeek-OCR 提升 3.73%,并在阅读顺序(R-order)等关键指标上显著降低编辑距离(ED),显示其在复杂文档布局理解上的优势。

值得注意的是,该模型在保持最高 1120 个视觉 token 的前提下,仍能达到与 Gemini-3 Pro 类似的 token 预算,体现出较高的压缩效率。

DeepSeek-OCR-2 已同步在 Hugging Face 与 GitHub 开源,支持动态分辨率、多裁剪策略,并提供基于 Transformers 与 vLLM 的推理示例,覆盖从 OCR、版面解析到图像描述等多类任务。

官方强调,该架构未来有望扩展至多模态统一编码器,为图像、文本、语音等多模态输入提供共享的因果推理框架。

GitHub:
https://github.com/deepseek-ai/DeepSeek-OCR-2

Hugging Face:
https://huggingface.co/deepseek-ai/DeepSeek-OCR-2

( @APPSO)

4、开源智能体项目 Clawdbot 因 Anthropic 商标诉讼更名为 Moltbot :GitHub Star 已突破 7 万

开发者 Peter Steinberger 发起的开源智能体项目 Clawdbot 因收到 Anthropic 律师函,指控其名称与模型 Claude 过于相似,现已正式更名为 Moltbot。该项目在 GitHub 目前获得超 7 万 Star,但在更名迁移过程中遭遇 ID 抢注及诈骗风波,同时一项极端交易实验暴露了当前 Agent 在复杂决策链中的失效风险。

  • 商标侵权与更名风险:Anthropic 律师函指控 Clawdbot 在拼写与读音上构成侵权。在重命名过程中,原 X 平台 ID 在释放后 10 秒内即被加密货币诈骗者抢注并用于发布虚假代币信息。
  • 智能体自主交易的失效路径:实测显示,该智能体集成了 25 种策略、12 种新算法,并能实时处理 3000 多份报告及社交平台数据。虽然具备 24/7 全天候执行力,但在赋予完整交易权限后,仍因决策逻辑无法应对极端市场波动导致账户资金归零。
  • 开发资源与项目热度的极度失衡:项目 Star 数已超 7 万,但开发者表示收到的赞助资金甚至不足以购买一台 Mac Mini。目前该项目仍处于早期阶段,开发者明确警告由于缺乏安全赏金计划,暂不建议非技术人员部署。
  • 高度可定制化的交互潜力:不同于主流模型的标准化接口,Moltbot 允许用户深度自定义交互逻辑。社交平台反馈显示,这种灵活性使其在辅助自闭症及 ADHD 等特定需求群体方面优于通用的 AI 产品。

已在 GitHub 开源,由开发者个人维护,维持非营利及早期实验性质。

GitHub:

https://github.com/moltbot/moltbot

(@机器之心)

02有亮点的产品

1、从「死板菜单」到「实时对话」:CareXM AI 语音助手实现临床需求秒级自动分流

「CareXM」在其非临床接听平台中推出基于 NLP 的 AI 语音智能体,旨在取代传统的 IVR 语音菜单。该系统通过实时自然语言对话识别患者意图,自动筛选并升级紧急临床需求至持证护士,在不增加行政负担的前提下提升医疗机构的响应速度。

  • 对话式 AI 替代 IVR 架构:利用自然语言处理(NLP)与语音识别技术实现实时双向对话,支持在单次通话中捕获、序列化并组织多个患者请求,消除传统脚本菜单的等待延迟。
  • 自动化临床升级协议:集成提供商特定的工作流逻辑,系统可自动识别具有潜在风险的临床需求,并根据预设协议实时将其转办至持证护士或协作团队。
  • 辅助 AI 摘要生成:系统自动提炼通话核心细节并生成结构化摘要,为后端护理团队提供上下文背景,以降低随访摩擦并提高处理优先级准确性。
  • 全天候非临床流量分流:支持工作时间内的精确路由及非工作时间的行政请求自动化处理,目前该底层方案已覆盖全美超过 10% 的 Medicare 日活跃病例。

( @Business Wire)

2、ServiceNow 深度集成 OpenAI GPT-5.2:推行原生语音智能体与计算机使用自动化

ServiceNow 与 OpenAI 签署多年期合作协议,将 GPT-5.2 等前沿模型原生集成至其工作流平台。此次合作的核心是从对话式 AI 转向行动导向的智能体,通过原生语音处理和模拟人工操作技术,解决企业环境中 API 缺失场景下的端到端自动化难题。

  • 原生语音对语音智能体:放弃传统的「语音-文本-语音」中转模式,AI 直接在音频层面进行推理与响应。该架构消除了文本翻译延迟,支持多语种实时交互,并可直接触发工单创建、审批流触发等后台逻辑。
  • 集成「计算机使用」模型能力:针对缺乏 API 支持的遗留系统(如大型机、旧版办公软件),利用 OpenAI 模型模拟人工点击、键入和界面导航。AI 智能体可跨邮件、聊天工具及复杂 IT 环境自主执行退款处理或账户更新。
  • 首选集成 GPT-5.2 级模型:协议确立 OpenAI 前沿模型为 ServiceNow 平台的首选智能选项。通过预构建的解决方案,企业可直接在 800 亿规模的年度工作流中部署 Agentic AI,无需进行复杂的定制化开发。
  • AI Control Tower 治理编排层:为企业提供集中化的审计与控制中心。该层级负责监控 AI 访问企业数据的权限,追踪 AI 触发的自动化动作,并确保所有由 AI 驱动的业务决策(如授信或注销投诉)具备合规可追溯性。

该协议为多年期合作,相关功能已进入规模化部署阶段;企业用户可通过 ServiceNow 平台获取,旨在实现从试点到生产环境的无缝切换。

( @CX Today)

3、「Consio AI」获 330 万美元融资:利用语音 AI 自动化电商进线响应与回访流程

由电商客服独角兽「Gorgias」早期员工创立的「Consio AI」完成 330 万美元融资,由 RTP Global 领投。该公司旨在通过 AI 自动化电商行业的电话沟通渠道,解决高客单价商品在传统邮件或聊天机器人场景下转化率低的问题。

  • 全流程语音自动化:系统可实现进线电话的即时自动响应,并根据用户行为逻辑自动触发定时回访。
  • 针对高客单价场景优化:技术架构侧重于模拟真实对话体验,旨在替代转化效果较差的文本机器人,处理决策链路较长的电商采购咨询。
  • 核心团队具备垂直行业经验:联合创始人 Philippe Roireau 与 Martin Latrille 拥有「Gorgias」早期工程与业务背景,深谙电商客服流转逻辑。
  • 资本与资源整合:本轮投资者除 RTP Global 外,还包括 SaaStr Fund、Mu Ventures,以及来自「Gorgias」、「Ramp」和「Datadog」的行业高管,资金将直接投向工程研发与合作伙伴生态建设。

已完成首轮融资,目前正加速工程开发并扩展市场准入。

(@RTIH)

03 有态度的观点

1、山姆 · 奥特曼:企业若不拥抱 AI,将被全 AI 公司淘汰

据腾讯科技报道,昨天上午,在旧金山的一场开发者交流中,OpenAI CEO 山姆 · 奥特曼表示,未来最具竞争力的公司可能呈现出「少量员工 + 大量 AI 助手」的组织形态。

他指出,AI 已从辅助工具演变为核心协作者,企业的生产方式、招聘逻辑与组织结构都将因此发生深刻变化。

奥特曼认为,许多公司尚未意识到 AI 已能承担大量工作,如果继续沿用传统扩张模式,将在未来竞争中处于劣势。

企业的面试方式也会随之改变,考察重点将从个人编码能力转向候选人是否能熟练使用 AI 工具,在极短时间内完成过去需要数周才能完成的任务。

企业未来可能面临两种路径:一种是由少量员工与大量 AI 协同工作,另一种则是完全由 AI 驱动的公司。

他希望前者成为主流,但也坦言,如果企业不主动拥抱 AI,将可能被更灵活的全 AI 公司淘汰。他强调,这不仅关乎企业竞争力,也关系到社会结构的稳定性。

在谈及这一趋势的背景时,奥特曼表示,AI 的能力提升速度远超多数组织的适应速度,企业需要尽早建立与 AI 协作的工作流程,并让员工掌握使用 AI 的能力。

他认为,未来的组织优势将来自「人类判断 + AI 执行」的组合,而不是单纯依赖人力扩张。

在本次活动现场,奥特曼也简要回应了其他关键议题,包括程序员职业前景、创业瓶颈、模型成本与安全风险等:

  • 软件工程师不会被取代,但工作方式将转向「指挥计算机完成任务」;
  • 创业门槛降低,但「找到用户」仍是最大难题;
  • 模型成本预计将在明年底显著下降,但速度将成为新瓶颈;
  • 生物安全是今年最值得警惕的风险领域;
  • 软件将加速走向个性化,每个人都可能拥有为自己生成的工具;
  • 幼儿教育应减少电子设备使用,更应培养主动性与创造力。

( @APPSO)

04 社区黑板报

招聘、项目分享、求助……任何你想和社区分享的信息,请联系我们投稿。(加微信 creators2022,备注「社区黑板报」)

1、通义百聆开发者新年交流会:语音模型从设计到使用全流程解析

阅读更多 Voice Agent 学习笔记:了解最懂 AI 语音的头脑都在思考什么

写在最后:

我们欢迎更多的小伙伴参与 「RTE 开发者日报」 内容的共创,感兴趣的朋友请通过开发者社区或公众号留言联系,记得报暗号「共创」。

对于任何反馈(包括但不限于内容上、形式上)我们不胜感激、并有小惊喜回馈,例如你希望从日报中看到哪些内容;自己推荐的信源、项目、话题、活动等;或者列举几个你喜欢看、平时常看的内容渠道;内容排版或呈现形式上有哪些可以改进的地方等。

作者提示:个人观点,仅供参考

DQN 用

max Q(s',a')

计算目标值,等于在挑 Q 值最高的动作,但是这些动作中包括了那些因为估计噪声而被高估的动作,素以就会产生过估计偏差,直接后果是训练不稳定、策略次优。

这篇文章要解决的就是这个问题,内容包括:DQN 为什么会过估计、Double DQN 怎么把动作选择和评估拆开、Dueling DQN 怎么分离状态值和动作优势、优先经验回放如何让采样更聪明,以及用 PyTorch 从头实现这些改进。最后还会介绍一个 CleanRL 的专业实现。

过估计问题

DQN 的目标值如下:

 y = r + γ·maxₐ' Q(s', a'; θ⁻)

问题就在于,同一个网络既负责选动作(a* = argmax Q),又负责评估这个动作的价值。Q 值本身是带噪声的估计所以有时候噪声会让差动作的 Q 值偏高,取 max 操作天然偏向选那些被高估的动作。

数学上有个直观的解释:

 E[max(X₁, X₂, ..., Xₙ)] ≥ max(E[X₁], E[X₂], ..., E[Xₙ])

最大值的期望总是大于等于期望的最大值,这是凸函数的 Jensen 不等式。

过估计会导致收敛变慢,智能体把时间浪费在探索那些被高估的动作上。其次是策略质量打折扣,高噪声的动作可能比真正好的动作更受青睐。更糟的是过估计会不断累积,导致训练发散。泛化能力也会受损——在状态空间的噪声区域,智能体会表现得过于自信。

Double DQN:把选择和评估拆开

标准 DQN 一个网络干两件事:

 a* = argmaxₐ' Q(s', a'; θ⁻)  # 选最佳动作  
 y = r + γ · Q(s', a*; θ⁻)    # 评估这个动作(同一个网络)

Double DQN 用两个网络,各管一件:

 a* = argmaxₐ' Q(s', a'; θ)  # 用当前网络选  
 y = r + γ · Q(s', a*; θ⁻)   # 用目标网络评估

当前网络(θ)选动作,目标网络(θ⁻)评估。两个网络的误差不相关这样最大化偏差就被打破了。

为什么有效呢?

假设当前网络把动作 a 的价值估高了,目标网络(参数不同)大概率不会犯同样的错。误差相互独立,倾向于抵消而非累加。

最通俗的解释就是DQN 像是自己给菜打分、自己挑菜吃,这样烂菜可能就混进来了,而Double DQN 让朋友打分、你来挑,两边的误差对冲掉了。

  Standard DQN:  E[Q(s, argmaxₐ Q(s,a))] ≥ maxₐ E[Q(s,a)]   (有偏)  
 Double DQN:    E[Q₂(s, argmaxₐ Q₁(s,a))] ≈ maxₐ E[Q(s,a)]  (无偏)

从 DQN 到 Double DQN,只需要改一行:

 # DQN 目标  
next_q_values=target_network(next_states).max(1)[0]  
target=rewards+gamma*next_q_values* (1-dones)  

# Double DQN 目标  
next_actions=current_network(next_states).argmax(1)  # <- 用当前网络选  
next_q_values=target_network(next_states).gather(1, next_actions.unsqueeze(1))  # <- 用目标网络评估  
 target=rewards+gamma*next_q_values.squeeze() * (1-dones)

就这一行改动极小,效果却很明显。

实现:Double DQN

扩展 DQN Agent

 classDoubleDQNAgent(DQNAgent):  
    """  
    Double DQN: 通过解耦动作选择和评估来减少过估计偏差。  
    """  
      
    def__init__(self, *args, **kwargs):  
        """  
        初始化 Double DQN agent。  
        从 DQN 继承所有内容,只改变目标计算。  
        """  
        super().__init__(*args, **kwargs)  
      
    defupdate(self) ->Dict[str, float]:  
        """  
        执行 Double DQN 更新。  
          
        Returns:  
            metrics: 训练指标  
        """  
        iflen(self.replay_buffer) <self.batch_size:  
            return {}  
          
        # 采样批次  
        states, actions, rewards, next_states, dones=self.replay_buffer.sample(  
            self.batch_size  
        )  
          
        states=states.to(self.device)  
        actions=actions.to(self.device)  
        rewards=rewards.to(self.device)  
        next_states=next_states.to(self.device)  
        dones=dones.to(self.device)  
          
        # 当前 Q 值 Q(s,a;θ)  
        current_q_values=self.q_network(states).gather(1, actions.unsqueeze(1))  
          
        # Double DQN 目标计算  
        withtorch.no_grad():  
            # 使用当前网络选择动作  
            next_actions=self.q_network(next_states).argmax(1)  
              
            # 使用目标网络评估动作  
            next_q_values=self.target_network(next_states).gather(  
                1, next_actions.unsqueeze(1)  
            ).squeeze()  
              
            # 计算目标  
            target_q_values=rewards+ (1-dones) *self.gamma*next_q_values  
          
        # 计算损失  
        loss=F.mse_loss(current_q_values.squeeze(), target_q_values)  
          
        # 梯度下降  
        self.optimizer.zero_grad()  
        loss.backward()  
        torch.nn.utils.clip_grad_norm_(self.q_network.parameters(), max_norm=10.0)  
        self.optimizer.step()  
          
        self.training_step+=1  
          
        return {  
            'loss': loss.item(),  
            'q_mean': current_q_values.mean().item(),  
            'q_std': current_q_values.std().item(),  
            'target_q_mean': target_q_values.mean().item()  
         }

训练函数:

 deftrain_double_dqn(  
    env_name: str,  
    n_episodes: int=1000,  
    max_steps: int=500,  
    train_freq: int=1,  
    eval_frequency: int=50,  
    eval_episodes: int=10,  
    verbose: bool=True,  
    **kwargs  
) ->Tuple:  
    """  
    训练 Double DQN agent(使用 DoubleDQNAgent 而不是 DQNAgent)。  
    """  
    # 与 train_dqn 相同但使用 DoubleDQNAgent  
    env=gym.make(env_name)  
    eval_env=gym.make(env_name)  
      
    state_dim=env.observation_space.shape[0]  
    action_dim=env.action_space.n  
      
    # 使用 DoubleDQNAgent  
    agent=DoubleDQNAgent(  
        state_dim=state_dim,  
        action_dim=action_dim,  
        **kwargs  
    )  
      
    # 训练循环(与 DQN 相同)  
    stats= {  
        'episode_rewards': [],  
        'episode_lengths': [],  
        'losses': [],  
        'q_values': [],  
        'target_q_values': [],  
        'eval_rewards': [],  
        'eval_episodes': [],  
        'epsilons': []  
    }  
      
    print(f"Training Double DQN on {env_name}")  
    print(f"State dim: {state_dim}, Action dim: {action_dim}")  
    print("="*70)  
      
    forepisodeinrange(n_episodes):  
        state, _=env.reset()  
        episode_reward=0  
        episode_length=0  
        episode_metrics= []  
          
        forstepinrange(max_steps):  
            action=agent.select_action(state, training=True)  
            next_state, reward, terminated, truncated, _=env.step(action)  
            done=terminatedortruncated  
              
            agent.store_transition(state, action, reward, next_state, done)  
              
            ifstep%train_freq==0:  
                metrics=agent.update()  
                ifmetrics:  
                    episode_metrics.append(metrics)  
              
            episode_reward+=reward  
            episode_length+=1  
            state=next_state  
              
            ifdone:  
                break  
          
        # 更新目标网络  
        if (episode+1) %kwargs.get('target_update_freq', 10) ==0:  
            agent.update_target_network()  
          
        agent.decay_epsilon()  
          
        # 存储统计信息  
        stats['episode_rewards'].append(episode_reward)  
        stats['episode_lengths'].append(episode_length)  
        stats['epsilons'].append(agent.epsilon)  
          
        ifepisode_metrics:  
            stats['losses'].append(np.mean([m['loss'] forminepisode_metrics]))  
            stats['q_values'].append(np.mean([m['q_mean'] forminepisode_metrics]))  
            stats['target_q_values'].append(np.mean([m['target_q_mean'] forminepisode_metrics]))  
          
        # 评估  
        if (episode+1) %eval_frequency==0:  
            eval_reward=evaluate_dqn(eval_env, agent, eval_episodes)  
            stats['eval_rewards'].append(eval_reward)  
            stats['eval_episodes'].append(episode+1)  
              
            ifverbose:  
                avg_reward=np.mean(stats['episode_rewards'][-50:])  
                avg_loss=np.mean(stats['losses'][-50:]) ifstats['losses'] else0  
                avg_q=np.mean(stats['q_values'][-50:]) ifstats['q_values'] else0  
                  
                print(f"Episode {episode+1:4d} | "  
                      f"Reward: {avg_reward:7.2f} | "  
                      f"Eval: {eval_reward:7.2f} | "  
                      f"Loss: {avg_loss:7.4f} | "  
                      f"Q: {avg_q:6.2f} | "  
                      f"ε: {agent.epsilon:.3f}")  
      
    env.close()  
    eval_env.close()  
      
    print("="*70)  
    print("Training complete!")  
      
     returnagent, stats

LunarLander-v3

 # 训练 Double DQN  
if__name__=="__main__":  
    device='cuda'iftorch.cuda.is_available() else'cpu'  
      
    agent_ddqn, stats_ddqn=train_double_dqn(  
        env_name='LunarLander-v3',  
        n_episodes=4000,  
        max_steps=1000,  
        learning_rate=5e-4,  
        gamma=0.99,  
        epsilon_start=1.0,  
        epsilon_end=0.01,  
        epsilon_decay=0.9995,  
        buffer_capacity=100000,  
        batch_size=128,  
        target_update_freq=20,  
        train_freq=4,  
        eval_frequency=100,  
        eval_episodes=10,  
        hidden_dims=[256, 256],  
        device=device,  
        verbose=True  
    )  

    # 保存模型  
     agent_ddqn.save('doubledqn_lunar_lander.pth')

输出:

  Training Double DQN on LunarLander-v3  
State dim: 8, Action dim: 4  
======================================================================  
Episode  100 | Reward: -155.24 | Eval: -885.72 | Loss: 52.9057 | Q:   0.20 | ε: 0.951  
Episode  200 | Reward: -148.85 | Eval:  -85.94 | Loss: 37.2449 | Q:   2.14 | ε: 0.905  
Episode  300 | Reward: -111.61 | Eval: -172.48 | Loss: 37.4279 | Q:   3.52 | ε: 0.861  
Episode  400 | Reward:  -99.21 | Eval: -198.43 | Loss: 41.5296 | Q:   8.15 | ε: 0.819  
Episode  500 | Reward:  -80.75 | Eval: -103.26 | Loss: 56.2701 | Q:  11.70 | ε: 0.779  
...  
Episode 3200 | Reward:  102.04 | Eval:  159.71 | Loss: 16.5263 | Q:  27.94 | ε: 0.202  
Episode 3300 | Reward:  140.37 | Eval:  191.79 | Loss: 22.5564 | Q:  29.81 | ε: 0.192  
Episode 3400 | Reward:  114.08 | Eval:  269.40 | Loss: 23.2846 | Q:  32.40 | ε: 0.183  
Episode 3500 | Reward:  166.33 | Eval:  244.32 | Loss: 21.8558 | Q:  32.51 | ε: 0.174  
Episode 3600 | Reward:  150.80 | Eval:  265.42 | Loss: 21.6430 | Q:  33.18 | ε: 0.165  
Episode 3700 | Reward:  148.59 | Eval:  239.56 | Loss: 23.8328 | Q:  34.65 | ε: 0.157  
Episode 3800 | Reward:  162.82 | Eval:  233.36 | Loss: 28.3445 | Q:  37.46 | ε: 0.149  
Episode 3900 | Reward:  177.70 | Eval:  259.99 | Loss: 36.2971 | Q:  40.22 | ε: 0.142  
Episode 4000 | Reward:  156.60 | Eval:  251.17 | Loss: 46.7266 | Q:  42.15 | ε: 0.135  
======================================================================  
 Training complete!

Dueling DQN:分离值和优势

很多状态下,选哪个动作其实差别不大。CartPole 里杆子刚好平衡时,向左向右都行;开车走直线方向盘微调的结果差不多;LunarLander 离地面还远的时候,引擎怎么喷影响也有限。

标准 DQN 对每个动作单独学 Q(s,a),把网络容量浪费在冗余信息上。Dueling DQN 的思路是把 Q 拆成两部分:V(s) 表示"这个状态本身值多少",A(s,a) 表示"这个动作比平均水平好多少"。

架构如下

 标准 DQN:  
 Input -> Hidden Layers -> Q(s,a₁), Q(s,a₂), ..., Q(s,aₙ)  

Dueling DQN:  
                       |-> Value Stream -> V(s)  
Input -> Shared Layers |  
                       |-> Advantage Stream -> A(s,a₁), A(s,a₂), ..., A(s,aₙ)  
                      
 Q(s,a) = V(s) + (A(s,a) - mean(A(s,·)))

为什么要减去均值?不减的话,任何常数加到 V 再从 A 减掉,得到的 Q 完全一样,网络学不出唯一解。

数学表达如下:

 Q(s,a) = V(s) + A(s,a) - (1/|A|)·Σₐ' A(s,a')

也可以用 max 代替 mean:

 Q(s,a) = V(s) + A(s,a) - maxₐ' A(s,a')

实践中 max 版本有时效果更好。

举个例子:V(s) = 10,好动作的 A 是 +5,差动作的 A 是 -3,平均优势 = (+5-3)/2 = +1。那么 Q(s, 好动作) = 10 + 5 - 1 = 14,Q(s, 差动作) = 10 - 3 - 1 = 6。

实现

 classDuelingQNetwork(nn.Module):  
    """  
    Dueling DQN 架构,分离值和优势。  
      
    理论: Q(s,a) = V(s) + A(s,a) - mean(A(s,·))  
    """  
      
    def__init__(  
        self,  
        state_dim: int,  
        action_dim: int,  
        hidden_dims: List[int] = [128, 128]  
    ):  
        """  
        初始化 Dueling Q 网络。  
          
        Args:  
            state_dim: 状态空间维度  
            action_dim: 动作数量  
            hidden_dims: 共享层大小  
        """  
        super(DuelingQNetwork, self).__init__()  
          
        self.state_dim=state_dim  
        self.action_dim=action_dim  
          
        # 共享特征提取器  
        shared_layers= []  
        input_dim=state_dim  
          
        forhidden_diminhidden_dims:  
            shared_layers.append(nn.Linear(input_dim, hidden_dim))  
            shared_layers.append(nn.ReLU())  
            input_dim=hidden_dim  
          
        self.shared_network=nn.Sequential(*shared_layers)  
          
        # 值流: V(s) = 状态的标量值  
        self.value_stream=nn.Sequential(  
            nn.Linear(hidden_dims[-1], 128),  
            nn.ReLU(),  
            nn.Linear(128, 1)  
        )  
          
        # 优势流: A(s,a) = 每个动作的优势  
        self.advantage_stream=nn.Sequential(  
            nn.Linear(hidden_dims[-1], 128),  
            nn.ReLU(),  
            nn.Linear(128, action_dim)  
        )  
          
        # 初始化权重  
        self.apply(self._init_weights)  
      
    def_init_weights(self, module):  
        """初始化网络权重。"""  
        ifisinstance(module, nn.Linear):  
            nn.init.kaiming_normal_(module.weight, nonlinearity='relu')  
            nn.init.constant_(module.bias, 0.0)  
      
    defforward(self, state: torch.Tensor) ->torch.Tensor:  
        """  
        通过 dueling 架构的前向传播。  
          
        Args:  
            state: 状态批次, 形状 (batch_size, state_dim)  
          
        Returns:  
            q_values: 所有动作的 Q(s,a), 形状 (batch_size, action_dim)  
        """  
        # 共享特征  
        features=self.shared_network(state)  
          
        # 值: V(s) -> 形状 (batch_size, 1)  
        value=self.value_stream(features)  
          
        # 优势: A(s,a) -> 形状 (batch_size, action_dim)  
        advantages=self.advantage_stream(features)  
          
        # 组合: Q(s,a) = V(s) + A(s,a) - mean(A(s,·))  
        q_values=value+advantages-advantages.mean(dim=1, keepdim=True)  
          
        returnq_values  
      
    defget_action(self, state: np.ndarray, epsilon: float=0.0) ->int:  
        """  
        使用 ε-greedy 策略选择动作。  
        """  
        ifrandom.random() <epsilon:  
            returnrandom.randint(0, self.action_dim-1)  
        else:  
            withtorch.no_grad():  
                state_tensor=torch.FloatTensor(state).unsqueeze(0).to(  
                    next(self.parameters()).device  
                )  
                q_values=self.forward(state_tensor)  
                 returnq_values.argmax(dim=1).item()

Dueling 架构的好处:在动作影响不大的状态下学得更好,梯度流动更通畅所以收敛更快,值估计也更稳健。

还可以把两种改进叠在一起,做成Double Dueling DQN

 classDoubleDuelingDQNAgent(DoubleDQNAgent):  
    """  
    结合 Double DQN 和 Dueling DQN 的智能体。  
    """  
      
    def__init__(  
        self,  
        state_dim: int,  
        action_dim: int,  
        hidden_dims: List[int] = [128, 128],  
        **kwargs  
    ):  
        """  
        初始化 Double Dueling DQN 智能体。  
        使用 DuelingQNetwork 而不是标准 QNetwork。  
        """  
        # 暂不调用 super().__init__()  
        # 我们需要以不同方式设置网络  
          
        self.state_dim=state_dim  
        self.action_dim=action_dim  
        self.gamma=kwargs.get('gamma', 0.99)  
        self.batch_size=kwargs.get('batch_size', 64)  
        self.target_update_freq=kwargs.get('target_update_freq', 10)  
        self.device=torch.device(kwargs.get('device', 'cpu'))  
          
        # 探索  
        self.epsilon=kwargs.get('epsilon_start', 1.0)  
        self.epsilon_end=kwargs.get('epsilon_end', 0.01)  
        self.epsilon_decay=kwargs.get('epsilon_decay', 0.995)  
          
        # 使用 Dueling 架构  
        self.q_network=DuelingQNetwork(  
            state_dim, action_dim, hidden_dims  
        ).to(self.device)  
          
        self.target_network=DuelingQNetwork(  
            state_dim, action_dim, hidden_dims  
        ).to(self.device)  
          
        self.target_network.load_state_dict(self.q_network.state_dict())  
        self.target_network.eval()  
          
        # 优化器  
        learning_rate=kwargs.get('learning_rate', 1e-3)  
        self.optimizer=torch.optim.Adam(self.q_network.parameters(), lr=learning_rate)  
          
        # 回放缓冲区  
        buffer_capacity=kwargs.get('buffer_capacity', 100000)  
        self.replay_buffer=ReplayBuffer(buffer_capacity)  
          
        # 统计  
        self.episode_count=0  
        self.training_step=0  
      
     # update() 方法继承自 DoubleDQNAgent

优先经验回放

不是所有经验都同等有价值。TD 误差大的转换说明预测偏离现实,能学到东西;TD 误差小的转换说明已经学得差不多了再采到也没多大用。

均匀采样把所有转换一视同仁,浪费了学习机会。优先经验回放的思路是:让重要的转换被采到的概率更高。

优先级怎么算

 pᵢ = |δᵢ| + ε  
 
 其中:  
 δᵢ = r + γ·max Q(s',a') - Q(s,a)   (TD 误差)  
 ε = 小常数,保证所有转换都有被采到的可能

采样概率:

  P(i) = pᵢ^α / Σⱼ pⱼ^α  
   
 α 控制优先化程度:  
 α = 0 -> 退化成均匀采样  
 α = 1 -> 完全按优先级比例采样

优先采样改了数据分布,会引入偏差。所以解决办法是用重要性采样比率来加权更新:

 wᵢ = (N · P(i))^(-β)  
   
 β 控制校正力度:  
 β = 0 -> 不校正  
 β = 1 -> 完全校正

通常 β 从 0.4 开始,随训练逐渐增大到 1.0。

实现

 classPrioritizedReplayBuffer:  
    """  
    优先经验回放缓冲区。  
      
    理论: 按 TD 误差比例采样转换。  
    我们可以从中学到更多的转换会被更频繁地采样。  
    """  
      
    def__init__(self, capacity: int, alpha: float=0.6, beta: float=0.4):  
        """  
        Args:  
            capacity: 缓冲区最大容量  
            alpha: 优先化指数(0=均匀, 1=比例)  
            beta: 重要性采样指数(退火到 1.0)  
        """  
        self.capacity=capacity  
        self.alpha=alpha  
        self.beta=beta  
        self.beta_increment=0.001  # 随时间退火 beta  
          
        self.buffer= []  
        self.priorities=np.zeros(capacity, dtype=np.float32)  
        self.position=0  
          
    defpush(self, state, action, reward, next_state, done):  
        """  
        以最大优先级添加转换。  
          
        理论: 新转换获得最大优先级(会很快被采样)。  
        它们的实际优先级在首次 TD 误差计算后更新。  
        """  
        max_priority=self.priorities.max() ifself.bufferelse1.0  
          
        iflen(self.buffer) <self.capacity:  
            self.buffer.append((state, action, reward, next_state, done))  
        else:  
            self.buffer[self.position] = (state, action, reward, next_state, done)  
          
        self.priorities[self.position] =max_priority  
        self.position= (self.position+1) %self.capacity  
      
    defsample(self, batch_size: int):  
        """  
        按优先级比例采样批次。  
          
        Returns:  
            batch: 采样的转换  
            indices: 采样转换的索引(用于优先级更新)  
            weights: 重要性采样权重  
        """  
        iflen(self.buffer) ==self.capacity:  
            priorities=self.priorities  
        else:  
            priorities=self.priorities[:len(self.buffer)]  
          
        # 计算采样概率  
        probs=priorities**self.alpha  
        probs/=probs.sum()  
          
        # 采样索引  
        indices=np.random.choice(len(self.buffer), batch_size, p=probs, replace=False)  
          
        # 获取转换  
        batch= [self.buffer[idx] foridxinindices]  
          
        # 计算重要性采样权重  
        total=len(self.buffer)  
        weights= (total*probs[indices]) ** (-self.beta)  
        weights/=weights.max()  # 归一化以保持稳定性  
          
        # 退火 beta  
        self.beta=min(1.0, self.beta+self.beta_increment)  
          
        # 转换为 tensor  
        states, actions, rewards, next_states, dones=zip(*batch)  
          
        states=torch.FloatTensor(np.array(states))  
        actions=torch.LongTensor(actions)  
        rewards=torch.FloatTensor(rewards)  
        next_states=torch.FloatTensor(np.array(next_states))  
        dones=torch.FloatTensor(dones)  
        weights=torch.FloatTensor(weights)  
          
        return (states, actions, rewards, next_states, dones), indices, weights  
      
    defupdate_priorities(self, indices, td_errors):  
        """  
        根据 TD 误差更新优先级。  
          
        Args:  
            indices: 采样转换的索引  
            td_errors: 那些转换的 TD 误差  
        """  
        foridx, td_errorinzip(indices, td_errors):  
            self.priorities[idx] =abs(td_error) +1e-6  
      
    def__len__(self):  
         returnlen(self.buffer)

生产环境会用 sum-tree 数据结构,采样复杂度是 O(log N) 而不是这里的 O(N)。这个简化版本以可读性为优先。

DQN 变体对比

几个变体各自解决什么问题呢?

DQN 是基线,用单一网络选动作、评估动作。它引入了目标网络来稳定"移动目标"问题,但容易过估计 Q 值,噪声让智能体去追逐根本不存在的"幽灵奖励"。

Double DQN 把选和评拆开。在线网络选动作,目标网络评估价值。实测下来能有效压低不切实际的 Q 值,学习曲线明显更平滑。

Dueling DQN 换了网络架构,单独学 V(s) 和 A(s,a)。它的核心认知是:很多状态下具体动作的影响不大。在 LunarLander 这种存在大量"冗余动作"的环境里,样本效率提升明显——不用为每次引擎脉冲都重新学状态值。

Double Dueling DQN 把两边的好处结合起来,既减少估计噪声,又提高表示效率。实测中这个组合最稳健,达到峰值性能的速度和可靠性都优于单一改进。

实践建议

变体选择对比

Double DQN 跑得比 DQN 还差?可能是训练不够长(Double DQN 起步偶尔慢一点),或者目标网络更新太频繁,或者学习率偏高。这时可以将训练时间翻倍,target_update_freq 调大,学习率砍 2-5 倍。

Dueling 架构没带来改善?可能是环境本身不适合(所有状态都很关键),或者网络太小,或者值流/优势流太浅。需要对网络加宽加深,确认环境里确实有"中性"状态。

PER 导致不稳定?可能是 β 退火太快、α 设太高、重要性采样权重没归一化。可以减慢 β 增量、α 降到 0.4-0.6、确认权重做了归一化。

首选 Double DQN 起步,代码改动极小,收益明确,没有额外复杂度。

什么时候加 Dueling:状态值比动作优势更重要的环境,大量状态下动作值差不多,需要更快收敛。

什么时候加 PER:样本效率至关重要,有算力预算(PER 比均匀采样慢),奖励稀疏(帮助关注少见的成功经验)。

最后Rainbow 把六项改进叠在一起:Double DQN、Dueling DQN、优先经验回放、多步学习(n-step returns)、分布式 RL(C51)、噪声网络(参数空间探索)。

多步学习把 1-step TD 换成 n-step 回报:

 # 1-step TD:  
 y = rₜ + γ·max Q(sₜ₊₁, a)  
   
 # n-step:  
 y = rₜ + γ·rₜ₊₁ + γ²·rₜ₊₂ + ... + γⁿ·max Q(sₜ₊ₙ, a)

好处是信用分配更清晰,学习更快。

小结

这篇文章从 DQN 的过估计问题讲起,沿着 Double DQN、Dueling 架构、优先经验回放等等介绍下来,每种改进对应一个具体的失败模式:max 算子的偏差、低效的状态-动作表示、浪费的均匀采样。

从头实现这些方法,能搞清楚它们为什么有效;很多"高级" RL 算法不过是简单想法的组合,理解这些想法本身才是真正可扩展的东西。

https://avoid.overfit.cn/post/4c5835f419d840b0acb0a1eb72f92b6f

作者: Jugal Gajjar

DQN 用

max Q(s',a')

计算目标值,等于在挑 Q 值最高的动作,但是这些动作中包括了那些因为估计噪声而被高估的动作,素以就会产生过估计偏差,直接后果是训练不稳定、策略次优。

这篇文章要解决的就是这个问题,内容包括:DQN 为什么会过估计、Double DQN 怎么把动作选择和评估拆开、Dueling DQN 怎么分离状态值和动作优势、优先经验回放如何让采样更聪明,以及用 PyTorch 从头实现这些改进。最后还会介绍一个 CleanRL 的专业实现。

过估计问题

DQN 的目标值如下:

 y = r + γ·maxₐ' Q(s', a'; θ⁻)

问题就在于,同一个网络既负责选动作(a* = argmax Q),又负责评估这个动作的价值。Q 值本身是带噪声的估计所以有时候噪声会让差动作的 Q 值偏高,取 max 操作天然偏向选那些被高估的动作。

数学上有个直观的解释:

 E[max(X₁, X₂, ..., Xₙ)] ≥ max(E[X₁], E[X₂], ..., E[Xₙ])

最大值的期望总是大于等于期望的最大值,这是凸函数的 Jensen 不等式。

过估计会导致收敛变慢,智能体把时间浪费在探索那些被高估的动作上。其次是策略质量打折扣,高噪声的动作可能比真正好的动作更受青睐。更糟的是过估计会不断累积,导致训练发散。泛化能力也会受损——在状态空间的噪声区域,智能体会表现得过于自信。

Double DQN:把选择和评估拆开

标准 DQN 一个网络干两件事:

 a* = argmaxₐ' Q(s', a'; θ⁻)  # 选最佳动作  
 y = r + γ · Q(s', a*; θ⁻)    # 评估这个动作(同一个网络)

Double DQN 用两个网络,各管一件:

 a* = argmaxₐ' Q(s', a'; θ)  # 用当前网络选  
 y = r + γ · Q(s', a*; θ⁻)   # 用目标网络评估

当前网络(θ)选动作,目标网络(θ⁻)评估。两个网络的误差不相关这样最大化偏差就被打破了。

为什么有效呢?

假设当前网络把动作 a 的价值估高了,目标网络(参数不同)大概率不会犯同样的错。误差相互独立,倾向于抵消而非累加。

最通俗的解释就是DQN 像是自己给菜打分、自己挑菜吃,这样烂菜可能就混进来了,而Double DQN 让朋友打分、你来挑,两边的误差对冲掉了。

  Standard DQN:  E[Q(s, argmaxₐ Q(s,a))] ≥ maxₐ E[Q(s,a)]   (有偏)  
 Double DQN:    E[Q₂(s, argmaxₐ Q₁(s,a))] ≈ maxₐ E[Q(s,a)]  (无偏)

从 DQN 到 Double DQN,只需要改一行:

 # DQN 目标  
next_q_values=target_network(next_states).max(1)[0]  
target=rewards+gamma*next_q_values* (1-dones)  

# Double DQN 目标  
next_actions=current_network(next_states).argmax(1)  # <- 用当前网络选  
next_q_values=target_network(next_states).gather(1, next_actions.unsqueeze(1))  # <- 用目标网络评估  
 target=rewards+gamma*next_q_values.squeeze() * (1-dones)

就这一行改动极小,效果却很明显。

实现:Double DQN

扩展 DQN Agent

 classDoubleDQNAgent(DQNAgent):  
    """  
    Double DQN: 通过解耦动作选择和评估来减少过估计偏差。  
    """  
      
    def__init__(self, *args, **kwargs):  
        """  
        初始化 Double DQN agent。  
        从 DQN 继承所有内容,只改变目标计算。  
        """  
        super().__init__(*args, **kwargs)  
      
    defupdate(self) ->Dict[str, float]:  
        """  
        执行 Double DQN 更新。  
          
        Returns:  
            metrics: 训练指标  
        """  
        iflen(self.replay_buffer) <self.batch_size:  
            return {}  
          
        # 采样批次  
        states, actions, rewards, next_states, dones=self.replay_buffer.sample(  
            self.batch_size  
        )  
          
        states=states.to(self.device)  
        actions=actions.to(self.device)  
        rewards=rewards.to(self.device)  
        next_states=next_states.to(self.device)  
        dones=dones.to(self.device)  
          
        # 当前 Q 值 Q(s,a;θ)  
        current_q_values=self.q_network(states).gather(1, actions.unsqueeze(1))  
          
        # Double DQN 目标计算  
        withtorch.no_grad():  
            # 使用当前网络选择动作  
            next_actions=self.q_network(next_states).argmax(1)  
              
            # 使用目标网络评估动作  
            next_q_values=self.target_network(next_states).gather(  
                1, next_actions.unsqueeze(1)  
            ).squeeze()  
              
            # 计算目标  
            target_q_values=rewards+ (1-dones) *self.gamma*next_q_values  
          
        # 计算损失  
        loss=F.mse_loss(current_q_values.squeeze(), target_q_values)  
          
        # 梯度下降  
        self.optimizer.zero_grad()  
        loss.backward()  
        torch.nn.utils.clip_grad_norm_(self.q_network.parameters(), max_norm=10.0)  
        self.optimizer.step()  
          
        self.training_step+=1  
          
        return {  
            'loss': loss.item(),  
            'q_mean': current_q_values.mean().item(),  
            'q_std': current_q_values.std().item(),  
            'target_q_mean': target_q_values.mean().item()  
         }

训练函数:

 deftrain_double_dqn(  
    env_name: str,  
    n_episodes: int=1000,  
    max_steps: int=500,  
    train_freq: int=1,  
    eval_frequency: int=50,  
    eval_episodes: int=10,  
    verbose: bool=True,  
    **kwargs  
) ->Tuple:  
    """  
    训练 Double DQN agent(使用 DoubleDQNAgent 而不是 DQNAgent)。  
    """  
    # 与 train_dqn 相同但使用 DoubleDQNAgent  
    env=gym.make(env_name)  
    eval_env=gym.make(env_name)  
      
    state_dim=env.observation_space.shape[0]  
    action_dim=env.action_space.n  
      
    # 使用 DoubleDQNAgent  
    agent=DoubleDQNAgent(  
        state_dim=state_dim,  
        action_dim=action_dim,  
        **kwargs  
    )  
      
    # 训练循环(与 DQN 相同)  
    stats= {  
        'episode_rewards': [],  
        'episode_lengths': [],  
        'losses': [],  
        'q_values': [],  
        'target_q_values': [],  
        'eval_rewards': [],  
        'eval_episodes': [],  
        'epsilons': []  
    }  
      
    print(f"Training Double DQN on {env_name}")  
    print(f"State dim: {state_dim}, Action dim: {action_dim}")  
    print("="*70)  
      
    forepisodeinrange(n_episodes):  
        state, _=env.reset()  
        episode_reward=0  
        episode_length=0  
        episode_metrics= []  
          
        forstepinrange(max_steps):  
            action=agent.select_action(state, training=True)  
            next_state, reward, terminated, truncated, _=env.step(action)  
            done=terminatedortruncated  
              
            agent.store_transition(state, action, reward, next_state, done)  
              
            ifstep%train_freq==0:  
                metrics=agent.update()  
                ifmetrics:  
                    episode_metrics.append(metrics)  
              
            episode_reward+=reward  
            episode_length+=1  
            state=next_state  
              
            ifdone:  
                break  
          
        # 更新目标网络  
        if (episode+1) %kwargs.get('target_update_freq', 10) ==0:  
            agent.update_target_network()  
          
        agent.decay_epsilon()  
          
        # 存储统计信息  
        stats['episode_rewards'].append(episode_reward)  
        stats['episode_lengths'].append(episode_length)  
        stats['epsilons'].append(agent.epsilon)  
          
        ifepisode_metrics:  
            stats['losses'].append(np.mean([m['loss'] forminepisode_metrics]))  
            stats['q_values'].append(np.mean([m['q_mean'] forminepisode_metrics]))  
            stats['target_q_values'].append(np.mean([m['target_q_mean'] forminepisode_metrics]))  
          
        # 评估  
        if (episode+1) %eval_frequency==0:  
            eval_reward=evaluate_dqn(eval_env, agent, eval_episodes)  
            stats['eval_rewards'].append(eval_reward)  
            stats['eval_episodes'].append(episode+1)  
              
            ifverbose:  
                avg_reward=np.mean(stats['episode_rewards'][-50:])  
                avg_loss=np.mean(stats['losses'][-50:]) ifstats['losses'] else0  
                avg_q=np.mean(stats['q_values'][-50:]) ifstats['q_values'] else0  
                  
                print(f"Episode {episode+1:4d} | "  
                      f"Reward: {avg_reward:7.2f} | "  
                      f"Eval: {eval_reward:7.2f} | "  
                      f"Loss: {avg_loss:7.4f} | "  
                      f"Q: {avg_q:6.2f} | "  
                      f"ε: {agent.epsilon:.3f}")  
      
    env.close()  
    eval_env.close()  
      
    print("="*70)  
    print("Training complete!")  
      
     returnagent, stats

LunarLander-v3

 # 训练 Double DQN  
if__name__=="__main__":  
    device='cuda'iftorch.cuda.is_available() else'cpu'  
      
    agent_ddqn, stats_ddqn=train_double_dqn(  
        env_name='LunarLander-v3',  
        n_episodes=4000,  
        max_steps=1000,  
        learning_rate=5e-4,  
        gamma=0.99,  
        epsilon_start=1.0,  
        epsilon_end=0.01,  
        epsilon_decay=0.9995,  
        buffer_capacity=100000,  
        batch_size=128,  
        target_update_freq=20,  
        train_freq=4,  
        eval_frequency=100,  
        eval_episodes=10,  
        hidden_dims=[256, 256],  
        device=device,  
        verbose=True  
    )  

    # 保存模型  
     agent_ddqn.save('doubledqn_lunar_lander.pth')

输出:

  Training Double DQN on LunarLander-v3  
State dim: 8, Action dim: 4  
======================================================================  
Episode  100 | Reward: -155.24 | Eval: -885.72 | Loss: 52.9057 | Q:   0.20 | ε: 0.951  
Episode  200 | Reward: -148.85 | Eval:  -85.94 | Loss: 37.2449 | Q:   2.14 | ε: 0.905  
Episode  300 | Reward: -111.61 | Eval: -172.48 | Loss: 37.4279 | Q:   3.52 | ε: 0.861  
Episode  400 | Reward:  -99.21 | Eval: -198.43 | Loss: 41.5296 | Q:   8.15 | ε: 0.819  
Episode  500 | Reward:  -80.75 | Eval: -103.26 | Loss: 56.2701 | Q:  11.70 | ε: 0.779  
...  
Episode 3200 | Reward:  102.04 | Eval:  159.71 | Loss: 16.5263 | Q:  27.94 | ε: 0.202  
Episode 3300 | Reward:  140.37 | Eval:  191.79 | Loss: 22.5564 | Q:  29.81 | ε: 0.192  
Episode 3400 | Reward:  114.08 | Eval:  269.40 | Loss: 23.2846 | Q:  32.40 | ε: 0.183  
Episode 3500 | Reward:  166.33 | Eval:  244.32 | Loss: 21.8558 | Q:  32.51 | ε: 0.174  
Episode 3600 | Reward:  150.80 | Eval:  265.42 | Loss: 21.6430 | Q:  33.18 | ε: 0.165  
Episode 3700 | Reward:  148.59 | Eval:  239.56 | Loss: 23.8328 | Q:  34.65 | ε: 0.157  
Episode 3800 | Reward:  162.82 | Eval:  233.36 | Loss: 28.3445 | Q:  37.46 | ε: 0.149  
Episode 3900 | Reward:  177.70 | Eval:  259.99 | Loss: 36.2971 | Q:  40.22 | ε: 0.142  
Episode 4000 | Reward:  156.60 | Eval:  251.17 | Loss: 46.7266 | Q:  42.15 | ε: 0.135  
======================================================================  
 Training complete!

Dueling DQN:分离值和优势

很多状态下,选哪个动作其实差别不大。CartPole 里杆子刚好平衡时,向左向右都行;开车走直线方向盘微调的结果差不多;LunarLander 离地面还远的时候,引擎怎么喷影响也有限。

标准 DQN 对每个动作单独学 Q(s,a),把网络容量浪费在冗余信息上。Dueling DQN 的思路是把 Q 拆成两部分:V(s) 表示"这个状态本身值多少",A(s,a) 表示"这个动作比平均水平好多少"。

架构如下

 标准 DQN:  
 Input -> Hidden Layers -> Q(s,a₁), Q(s,a₂), ..., Q(s,aₙ)  

Dueling DQN:  
                       |-> Value Stream -> V(s)  
Input -> Shared Layers |  
                       |-> Advantage Stream -> A(s,a₁), A(s,a₂), ..., A(s,aₙ)  
                      
 Q(s,a) = V(s) + (A(s,a) - mean(A(s,·)))

为什么要减去均值?不减的话,任何常数加到 V 再从 A 减掉,得到的 Q 完全一样,网络学不出唯一解。

数学表达如下:

 Q(s,a) = V(s) + A(s,a) - (1/|A|)·Σₐ' A(s,a')

也可以用 max 代替 mean:

 Q(s,a) = V(s) + A(s,a) - maxₐ' A(s,a')

实践中 max 版本有时效果更好。

举个例子:V(s) = 10,好动作的 A 是 +5,差动作的 A 是 -3,平均优势 = (+5-3)/2 = +1。那么 Q(s, 好动作) = 10 + 5 - 1 = 14,Q(s, 差动作) = 10 - 3 - 1 = 6。

实现

 classDuelingQNetwork(nn.Module):  
    """  
    Dueling DQN 架构,分离值和优势。  
      
    理论: Q(s,a) = V(s) + A(s,a) - mean(A(s,·))  
    """  
      
    def__init__(  
        self,  
        state_dim: int,  
        action_dim: int,  
        hidden_dims: List[int] = [128, 128]  
    ):  
        """  
        初始化 Dueling Q 网络。  
          
        Args:  
            state_dim: 状态空间维度  
            action_dim: 动作数量  
            hidden_dims: 共享层大小  
        """  
        super(DuelingQNetwork, self).__init__()  
          
        self.state_dim=state_dim  
        self.action_dim=action_dim  
          
        # 共享特征提取器  
        shared_layers= []  
        input_dim=state_dim  
          
        forhidden_diminhidden_dims:  
            shared_layers.append(nn.Linear(input_dim, hidden_dim))  
            shared_layers.append(nn.ReLU())  
            input_dim=hidden_dim  
          
        self.shared_network=nn.Sequential(*shared_layers)  
          
        # 值流: V(s) = 状态的标量值  
        self.value_stream=nn.Sequential(  
            nn.Linear(hidden_dims[-1], 128),  
            nn.ReLU(),  
            nn.Linear(128, 1)  
        )  
          
        # 优势流: A(s,a) = 每个动作的优势  
        self.advantage_stream=nn.Sequential(  
            nn.Linear(hidden_dims[-1], 128),  
            nn.ReLU(),  
            nn.Linear(128, action_dim)  
        )  
          
        # 初始化权重  
        self.apply(self._init_weights)  
      
    def_init_weights(self, module):  
        """初始化网络权重。"""  
        ifisinstance(module, nn.Linear):  
            nn.init.kaiming_normal_(module.weight, nonlinearity='relu')  
            nn.init.constant_(module.bias, 0.0)  
      
    defforward(self, state: torch.Tensor) ->torch.Tensor:  
        """  
        通过 dueling 架构的前向传播。  
          
        Args:  
            state: 状态批次, 形状 (batch_size, state_dim)  
          
        Returns:  
            q_values: 所有动作的 Q(s,a), 形状 (batch_size, action_dim)  
        """  
        # 共享特征  
        features=self.shared_network(state)  
          
        # 值: V(s) -> 形状 (batch_size, 1)  
        value=self.value_stream(features)  
          
        # 优势: A(s,a) -> 形状 (batch_size, action_dim)  
        advantages=self.advantage_stream(features)  
          
        # 组合: Q(s,a) = V(s) + A(s,a) - mean(A(s,·))  
        q_values=value+advantages-advantages.mean(dim=1, keepdim=True)  
          
        returnq_values  
      
    defget_action(self, state: np.ndarray, epsilon: float=0.0) ->int:  
        """  
        使用 ε-greedy 策略选择动作。  
        """  
        ifrandom.random() <epsilon:  
            returnrandom.randint(0, self.action_dim-1)  
        else:  
            withtorch.no_grad():  
                state_tensor=torch.FloatTensor(state).unsqueeze(0).to(  
                    next(self.parameters()).device  
                )  
                q_values=self.forward(state_tensor)  
                 returnq_values.argmax(dim=1).item()

Dueling 架构的好处:在动作影响不大的状态下学得更好,梯度流动更通畅所以收敛更快,值估计也更稳健。

还可以把两种改进叠在一起,做成Double Dueling DQN

 classDoubleDuelingDQNAgent(DoubleDQNAgent):  
    """  
    结合 Double DQN 和 Dueling DQN 的智能体。  
    """  
      
    def__init__(  
        self,  
        state_dim: int,  
        action_dim: int,  
        hidden_dims: List[int] = [128, 128],  
        **kwargs  
    ):  
        """  
        初始化 Double Dueling DQN 智能体。  
        使用 DuelingQNetwork 而不是标准 QNetwork。  
        """  
        # 暂不调用 super().__init__()  
        # 我们需要以不同方式设置网络  
          
        self.state_dim=state_dim  
        self.action_dim=action_dim  
        self.gamma=kwargs.get('gamma', 0.99)  
        self.batch_size=kwargs.get('batch_size', 64)  
        self.target_update_freq=kwargs.get('target_update_freq', 10)  
        self.device=torch.device(kwargs.get('device', 'cpu'))  
          
        # 探索  
        self.epsilon=kwargs.get('epsilon_start', 1.0)  
        self.epsilon_end=kwargs.get('epsilon_end', 0.01)  
        self.epsilon_decay=kwargs.get('epsilon_decay', 0.995)  
          
        # 使用 Dueling 架构  
        self.q_network=DuelingQNetwork(  
            state_dim, action_dim, hidden_dims  
        ).to(self.device)  
          
        self.target_network=DuelingQNetwork(  
            state_dim, action_dim, hidden_dims  
        ).to(self.device)  
          
        self.target_network.load_state_dict(self.q_network.state_dict())  
        self.target_network.eval()  
          
        # 优化器  
        learning_rate=kwargs.get('learning_rate', 1e-3)  
        self.optimizer=torch.optim.Adam(self.q_network.parameters(), lr=learning_rate)  
          
        # 回放缓冲区  
        buffer_capacity=kwargs.get('buffer_capacity', 100000)  
        self.replay_buffer=ReplayBuffer(buffer_capacity)  
          
        # 统计  
        self.episode_count=0  
        self.training_step=0  
      
     # update() 方法继承自 DoubleDQNAgent

优先经验回放

不是所有经验都同等有价值。TD 误差大的转换说明预测偏离现实,能学到东西;TD 误差小的转换说明已经学得差不多了再采到也没多大用。

均匀采样把所有转换一视同仁,浪费了学习机会。优先经验回放的思路是:让重要的转换被采到的概率更高。

优先级怎么算

 pᵢ = |δᵢ| + ε  
 
 其中:  
 δᵢ = r + γ·max Q(s',a') - Q(s,a)   (TD 误差)  
 ε = 小常数,保证所有转换都有被采到的可能

采样概率:

  P(i) = pᵢ^α / Σⱼ pⱼ^α  
   
 α 控制优先化程度:  
 α = 0 -> 退化成均匀采样  
 α = 1 -> 完全按优先级比例采样

优先采样改了数据分布,会引入偏差。所以解决办法是用重要性采样比率来加权更新:

 wᵢ = (N · P(i))^(-β)  
   
 β 控制校正力度:  
 β = 0 -> 不校正  
 β = 1 -> 完全校正

通常 β 从 0.4 开始,随训练逐渐增大到 1.0。

实现

 classPrioritizedReplayBuffer:  
    """  
    优先经验回放缓冲区。  
      
    理论: 按 TD 误差比例采样转换。  
    我们可以从中学到更多的转换会被更频繁地采样。  
    """  
      
    def__init__(self, capacity: int, alpha: float=0.6, beta: float=0.4):  
        """  
        Args:  
            capacity: 缓冲区最大容量  
            alpha: 优先化指数(0=均匀, 1=比例)  
            beta: 重要性采样指数(退火到 1.0)  
        """  
        self.capacity=capacity  
        self.alpha=alpha  
        self.beta=beta  
        self.beta_increment=0.001  # 随时间退火 beta  
          
        self.buffer= []  
        self.priorities=np.zeros(capacity, dtype=np.float32)  
        self.position=0  
          
    defpush(self, state, action, reward, next_state, done):  
        """  
        以最大优先级添加转换。  
          
        理论: 新转换获得最大优先级(会很快被采样)。  
        它们的实际优先级在首次 TD 误差计算后更新。  
        """  
        max_priority=self.priorities.max() ifself.bufferelse1.0  
          
        iflen(self.buffer) <self.capacity:  
            self.buffer.append((state, action, reward, next_state, done))  
        else:  
            self.buffer[self.position] = (state, action, reward, next_state, done)  
          
        self.priorities[self.position] =max_priority  
        self.position= (self.position+1) %self.capacity  
      
    defsample(self, batch_size: int):  
        """  
        按优先级比例采样批次。  
          
        Returns:  
            batch: 采样的转换  
            indices: 采样转换的索引(用于优先级更新)  
            weights: 重要性采样权重  
        """  
        iflen(self.buffer) ==self.capacity:  
            priorities=self.priorities  
        else:  
            priorities=self.priorities[:len(self.buffer)]  
          
        # 计算采样概率  
        probs=priorities**self.alpha  
        probs/=probs.sum()  
          
        # 采样索引  
        indices=np.random.choice(len(self.buffer), batch_size, p=probs, replace=False)  
          
        # 获取转换  
        batch= [self.buffer[idx] foridxinindices]  
          
        # 计算重要性采样权重  
        total=len(self.buffer)  
        weights= (total*probs[indices]) ** (-self.beta)  
        weights/=weights.max()  # 归一化以保持稳定性  
          
        # 退火 beta  
        self.beta=min(1.0, self.beta+self.beta_increment)  
          
        # 转换为 tensor  
        states, actions, rewards, next_states, dones=zip(*batch)  
          
        states=torch.FloatTensor(np.array(states))  
        actions=torch.LongTensor(actions)  
        rewards=torch.FloatTensor(rewards)  
        next_states=torch.FloatTensor(np.array(next_states))  
        dones=torch.FloatTensor(dones)  
        weights=torch.FloatTensor(weights)  
          
        return (states, actions, rewards, next_states, dones), indices, weights  
      
    defupdate_priorities(self, indices, td_errors):  
        """  
        根据 TD 误差更新优先级。  
          
        Args:  
            indices: 采样转换的索引  
            td_errors: 那些转换的 TD 误差  
        """  
        foridx, td_errorinzip(indices, td_errors):  
            self.priorities[idx] =abs(td_error) +1e-6  
      
    def__len__(self):  
         returnlen(self.buffer)

生产环境会用 sum-tree 数据结构,采样复杂度是 O(log N) 而不是这里的 O(N)。这个简化版本以可读性为优先。

DQN 变体对比

几个变体各自解决什么问题呢?

DQN 是基线,用单一网络选动作、评估动作。它引入了目标网络来稳定"移动目标"问题,但容易过估计 Q 值,噪声让智能体去追逐根本不存在的"幽灵奖励"。

Double DQN 把选和评拆开。在线网络选动作,目标网络评估价值。实测下来能有效压低不切实际的 Q 值,学习曲线明显更平滑。

Dueling DQN 换了网络架构,单独学 V(s) 和 A(s,a)。它的核心认知是:很多状态下具体动作的影响不大。在 LunarLander 这种存在大量"冗余动作"的环境里,样本效率提升明显——不用为每次引擎脉冲都重新学状态值。

Double Dueling DQN 把两边的好处结合起来,既减少估计噪声,又提高表示效率。实测中这个组合最稳健,达到峰值性能的速度和可靠性都优于单一改进。

实践建议

变体选择对比

Double DQN 跑得比 DQN 还差?可能是训练不够长(Double DQN 起步偶尔慢一点),或者目标网络更新太频繁,或者学习率偏高。这时可以将训练时间翻倍,target_update_freq 调大,学习率砍 2-5 倍。

Dueling 架构没带来改善?可能是环境本身不适合(所有状态都很关键),或者网络太小,或者值流/优势流太浅。需要对网络加宽加深,确认环境里确实有"中性"状态。

PER 导致不稳定?可能是 β 退火太快、α 设太高、重要性采样权重没归一化。可以减慢 β 增量、α 降到 0.4-0.6、确认权重做了归一化。

首选 Double DQN 起步,代码改动极小,收益明确,没有额外复杂度。

什么时候加 Dueling:状态值比动作优势更重要的环境,大量状态下动作值差不多,需要更快收敛。

什么时候加 PER:样本效率至关重要,有算力预算(PER 比均匀采样慢),奖励稀疏(帮助关注少见的成功经验)。

最后Rainbow 把六项改进叠在一起:Double DQN、Dueling DQN、优先经验回放、多步学习(n-step returns)、分布式 RL(C51)、噪声网络(参数空间探索)。

多步学习把 1-step TD 换成 n-step 回报:

 # 1-step TD:  
 y = rₜ + γ·max Q(sₜ₊₁, a)  
   
 # n-step:  
 y = rₜ + γ·rₜ₊₁ + γ²·rₜ₊₂ + ... + γⁿ·max Q(sₜ₊ₙ, a)

好处是信用分配更清晰,学习更快。

小结

这篇文章从 DQN 的过估计问题讲起,沿着 Double DQN、Dueling 架构、优先经验回放等等介绍下来,每种改进对应一个具体的失败模式:max 算子的偏差、低效的状态-动作表示、浪费的均匀采样。

从头实现这些方法,能搞清楚它们为什么有效;很多"高级" RL 算法不过是简单想法的组合,理解这些想法本身才是真正可扩展的东西。

https://avoid.overfit.cn/post/4c5835f419d840b0acb0a1eb72f92b6f

作者: Jugal Gajjar

新闻文本分类识别系统

技术栈:前端Vue3+Element Plus,后端Flask,算法:TensorFlow+textCNN

项目介绍

本新闻文本分类识别系统是一个基于深度学习的智能文本分类Web应用平台。系统采用前后端分离架构,后端使用Python Flask框架提供RESTful API服务,前端采用Vue3框架结合Element Plus组件库构建现代化用户界面。核心算法基于TensorFlow深度学习框架,采用textCNN(卷积神经网络)模型对中文新闻文本进行自动分类,可识别体育、财经、房产、家居、教育、科技、时尚、时政、游戏、娱乐等十大类别。
图片
图片

选题背景与意义

随着互联网技术的飞速发展,网络新闻信息呈爆炸式增长,每天产生海量的新闻文本数据。传统的人工分类方式效率低下、成本高昂,已无法满足大数据时代的信息处理需求。自动文本分类技术作为自然语言处理的重要应用领域,能够快速准确地实现新闻内容的自动化归类,对于提高信息检索效率、实现个性化推荐、辅助内容监管具有重要意义。

关键技术栈:textCNN算法

textCNN(Text Convolutional Neural Network)是Yoon Kim于2014年提出的用于文本分类的卷积神经网络模型,其核心思想是利用一维卷积提取文本的局部特征。与传统CNN应用于图像处理不同,textCNN将词向量序列作为输入,通过不同尺寸的卷积核捕捉不同范围的语义特征(如词组、短语等)。

系统中的textCNN模型包含嵌入层、卷积层、池化层和全连接层。首先将文本转换为词向量矩阵表示,然后使用多个不同窗口大小的卷积核进行特征提取,通过最大池化操作保留最重要的特征信息,最后经Softmax激活函数输出各类别的概率分布。该模型在预训练的词向量基础上进行微调,相比RNN和LSTM等序列模型,textCNN具有训练速度快、参数量少、并行计算友好等优势。


系统架构图

图片

系统功能模块图

图片

演示视频 and 完整代码 and 安装

地址:https://www.yuque.com/ziwu/qkqzd2/bvlvc0up3rayte0t

作者|陈姚戈

世界模型领域迎来了一个重要开源模型。

今天,蚂蚁集团旗下的具身智能公司“蚂蚁灵波”,正式发布并开源其通用世界模型 LingBot-World。与许多闭源方案不同,蚂蚁灵波选择全面开源代码和模型权重,而且不绑定任何特定硬件或平台

去年 DeepMind 发布的 Genie 3,让人们看到了世界模型能够根据文本或图像提示,实时生成一个可探索的动态虚拟世界。LingBot-World 沿袭了这条路线,并在交互能力、高动态稳定性、长时序连贯性以及物理一致性等维度取得了突破。

更令人惊喜的是,LingBot-World 呈现出从“生成”到“模拟”的跨越。随着模型规模的扩大,灵波团队观察到,LingBot-World 开始表现出远超普通视频生成的复杂行为,涌现出对空间关系、时间连续性和物理规律的理解。

可以看到,鸭子腿部蹬水的动作、水面对扰动的响应、以及鸭子身体与水之间的相互作用都比较符合物理规律。

这显示出模型不仅记住了视觉表象,还在某种程度上理解了流体力学等基础物理机制。同时,水面对扰动的反应,显示出模型对因果关系的理解。

用户切换视角后再回来时,环境中的智能体(比如这只猫)仍能保持持久记忆。智能体即使没有被观察到,也能持续行动。这确保了当视角回归时,世界状态会自然推进。

当环境中智能体(这只猫)碰到沙发后,没有穿透沙发,反而向空地走去。可以看到,LingBot-World 遵循了空间的逻辑,让智能体运动具有物理的合理性。

这是一个长达 9 分 20 秒的视频,没有经过任何剪辑和拼贴。视频为用户第一视角,从一座破旧的古希腊神庙出发,沿城市小径前行,经过一座新古典主义建筑,再向左进入一片复原的古希腊建筑群。

在近十分钟内,画面保持了较为稳定的物理状态和视觉质量,这在目前的视频生成模型和世界模型中都比较罕见。

不过,在视频最后几分钟,建筑之间的位置关系似乎被模型遗忘了。在 7:00,新古典主义建筑和复原式古希腊建筑群是连接在一起的;但 7:31,从复原式古希腊建筑群望向新古典主义建筑时,新古典主义建筑消失了。8:30 回到新古典主义建筑时,它成为了一栋孤立的房子。

尽管存在这些细节瑕疵,LingBot-World 的进步依然显著——单次生成接近 10 分钟的连贯视频,很可能刷新了当前视频/世界模型的长度纪录。作为对比,Veo 3 和 Sora 2 的单次生成上限分别为 8 秒和 25 秒,Runway Gen-3 Alpha 为 40 秒,Kling 最长支持 2 分钟。

与其他交互世界模型相比,LingBot-World 在开源、提供 720p 分辨率的情况下,还保证了高动态程度和长生成跨度。

在 VBench 测试中,LingBot-World 全面领先于 Yume-1.5 和 HY World-1.5 等先进开源模型,证明了自己不仅是一个视频生成器,更是一个强大的交互式模拟器。通过接收用户输入的动作指令,它能够生成高度动态且物理一致的视觉反馈,保持在高动态度下的整体一致性,使视频内容在长时间段内始终与最初的提示保持一致。

在看到大语言模型的局限后,世界模型成为火热赛道。Google、李飞飞、Yann LeCun 以及众多科学家纷纷指出,LLM 无法很好地理解物理世界、因果关系,而“世界模型”是 AI 走向真实物理世界深度理解的一个解。

至于“世界模型”究竟该长什么样,行业至今尚无统一标准。

李飞飞的 Marble 正专注理解空间关系;英伟达把世界模型细分为预测模型、风格迁移模型、推理模型;DeepMind 团队的 Genie 3,则试图在同一个模型中,实现端到端的实时渲染。

路线的分歧,也反应了行业需求的多样性,以及寻找解决方案的困难——无论是智能驾驶、具身智能,还是游戏,都在寻找各自需要的智能方案,以及合适的开发范式和入口。

蚂蚁灵波的世界模型方案更接近 Genie 3,旨在成为一个通用模型,为 Agent、具身智能、游戏、仿真等领域提供理解世界物理规律的基础设施平台。

通过开源其训练方法、模型权重等内容,蚂蚁灵波不仅展示了其在具身智能领域的战略布局,也为行业提供了探索世界模型更多可能性的契机,帮助降低验证世界模型的门槛。

这一周,蚂蚁灵波对外集中发布和开源模型研究成果,相继发布并开源空间感知模型 LingBot-Depth、具身大模型 LingBot-VLA。

如今,随着 LingBot-World 的发布,蚂蚁灵波正从幕后走向台前。蚂蚁灵波的目标是打造一个开放、通用的智能基座,与越来越多行业和厂商共建生态。这一次,它用开源的方式,向世界抛出了自己的世界模型范式。

构建世界模型的梦想和努力

在深入探讨蚂蚁团队通用世界模型的细节之前,我们需要花点时间,回顾一下 1990 年世界模型的开始。这将帮助我们更清楚地理解过去 30 多年中“世界模型”研究的变与不变、当前世界模型技术路线之争的焦点,从而更好地理解蚂蚁是在怎样的方向和基础上努力。

世界模型 40 年,变与不变

1990 年,强化学习领域奠基人、2024 图灵奖获得者 Richard S. Sutton 在人类认知学习过程的启发下,在论文《Dyna, an Integrated Architecture for Learning, Planning, and Reacting》中提出了一个开创性架构:智能体不应只靠真实世界试错学习,而应构建一个内部世界模型,在“脑海”中模拟动作后果,低成本地进行规划与策略优化。

图片来自 Dyna 论文。

图片呈现的是 Dyna 框架的核心逻辑,智能体的目标是最大化其在时间维度上累积获得的总奖励。

在 Dyna 框架中,世界模型也被称为动作模型,它被视为一个“黑盒子”,输入当前的情境和动作,输出对下一个情境和即时奖励的预测。模型的作用是模拟现实世界,Agent 通过与现实世界的持续互动产生经验,并利用这些经验通过监督学习方法来改进模型,使其更接近真实的物理规律。

在 2026 年回顾这篇 36 年前的论文,会发现这份古早的研究为理解当下复杂的技术路线之争提供了共同的根基——

对世界模型的探究,起源于对人类、机器,以及更广泛的智能体如何学习和行动的好奇。

而“世界模型”作为一种方法,提出的解决方案是在模拟出的世界中,让智能体学习、行动、获得反馈和迭代。

Dyna 这篇论文的核心理念,成为了今天世界模型的研究的底层思路。

不管是 NVIDIA Cosmos、World labs、Google Genie,还是 LingBot-World,都沿袭了 Dyna 的核心理念:世界模型是为智能体提供“模拟经验”的内部环境,使得智能体可以在一个虚拟的环境中进行规划和策略训练。

在不同方向的探索中,我们可以得到的共识是:世界模型从多样化的输入数据中学习对真实世界环境的内部表征,包括物理规律、空间动态和因果关系等。这些表征帮助模型预测未来状态,模拟动作序列,并支持复杂的规划与决策,而不需要反复进行真实世界的实验。

36 年过去,我们正站在大语言模型的阴影和语境中讨论世界模型。LLM 在理解真实物理世界、及模拟/预测未来后果等方面的局限,正加速科研和商业领域对世界模型的探索。

在 2025 年的一次访谈中,Dyna 的创作者 Richard S. Sutton 强调,LLM 已经走到了瓶颈。他指出,LLM 的核心缺陷在于,它们仅仅是在模仿人类行为,而无法理解世界、预测现实世界中的未来事件。他提倡放弃基于 LLM 的路径,转而开发基于强化学习、拥有世界转换模型(Transition model of the world)。这种世界模型不仅能学习奖励,还能从所有感官信息中获取环境的丰富理解,最终能够预测“如果做某事,后果将是什么”。

大语言模型在理解真实物理世界的不足,以及模拟/预测未来后果的不足,让一批科学家转向,在世界模型中寻找解法。

李飞飞认为 LLM 缺乏对物理世界的感知,提出“空间智能”(Spatial Intelligence)是 AI 的下一个北极星,AI 需要理解三维空间、几何、物理规则以及因果关系,才能从“理解文本”迈向“理解并作用于物理世界”。

Yann LeCun 则批评 LLM 依赖文本概率预测,感知学习世界的方式背道而驰。为此,他推广 JEPA(联合嵌入预测架构),并成立 AMI Labs,通过世界模型的路径实现 AGI,探索如何让 AI 系统具备理解物理世界、持久记忆、逻辑推理以及复杂任务规划能力。

DeepMind 联合创始人兼 CEO Demis Hassabis 在今年 1 月的对谈节目中强调,目前的 AI 系统还不能理解物理世界、因果关系、行为如何影响结果,而精确的世界模型是实现科学发现或理论创新的关键。他表示,Genie 这样的模型还只是“胚胎期世界模型”,Genie 体现出的,生成关于世界的内容的能力,某种程度上体现了模型理解了世界的知识。

Google AI 团队深度押注了世界模型的发展,并认为它会在 2026 年赢得重大发展。Hassabis 在谈及 2026 年的突破和期待时提到,“最令我兴奋的,莫过于进一步推动‘世界模型’的发展,提升其运行效率,从而使其能够真正被用于我们通用模型中的‘规划’环节。”这可能意味着,未来世界模型将融入 Gemini 这样的基础模型中。

世界模型的路线分歧

在探索 AGI 的道路时,蚂蚁集团也看到了世界模型的潜力。

作为蚂蚁集团旗下的具身智能企业,蚂蚁灵波的定位是“智能基座公司”,致力于打造一个能够理解世界、物理规律以及时空演化的 AI 系统。而世界模型正是实现这一目标的重要方式之一。

尽管各方都将世界模型视为未来的关键技术,然而不同公司选择的路径却各不相同。总体上,这些路径可以分为生成式和非生成式两类,两种路径的核心区别在于预测空间。

NVIDIA Cosmos、DeepMind Genie 和 World Labs 都是生成式路径的代表。

Cosmos 和 Genie 主要使用由像素构成的观测空间,利用大规模高维视觉数据训练,通过特定的时空架构设计,让模型产生对三维物理世界的理解。Genie 3 官网中特别提到“Genie 3 的一致性是一种涌现能力……Genie 3 生成的世界更为动态和丰富,因为它们是基于世界描述和用户动作逐帧创建的。”

World Labs 则另辟蹊径,将预测空间设定为在 3D 空间中带有位姿的帧,通过查询待生成帧的位姿来生成新图像。其发布的 RTFM 模型表明:“模型对世界的记忆(存储在各个帧中)具备了空间结构;它将带有位姿信息的帧视作一种‘空间存储’,这赋予了模型一种弱先验——即所建模的世界是三维欧几里得空间,而无需强迫模型显式预测该世界中的物体几何结构。”

非生成路径的代表是 Yann LeCun 的联合嵌入预测架构(Joint Embedding Predictive Architecture, JEPA)。JEPA 通过编码器将输入转化为潜空间(Latent Space),并在该空间内预测未来抽象表征(Embeddings),从而无需进行像素级的重建。

蚂蚁灵波的 LingBot-World 选择了类似 Genie 的路径,试图在此基础上解决从视频生成到世界模拟之间的技术障碍。

拆解 LingBot-World

在前文的案例和分析中,我们看到蚂蚁灵波的 LingBot-World 沿袭了 Gienie 的生成式路线,同时在交互能力、高动态稳定性、长时序连贯性以及物理一致性上表现惊艳。

在此基础上,蚂蚁灵波选择开源代码和模型权重,并在论文中完整披露了从数据采集到训练部署的全链路设计,鼓励社区测试、使用和复现。

即使是在近 10 分钟的超长视频中、或是快速运动下,画面中的物体依然保持了较为稳定的几何物理特性,没有出现视频生成模型常见的崩坏。这种稳定性,源于其独特的数据引擎和模型架构设计。

数据引擎

许多从视频生成模型切入世界模型研发的团队,很快会撞到数据瓶颈。

互联网上浩如烟海的短视频大多是“被动”记录,缺乏因果链条。对于世界模型而言,它需要理解的是动作和后果之间的关系。

比如:“按下 W 键向前走,门是否会打开?”“绕到建筑背面,窗户是否依然存在?”这类智能体动作与环境反馈之间的因果闭环,在普通视频中几乎不存在,在真实世界中规模化采集的成本也很高。

为了构建“动作-反馈”的闭环,LingBot-World 打造了从采集、处理到标注的流程。

LingBot-World 的数据包含通用视频、游戏数据和合成渲染数据,以确保训练语料的丰富性、高质量和交互性。为游戏数据,灵波团队还开发了专门的平台,捕获 RGB 帧并严格对齐用户的输入和相机参数。合成数据由 Unreal Engine 生成,带有精确相机数据和自定义轨迹。

LingBot-World 数据处理和标注流程

在数据处理层面,灵波团队首先对原始视频进行质量筛选与切分,生成结构清晰的视频片段;然后借助 VLM 视频的视觉质量、场景类型和视角等,结合几何标注提供必要的 3D 结构先验,产出元数据。

在此基础上,团队引入三种不同粒度的描述标注,涵盖视频全过程的宏观描述、去除了动作和相机数据的静态描写,以及带有时间标注的描述。

模型构建和训练

LingBot-World 将世界模型定义为一个条件生成过程,模拟由智能体动作驱动的视觉状态演化。

从模型构建和训练过程,我们可以看到,LingBot-World 是从“视频生成模型”起步,通过不同阶段训练,让模型从“生成”走向“模拟”。

从目标函数上看,这种模拟本质上是一种概率预测

LingBot-World 的目标函数明确表达了这一思想:

$$\max_\theta \sum_{t=1}^{T-1} \log p_\theta(x_{t+1} | x_{1:t}, a_{1:t})$$

即在最大化给定历史帧 ($x_{1:t}$) 和动作序列 ($a_{1:t}$) 的条件下,预测下一帧状态 ($x_{t+1}$) 的似然概率。

简单来说,就是让模型学会根据过去看到的画面和执行过的动作,尽可能准确地预测下一帧画面。

为了避免直接从零训练导致的计算开销和模式崩塌,LingBot-World 采取了分阶段的训练策略。

预训练负责建立稳健的通用视频先验,确保高保真开放域生成;中训练注入世界知识和动作可控性,使模型能够模拟具有一致交互逻辑的长期坚持动态;后训练使架构适应实时交互,采用因果注意力和少步蒸馏以实现低延迟和严格因果性。

LingBot-World 模型训练流程。

从“生成视频”到“模拟世界”,LingBot-World 带来的可能性

LingBot-World 的意义绝不仅在于生成一段精美的视频,而在于它提供了一个高保真的物理交互沙盒,成为具身智能、自动驾驶与虚拟现实等下游任务的通用基础设施。

LingBot-World 最直观的突破在于它赋予了通过自然语言控制模拟过程。例如,通过输入“冬季”或“夜晚”,模型会渲染出城堡结冰或夜晚灯光变化的物理效果,同时支持向“像素风”或“蒸汽朋克”等风格的切换。还可以在具体场景中精确注入特定物体。例如,在城堡上空触发烟花,或在喷泉中生成鱼和鸟。

在环境中生成烟花效果

改变环境整体风格

在自动驾驶训练中,这种能力极具价值。算法团队可以人为制造“鬼探头”、极端天气或突发交通冲突,构建出严苛的因果推理环境,从而低成本地解决智驾中的长尾问题。

深层物理特性的稳定性,则为这种模拟提供了实际应用的底座。得益于模型展现的长程记忆,生成的视频序列具备了较高的 3D 一致性,这使得视觉信息可以直接转化为场景点云,从而服务于 3D 重建或高精度仿真任务。

LingBot-World 具有很好的 3D 一致性。可以看到,视角变化的情况下,房间结构和物理性状仍然保持稳定。

这种稳定性试图触及具身智能训练中的一个核心痛点:机器人的导航或复杂操作往往涉及跨越长时序的决策序列。LingBot-World 展现的 10 分钟级别生成能力,在理论上为多步骤任务提供了更稳定的物理一致性。如果这种长程模拟能有效控制累积误差,将有助于机器人在虚拟环境中进行高频次、深度、低成本试错。

在此基础上,LingBot-World 与 LingBot-VLA(视觉-语言-动作模型)的结合,勾勒出了一种具身大脑的闭环方案。在这种设定下,世界模型充当了机器人的“内部模拟器”:在 VLA 模型输出最终指令前,系统可以在虚拟空间中先行演练不同的动作轨迹,评估其物理后果,从而筛选出更符合物理规律且具备安全性的执行路径。

令人惊喜的是,利用训练 LingBot-World 的数据,蚂蚁灵波团队还微调出了动作智能体。智能体可以被置于 LingBot-World 打造的环境中,Agent 的动作改变会实时重塑环境状态,而环境的演变则反过来决定 Agent 的下一步决策。

灵波团队利用 LingBot-World 相同数据训练处的自主智能体,能在生成的世界中自主规划并执行动作。

这种互动揭示了世界模型在“模拟沙盒”之外的另一种可能——它不仅能理解环境对智能体变化的响应,也具备预测智能体动作流的能力。

这意味着,世界模型未来或许不仅仅是训练智能体的工具,也有可能成为驱动智能体(包括机器人)的底座。

项目官网:

https://technology.robbyant.com/lingbot-world

论文连接:

https://arxiv.org/abs/2601.20540

代码和模型权重下载:

https://github.com/robbyant/lingbot-world

https://huggingface.co/robbyant/lingbot-world

https://www.modelscope.cn/models/Robbyant/lingbot-world-base-cam

玩美股,最近心血来潮,想看看各个 ai 对股票的分析能力如何。
比较了 豆包、Grok 、Gemini3Pro (家庭版,已付费)。
以$FLUT 这支股票为例。
用同样的 英文提示词 提问。
Gemini3pro ,豆包、Grok 的结果(无敏感诱导信息)。


$FLUT 截止 2026-01-28 的价格是 166.730$。豆包、grok 都基本正确(夜盘等因素)。可 gemini3pro 就跑偏太多了,居然用了$265 的价格。。。不知道他哪查的。

追问 Gemini3 为什么出错,给了这三个理由。我也不知道 后续他会不会自我纠正。



不知道各位 V2er 是否有遇到过 Gemini 的这个问题。是否是哪里配置需要开启?

补充:在 System instructions 中,已明确 gemini 使用最新的 google 数据。

我一直记得小学一年级(人教版,课程改革 2003 年)学的第一首诗:一去二三里

一去二三里
烟村四五家
亭台六七座
八九十枝花

之前问别人,好多人不记得了?
今天找了一下以前的图,确实是的,但是封面好陌生。
image
封面

image

前言:如果说 2023 年是“大模型”的破壳时刻,那么 2026 年则被科技界正式定义为 “智能体(AI Agent)元年”。这一年,AI 完成了从“只会聊天的计算器”到“能办事的数字员工”的跨越。一场关于行动力、自主权与新赛道的产业革命已然拉开序幕。

一、 范式跃迁:从“静态生成”到“动态执行”


2026 年,我们正见证 AI 逻辑的根本性扭转。过去,大模型以“知”见长,而现在的智能体以“行”取胜。

  • 自主决策的闭环: 智能体不再是被动等待指令的对话框,而是具备目标感知、环境交互与任务规划能力的“数字生命”。
  • 具身智能的延伸: 通过多模态模型的融合,智能体开始走出屏幕,深入到自动驾驶、智能制造以及复杂的个人事务处理中,实现了从“辅助工具”到“行动主体”的质变。

二、 赛道开辟:2026 产业生态的三大爆发点


在这一条全新的赛道上,三根核心支柱正支撑起万亿级的市场空间:

1. 智能体原生市场的形成

如同当年的 App Store 改变了移动互联网,2026 年的“智能体市场”成为了新的流量入口。开发者不再仅仅提供算法,而是发布具备专业技能(如理财顾问、代码架构师、健康管家)的独立智能单元。

2. 跨系统协同的“数字劳动力”

智能体之间开始学会“对话”。通过标准化的协作协议,不同的智能体可以像人类部门一样相互配合,完成从市场调研到方案落地的一站式自动化办公。

3. 可信治理与责任伦理

随着 AI 拥有了代理权,2026 年也成为了“AI 治理元年”。全球范围内关于智能体身份认证、行为审计与权限分级的法律框架基本成型,为新赛道的狂飙突进安上了“安全阀”。


三、 角色再造:人类从“操作员”转型为“协调者”


智能体的普及并非对人的取代,而是对人类价值的重新定义。在 2026 年的工作流中,人类的角色发生了以下转变:

人类设定目标(What to do)- 智能体规划路径(How to do)

人类判断价值(Why it matters)- 智能体执行交付(Get it done)

未来的核心竞争力,不再是你会不会写代码或画图,而是你是否具备“智能体调度能力”——即如何高效地管理一群 AI 智能体来达成复杂的商业目标。


四、 结语:开辟者,终将定义未来


2026 年,大幕已启。智能体来了,它带来的不仅是技术的迭代,更是一次文明层面的协作升级。在这条新赛道上,先行者正在重塑行业逻辑,而跟随者也将在 AI 原民的时代找到新的生态位。

这或许就是“智能体元年”最深刻的启示:技术的终点,永远是人的升华。

本文章和图片由AI负责生成

作者介绍

苏国庆

资深审计内控专家 | 全栈架构师

Oinone Codelab 开源组织 核心用户

行业领先内控审计公司技术负责人,10 年+ 行业深耕,拥有从架构设计至业务落地的全链路闭环能力。

精通全栈开发与数据治理,在复杂数据采集及深度分析领域造诣深厚,擅长攻克高难度业务数据挑战。

在国家大力推进教育治理体系和治理能力现代化的背景下,财政部、教育部联合发布《关于进一步加强高等学校内部控制建设的指导意见》(财会〔2024〕16号),明确提出到2026年基本建成制度健全、权责清晰、制衡有力、运行有效、风险可控、监督到位的高校内部控制体系。

如何让内部控制体系实际融入单位业务并服务于单位治理,让风险可监测、可跟踪、可预警、可纠偏,成为现实难题。以此为驱动,河南中审科技有限公司依托数式Oinone低代码平台,成功打造了面向各级院校的“院校内部控制数智化服务平台”,以真实业务场景为载体,探索出了一条“用内控规则驱动业务、用数据支撑治理”的可落地路径。不仅响应了国家对高校治理能力提升的战略要求,更充分展现了Oinone作为企业级产品化引擎在复杂业务场景中的强大支撑能力。

政策驱动内部控制成为单位治理能力提升的重要抓手

近年来,国家层面持续释放明确信号:

第十四届全国人大常委会第十次会议表决通过《关于修改(中华人民共和国会计法)的决定》,首次将内部控制写入会计法,明确提出“各单位应当建立、健全本单位内部会计监督制度,并中华人民共和闻会计法纳入本单位内部控制制度”,为各单位开展内部控制评价工作提供了坚实的法律保障。

2023年2月8日,中共中央办公厅、国务院办公厅印发了《关于进一步加强财会监督工作的意见》,并发出通知,要求各地区各部结合实际认真贯彻落实。其中,《意见》从5个方面明确要求完善“内部控制”:

图片

尤其是在高校领域,财政部、教育部最新文件明确要求:

规范债务管理,加强对外合作管理,强化科研管理,加强财政专项项目管理,规范非学历教育办学行为,强化所属企业管理,规范附属单位和校内独立核算单位管理,加强教育基“6+N"金会管理。全面提升高等学校内部控制的信息管理水平。

到2026年,基本建立制度健全、权责清晰、制衡有力、运行有效、风险可控、监督到位的内部控制体系。

图片

充分发挥高等学校党委在内部控制建设中的领导作用,内部控制相关重要议题应提请党委决策审议。明确高等学校校长是内部控制建设和实施工作的首要责任人明确学校领导班子其他成员是各自分管领域内部控制建设与实施的负责人。内部控制工作应纳入高等学校领导班子年度履职清单。

现实痛点为什么“有内控,却防不住风险”

在大量高校实践中,我们发现几个高度共性的难题:

图片

1.建设成效与预期存在偏差

· 建设成果与单位业务不匹配未达建设预期成效;

· 未将内部控制融入单位业务流程业务覆盖不全面;

· 未形成对单位治理的支撑作用无法充分发挥管控效能;

2.传统建设方法无法满足新要求

· 传统内控建设方法耗费工时多、质量低、效果差;

· 需采购第三方服务与过“紧日子”的要求不符合;

· 对人员专业能力和经验依赖性高无法确保内控建设质量和效果;

3.风险管控响应滞后

· 传统模式依赖人工排查风险管控响应存在滞后;

· 人工识别易出现风险遗漏判断结果存在偏差;

· 风险管控以事后补救整改为主事前防控不足;

这些问题的本质在于:内控规则没有进入业务系统“跑起来”。

关键支撑数式 Oinone 平台让内控数字化、数智化、数治化

高校内控系统对平台能力要求高:业务复杂、规则多、变化快、国产化要求严格。

数式Oinone在本项目中,成为内控数智化真正落地的关键底座。基于内部控制体系成果构建内控规则库,形成单位管控的业务底座,实现内部控制数字化;通过内部控制形成基于规则前置的经济业务的全流程应用,实现内部控制数智化;基于内控规则对业务过程深度分析,让数据话说,挖掘潜在风险,织密廉政风险防范网,实现内部控制数治化。

1.数据驱动:平台级能力的统一建模与演进基础

数式Oinone以元数据驱动作为平台的底层设计理念,将应用中的模型、页面、流程、权限、集成关系等共性要素统一抽象为可管理的元数据对象,使系统具备:

· 可建模:核心业务要素在平台层面形成统一描述,而不是分散在各类实现代码中;

· 可复用:已沉淀的模型结构、交互模式和流程能力可在不同应用、不同项目中复用;

· 可演进:通过元数据的差量管理和版本管理机制,支撑产品持续迭代和升级;

基于这一能力,平台实现了产品结构与实现逻辑的解耦,为复杂业务系统的长期演进、模块扩展和规模化交付提供了稳定而可持续的技术基础。

图片

2.低无一体:效率与灵活兼得

面对高校差异化管理需求,又可通过Java / Vue原生代码深度扩展,实现了真正的 “低无一体”开发模式,既快,又不受限。

图片

3.复杂流程建模能力,匹配真实内控场景

Oinone原生支持复杂流程引擎,使内控规则能够完整嵌入真实业务流转,而非简单审批。

图片

4.标品与个性化共存,支撑规模化复制

· 内控核心能力被沉淀为标准产品;

· 学校个性化规则以扩展包方式实现;

· 标品可持续升级,个性化不被覆盖;

图片

Oinone“产品化引擎”的能力解决了:项目能交付,产品却难迭代的行业共性难题。

5.国产化全栈支持,满足政务要求

平台全面适配:国产操作系统、国产数据库、国产中间件。

图片

落地成效内控从“制度约束”走向“治理工具”

基于 Oinone 平台构建的内控系统,在高校实际应用中,实现了:

✅ 规则前置

制度要求自动融入业务,不符合规则的操作即时提示或限制。

✅ 风险可视

预算执行、项目进度、合同履约、资产变动等 全流程可回溯。

✅ 管理闭环

问题发现 → 预警 → 整改 → 留痕,全程留痕可追溯。

✅ 治理升级

内部控制体系成果成为单位治理的“业务底座”

Oinone 平台成为单位治理的“技术底座”;

内部控制执行过程成为单位治理的“数据底座”;

“业务底座”+“技术底座”+“数据底座”促进单位治理能力进阶升级。

用Oinone,让专业能力变成可复制的产品

高校内控数智化实践证明:

优秀的平台,能够让复杂制度变得可运行,让专业能力变得可复制。

数式Oinone并不仅是一个低代码工具,而是一个面向软件公司的企业级产品化引擎:

· 帮助软件企业沉淀行业能力;

· 支撑标准产品与个性化交付并行;

· 让“项目经验”真正升级为“产品能力”。

而基于 Oinone 打造的内控数智化平台,也正在成为高校治理现代化进程中的重要数字基础设施。

ARC-AGI 测试

ARC-AGI 测试,是只给 AI 一两个「图形变形、变位、变色」的例子,根据这个例子,让 AI 做下一道题。

类似于数字猜谜时,我出 2,4,6 然后填(8)作为例子,然后再出 1,3,5 让 AI 填(7)。ARC-AGI 只不过是用图形的方式。

ARC-AGI 的核心假设

ARC-AGI 的核心假设是,人类是被进化调教的智能,预制了一些核心的先验知识(即娘胎里带来的),这些核心先验知识,是关于「物体恒常性」、「目标导向性」、「大小计数」、「形状拓扑」这些物理先验知识的。所以未来的 AGI ,应该也要对齐到这些。

可以理解的 ARC-AGI-1 和 ARC-AGI-2

前 2 版还可以理解(动手试试看):

第 1 版: https://arcprize.org/play?task=007bbfb7

ARC-AGI-1

第 2 版: https://arcprize.org/play?task=1ae2feb7

ARC-AGI-2

只不过,前 2 版都难不住现在的 AI: https://arcprize.org/leaderboard

ARC-AGI-SCORE

变态的 ARC-AGI-3

既然前 2 版难不倒 AI ,那就开发第 3 版啊,于是第 3 版全面升级,开始用互动游戏来测试了。

但,第 3 版这是谁出的第一个啊,太变态了!!

试试看,你能不能解出来: https://three.arcprize.org/games/ls20

ARC-AGI-3

前言

本篇文章主要讲解 RBAC 权限方案在中后台管理系统的实现

在公司内部写过好几个后台系统,都需要实现权限控制,在职时工作繁多,没有系统性的来总结一下相关经验,现在人已离职,就把自己的经验总结一下,希望能帮助到你

本文是《通俗易懂的中后台系统建设指南》系列的第九篇文章,该系列旨在告诉你如何来构建一个优秀的中后台管理系统

权限模型有哪些?

主流的权限模型主要分为以下五种:

  • ACL模型:访问控制列表
  • DAC模型:自主访问控制
  • MAC模型:强制访问控制
  • ABAC模型:基于属性的访问控制
  • RBAC模型:基于角色的权限访问控制

这里不介绍全部的权限模型,有兴趣你可以看看这篇文章:权限系统就该这么设计,yyds

如果你看过、用过市面上一些开源后台系统及权限设计,你会发现它们主要都是基于 RBAC 模型来实现的

为什么是 RBAC 权限模型?

好问题!我帮你问了下 AI

对比维度ACL (访问控制列表)RBAC (基于角色)ABAC (基于属性)
核心逻辑用户 ↔ 权限
直接点对点绑定,无中间层
用户 ↔ 角色 ↔ 权限
引入“角色”解耦,权限归于角色
属性 + 规则 = 权限
动态计算 (Who, When, Where)
优点模型极简,开发速度快,适合初期 MVP结构清晰,复用性高,符合企业组织架构,维护成本低极度灵活,支持细粒度控制
(如:只能在工作日访问)
缺点用户量大时维护工作呈指数级增长,极易出错角色爆炸:若特例过多,可能导致定义成百上千个角色开发复杂度极高,规则引擎难设计,有一定的性能消耗
适用场景个人博客、小型内部工具中大型后台系统、SaaS 平台 (行业标准)银行风控、AWS IAM、国家安全级系统

总结来说,在后台系统的场景下,RBAC 模型在灵活性(对比ACL)和复杂性(对比ABAC)上取得了一个很好的平衡

RBAC 概念理解

RBAC 权限模型,全称 Role-Based Access Control,基于角色的权限访问控制

模型有三要素:

  • 用户(User):系统主体,即操作系统的具体人员或账号
  • 角色(Role):角色是一组权限的集合,代表了用户在组织中的职能或身份
  • 权限(Permission):用户可以对系统资源进行的访问或操作能力

RBAC 的设计是将角色绑定权限,用户绑定角色,从而实现权限控制

RBAC 权限模型

并且,它们之间的逻辑关系通常是多对多的:

用户 - 角色 (User-Role): 一个用户可以拥有多个角色(例如:某人既是“项目经理”又是“技术委员会成员”)

角色 - 权限(Role-Permission): 一个角色包含多个权限(例如:“人事经理”角色拥有“查看员工”、“编辑薪资”等权限)

主导权限控制的前端、后端方案

市面上这些开源 Admin 的权限控制中,存在两种主要的权限主导方案:前端主导的权限方案和后端主导的权限方案

前端主导的权限方案

前端主导的权限方案,一个主要的特征是菜单数据由前端维护,而不是存在数据库中

后端只需要在登录后给到用户信息,这个信息中会包含用户的角色,根据这个角色信息,前端可以筛选出具有权限的菜单、按钮

这种方案的主要逻辑放在前端,而不是后端数据库,所以安全性没保障,灵活性也较差,要更新权限,就需要改动前端代码并重新打包上线,无法支持“动态配置权限”

适合一些小型、简单系统

后端主导的权限方案

后端控制方案,即登录后在返回用户信息时,还会给到此用户对应的菜单数据和按钮权限码等

菜单数据、按钮权限码等都存在数据库,这样一来,安全性、灵活性更高,要更新权限数据或用户权限控制,提供相应接口即可修改

倒也不是说前端完全不用管菜单数据,而是前端只需要维护一些静态菜单数据,比如登录页、异常页(404、403...)

在企业级后台系统中,后端主导的权限方案是比较常用的,本文只介绍后端主导的权限方案

权限方案整体流程

在开始写代码之前,要清晰知道整体实现流程,我画了一张图来直观展示:

权限方案整体流程

后台系统中的 RBAC 权限实战

权限菜单类型定义

首先,在前后端人员配合中,我们最好约定一套菜单数据的结构,比如:

import type { RouteMeta, RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router';
import type { Component } from 'vue';
import type { DefineComponent } from 'vue';
import type { RouteType } from '#/type';

declare global {
  export interface CustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
    /**
     * 路由地址
     */
    path?: string;
    /**
     * 路由名称
     */
    name?: string;
    /**
     * 重定向路径
     */
    redirect?: RouteRecordRedirectOption;
    /**
     * 组件
     */
    component?: Component | DefineComponent | (() => Promise<unknown>);
    /**
     * 子路由信息
     */
    children?: CustomRouteRecordRaw[];
    /**
     * 路由类型
     */
    type?: RouteType;
    /**
     * 元信息
     */
    meta: {
      /**
       * 菜单标题
       */
      title: string;
      /**
       * 菜单图标
       */
      menuIcon?: string;
      /**
       * 排序
       */
      sort?: number;
      /**
       * 是否在侧边栏菜单中隐藏
       * @default false
       */
      hideMenu?: boolean;
      /**
       * 是否在面包屑中隐藏
       * @default false
       */
      hideBreadcrumb?: boolean;
      /**
       * 当只有一个子菜单时,是否隐藏父级菜单直接显示子菜单内容
       * @default false
       */
      hideParentIfSingleChild?: boolean;
    };
  }

  /**
   * 后端返回的权限路由类型定义
   */
  export type PermissionRoute = Omit<CustomRouteRecordRaw, 'component' | 'children' | 'type'> & {
    /**
     * 路由ID
     */
    id?: number;
    /**
     * 路由父ID
     */
    parentId?: number;
    /**
     * 组件路径(后端返回时为字符串,前端处理后为组件)
     */
    component: string;
    /**
     * 子路由信息
     */
    children?: PermissionRoute[];
    /**
     * 路由类型
     */
    type: RouteType;
  };
}
router.d.ts 找到类型文件

以上面的类型定义为例,我们约定 PermissionRoute 类型是后端返回的权限路由类型:

我这里使用 ApiFox 来 Mock 权限路由数据,数据是这样的:

clean-admin ApiFox 文档在线地址

路由表 Mock 数据

从登录页到路由守卫

权限方案的第一步,是登录并拿到用户信息

假设我们现在用 Element Plus 搭建起了一个登录页面,当用户点击登录时,我们需要做这几件事:

  1. 调用登录接口,将账号、密码发送给后端进行验证,验证通过则返回 JWT 信息
  2. 将返回的 JWT 信息保存到本地,后续每次请求都携带 Token 来识别用户身份并决定你能拿到的权限路由数据
  3. 触发路由守卫拦截

登录操作

account-login.vue 找到全部代码

基本 Vue Router 配置

登录完成后,我们就可以触发路由守卫了,但在写路由守卫之前,我们先来配置一下基本的 Vue Router

在整个权限系统中,我们将路由数据分为两种:

  1. 静态路由:系统固定的路由,比如登录页、异常页(404、403...)
  2. 动态路由:由后端接口返回的用户角色对应的菜单路由数据

静态路由是直接由前端定义,不会从后端接口返回、不会根据用户角色动态变化,所以这部分路由我们直接写好然后注册到 Vue Router 中即可

Vue Router 配置:

import { createRouter, createWebHashHistory } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';
import type { App } from 'vue';
import type { ImportGlobRoutes } from './typing';
import { extractRoutes } from './helpers';
import { afterEachGuard, beforeEachGuard } from './guards';

/** 静态路由 */
const staticRoutes = extractRoutes(
  import.meta.glob<ImportGlobRoutes>(['./modules/constant-routes/**/*.ts'], {
    eager: true,
  }),
);

/** 系统路由 */
const systemRoutes = extractRoutes(
  import.meta.glob<ImportGlobRoutes>(['./modules/system-routes/**/*.ts'], {
    eager: true,
  }),
);

const router = createRouter({
  history: createWebHashHistory(),
  routes: [...staticRoutes, ...systemRoutes] as RouteRecordRaw[],
  strict: true,
  scrollBehavior: () => ({ left: 0, top: 0 }),
});

beforeEachGuard(router);
afterEachGuard(router);

/** 初始化路由 */
function initRouter(app: App<Element>) {
  app.use(router);
}

export { router, initRouter, staticRoutes };
图中的静态路由和系统路由是同一类路由数据,即静态路由

这个配置文件可以在 router/index.ts 找到

这个基本的 Vue Router 配置,做了这么几件事:

  1. 导入 modules 文件夹下的静态路由进行注册
  2. 路由初始化配置 initRouter ,在 main.ts 中调用
  3. 注册全局前置守卫 beforeEach、全局后置守卫 afterEach

我们实现动态路由注册的逻辑就写在 beforeEach

值得一提的是,使用了 import.meta.glob 来动态导入指定路径下的文件模块,这是 Vite 提供的一种导入方式,参考:Vite Glob 导入

路由守卫与动态注册

路由守卫是 Vue Router 提供的一种机制,主要用来通过跳转或取消的方式守卫导航:Vue Router 路由守卫

重头戏在全局前置守卫 router.beforeEach 中实现,来看看我们做哪些事:

import { ROUTE_NAMES } from '../config';
import type { RouteRecordNameGeneric, RouteRecordRaw, Router } from 'vue-router';
import { getLocalAccessToken } from '@/utils/permission';
import { userService } from '@/services/api';
import { nprogress } from './helpers';
import { storeToRefs } from 'pinia';

/** 登录认证页面:账号登录页、短信登录页、二维码登录页、忘记密码页、注册页... */
const authPages: RouteRecordNameGeneric[] = [
  ROUTE_NAMES.AUTH,
  ROUTE_NAMES.ACCOUNT_LOGIN,
  ROUTE_NAMES.SMS_LOGIN,
  ROUTE_NAMES.QR_LOGIN,
  ROUTE_NAMES.FORGOT_PASSWORD,
  ROUTE_NAMES.REGISTER,
];

/** 页面白名单:不需要登录也能访问的页面 */
const pageWhiteList: RouteRecordNameGeneric[] = [...authPages];

export function beforeEachGuard(router: Router) {
  router.beforeEach(async (to) => {
    /** 进度条:开始 */
    nprogress.start();

    const { name: RouteName } = to;

    const userStore = useUserStore();
    const { getAccessToken, getRoutesAddStatus, registerRoutes } = storeToRefs(userStore);
    const { setRoutesAddStatus, setUserInfo, logout } = userStore;

    /** 访问令牌 */
    const accessToken = getAccessToken.value || getLocalAccessToken();

    // 1.用户未登录(无 Token)
    if (!accessToken) {
      const isWhitePage = pageWhiteList.includes(RouteName);
      // 1.1 未登录,如果访问的是白名单中的页面,直接放行
      if (isWhitePage) return true;

      nprogress.done();

      // 1.2 未登录又不在白名单,则拦截并重定向到登录页
      return { name: ROUTE_NAMES.ACCOUNT_LOGIN };
    }

    // 如果已登录用户试图访问登录页,避免重复登录,要强制重定向到首页
    if (authPages.includes(RouteName)) {
      nprogress.done();
      return { name: ROUTE_NAMES.ROOT };
    }

    // 判断是否需要动态加载路由的操作
    if (!getRoutesAddStatus.value) {
      // isRoutesAdded 默认为 false(未持久化),在已经动态注册过时会设置为true,在页面刷新时会重置为 false
      try {
        // 1.拉取用户信息
        const userInfo = await userService.getUserInfo();

        // 2.将用户信息存入 Store
        setUserInfo(userInfo);

        // 3.动态注册路由,registerRoutes 是处理后的路由表
        registerRoutes.value.forEach((route) => {
          router.addRoute(route as unknown as RouteRecordRaw);
        });

        // 4.标记路由已添加
        setRoutesAddStatus(true);

        // 5.中断当前导航,重新进入守卫
        return { ...to, replace: true };
      } catch (error) {
        // 获取用户信息失败(如 Token 过期失效、网络异常)
        logout();
        nprogress.done();
        // 重定向回登录页,让用户重新登录
        return { name: ROUTE_NAMES.ACCOUNT_LOGIN };
      }
    }

    return true;
  });
}
before-each-guard.ts 找到全部代码

上面的代码已经给出了很详细的注释,从整体角度来讲,我们做了两件事:

  1. 处理一些情况,比如用户未登录、登录后访问登录页、白名单等情况
  2. 拉取用户信息,动态注册路由

路由守卫逻辑图

在路由守卫中“拉取用户信息”,一般来说,除了返回用户本身的信息外,还会给到权限路由信息、权限码信息,这里的数据结构可以跟后端进行约定

比如在 vue-clean-admin 中,返回的数据结构是这样的:

在 ApiFox 文档可以找到用户接口说明:ApiFox 文档 - 用户信息

后端路由结构的转化

在通过“拉取用户信息”拿到路由数据后,并不是直接注册到 Vue Router,而是需要进行处理转化,才能符合 Vue Router 定义的路由表结构,registerRoutes 就是处理后的路由表,处理后的类型定义可以参考 CustomRouteRecordRaw

处理什么内容呢?

比如,接口拿到的路由数据字段 component 是一个字符串路径,这是一个映射路径,映射到前端项目下的真实组件路径

路由表结构转换

实现路由结构转换的代码,我写在了 router/helpers.ts,最主要逻辑是 generateRoutes 函数:

/**
 * 生成符合 Vue Router 定义的路由表
 * @param routes 未转化的路由数据
 * @returns 符合结构的路由表
 */
export function generateRoutes(routes: PermissionRoute[]): CustomRouteRecordRaw[] {
  if (!routes.length) return [];
  return routes.map((route) => {
    const { path, name, redirect, type, meta } = route;
    const baseRoute: Omit<CustomRouteRecordRaw, 'children'> = {
      path,
      name,
      redirect,
      type,
      component: loadComponent(route),
      meta: {
        ...meta,
        // 是否在侧边栏菜单中隐藏
        hideMenu: route.meta?.hideMenu || false,
        // 是否在面包屑中隐藏
        hideBreadcrumb: route.meta?.hideBreadcrumb || false,
        // 当只有一个子菜单时,是否隐藏父级菜单直接显示子菜单内容
        hideParentIfSingleChild: route.meta?.hideParentIfSingleChild || false,
      },
    };

    // 是目录数据,设置重定向路径
    if (type === PermissionRouteTypeEnum.DIR) {
      baseRoute.redirect = redirect || getRedirectPath(route);
    }
    // 递归处理子路由
    const processedChildren =
      route.children && route.children.length ? generateRoutes(route.children) : undefined;

    return {
      ...baseRoute,
      ...(processedChildren ? { children: processedChildren } : {}),
    };
  });
}

经过 generateRoutes 处理的路由表,再 addRoute 到 Vue Router 中

侧边栏菜单的渲染

当路由守卫的逻辑走完后,就进入到首页,在首页中,我们会根据路由表(转换过的)来渲染侧边栏菜单

侧边栏菜单是拿 Element Plus 的 el-menu 组件来做的,我们封装了一个菜单组件,除了渲染路由数据外,也更方便自定义配置菜单属性(meta)来实现一些功能

封装不难,就是拿处理后的路由表循环渲染 menu-item,根据 meta 配置项来实现"是否隐藏菜单","当只有一个子菜单时,是否隐藏父级菜单直接显示子菜单内容"等

菜单组件封装

菜单组件的封装代码在 basic-menu 文件夹中

到这一步,已经实现了动态权限路由及侧边栏菜单的渲染,但还不算完

因为我们还不能自由定义菜单信息、角色信息、用户信息来实现权限控制,在下一篇文章来聊聊管理模块

了解更多

系列专栏地址:GitHub 博客 | 掘金专栏 | 思否专栏

实战项目:vue-clean-admin

交流讨论

文章如有错误或需要改进之处,欢迎指正

一、

前天,Kimi 突然发布了旗舰模型 K2.5,事先没有一点风声。

在国内,Kimi 是比较低调的公司,关注度相对不高。但是,它的产品并不弱。

半年前,K2 模型一鸣惊人,得到了很高的评价,公认属于全球第一梯队。所以,新版本 K2.5 出来以后,立刻上了新闻,在黑客新闻、推特等平台都是热门话题。

著名开发者 Simon Willion 当天就写了详细介绍

但是,这一次真正有趣的地方,不是模型本身,而是 Kimi 做了另一件事。

二、

这次的 K2.5 很强,各方面比 K2 都有进步。官方给出的评测跑分,基本都是全球前三位,甚至第一名(见发布说明)。

根据 LMArena(现改名为 arena.ai)的榜单,Kimi K2.5 的编码能力,是所有开源模型的第一,在总榜上仅次于 Claude 和 Gemini(下图)。

但是,最大的亮点其实不是模型,而是 Kimi 同时发布了一个基于这个模型的 Agent(智能体)。

也就是说,这次其实同时发布了两样东西:K2.5 模型和 K2.5 Agent。K2.5 是底层模型,K2.5 Agent 则是面向最终用户的一个网络应用。

我的印象中,这好像是第一次,大模型公司这么干。以前发布的都是模型本身,没见过谁把模型和 Agent 绑在一起发布的。

这么说吧,Kimi 走上了一体化的道路。

三、

大家知道,大模型是底层的处理引擎,Agent 是面向用户的上层应用。

它们的关系无非就是两种:分层开发和一体化。前者是大模型跟 agent 分开,各自开发;后者是做成一个整体一起开发。

前不久,被 Meta 公司高价收购的 Manus,就是分层开发的最好例子。

Manus 使用的模型是 Anthropic 公司的 Claude,它自己在其上开发一个独立的智能体,最终被收购。

它的成功鼓舞了许多人投入智能体的开发。因为模型的投入太大,不是谁都能搞的,而智能体的投入比较少,再小的开发者都能搞。

Kimi 这一次的尝试,则是朝着另一个方向迈出了一大步,把大模型和 Agent 合在了一起。毕竟,大模型公司自己来做这件事更方便,更有利于扩大市场份额、争取用户。

很难说,这两种做法哪一种更好。就像手机一样,苹果和安卓的外部应用,可以更好地满足用户需求,而自带的内置应用则能充分跟操作系统融合,用起来更顺滑。

四、

模型的测试已经很多了,下面我就来测一下,这次发布的 K2.5 Agent。

看得出来,Kimi 对 Agent 很重视,倾注了很大心血,发布说明的大部分篇幅介绍的都是 Agent 的功能。

其中有几个功能是比较常规的:

(1)Kimi Office Agent:专家级的 Word、Excel、PowerPoint 文件生成。

(2)Kimi Code:对标 Claude Code 的命令行工具,专门用于代码生成。

(3)长程操作:一次性完成最多1500步的操作,这显然在对标以多步骤操作闻名的 Manus。

我比较在意的是下面两个全新的功能,都是第一次看到,其他公司好像没有提过。

(4)视觉编程:通过模型的视觉能力,理解图片和视频,进而用于编程。只要上传设计稿和网页视频,就能把网页生成出来。

(5)蜂群功能(agent swarm):遇到复杂任务时,Agent 内部会自动调用最多100个 Agent,组成一个集群,并发执行任务,比如并发下载、并发生成等。

碍于篇幅,我就简单说一下,我的"视觉编程"测试结果。

五、

首先,打开 Kimi 官网,K2.5 已经上线了,能够直接使用(下图)。

注意,模型要切换到"智能体模式" K2.5 Agent。

我的第一个测试是动效生成,即上传一段动画效果的视频,让它来生成。下面是原始动画,是用 Lottie 库做的。

上传后,在网页输入提示词:

视频里面的动画效果,一模一样地在网页上还原出来

模型很快推断出,这是橘猫玩球的动画。然后,居然把动画每一帧都截图了,进行还原。

最终,它使用 Python 生成了 SVG 动画文件。

尾巴、眼球、小球滚动的动画效果,都正确还原出来了。可惜的是,主体的小猫是由多个 SVG 形状拼接而成,没法做到很像。

大家可以去这个网址,查看最终效果和网页代码。

六、

第二个测试是上传一段网站视频,让模型生成网站。

我在 B 站上,随便找了一个设计师网站的视频

大家可以去访问这个网站,看看原始网页的效果。

我把视频上传到模型,然后要求"把视频里面的网站还原出来"。

生成的结果(下图)完全超出了我的预期,还原度非常高,几乎可以直接上线。

大家可以去这个网址,查看生成的结果。

七、

经过简单测试,我的评价是,Kimi K2.5 Agent 的"视觉编程"不是噱头,确实有视觉理解能力,完全能够生成可用的结果。

目前看上去,Kimi 这次"模型 + Agent"的一体化尝试是成功的。一方面,强大的 Agent 发挥出了底层模型的能力,方便了用户使用;另一方面,模型通过 Agent 扩展了各种用例,可以吸引更多的用户,有利于自身的推广。

最后,在当下国际竞争的格局之中,一体化还有一个额外的优势。

Manus 依赖的是美国模型,最终不得不选择在海外注册公司,而 Kimi 的底层模型是自研的,而且开源,完全不存在卡脖子的风险。

(完)

我的笔记需求:轻量、支持同步、分享方便
刚进 2 站的时候有老友分享可以免费获取它的终身订阅 superthread 终身会员限时领取
一圈体验下来,还是挺不错的

优点:

  1. 轻量、反应快,操作流畅,排版美观
  2. 自带图床
  3. 跟随账号同步
  4. 支持多人协作
  5. 可以一键创建分享链接,比如这个 测试文档

缺点:

  1. 代码编辑器没有自动缩进
  2. 创建笔记的操作步骤有点多,体验有阻塞感

PixPin_2026-01-29_18-10-07

本着闲着也是闲着的态度,
开始琢磨看看能不能通过插件实现一键创建笔记的功能
发现官方有 api 文档 ,可以按需调接口实现部分功能
image

于是思路有了:

  • 注入一个“创建笔记”按钮
  • 点击触发“创建页面”请求
  • 接口调用成功后触发history.pushState切换到创建的笔记

codex 启动!

最后效果:
PixPin_2026-01-29_18-24-11


写在最后

我是没想到一个小小的功能还要这么折腾去实现,可能是我的使用方式不对吧facepalm

真实的海洋是动态且充满复杂相互作用的,藏在数据深处的洋流如何能可见、可理解?在制作“全球洋流”案例之前,我们面临的挑战是:如何将覆盖全球、深达5000米的洋流数据转化为实时、交互、直观的可视化体验?如何让包含23亿个数据值的全球洋流场在三维地球上“动”起来?
我们的目标很简单:让全球洋流的每一次流动,都能被看见、被分析、被应用

在三维数字地球表面,不计其数的发光粒子循着洋流轨迹奔腾穿梭——从北大西洋涡旋的回旋缠绕,到南极绕极流的绵延浩荡,再到深海底层流的隐秘流动,原本藏在数据深处的全球洋流,终以全维度、实时态的形式完整“显形”。

大规模粒子渲染,还原真实洋流的具象表达

接下来,我们将以技术实现者的视角,介绍这一个个让抽象的洋流数据转化为可直观感知的动态场景,从北极冰盖下的隐秘涡旋到赤道太平洋的大气海洋耦合,每一个“分镜头”背后,都是算法与物理规律的高度契合。

一、极地系统:冰封下的有序运动

1. 北极环流 • 波弗特涡旋

镜头聚焦北冰洋加拿大海盆,粒子以缓慢而稳定的顺时针轨迹旋转,形成直径约1000公里的巨大顺时针螺旋结构。粒子从边缘向中心缓慢汇聚,轨迹清晰显示涡旋的完整边界,精准还原北冰洋 波弗特涡旋 的特征——“几乎静止的旋转”的空间结构和时间持续性。

2.南极绕极流:全球海洋的“连接纽带”

镜头从南极上空俯视,粒子呈环绕南极的连续光环,环绕南极大陆无任何断裂以强劲、连续的轨迹,技术通过大范围坐标系适配,完美还原其连接三大洋的“传送带”功能。

二、边界流:海洋的“高速公路”

沿大陆边缘流动的洋流,受海陆分布和地形强烈影响。这类洋流在粒子渲染中表现为狭窄、高速、色彩鲜明的带状流动,粒子轨迹平直密集,流速梯度极大。

1.墨西哥湾流与黑潮:西边界强化流

北大西洋的墨西哥湾流和北太平洋的黑潮代表了“西边界强化”现象。粒子形成狭窄密集的高速丝带,紧贴大陆坡流动,流轴稳定。墨西哥湾流,全球最强的暖流之一,自美国佛罗里达海峡至纽芬兰岛的路径,粒子以高速密集轨迹流动,湾流呈现鲜明的橙红色,与周围蓝绿色冷水形成强烈对比。

黑潮与墨西哥湾流齐名的西边界强流,因水体透明度高、呈现深蓝色而得名。沿中国台湾东岸、日本群岛南岸延伸,靛蓝色粒子轨迹如“黑丝带”般清晰勾勒,精准还原其与周边海水的界限特征。

2.秘鲁、加那利与本格拉寒流:东边界的“冷输送带”

聚焦南美洲西岸秘鲁寒流、北大西洋东部加那利寒流、南大西洋东部本格拉寒流三大沿岸洋流。洋流沿大陆边缘流动的洋流,受海陆分布和地形强烈影响。粒子呈现宽缓的沿岸流动带,粒子速度较西边界流放缓,呈扩散特征。
加那利寒流通过大规模粒子精准勾勒分布与流动特征:数千粒子沿非洲西北部海岸呈狭长带状南下,轨迹紧贴大陆架边缘,密度由近岸向大洋方向逐步稀疏,清晰呈现其“贴岸流动、势力随离岸距离衰减”的分布规律,直观还原寒流沿程延伸特征。

本格拉寒流则通过粒子渲染形成鲜明呼应:近岸区域粒子呈现明显的“向上汇聚”轨迹,从深海层粒子向上层海域攀升,且上升流区域粒子密度显著高于周边,精准呈现其沿非洲西南海岸分布、近岸上升流旺盛的核心特征,粒子轨迹的垂直运动形态,更将这一寒流“深层营养盐向上输送”的关键属性具象化。

聚焦南美洲西岸秘鲁寒流,镜头贴近南美洲西海岸,粒子模拟向北流动的寒流及沿岸上升流,粒子在沿岸密集,离岸后扩散,展示了上升流将深层水带到表层的过程,清晰呈现其造就世界著名渔场的核心逻辑。

三、季风驱动流:北印度洋季风环流

作为全球唯一受大陆季风支配、流向季节性逆转的环流,镜头聚焦阿拉伯海与孟加拉湾核心区域:
•夏季模式(6-9月):粒子从索马里沿岸向东流动,在阿拉伯海形成顺时针大漩涡。索马里沿岸粒子呈现强烈的上升运动,垂向速度被放大100倍可视化
•冬季模式(12-2月):粒子流向完全逆转,形成逆时针环流。孟加拉湾粒子密集,反映冬季东北季风驱动的盆地尺度环流
•过渡期紊乱:季风转换期间(4-5月、10-11月),粒子运动杂乱,轨迹交叉频繁,反映流场的不稳定状态。

五、赤道流系:海洋的“多层立交”

赤道流系(含南/北赤道流、赤道潜流),镜头覆盖赤道太平洋上空及海表,粒子轨迹与大气环流箭头协同运动,生动呈现驱动厄尔尼诺现象的大气-海洋耦合机制——表层洋流的东西向流动与下层海水的上涌、下沉动作精准联动,让原本抽象的气候驱动因子变得具象可感,为科研人员研究厄尔尼诺现象提供了直观的动态工具。

六、跨洋副热带环流:大洋“漩涡”

涵盖南太平洋副热带环流、南印度洋副热带环流,大洋中部的大尺度闭合环流,构成全球海洋环流的基本单元环流边缘粒子密集,形成清晰的"粒子墙"。粒子呈现巨大涡旋结构,南印度洋洋流以宽阔的逆时针轨迹铺展,清晰区分厄加勒斯暖流(非洲东岸南下)与西澳大利亚寒流的流向差异;南太平洋环流则展现出宏大缓慢的逆时针旋转,粒子在环流中心稀疏分布,呼应其“海洋沙漠”(最清澈、生命最稀少区域)的特征,技术通过粒子密度智能分配,还原了洋流能量分布的真实状态。


这些生动的洋流“分镜头”,最终汇聚成一幅完整的全球海洋动力图景。所有可视化效果均基于真实数据与物理规律。下面我们将以技术实现者的视角,解析系统如何通过粒子追踪与动态渲染,精确再现全球经典洋流现象。

数据挑战:从静态数字到动态图像的数据壁垒

原始洋流数据藏在 NetCDF 格式的 “数据黑箱” 里,覆盖经度-180°至180°,纬度-80°至90°,垂直方向包含40个深度层。4500×4251×40 的原始分辨率意味着单文件就包含近 8 亿个数据点,每个网格点记录海水流速、流向的核心向量信息,单时间步数据量达 9.18 GB。
传统技术要么无法承载海量数据的实时运算,要么只能通过静态图表间接推测洋流动态,难以精准还原其复杂的三维运动特征,形成了“数据丰富但应用受限”的行业痛点。真正的突破需要从数据预处理到最终渲染的全流程重构。

1. 智能数据压缩:在保留与精简间寻找平衡

原始数据 4500×4251×40 的分辨率虽然精准,但计算量巨大。直接处理原始数据在实时交互场景中不可行。我们基于海洋动力学特征的智能抽稀算法,有针对性的进行特征保留,相当于 “把 4K 电影压缩成 1080P——既保留了关键洋流的细节特征,又让系统能在普通工作站上流畅运行。这种 “减法” 的智慧,让复杂的科研数据不再是实验室的专属,而是能被更多人轻松访问的动态可视化工具。

2. 三维瓦片地球的 “精准画布”:全球、全维度映射

全球洋流的经纬度跨度达 - 180°~180°、-80°~90°,要在三维瓦片地图上精准贴合,就像给地球 “穿衣服”—— 不仅要合身,还要能跟着地球自转、缩放自适应。我们的系统支持大范围坐标系动态适配,从南极冰盖到赤道暖流,每一道粒子轨迹都能与真实地理位置精准对齐。

3. GPU并行架构:粒子系统的实时演化

海量数字粒子同时在地球表面运动,每个粒子都要根据洋流向量实时计算轨迹,每秒要完成数百万次向量运算。我们用 GPU 并行计算技术,从数据解析、粒子更新到最终渲染,全流程在GPU上完成,每个 GPU 线程处理一个粒子,实现真正的数据级并行,粒子通过实例化渲染技术批量提交,结合深度测试与透明度混合,与三维地形瓦片无缝融合。

多模式可视化:从不同视角理解海洋

1. 地理模式:直观的空间认知

地理模式提供最符合认知的视角,与标准地图服务集成,叠加国界、国家名称等参考信息,用户可以自由旋转缩放,观察全球尺度环流格局,聚焦特定水层流动特征。

2 分析模式:深入的科学研究

为专业用户设计分析模式,支持多坐标系同时显示。天球坐标系从宇宙视角展示洋流与地球轨道关系;赤道面与黄道面叠加可视化,揭示太阳辐射与地球自转对环流的复合影响等,提供深入分析工具。

3 交互模式:灵活的动态理解

用户可通过参数面板,实时调节粒子生命周期、尺寸、尾迹长度等参数,对选择特定粒子或区域进行追踪,观察水团在数天至数月时间尺度上的运动路径,就像给洋流装了 “动态心电图”,细微的涡旋和暗流都能被精准捕捉。

在线体验地址:
https://www.tuguan.net/online-experience/code-sandbox4.html#向量图层_全球洋流图_stream
(请复制链接在浏览器中访问)

结语:可视化作为认知桥梁

从 23亿 数据点到指尖流动的光影,它构建了连接抽象数据与人类认知的桥梁,将复杂的海洋动力过程转化为可交互、可探索、可理解的视觉语言。
在气候变化深刻影响人类社会的今天,理解海洋这一地球系统关键组成部分变得尤为重要。我们的工作表明,通过创新的计算与可视化方法,曾经专属于超级计算机与领域专家的海洋环流知识,现在能够以直观形式服务于更广泛的科研、教育与应用领域。
从 NetCDF 数据的“黑箱”破解,到 GPU 实时运算的算力突破,再到全维度场景的精准呈现,本次案例印证了数字孪生、三维渲染技术在复杂向量场数据可视化中的技术优势。未来,我们将持续深耕技术创新,让更多“看不见”的自然规律,通过技术手段转化为可感知、可应用的价值成果,赋能更多行业实现数字化升级。

生成式 AI 的投资回报远超预期?Snowflake 调研全球 1900 位企业与 IT 专业人士后发现平均 ROI 高达 41%!点击下载完整报告

随着大语言模型能力的成熟,围绕 AI 智能体的讨论正在迅速升温。构建一个能够执行任务、调用工具的 Agent,已经不再是少数团队的专属能力。但在这场技术热潮之下,一个更现实的问题逐渐浮出水面:当智能体不再停留在演示环境,而是被放入真实业务系统中运行时,会发生什么?

CrewAI 创始人兼 CEO Joao Moura 以实践者的视角,在 BUILD 2025 大会上系统梳理了 AI 智能体从概念、原型走向生产环境过程中,所面临的一系列关键问题。这场分享的核心,并不在于“如何快速做出一个 Agent”,而在于如何让 Agent 在复杂系统中长期、稳定地工作。

从“会生成”到“会决策”:重新理解智能体的能力边界

在分享中,Joao 首先回到一个基础问题:什么才是 AI 智能体真正的能力来源。

他指出,很多人已经非常熟悉大语言模型在内容生成上的表现,例如生成文本、改写表达、调整语气。这些能力本质上仍然是“输出导向”的,模型根据输入,生成一段结果。

而智能体的出现,源于另一类能力的被系统性使用:决策能力。当模型不仅要给出答案,还需要在多个选项之间做出判断,并说明为什么选择其中一个时,它开始参与“思考过程”。

在此基础上,当系统为模型提供可调用的工具,并赋予其一个明确目标,模型就不再只是被动响应请求,而是开始围绕目标不断判断下一步行动。这种行动可能包括调用内部系统、获取业务数据、更新状态,甚至触发后续流程。

智能体并不是某种全新的技术形态,而是一个围绕目标进行持续决策与行动的系统。理解这一点,是后续讨论生产化问题的前提。

真正的分水岭:为什么原型和生产完全是两回事

在谈到智能体落地时,Joao 明确指出了一个现实情况:从原型到生产,并不是一次线性升级,而是一道本质不同的门槛。

原型阶段,团队关注的往往是“能不能跑起来”;而进入生产环境后,关注点会迅速转向“能不能持续运行”。这时,模型本身反而不再是唯一变量,系统层面的复杂性开始占据主导。

智能体一旦被放入真实业务系统,就意味着它将与 ERP、CRM 等核心系统交互,其行为可能直接影响业务流程。在这种情况下,系统是否稳定、决策是否可控、行为是否可预测,都会变成不可回避的问题。

很多阻碍智能体进入生产的因素,并不来自 AI 本身,而是来自工程、架构和系统集成层面的现实约束。这也是为什么不少 Agent 项目停留在 Demo 阶段,却迟迟无法真正上线的原因。

决策、工具与执行:Agent 在系统中的运行方式

一个智能体并不是简单地“调用模型”,而是需要在决策、工具调用和执行之间形成闭环。模型负责判断当前状态下应该采取什么行动,而系统则需要确保这些行动能够被安全、准确地执行。

当智能体需要调用外部工具时,问题并不止于“能不能连上接口”,而在于调用是否可控、结果是否可追踪、失败是否可恢复。这些因素,都会直接影响智能体是否具备进入生产环境的条件。

在这种结构下,Agent 更像是一个被嵌入到系统中的“决策节点”,而不是一个独立存在的智能模块。它的价值,取决于整个系统是否为它提供了稳定的运行土壤。

当数量上升:规模化带来的管理问题

当智能体不再是单点实验,而是开始成批部署时,另一个问题随之出现:如何管理这些 Agent。

规模化并不意味着简单复制更多实例。随着智能体数量的增加,部署、运行、监控和管理本身会迅速成为新的复杂系统。如果缺乏系统性的设计,智能体越多,整体风险反而越高。

回到整场分享的核心,Joao 传递出的判断其实非常清晰:AI 智能体的真正价值,并不在于是否足够聪明,而在于是否能够被可靠地使用。

当讨论从“能不能做”转向“值不值得用”,从原型转向生产,智能体面临的已经不是技术炫技的问题,而是工程与系统成熟度的检验。也正是在这个阶段,智能体才真正开始进入创造长期价值的轨道。

原视频地址:https://www.snowflake.com/en/build/americas/agenda/?login=ML

🔥【活动推荐】2 月 2 日-6 日,Snowflake Discover重磅上线!这是一场免费、线上、可实时互动的技术活动,旨在帮助您全面提升数据与 AI 能力,深入了解如何更高效地管理、整合与分析数据。4 天时间 18 场技术干货分享,由来自亚太地区的一线技术专家亲自分享与讲解~

点击报名 Discover,更多 Snowflake 精彩活动请关注专区

市面上 AI 课程一大堆,但要么太理论,要么太基础。本文对 Coursera 上 6 门优质 AI 课程进行了评测,结合国内初级开发者视角,帮你看懂各课程适合什么人、侧重点是什么,以及如何按自己的起点与目标做出选课决策。

导语

想系统学 AI 的程序员,近两年大概都干过一件事:

打开 Coursera 或其他平台,看到铺天盖地的 AI/ML 课程,然后 —— 关掉网页,继续刷短视频。

不是你不想学,而是:

  • 有的课过于理论,上了几节就被数学公式劝退;
  • 有的课过于入门,讲半天“什么是 AI”,却完全帮不上忙;
  • 真正能让你在简历和工作里都“有感觉”的课,又埋在一大堆选项里。

本文筛选出了 6 门“不浪费时间、能换来实际职业价值”的 Coursera 课程,并结合初级开发者视角,帮你搞清楚:

  • 这 6 门课,各自适合谁?
  • 如果你是初级开发者,应该先上哪一门?
  • 上完之后,应该怎么把所学变成真正的项目经验?

问题:AI 课很多,真正适合职场开发者的却不多

过去一年,很多人都有类似经历:

  • 带着“我要系统学 AI”的决心报了课;
  • 三节课之后,发现不是太抽象,就是太基础;
  • 最后课程一堆“进行中”,真正完成的少之又少。

大部分 AI 课程存在两个极端

  1. 要么面向研究生,数学证明一大堆,工作中很难直接用上;
  2. 要么把你当成完全不会电脑的小白,讲得过于浅,学完也不知道能干嘛。

而身处职场、尤其是入行 1–5 年的开发者,真正想要的是:

  • 上完课可以直接放到简历上的实打实的项目或证书
  • 能够帮助自己在团队里承担更多和 AI 相关的工作;
  • 在未来 1–2 年的职业选择里,多几条通道,而不是只会“跟风看热闹”。

所以,问题并不是“要不要学 AI”,而是:

怎样选到既不浪费时间、又能真实提升职场竞争力的 AI 课程?

误区:两种最常见的“选课踩坑”

误区一:只看“最难、最硬核”,结果半途而废

很多程序员的直觉是:

“一定要选最硬核、最学术的课,才显得值。”

结果报了课才发现:

  • 你要先补完一整套高数、概率统计、线性代数;
  • 课程作业更像研究生作业,而不是工程项目;
  • 上了几周,既看不见和工作场景的连接,也看不到短期内的产出。

这种“过度学术化”的路径,

  • 对想做科研或者攻读相关学位的人当然有价值;
  • 但对大多数只想把 AI 用到工作里的开发者来说,性价比非常低

误区二:只看“最轻松、最快拿证”,结果学完没用

另一种极端,是专门找:

  • 课时少、作业简单、几乎不用动手;
  • 全程在听“AI 概念故事”,几乎没有真实项目;
  • 学完唯一收获就是“多了一个证书链接”。

这类课程短期看很爽,

  • 但它既不会改变你写代码的方式;
  • 也很难在面试中解释“你到底掌握了什么”。

好课程既不能只停留在概念层面,也不能把你扔进纯数学海洋。

它应该:尊重你的智商,又尊重你的时间。


方法:一套更靠谱的 AI 选课思路

我们可以用一套简单的三问法来筛课:

  1. 课程是否清楚标明“适合谁”?

    • 是给完全不写代码的人,还是给开发者、产品、管理者?
  2. 课程是否有“可展示”的成果?

    • 项目、作业、证书,是否能放到简历或作品集中?
  3. 课程内容能否连接到 1–2 年内的职业机会?

    • 比如:AI 产品经理、AI 应用开发、数据驱动业务岗位等。

在这套筛选逻辑下,本文精选出的 6 门 Coursera 课程,大致覆盖了三类典型需求:

  • “我想从零开始理解 AI,并做点东西”
  • “我需要为团队、公司做 AI 相关的业务决策”
  • “我已经会写代码,想向更专业的 AI 工程方向迈一步”

下面将这 6 门课逐一拆解,告诉你适合哪些人学。


6 门 Coursera AI 课程逐一拆解

1)IBM 的人工智能导论(Introduction to Artificial Intelligence)

IBM 的人工智能导论

链接:https://www.coursera.org/learn/introduction-to-ai

一句话理解:

既照顾零基础,又不只是“科普故事”的 AI 入门课,
用动手实验带你跑通从概念到简单应用的闭环。

课程亮点:

  • 通过 实操实验 而不是长篇理论介绍 AI 基础;
  • 覆盖机器学习、深度学习、神经网络等核心概念;
  • 你会真正去 构建一个面向业务场景的生成式 AI 解决方案
  • 涉及 NLP、计算机视觉、机器人等典型应用方向;
  • 有一个简短但重要的 AI 伦理 模块,帮你建立底线意识。

适合谁:

  • 入行 1–3 年、已经会一门编程语言的开发者;
  • 想要一个“既不劝退、又有实战味道”的 AI 第一门课;
  • 希望拿到一个可以放 LinkedIn/简历上的 IBM 证书。

作为初级开发者,可以这样用这门课:

  • 把课程里的业务案例,

    • 尽量贴近自己所在行业(如电商、金融、物流);
    • 在完成作业的基础上,再自己加一点小改造;
  • 上完课后写一篇小总结:

    • “如何用生成式 AI 优化我们团队的某个流程”,
    • 这是非常适合放到公众号或内部分享的内容。

2)Andrew Ng 的 AI For Everyone

Andrew Ng 的 AI For Everyone

链接:https://www.coursera.org/learn/ai-for-everyone

一句话理解:

这不是教你写代码的课,而是教你
看懂 AI 项目真正的边界与机会,尤其适合想往“技术 + 业务”方向走的人。

课程亮点:

  • Andrew Ng 的教学能力不用多说,讲解清晰、接地气;
  • 面向 非技术背景跨职能角色(产品、运营、管理者等);
  • 重点讲:

    • AI 实际能做什么、不能做什么;
    • 如何在组织中识别 AI 机会;
    • 一个 AI 项目从立项到上线大致长什么样;
  • 有专门的 AI 战略模块,讲如何规划路线图和预算。

适合谁:

  • 想往 Tech Lead / 架构 / 产品化 路线发展的开发者;
  • 在中小团队里,已经开始参与需求评审、方案设计的人;
  • 希望和老板、业务方沟通 AI 方案时,能讲清楚利弊和边界。

作为初级开发者,你可以这样用:

  • 上完课之后,试着为你所在团队/部门写一页纸:

    • “我们这半年有哪些可行的 AI 应用机会”;
  • 即使你暂时做不了这些项目,这份文档也会:

    • 让你在团队里显得更“懂业务 + 懂技术”;
    • 成为你日后做晋升述职、项目立项时的素材库。

3)Google 的人工智能导论(Introduction to AI)

Google 的人工智能导论

链接:https://www.coursera.org/learn/google-introduction-to-ai

一句话理解:

从 Google 视角讲的“AI 是怎么从数据中学会东西的”,
重点在于让你弄清楚 能力与局限,而不是只会喊“好强大”。

课程亮点:

  • 是 Google AI Essentials 专项课程的一部分,结构清晰;
  • 讲清楚:

    • AI 如何从数据中学习;
    • 现实世界里的 能力边界 在哪里;
  • 特别强调 人的监督与参与

    • 反对“AI 自动跑就行”的想象;
  • 涉及:

    • 自然语言处理(NLP);
    • 大语言模型(LLM)应用;
    • 如何设计 AI 工作流;
  • 还有关于 创新和批判性思维 的部分,提醒你不要做“工具奴隶”。

适合谁:

  • 已经在使用 ChatGPT / Claude / Copilot 等工具的开发者;
  • 想更系统地理解“这些 LLM 背后大概在干嘛”;
  • 希望在做方案评估和技术选型时,有更多判断力的人。

对于初级开发者的用法:

  • 把课程里学到的 AI 工作流思想,套到你日常的一个小项目:

    • 例如:日志分析、简单问答机器人、文档检索助手;
  • 尝试用课程中的方法,画一个 “我们团队内部的 AI 工作流草图”

    • 这是你在团队里带节奏的好机会。

4)宾夕法尼亚大学的商业人工智能(AI For Business Specialization)

宾夕法尼亚大学的商业人工智能

链接:https://www.coursera.org/specializations/ai-for-business-wharton

一句话理解:

这是面向“想把 AI 用在商业上”的人,
帮你从营销、风控、人力等多个角度看 AI 如何改变业务。

课程亮点:

  • 这是一个 专项课程(Specialization),包括 4 门课;
  • 核心围绕:

    • 大数据、机器学习如何支撑商业决策;
    • AI 在 营销、用户生命周期、风险管理 等领域的落地;
  • 有专门讲 AI 伦理与治理 的内容;
  • HR 与人才管理模块很特别:

    • 讲机器学习如何用在招聘、绩效、员工发展;
  • 案例实操包括:欺诈检测、信用风险、个性化推荐等;
  • 结业证书来自沃顿商学院,对简历有加成。

适合谁:

  • 金融、电商、SaaS 等领域工作的工程师或产品人;
  • 正在向 技术负责人 / 业务负责人 方向发展的人;
  • 想系统理解“AI + 业务”的,尤其是对数据驱动决策感兴趣的人。

对初级开发者的意义:

  • 如果你现在还主要写 CRUD 业务代码,

    • 这门课会帮你看到系统背后的“生意逻辑”
  • 你可以从课里挑一两个案例,

    • 结合自己的行业,写一份“小型 AI 业务方案”,
    • 这类内容非常适合作为晋升材料或内部分享。

5)AWS 的机器学习与人工智能基础(Fundamentals of Machine Learning and Artificial Intelligence)

AWS 的机器学习与人工智能基础

链接:https://www.coursera.org/learn/fundamentals-of-machine-learning-and-artificial-intelligence

一句话理解:

以 AWS 生态为载体,把 AI、ML、深度学习和生成式 AI 串成一张“业务地图”。

课程亮点:

  • AWS 官方出品,内容围绕其云服务展开;
  • 重点帮助你厘清:

    • AI、机器学习、深度学习、生成式 AI 之间的关系;
    • 每一类问题适合什么样的技术路径;
  • 带你认识 AWS 上的各种 AI 服务:

    • 例如用于文本分析、图像识别、对话机器人等;
  • 课程不长,但信息密度很高;
  • 如果你目标岗位偏向 AWS 生态,这张证书的价值更高。

适合谁:

  • 公司已经在用 AWS,或者你考虑转向云相关岗位;
  • 希望把“AI 能力”和“云平台技能”结合起来的人;
  • 想理解:

    • “在真实公司里,AI 不只是写模型,还要跑在云上”。

对初级开发者的用法:

  • 结合课程内容,自己尝试在 AWS 上做一个小 demo:

    • 例如:一个简单的图像分类服务、文本情感分析 API;
  • 然后把“架构图 + 简短说明”写成一页纸:

    • 这是既能当作品集,又能说明你懂云的好材料。

6)IBM RAG 与智能体 AI 专业证书(IBM RAG and Agentic AI Professional Certificate)

IBM RAG 与智能体 AI 专业证书

链接:https://www.coursera.org/professional-certificates/ibm-rag-and-agentic-ai

一句话理解:

这是六门里最“硬核”的一套,
真正面向想在 RAG、多模态、Agent 等前沿方向 深耕技术栈 的人。

课程亮点:

  • 完整的 专业证书项目,包含 8 门课程;
  • 系统覆盖:

    • RAG(检索增强生成)流水线;
    • 多模态 AI 应用;
    • 自主 Agent 系统;
  • 会用到的一些关键工具:

    • LangChain、LangGraph、CrewAI、AG2;
    • 各类向量数据库(例如 Chroma);
    • Gradio 这类 Web UI 框架;
    • 以及 Model Context Protocol(MCP)等现代接口;
  • 课程里有不少项目:

    • 数据可视化 Agent;
    • 具备上下文理解能力的应用;
    • 能调用外部工具的智能体。

适合谁:

  • 已经有一定编程和 AI 基础,想往 AI 工程 / AI 平台 方向发展的人;
  • 希望将来做“AI 应用开发 / AI Agent 平台开发”的工程师;
  • 对 RAG、多模态、Agent 等前沿方向有强烈兴趣的人。

给初级开发者的提醒:

  • 这套课门槛相对较高,不建议当作你的第一门 AI 课;
  • 更好的路径是:

    • 先通过 1–3 门入门/业务向课程,
    • 确认自己真的对 AI 开发方向有兴趣,
    • 再用这套证书做“进阶突击”。

总结:不要指望一门课改变人生,但可以让它改变你学习 AI 的方式

再好的课程,也不会在几周之内把你变成“AI 专家”。

它们做不到的:

  • 立刻帮你找到一份梦幻工作;
  • 取代你在真实项目中的试错和踩坑;
  • 让你不写一行代码,就变成“AI 大师”。

但它们做得到的是:

  • 让你少在错误的课程上浪费时间和金钱;
  • 给你一组 清晰的概念框架可以展示的作品/证书
  • 帮你在团队内外,打开更多围绕 AI 的机会窗口。

对初级开发者来说,更重要的是心态的转变:

  • 不再迷信“最难的课就是最好的课”;
  • 也不再沉迷“最容易拿证的课”;
  • 而是根据自己的起点和目标,有意识地做出选课决策。
真正拉开差距的,往往不是“你选了哪一门课”,
而是“你能不能把学到的东西,变成一个又一个实际的小项目和分享”。

如果你愿意,可以从这 6 门课里只选 1 门

  • 认真上完;
  • 认真做完作业和项目;
  • 再用你自己的方式,复盘、分享、迭代。

这比一次性报十几门课,却一门都没上完,要有用得多。


Hi,我是俞凡,一名兼具技术深度与管理视野的技术管理者。曾就职于 Motorola,现任职于 Mavenir,多年带领技术团队,聚焦后端架构与云原生,持续关注 AI 等前沿方向,也关注人的成长,笃信持续学习的力量。在这里,我会分享技术实践与思考。欢迎关注公众号「DeepNoMind」,星标不迷路。也欢迎访问独立站 www.DeepNoMind.com,一起交流成长。

本文由mdnice多平台发布

从市场转项目经理后,我最不适应的不是排计划,而是跨部门沟通:需求一改再改、大家都忙、信息越聊越散,最后项目像被“讨论”拖住。后来我把沟通从“多说几句”改成“把事情对齐”,用 3张表+2次对齐,把跨部门协作从扯皮拉回推进。本文是我踩坑后的复盘版,希望你少走弯路。

沟通不是“说服”,而是“让大家站在同一张地图上”

刚转型那会儿,我对“沟通能力”有点自信——市场做久了,写方案、做汇报、协调资源都不陌生。我以为跨部门协作的关键,是把话讲清楚、态度放柔软、跟进足够勤。
结果我遇到的第一类崩溃场景是这样的:

  • 需求方说:“就加个按钮,别复杂。”(他们脑子里是“体验优化”)
  • 研发说:“按钮背后是权限、埋点、风控链路。”(他们脑子里是“系统代价”)
  • 测试说:“你们什么时候稳定?我得排期。”(他们脑子里是“交付风险”)

我夹在中间,开会、纪要、催进度——越努力越像在搅浑水。

后来我复盘才意识到:跨部门沟通最可怕的不是没人沟通,而是每个人都在用自己那套“事实版本”做判断。你以为你在推进项目,其实你只是让信息流动得更快,但信息没有被“对齐”成同一套共识。

所以当有人问我“跨部门沟通怎么做”时,我现在会先把目标从“说服对方”改成两件事:

  • 建立共同事实:对齐目标、范围、验收口径(范围管理 + 验收标准);
  • 建立共同承诺:对齐责任、里程碑、变更处理方式(干系人管理 + 变更管理)。

用一句话理解就是:跨部门协作想推进,靠的不是“多沟通”,而是“先对齐事实,再对齐承诺”。

我试过很多复杂模板,最后留下的,是一套我能坚持、团队也不反感的“最小可用沟通机制”:3张表 + 2次对齐。它专门解决三类高频场景:跨部门总扯皮(因为事实不一致)、项目推进卡住(因为责任/接口不清)、需求频繁变更(因为取舍不透明)。

方法总览:3张表+2次对齐是什么?

我把它理解为一套“把口头沟通变成可执行协作”的最小结构。为什么强调“最小”?因为新人PM常犯的错(我也犯过)是:文档越做越漂亮,协作越变越沉重,最后大家都不看。所以我只保留三张“够用就好”的表,它们分别解决三个核心问题:

3张表(信息载体):把沟通从“感觉”落到“事实”

  1. 目标-范围表:我们要达成什么?这版做什么/不做什么?(范围管理)
  2. 责任-接口表(RACI):谁负责、谁拍板、谁被咨询、谁被知会?(干系人/责任边界)
  3. 里程碑-风险表:节奏怎么走?卡点在哪里?风险如何提前暴露?(进度/风险管理)

2次对齐(关键会议):在最容易跑偏的节点强制校准

  • 启动对齐(开工前):对齐版本、边界、验收与承诺
  • 变更对齐(需求变化时):对齐影响、取舍与更新后的计划

可直接照抄的清单版(方便你保存/转发):

  • 目标-范围表:目标 / In / Out / 验收口径 / 前置依赖
  • RACI表:交付物 / R / A / C / I / 接口Owner
  • 里程碑-风险表:里程碑交付物 / 时间盒 / 风险 / 触发信号 / 应对动作 / 责任人
  • 启动对齐:确认三张表的初版(尤其 Out、A、验收口径)
  • 变更对齐:确认“原因-影响-取舍-更新”并同步单一事实源

这样做的核心不是为了让所有人满意,而是让争论发生在纸面上(事实与取舍),而不是发生在人身上(情绪与立场)。

第一张表:目标-范围表(把“要做的事”说成同一句话)

定义一下:目标-范围表,是把“我们到底要做什么”变成可讨论、可裁剪、可验收的一张纸。跨部门误会的起点,往往是“同一个词,三种理解”。尤其是那句万能话:“很简单,加个按钮。”我现在做目标-范围表,会固定写六行,简单但很顶用:

1)目标(Why):一句话写清“要改变什么”

我会逼自己避开“提升体验”这种虚词,改成可验证句子:

  • 目标:将【某流程】的完成率从 A 提升到 B
  • 衡量:上线后看【指标/漏斗/反馈】验证
  • 期限:这次目标对应的业务窗口期是什么(活动/版本/政策)

为什么要写这么“死”?因为你需要一个锚点:“要不要加这个功能?”——先看它对目标的贡献,再谈实现代价。

2)范围(What):In / Out 是跨部门沟通的护城河

我会把范围写成三栏:必须做(MVP):不做就达不成目标;应该做(Should):做了更好,但可以延后;明确不做(Out):看似相关,但这版不做。这一步我以前觉得“写Out很尴尬”,怕得罪人。后来发现:不写Out,才是对团队最大的伤害。 因为Out不明确,所有“顺手加一下”都会默默流进开发和测试的夜里。

3)验收口径(Done):别让“做完了”变成各说各话

我会补一行“算完成的标准是什么”:覆盖哪些页面/接口/流程?哪些异常场景必须处理?是否包含数据埋点/日志/权限/兼容性。如果你只记一句:写验收口径,比写需求描述更能减少扯皮。它能直接回答“怎么判断完成”,这对跨团队协作太关键了。

4)前置条件 & 假设:提前暴露依赖,避免“后知后觉”

我会写一句:需求成立依赖什么?例如:接口可获取某字段、法务/合规确认通过、运营能配合灰度与公告。很多跨部门冲突,都是“依赖没说清”,结果上线前一天才发现缺口。

5)常见分歧怎么处理:用“目标优先”做裁剪

当业务坚持加一个Should,研发觉得成本大时,我会用这种说法:“我们先把它放进Should,并评估它对目标的贡献。如果贡献不大,我们安排到下个版本,先确保MVP按期交付”。你会发现,一旦你把争论从“要不要支持业务”转成“对目标贡献/代价/节奏”,情绪会明显下降。

6)可复制模板字段(建议直接复制到文档)

  • 目标(指标/期限)
  • In(MVP/Should)
  • Out(明确不做)
  • 验收口径(Done标准)
  • 前置依赖/假设
  • 待定项(谁在何时给结论)

第二张表:责任-接口表(RACI)(把“谁该做什么”摆到明面)

定义一下:RACI表的作用,是把责任边界“提前公开”,避免问题出现后才开始找负责人。很多项目推进失败,不是没人干活,而是默认别人会干。

  • R(负责):真正动手的人
  • A(拍板):最终对结果负责的人(最好只有一个)
  • C(被咨询):需要提供输入的人
  • I(被知会):需要同步的人

我踩过的坑是:A写了一堆人,结果等于没有A。A多=没人负责,R多=没人行动。所以我会坚持两条原则:每个交付物必须有一个明确A;每个关键动作必须有明确R。
我常用的“接口归属”写法(示例):

  • 需求与验收口径:R=业务/产品,A=业务负责人,C=研发/测试,I=相关方
  • 接口联调:R=研发,A=研发TL,C=数据/平台,I=PM/测试
  • 发布与回滚:R=研发/运维,A=技术负责人,C=PM/测试,I=业务方

让对方愿意“接责任”的小技巧:先给价值,再要承诺

我以前会说:“这个你负责一下。”(很容易被拒)现在我会换成:“为了减少你后面被反复打扰,我把边界写清楚:你只需要对【接口字段冻结】拍板,材料我来整理”。跨部门里,大家抗拒的不是责任本身,而是“无底洞式的额外负担”。
可复制模板字段(建议直接复制)

交付物/动作(例如:验收口径确认、接口冻结、灰度发布)

  • R / A / C / I
  • 接口Owner(单点负责人)
  • 依赖输入(例如:数据字段、合规确认)
  • 默认生效规则(X小时未反馈视为确认)

第三张表:里程碑-风险表(把推进从“催”变成“共同维护的跑道”)

定义一下:里程碑-风险表不是进度表,它更像“协作跑道”:让大家知道下一步交付物是什么,风险在哪儿。我以前推进项目的方式很朴素:每天问进度。后来发现,跨部门里“催”往往换不来产能,只会换来“我很忙”的反弹。

1)里程碑:用“交付物”定义节点,而不是用日期自我安慰

  • 需求评审通过(含验收口径)
  • 方案评审通过(含风险与回滚)
  • 提测包提交(清单完整)
  • 缺陷收敛到 X(阻塞项清零)
  • 灰度上线(核心指标无异常)
  • 全量上线(复盘完成)

“交付物写清楚”能减少大量“差不多了”“快好了”的模糊表达。

2)风险:写给“提前救火”的(风险台账 + 触发信号)

我写风险会包含三项:

  • 风险描述:可能发生什么
  • 触发信号:什么迹象说明它正在发生
  • 应对动作:谁来做什么,何时做

例子:

  • 风险:接口字段不稳定导致联调反复
  • 触发信号:2天内字段变更≥2次
  • 应对:字段冻结;变更需评审;指定接口Owner

触发信号是关键——它让风险从“感觉”变成“可监控”。

3)同步频率:用短周期让问题变小(沟通闭环)

我会设置一个“小节奏”:

  • 每周一次里程碑复盘(10–15分钟)
  • 联调/提测/上线阶段提高频率

目的不是“开更多会”,而是:让问题在小范围、小成本时被看见。

可复制模板字段:

  • 里程碑交付物 / 目标时间 / 责任人
  • 当前状态(绿/黄/红)
  • 风险描述 / 触发信号 / 应对动作 / Owner
  • 阻塞项(需要谁协助、截止时间)

第一次对齐:启动对齐(开工前把“版本”对齐)

启动对齐可以理解成“项目启动会”的轻量版本:不是热闹,是明确。

会前:三件事准备好,会议才不会开成空会

  • 目标-范围表初稿(至少写出MVP/Out)
  • 验收口径初稿(哪怕很粗)
  • RACI候选归属(让大家确认而不是从零讨论)
  • 会中:四个输出必须落地
  • 目标-范围表确认(尤其Out)
  • 验收口径确认(什么算Done)
  • RACI确认(谁拍板、谁负责、接口Owner是谁)
  • 里程碑-风险表初版(更新频率与维护人)

控场句(我常用三句)

  1. “我们先对齐事实:目标、范围、验收。”
  2. “有分歧先写进风险或待定项,别用口头承诺糊过去。”
  3. “会后我会发一页纸纪要,默认生效;不同意请在X小时内提出。”

“默认生效”听起来强势,但它其实在保护协作:没有默认机制,就会无限确认;无限确认,就是无限消耗。

第二次对齐:变更对齐(需求变化时,把取舍摆上桌)

如果你问我“需求频繁变更怎么沟通”,我的答案基本等同于:做变更对齐。
跨部门最伤的不是变化,而是变化没有代价——因为代价会被默默转嫁到开发、测试和交付节奏里。

变更对齐固定四问(原因-影响-取舍-更新)

  • 变更原因?(目标变了,还是理解变了?)
  • 影响是什么?(范围/工期/风险/质量)
  • 取舍是什么?(删什么、延什么、加资源还是降质量)
  • 更新什么?(三张表与里程碑怎么改,谁确认)

我会把结论写成一句可执行的话:“本次增加X,删除Y,里程碑顺延Z天,由A确认,R在周三前完成”。这句话的价值是:把“你让我改”变成“我们共同选择了一个方案”。

一些我后来才懂的小技巧

1)把“情绪”转成“事实”:先接住,再落表
当有人说:“你们需求太离谱了”。我会回:“我理解你压力很大,我们先把离谱点拆成范围或风险,逐条落到表里”。

2)少用“麻烦你”,多用“我来承担结构化工作”
跨部门最讨厌的是额外负担。我会说:“材料我整理,你只需要确认两个结论:Out 和接口Owner”。

3)所有对齐都要有“单一事实源”(SSOT)
我会明确:最终以哪份文档为准,放在哪个位置,谁维护、多久更新一次。否则群里一句话、会议一句话、口头一句话,版本立刻分裂。

4)让文档“轻”,但让结论“硬”
三张表不需要精美,但必须做到:可追溯(谁确认、何时确认)、责任明确(R/A清晰)、变更有记录(旧版本不丢)

转型做项目经理后,我最大的变化是:以前我以为沟通是“把话说清楚”;现在我更相信,沟通是“把事情对齐”。当你在问“跨部门沟通怎么做”时,你真正想要的,可能是:如何让一群很忙、目标不完全一致的人,愿意在同一条路径上推进项目。

我的答案是:用 3张表建立共同事实,用 2次对齐建立共同承诺。它不酷,甚至有点笨,但它让我从“到处追着问的人”,慢慢变成“能把项目推着走的人”。项目管理不是控制混乱,而是学会与不确定共处:你不可能让变化消失,但你可以让变化有代价、有记录、有共识。如果你也正处在转型期,希望这套方法能帮你少走一点弯路——至少在下一次跨部门会议里,你能更从容一点。