<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>HotTop - 掘金</title><link>https://juejin.cn</link><description>掘金 实时热榜</description><lastBuildDate>Tue, 01 Sep 2026 09:44:11 +0000</lastBuildDate><item><title>OpenJDK 全面禁止 AI 生成代码！</title><link>https://juejin.cn/post/7680074643861291034</link><guid isPermaLink="false">juejin-db7d3c7362c526cc</guid><description>文章的开头，咱们先来一个小问卷调查吧。 不知道咱们这里有多少小伙伴还在做 Java 开发的？ 最近 Java 圈子里出了一件事情被大家讨论得蛮多的，相信不少同学也都看到了。 那就是 Oracle 针对 OpenJDK 发布了一项极其严格的代码贡献政策，即： 禁止提交 AI 生成的代码。 消息一出，社区里吵翻了天。 有人拍手叫好，认为这是保护开源质量的底线，也有人嗤之以鼻，觉得这是逆时代潮流的老登行为。 关键更搞笑的是，Oracle 自己一边喊着用 AI，一边却又把 AI 生成的代码死死挡在 OpenJDK 项目门外。 这种割裂双标感，让许多开发者直呼看不懂。 这个禁令到底有多狠 我们先来看看这次 OpenJDK 官网上的政策原文。 OpenJDK 的这份政策，并不是可选建议，而是一份强制的硬性要求。 该政策明确规定，无论是全部还是部分，只要内容里沾了 AI 生成的边，一律都不得提交。 这里面的内容不仅包括源代码，类似像文本、图片、邮件、Wiki、PR等等，都在管控范围之内。 这还不算，官方 FAQ 里还给出了一个问题场景： 有人问，如果我用 AI 生成了 100 行代码，自己手改了 10 行，那这算合格的吗？ 官方回答干脆利落： 不行。 只要提交内容里包含了 AI 生成的内容，无论全部还是部分，一律禁止。 不仅如此，类似像文本内容、图片、PR、邮件内容、Wiki 内容……，只要是用 AI 生成的，统统不行。 唯一允许的，是你私下用 AI 来学习、调试、理解现有代码，但绝不能把 AI 的生成产物提交上去。 或者再换句话来说： AI 可以当你私下学习的助手，但却不能当你提交代码的责任人。 为什么要这么做 很多人觉得 OpenJDK 疯了，放着免费的代码生产力不用。 但如果你了解 OpenJDK 的地位，可能就会考虑到或许这也算是其一种清醒的策略取舍。 OpenJDK 不是普通的开源项目，它可以说是整个 Java 开源生态的地基，也是当今互联网生态中不可或缺的基础设施级软件。 全球大量的金融系统、政务系统、企业后台等都跑在它上面。在这个级别的项目里，稳定性和合规性压倒一切。 OpenJDK 官方给出了三个禁止理由，每一个都直击要害： 第一点是：审查负担问题。 AI 写代码太容易了，一天生成几十个 PR 完全不在话下，而且这些生成物看起来语法正确、逻辑通顺。 但问题是，这些代码往往看起来 OK，但实则暗藏杀机。比如藏匿着的细微错误、边界缺陷，或者压根就没有考虑到的长期维护性问题和兼容性问题。 OpenJDK 的维护者人数有限，资源也有限。如果放任大量 AI 产物的涌入，维护者会被淹没在看似合理但难以维护的代码红海里，最终导致项目瘫痪。 第二点则是：安全风险问题。 AI 模型有幻觉这是大家都有目共睹的共识，它可能会自信满满地写出一个看似能解决 Bug，实则引入新漏洞的代码。 在普通应用里，这或许只是个线上事故，但在 JDK 这里，这可能是一个波及全球的软件安全灾难。 第三点，也是最棘手的：知识产权问题。 AI 训练数据集来自海量互联网数据，生成的代码存在无意复制他人代码的风险。 而目前大家对于 AI 产出物版权归属的问题还没有一个明确定论。 一旦 AI 无意中缝合了带有版权隐患的代码合入 OpenJDK，那未来可能面临的就是巨大的法律风险和版权纠纷。 这对于 Oracle 等这种在知识产权上极其敏感的公司来说，这是绝对不能触碰的红线。 尴尬的反差 对于 Oracle 而言，你要说你旗下各个开源项目你都禁 AI 的话，大家其实也没啥疑惑。 但问题是，人家 Oracle 偏偏不这样。 这是因为其旗下另外一个知名开源项目 GraalVM 在 AI 工具辅助这件事情上则采取了完全相反的政策态度。 GraalVM 项目政策里明确允许 AI 辅助贡献，不过这里有一个前提： 代码提交者必须对内容负全责 。 而作为代码贡献者，你必须能能解释和维护你所提交的代码，如果做不到，该代码可能会被拒绝。 而在代码署名这一块，项目鼓励贡献者主动披露 AI 大模型参与情况， 但不作强制要求 。 其实在知识产权确认这方面，虽然两个项目都要求贡献者签署 OCA，但 OpenJDK 项目聚焦的是知识产权风险，而 GraalVM 项目则聚焦的是贡献者责任，出发点不一样，责任视角不一样，所反映出来的政策要求也完全不一样。 写在最后 众所周知，最近这两年 AI Coding 持续爆火，网上也时不时有程序员即将被 AI 替代的各种观点冒出。 但 OpenJDK 用一纸政策告诉了我们： AI 可以帮你写代码，但却不能替你担责 。 看到没，涉及到背锅问题的时候，AI 也不好使了吧。 细想一下，OpenJDK 的禁令，其实不是反 AI，而是反 不负责任的 AI 产物 。 AI 时代，虽说代码的生成成本趋近于零，但代码的出处、权属与责任问题，却并没有清晰的界限定论。 所以在把 AI 拉进你的实际生产协作流程之前，你还得先想清楚一个问题： 这段代码，到底谁能为它负责？ 注：本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录，里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记，以及程序员生活和感悟，欢迎star。</description><pubDate>Tue, 01 Sep 2026 01:20:48 +0000</pubDate></item><item><title>Redis已正式接入AI</title><link>https://juejin.cn/post/7680014875347255334</link><guid isPermaLink="false">juejin-e52dc9011ba72a05</guid><description>前言 Redis正在从一个“缓存中间件”进化成AI应用的“实时数据基础设施”。 2026年，Redis在AI方向上的布局已经全面铺开—— 向量搜索、Vector Sets、语义缓存、AI Agent上下文引擎 ，一条完整的AI能力矩阵已经成型。 今天这篇文章就专门跟大家一起聊聊Redis接入AI的话题，希望对你会有所帮助。 更多项目实战在Java突击队网：susan.net.cn/project 一、Redis为什么要接入AI？ 在聊具体能力之前，我们先理解一个根本问题—— 为什么Redis要做这件事？ 传统的业务系统对数据层的诉求是“快”——读得快、写得快、延迟低。 Redis凭借内存数据库的天然优势，在这个领域已经做到了极致。 但AI应用对数据层的诉求跟传统业务系统完全不同： 需要存 向量 ，做 相似度检索 需要管理 对话上下文 和 Agent记忆 需要缓存 模型推理结果 这些需求，恰好命中了Redis的老底子—— 内存优先、亚毫秒延迟、数据结构丰富 。 Redis团队在2026年初的年度预测中说了一句话，我觉得很能说明问题： “AI应用如果没有上下文引擎，注定会失败” 。 而Redis正在做的，就是把自己变成那个“上下文引擎”。 一句话总结：Redis不是在“蹭AI的热度”，而是在用自己最擅长的方式——内存速度、亚毫秒延迟、丰富的数据结构——解决AI应用最核心的数据基础设施问题。 二、Redis AI能力全景图 2026年的Redis，已经从单纯的缓存中间件进化成了完整的AI数据基础设施。 下面我逐一拆解这四大能力。 三、能力一：向量检索 Redis成了向量数据库。 这是Redis接入AI最基础、最核心的能力。 传统的Redis查询是精确匹配——你给一个key，它返回一个value。 但AI场景下，很多查询本质上是“模糊的”： 用户问“怎么退货”，你需要从知识库里找到 语义最接近 的FAQ 推荐系统要找和当前商品**“最像”**的其他商品 风控系统要找和当前交易行为**“最相似”**的历史欺诈模式 这些都不是精确匹配能搞定的，需要的是 向量相似度搜索 。 3.1 技术原理 Redis Query Engine（原来的RediSearch模块）支持将高维向量存储在Hash或JSON数据结构中，并在其上建立向量索引。 支持的索引算法： FLAT ：暴力搜索，精确但慢，适合小数据集（&lt;10万条） HNSW ：近似最近邻搜索，查询速度快，生产环境的主力选择 SVS-VAMANA ：Redis 8.4新增，针对大规模数据集优化 支持的距离度量： 余弦相似度——最常用，适合文本嵌入 欧氏距离 内积 3.2 混合查询——Redis的差异化优势 Redis向量搜索最大的优势是 支持混合查询 ——向量相似度 + 传统过滤条件可以组合使用。 # 在 category 为 &quot;database&quot; 的文档中，找最相似的 5 篇 FT.SEARCH doc_idx &quot;(@category:{database})=&gt;[KNN 5 @embedding $query_vec AS score]&quot; PARAMS 2 query_vec &lt;查询向量&gt; SORTBY score DIALECT 2 这在实际业务中非常实用。比如电商推荐场景，先按类目过滤，再做向量相似度排序，避免跨类目推荐出不相关的商品。 3.3 实战：Java中创建向量索引并检索 在Spring AI中，可以通过 RedisVectorStore 来操作： // 创建RedisVectorStore RedisVectorStore vectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel) .indexName( &quot;doc_idx&quot; ) .prefix( &quot;doc:&quot; ) .build(); // 添加文档向量 List&lt;Document&gt; documents = ... vectorStore.add(documents); // 执行相似度搜索 List&lt;Document&gt; results = vectorStore.similaritySearch( SearchRequest.builder() .query( &quot;Redis向量搜索&quot; ) .topK(5) .build() ); Redis官方还提供了 RedisVL for Java ，这是一个专为AI原生应用设计的Java客户端： // RedisVL for Java - 高级向量操作 VectorQuery query = VectorQuery.builder() .vector(embedding) .topK(10) .build(); List&lt;SearchResult&gt; results = vectorStore.search(query); 四、能力二：Vector Sets Redis 8的原生向量数据类型。 Redis 8引入了一个全新的数据类型—— Vector Sets 。 4.1 Vector Sets vs Query Engine的区别 对比维度 Query Engine Vector Sets 定位 在Hash/JSON上建索引 原生向量数据类型 使用方式 需要创建索引 直接用 VSIM 命令 适用场景 复杂混合查询 纯粹的相似度检索 Redis版本 7.x+ 8.0+ 4.2 使用方式 Vector Sets提供了两个核心命令： VADD ：向Vector Set中添加向量 VSIM ：在Vector Set中找到与指定向量最相似的现有元素 # 创建Vector Set并添加向量 VSIM my_vectors 查询向量 返回 10 Redis 8.8还进一步优化了向量存储的内存效率，新增的浮点精度控制选项 可实现最高92%的内存节省 。 对于大规模部署向量检索的团队，这意味着 基础设施成本的大幅下降 。 五、能力三：语义缓存 省Token、降延迟。 大模型调用最大的成本是什么？ Token 。 每次调用都要花钱，而且响应时间受网络和模型推理速度影响。 Redis给出的解决方案是 LangCache ——全托管语义缓存。 它的工作原理是：存储向量嵌入和缓存的LLM响应，通过相似度匹配来命中缓存，而不是精确匹配。 实测数据 ： 最高70%的成本节省 ——消除冗余LLM调用 15倍的响应速度提升 ——缓存命中时 比自建语义缓存 设置速度更快 在电商客服场景中，大量用户问的是“怎么退货”“什么时候发货”这类相似问题。 有了语义缓存，第一个用户问的时候调用了大模型，后面的用户问相似问题直接命中缓存。 六、能力四：Redis Iris AI Agent的上下文引擎。 这是2026年Redis在AI领域 最重要的一次发布 。 6.1 为什么需要上下文引擎？ Redis团队在官方博客里说了一句很关键的话： “Agent的问题不是智能不够，而是上下文不够” 。 Agent在工作流中需要跨系统、跨会话、跨时间地访问数据——CRM里的客户信息、文档库里的知识、实时事件流里的状态。 如果每次都要重新加载、重新拼接，Agent很容易在长任务中“迷失方向”。 6.2 Redis Iris是什么？ 2026年5月，Redis正式发布了 Redis Iris ——一个专门为AI Agent设计的上下文引擎。 Redis Iris是AI技术栈中的一个关键层，位于Agent和它需要的数据之间。它由五个核心工具组成： Redis Context Retriever ：让外部数据源可被Agent检索 Redis Agent Memory ：跨工作流和会话的记忆管理 Redis Data Integration ：实时数据集成 Redis LangCache ：语义缓存 Redis Search ：向量搜索 6.3 Agent Memory的双层架构 Redis Agent Memory采用**“双层”架构**来管理Agent的状态： 层级 功能 存储内容 短期记忆 当前会话上下文 对话历史、临时状态 长期记忆 跨会话持久存储 用户偏好、历史事实、知识图谱 这意味着Agent不仅能记住你上一句说了什么，还能记住你上周说过“喜欢简约风格”。 在Java中，可以用Lettuce配合DJL（PyTorch）构建Redis-backed Agent记忆层： // 使用Lettuce操作Redis Agent Memory // 工作记忆存储在Hash中 // 长期记忆存储在JSON中，并建立向量索引 // 事件日志存储在Stream中 七、Redis在AI场景下到底有多快？ 下面是Redis官方公布的几组关键性能数据： 场景 性能指标 向量插入 66,000次/秒（HNSW, 95%精度） 十亿级向量搜索 200ms中位延迟（90%精度） 亚毫秒级延迟 内存存储，毫秒级以下 JSON向量存储 最高节省92%内存 Streams吞吐 提升83% Sorted Sets 提升74% LangCache响应 缓存命中时15倍加速 Redis 8.8 GA版本也带来了更多的性能提升和更低的基础设施成本。 八、一张图看懂Redis AI完整链路 九、优缺点 优点 1. 性能极致 66,000次/秒的向量插入，十亿级数据200ms检索，亚毫秒级延迟。AI应用对延迟极其敏感，Redis的内存架构是天然优势。 2. 一站式AI数据基础设施 向量检索、语义缓存、Agent记忆、上下文引擎——所有AI应用需要的数据能力，Redis都有了。 3. 无需引入新的技术栈 如果团队已经在用Redis，不需要再额外学习和维护一套向量数据库。 4. 混合查询能力 向量相似度 + 传统过滤条件可以组合使用，这在业务场景中非常实用。 5. 成本优化显著 语义缓存可节省高达70%的LLM调用成本，向量存储最高可节省92%的内存。 6. 生态完善 官方提供RedisVL for Java、Spring AI集成、LangChain/LangGraph集成等丰富的客户端库。 缺点 1. 向量检索能力不如专用向量数据库 Redis的向量检索是“在Redis上叠加的能力”，而非从头设计的向量数据库。在极端规模（百亿级以上）和复杂索引策略上，可能不如Milvus等专用方案。 2. 内存成本 虽然Redis 8.8优化了内存效率，但向量数据全部放在内存中，成本仍然高于磁盘向量数据库。 3. Redis Stack已废弃 RediSearch模块已被整合到Redis 8中，但迁移需要一定工作。 十、适用场景 场景 推荐程度 理由 RAG知识库问答 ✅✅✅ 强烈推荐 向量检索+混合查询，延迟极低 AI Agent记忆 ✅✅✅ 强烈推荐 Redis Iris提供完整的Agent记忆方案 语义缓存 ✅✅✅ 强烈推荐 节省70%LLM成本，响应速度提升15倍 推荐系统 ✅✅✅ 强烈推荐 向量相似度检索，亚毫秒级响应 实时搜索 ✅✅✅ 强烈推荐 混合查询支持向量+标签+全文 已用Redis的团队 ✅✅✅ 强烈推荐 无需引入新组件 百亿级向量规模 ⚠️ 需评估 专用向量数据库可能更合适 更多项目实战在Java突击队网：susan.net.cn/project 十一、写在最后 回到最初的问题： Redis接入AI，到底接了什么？ 它不是“在Redis上加了个AI功能”，而是 把整个Redis变成了AI应用的数据基础设施 。 从Redis 7.4的向量搜索原生支持，到Redis 8的Vector Sets，到Redis 8.8的92%内存节省，到LangCache的70%成本降低，再到Redis Iris的完整Agent上下文引擎——2026年的Redis，已经不再是那个“只做缓存”的中间件了。 对于一个已经在用Redis的Java团队来说，这意味着 不需要引入新的技术栈 ，就能获得AI应用所需的数据能力。</description><pubDate>Mon, 31 Aug 2026 07:51:17 +0000</pubDate></item><item><title>公司没给活干，却因为员工看手机把人开了：法院判赔11万元</title><link>https://juejin.cn/post/7679402031226110004</link><guid isPermaLink="false">juejin-991811b148943e48</guid><description>看到这个新闻，很多打工人第一反应可能不是惊讶，而是后背发凉。 因为谁上班没看过手机？ 查看钉钉工作消息、回复客户信息、搜工作相关的资料，甚至只是上个厕所、接杯水，都可能被摄像头记录下来。可一旦公司想找你麻烦，这些原本很普通的动作，就可能被重新解释成“不认真工作”“工作态度消极”，甚至“严重违纪”。 南京这起案件里，技术工程师林某就是这样被公司开除的。 公司说他上班“摸鱼”，证据包括离座9分钟、和同事说话4分钟、迟开电脑8分钟、看手机、提前3分钟离岗等，一共14次。 最后，法院没有支持公司。 一审、二审都认为，公司没有证据证明林某的行为与工作无关，也没有达到严重违纪的程度。公司最终要支付工资和赔偿金，共计11万余元。 这个结果，打工人当然应该拍手。 但这件事真正值得讨论的，并不是“员工上班能不能看手机”，而是： 公司到底应该根据什么判断一个人有没有工作？ 看手机，不等于没工作 林某是做App产品相关工作的技术工程师。他在法庭上解释，手机是他的工作工具，需要用来调研竞品、体验产品，还要每天提交工作日报，记录自己的调研内容。 这个解释其实很正常。 现在很多工作，本来就不可能只发生在电脑前。做产品的人要体验App，做运营的人要看平台内容，做技术的人也可能需要测试移动端功能。 即便不是这些岗位，员工在工作时间看几分钟手机，也不能直接等同于偷懒。 问题在于，公司只证明了“他拿过手机”，却没有证明“他拿手机与工作无关”。 这两件事，中间差得很远。 公司如果想认定员工违纪，就不能只靠“我看见了一个动作”，还要拿出完整证据，证明这个动作确实影响了工作，而且严重到可以解除劳动合同。 否则，今天是看手机，明天可能就是去洗手间，后天可能就是和同事多说了两句话。 只要领导愿意，任何普通行为都可以被包装成问题。 公司没有活安排给你，却要求你一直表现得很忙 这起案件里，还有一个特别容易被忽视的细节： 林某结束外派回到公司后，公司没有给他安排具体任务，只要求他做竞品调研和日报告。 这就很尴尬了。 公司没有明确的项目，没有清晰的工作量，也没有具体的交付标准，却要求员工每天坐在工位上，看起来一直很忙。 那员工到底要怎么做？ 如果他一直盯着电脑，公司可以说他效率低、成果少；如果他拿手机调研，公司又可以说他在摸鱼；如果他和同事交流，公司说他工作时间闲聊；如果他离开座位，公司又说他不遵守纪律。 这种管理方式的核心不是让员工把事情做好，而是要求员工始终保持一种“正在努力工作”的姿态。 说白了，公司不关心你有没有产出，只关心你看起来像不像在工作。 这在很多职场里都很常见。 有的人手里的任务半小时就做完了，但不敢马上走，因为怕别人觉得他没事干。于是他只能打开一个文档，坐在电脑前继续耗时间。 还有的人明明已经完成了当天的工作，却不敢关电脑，因为公司默认“晚走的人更努力”。 时间久了，员工学会的不是提高效率，而是如何伪装忙碌。 这对公司没有任何好处。真正有能力的人，最后反而会变得小心翼翼，生怕自己的正常状态被误读。 “严重违纪”不能成为公司的万能理由 很多公司的员工手册里都会写着“严重违反规章制度，可以解除劳动合同”。 这句话本身没有问题。 员工确实不能长期旷工、拒绝工作、故意损害公司利益。如果一个人真的严重失职，公司依法处理是合理的。 但现实里，有些公司特别喜欢把“严重违纪”当成一张万能牌。 只要员工不符合管理者的期待，就往这张牌上靠： 晚到几分钟，少坐一会儿，看几眼手机，工作成果不好，算严重违纪； 更离谱的和领导意见不一致，也可能被算成态度问题。 可“严重”不是公司说了算。 一个人离座9分钟，和一个人连续旷工几天，显然不是同一个性质。上班看手机几分钟，和拿公司时间长期打游戏，也不能放在一起比较。 更重要的是，公司在解除劳动合同前，至少应该把事情调查清楚，听听员工怎么解释，而不是先决定开除，再去收集能够支持开除的材料。 如果员工说自己是在做竞品调研，公司就应该核实日报、工作成果和具体内容。 如果公司连这些都没有核实，就直接把“看手机”认定为“摸鱼”，那它处理的就不是违纪，而是自己的主观猜测。 法院保护的不是摸鱼，而是普通人的体面 有些人看到法院判决后会说： “那以后员工是不是可以随便玩手机了？” 不是。 法院并没有说员工可以借工作之名随意玩手机，也没有说公司不能管理工作纪律。 法院真正否定的是一种简单粗暴的逻辑： “我在监控里看到你看手机，所以你就是在摸鱼；你就是严重违纪；我就可以开除你。” 这条逻辑如果被认可，职场就会变得非常可怕。 因为员工永远没有办法证明自己每一次看手机都在工作。你可能刚好在看客户消息，也可能刚好在查资料，还可能只是休息两分钟。 如果公司只需要提供一段监控视频，就能把员工的行为定性为严重违纪，那么员工在公司里就没有任何安全感。 他不是在工作，而是在接受实时审判。 这起案件给打工人的最大安慰，是法院没有接受这种推理。 法院要求公司拿出更完整的证据，也要求公司承担相应的管理责任。不能因为员工有几个动作“不够像在工作”，就剥夺他继续工作的权利。 打工人也别只顾着生气，要留下证据 林某能够打赢这场官司，一个重要原因是他每天都有提交工作日报，能够说明自己做了什么，也能解释为什么使用手机。 这对很多打工人来说，反而是一个很现实的提醒。 如果公司没有给你安排清晰任务，最好通过邮件、工作群或日报留下记录。你今天完成了什么、正在推进什么、卡在哪里，都尽量写清楚。 如果工作需要使用手机，也不要只停留在口头解释。把调研内容、测试结果、客户沟通记录留下来。 不是因为我们应该小心翼翼地活着，而是现实中，一旦发生争议，最后能说话的往往不是你的记忆，而是这些工作痕迹。 职场里最吃亏的一类人，就是明明做了很多事情，却没有留下任何记录。等公司一句“你没干活”扣过来，他只能反复解释：“我真的做了。” 但劳动争议不是比谁声音大，而是看谁能拿出证据。 写在最后 个人看法，我不支持员工上班长期玩手机，也不认为所有“摸鱼”都应该被原谅。 但我更反对公司把员工当成监控画面里的一个坐标，只要发现他没有持续盯着电脑，就开始怀疑他是不是在偷懒。 工作不是坐牢。 一个人短暂离开座位、看几分钟手机、和同事交流几句，本身都不能证明他没有完成工作。判断员工是否合格，最终还是要看任务有没有完成，结果是否达标，而不是看他一天有多少时间保持着“认真工作的姿势”。 公司如果没有任务可分配，却要求员工一直坐在那里装忙；没有实际损失，却拿几分钟离座和看手机当成开除理由；不沟通、不核实，直接把员工推上“严重违纪”的位置。 那它真正缺少的，可能不是一套更严格的员工手册，而是最基本的管理能力。 这11万元赔偿，赔的不只是林某的工资和损失。 它也在提醒所有打工人： 你可以对工作负责，但不需要为每一个喝水、离座、看手机的瞬间证明自己没有犯罪。 而那些习惯用监控寻找员工错误的公司，也应该明白： 员工不是因为摄像头一直拍着，才会认真工作。一个真正值得信任的团队，靠的是明确的任务、合理的评价和彼此之间最起码的尊重。</description><pubDate>Mon, 31 Aug 2026 02:49:14 +0000</pubDate></item><item><title>豆包工作Agent正式发布，直接给到夯。</title><link>https://juejin.cn/post/7679623224678613034</link><guid isPermaLink="false">juejin-cacc01d696679081</guid><description>大家好，我是二哥呀。 昨天有读者在评论区留言说，“我觉得豆包工作会很快起来，并且到第一！因为飞书+豆包这个体系我感觉确实是很能打。” 你别说，深度体验了一天后，深有同感。 豆包工作，一个全新 AI 办公 Agent，能自己拆解任务、操作你的电脑、直接把结果写进飞书云文档。 你说一句话，它干完交活。 跟飞书是原生打通的，企业文档、群聊、会议、任务上下文都能继承。 我在本地已经下载安装了。 首次打开的界面非常清晰，非常简洁，是我喜欢的样子。 这是豆包工作的官网： www.doubao.com/work 让我惊喜的是，豆包工作有一个多端同步功能，值得小吹一下。 它有两种工作模式——本地电脑和云电脑。 本地电脑就是你的本地电脑了（皮。云电脑就是豆包工作在云端帮你开的一台虚拟机，24 小时在线。 什么意思呢？ 就是你本地的电脑关机了、睡眠了，任务照样可以切换到云电脑继续跑。 相当于我用了豆包工作，还能额外获得一台云服务器？ 对，你没理解错。 用法很简单，工作任务选择本地电脑或者云电脑就行了。 豆包 APP 端也是可以同步的。比如说，你在手机端把工作任务从云电脑切换到本地电脑的时候，上下文也可以共用。 注意看，这是我的手机端。我一开始用的是云电脑，现在切换到了本地电脑。 我电脑上的豆包工作也跟着从云端自动切换到了本地电脑。 换句话说，你可以在手机端派活，也可以在电脑端派活，豆包工作的执行环境可以选云端，比如说你需要操作飞书文档这种不需要本地环境的任务；也可以选你本地的电脑，如果你的任务是需要操控本地电脑的话，比如说编辑本地文件。 再换句话说，有没有电脑，你都可以随时随地派活给豆包工作，让它为你做牛做马，哈哈。 来吧来吧，让牛马开始给我们干活吧。 01、一句话派活 关注我的读者应该知道，我这周上架了一款产品——派简历，代码完全开源，一个基于 AI 的智能简历优化工具。 GitHub地址： github.com/itwanger/Pa… 产品刚上线，需要一手宣传文案，外加一手 GitHub 主页。 刚好这件事可以交给豆包工作来干。 「帮我写一篇派简历的宣传文档，我的产品地址是 resume.paicoding.com，重点突出 AI 智能优化和无损一页这两个卖点。写完输出到飞书文档。然后再帮我更新一下 GitHub 主页的 README，要求有产品的截图，产品介绍，要尽量美观大方。」 豆包工作拿到任务后，自己做了拆解。 第一步，它先去官网浏览了一遍产品页面。我看它把首页的功能介绍、使用流程、技术栈这些信息都扒了下来。信息搜集这一步做得挺细的。 然后开始组织文案。它给文档分了三个板块——核心卖点、实用功能、用户真实评价。每个板块下面还有几条要点，层次很清晰。 我翻了一遍，产品卖点基本都抓到了，AI 智能优化和一键生成放在了最前面，优先级也对。 不过这里我需要提一点点小小的建议，既然和飞书打通了，能不能我登录了豆包工作，在豆包工作里打开飞书链接的时候直接就帮我登录好。 接着，豆包工作自动切到了第二个任务——更新 GitHub 主页。 它在我电脑上找到 GitHub 仓库的readme进行修改，完事后直接帮我 commit（我用的是 Auto 模式）。我看了一下改动，格式和其他项目完全一致，很正宗。 整个过程我就输入了一段话，中间没切过任何窗口，所有操作都在豆包工作里完成。 包括截图。 这也是我觉得豆包工作和其他 work Agent 的最大区别——交付的是成果。飞书文档直接生成好，不用我复制粘贴再排版；GitHub 直接改好并提交，不用我自己开终端敲命令。 完美。 这个任务并不复杂，但其实最能体现一个 Agent 的自主能力，能不能在不需要人类干预的情况下，自己去拆解任务、自己去找信息、自己去组织内容、自己去执行操作。 02、反推 Skill + Seedance 创作 我所有文章的配图都是用 image2 生成的，很多很多读者都想要这种风格，希望我能开源。 那等我整理完，就开源到GitHub。 这里先教大家一个邪修的办法，正好试试豆包工作能不能帮我搞定。 思路很简单。 把一张我文章的配图丢给豆包工作，让它帮我反推出风格描述和可复用的 Skill。 「看看这张图片，帮我拆解它的风格特征——配色方案、构图方式、元素类型、字体风格、背景处理这些，然后整理成一个可复用的 Skill。我希望以后输入不同的主题和关键词，就能生成类似风格的配图。」 豆包工作的图片理解能力给了我一个小惊喜。 它把配图的风格拆得挺细，我摘几条它分析出来的内容： 我详细拆解了图片的配色方案，确定主色为深黑 #1A202C，辅助色为橙色 #DD6B20，背景用纯白，模块填充为极淡同色系，整体饱和度中等偏低，呈现清新手绘感。 识别出包含手绘粗边框圆角矩形、虚线框、3D 立方体等多种视觉元素，同时明确底部总结语、角落装饰的设计形式，以及整体平衡对称、留白充足的布局特点。 接着它帮我整理了一份完整的 Skill，包含了配色模板、构图规则、元素选用标准、文字排版约束。 然后我们就用这个 Skill 直接生成新的配图。 大家感受一下，我是觉得挺不错的。 豆包工作内置了字节自研的 Seedance 和 Seedream 模型。Seedance 生成视频，Seedream 生成图片。不用跳到别的工具，在对话里就能直接调用。 接下来，我们就用刚才反推的 Skill，让 Seedream 按截图占位符的规格生成一张新配图： 「用刚才的配图 Skill，帮我生成一张风格为 data-board 的配图，主题是『豆包工作核心能力一览』，关键词：多端同步、云电脑、Skill 调用。尺寸 16:9。」 效果我给 95 分。 少的 5 分是希望豆包工作不要太骄傲，继续精进，哈哈。 整体风格很对味，配色也很到位，卡片式布局的感觉也出来了。 正所谓授之以鱼不如授之以渔。 此处应该有掌声👏。 03、数据处理 自从好友苍何给我安利了飞书多维表格后，我修改简历的数据都记录在了上面，目前已经有 1000 多条记录了。 这也是我的功劳薄了，以后给孩子吹牛逼的时候，也可以自信的说，「别看你爸这么普通，当年可是给全国所有高校的同学都改过简历，包括北京大学、浙江大学这种顶级名校。」 里面记录了教育背景、截图、92标签、哪一届等信息。 正好可以拿来试试豆包工作的数据处理能力。 「帮我分析一下简历修改记录。先做一个柱状图，看看 985、211、非 92 的数量分布。再来一个饼图，看看 26届和 27届的占比情况。」 豆包工作直接读取了飞书文档里的数据。 甚至自己安装了 matplotlib，这一步的体验让我很舒服。 因为飞书和豆包工作是原生打通的，数据直接就能读，我不需要先把飞书文档导出成 CSV，再上传到对话框里。 柱状图先出来了，一目了然。 饼图跟着就到，27届占比多，主要是我从今年才开始用飞书多维表格，去年 26 届没怎么记录。 还有一部分是工作党。 有一说一，数据处理这块，豆包工作最大的优势就是和飞书的原生打通。 数据就在飞书里，豆包工作能直接读取、直接分析、直接出图表，整个流程没有「导出-上传」的断层。对于数据本来就在飞书的团队，这个体验实在是太棒了。 像我，基本上所有需要存的数据，都会优先使用飞书多维表格和云文档。 第一，可以通过飞书 CLI 和 Agent 打通。 第二，飞书的多维表格支持各种状态的修改，包括机器人、各种自动化。 第三，有了豆包工作，以后更离不开了。 04、后台运行 + 随时打断 这两个功能放在一起聊，因为它们解决的是同一个痛点——你不用干等着 Agent 跑完。 豆包工作支持小窗模式，可以把它缩到屏幕角落，Agent 在后台继续跑任务，你该干嘛干嘛。 比如我让它分析数据生成图表的时候，可以直接切到 VSCode 继续写教程了。等它跑完会弹一个通知，我再切回去看结果就行。 另一个让我觉得很舒服的功能是随时打断。 豆包工作在执行任务的过程中，我们可以随时插一嘴补充信息，不用等它跑完再重新来。 比如我让它帮我写一份工作周报，在它干活的时候可以继续插一嘴【要让老板感受到我干活的诚意。】 豆包工作就自然地把补充的信息融入上下文。 再给兄弟姐妹们看一下豆包工作帮我完成的周报哈，真的，哪怕是再挑剔的老板，看了都觉得「哇，这位同学真是勤快，干活有诚意。」 完成豆包多轮对话上下文理解优化方案设计，组织需求评审并通过；明确交互逻辑、埋点方案与验收标准，已进入开发排期 梳理近两周用户反馈共 1,200+ 条，定位 TOP5 问题（上下文丢失、回答冗长、格式错乱等），输出问题归因分析与改进建议文档 与算法团队对齐新一轮模型迭代节奏，完成新能力（长文档理解增强）的灰度测试方案设计，确定灰度比例、观测指标与回滚机制 ending 深度使用了一天豆包工作，我最大的感受是—— 之前用其他的 AI 办公工具，总得在下指令和动手干之间来回切。搞得最后还不如自己动手。 相信有小伙伴会有这种体会哈，明明 Agent 是来帮我们提效的，结果累的半死。 尤其是，你们团队的文档没有和这个 Agent 打通的情况下，那真的是要了老命。 豆包工作把中间这层搬运工作给省了。数据从飞书里读，文档往飞书里写。再加上云电脑 24 小时在线、手机端随时同步，这个体验才是办公 Agent 该有的样子。 不得不说，现在的产品，真的是越来越贴心了。 贴心到你根本没办法拒绝，好吧？ 团队有需求的兄弟姐妹，可以去下载试试。 顺带写份让老板都觉得赏心悦目的周报吧</description><pubDate>Sun, 30 Aug 2026 23:59:07 +0000</pubDate></item><item><title>「速通Shell」Shell 编程性能优化</title><link>https://juejin.cn/post/7678975240300773428</link><guid isPermaLink="false">juejin-e50d157861783091</guid><description>前面几篇讲了 shell 的语言特性、错误处理、模块化、测试、信号处理，又用三篇讲了常用命令。脚本能跑、能维护、能上线，到这一步很多人就觉得够用了，但 真正把脚本从能跑推到专业还有最后一关——性能 。 很多人对 shell 性能有误解。一种觉得&quot;shell 不就是跑命令嘛，能快到哪去&quot;；另一种觉得&quot;性能有问题就上 Python&quot;。两种看法都不对，shell 脚本和 Python 性能差距没那么夸张，很多时候差的是写法。 把&quot;慢 shell 脚本&quot;重写成&quot;快 shell 脚本&quot;，往往只是几个习惯的改变 。 这一篇我们系统讲 shell 性能优化的思路和具体技巧。读完你应该能做到：写脚本时心里有根弦，知道哪些写法是性能陷阱，知道怎么用 awk、bash 内置、并行化等手段加速。 一、shell 性能的基本法则 在讲具体技巧之前，先建立一个认知： shell 性能问题的根源是 fork 。 什么是 fork？每当你调一个外部命令，shell 就要 fork 一个子进程去执行它。fork 本身的开销其实不大（微秒级），但 bash 里 fork 还要 exec 系统调用、加载动态库、初始化 stdio——整个过程通常要几毫秒。 这就带来了 shell 性能的第一法则： 避免不必要的 fork 。一个几毫秒的开销看起来不多，但脚本里如果有几千次循环，每次循环 fork 一次——几秒钟就过去了。 真实差距有多大？一个简单的例子：统计文件行数。 # 写法一：wc time wc -l bigfile.txt # 0.005s # 写法二：循环 read time while read line; do ((count++)); done &lt; bigfile.txt # 0.8s 同一个文件，wc 比 bash 循环快 160 倍。这就是 fork 和不 fork 的差距。 理解了&quot;fork 是慢的&quot;这一点，下面的所有技巧都是围绕它展开的。 二、避免不必要的 fork 2.1 永远不要 cat file | grep 这是 shell 圈最经典的&quot;反模式&quot;： # 反模式：cat + 管道 + grep time cat file.txt | grep &quot;pattern&quot; &gt; result.txt # 0.020s # 正确：grep 直接读文件 time grep &quot;pattern&quot; file.txt &gt; result.txt # 0.010s 反模式创建了两个进程（cat + grep），正确写法只创建一个（grep）。 性能提升 2 倍 。 类似的反模式还有： # 反模式：cat 多文件 + grep time cat file1 file2 file3 | grep &quot;pattern&quot; # 0.030s # 正确：grep 接受多个文件 time grep &quot;pattern&quot; file1 file2 file3 # 0.012s 能用命令直接读文件，就不要 cat。 2.2 用 ( &lt; f i l e ) 代替 (&lt;file) 代替 ( &lt; f i l e ) 代替 (cat file) 把文件内容读进变量： # 写法一：cat + 命令替换 content =$(cat file.txt) # fork cat # 0.020s # 写法二：bash 内置 content =$(&lt;file.txt) # 无 fork # 0.001s $(&lt;file) 是 bash 的&quot;读文件到变量&quot;语法，不调用任何外部命令，性能差 20 倍。 这个技巧在你需要把整个文件读进变量处理时特别有用。 2.3 用 read 代替 awk 取字段 如果只是按分隔符切几列， 用 read 比 awk 快 ： # 写法一：awk time awk -F: &apos;{print $1}&apos; /etc/passwd &gt; users.txt # 0.015s # 写法二：read + bash 内置 time while IFS=: read -r user _; do echo &quot; $user &quot; ; done &lt; /etc/passwd &gt; users.txt # 0.030s 写法二反而更慢？这是因为 bash 循环的开销超过了 awk 一次调用的成本。awk 是用 C 写的，处理几百万行比 bash 循环快。 所以这个例子的真实结论是： 对大量数据用 awk，对少量数据用 read 。前面字符串那篇讲过这个权衡，这里再强调一次。 2.4 字符串处理用 bash 内置而不是 sed 简单的字符串操作，bash 内置比 sed 快很多： # 写法一：sed time echo &quot;hello world&quot; | sed &apos;s/world/shell/&apos; # 0.005s # 写法二：bash 变量展开 time str= &quot;hello world&quot; ; str= &quot; ${str/world/shell} &quot; ; echo &quot; $str &quot; # 0.0001s 性能差 50 倍。 简单的字符串替换、删除前后缀，bash 变量展开永远比 sed 快 。 复杂的正则匹配才用 sed/awk。判断标准：能用 ${var//pattern/replacement} 解决的，就不要用 echo &quot;$var&quot; | sed 。 2.5 数组长度用 ${#arr[@]} 而不是 wc # 写法一：wc time echo &quot; ${arr[@]} &quot; | wc -w # 0.020s # 写法二：bash 内置 time echo &quot; ${#arr[@]} &quot; # 0.0001s ${#arr[@]} 是 bash 内置的数组长度——不 fork 任何进程。 2.6 测试用 [[ ]] 而不是 [ ] # 写法一：[ ]（test 命令） time for i in { 1. .1000 }; do [ &quot;$i&quot; -gt 500 ] &amp;&amp; true ; done # 0.300 s # 写法二： [[ ]] （bash 内置） time for i in { 1. .1000 }; do [[ &quot;$i&quot; -gt 500 ]] &amp;&amp; true ; done # 0.005 s [ ] 实际上是 fork 一个 test 进程； [[ ]] 是 bash 内置关键字。1000 次循环差了 60 倍。 日常用 [[ ]]，[ ]` 只在需要 POSIX 兼容时用。 三、bash 内置 vs 外部命令 shell 命令分两类：bash 内置和外部命令。 内置：bash 自身实现，不 fork。cd、echo、read、test、printf、declare、set、export、let、local 都是。 外部命令：独立可执行文件，每次调用都 fork。ls、cat、grep、sed、awk、wc、sort 都是。 原则：能用内置就用内置 。下面是常见的内置替身对照表： 操作 外部命令（慢） bash 内置（快） 性能差 字符串长度 echo &quot;$s&quot; | wc -c ${#s} 100x 数组长度 echo &quot;${arr[@]}&quot; | wc -w ${#arr[@]} 100x 子串提取 echo &quot;$s&quot; | cut -c1-5 ${s:0:5} 50x 字符串替换 echo &quot;$s&quot; | sed &apos;s/a/b/&apos; ${s/a/b} 50x 数值计算 expr $a + $b $((a + b)) 20x 条件测试 [ &quot;$a&quot; -gt 0 ] [[ &quot;$a&quot; -gt 0 ]] 60x 读文件 $(cat file) $(&lt;file) 20x 文件存在 [ -f file ] [[ -f file ]] 50x 记住这张表，写脚本时优先用内置。 四、awk/sed 是性能加速器 前面说&quot;用 bash 内置比外部命令快&quot;，但awk 是例外。 awk 一次调用就能完成 bash 循环 + 多次命令调用的事。 对大量数据，awk 几乎总是比 bash 循环快一个数量级 。 4.1 awk 替代循环 # 写法一：bash 循环统计 time while read line; do ((count++)); done &lt; bigfile.txt # 2.5s # 写法二：awk time awk &apos;END{print NR}&apos; bigfile.txt # 0.05s awk 跑了几十倍快。这是 awk 用 C 写的 + 一次调用处理整个文件的红利。 再看一个例子——按列求和： # 写法一：bash 循环 time awk &apos;{sum += $3} END {print sum}&apos; data.txt &gt; result.txt # 写法二：纯 bash time sum =0; while read -r a b c rest; do ((sum += c)); done &lt; data.txt; echo $sum &gt; result.txt # 写法一：0.05s # 写法二：2.0s awk 快了 40 倍。 4.2 什么时候用 awk awk 适合 ： 处理大文件（MB 级别以上） 按列处理数据 需要做统计（计数、求和、平均） 复杂的字段操作 awk 不适合 ： 简单的字段切分（小文件用 read 就够） 复杂的逻辑判断（bash 写起来更清晰） 需要调用大量其他命令的场景 一般来说，1000 行以下 bash 循环能搞定，1000 行以上用 awk。 4.3 sed 适合什么 sed 适合&quot;按行批量修改&quot;—— s/old/new/g 、 d 删除、 p 打印。 对于模式匹配的替换，sed 比 bash 循环快得多 。 # 写法一：bash 循环 + 字符串替换 time while read line; do echo &quot; ${line/old/new} &quot; ; done &lt; file.txt # 写法二：sed time sed &apos;s/old/new/g&apos; file.txt # 写法一：2.0s # 写法二：0.05s 五、避免循环里的重复工作 很多时候脚本慢不是因为单个操作慢，而是循环里的小开销被放大了一千次。 5.1 缓存常用的命令结果 # 反模式：每次循环都算一次 for file in *. log ; do today=$( date +%Y%m%d) count=$( wc -l &lt; &quot; $file &quot; ) echo &quot; $file : $count lines, today: $today &quot; done # 优化：提到循环外 today=$( date +%Y%m%d) for file in *. log ; do count=$( wc -l &lt; &quot; $file &quot; ) echo &quot; $file : $count lines, today: $today &quot; done date 调用一次 0.005s，1000 个文件就 5s。提到循环外省 5s。 类似的还有： 配置文件读一次，不要每个函数都重读 常量字符串用变量，不要每次拼接 路径解析一次，不要每次都 cd + pwd 5.2 用关联数组替代 grep 判断&quot;某个值在不在列表里&quot;： # 写法一：每次都 grep time for name in &quot; ${names[@]} &quot; ; do if grep -q &quot;^$name$&quot; allowed.txt; then echo &quot; $name allowed&quot; fi done # 5s # 写法二：先把 allowed 读进关联数组 time declare -A allowed while read -r line; do allowed[ $line ]=1; done &lt; allowed.txt for name in &quot; ${names[@]} &quot; ; do if [[ -n &quot; ${allowed[$name]} &quot; ]]; then echo &quot; $name allowed&quot; fi done # 0.5s O(N*M) 变成 O(N+M)，性能差 10 倍。 5.3 减少文件 IO 文件读写是相对慢的操作， 能一次读的就不要分多次读 ： # 反模式：每次循环都读文件 for id in &quot; ${ids[@]} &quot; ; do name=$(grep &quot;^ $id :&quot; /etc/passwd | cut -d: -f5) echo &quot; $id : $name &quot; done # 优化：一次读完整个文件 declare -A names while IFS=: read -r id _ _ _ name _; do names[ $id ]= $name done &lt; /etc/passwd for id in &quot; ${ids[@]} &quot; ; do echo &quot; $id : ${names[$id]:-unknown} &quot; done 文件读一次比读 1000 次快得多。 六、并行化 如果脚本里有独立的任务，需要处理多个文件、调多个 API、查多个数据库，并行化能把多核机器的性能充分利用起来。 6.1 &amp; + wait：基础并行 # 串行：5 个任务各 1 秒 time for host in web1 web2 web3 web4 web5; do ssh &quot; $host &quot; uptime done # 5s # 并行：5 个任务并发 time for host in web1 web2 web3 web4 web5; do ssh &quot; $host &quot; uptime &amp; done wait # 1s &amp; 把任务放后台， wait 等所有后台任务完成。 5 倍提速 。 6.2 xargs -P：更可控的并行 xargs 的 -P 参数指定并行进程数， 比手写 &amp; 灵活 ： # 8 个进程并行处理 time find . -name &quot;*.jpg&quot; | xargs -P 8 -I {} convert {} -resize 50 % {}.small.jpg # 串行：40s # 并行（8 核）：5s 8 倍提速 。 xargs -P 适合&quot;批量独立任务&quot;，每个任务处理一个文件。 6.3 GNU parallel：最强大的并行 GNU parallel 是 xargs 的超集，支持并行、进度显示、结果合并。 # 安装 apt install parallel # Debian/Ubuntu yum install parallel # CentOS # 并行压缩所有 .log time find . -name &quot;*.log&quot; | parallel gzip {} # 8 倍提速 # 显示进度 find . -name &quot;*.log&quot; | parallel --progress gzip {} # 收集结果到文件 find . -name &quot;*.log&quot; | parallel --result output/ gzip {} parallel vs xargs -P ： xargs 简单、轻量、内置 parallel 功能多、显示进度、跨平台一致性更好 日常用 xargs 够用，复杂场景上 parallel。 6.4 并行化的几个坑 坑一：输出会乱 。多个进程同时输出到 stdout，结果会交错。 解决：每个任务输出到独立文件，最后合并 ： for host in web1 web2 web3; do ssh &quot; $host &quot; uptime &gt; &quot;/tmp/uptime_ $host .txt&quot; &amp; done wait cat /tmp/uptime_*.txt rm /tmp/uptime_*.txt 坑二：进程数太多反而慢 。100 个并发把 CPU 跑满、磁盘 IO 拥堵。 经验：CPU 密集用 N=核数，IO 密集用 N=10-50 。 坑三：日志混乱 。多个任务同时写同一个日志文件，输出会错乱。 每个任务独立日志 。 七、性能分析工具 优化之前要先知道&quot;哪里慢&quot;。这一节讲几个常用的性能分析工具。 7.1 time：基本计时 time ./script.sh # real 0m5.123s # 实际耗时 # user 0m3.456s # 用户态 CPU 时间 # sys 0m0.234s # 内核态 CPU 时间 real 是实际等待时间， user + sys 是 CPU 占用时间。如果 real 远大于 user + sys，说明在等 IO（网络/磁盘）。 7.2 strace：追踪系统调用 # 看脚本调用了哪些系统调用 strace -c ./script.sh # 看具体的调用 strace -e trace=open, read ,write ./script.sh strace 能告诉你脚本在等什么，是 fork 太多、还是磁盘 IO、还是网络。 7.3 perf：CPU 性能分析 # 记录 CPU 事件 perf record ./script.sh perf report perf 能告诉你CPU 时间花在了哪些函数上。对 shell 脚本来说，主要看 awk、grep 这些外部命令的调用次数。 7.4 bash 内置的时间 # bash 内置的 time，精度更高 TIMEFORMAT = &apos;real %3R, user %3U, sys %3S&apos; time ./script.sh 这个 time 是 shell 关键字，不会 fork /usr/bin/time。 八、实例：把一个慢脚本加速 我们看一个真实的慢脚本案例，看怎么一步步优化。 原始版本 （处理 1000 个日志文件，统计错误数）： #!/bin/bash for file in *. log ; do count=$( cat &quot; $file &quot; | grep &quot;ERROR&quot; | wc -l) echo &quot; $file : $count errors&quot; done 性能 ：1000 个文件，每文件 0.05s，总共 50s。 优化第一步：去 cat for file in *. log ; do count=$(grep -c &quot;ERROR&quot; &quot; $file &quot; ) # grep -c 直接计数 echo &quot; $file : $count errors&quot; done 性能 ：每文件 0.04s，总共 40s，提速 1.25x。 优化第二步：循环里避免重复的 cat total=0 for file in *. log ; do count=$(grep -c &quot;ERROR&quot; &quot; $file &quot; ) echo &quot; $file : $count errors&quot; total=$((total + count)) done echo &quot;总计: $total &quot; 性能 ：基本没变化，但 代码更清晰 。 优化第三步：并行化 total=0 for file in *. log ; do ( count=$(grep -c &quot;ERROR&quot; &quot; $file &quot; ) echo &quot; $file : $count errors&quot; &gt;&gt; /tmp/results.txt echo &quot; $count &quot; ) &amp; done &gt; /tmp/counts.txt # 等所有任务完成 wait # 汇总 total=$(awk &apos;{s+=$1} END {print s}&apos; /tmp/counts.txt) echo &quot;总计: $total &quot; rm -f /tmp/results.txt /tmp/counts.txt 性能 ：8 核机器，6.25s，提速 8x。 优化第四步：换思路，整个流程用 awk # 一次 awk 调用处理所有文件 total=0 for file in *. log ; do count=$(awk &apos;/ERROR/{c++} END{print c+0}&apos; &quot; $file &quot; ) total=$((total + count)) echo &quot; $file : $count errors&quot; done echo &quot;总计: $total &quot; 性能 ：每文件 0.05s，总共 50s。awk 一次只处理一个文件，没有优势。 真正的优化：批处理 # 把所有日志合并后一次处理 cat *. log | awk &apos;/ERROR/{count++} END{print &quot;总计:&quot;, count+0}&apos; 但这样拿不到每个文件的统计——所以保留每个文件单独统计 + 并行化是最佳方案。 最终性能对比： 原始：50s 去 cat：40s 并行：6.25s 总提速 8x。这就是 shell 性能优化的实际效果，主要靠并行化，单点优化收益有限。 九、总结 shell 性能优化是个细节活，核心思路是： 避免不必要的 fork ：能内置就别调外部命令 bash 内置优先 ：变量展开、 [[ ]] 、 ${#arr[@]} 都很便宜 awk 适合处理大数据 ：一次调用处理整文件 并行化是最大提速 ：CPU 密集任务用核数倍并行 先 profile 再优化 ：用 time/strace/perf 找瓶颈 shell 性能问题通常不是shell 语言慢，而是用法不对，很多慢脚本改改写法就快 10 倍。 性能优化聊完，整个 shell 编程系列的主题到这里就比较完整了。从语言特性到工程实践，从常用命令到性能调优，shell 编程需要掌握的核心内容基本覆盖了。</description><pubDate>Sat, 29 Aug 2026 08:59:53 +0000</pubDate></item><item><title>我的开源项目登顶 GitHub 趋势榜 No1 了！</title><link>https://juejin.cn/post/7678618678956163078</link><guid isPermaLink="false">juejin-fa2503fe53a843d0</guid><description>这是苍何的第 585 篇原创！ 大家好，我是苍何。 兄弟们，今天是一个非常值得纪念的时刻。 我的 GitHub 开源项目冲上全球趋势榜 No 1 了。 GitHub 趋势榜地址： github.com/trending 项目地址： github.com/freestylefl… 我真的没想到过，一个独立开发者搞出来的个人项目，能有机会能登顶趋势榜。 同台竞争的是来自全球最顶尖的开发者和团队，以及超级多的牛逼的开源项目。 而今天的热榜来自一名普通的不能再普通的中国开发者。 或许，这就是 AI 时代吧，一切皆有可能。 要不是江树昨天和我说，我的项目登上 GitHub 趋势榜 top 2，我还真没关注，只知道 star 涨的很快。 我一看，还真是： 然后，我就发了一条帖子，很多好朋友帮我转了一波（跪谢大哥们），然后就直接快 60 万曝光。 GitHub一天就涨了近 1700 的 star，然后隔着一天我看就直接冲上 GitHub 趋势榜第一了。 一切仿佛那么不真实。 你可能觉得我在装逼或者干啥的，其实我现在已经不介意了，因为这一刻，我想我平淡无奇的人生中，是为数不多可以和我孩子吹牛逼的东西。 他的爸爸，做了一辈子的程序员，没想到靠 AI，有希望能被全球的开发者看到，他的爸爸真的有很努力，虽然很多时候，努力也没用。 这个开源项目，我是从今年 4 月份开始做的，那个时候 OpenAI 刚发布了最强的图片模型 GPT-Image-2。 我就很激动，然后连夜搞了搜集了很多提示词，并做了一些模板。 这几个月以来，我几乎每天都会有更新。 网站也不知道迭代了多少个版本，虽然最后呈现的审美我也不能说有多好，但已经是烧光了我一个直男为数不多的审美细胞。 网站可以直接生图测试，但我之前买的中转倒闭了，导致现在生图还有些问题。 说实话，搜集是一件很枯燥缺很耗时的事情，很多人都可以做，但很少有人能持续长期的在做同一件事。 特别是当这件事，如果带来不了收益的情况下。 我觉得我能坚持的一部分原因，是源于热爱吧，当我看到来自全球的伙伴用 GPT-Image-2 设计出那么好看的图片时，我是非常兴奋的。 很多人也主动提交了很多有价值的，值得参考的提示词，他们的作品得到了曝光，也给他们带来了增长。 它更像是一个小广场。 有人把自己的作品放进来，有人在这里找到灵感，也有人因为一次提交，第一次被更多人看见。 我只是最早把桌子支起来的人，后来，是一群素未谋面的人，把它一点点摆满了。 所以，今天真正让我开心的，早已超出了 GitHub 趋势榜第一这个名次本身。 榜单很快会变，热度也一定会退。 可能过几天，就没有多少人记得这个项目曾经登顶过。 但这件事至少让我确认了一点： 在 AI 时代，一个普通人只要愿意持续做一件对别人有用的事，就有机会绕过学历、公司、城市和资源的限制，直接被这个世界看见。 以前我总觉得，做出一个能被全球用户使用的产品，是大公司和天才程序员的事情。 一个普通开发者，没有团队、没有资金、没有什么资源，能把自己的一亩三分地折腾明白，就已经不错了。 但 AI 正在改变这件事。 它让一个人拥有了过去一支小团队才有的能力，也让一个原本不起眼的想法，可以用更低的成本，变成真正能被人使用的产品。 当然，我并不想把这次总结成一句“只要努力，就一定会成功”。 那太鸡汤了。 这次能够登顶，有时机，有运气，也有很多朋友的帮助。 努力从来不保证结果。 但在运气到来之前，我们至少要先做出一个值得被看见的东西，并且让自己在牌桌上待得足够久。 AI 可以帮你写代码，做页面，翻译内容，甚至帮你完成很多过去不会做的事情。 但它替代不了你在没人看见的时候继续更新，替代不了你把一个小问题反复打磨几个月，也替代不了你真心想为别人创造一点价值。 AI 降低了创造的门槛，却没有降低坚持的门槛。 而人与人之间真正的差距，可能也正在这里重新拉开。 我们大多数人都经历过那种时刻： 写的第一篇文章没人看，做的第一个产品没人用，开源的第一个项目也没人点 star。 你不知道自己做的事情究竟有没有意义，也不知道还要坚持多久，才会等来一个结果。 就像我当初写公众号默默无闻，连续日更了好几个月，每天雷打不动。 那种感觉，就像往一口很深的井里扔下一颗石头，等了很久，也听不到回声。 我也有过很多这样的时刻。 只是这一次，那颗扔了几个月的石头，终于传回了回声。 所以，如果你也正在做一件暂时没有收益、没有掌声，甚至没有人理解的事情，我想把这次经历送给你： 不要因为暂时没有回声，就怀疑自己发出的声音。 先认真做完，持续做好，然后公开地把它交给世界。 你永远不知道，哪一次更新、哪一个作品，会穿过茫茫人海，被一个素不相识的人看见。 至于我，这个第一不会让我停下来。 榜单只是一天的，项目却还要做很久。 接下来，我还是会继续补充高质量的提示词，优化网站体验，修复生图功能，也欢迎更多朋友参与进来，一起共建这个项目。 因为比“登顶 GitHub”更让我开心的，是这个由一个普通人开始的项目，真的帮助到了更多普通人。 最后，感谢每一个给项目点过 star、提交过作品、提过建议，以及帮我转发的朋友。 这个 NO. 1，属于每一个愿意创造、愿意分享的人。 也许，AI 时代最浪漫的事情，就是当大模型变得越来越聪明，那些原本不容易被看见的普通人，也终于有机会把自己的作品交到全世界面前。 我已经把它交出去了。 也希望有一天，你也能。</description><pubDate>Fri, 28 Aug 2026 07:22:00 +0000</pubDate></item><item><title>🤔 v-mortal 是什么？！我只知道 v-model 啊！</title><link>https://juejin.cn/post/7678703379591708724</link><guid isPermaLink="false">juejin-ee5cd5816d31076a</guid><description>昨天，我正在 review 代码，突然看到几个 v-mortal 出现在 Diff 中。 嗯？很陌生啊，完全没见过，搜索引擎没有结果，AI 也说不知道，问我是不是拼写错误，可能是 v-model ？ 于是我找到这段代码的作者，同事小爽，说：“这里是不是拼写错了，应该是 v-model 吧？” 小爽嘿嘿一笑：“兄弟，这个是我自研的指令，很高级，跟 v-model 一样能双向绑定数据！” 原来，小爽的英文名叫 Mortal，所以他写了一个 Vite 插件可以把所有的 v-mortal 在编译时替换为 v-model 。 这样做很巧妙，Vue 编译器无需处理 v-mortal ，没有额外的运行时开销，一起来看看他是怎么实现的吧！ 编写插件 以 Vite 8 为例，查阅 插件 API 简单示例 文档， 先新建一个 VMortalPlugin 插件函数： import type { Plugin } from &quot;vite&quot; ; export function VMortalPlugin ( ): Plugin { return { name : &quot;v-mortal&quot; , enforce : &quot;pre&quot; , transform : { filter : { id : /\.vue$/ }, handler ( code ) { // ... } } }; } 使用 v-mortal 作为 name 标识插件，这个名称会出现在日志和错误信息中； 因为需要在 Vue 编译器之前处理，所以将 enforce 设置为 pre ，参考 插件顺序 ； transform.filter.id 传 /\.vue$/ 表示只处理 Vue 文件，通常来说只有 .vue 文件中才会使用指令。 接下来在 transform.handler 中使用 vue/compiler-sfc 提供的 parse() 解析 Vue 代码，并提取出 template 代码块： import { parse } from &quot;vue/compiler-sfc&quot; ; const { template } = parse (code). descriptor ; 然后遍历 template.ast 找到所有的 v-mortal 指令，并使用 MagicString 替换为 v-model ： const visit = ( node: TemplateChildNode ) =&gt; { // 只处理元素节点（例如 &lt;input&gt;、&lt;CustomComponent&gt; 等） if (node. type === NodeTypes . ELEMENT ) { for ( const prop of node. props ) { // 查找自定义指令 v-mortal if (prop. type === NodeTypes . DIRECTIVE &amp;&amp; prop. name === MORTAL ) { const start = prop. loc . start . offset ; // 将 v-mortal 替换为 v-model // 只替换指令名称部分，保留参数和值等内容 ms. overwrite (start, start + V_MORTAL . length , &quot;v-model&quot; ); } } } // 如果当前节点包含 children，则继续递归遍历子节点 if ( &quot;children&quot; in node) { for ( const child of node. children ) { // 过滤掉简单表达式节点，只处理可能包含模板结构的节点 if ( typeof child === &quot;object&quot; &amp;&amp; child. type !== NodeTypes . SIMPLE_EXPRESSION ) { visit (child); } } } }; // 从模板 AST 的根 children 开始遍历所有节点 for ( const child of template. ast . children ) { visit (child); } 使用 AST 遍历可以很精确快速地找到 v-mortal 指令，而不必使用正则考虑太多边界条件，也不会错误修改字符串或者注释中的 v-mortal ； MagicString 是一个轻量且高效的工具，用于操作字符串以及生成源码映射（source-map）。 处理完毕，最后返回 code 和 map （source-map）： return { code : ms. toString (), map : ms. generateMap ({ hires : true }) }; 至此，插件编写完成，现在 Vue 代码中的所有 v-mortal 指令都会在编译阶段被自动替换为 v-model 。 完整代码如下： // v-mortal.ts import type { TemplateChildNode } from &quot;@vue/compiler-core&quot; ; import { NodeTypes } from &quot;@vue/compiler-core&quot; ; import type { Plugin } from &quot;vite&quot; ; import { MagicString , parse } from &quot;vue/compiler-sfc&quot; ; const MORTAL = &quot;mortal&quot; ; const V_MORTAL = `v- ${MORTAL} ` ; export function VMortalPlugin ( ): Plugin { return { name : &quot;v-mortal&quot; , enforce : &quot;pre&quot; , transform : { filter : { id : /\.vue$/ }, handler ( code ) { const { template } = parse (code). descriptor ; if (template == null || template. ast == null ) { return ; } const ms = new MagicString (code); const visit = ( node: TemplateChildNode ) =&gt; { if (node. type === NodeTypes . ELEMENT ) { for ( const prop of node. props ) { if (prop. type === NodeTypes . DIRECTIVE &amp;&amp; prop. name === MORTAL ) { const start = prop. loc . start . offset ; ms. overwrite (start, start + V_MORTAL . length , &quot;v-model&quot; ); } } } if ( &quot;children&quot; in node) { for ( const child of node. children ) { if ( typeof child === &quot;object&quot; &amp;&amp; child. type !== NodeTypes . SIMPLE_EXPRESSION ) { visit (child); } } } }; for ( const child of template. ast . children ) { visit (child); } return { code : ms. toString (), map : ms. generateMap ({ hires : true }) }; } } }; } 智能提示 大家平时在写 v-model 的时候，输入 v-m IDE 就会智能提示 v-model ，那么怎样才能让 v-mortal 也有智能提示呢？ 小爽很聪明，他又翻阅了 Vue 官方文档，找到 为自定义全局指令添加类型 部分的说明，他仿照提供了一个 v-mortal.d.ts 。 // v-mortal.d.ts import type { Directive } from &quot;vue&quot; ; export type MortalDirective = Directive &lt; HTMLElement , any &gt;; declare module &quot;vue&quot; { export interface GlobalDirectives { vMortal : MortalDirective ; } } 这样 v-mortal 也会有智能提示啦，开发体验直接拉满！ 总结回顾 通过这个小案例，我们了解了 Vite 插件开发的基本原理以及使用 TypeScript 提升开发体验的工程实践。 通过编译时转换来扩展 Vue 的语法能力，让开发者拥有更符合团队习惯的写法，同时又不引入额外的运行时成本。 v-mortal 虽然只是一个并不恰当的简单示例，但它展示了前端工程化中非常重要的思想： 将复杂逻辑尽可能提前到构建阶段处理，把运行时的负担转移到编译器和工具链 。 注意，这个案例只是用于演示插件开发，日常工作中不推荐大家这样做，以免被热心同事“亲切问候”～ 😁 写在最后 我是小何 xiaohe0601，热爱代码，目前专注于 AI 和前端开发领域。 欢迎关注我的微信公众号「小何不会写代码」，我会不定期分享一些开发心得、最佳实践以及技术探索等内容，希望能够帮到你！</description><pubDate>Fri, 28 Aug 2026 06:35:23 +0000</pubDate></item><item><title>ValidX错误消息国际化完全指南：8种语言9个语言包与三级回退机制</title><link>https://juejin.cn/post/7678747733979201536</link><guid isPermaLink="false">juejin-efde3a1ab17fc78d</guid><description>ValidX错误消息国际化完全指南：8种语言9个语言包与三级回退机制 引言 假设你的系统准备出海： 日本用户提交了非法手机号，收到的提示是中文&quot;手机号码格式不正确&quot;； 德国用户填写生日填错格式，报错信息是一串英文+中文混杂的乱码； 更常见的是：为了给海外版单独做一套报错文案，开发在 Controller 里写满 if (lang == EN) return &quot;...&quot; ，改一次文案要改三个语言分支。 这些都是&quot;验证错误消息国际化&quot;没做好的典型表现。一个成熟的开源验证库，错误消息应该 开箱即用、随用户语言自动切换 ——不用你翻译、不用你配置语言分支。 ValidX 的多语言支持正是这个思路：内置 9 个语言包文件、覆盖 8 种语言 （简体中文、英文、日文、韩文、法文、德文、西班牙文、俄文；中文由无后缀默认包与 _zh 包两个文件承载），注解方式与链式 API 两条路径共用一套消息体系 ，并提供 三级回退 保证任何语言环境下都能拿到可读的报错。本文对照源码逐层拆解这套机制，并给出可直接照抄的实战用法。 说明：文中所有实现细节均对照 ValidX v1.2.0 源码（ MessageManager 、 ValidX 、 ValidationMessages_*.properties ）逐一核实；示例代码与测试用例引自项目测试目录，可自行复现。 一、国际化机制全景 1.1 支持的语言 语言 资源文件 说明 简体中文 ValidationMessages_zh.properties 中文包 简体中文（默认） ValidationMessages.properties 无后缀兜底文件，内容为中文 英文 ValidationMessages_en.properties 兜底语言（回退目标） 日文 ValidationMessages_ja.properties 韩文 ValidationMessages_ko.properties 法文 ValidationMessages_fr.properties 德文 ValidationMessages_de.properties 西班牙文 ValidationMessages_es.properties 俄文 ValidationMessages_ru.properties 冷知识： ValidationMessages.properties （无语言后缀）虽然名字是&quot;默认&quot;，内容其实是 中文 ；而英文是单独一个 _en 包。这意味着&quot;未覆盖的语言环境最终会落到中文&quot;，&quot;兜底兜底再兜底&quot;才到英文。 1.2 两条消费路径，一套消息体系 ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ 注解验证（ @Email 等） │ │ 链式 API（ValidX. init ()） │ │ message ()= &quot;{完全限定key}&quot; │ │ withLocale (locale) │ └──────────────┬───────────────┘ └──────────────┬───────────────┘ │ │ ▼ ▼ Hibernate Validator MessageManager ResourceBundleMessageInterpolator . getMessage (key, locale) （解析 { key }，按线程 Locale 取） ├─ 语言包缓存 ConcurrentHashMap │ ├─ UTF8Control 强制 UTF-8 读取 └──────────┬───────────┼─ 三级回退：指定语言→英文→ key │ └─ ThreadLocal 线程级语言切换 ▼ ValidationMessages_ * .properties （ 9 个语言包） 注解方式 ：默认消息写成 {key} 花括号占位符，交给标准 Bean Validation 的 MessageInterpolator 解析替换； 链式方式 ：直接调用自研 MessageManager.getMessage(key, locale) 生成对应语言的文本。 两条路径最终都从 同一套 properties 语言包 取文案，所以不存在&quot;注解是中文、链式变英文&quot;的口径分裂。 1.3 消息 key 的命名规范 消息 key 采用 完全限定名 ，前缀即包名，可读性好且天然避免冲突： 类别 key 格式 示例 注解消息 io.github.vipxieliang.validx.annotation.&lt;name&gt; ...annotation.chinese.idcard 验证器消息 io.github.vipxieliang.validx.validator.&lt;name&gt; ...validator.date.pattern.contains.time 通用值消息 io.github.vipxieliang.validx.value.&lt;name&gt; ...value.null 二、语言包与消息对照 2.1 中英文对照示例 ValidationMessages_en.properties ： io.github.vipxieliang.validx.value.null=Value cannot be null io.github.vipxieliang.validx.annotation.chinese.idcard=Invalid Chinese ID card number io.github.vipxieliang.validx.annotation.chinese.phone=Invalid mobile phone number format io.github.vipxieliang.validx.annotation.email=Invalid email address format io.github.vipxieliang.validx.annotation.date.format=Invalid date format ValidationMessages_zh.properties ： io.github.vipxieliang.validx.value.null=值不能为空 io.github.vipxieliang.validx.annotation.chinese.idcard=身份证号码不正确 io.github.vipxieliang.validx.annotation.chinese.phone=手机号码格式不正确 io.github.vipxieliang.validx.annotation.email=邮箱地址格式不正确 io.github.vipxieliang.validx.annotation.date.format=日期格式不正确 2.2 注解的默认消息长什么样 每个注解的 message() 默认值就是一个 {完全限定key} ： // ChineseIdCard.java String message () default &quot;{io.github.vipxieliang.validx.annotation.chinese.idcard}&quot; ; // Email.java String message () default &quot;{io.github.vipxieliang.validx.annotation.email}&quot; ; // Date.java String message () default &quot;{io.github.vipxieliang.validx.annotation.date.format}&quot; ; 花括号中的 key 与 properties 文件的键一一对应。这就是&quot;开箱即用&quot;的来源： 你只要写上 @Email ，报错文案就已经是 8 种语言的了。 三、注解方式的国际化 3.1 不写 message，自动跟随语言环境 public class UserDTO { @Email private String email; @ChineseIdCard private String idCard; } 在中文系统环境下验证失败，得到&quot;邮箱地址格式不正确&quot;；在英文系统环境下，得到&quot;Invalid email address format&quot;。 同一行代码，零配置，消息自动切换。 3.2 显式指定语言（Hibernate Validator 配置） 注意： new ResourceBundleMessageInterpolator() 只负责把 {key} 解析为具体文案， 语言仍由线程 / 默认 Locale 决定 ，光配它并不能&quot;固定&quot;语言。要固定语言，正确做法是配合设置默认 Locale： Locale.setDefault(Locale.ENGLISH); // 影响整个 JVM 的默认语言（生产环境慎用，注意还原） ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator englishValidator = factory.getValidator(); Set&lt;ConstraintViolation&lt;UserDTO&gt;&gt; violations = englishValidator.validate(dto); 更推荐的做法：在 Spring 中配置固定的 LocaleResolver ，或用 ValidX 链式 API 的 withLocale() / 线程级 MessageManager.setCurrentLocale() （见下文 §4），作用范围更可控。 3.3 Spring Boot：自动跟随 Accept-Language 在 Spring Boot 中，验证由 LocalValidatorFactoryBean 托管，Hibernate Validator 的消息插值会 读取当前线程的 Locale ，而 Spring 的 LocaleResolver （默认 AcceptHeaderLocaleResolver ）会解析请求头 Accept-Language 设置线程 Locale。因此： 中文用户浏览器发 Accept-Language: zh-CN → 报错中文； 日文用户发 Accept-Language: ja → 报错日文； 无需在 Controller 里写任何语言判断代码。 3.4 局部覆盖：自己写 message 不想用默认文案时，直接写死或自定义 key 即可，覆盖优先级最高： public class UserDTO { // 硬编码覆盖 @Email(message = &quot;邮箱格式不对，请检查&quot;) private String email; // 自定义 key，放到自己的 ValidationMessages.properties 里 @Email(message = &quot;{myapp.msg.email}&quot;) private String email2; } 标准 Bean Validation 注解（ @NotBlank 、 @Size 等）的消息同样走这套机制，key 为 jakarta.validation.constraints.NotBlank.message 等，Hibernate Validator 自带英文默认值。 四、链式 API 的国际化 4.1 方式一：withLocale() 显式指定 import java.util.Locale; // 系统默认语言 ValidX chain1 = ValidX.init().isEmail( &quot;invalid-email&quot; ); // 显式中文 ValidX chain2 = ValidX.init() .withLocale(Locale.SIMPLIFIED_CHINESE) .isEmail( &quot;invalid-email&quot; ); // 显式英文 ValidX chain3 = ValidX.init() .withLocale(Locale.ENGLISH) .isEmail( &quot;invalid-email&quot; ); System.out.println(chain3.getErrorMessage()); // Invalid email address format 4.2 方式二：线程级语言环境（自动切换） 不想每次 withLocale ，可以用 MessageManager 设置 当前线程 的语言，该线程后续所有验证自动跟随： import io.github.vipxieliang.validx.i18n.MessageManager; // 设置当前线程语言为中文（影响本线程所有验证） MessageManager.setCurrentLocale(Locale.SIMPLIFIED_CHINESE); ValidX chain = ValidX.init().isEmail( &quot;invalid-email&quot; ); // chain.getErrorMessage() → 邮箱地址格式不正确 // 用完清理，避免线程池复用导致语言串台 MessageManager.clearCurrentLocale(); 4.3 Locale 优先级：显式 &gt; 线程级 &gt; 系统默认 源码中 ValidX.getLocale() 的逻辑： private Locale getLocale () { if (locale != null ) return locale; // 1. withLocale 显式指定 return MessageManager.getCurrentLocale(); // 2. 线程级 → 3. 系统默认 } 优先级 设置方式 作用范围 1 withLocale(Locale) 单次验证链 2 MessageManager.setCurrentLocale(Locale) 当前线程全部验证 3 Locale.getDefault() 全进程默认 4.4 错误消息获取 API ValidX validator = ValidX.init() .field( &quot;邮箱&quot; ).isEmail( &quot;invalid-email&quot; ) .field( &quot;电话&quot; ).isPhoneNumber( &quot;123&quot; ); validator.passed(); // false，是否全部通过 validator.isValid(); // false，同上 validator.getErrors(); // [&quot;邮箱: Invalid email address format&quot;, ...]，错误列表（副本） validator.getErrorMessage(); // &quot;邮箱: Invalid email address format, ...&quot;，逗号拼接 五、MessageManager 核心机制拆解 MessageManager 是链式 API 国际化的枢纽（ src/main/java/io/github/vipxieliang/validx/i18n/MessageManager.java ）。四个关键设计： 5.1 三级回退：指定语言 → 英文 → key 本身 public static String getMessage (String key, Locale locale) { try { ResourceBundle bundle = BUNDLES.computeIfAbsent(locale, l -&gt; ResourceBundle.getBundle(BASE_NAME, l, UTF8_CONTROL)); return bundle.getString(key); } catch (MissingResourceException e) { try { return DEFAULT_BUNDLE.getString(key); // 二级：英文兜底 } catch (MissingResourceException ex) { return key; // 三级：返回 key 本身 } } } 层级 条件 结果 1 指定语言包中有该 key 对应语言的文案 2 指定语言包缺失 → 英文包 英文文案 3 英文包也缺失 返回 key 本身（不会抛异常、不会空指针） 这意味着： 新加一个验证规则却漏配某个语言包时，最坏情况是用户看到 key 字符串，而不是系统崩溃。 5.2 UTF8Control：强制 UTF-8 读取 Java 的 PropertyResourceBundle 默认按 ISO-8859-1 读取 properties，中文会乱码。 MessageManager 内部类 UTF8Control 重写了 newBundle() ，用 InputStreamReader(stream, StandardCharsets.UTF_8) 强制按 UTF-8 加载： return new PropertyResourceBundle ( new InputStreamReader (stream, StandardCharsets.UTF_8)); 因此语言包文件 既可以 以 UTF-8 明文保存中文/日文/韩文（UTF8Control 可直接读取）， 也可以 沿用 Java properties 的 \uXXXX 转义格式（当前仓库 9 个语言包文件即为此格式）——两种写法 UTF8Control 都能正确加载。 5.3 中文不回退的细节 UTF8Control 还重写了 getFallbackLocale() ： 对中文 locale 返回 null（不回退） 。这是为了防止&quot;中文系统环境下 zh 包缺某个 key 时回退到默认英文包&quot;——确保中文环境拿到的始终是中文文案（或三级回退的 key）。 5.4 缓存：ConcurrentHashMap private static final Map&lt;Locale, ResourceBundle&gt; BUNDLES = new ConcurrentHashMap &lt;&gt;(); // ... BUNDLES.computeIfAbsent(locale, l -&gt; ResourceBundle.getBundle(BASE_NAME, l, UTF8_CONTROL)); 每个 Locale 的语言包只加载一次，后续直接命中缓存，多语言验证没有重复 IO 开销。 DEFAULT_BUNDLE （英文）在类加载时就预加载，保证回退路径永远可用。 5.5 动态参数替换 MessageManager 只负责取静态文本，带参数的消息由调用方在取到文案后做替换，例如 validateAge ： String message = MessageManager.getMessage( &quot;...annotation.age&quot; , locale); message = message.replace( &quot;{min}&quot; , String.valueOf(minAge)) .replace( &quot;{max}&quot; , String.valueOf(maxAge)); 六、实战场景 6.1 Web 接口：按用户语言返回报错 @RestController public class RegisterController { @PostMapping(&quot;/register&quot;) public Result&lt;Void&gt; register ( @Valid @RequestBody RegisterDTO dto) { return Result.success(); } } 注解方式 ：Spring Boot 自动按 Accept-Language 切换，无需任何代码。 链式方式 （比如验证动态 Map 数据时）：在请求入口读取用户语言，绑定到当前线程： @Component public class RequestLocaleFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String lang = req.getHeader( &quot;Accept-Language&quot; ); if (lang != null &amp;&amp; lang.startsWith( &quot;zh&quot; )) { MessageManager.setCurrentLocale(Locale.SIMPLIFIED_CHINESE); } else if (lang != null &amp;&amp; lang.startsWith( &quot;ja&quot; )) { MessageManager.setCurrentLocale(Locale.JAPANESE); } else { MessageManager.setCurrentLocale(Locale.ENGLISH); } try { chain.doFilter(request, response); } finally { MessageManager.clearCurrentLocale(); // 清理，防止线程池串语言 } } } 6.2 纯 Java 工具类：不依赖 Spring public class CertNoCheckUtil { public static String checkCertNo (String certNo, Locale locale) { ValidX validator = ValidX.init() .withLocale(locale) .field( &quot;证件号&quot; ).isChineseIdCard(certNo); return validator.isValid() ? &quot;OK&quot; : validator.getErrorMessage(); } } 6.3 前端拿到可展示的错误 // 后端返回结构化错误，前端直接展示 @ExceptionHandler(MethodArgumentNotValidException.class) public Result&lt;List&lt;String&gt;&gt; handleValid (MethodArgumentNotValidException e) { List&lt;String&gt; msgs = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) // 已是当前语言 .collect(Collectors.toList()); return Result.fail( 400 , msgs); } 七、测试保障：i18n 行为是锁死的 项目测试目录（ src/test/java/.../i18n/ ）对国际化行为做了完整锁定，也是文章所有结论的验证依据： 7.1 语言包完整性测试 DateValidatorI18nTest 、 DateTimeValidatorI18nTest 断言： 8 种语言包里验证器级 key 都存在、非空，且中/英/日消息互不相同 ——确保每个语言包都是&quot;真翻译&quot;，而不是全部回退英文充数。 7.2 Locale 优先级测试 AutoLocaleValidationChainTest 覆盖三种场景： 场景 设置 结果 不设置任何 Locale — 用系统默认语言 设置线程级 setCurrentLocale(zh) 中文消息 显式覆盖线程级 setCurrentLocale(zh) + withLocale(en) 英文 （显式优先） 八、最佳实践清单 注解方式零配置 ：默认 {key} 消息已内置 8 种语言，别急着写死 message ； 局部覆盖用自定义 key ：需要自定义文案时，优先用 &quot;{myapp.xxx}&quot; 放进自己的 ValidationMessages.properties ，而不是硬编码中文； Spring Boot 里用注解验证 ：自动跟随 Accept-Language ，不要自己写语言分支； 链式 API 场景 ：单次指定用 withLocale() ；线程内统一用 MessageManager.setCurrentLocale() ， 用完务必 clearCurrentLocale() （尤其线程池环境）； 新增语言包不要漏 key ：三级回退保证不崩，但漏 key 会让用户看到 key 字符串，上线前用测试遍历所有 key × 所有语言； 语言包文件两种格式皆可 ： UTF8Control 支持 UTF-8 明文与 \uXXXX 转义两种写法（仓库现有文件为转义格式），改动时与既有格式保持一致即可； 优先级记牢 ： withLocale &gt; 线程级 &gt; 系统默认，别让显式设置被覆盖。 总结 ValidX 内置 9 个语言包文件、8 种语言 （中文默认、英文兜底 + 日/韩/法/德/西/俄），注解与链式 API 共用一套消息体系 ； 注解方式通过 {完全限定key} + Bean Validation 的 MessageInterpolator 解析，Spring Boot 下 自动跟随 Accept-Language ； 链式 API 通过 MessageManager 实现， withLocale() 显式指定、线程级 setCurrentLocale() 自动切换； 核心机制四件套： 三级回退 （指定语言→英文→key）、 UTF8Control （强制 UTF-8）、 语言包缓存 （ConcurrentHashMap）、 中文不回退 （避免中文环境拿英文）； 国际化做得好的标志： 用户永远看到自己语言的可读报错，开发者永远不需要写语言分支。 ValidX 是基于 Jakarta Bean Validation 规范的 Java 验证库，注解与链式 API 双模式，内置 100+ 验证规则。项目地址： github.com/vipxieliang/validx</description><pubDate>Fri, 28 Aug 2026 02:23:29 +0000</pubDate></item><item><title>爆火的《牛来》模型正式发布！DeepSeek 的排名又下降了。。</title><link>https://juejin.cn/post/7678618274796961843</link><guid isPermaLink="false">juejin-a1035cbf63ae709a</guid><description>大家好，我是程序员鱼皮。 前几天，海外 AI 平台 OpenRouter 和 OpenCode 上突然冒出了一个没有任何背景介绍的匿名模型 Ox-Alpha ，俗称「牛来」模型。 没人知道它是谁做的，但开发者们试用之后纷纷上头了。上线首日直接冲到 OpenRouter 榜首，刷新了平台单日 Token 用量的历史纪录，甚至终结了 DeepSeek 在 OpenCode 连续 56 天霸榜的纪录！ 谷歌还屁颠屁颠儿地跑出来蹭热度认领，说这是自家的模型，结果被全网笑惨了。 就在昨天，这个神秘模型终于正式揭晓身份。 它是智谱的 GLM-5.3-Flash，是国产模型！ GLM-5.3-Flash 是智谱的首个 原生多模态模型 ，采用全新架构，仅 320B 参数（激活仅 18B），能力就可以对标 Claude Opus 4.8， 而且价格竟然只有 Opus 4.8 的 1/40 ！ GLM-5.3-Flash 上线即开源，采用 MIT 协议，而且全部流量跑在 10 万张国产芯片上！ 在 Artificial Analysis Intelligence Index 这个国际权威综合能力榜上，GLM-5.3-Flash 拿到了 57 分，跟 Claude Opus 4.8 持平，超过了 DeepSeek V4 Pro（53 分）和 DeepSeek V4 Flash（52 分）。在 Coding 方向的多项基准上也都逼近甚至超过了 Opus 4.8，但价格目前仅为 Opus 4.8 的 1/40，常规场景下也比涨价后的 DeepSeek V4 Flash 更便宜。 一条新的 AI 模型斩杀线，正在形成。 以前大家默认 AI 具有前沿能力就意味着高昂的价格，GLM-5.3-Flash 把这个等式打破了。 GLM 终于开眼 对我来说，这次最兴奋的升级不是价格，而是原生多模态。 GLM-5.3 虽然编程能力很强，但它是个「瞎子模型」，写完代码之后没办法自己看一眼页面长什么样。同样的问题在测试 DeepSeek V4 Pro 的时候也遇到了，前端做出来的效果对不对、好不好看，模型自己完全判断不了。 AI 公司当然能意识到这个痛点，于是前阵子 DeepSeek V4 Flash 率先放出了多模态版本 DeepSeek-V4-Vision-Exp，这次智谱直接跟上，而且视觉能力是从预训练阶段就原生融入的，让 AI 天生就有一双慧眼。 也就是说，AI 写完一段前端代码之后，它自己就能截图看效果、发现布局错位或者配色不对、然后自己修复，不需要你人工介入一步步测试和验收了。这对 AI 编程的体验提升是巨大的。 官方提供了几个 Demo 案例，咱们一起来看看。 1、3D Blender 建模 GLM-5.3-Flash 在没有任何外部素材的情况下，自主运行了 16 个小时，用 Blender 从零搭建了一套约 400 平方米的专业主厨自宅和厨房。模型通过反复渲染、查看画面、定位问题、再迭代修正的方式，完成了整个场景构建。 2、设计稿到完整 APP 让 AI 基于参考页面的截图进行像素级复刻，模型会分析设计体系、页面关系、共享组件、导航结构和动效逻辑，然后逐页截图与参考图对比，持续修正差异，直到视觉还原度达标。 设计稿原型图： 复刻出来的成品 APP，还原度很高： 3、主题视觉复刻 基于节日视觉设计，复刻红包封面的平面艺术风格和动态展示体验。 这个复刻效果，你觉得怎么样？ 4、游戏复刻 让 AI 自主复刻经典的 Godot 游戏《喷射战士》。 好家伙，看看这个效果，我甚至以为是原作…… 当然，官方 Demo 再好看也只是参考，到底好不好用还是得自己试了才知道。 下面我准备了 3 个项目来实测 GLM-5.3-Flash 的编程能力，从前端视觉复刻、全栈应用开发到多模态综合任务，由浅入深。 工具准备 这次实战我选用的 AI 编程工具是 ZCode ，这是智谱 GLM 系列的官方 Harness 工具。 如果你平时就打算用 GLM 系列模型来 AI 编程，那 ZCode 是更好的选择，毕竟是角色专武嘛。它对自家模型 GLM-5.3-Flash 的多模态能力支持得最好，还内置了浏览器控制和电脑控制功能，模型可以直接操作浏览器、截图验证、自主调试。 打开 ZCode，会先让你登录授权，国内选择连接 BigModel 平台。 登录之后就进入了熟悉的 Agent 面板，跟 Codex 长得几乎一模一样，可以在对话框中直接切换使用最新的 GLM-5.3-Flash 模型。 另外，为了让 AI 运行时能够获取到最新的信息，还需要准备好联网搜索插件 Firecrawl、以及获取最新文档的插件 Context7。分别到各自的官网安装就好了，ZCode 会自动识别出本地已经安装的技能。 ZCode 自带浏览器控制和电脑控制功能，开启之后模型就能直接打开浏览器截图验收、操作桌面应用。需要先在设置里点击授权，允许辅助功能和屏幕录制权限。 准备工作搞定了，下面开始实战。 GLM-5.3-Flash 实战测评 实战 1、复刻 Apple Vision 产品页 第一个任务，我想测的是 GLM-5.3-Flash 的视觉理解能力，看看到底有多强？ 最直观的方式就是让 AI 去复刻一个视觉效果很复杂的网页。我选了 Apple Vision Pro 的官方产品页，这个页面有大量的滚动触发动画、3D 产品展示、深色科技感背景和精致的文字排版节奏，是公认难度很高的前端设计。 提示词里我要求 AI 先用 Firecrawl 抓取目标页面的结构信息，然后充分利用视觉理解能力，先截图分析目标网站的布局和风格，开发过程中每完成一个模块就自己截图对比验证，不满意就优化，直到满意再交付。 我特地开启了完全访问模式，让 AI 可以自由安装依赖和启动服务，省去中间的确认环节： AI 拿到任务之后，先加载 Firecrawl 技能联网抓取了 Apple Vision Pro 产品页的网页结构，然后加载浏览器自动化技能，对目标页做了 8 个不同滚动位置的截图分析，提取出页面的章节节奏、双导航结构、文字韵律等设计细节。 做完这些准备工作之后，AI 才正式开工写代码。 整个开发过程中，AI 会不断截图对比自己的成品和 Apple 目标页的效果，发现差异就调整。比如它会检查缩放舞台的文字与产品淡出时序是不是跟原版一致，章节标题的亮度对不对，媒体辉光效果够不够。 经过了 12 轮截图验证，AI 花了大约 19 分钟完成了整个任务，并且自动帮我启动了项目： 来看看成品效果。在没有使用任何 Apple 官方图片素材的情况下，整体布局的还原度相当高！ 举个例子，对比一下导航栏的复刻效果，双层导航结构、胶囊按钮、产品子导航的排列方式都还原了： 整个页面的深色科技氛围和原版很相似，文字的排版节奏也是对味儿的： 虽然没有真实的产品摄影图片，但整体的视觉还原度已经很不错了。如果把正式的产品图片替换上去，基本可以直接当成一个完整的产品宣传页来使用。 以后如果你想做个产品宣传网站，直接把要复刻的网址或者 UI 截图发给 AI，它就能帮你搭出来个八九不离十的版本，然后再手动微调细节就行了。 实战 2、全栈 AI 绘图工具 第二个任务我换了方向，测试 GLM-5.3-Flash 的全栈工程能力。 我选了跟之前测评 GLM-5.3 时完全一样的案例，让 AI 开发一个 AI 智能绘图工具。 用户输入自然语言描述，后端调用大模型生成 draw.io 兼容的 XML 图表结构，前端嵌入 draw.io 开源编辑器实时渲染，还支持连续对话修改。 之所以用同一个案例，是为了跟之前在 DeepSeek Harness 里测试的 GLM-5.3、DeepSeek V4 Pro、Kimi K3 做横向对比，看看 GLM-5.3-Flash 在 ZCode 环境下表现如何。 而且 GLM-5.3-Flash 本身就支持 API 接入，正好可以作为这个应用内置的 AI 模型，一举两得。 提示词跟之前完全一样，只是把内置的 AI 模型从 DeepSeek V4 Pro 换成了 GLM-5.3-Flash。 AI 通过 Firecrawl 联网抓取了 draw.io 仓库结构和嵌入协议文档，发现了一个关键信息：draw.io 支持 AUTOSAVE 事件，编辑器会主动推送 XML，这样就能实现双向实时同步，不需要魔改源码。 大约 46 分钟后，AI 完成了任务，主要的时间都花在了自动化验证上。 来看看成品效果。刚打开页面我就被吸引到了，这个 UI 设计很有科技感，而且 draw.io 的集成非常自然，不像是生硬嵌入的第三方组件，而是融合成了产品本身的一部分。 我让 AI 帮我画出 DeepSeek Harness 的架构图。左侧可以实时看到 AI 工作的过程，右侧显示了一个加载动画。 生成完成后，右侧立刻渲染出了完整的架构图，分层清晰，而且保留了 draw.io 原本的元素编辑能力，可以直接拖拽和修改。 更强的是，网站支持连续对话修改。比如我说「帮我把 Harness 服务层这块的背景改为红色」，AI 就能精准定位到对应的元素并修改样式，右侧画布也会实时更新。 不过偶尔也会出现 AI 生成的 XML 跟 draw.io 不兼容的情况，弹出解析错误的提示。之前我用 Kimi K3 跑同一个案例的时候也遇到过类似的问题，这估计是目前 AI 生成结构化代码时的通病了。 看了 GLM-5.3-Flash 生成的效果后，再跟之前在 DeepSeek Harness 里测试的其他模型对比一下。 之前 GLM-5.3 做的版本界面科技感最强，把 draw.io 深度融合到了自己设计的界面中，生成的架构图分层清晰，连续对话修改也很顺畅： DeepSeek V4 Pro 的版本虽然功能跑通了，但整体界面粗糙，draw.io 是直接整个嵌入的，给人的感觉很生硬： Kimi K3 的版本前端设计挺好看，draw.io 的融合度也不错，但后端偶尔翻车，会直接把 XML 源码糊在界面上而不是渲染成图形。 综合来看，GLM-5.3-Flash 这次的前端效果比 GLM-5.3 和 Kimi K3 略逊一筹，主要体现在界面精致度上。 后端能力比 GLM-5.3 弱了一些，偶尔会出现 XML 不兼容的情况，跟 Kimi K3 的水平差不多。但综合效果明显超过 DeepSeek V4 Pro，毕竟 draw.io 的集成方式和整体产品设计意识都好了不止一个档次。 实战 3、3D 联机对战游戏 最后一个任务我想测的是 GLM-5.3-Flash 的多模态综合理解能力和长程工程能力。 一方面看它能不能从图片中提取视觉信息并转化成实际的代码产出，另一方面看它能不能在复杂的大项目中稳定运行而不翻车。 最近《牛来》不是很火么？ 于是我让 AI 开发一个 3D 双人联机实时拳击对战游戏《牛来格斗》，致敬拳皇和奥特曼格斗进化系列的经典对战体验。 关键在于，我会提供 6 张《牛来》动画电影的角色原片图片，让 AI 分析这些角色的造型、体态比例和色彩风格，然后把它们还原成游戏中的可选角色。 这个任务对多模态能力的要求非常高，AI 不仅要理解格斗游戏的玩法机制，还得从图片中提取视觉信息并转化成 3D 角色建模。 AI 拿到 6 张参考图之后，通过联网搜索获取了拳皇和街霸的核心机制，然后分析参考图确定了 4 个可选角色的方案： 整个 ZCode 的开发体验还是很丝滑的。AI 自己就能打开内嵌浏览器，操作页面然后截图验证效果，发现问题就自己修复，不需要我做任何干预。 苦苦等待了 2 个多小时，AI 终于完成了任务。总共经历了 4 轮完整的开发 - 验证循环，长程任务没有翻车。 我已经开始期待成品了…… 打开游戏主页，《牛来》那股抽象的味儿扑面而来~ 试试人机练习模式，先选择你的斗士。这 4 个角色的 3D 建模我觉得还不错，金黄色的牛麻麻、橙色的小牛来、斑点金钱豹、棕色的牛霸霸，跟我给的参考图还是挺像的吧？ 开始对战！ 角色的出招方式很丰富，轻拳、重拳、轻踢、重踢、防御、闪避一应俱全。 看我豹拉一脚把牛来踹了个跟头，打的牛来叫「妈妈」： 就你欺负我儿子牛来是吧？ 牛麻麻亲自下场，先给你来一连串牛家拳，15 连击！ 还可以释放必杀技，蛮牛冲撞！特效还可以，打击感十足~ 每个角色的必杀技风格都不一样，小牛来的必杀是「飞天小牛弹」，会把对手轰出画面，动画效果很流畅： 再试一下局域网联机功能。虽然能够正常加入房间开局，但双方的画面并没有实时同步。说明 GLM-5.3-Flash 在处理复杂的实时网络同步逻辑上还有一些不足。这也算是意料之中的事，WebSocket 状态同步本来就是后端最难搞的场景之一。 但游戏单机对战的体验确实超出了我的预期。一个 AI 从 6 张图片出发，自己搜索格斗游戏的设计规范，自己分析角色造型转化成 3D 建模，最后做出一个有连招系统、必杀技特效、完整 UI 流程的格斗游戏，整个过程不需要人工干预。 视觉理解能力对 AI 编程的加持，在这个测评中体现得淋漓尽致。 我的感受 三个项目都跑完了，聊聊我的真实感受。 这次 GLM-5.3-Flash 最大的亮点毫无疑问是视觉理解能力。之前测试 DeepSeek V4 Pro 和 GLM-5.3 的时候，前端效果好不好看、布局有没有错位，模型完全判断不了，只能靠人工验收或者 Playwright 做一些基础的文字化断言。 现在 GLM-5.3-Flash 能自己截图验证了，整个开发体验完全不一样。复刻 Apple 产品页的时候 AI 做了 12 轮截图对比，开发格斗游戏的时候 AI 通过截图抓到了一个纯代码审查根本发现不了的视觉 Bug。这种「写完代码自己看一眼效果」的能力，对 AI 编程来说真的是质的提升。 除了模型能力之外，大家最关心的肯定还是性价比。我开通了 GLM Coding Plan 的 Pro 套餐，三个项目跑完之后看了一下使用统计，5 小时额度才花了 40% 左右，这周才花了 8% 的额度。 换算一下，GLM-5.3-Flash 相比 GLM-5.3 额度翻了 3 倍，而且非高峰时段（包括周末全天）积分消耗减半。按照 Pro 套餐的周额度和我实际消耗的比例来算，跑完这 3 个项目的成本才不到 10 块钱。而且我测的项目复杂度都不低，如果你只是日常写点小工具、做个页面的话，一周的额度根本用不完。 相比之下，DeepSeek 八月份全面提价之后，同样的任务量如果用 V4 Pro 来跑，估计要花几十块，V4 Flash 来跑也要花 20 多块，差距一目了然。 此外，智谱也学会 Codex 那一套了，GLM-5.3-Flash 发布当天直接重置了所有用户的使用限额。对用户来说这当然是好事，白票一波新模型的机会，说不定以后有事儿没事儿就能重置一波呢？ 不过 GLM-5.3-Flash 的能力上限肯定是比不过 GLM-5.3 这个旗舰模型的。在 AI 绘图工具的横评里能明显看出，GLM-5.3 做出来的产品可用性和精细度还是更高一截。 GLM-5.3-Flash 的定位是 日常好用、高性价比的牛马模型 ，大部分任务它都能胜任，偶尔遇到需要极致效果的场景再切换到旗舰模型就好。 综上，我单方面不负任何责任地宣布，DeepSeek 的排名又下降了一位。 OK 就分享到这里，本文会收录到我免费开源的 《AI 编程零基础入门教程》 ，上千张图、几十万字，带你从 0 开始快速学会 AI 编程，做出自己的产品、跑通变现全流程，一次拿捏。 开源指路： github.com/liyupi/ai-g… 我是鱼皮，持续分享 AI 编程干货。觉得有用的话记得点赞收藏和关注~ 也欢迎在评论区分享你自己使用 GLM-5.3-Flash 模型的体验。</description><pubDate>Fri, 28 Aug 2026 00:58:08 +0000</pubDate></item><item><title>🚀苹果的液态玻璃咋做？🚀</title><link>https://juejin.cn/post/7678529068804554787</link><guid isPermaLink="false">juejin-82103c9f471c750b</guid><description>先看看现有方案。 大佬实现的： github.com/iyinchao/li… 效果非常牛逼，但是目前只能渲染图片对于html却无能为力。 svg版本： vue-bits.dev/components/… 简单，但是存在致命问题，平滑度不够并会出现毛边且会出现颜色异常。 目前最好方案：three.js + webGL fluid-glass 1. 渲染原理概览 Fluid Glass 的核心是利用 FBO 离屏渲染 与 体积透射材质 ( MeshTransmissionMaterial ) 实现真 3D 光学折射与色散。 数据流向： 核心机制： Scene 隔离 ：背景文字与画廊通过 createPortal 挂载到独立离屏 Scene ，不直接出现在主画布上。 离屏烘焙 ：每帧渲染循环中，先将离屏 Scene 绘制到帧缓冲对象（FBO），生成背景纹理。 单 Pass 折射采样 ：3D 玻璃网格直接使用该 FBO 纹理作为透射采样源，基于网格表面法线与光学参数（IOR、厚度、色散）计算折射偏移。 2. 关键实现解析 ( FluidGlass.tsx ) 2.1 模型预加载与参数配置 import { memo, Suspense , useEffect, useMemo, useRef, useState, type ReactNode } from &apos;react&apos; ; import * as THREE from &apos;three&apos; ; import { Canvas , createPortal, useFrame, useThree } from &apos;@react-three/fiber&apos; ; import { Image , MeshTransmissionMaterial , Preload , Scroll , ScrollControls , Text , useFBO, useGLTF, useScroll, } from &apos;@react-three/drei&apos; ; import { easing } from &apos;maath&apos; ; export type FluidMode = &apos;lens&apos; | &apos;cube&apos; | &apos;bar&apos; ; export type FluidGlassProps = { mode?: FluidMode ; scale?: number ; ior?: number ; thickness?: number ; chromaticAberration?: number ; anisotropy?: number ; }; const LENS_GLB = &apos;/assets/3d/lens.glb&apos; ; const CUBE_GLB = &apos;/assets/3d/cube.glb&apos; ; const BAR_GLB = &apos;/assets/3d/bar.glb&apos; ; // 预加载模型，避免切换形态时由于异步加载引发画面闪烁 useGLTF. preload ( LENS_GLB ); useGLTF. preload ( CUBE_GLB ); useGLTF. preload ( BAR_GLB ); 2.2 核心包装器 ModeWrapper ：事件与状态处理 ModeWrapper 负责状态管理、模型加载、事件监听与渲染管线调度。 type ModeWrapperProps = { children?: ReactNode ; glb : string ; geometryKey : string ; followPointer?: boolean ; lockToBottom?: boolean ; modeProps : Record &lt; string , unknown &gt;; }; const ModeWrapper = memo ( function ModeWrapper ( { children, glb, geometryKey, lockToBottom = false , followPointer = true , modeProps = {}, }: ModeWrapperProps ) { const meshRef = useRef&lt; THREE . Mesh &gt;( null ); const gltf = useGLTF (glb); const nodes = gltf. nodes as Record &lt; string , THREE . Mesh &gt;; const buffer = useFBO (); // 分配离屏 FBO 渲染目标 const { viewport, gl } = useThree (); const scene = useMemo ( () =&gt; new THREE . Scene (), []); // 创建独立的离屏场景 const geoWidthRef = useRef ( 1 ); const pointerNDC = useRef ( new THREE . Vector2 ()); // 1. 获取模型包围盒尺寸，用于未指定 scale 时的自适应计算 useEffect ( () =&gt; { const geo = nodes[geometryKey]?. geometry ; if (!geo) return ; geo. computeBoundingBox (); const box = geo. boundingBox ; geoWidthRef. current = box ? box. max . x - box. min . x || 1 : 1 ; }, [nodes, geometryKey]); // 2. 指针事件监听与 NDC (归一化设备坐标) 转换 useEffect ( () =&gt; { const canvas = gl. domElement ; const onMove = ( event: PointerEvent ) =&gt; { const rect = canvas. getBoundingClientRect (); if (rect. width === 0 || rect. height === 0 ) return ; pointerNDC. current . set ( ((event. clientX - rect. left ) / rect. width ) * 2 - 1 , -((event. clientY - rect. top ) / rect. height ) * 2 + 1 , ); }; window . addEventListener ( &apos;pointermove&apos; , onMove); return () =&gt; window . removeEventListener ( &apos;pointermove&apos; , onMove); }, [gl]); 由于外层使用了 Drei 的 ScrollControls ，滚动容器会劫持一部分默认事件，因此这里通过原生 pointermove 与 canvas.getBoundingClientRect() 自行计算归一化坐标。 2.3 逐帧调度： useFrame 渲染循环 useFrame 回调由 requestAnimationFrame 驱动，在每一帧主画面渲染前执行： useFrame ( ( state, delta ) =&gt; { const mesh = meshRef. current ; if (!mesh) return ; const { gl, camera } = state; // 计算透镜所在深度 (Z=15) 的视口尺寸 const view = viewport. getCurrentViewport (camera, [ 0 , 0 , 15 ]); const destX = followPointer ? (pointerNDC. current . x * view. width ) / 2 : 0 ; const destY = lockToBottom ? -view. height / 2 + 0.2 : followPointer ? (pointerNDC. current . y * view. height ) / 2 : 0 ; // 惯性平滑插值 easing. damp3 (mesh. position , [destX, destY, 15 ], 0.15 , delta); // 自适应缩放 if (modeProps. scale == null ) { const maxWorld = view. width * 0.9 ; mesh. scale . setScalar ( Math . min ( 0.15 , maxWorld / geoWidthRef. current )); } // 将离屏 Scene 渲染进 FBO gl. setRenderTarget (buffer); gl. render (scene, camera); gl. setRenderTarget ( null ); gl. setClearColor ( 0x5227ff , 1 ); }); 2.4 视口与坐标计算原理 1. 深度定在 Z = 15 相机位置在 Z = 20 （ camera={{ position: [0, 0, 20], fov: 15 }} ）。 场景背景内容分布在 Z = 0 ~ 12 。 透镜放置在 Z = 15 ，位于相机与背景内容之间，使光线穿过玻璃后折射背景。 2. view 的作用 在透视投影（Perspective Projection）下，视锥体会随深度变化： viewport.getCurrentViewport(camera, [0, 0, 15]) 用于计算在 Z = 15 切平面上，屏幕对应的世界坐标宽高（ view.width , view.height ）。 3. 坐标计算除以 2 的原因 Three.js 场景原点 (0, 0, 0) 位于视口正中央： pointerNDC.x 范围为 [-1, 1] 。 视口正中为 0 ，最右边界为 +view.width / 2 ，最左边界为 -view.width / 2 。 坐标换算公式： destX = pointerNDC.x × view.width 2 \text{destX} = \text{pointerNDC.x} \times \frac{\text{view.width}}{2} destX = pointerNDC.x × 2 view.width destY = pointerNDC.y × view.height 2 \text{destY} = \text{pointerNDC.y} \times \frac{\text{view.height}}{2} destY = pointerNDC.y × 2 view.height 2.5 场景隔离与 FBO 离屏渲染时序 1. const buffer = useFBO() 初始化时仅在显存中开辟一块渲染缓冲（此时纹理内无有效像素数据）。 数据写入发生在 useFrame 中调用 gl.render(scene, camera) 时。 2. createPortal(children, scene) 将 React 子节点（文字与图片）挂载到独立的 scene （ new THREE.Scene() ）。 这些元素脱离默认场景树，不会直接渲染到屏幕上，专门用于 FBO 离屏绘制。 2.6 双材质设计：底图平面与透射材质 return ( &lt;&gt; {createPortal(children, scene)} {/* 1. 底层：全屏背景平面 */} &lt; mesh scale = {[viewport.width, viewport.height , 1 ]}&gt; &lt; planeGeometry /&gt; &lt; meshBasicMaterial map = {buffer.texture} transparent /&gt; &lt;/ mesh &gt; {/* 2. 顶层：3D 玻璃网格 */} &lt; mesh ref = {meshRef} scale = {(scale as number | undefined ) ?? 0.15 } rotation-x = {Math.PI / 2 } geometry = {nodes[geometryKey]?.geometry} &gt; &lt; MeshTransmissionMaterial buffer = {buffer.texture} ior = {(ior as number | undefined ) ?? 1.15 } thickness = {(thickness as number | undefined ) ?? 5 } anisotropy = {(anisotropy as number | undefined ) ?? 0.01 } chromaticAberration = {(chromaticAberration as number | undefined ) ?? 0.1 } { ...extraMat } /&gt; &lt;/ mesh &gt; &lt;/&gt; ); 材质与参数 数据源 作用与机制 meshBasicMaterial map={buffer.texture} buffer.texture 无光照贴图 ：将 FBO 内容 1:1 贴满视口，作为未被透镜遮挡时的正常背景底图。 MeshTransmissionMaterial buffer={buffer.texture} buffer.texture 透射折射采样源 ：片元着色器根据网格法线、 ior （折射率）、 thickness （厚度）和 chromaticAberration （色散）对纹理进行偏移动态采样。 3. 场景组件与模式切换 3.1 透镜形态模式（Lens / Cube / Bar） function Lens ( { children, modeProps }: { children?: ReactNode; modeProps: Record&lt; string , unknown &gt; } ) { return ( &lt; ModeWrapper glb = {LENS_GLB} geometryKey = &quot;Cylinder&quot; followPointer modeProps = {modeProps} &gt; {children} &lt;/ ModeWrapper &gt; ); } function Cube ( { children, modeProps }: { children?: ReactNode; modeProps: Record&lt; string , unknown &gt; } ) { return ( &lt; ModeWrapper glb = {CUBE_GLB} geometryKey = &quot;Cube&quot; followPointer modeProps = {modeProps} &gt; {children} &lt;/ ModeWrapper &gt; ); } function Bar ( { children, modeProps = {} }: { children?: ReactNode; modeProps?: Record&lt; string , unknown &gt; } ) { return ( &lt; ModeWrapper glb = {BAR_GLB} geometryKey = &quot;Cube&quot; lockToBottom followPointer = {false} modeProps = {{ transmission: 1 , roughness: 0 , thickness: 10 , ior: 1.15 , color: &apos;# ffffff &apos;, attenuationColor: &apos;# ffffff &apos;, attenuationDistance: 0.25 , ...modeProps , }} &gt; {children} &lt;/ ModeWrapper &gt; ); } Lens ：圆柱透镜网格，跟随指针。 Cube ：立方体网格，呈现多面折射，跟随指针。 Bar ：长条网格，固定在视口底部，作为底部毛玻璃导航栏。 3.2 滚动画廊组件 Images type ZoomMaterial = THREE . MeshBasicMaterial &amp; { zoom : number }; function Images ( ) { const group = useRef&lt; THREE . Group &gt;( null ); const data = useScroll (); const { height } = useThree ( ( state ) =&gt; state. viewport ); useFrame ( () =&gt; { const children = group. current ?. children ; if (!children || children. length &lt; 5 ) return ; const zoom = ( index: number , value: number ) =&gt; { ((children[index] as THREE . Mesh ). material as ZoomMaterial ). zoom = value; }; zoom ( 0 , 1 + data. range ( 0 , 1 / 3 ) / 3 ); zoom ( 1 , 1 + data. range ( 0 , 1 / 3 ) / 3 ); zoom ( 2 , 1 + data. range ( 1.15 / 3 , 1 / 3 ) / 2 ); zoom ( 3 , 1 + data. range ( 1.15 / 3 , 1 / 3 ) / 2 ); zoom ( 4 , 1 + data. range ( 1.15 / 3 , 1 / 3 ) / 2 ); }); return ( &lt; group ref = {group} &gt; &lt; Image position = {[-2, 0 , 0 ]} scale = {[3, height / 1.1 ]} url = &quot;/assets/demo/cs1.webp&quot; /&gt; &lt; Image position = {[2, 0 , 3 ]} scale = {3} url = &quot;/assets/demo/cs2.webp&quot; /&gt; &lt; Image position = {[-2.05, -height , 6 ]} scale = {[1, 3 ]} url = &quot;/assets/demo/cs3.webp&quot; /&gt; &lt; Image position = {[-0.6, -height , 9 ]} scale = {[1, 2 ]} url = &quot;/assets/demo/cs1.webp&quot; /&gt; &lt; Image position = {[0.75, -height , 10.5 ]} scale = {1.5} url = &quot;/assets/demo/cs2.webp&quot; /&gt; &lt;/ group &gt; ); } 3.3 响应式排版组件 Typography 与 NavItems function Typography ( ) { const DEVICE = { mobile : { fontSize : 0.2 }, tablet : { fontSize : 0.4 }, desktop : { fontSize : 0.6 }, }; const getDevice = (): keyof typeof DEVICE =&gt; { const width = window . innerWidth ; return width &lt;= 639 ? &apos;mobile&apos; : width &lt;= 1023 ? &apos;tablet&apos; : &apos;desktop&apos; ; }; const [device, setDevice] = useState (getDevice); useEffect ( () =&gt; { const onResize = ( ) =&gt; setDevice ( getDevice ()); window . addEventListener ( &apos;resize&apos; , onResize); return () =&gt; window . removeEventListener ( &apos;resize&apos; , onResize); }, []); return ( &lt; Text position = {[0, 0 , 12 ]} fontSize = {DEVICE[device].fontSize} letterSpacing = {-0.05} outlineWidth = {0} outlineBlur = &quot;20%&quot; outlineColor = &quot;#000&quot; outlineOpacity = {0.5} color = &quot;white&quot; anchorX = &quot;center&quot; anchorY = &quot;middle&quot; &gt; React Bits &lt;/ Text &gt; ); } function NavItems ( { items }: { items: { label: string ; link: string }[] } ) { const group = useRef&lt; THREE . Group &gt;( null ); const { viewport, camera } = useThree (); const DEVICE = { mobile : { max : 639 , spacing : 0.2 , fontSize : 0.035 }, tablet : { max : 1023 , spacing : 0.24 , fontSize : 0.035 }, desktop : { max : Infinity , spacing : 0.3 , fontSize : 0.035 }, }; const getDevice = (): keyof typeof DEVICE =&gt; { const width = window . innerWidth ; return width &lt;= DEVICE . mobile . max ? &apos;mobile&apos; : width &lt;= DEVICE . tablet . max ? &apos;tablet&apos; : &apos;desktop&apos; ; }; const [device, setDevice] = useState (getDevice); useEffect ( () =&gt; { const onResize = ( ) =&gt; setDevice ( getDevice ()); window . addEventListener ( &apos;resize&apos; , onResize); return () =&gt; window . removeEventListener ( &apos;resize&apos; , onResize); }, []); const { spacing, fontSize } = DEVICE [device]; useFrame ( () =&gt; { const nav = group. current ; if (!nav) return ; const view = viewport. getCurrentViewport (camera, [ 0 , 0 , 15 ]); nav. position . set ( 0 , -view. height / 2 + 0.2 , 15.1 ); nav. children . forEach ( ( child, index ) =&gt; { child. position . x = (index - (items. length - 1 ) / 2 ) * spacing; }); }); return ( &lt; group ref = {group} renderOrder = {10} &gt; {items.map(({ label }) =&gt; ( &lt; Text key = {label} fontSize = {fontSize} color = &quot;white&quot; anchorX = &quot;center&quot; anchorY = &quot;middle&quot; outlineWidth = {0} outlineBlur = &quot;20%&quot; outlineColor = &quot;#000&quot; outlineOpacity = {0.5} renderOrder = {10} &gt; {label} &lt;/ Text &gt; ))} &lt;/ group &gt; ); } 3.4 根组件容器 FluidGlass export function FluidGlass ( { mode = &apos;lens&apos; , scale = 0.2 , ior = 1.15 , thickness = 2 , chromaticAberration = 0.05 , anisotropy = 0.01 , }: FluidGlassProps ) { const Wrapper = mode === &apos;bar&apos; ? Bar : mode === &apos;cube&apos; ? Cube : Lens ; const modeProps = { scale, ior, thickness, chromaticAberration, anisotropy, transmission : 1 , roughness : 0 , }; return ( &lt; Canvas camera = {{ position: [ 0 , 0 , 20 ], fov: 15 }} gl = {{ alpha: true }}&gt; &lt; Suspense fallback = {null} &gt; &lt; ScrollControls damping = {0.2} pages = {3} distance = {0.4} &gt; {mode === &apos;bar&apos; &amp;&amp; ( &lt; NavItems items = {[ { label: &apos; Home &apos;, link: &apos;&apos; }, { label: &apos; About &apos;, link: &apos;&apos; }, { label: &apos; Contact &apos;, link: &apos;&apos; }, ]} /&gt; )} &lt; Wrapper modeProps = {modeProps} &gt; &lt; Scroll &gt; &lt; Typography /&gt; &lt; Images /&gt; &lt;/ Scroll &gt; &lt; Scroll html /&gt; &lt; Preload /&gt; &lt;/ Wrapper &gt; &lt;/ ScrollControls &gt; &lt;/ Suspense &gt; &lt;/ Canvas &gt; ); } 4. 架构与渲染流程图 4.1 核心渲染与数据流向 ( core-pipeline ) 4.2 每帧执行流程 ( frame-pipeline ) 5. 玻璃渲染方案对比 维度 Fluid Glass ( /fluid-glass.html ) Studio 四 Pass GLSL ( / ) SVG Filter ( /glass-svg.html ) 渲染技术 Three.js + R3F + FBO 离屏渲染 WebGL 原生四 Pass (Offscreen FBO) 原生 DOM + SVG 滤镜 形状表现 3D 网格模型 ( .glb 几何体) 2D 符号距离场 (SDF) HTML DOM 盒模型 折射机制 物理法线折射 ( MeshTransmissionMaterial ) GLSL 片元多重采样 feDisplacementMap 像素位移 色散支持 分通道 RGB 物理色散 Shader 手动偏移采样色散 伪色相偏移 适用场景 3D 模型交互、物理透镜视觉特效 参数化玻璃材质编辑器、高斯模糊背景 轻量级纯网页 HTML UI 装饰 6. 完整源代码 ( FluidGlass.tsx ) /* eslint-disable react/no-unknown-property */ import { memo, Suspense , useEffect, useMemo, useRef, useState, type ReactNode } from &apos;react&apos; ; import * as THREE from &apos;three&apos; ; import { Canvas , createPortal, useFrame, useThree } from &apos;@react-three/fiber&apos; ; import { Image , MeshTransmissionMaterial , Preload , Scroll , ScrollControls , Text , useFBO, useGLTF, useScroll, } from &apos;@react-three/drei&apos; ; import { easing } from &apos;maath&apos; ; export type FluidMode = &apos;lens&apos; | &apos;cube&apos; | &apos;bar&apos; ; export type FluidGlassProps = { mode?: FluidMode ; scale?: number ; ior?: number ; thickness?: number ; chromaticAberration?: number ; anisotropy?: number ; }; type ZoomMaterial = THREE . MeshBasicMaterial &amp; { zoom : number }; type ModeWrapperProps = { children?: ReactNode ; glb : string ; geometryKey : string ; followPointer?: boolean ; lockToBottom?: boolean ; modeProps : Record &lt; string , unknown &gt;; }; const LENS_GLB = &apos;/assets/3d/lens.glb&apos; ; const CUBE_GLB = &apos;/assets/3d/cube.glb&apos; ; const BAR_GLB = &apos;/assets/3d/bar.glb&apos; ; useGLTF. preload ( LENS_GLB ); useGLTF. preload ( CUBE_GLB ); useGLTF. preload ( BAR_GLB ); const ModeWrapper = memo ( function ModeWrapper ( { children, glb, geometryKey, lockToBottom = false , followPointer = true , modeProps = {}, }: ModeWrapperProps ) { const meshRef = useRef&lt; THREE . Mesh &gt;( null ); const gltf = useGLTF (glb); const nodes = gltf. nodes as Record &lt; string , THREE . Mesh &gt;; const buffer = useFBO (); const { viewport, gl } = useThree (); const scene = useMemo ( () =&gt; new THREE . Scene (), []); const geoWidthRef = useRef ( 1 ); const pointerNDC = useRef ( new THREE . Vector2 ()); useEffect ( () =&gt; { const geo = nodes[geometryKey]?. geometry ; if (!geo) return ; geo. computeBoundingBox (); const box = geo. boundingBox ; geoWidthRef. current = box ? box. max . x - box. min . x || 1 : 1 ; }, [nodes, geometryKey]); useEffect ( () =&gt; { const canvas = gl. domElement ; const onMove = ( event: PointerEvent ) =&gt; { const rect = canvas. getBoundingClientRect (); if (rect. width === 0 || rect. height === 0 ) return ; pointerNDC. current . set ( ((event. clientX - rect. left ) / rect. width ) * 2 - 1 , -((event. clientY - rect. top ) / rect. height ) * 2 + 1 , ); }; window . addEventListener ( &apos;pointermove&apos; , onMove); return () =&gt; window . removeEventListener ( &apos;pointermove&apos; , onMove); }, [gl]); useFrame ( ( state, delta ) =&gt; { const mesh = meshRef. current ; if (!mesh) return ; const { gl, camera } = state; const view = viewport. getCurrentViewport (camera, [ 0 , 0 , 15 ]); const destX = followPointer ? (pointerNDC. current . x * view. width ) / 2 : 0 ; const destY = lockToBottom ? -view. height / 2 + 0.2 : followPointer ? (pointerNDC. current . y * view. height ) / 2 : 0 ; easing. damp3 (mesh. position , [destX, destY, 15 ], 0.15 , delta); if (modeProps. scale == null ) { const maxWorld = view. width * 0.9 ; mesh. scale . setScalar ( Math . min ( 0.15 , maxWorld / geoWidthRef. current )); } gl. setRenderTarget (buffer); gl. render (scene, camera); gl. setRenderTarget ( null ); gl. setClearColor ( 0x5227ff , 1 ); }); const { scale, ior, thickness, anisotropy, chromaticAberration, ...extraMat } = modeProps as FluidGlassProps &amp; Record &lt; string , unknown &gt;; return ( &lt;&gt; {createPortal(children, scene)} &lt; mesh scale = {[viewport.width, viewport.height , 1 ]}&gt; &lt; planeGeometry /&gt; &lt; meshBasicMaterial map = {buffer.texture} transparent /&gt; &lt;/ mesh &gt; &lt; mesh ref = {meshRef} scale = {(scale as number | undefined ) ?? 0.15 } rotation-x = {Math.PI / 2 } geometry = {nodes[geometryKey]?.geometry} &gt; &lt; MeshTransmissionMaterial buffer = {buffer.texture} ior = {(ior as number | undefined ) ?? 1.15 } thickness = {(thickness as number | undefined ) ?? 5 } anisotropy = {(anisotropy as number | undefined ) ?? 0.01 } chromaticAberration = {(chromaticAberration as number | undefined ) ?? 0.1 } { ...extraMat } /&gt; &lt;/ mesh &gt; &lt;/&gt; ); }); function Lens ( { children, modeProps }: { children?: ReactNode; modeProps: Record&lt; string , unknown &gt; } ) { return ( &lt; ModeWrapper glb = {LENS_GLB} geometryKey = &quot;Cylinder&quot; followPointer modeProps = {modeProps} &gt; {children} &lt;/ ModeWrapper &gt; ); } function Cube ( { children, modeProps }: { children?: ReactNode; modeProps: Record&lt; string , unknown &gt; } ) { return ( &lt; ModeWrapper glb = {CUBE_GLB} geometryKey = &quot;Cube&quot; followPointer modeProps = {modeProps} &gt; {children} &lt;/ ModeWrapper &gt; ); } function Bar ( { children, modeProps = {} }: { children?: ReactNode; modeProps?: Record&lt; string , unknown &gt; } ) { return ( &lt; ModeWrapper glb = {BAR_GLB} geometryKey = &quot;Cube&quot; lockToBottom followPointer = {false} modeProps = {{ transmission: 1 , roughness: 0 , thickness: 10 , ior: 1.15 , color: &apos;# ffffff &apos;, attenuationColor: &apos;# ffffff &apos;, attenuationDistance: 0.25 , ...modeProps , }} &gt; {children} &lt;/ ModeWrapper &gt; ); } function Images ( ) { const group = useRef&lt; THREE . Group &gt;( null ); const data = useScroll (); const { height } = useThree ( ( state ) =&gt; state. viewport ); useFrame ( () =&gt; { const children = group. current ?. children ; if (!children || children. length &lt; 5 ) return ; const zoom = ( index: number , value: number ) =&gt; { ((children[index] as THREE . Mesh ). material as ZoomMaterial ). zoom = value; }; zoom ( 0 , 1 + data. range ( 0 , 1 / 3 ) / 3 ); zoom ( 1 , 1 + data. range ( 0 , 1 / 3 ) / 3 ); zoom ( 2 , 1 + data. range ( 1.15 / 3 , 1 / 3 ) / 2 ); zoom ( 3 , 1 + data. range ( 1.15 / 3 , 1 / 3 ) / 2 ); zoom ( 4 , 1 + data. range ( 1.15 / 3 , 1 / 3 ) / 2 ); }); return ( &lt; group ref = {group} &gt; &lt; Image position = {[-2, 0 , 0 ]} scale = {[3, height / 1.1 ]} url = &quot;/assets/demo/cs1.webp&quot; /&gt; &lt; Image position = {[2, 0 , 3 ]} scale = {3} url = &quot;/assets/demo/cs2.webp&quot; /&gt; &lt; Image position = {[-2.05, -height , 6 ]} scale = {[1, 3 ]} url = &quot;/assets/demo/cs3.webp&quot; /&gt; &lt; Image position = {[-0.6, -height , 9 ]} scale = {[1, 2 ]} url = &quot;/assets/demo/cs1.webp&quot; /&gt; &lt; Image position = {[0.75, -height , 10.5 ]} scale = {1.5} url = &quot;/assets/demo/cs2.webp&quot; /&gt; &lt;/ group &gt; ); } function Typography ( ) { const DEVICE = { mobile : { fontSize : 0.2 }, tablet : { fontSize : 0.4 }, desktop : { fontSize : 0.6 }, }; const getDevice = (): keyof typeof DEVICE =&gt; { const width = window . innerWidth ; return width &lt;= 639 ? &apos;mobile&apos; : width &lt;= 1023 ? &apos;tablet&apos; : &apos;desktop&apos; ; }; const [device, setDevice] = useState (getDevice); useEffect ( () =&gt; { const onResize = ( ) =&gt; setDevice ( getDevice ()); window . addEventListener ( &apos;resize&apos; , onResize); return () =&gt; window . removeEventListener ( &apos;resize&apos; , onResize); }, []); return ( &lt; Text position = {[0, 0 , 12 ]} fontSize = {DEVICE[device].fontSize} letterSpacing = {-0.05} outlineWidth = {0} outlineBlur = &quot;20%&quot; outlineColor = &quot;#000&quot; outlineOpacity = {0.5} color = &quot;white&quot; anchorX = &quot;center&quot; anchorY = &quot;middle&quot; &gt; React Bits &lt;/ Text &gt; ); } function NavItems ( { items }: { items: { label: string ; link: string }[] } ) { const group = useRef&lt; THREE . Group &gt;( null ); const { viewport, camera } = useThree (); const DEVICE = { mobile : { max : 639 , spacing : 0.2 , fontSize : 0.035 }, tablet : { max : 1023 , spacing : 0.24 , fontSize : 0.035 }, desktop : { max : Infinity , spacing : 0.3 , fontSize : 0.035 }, }; const getDevice = (): keyof typeof DEVICE =&gt; { const width = window . innerWidth ; return width &lt;= DEVICE . mobile . max ? &apos;mobile&apos; : width &lt;= DEVICE . tablet . max ? &apos;tablet&apos; : &apos;desktop&apos; ; }; const [device, setDevice] = useState (getDevice); useEffect ( () =&gt; { const onResize = ( ) =&gt; setDevice ( getDevice ()); window . addEventListener ( &apos;resize&apos; , onResize); return () =&gt; window . removeEventListener ( &apos;resize&apos; , onResize); }, []); const { spacing, fontSize } = DEVICE [device]; useFrame ( () =&gt; { const nav = group. current ; if (!nav) return ; const view = viewport. getCurrentViewport (camera, [ 0 , 0 , 15 ]); nav. position . set ( 0 , -view. height / 2 + 0.2 , 15.1 ); nav. children . forEach ( ( child, index ) =&gt; { child. position . x = (index - (items. length - 1 ) / 2 ) * spacing; }); }); return ( &lt; group ref = {group} renderOrder = {10} &gt; {items.map(({ label }) =&gt; ( &lt; Text key = {label} fontSize = {fontSize} color = &quot;white&quot; anchorX = &quot;center&quot; anchorY = &quot;middle&quot; outlineWidth = {0} outlineBlur = &quot;20%&quot; outlineColor = &quot;#000&quot; outlineOpacity = {0.5} renderOrder = {10} &gt; {label} &lt;/ Text &gt; ))} &lt;/ group &gt; ); } export function FluidGlass ( { mode = &apos;lens&apos; , scale = 0.2 , ior = 1.15 , thickness = 2 , chromaticAberration = 0.05 , anisotropy = 0.01 , }: FluidGlassProps ) { const Wrapper = mode === &apos;bar&apos; ? Bar : mode === &apos;cube&apos; ? Cube : Lens ; const modeProps = { scale, ior, thickness, chromaticAberration, anisotropy, transmission : 1 , roughness : 0 , }; return ( &lt; Canvas camera = {{ position: [ 0 , 0 , 20 ], fov: 15 }} gl = {{ alpha: true }}&gt; &lt; Suspense fallback = {null} &gt; &lt; ScrollControls damping = {0.2} pages = {3} distance = {0.4} &gt; {mode === &apos;bar&apos; &amp;&amp; ( &lt; NavItems items = {[ { label: &apos; Home &apos;, link: &apos;&apos; }, { label: &apos; About &apos;, link: &apos;&apos; }, { label: &apos; Contact &apos;, link: &apos;&apos; }, ]} /&gt; )} &lt; Wrapper modeProps = {modeProps} &gt; &lt; Scroll &gt; &lt; Typography /&gt; &lt; Images /&gt; &lt;/ Scroll &gt; &lt; Scroll html /&gt; &lt; Preload /&gt; &lt;/ Wrapper &gt; &lt;/ ScrollControls &gt; &lt;/ Suspense &gt; &lt;/ Canvas &gt; ); } 7. 相关文档与参考资源 7.1 本项目不同玻璃实现模块与页面 模块页面 源码目录 技术方案与特征 流体 3D 玻璃透镜 ( /fluid-glass.html ) src/fluid-glass/ Three.js + R3F + FBO 离屏渲染 + GLB 3D 模型透射折射 材质实验室主工作台 ( / ) src/ ( App.tsx , shaders/ ) WebGL2/WebGPU + 四 Pass 高斯模糊与 SDF 物理光学着色 毛玻璃悬浮交互按键 ( /glass-buttons.html ) src/glass-buttons/ DOM 捕获 + Shader Overlay 玻璃浮层 SVG 滤镜轻量玻璃 ( /glass-svg.html ) src/glass-svg/ 纯 DOM + SVG feDisplacementMap 位移滤镜 7.2 核心参考库与规范 React Three Fiber (R3F) 官方文档 ：React 声明式 Three.js 渲染器。 @react-three/drei - MeshTransmissionMaterial ：基于物理折射率 (IOR)、色散与厚度的体透射材质组件。 Three.js 官方文档 - WebGLRenderTarget (FBO) ：离屏帧缓冲对象 API 规范。 pmndrs/maath ：用于 3D 物理运动与相机平滑插值的数学库 ( easing.damp3 )。</description><pubDate>Thu, 27 Aug 2026 11:23:01 +0000</pubDate></item><item><title>我拿 4 个真实前端任务试了 GLM-5.3 Flash：代码一遍跑通，账单 4 分钱</title><link>https://juejin.cn/post/7678531174247874586</link><guid isPermaLink="false">juejin-6fd75c5393c1d18a</guid><description>昨天智谱把 GLM-5.3 Flash 上线并开源了。就是上周在 OpenCode 和 OpenRouter 上刷调用量纪录、被大家叫「牛来」的那个匿名模型 Ox-Alpha，测试流量全程跑在国产芯片上，昨天才认领。 朋友圈都在转这条新闻，我没转。我一个写业务的前端，关心点很朴素：它能不能干活，跑一次多少钱。今天上午扔了 4 个真实任务过去，结果有点超出预期。 先说我怎么测的 不跑跑分题。跑分题是模型公司公关稿的素材，不是我的日子。我从自己日常里抽了 4 件活，脱敏后原样扔给它： 一段有竞态问题的搜索联想代码，线上偶现「输入和结果对不上」那种； 从零写一个并发请求控制器，写完直接 node 跑； 一段赶工写出来的 React 取数代码，坑是我自己埋的； 一个我自己造的页面，埋了 4 个布局 bug，截图给它诊断。 结论放前面：前三项超预期，第四项及格但漏了一个。总账单 4 分钱人民币。对，4 分。 任务一：它比我先找到竞态 搜索联想这个 bug 是经典款：fetch 是异步的，谁后返回谁渲染，迟到的旧响应会把新结果盖掉。这种 bug 我自己写过，也踩线上过，所以我想看看它要多久能定位。 结果一次命中，还给了张复现时序表，解释为什么「只有网络抖动的时候才偶现」。 真正让我停下来的是两个我没问的细节。一个是中文输入法：组词期间（nihao → 你好）的 input 事件会触发中间态搜索，得用 isComposing 过滤。另一个是 render 如果用 innerHTML 拼联想词，有 XSS。我回头翻了自己的 prompt，确认这两件事我一个字都没提。 修复方案也有分寸：序号守卫是根治，AbortController 是减负，防抖只是锦上添花。哪部分管正确性、哪部分管体验，分得清清楚楚。 let latestId = 0 ; async function search ( keyword ) { const myId = ++latestId; // 最后一次触发持有最大值 // ... 请求逻辑 ... if (myId !== latestId) return ; // 迟到的旧回调，没资格渲染 render (cache[keyword]?. items || []); } 值过班的人都知道，这种表达在评审里有多稀缺。 任务二：它写的代码，我直接 node 跑 要求：限并发、失败自动重试、仍失败记 error 但不中断其它任务、结果顺序和入参一致，附自测代码。 我没先检查，直接跑。第一遍过。 跑完我才回去逐行读。retries: 1 是「失败后再试一次」，注释里写明；取索引用同步自增，worker 之间不竞争；失败占位不破坏顺序。风格保守，没什么炫技，挑不出错。 说句实话：这活平时交给新同事，我大概率要打回一次。 任务三：评审比我想的凶 那段 React 代码我埋了 4 个坑：Date.now() 进依赖数组、无竞态保护、无错误处理、首帧渲染 null。它全命中，按 P0/P1/P2 排好，还多抓了两个我没埋的：卸载后 setState、userId 不编码直接拼 URL。 Date.now() 那个坑，它一句话讲透：依赖数组每次渲染重新求值，时间戳必然变，effect 必然重跑，「直到某次渲染恰好落在同一毫秒才意外停止」。 看完我顺手在自己项目里 grep 了一把 Date.now()。建议你也 grep 一把。 修复版是显式状态机加 AbortController 清理，一个 cleanup 同时解决竞态、卸载更新、StrictMode 双执行。这要是同事写的评审意见，我得请人喝杯咖啡。 任务四：它漏了一个，我反而放心 测试素材是我自己造的页面，4 个布局 bug： 命中 3 个：弹窗被顶栏压住、徽标盖住价格、按钮文案截断，每个都给了能直接用的 CSS 修复。漏了头像拉伸变形。 但它补了个我没想到的角度：「三个互不相关的布局同时出问题，比较像整体样式出了状况」——建议先查 CSS 是不是 404、构建产物版本对不对、弹窗是不是渲染进了错误的父容器。 漏判和这个补充对冲下来，我反而觉得是加分。视觉能力的结论：能用，大概一年经验同事的水平，别指望火眼金睛。 算账：4 分钱 任务 耗时 输出 token（含推理） 竞态 bug 诊断 137s 5,544 并发控制器生成 70s 4,488 React 代码评审 147s 6,408 截图 UI 诊断 95s 5,228 输入 2,309 token，输出 21,668。限时 5 折价（到 9 月 9 日：输入 0.075 / 百万、输出 0.075/百万、输出 0.075/ 百万、输出 0.25/百万）算下来 0.0056 ，折合人民币 4 分钱。折扣结束后的原价（ 0.0056，折合人民币 4 分钱。折扣结束后的原价（ 0.0056 ，折合人民币 4 分钱。折扣结束后的原价（ 0.15/$0.50，本身也只有 GLM-5.3 的 1/10）跑同样一套，8 分。 同样输出量喂 Claude Opus 4.8（输出 75 / 百万），光输出 75/百万），光输出 75/ 百万），光输出 1.6，约 11 块 6。差近 300 倍。官方口径是 Flash 综合成本约 Opus 的 1/40，我按输出价实测，差距比这还夸张。 补一句实话：Flash 的「快」指价格和吞吐，不指等待时间。单次调用 1-2.5 分钟，吞吐约 48 token/秒——慢是因为想得细，79% 的输出是推理 token。赶时间关推理，省钱它就是答案。 换算成月更直观：每天扔 50 个这种任务，月账单几块钱，不到一杯奶茶。这个价格下，AI 调用不再是团队要审批的「福利」，是水电。 测完几句感想 测完最大的感受不是「模型真便宜」。 是当一个完整的前端任务只要 1 分钱，「用不用 AI」就彻底不是门槛了。门槛变成：你能不能判断它给的东西。任务一里看不出序号守卫才是根治、防抖只是优化，就会把防抖当修复上线，bug 照旧偶现；任务三里评审不出 Date.now() 进依赖是死循环，模型写得再对也不敢合。 工具越便宜，人值钱的那部分越贵。这大概是 AI 时代前端求生最真实的一句话。 我现在把它排进工作流的方式：初稿、初审交给 Flash；终判留给人——正确性、边界条件、取舍，这三样不外包。 附张速查表，选型时直接用： 模型 输入/百万 输出/百万 备注 GLM-5.3-Flash（5 折至 9/9） $0.075 $0.25 本次实测模型，原生多模态 GLM-5.3-Flash（原价） $0.15 $0.50 约为 GLM-5.3 的 1/10 Claude Opus 4.8 $15 $75 官方称 Flash 综合成本约其 1/40 你拿它跑过什么真实任务？评论区聊聊。我先说：任务四它漏掉头像变形的时候，我比它全对的时候更放心——知道边界在哪，才敢把它排进工作流。</description><pubDate>Thu, 27 Aug 2026 11:15:21 +0000</pubDate></item><item><title>🔥 AI 时代，Token 就是你的数字燃料！晒账单，赢好礼！</title><link>https://juejin.cn/post/7678580101931679753</link><guid isPermaLink="false">juejin-576b57f5ee16a9eb</guid><description>写代码、产文档、做方案、调项目…… 每一次 AI 调用，都在燃烧你的 Token 库存。 你的账单背后，藏着怎样的 AI 使用故事？ 别人的 Token 都烧在哪？哪个场景最“吃”Token？ 有没有省 Token 的野路子？ ——来掘金「 AI 用量排行榜 」晒图活动，一次聊透！ 三大赛道，按需选择 赛道 参与要求 🎁 阳光普照奖 发沸点 + 消耗清晰截图 ✨ 精选沸点奖 沸点 + 消耗截图 + 详细说明使用场景 &amp; Token 消耗情况 📢 传播达人奖 ① 在 朋友圈 / 小红书 / 微博 等任选 3个 外部平台发布 插件链接 + 你的Token截图＋使用感受；② 将外部分享截图发到掘金沸点 📝 怎么参与？ ①下载插件 ②安装保存截图 ③发沸点，3步完成挑战： Step 1・下载插件 打开插件链接： juejin.cn/aiusage/das… ，下载客户端并安装 ： Step 2：保存截图 安装配置好插件并保存自己的 Token 消耗截图 （防冒用提示：截图需要包含你的个人头像； Step 3・发布掘金沸点 将自己的感受截图整理后，在掘金发布沸点，带上话题：# AI用量排行榜 实例文案： 📊 写代码、写文档、调项目，原来我每天烧掉的 Token 比我以为的多多了 😂 点开一看：好家伙，最烧钱的是调 bug！ 推荐大家都去测测自己的 &quot;数字燃料&quot; 账单！ 掘金出品，值得一试👉 juejin.cn/aiusage/das… 沸点指路 ： 📅 活动时间 即日起 — 9月10日 23:59 💡 不知道写啥？给你4个创作方向 晒账单型：晒出消耗排行，聊聊写代码占多少、文档占多少、闲聊占多少，哪个最烧？ 干货经验型：分享你的省Token秘籍——Prompt优化、模型选型、提效技巧，比如《我把Token用量降低50%的3个实操技巧》。 场景故事型：讲一个Side Project背后的故事——烧了多少Token、踩过什么坑、最后收获了什么？ 对比测评型：横向测多款AI工具/模型，对比Token消耗与产出效率，给掘友排雷。 ✨ 获奖加分三件套：消耗截图 + 场景描述 + 技巧/心得，一个都不能少！ 🎁 奖品清单 奖项 名额 奖品 获奖条件 🥇 精选沸点奖 5名 罗技 Lift 人体工学鼠标 沸点写清场景 &amp; 消耗截图，综合内容质量 &amp; 互动热度评选 🥈传播达人奖 10名 新秀丽背包 在 朋友圈 / 小红书 / 微博 等任选 3个 外部平台发布 插件链接 + 你的Token截图＋使用感受；② 将外部分享截图发到掘金沸点综合内容质量评选 🎉 阳光普照奖 不限 1W 矿石 / 人 满足全部合规条件，沸点互动量（赞+评）≥5（刷量取消资格） 互动量 = 点赞 + 评论，自我刷量无效。想冲实物大奖，首选传播达人；想轻松参与，发沸点冲精选或阳光普照即可。 🏅 评选规则 本次评选各奖项均已以内容质量为主，官方评审内容价值， 互动量仅作为参考。 所有沸点话题均需要带上沸点话题和消耗截图。 传播达人奖在外部平台分享必须带上插件链接： juejin.cn/aiusage/dow… 防冒用提示： Token 消耗截图需要包含你的个人头像。 📢 传播奖专属参赛步骤（2步搞定！） Step 1： 在 朋友圈 / 小红书 / 微博 等任意3个外部平台， 发布 插件链接 + 你的Token消耗截图＋使用感受。 Step 2 · 把外部平台的分享截图整理好，在掘金发布沸点，带上话题 #AI 用量排行榜 即可。 晒账单、聊心得、赢好礼，下一个获奖者就是你 🎉 快来点击 [下载安装] ，发沸点吧！ 插件下载链接： juejin.cn/aiusage/dow… 沸点话题链接： AI用量排行榜 - 沸点 - 掘金 插件详细介绍： juejin.cn/vibe-work/7…</description><pubDate>Thu, 27 Aug 2026 09:58:42 +0000</pubDate></item><item><title>GLM5.3Flash 我“忍”你很久，今天“曝光”你！</title><link>https://juejin.cn/post/7678324732161851442</link><guid isPermaLink="false">juejin-c930c35f322815ed</guid><description>这几天忍着不发，可憋死我了！ 今天终于可以“曝光”它了！Ox 模型到底是谁，想必大家已经知道了。 没错，主角就是 GLM5.3Flash ，搞了一个星期的前戏，今天终于迎来高潮了！ 我有一段时间天天“怼”GLM，年初的时候甚至骂过它“若只”！ 但是我也说过很多次， GLM 国内综合实力最强，实战能力最强 ！这一点没毛病啊，老粉应该很清楚。 今天我就给大家盘一盘这个 Flash，好的坏的，我都会说！吹牛逼这种事情，全网会有很多人干，我就只展示我的测试结果，表达我的观点。 这一次，我的测试量还是非常大的！ 先快速给大家提炼一些关键信息，方便大家吃瓜！ ~ 马甲模型 Ox Alpha 刷爆了 OpenRouter 和 OpenCode 的历史榜单！ ~ 能力对标 Opus 4.8，价格嘛 1/40！！！（DS 要颤抖了） ~ 模型不大，只有 320B，能力超 GLM5.2，AA 大概 57 分（DS 又被压一头） ~ 全新架构，原生多模态啊，100 万上下文！！！ ~ 全国产卡推理 ~ 上线即开源 核心基准数据如下： 看到这些信息，我的脑中一闪而过的是： 一直斩杀别人的 DS 的斩杀线来了！ 当然，我是一个挑剔的人，我立马就能发现它的一些问题！ 上周，我是冲着 GLM5.5 去的，神神秘秘地给我忽悠到 GLM5.3Flash 了。当时我就很奇怪，这模型，你说它是小模型嘛，又有点强，你说大模型，好像又没有那么高智商。 好了，那些杂七杂八的说完了，我们可以回归模型本身了！我们使用一个模型，核心关注点主要是“性能”和“价格”， 价格基本清楚了，非常香，性能就成为最关键的变量了 。 GLM5.3Flash 到底怎么样，优点是什么，缺点是什么？我们就一点一点来看吧，看完了就知道了。 因为这次是一个原生多模态，所以我先给大家看一个非常明显的对比。 甲维斯的车库 这个例子，因为涉及到了多模态能力，所以之前 DeepSeek 和 GLM 都无法参赛！现在它们可以参赛了，而且刚好都是新鲜出炉。 这个题目的内容如下： 我在网上看到一个很有意思的例子，就是有人做了一个在线车库，可以快速切换车库中的各种车辆，以及显示这个载具的数据，中间显示具体的内容，可以通过鼠标拖动查看不同角度，然后可以切换涂漆，可以切换视角，可以自动旋转展示！网页配色，布局，内容我希望你尽可能还原参考图 2.png ，不同之处在于车库里的车子，以及骑车的人，以及车库的名字。车子我希望你做一辆自行车，一辆电瓶车，一辆机车，一辆三蹦子，一辆五菱宏光，一辆特斯拉，一辆小米，一辆保时捷，一套钢铁侠的经典战甲。具体型号你自己定，要主流的型号。骑车开车的人物根据参考图 1.jpg 的头部形象进行设计，做一个 3D Q 版卡通形象。车库的主人是 Jarvis。我的需求就是这样，细节你来完成，我只负责验收。使用单个 HTML 和前端技术完成，只输出最终结果页面。 然后这个题目是有两张参考图： 这是非常综合的一个例子！会考网页复刻能力、3D 建模能力，以及一些世界知识、物理知识！ 下面就直接上结果了。 GLM5.3Flash 的结果： GLM5.2 的结果： DeepSeek-V4-Flash-Vision-Exp 的结果： 这个对比结果应该很清晰。 GLM5.2 当时应该通过外置的视觉模型，抓取了一些简单的视觉信息，就开始开发了。所以整个颜色和内容都是对不上的。 DS 和 GLM5.3 这两位都有了原生多模态。所以整体还原度都不错。 这两个在布局的还原度上都已经很高了。 差别主要在细节和建模！ GLM5.3 的建模和渲染效果明显比 DS 要好很多！ 另外 DS 的左上角的配色莫名其妙多了 4 种，右下角的旋转控件也变了样式，还有整个页面的背景色，以及一些其他控件的颜色都有比较大的漂移。 左下角的鼠标图标和拖动提示也没有复刻出来！ 从这个例子来看，GLM5.3Flash，虽然后缀有个 Flash，但是它还是挺厉害的。 没想到它的 3D 建模能力好像也增强了。 浮屿世界 说起 3D 建模，我还让它帮我做了一个新的 Demo——《浮屿世界》 这个创意来自一个区块链游戏 Decentraland 。用多边形、低饱和、治愈温度风格构建一个虚拟世界。主角可以在世界里任意游走！ 我在原先的基础上加了 “每一个人都是一座孤岛” 的概念，让它设计了很多悬浮在天空中的岛屿。这个理念落地做得还是比较好的。虽然没法和产品级的游戏相比，但是作为一个原型设计工具，已经非常够用了。 它一次就创建了所有内容，而且主角是可以行走和跳跃的。里面包含了日夜、四季、天气的轮转。左上角有操作提示，右下角有探索情况。 我发现它还设计了交互功能，进入每个岛屿会显示岛屿的名字，然后还设计了一个可以交互的物品。比如椅子可以坐下！它的交互 UI 还是 Claude 风格的，深得我心！ 天文机械表 这个题目既考脑力，又考视觉呈现能力，还考网页制作能力！这个例子我已经说过很多次了，详细的考点，以及提示词，都有介绍过，在网站上也有贴。 我就直接上结果了： 我记得测试 GLM5.3 以及 GLM5.2 的时候，这个例子设计得都不错，各方面表现都不错。但是就是月相的呈现部分有比较明显的问题，而这点恰恰是比较重要的考点。 这一次，月相部分也完整地画出来了！ 这个例子中的四五个考点，基本都做到了！ 它这次的成功并非偶然： 从运行记录可以看到，它已经做了很多次多维度的验证了，而且还多次截图验证。 这个题目本来是考模型的注意力，现在好了，Agent 流程化处理，每次抓一个点优化，自然是给你整得好好的了。这个过程很好地体现了一点：GLM 在智能体流程和后训练的优化上已经非常成熟了。 有些模型它是不会想这么多的，即便它也有智能体，但是它并不会做这么多验证，即便做了，最后也还是有各种问题。 复刻游戏 《超级玛丽》和《坦克大战》已经成为我的必测项目了。 这都是老游戏，但是 AI 在复刻这些老游戏的时候，表现其实并不好。我最早测试的一批模型，结果非常拉跨。只有 Fable 5 一下子惊艳了我，然后后来新出的一些模型表现也稍微好了一些！ 坦克大战的结果如下： 地图和玩法基本复刻了，打墙和敌方坦克的逻辑，有些小 Bug。整体来说还不错，能看能玩。看来它是听劝，去好好抄了一下。 超级玛丽： 它的画面风格和坦克大战一样，还原度不算高。但是地图和玩法的还原度非常高。 奔跑的惯性、加速跳高、隐藏蘑菇、冲刺夺旗，这些细节做得非常到位。 这就很神奇了。 理论上它只是 GLM5.3 的快速轻量版，为什么效果比 GLM5.3 还好呢？可能是全新架构和视觉能力加持的缘故。 3D 台球 我同样也测试了开放题“3D 台球”。这个题目我不提供任何需求细节。我只是说“我想开发一个 3D 台球，你有什么想法”。然后跟它稍微探讨一下，主要是按它的思路来实现。 这个例子整体结果也不错！ 建模灯光这些都不错。 然后来看一下顶视图： 其它都问题不大，就是这个“球袋”的建模差点意思。第一个版建模是那种 1/4 圆，我提醒它优化一下，然后它就搞出来奇形怪状的洞口。这个点位的建模很多模型都搞不好，目前只有 Fable 5 和 Opus 5 默认建模不错，然后可以在你的提示下进行优化。 赛博朋克版清明上河图 这一题考基础常识、概念融合、创意、空间感、审美这些。本来这个题目是应该放在第一个的，为什么放在靠后的位置呢？主要是 GLM5.3Flash 在这个主题上并不是特别亮眼。 具体效果如下： 其实这个版本在描绘这个场景的时候，细节方面已经非常到位了，各种人物，飞行器，建筑都有，而且细节很充分。 但是空间感和色彩对比方面做得比较差，导致很多东西都叠在一起了。 另外还有两个比较明显的 Bug：一个是天空中的飞行器出现了倒着飞的情况，另一个是里面的人物行为特别抽象。 所有人物好像挂在空中一样，腿都是飘在空中左右摇摆的，那画面，晚上我都不敢多看。 这种可能就是模型参数的局限性了，当然即便是参数如 K3，它的最终表现也一般。这个例子的难度可能不仅仅在参数，还有很多其他东西。 射击游戏 因为我拿到这个模型之后，时间和 Token 非常充沛，所以我就多测了一些东西。包括上面的浮屿世界，以及下面的这个射击游戏。 我让它模仿 CF 或者 CS 开发了一个射击游戏： 这个首页看起来啊完成度还挺高的，有好多功能点！ 但是……它的游戏画面和建模……极其抽象： 无论是建模，还是材质，以及操控，都不是太好！ 这个例子比 K3 和 Opus 5 都要差太多了！ 排查问题 关于 OpenCode + Git + DSH 在用户目录运行，把系统卡死，硬盘耗尽的问题。这个问题其实最先是 GLM5.3Flash 找出来的。 这个问题，它整整花了一个小时： 它的运行日志也非常有意思： 关键发现， 意外结果， 重大发现， 突破性进展， 决定性发现， 元凶找到了 ， 非常关键的发现， 反转了， 我的失误！ 破案了！ 实锤了， 真凶落网！ 全部查清了， 意外！ 人赃并获！ 经过层层排查，最终的关键问题给排查出来了。 从这个例子来看，它确实脑力有限，但是策略非常好。 它其实也走了很多错误的方向，但是在不断的复盘中和尝试中，终于找对了方向。 这个例子，如果大家有兴趣，我可以另起一篇，把它完整的思考过程给大家分享出来。过程还是挺有意义的。 这个例子，K3 失败了，GPT-5.6 失败了，DS Flash 失败了，Opus 5 成功了。Opus 5 只用了 13 分钟左右，几乎没有走任何弯路，一个 A/B 测试就锁定路径问题了。 例子大概就是这些了，因为例子比较多，我并没有展示执行过程。GLM5.3Flash 的执行过程还是非常有节奏的。 下面是我对这个模型的首测结论： 我们可能没有等来一个更大的模型，而是等来了一个更全面、更快、更实用、更便宜的模型！ 当时我还不知道这个模型具体叫什么，也不知道它的尺寸，只是写了一个初步的感受。 现在它的全貌基本清晰了： 这只是一个 320B（激活 18B）的“小模型”， DS Flash 是一个 284B，激活 13B 的模型！ 智谱的意图已经很清晰了，这个模型不是去打榜的，而是奔着性价比去的， 第一个要对标和干翻的肯定就是 DS Flash ，更准确地来说是要对标现在的 DeepSeek-V4-Flash-Vision-Exp！ 卧槽，DS 这个名字实在是太长了，本来已经很长了，现在加上 Vision Exp 简直了！ DS Flash 我之前详细地测试过，Vision Exp 测得并不多，因为之前对 DS Pro 比较失望，后续模型我就没有太大动力去搞了。 从我自身的感受来看，GLM5.3Flash 在前端、3D 建模等方面肯定是比 DSV 厉害，而 Agent 工作流大家都不错，现在大家都有视觉能力了，都可以有视觉反馈验证了，这部分在形式上差不多。但是深入分析的能力还是 GLM5.3Flash 略胜一筹。 GLM5.3Flash 的问题是，它的 Token 速度本身也不算慢，但是它每个例子消耗的时间非常多。按理说它是一个 Flash 模型，应该特别快才对，但是我实际使用过程中，发现它的处理时间特别长。好多例子，都是半个小时起步。一个需求跑 1 小时也很常见。 由于我拿到这个模型的时候只有开启和关闭选项，开启的情况应该是对应 Max，所以可能和 Max 也有一定的关联！ 但是整体来说，就是 GLM5.x 系列模型处理时间都比较久，相比而言， Opus 系列的模型，处理效率是非常高的，Token 的利用率也极高。 总而言之言而总之，这个模型的能力是没问题的，作为一个多模态 Flash 模型，有这个水平，已经很屌了，很多效果其实比 GLM5.3 好的，因为它有多模态。 再配上它这个价格就非常香了。 我还没有具体看到价格，我获取到的信息是这样的：模型定价为 1/10 的 GLM-5.3，限时折扣为 1/20 的 GLM-5.3， 1/40 的 Opus 4.8， 定价低于 DS-V4-Flash，限时折扣定价远低于 DS-V4-Flash 的谷时价格 。 这么一来，GLM5 套餐太贵买不起的话，直接搞个 API 也挺香的！ API 价格降低 10 倍的话，理论上套餐的用量也应该翻十倍才对，如果真的能如此，那套餐的性价比也就不错了。 看来年初赌 GLM 赌对了，它在综合能力、性价比、上限下限等方面，已经妥妥地拿下国产第一的交椅了。 刚刚看到，智谱也开始玩重置了，而且套餐配额 ×3，API 对折，看来用上国产芯片之后，算力充沛了不少。 做个最终总结 不管外面怎么传，核心就这些： 论能力肯定没法和顶级模型比，同时速度比较慢，慢得不像 Flash，但是下限高，各方面能力都不错，重点——便宜！ 这不是一个打榜模型，奔着实用来的 ！</description><pubDate>Thu, 27 Aug 2026 03:12:49 +0000</pubDate></item><item><title>GLM-5.3-Flash 发布：追平 Opus 4.8 的智力，1/40 的价格，跑在国产芯片上</title><link>https://juejin.cn/post/7678214547980582952</link><guid isPermaLink="false">juejin-46df7fd7c2d6573c</guid><description>今天，智谱上线并开源了 GLM-5.3-Flash（320B-A18B）——GLM-5 系列第一个原生多模态模型。 官方通报浓缩成一句：Artificial Analysis 综合智能指数 57 分，与 Claude Opus 4.8 持平；限时折扣价 4.5 美分一个任务，是 Opus 4.8 的 1/40。 翻译一下：你之前按奢侈品价格省着用的那种智力，现在按民用电价供电。 前沿智能要不要绑定前沿价格？这是智谱今天想撬动的行业默认设定。 先看官方公告原文： 公告不长，但有三条容易被略过的背景信息值得标出： MIT 许可证。 320B-A18B 直接以 MIT 协议发布——主流开源协议里限制最少的一档，商用、修改、再分发均不设附加条件。「开源」在这里不是一个需要打折理解的词。 1M token 上下文窗口。 和「原生多模态」并列写在公告第一行卖点里，百万级上下文是出厂配置，不是后挂的长文本方案。 发布即全渠道在线。 公告原话是「现已可在所有官方平台获取」：HuggingFace 权重、API、Coding Plan、ZCode、chat.z.ai 同步就位，没有渐进放量。 传播速度也是背景的一部分：公告于当晚 22:12 发出，一个半小时后浏览量 96 万、点赞 1.1 万。 前沿智能，一直是奢侈品定价 过去两年，行业里有一条不成文的等式： 前沿智力 = 前沿价格 。 独立开发者不敢让 Agent 跑过夜——一觉醒来，账单可能比房租贵。 创业团队把「上不上旗舰模型」当成融资级别的架构决策。 重度用户人均一套省钱秘籍：截断上下文、控制轮数、能降级就降级。 姿势各不相同，本质是同一件事： 按 token 计费的智力，得省着用。 牛来，原来是它 发布前有个插曲。 智谱以匿名模型「Ox-Alpha」把 GLM-5.3-Flash 放上 OpenCode 和 OpenRouter 收集真实反馈，中文社区给它起了个名字： 牛来 。 它当周就成为双平台最受欢迎的模型，创下 OpenCode 和 OpenRouter 调用量新高。 也就是说，不少人在不知道它是谁、不知道多少钱的情况下已经用过了，体感是「这模型又强又便宜」。 还有个当时没人知道的细节： 这些流量全部由国产芯片承载。 性能：贴着 Opus 4.8 打 先看硬数据。 GLM-5.3-Flash：320B 总参数，18B 激活，45 层，GLM-5 系列首个原生多模态模型。 AA 综合智能指数（v4.1.1）57 分，进入全球前沿模型区间，与 Anthropic 最受欢迎的 Claude Opus 4.8 持平；4.5 美分/任务的折扣价，此前这个智力水平大约要 10 倍价格才买得到。 编码与 Agent 基准全面超过参数量大它一倍的 GLM-5.2：DeepSWE v1.1 63.4 vs 46.2，AutomationBench 48.8 vs 26.2。 对 Opus 4.8 则是贴身缠斗：DeepSWE 63.4 vs 58.0，Toolathlon Verified 78.4 vs 76.2，AutomationBench 48.8 vs 41.0，多项反超；自研 Z.ai Code Bench 体感评估里，max effort 下 29.0 vs 29.5，几乎打平。 一个模型，能力追平头部旗舰，价格是它的四十分之一。 中间的差价不是补贴，是架构。 架构：为 1/40 的价格而生 三个关键词。 混合注意力。 首个采用稀疏注意力 + 线性注意力混合架构的开源前沿模型：线性注意力靠递归捕获局部依赖，稀疏注意力用轻量级索引器召回全局上下文，长上下文的服务成本被结构性压低。 IndexPool。 通过加权池化，把索引器的 4 个缓存向量压成 1 个，专门削减 1M token 上下文下索引器的时延和内存开销。 mHC（流形约束超连接）。 进一步提升模型的 scaling 效率。 再算一笔总账：总参数与 GLM-4.5 相当（320B vs 355B），但激活参数 32B→18B，层数 92→45，几乎减半；叠加最新的 30T token 多模态预训练语料。 结果是：对比 GLM-5.3，注意力计算量降 3.01 倍，KV 缓存降 4.44 倍；在 GLM-5.3、DeepSeek-V4-Flash、Kimi-K3 的对比里，单 token 注意力计算量最低。 克制地说，短板也有：KV 缓存仍略高于 Kimi-K3 和 DeepSeek-V4-Flash，官方承认这是下一步的优化方向。 视觉进编码循环，编码进专业工作 第一个案例，已经超出「编程」的字面范畴。 不给任何外部素材，GLM-5.3-Flash 在 Blender 里自主运行 16 个小时，搭出一套约 400 平方米的专业主厨自宅与测试厨房。 几何、材质、光照、空间关系要在所有视角下保持一致——把世界知识变成可检验的 3D 结构，Coding 成了模型表达并验证自己所知的代理。 第二个案例，闭环到像素。 在 ZCode 里，Browser Use Agent 和 Computer Use Agent 让模型在代码、浏览器、图形界面之间协同：自己写、自己渲染、自己看、自己改。 很多问题只有渲染出来、交互起来才会暴露——视觉能力原生内置后，模型能自己判断什么时候该「看一眼」。 第三个案例，生成即可交付。 金融：从有来源的金融研究、报告生成到建模分析全流程跑通，参考依据可追溯。 法律：审查合同的费用、账户与责任条款，按律师惯例批注留痕；起草律师函、合同与诉讼文书，格式符合实务规范。 专业工作基准 GDPval-AA v2 拿到 1773 分，是全部对比模型（含 Opus 4.8 的 1582、GPT-5.6 Terra 的 1571）里最高的；OfficeQA Pro 62.4，比 Opus 4.8 的 48.9 高出一截。 诚实注脚：BabyVision 等纯视觉感知基准上，与 GPT-5.6 Terra（61.6）、Gemini 3.7 Flash（70.9）仍有差距——它的视觉强项在「带工具的推理与编码」，不是裸眼感知。 真正值得记录的三件事 比模型本身更值得说的是这三件。 第一，国产芯片跑前沿模型，被大规模验证了。 过去一周，Ox-Alpha 的全部流量由国产芯片集群承载——数万张国产加速卡，自研高带宽互联。为克服单卡算力与内存的限制，智谱在 SGLang 基础上构建了专用推理引擎：生产级 EPD（Encode–Prefill–Decode）分离式架构、W8A8 量化、INT8/FP8/BF16 混合缓存量化、Layer Split。 对比同一硬件上的初始基线，端到端服务性能提升 3 倍， 硬件效率和单 token 成本达到与主流英伟达 GPU 相当的水平。 彩蛋是：这套推理引擎的开发本身，被 GLM-5.3 驱动的 infra agent 大幅加速——帮工程师写算子、诊断性能瓶颈、改进部署栈。 模型优化系统，系统承载模型。 第二，前沿智能的价格锚被重设了。 主公告发出十分钟后，Z.ai 团队的 Zixuan Li 补发了官方定价：GLM-5.3-Flash 通过官方 Z.ai API 即享五折，为期两周——并明确写了「该折扣也适用于第三方模型聚合商」。 落到账单上的数字：折后输入 0.075 、输出 0.075、输出 0.075 、输出 0.25、缓存输入 0.015 。放进同一张图里看更直观—— C l a u d e S o n n e t 5 的输出标价 0.015。放进同一张图里看更直观——Claude Sonnet 5 的输出标价 0.015 。放进同一张图里看更直观 —— Cl a u d e S o nn e t 5 的输出标价 10，GPT-5.6 Terra 标到 12 ，最便宜的 G e m i n i 3.7 F l a s h 也要 12，最便宜的 Gemini 3.7 Flash 也要 12 ，最便宜的 G e mini 3.7 Fl a s h 也要 7.5；GLM-5.3-Flash 即便按原价（输出 $0.5）也只是这些数字的零头，缓存输入折后更是只剩最低竞品的十分之一。 4.5 美分买到一个此前要 10 倍价格的任务智力。能力-成本曲线被推到新位置之后，所有「旗舰模型」的定价都需要重新解释。 第三，权重开源。 320B-A18B 的权重以 MIT 许可证放在 HuggingFace，SGLang、vLLM、TokenSpeed 开箱可跑。1/40 的价格锚，加上一张几乎没有使用门槛的开源协议，留给 API 定价者的缓冲区不多了。 如何上手 API ： BigModel 开放平台 、 Z.ai （Z.ai 官方渠道限时两周五折中，第三方聚合商同享） 在线体验 ： chat.z.ai 、 智谱清言 App GLM Coding Plan ： 已全量接入 ，可用配额是 GLM-5.3 的 3 倍，每天限量发放 1 万张体验卡 ZCode ： zcode.z.ai 开源权重 ： huggingface.co/zai-org/GLM… 官方博客的结论适合放在最后：前沿智能不必伴随前沿成本——这不是某一个技巧，而是 架构、语料、基础设施三层协同 的结果，而这套 recipe 正被用于更大的模型。 如果你还在按 token 省着用旗舰模型，不妨把这个开源模型加进默认选项，试一周。 参考 智谱官方博客：GLM-5.3-Flash — Frontier Intelligence, Flash Cost 微信公众号发布文：GLM-5.3-Flash——前沿智能进入普惠时代 开源权重 · HuggingFace 文中图表均截取自以上官方发布材料。</description><pubDate>Wed, 26 Aug 2026 17:08:23 +0000</pubDate></item><item><title>表格、文档、甘特、大屏、表单一站打通：pxcharts超级表格4.0正式上线！</title><link>https://juejin.cn/post/7678237761537916979</link><guid isPermaLink="false">juejin-2fe6f1028667d67b</guid><description>25 种字段 × 8 大视图：我们做了款Mini版飞书式多维表格 今天很高兴又和大家分享我们的AI创业项目。 花2年时间，我们做了一件「听起来有点疯狂」的事： 把多维表格协作平台完整做出来： 表格、看板、甘特图、日历、大屏、表单、文档、思维导图——全部围绕同一份数据，一处修改，多处同步。 技术栈： Next.js 14 + React 18 + TypeScript 5 + PostgreSQL + WebSocket ，开箱即用。 开源版地址： github.com/MrXujiang/p… 演示地址： pxcharts.turntip.cn 一、Pxcharts Saas 4.0诞生的背景 故事要从一次真实的项目管理说起。我们团队十几个人，需求、排期、Bug 全都存在一张张 Excel 里。每天早上的日常就是：「谁把表改乱了？」「最新版是哪一份？」「这个文件谁发我一下？」 后来大家陆续用上了商业多维表格产品，体验确实好——但新问题又来了： 数据放在别人服务器上，私有化部署贵得离谱，想加个定制字段类型根本不可能 。对很多中小企业和独立开发者来说，这是一道绕不过去的坎。 于是我们决定自己动手：做一款 功能对标主流商业产品、但可以完全私有化部署 的多维表格平台。它不仅仅是一个「能编辑单元格的网页」，而是一个完整的数据协作系统——有字段类型体系、有多视图、有公式、有自动化、有实时协同，最好还有 AI。 折腾了很久，它终于成型了，我们叫它 PxCharts 。 二、它是什么：一份数据，八种打开方式 一句话定位： PXCharts 是一个AI原生驱动的多维表格协作平台，让同一份数据在表格、看板、甘特图、日历、画廊、表单、层级、图表八种视图之间自由切换，并支持实时协同、自动化与 AI 能力。 下面展示一下它的多种“形态”： 1. 表格视图 2. 画廊视图 3. 任务看板 4. 表单视图 5. 甘特图 6. 日历视图 7. 可视化大屏 三、功能亮点深度分析 3.1 25 种字段类型：从文本到 AI，一张表装下所有业务 字段是多维表格的「原子」。我们在 lib/types.ts 里一口气定义了 25 种字段类型 ：文本、多行文本、数字、单选、多选、复选框、日期、图片、附件、链接、富文本、进度，还有进阶的 关联（relation）、查找引用（lookup）、汇总（rollup）、公式、AI 字段、人员、评分、货币、电话、邮箱、自动编号、按钮、条码、定位 。 业务价值： 「按钮字段」点一下就能触发一条自动化规则 ——比如把状态改成「已完成」并同步推送企微机器人。一个单元格，就是一个业务动作的入口。 3.2 八大视图：同一份数据，换一种看法就是另一个工具 数据只有一份，但看它的方式有八种： 表格、看板、甘特图、日历、画廊、表单、层级、图表 。项目经理看甘特，运营看看板，财务看图表，外部收集数据用表单视图一键生成填写页——谁也不用维护第二份数据。 业务价值：表格视图底层用了 react-window 虚拟滚动， 上万行数据也只渲染屏幕里看得见的那几十行 ，滚动依然顺滑。 3.3 公式引擎：32 个内置函数，Excel 用户无缝上手 我们手写了一个完整的公式解析引擎（词法分析 → 语法解析 → 求值），内置 32 个函数 ： SUM 、 AVERAGE 、 IF 、 CONCATENATE 、 DATEDIF ……函数命名和 Excel 完全对齐，老用户不用学第二套语法。 业务价值：公式重算做了 按行增量计算 ——编辑一个单元格只重算这一行的公式，不扫全表。优化后 2 万行的表，单次编辑的公式计算耗时从 86ms 降到了 0.006ms。 3.4 自动化引擎：3 种触发 × 7 种动作，表格自己会干活 「当记录创建/更新/按钮点击时，如果满足条件，就执行动作」——这套经典模型我们完整实现了。动作多达 7 种：更新字段、站内通知、Webhook、AI 生成、邮件、企业微信机器人、钉钉机器人 。 业务价值：引擎里专门写了一行防死循环逻辑—— 「自动化要更新的字段值没变化就跳过」 ，否则「更新字段」动作会触发「记录更新」事件，自己点燃自己。 3.5 实时协同：WebSocket 广播，你改的格子同事秒级看见 协同编辑是 pxcharts 多维表格平台的灵魂。我们基于 ws 库实现了独立的协同服务，与 Next.js 共用同一个 server.js 进程，同源部署零额外运维。单元格编辑、整行增删都会以 patch 消息广播给同房间的所有成员，同时还有 presence 在线状态消息，能看到「谁正在看这张表」。 业务价值：落库不是简单的 UPDATE ，而是 「SELECT ... FOR UPDATE 行锁 + 事务」的原子写 ——两个人同时改同一张表，谁也不会把谁的修改覆盖掉。 3.6 AI 全家桶：AI 字段、AI 建表、自然语言查询 AI 不是贴上去的装饰，而是长在字段体系里的：新增一个「AI 字段」，写好提示词模板（用 {{字段名}} 引用其他列），整列数据就能批量智能生成——比如根据「客户反馈」列自动产出「情感分析」列。 业务价值：还有两个隐藏技能—— 一句话生成整张表 （AI 建表，自动推断字段结构）和 自然语言查询 （输入「帮我筛出上周未完成的订单」，AI 翻译成筛选条件直接执行）。 3.7 32 个行业模板：9 大行业，拿来即用 空表格是最劝退的起点。我们内置了 32 个项目模板，覆盖电商、制造、餐饮、零售、物流、教育、人事、互联网、综合 9 大行业 ：电商订单管理、生产工单、设备巡检、学员档案、物流运单、招聘管理、财务报销……每个模板自带字段结构和示例数据，点一下就能开工。 业务价值：模板不是「示例截图」，是 完整的可运行项目 ——字段、视图、记录全部真实可编辑。 四、整体架构：一个进程把 REST 和 WebSocket 都扛了 架构上我们做了一个很务实的选择： Next.js 的 REST API 和协同 WebSocket 服务跑在同一个 server.js 进程里 。好处是部署只需要一个端口、一个进程， PM2 一拉就起来，小团队不用碰额外的消息中间件；同时客户端天然同源，不用折腾跨域和 HTTPS 下 ws/wss 的协议适配。 关键选型与理由： Next.js 14 ——前后端一体，116 个 API 路由全部是路由文件即接口； Zustand ——表格这种高频局部更新的状态，轻量 store 比全家桶顺手得多； PostgreSQL + JSONB ——字段结构灵活多变，一行 SELECT 拿全表，中小规模表格零 JOIN； ws ——原生轻量的 WebSocket 库，协同广播不需要更重的框架。 五、一次单元格编辑，背后跑了哪 6 步 设计上我们有个小讲究： 「先本地、后广播、再落库」的乐观更新 。用户敲完回车，自己屏幕上的格子立刻变（步骤 ②），不等网络；与此同时 patch 已经飞向服务端广播给同事（③④⑤），REST 落库（⑥）则在后台完成原子写入并顺路触发自动化规则。体验快，数据也不丢。 六、核心实现拆解：四段代码看门道 6.1 协同广播：房间模型，一个函数搞定 lib/collab-server.ts（简化） // 广播消息到房间内其他客户端（排除发送者） function broadcastToRoom(roomId, message, excludeClientId) { const clientIds = rooms.get(roomId) if (!clientIds) return // 房间不存在，直接忽略 const messageStr = JSON.stringify(message) clientIds.forEach((cid) =&gt; { if (cid === excludeClientId) return // 跳过发送者自己 const client = clients.get(cid) if (client &amp;&amp; client.ws.readyState === WebSocket.OPEN) { client.ws.send(messageStr) } }) } 大白话解读：每张表就是一个「房间」，谁打开这张表谁就进房。有人改了数据，服务端把消息挨个发给房间里其他人，唯独跳过发消息的自己——因为他的屏幕在步骤 ② 就已经更新过了。得意细节： 消息先 JSON.stringify 一次再循环发送 ，十几人的房间就省十几次序列化，协同高频场景下这是实打实的 CPU 节省。 6.2 原子写入：FOR UPDATE 行锁，并发改表不打架 lib/db.ts（简化） export async function atomicMutateTableRecords(tableId, mutate) { return transaction(async (client) =&gt; { // FOR UPDATE 行锁：同一张表的并发写入只能排队 const res = await client.query( &apos;SELECT data::text FROM tables WHERE id = 1 F O R U P D A T E ′ , [ t a b l e I d ] ) c o n s t r e c o r d s = J S O N . p a r s e ( r e s . r o w s [ 0 ] . d a t a ∣ ∣ ′ [ ] ′ ) c o n s t r e c o r d s : n e x t R e c o r d s , r e s u l t = m u t a t e ( r e c o r d s ) a w a i t c l i e n t . q u e r y ( ′ U P D A T E t a b l e s S E T d a t a = 1 FOR UPDATE&apos;, [tableId] ) const records = JSON.parse(res.rows[0].data || &apos;[]&apos;) const { records: nextRecords, result } = mutate(records) await client.query( &apos;UPDATE tables SET data = 1 FOR U P D A T E ′ , [ t ab l e I d ] ) co n s t recor d s = J SON . p a rse ( res . ro w s [ 0 ] . d a t a ∣ ∣ ′ [ ] ′ ) co n s t recor d s : n e x tR ecor d s , res u lt = m u t a t e ( recor d s ) a w ai t c l i e n t . q u ery ( ′ U P D A TEt ab l es SET d a t a = 1, updated_at = CURRENT_TIMESTAMP WHERE id = $2&apos;, [JSON.stringify(nextRecords), tableId] ) return result }) } 大白话解读：整张表的记录存在一个 JSONB 字段里，读写都特别简单，但「两个人同时写」怎么办？答案是一把行锁：改数据前先把这一行锁住，改完释放，后来的人自动排队。得意细节： 「读-改-写」三步全包在一个事务里 ，中途任何一步失败都会整体回滚，不会出现改了一半的脏数据。写入成功后还会异步打一份备份快照，数据可靠性再加一层。 6.3 公式引擎：32 个函数就是一张字典 lib/formula-engine.ts（简化） const BUILTIN_FUNCTIONS = { SUM: (...args) =&gt; args.reduce((s, v) =&gt; s + Number(v || 0), 0), IF: (cond, trueValue, falseValue) =&gt; cond ? trueValue : falseValue, DATEDIF: (start, end, unit = &apos;D&apos;) =&gt; { /* 日期差：D天/M月/Y年 */ }, // …共 32 个，命名与 Excel 对齐 } // 求值时按名字分发（大小写不敏感） const func = BUILTIN_FUNCTIONS[funcName.toUpperCase()] 大白话解读：公式先被拆成词法和语法树，遇到函数调用就查这张「函数字典」。新增一个函数，就是往字典里加一行，扩展成本几乎为零。得意细节： 所有函数名统一转大写再查表 ，用户写 sum(...) 还是 SUM(...) 都能跑，和 Excel 的宽容度保持一致。 6.4 自动化引擎：一行 continue 防住死循环 lib/automation-engine.ts（简化） for (const action of rule.actions) { if (action.type === &apos;update_field&apos; &amp;&amp; action.fieldId) { // 防自触发死循环：目标字段当前值已相同则跳过 if (event.recordData?.[action.fieldId] === action.value) continue fieldUpdates[action.fieldId] = action.value } // send_notification / send_webhook / ai_generate / // send_email / send_wecom / send_dingtalk …… } 大白话解读：规则命中后挨个执行动作，最危险的是「更新字段」——它本身又会造成一次记录更新，再次触发自动化。所以执行前先比对： 值没变就直接跳过 ，递归的火苗被这一行 continue 掐灭。得意细节：动作清单里还藏着 ai_generate ——自动化的一个动作可以是「让大模型写点东西填进字段」，规则和 AI 就这样接上了。 七、应用场景分享 下面站在我们调研的市场需求来和大家分享一下有哪些应用场景可以使用 pxcharts 超级表格： 🛒 电商团队 ：订单管理表 + 看板视图盯发货状态，自动化把超时未发货订单推到企微群。 🏭 制造工厂 ：生产工单 + 设备巡检记录，甘特视图排产，巡检异常自动邮件通知维保。 🚚 物流公司 ：运单跟踪表，表单视图给司机上报异常，层级视图看区域-线路-单号。 🧑‍💼 人事部门 ：招聘管理流水线，看板拖拽候选人阶段，AI 字段自动总结简历亮点。 🏫 教培机构 ：学员档案 + 课程排期日历视图，课时消耗用公式字段自动计算。 💻 研发团队 ：需求池 + Bug 跟踪，图表视图看迭代燃尽，自然语言查询「上周我名下未完成的需求」。 📊 数据敏感型企业 ：整套系统私有化部署在自己服务器，数据不出内网——这是商业 SaaS 给不了的。 八、写在最后 PXCharts 还在快速迭代中，字段体系、视图、自动化都会继续长。如果大家也有类似的需求和痛点，或者想要一个数据完全握在自己手里的多维表格，欢迎参考我们的方案： 开源版地址： github.com/MrXujiang/p… 觉得有用的话，给我们点个 Star，这就是最大的鼓励！ 让数据协作这件事，回到它本该有的样子——简单、实时、可控。 我们下期见！</description><pubDate>Wed, 26 Aug 2026 10:19:55 +0000</pubDate></item><item><title>为什么越来越多人用AgentScope ？</title><link>https://juejin.cn/post/7678161312637730862</link><guid isPermaLink="false">juejin-d135777bac3d71b7</guid><description>前言 Java开发者如何快速构建企业级AI Agent应用 最近这段时间，AI Agent（智能体）这个概念火得一塌糊涂。从OpenClaw到Claude Code，从Manus到各种Agent框架，仿佛一夜之间，“让AI自己干活”成了技术圈最热门的话题。 但很多Java开发者在尝试入局的时候，发现了一个尴尬的问题——市面上主流的Agent框架，绝大多数是Python生态的。 LangChain？Python的。 AutoGen？Python的。 CrewAI？还是Python的。 “三哥，我们团队都是Java技术栈，难道要为了做Agent专门去学Python吗？” 当然不用。 阿里巴巴开源的AgentScope-Java，就是专为Java开发者打造的智能体开发框架 。 今天这篇文章就专门跟大家一起聊聊AgentScope-Java，希望对你会有所帮助。 更多项目实战在Java突击队网：susan.net.cn/project 一、AgentScope-Java到底是什么？ 1.1 一句话说清 AgentScope-Java是阿里巴巴开源的一个面向智能体（Agent）编程的Java框架，用于构建基于大语言模型（LLM）的智能体应用 。 它的核心目标很明确—— 让Java开发者用自己熟悉的语言和工具链，快速构建生产级的AI Agent应用 。 1.2 AgentScope-Java解决了什么问题？ 在AgentScope出现之前，Java开发者想做Agent应用，基本只有两条路： 第一条路：用Python框架 。 学新语言、搭新环境、维护两套技术栈，团队分裂。 第二条路：自己从零造轮子 。 写ReAct循环、做工具调用、管理对话记忆、处理多Agent协作……每一项都是大工程。 AgentScope做的事情就是： 把Agent开发需要的所有基础设施——ReAct推理循环、工具调用、记忆管理、多智能体协作、分布式部署——全部封装成一个Java框架，开箱即用 。 1.3 和Spring AI Alibaba有什么区别？ 很多小伙伴可能会问：“三哥，阿里巴巴不是有Spring AI Alibaba吗？跟这个有什么区别？” 这是一个非常好的问题。两者定位完全不同： Spring AI Alibaba ：偏重“AI能力接入”——让Java应用方便地调用各种大模型API，适合做RAG、聊天机器人等场景 AgentScope-Java ：偏重“Agent工程化”——让开发者构建具有自主推理、工具调用、多Agent协作能力的智能体系统 两者不是竞争关系，而是 可以配合使用 的关系。AgentScope负责Agent的“大脑”（推理、决策、行动），Spring AI Alibaba负责“感官”（接入各种AI能力）。 二、核心概念 AgentScope-Java 2.0的核心设计思路非常清晰—— 提供两种Agent，覆盖从简单到复杂的所有场景 。 2.1 ReActAgent：最轻量的推理核心 ReActAgent 是AgentScope最基础的Agent实现，它实现了完整的 ReAct（Reasoning + Acting）推理循环 。 所谓ReAct，就是让LLM在“ 思考→行动→观察→再思考 ”的循环中自主完成任务： ReActAgent适合 轻量级、单次对话、不需要持久化状态 的场景。 2.2 HarnessAgent：生产级的工程化封装 HarnessAgent 是AgentScope 2.0推荐的 生产级入口 。 它在ReActAgent的基础上，额外封装了一套 工程化能力 ： 工程能力 说明 工作区（Workspace） Agent的人格、知识、技能、记忆统一沉淀在结构化工作区中 长期记忆（Memory） 跨会话的记忆持久化和语义检索 会话持久化（Session） 对话状态自动保存，重启后无缝恢复 子Agent编排 主Agent可以委派任务给多个子Agent 沙箱隔离（Sandbox） 工具执行在隔离环境中运行，保证安全 上下文压缩（Compaction） 长对话自动压缩，防止上下文溢出 核心区别 ：ReActAgent解决的是“这一次对话怎么跑”，HarnessAgent解决的是“长期运行的Agent怎么稳定、安全、可扩展”。 我的建议 ： 大部分场景直接用HarnessAgent 。 虽然看起来多了一些配置，但这些工程能力在生产环境中几乎是必需的。 三、5分钟跑通第一个Agent 3.1 前置要求 AgentScope-Java 2.0需要 JDK 17或更高版本 ，推荐使用 Maven 3.9+ 。 检查你的Java版本： java -version # 需要输出 17 或更高 3.2 添加Maven依赖 AgentScope的依赖设计很清晰—— 核心模块和模型扩展分离 。 第一步：添加核心依赖 &lt;dependency&gt; &lt;groupId&gt;io.agentscope&lt;/groupId&gt; &lt;artifactId&gt;agentscope-harness&lt;/artifactId&gt; &lt;version&gt;2.0.0&lt;/version&gt; &lt;/dependency&gt; agentscope-harness 会自动引入 agentscope-core ，包含了ReActAgent和HarnessAgent的核心实现。 第二步：添加模型扩展 根据你要用的模型，添加对应的扩展依赖。以通义千问（DashScope）为例： &lt;dependency&gt; &lt;groupId&gt;io.agentscope&lt;/groupId&gt; &lt;artifactId&gt;agentscope-extensions-model-dashscope&lt;/artifactId&gt; &lt;version&gt;2.0.0&lt;/version&gt; &lt;/dependency&gt; 3.3 配置API Key AgentScope通过环境变量读取API Key。以DashScope为例： export DASHSCOPE_API_KEY= &quot;sk-你的API密钥&quot; 如果你用的是DeepSeek或OpenAI兼容的服务： export OPENAI_API_KEY= &quot;sk-你的API密钥&quot; 3.4 第一个Agent：最简示例 下面这段代码是AgentScope-Java的“Hello World”——创建一个能对话的Agent。 package com.example; import io.agentscope.core.ReActAgent; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.formatter.openai.OpenAIChatFormatter; import io.agentscope.core.message.UserMessage; import io.agentscope.core.model.GenerateOptions; import io.agentscope.core.model.OpenAIChatModel; import io.agentscope.core.tool.Toolkit; import io.agentscope.harness.HarnessAgent; import java.nio.file.Path; public class FirstAgent { public static void main(String[] args) { // 1. 创建Model（以DeepSeek为例） String apiKey = System.getenv( &quot;DEEPSEEK_API_KEY&quot; ); OpenAIChatModel model = OpenAIChatModel.builder() .apiKey(apiKey) .modelName( &quot;deepseek-chat&quot; ) .baseUrl( &quot;https://api.deepseek.com&quot; ) .stream( true ) // 启用流式输出 .enableThinking( true ) // 启用思考模式 .formatter(new OpenAIChatFormatter()) .defaultOptions(GenerateOptions.builder() .thinkingBudget(1024) // 思考token预算 .build()) .build(); // 2. 创建Agent HarnessAgent agent = HarnessAgent.builder() .name( &quot;Assistant&quot; ) .sysPrompt( &quot;你是一个乐于助人的AI助手，请友好简洁地回答问题。&quot; ) .model(model) .workspace(Path.of( &quot;./workspace&quot; )) .build(); // 3. 发送消息并获取回复 UserMessage userMsg = new UserMessage( &quot;你好，请介绍一下自己&quot; ); String reply = agent.call(userMsg, RuntimeContext.empty()) .block() .getTextContent(); System.out.println(reply); } } 代码拆解 ： 第1步 ：创建 OpenAIChatModel ，配置API地址、模型名称、是否流式输出、是否启用思考模式 第2步 ：用 HarnessAgent.builder() 创建Agent，指定名称、系统提示词、模型和工作区目录 第3步 ：构造 UserMessage ，调用 agent.call() 获取回复 运行后 ，你会看到Agent的回复。整个过程不到10行核心代码，一个能对话的AI Agent就跑起来了。 四、工具系统：让Agent“长出手脚” 有些小伙伴可能会说：“Agent光会聊天有什么用？我要的是它能调用工具、执行操作！” 别急。AgentScope的 工具系统 就是干这个的。 没有工具的Agent只能“纸上谈兵”。AgentScope通过 @Tool 注解，让开发者可以 把任意Java方法注册为Agent可调用的工具 。 4.1 定义工具 用 @Tool 和 @ToolParam 注解定义工具： import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; public class WeatherTools { @Tool(name = &quot;get_weather&quot; , description = &quot;获取指定城市的当前天气&quot; ) public String getWeather( @ToolParam(name = &quot;city&quot; , description = &quot;城市名称，例如&apos;北京&apos;&quot; ) String city ) { // 这里可以调用真实的天气API return city + &quot;今天晴，温度25°C&quot; ; } @Tool(name = &quot;calculate&quot; , description = &quot;执行数学计算&quot; ) public double calculate( @ToolParam(name = &quot;expression&quot; , description = &quot;数学表达式&quot; ) String expression ) { // 这里可以集成表达式计算引擎 return 42.0; } } 关键点 ： @Tool 的 name 是工具的唯一标识，Agent调用时使用此名称 @Tool 的 description 描述工具功能，Agent根据此描述决定何时调用 @ToolParam 标注在方法参数上，描述参数的含义 4.2 注册工具 创建 Toolkit 实例，将工具注册进去： // 创建工具集 Toolkit toolkit = new Toolkit(); toolkit.registerTool(new WeatherTools()); // 将工具集传给Agent HarnessAgent agent = HarnessAgent.builder() .name( &quot;Assistant&quot; ) .sysPrompt( &quot;你是一个可以使用工具的助手。&quot; ) .model(model) .toolkit(toolkit) // 注册工具 .workspace(Path.of( &quot;./workspace&quot; )) .build(); 4.3 带工具的Agent public class ToolCallingExample { public static void main(String[] args) { // 创建Model OpenAIChatModel model = ...; // 创建工具集 Toolkit toolkit = new Toolkit(); toolkit.registerTool(new WeatherTools()); // 创建Agent并注册工具 HarnessAgent agent = HarnessAgent.builder() .name( &quot;Assistant&quot; ) .sysPrompt( &quot;你是一个可以使用工具的助手。当用户问天气时，调用get_weather工具。&quot; ) .model(model) .toolkit(toolkit) .build(); // 用户提问，Agent会自动决定是否调用工具 UserMessage userMsg = new UserMessage( &quot;北京今天天气怎么样？&quot; ); String reply = agent.call(userMsg, RuntimeContext.empty()) .block() .getTextContent(); System.out.println(reply); // 输出：北京今天晴，温度25°C } } 关键理解 ：Agent在ReAct循环中会 自主决定 是否调用工具、调用哪个工具、何时调用。开发者只需要定义工具，Agent自己会判断“什么时候该用”。 五、多Agent协作 有些小伙伴可能会问：“一个Agent不够用怎么办？复杂任务需要多个Agent协作怎么搞？” AgentScope 2.0提供了 orchestrator + workers 模式来实现多Agent协作。 5.1 核心模式 2.0版本的核心理念是： 主Agent扮演“主持人”，子Agent扮演“参与者” 。 主Agent负责接收用户任务、拆解任务、委派给子Agent、汇总结果。 5.2 定义子Agent 子Agent可以通过 文件驱动 的方式定义——在 workspace/subagents/ 目录下创建 .md 文件： workspace/subagents/weather.md ： id : weather description: 查城市天气。输入：城市名 + 日期。输出：温度区间、是否下雨。 sysPrompt: | 你是一个气象助理。用户给你一个城市和日期，你返回： - 温度（高/低） - 是否下雨 - 是否需要带伞 严格三行，不超过60字。 workspace/subagents/flight.md ： id : flight description: 查航班信息。输入：出发城市 + 到达城市 + 日期。 sysPrompt: | 你是一个航班查询助理。根据用户输入给出一个mock航班号和起降时间。 5.3 Java端补强 如果子Agent需要调用Java端的工具（比如真实的天气API），可以在Java端再注册一份： import io.agentscope.harness.agent.subagent.SubagentDeclaration; // Java端补强weather子Agent SubagentDeclaration weather = SubagentDeclaration.builder() .name( &quot;weather&quot; ) .description( &quot;查城市天气；输入城市+日期，返回温度区间和是否带伞&quot; ) .inlineAgentsBody( &quot;你是一个气象助理，会调用工具查询真实天气&quot; ) .build(); // 在HarnessAgent中注册子Agent HarnessAgent agent = HarnessAgent.builder() .name( &quot;TravelAssistant&quot; ) .model(model) .subagent(weather) // 注册子Agent .workspace(Path.of( &quot;./workspace&quot; )) .build(); 主Agent会自己决定 ：是否需要调用子Agent、调用哪些子Agent、调用顺序是什么。 六、底层原理 6.1 分层架构 AgentScope-Java采用 经典的分层架构设计 ： AgentScope的整体架构可以清晰分为四层： 模型适配层 ：负责与不同LLM提供商通信，支持OpenAI协议、DashScope（通义千问）、Anthropic Claude、Google Gemini等 ReAct推理层 ：实现ReAct推理循环（思考→行动→观察→再思考），是整个Agent的“大脑” Harness工程化层 ：在ReAct之上封装了工作区、记忆、会话、子Agent、沙箱等工程能力 应用层 ：你的业务代码 6.2 ReAct推理循环的执行流程 当一个用户消息进入Agent时，ReAct循环的执行流程如下： HarnessAgent在ReAct循环的 关键时机插入了Hook ，实现了工作区加载、记忆读写、会话持久化等功能。 6.3 分布式部署架构 AgentScope 2.0最核心的升级之一，就是 原生支持分布式部署 。 在单机开发阶段，状态默认落到本地 workspace 目录。 进入生产部署后，只需把状态后端切换为分布式存储： 同一份业务代码，只需切换存储后端，就能从单机模式切换到分布式模式。 任意副本都能恢复任意用户的完整上下文。 七、实战案例：多Agent天气助手 有些小伙伴可能会说：“单个Agent我跑通了，但真实业务需要多个Agent协作，怎么办？” AgentScope 2.0提供了 文件驱动的Subagent机制 。你只需要在 workspace/subagents/ 目录下放几个 .md 文件，主Agent就会自己决定“什么时候该叫谁”。 我们来看一个完整的实战—— 旅行助手 。用户问：“我明天从北京飞杭州，落地后去西湖，要带伞吗？” 1.x时代，你需要写代码串三个Agent（天气→航班→景点）。 2.0时代，主Agent自己决定先查天气还是航班，三个Subagent 并行启动 。 7.1 工程结构 travel-assistant/ ├── pom.xml └── workspace/ ├── MEMORY.md ├── subagents/ │ ├── weather.md │ ├── flight.md │ └── attraction.md └── state/ └── session-*.json # JsonFileAgentStateStore自动生成 7.2 三个Subagent文件 workspace/subagents/weather.md ： id : weather description: | 查城市天气。 输入：城市名 + 日期（YYYY-MM-DD）。 输出：温度区间、是否下雨、是否需要带伞。 sysPrompt: | 你是一个气象助理。 用户给你一个城市和日期，你返回： - 温度（高/低，摄氏度） - 是否下雨 - 是否需要带伞 严格三行，不超过60字。 workspace/subagents/flight.md ： id : flight description: | 查航班信息（mock）。 输入：出发城市 + 到达城市 + 日期。 输出：航班号、起飞时间、到达时间。 sysPrompt: | 你是一个航班查询助理。 根据用户输入给出一个mock航班号和起降时间。 注意：测试环境，无需真查询，给出合理mock即可。 workspace/subagents/attraction.md ： id : attraction description: | 景点信息助理（mock）。 输入：城市 + 景点名。 输出：开放时间、是否需要预约、周边交通。 sysPrompt: | 你是一个导游助理。 根据用户输入给出景点的实用信息。 这三份描述对主Agent来说是 路由表 ——主Agent全靠 description 决定要不要 spawn 它们。 7.3 Java端补强Subagent 如果某个Subagent需要调用Java端的真实工具（比如weather.md背后要接真的天气API），可以在Java端再注册一份—— HarnessAgent会把文件+Java声明合并 ： import io.agentscope.core.model.DashScopeChatModel; import io.agentscope.core.tool.Toolkit; import io.agentscope.harness.HarnessAgent; import io.agentscope.harness.agent.subagent.SubagentDeclaration; import java.nio.file.Path; public class TravelAssistant { public static void main(String[] args) { // 1. 创建Model DashScopeChatModel model = DashScopeChatModel.builder() .apiKey(System.getenv( &quot;DASHSCOPE_API_KEY&quot; )) .modelName( &quot;qwen-plus&quot; ) .build(); // 2. 创建Toolkit并注册天气查询工具 Toolkit toolkit = new Toolkit(); toolkit.registerTool(new WeatherLookupTool()); // 真实的天气API工具 // 3. Java端补强weather subagent——tools白名单过滤继承自父agent的工具 SubagentDeclaration weather = SubagentDeclaration.builder() .name( &quot;weather&quot; ) .description( &quot;查城市天气；输入城市+日期，返回温度区间和是否带伞&quot; ) .inlineAgentsBody( &quot;你是一个气象助理，会调用工具查询真实天气&quot; ) .build(); // 4. 创建HarnessAgent，注册subagent HarnessAgent agent = HarnessAgent.builder() .name( &quot;TravelAssistant&quot; ) .model(model) .toolkit(toolkit) .workspace(Path.of( &quot;./workspace&quot; )) .subagent(weather) // Java端补强的subagent .build(); // 5. 运行 UserMessage userMsg = new UserMessage( &quot;我明天从北京飞杭州，落地后去西湖，要带伞吗？&quot; ); String reply = agent.call(userMsg, RuntimeContext.empty()) .block() .getTextContent(); System.out.println(reply); } } 关键理解 ：主Agent在推理过程中会 自主决定 是否需要调用Subagent、调用哪些Subagent、调用的顺序是什么。 整个“编排”过程由LLM完成，不需要你写死Pipeline。 八、实战案例：MCP协议工具 有些小伙伴可能会说：“工具调用要自己写Java类，如果要接入GitHub、数据库、Slack这些外部服务，难道每个都要自己封装？” 不用。 AgentScope 2.0支持 MCP（Model Context Protocol）协议 ，你只需要在 workspace/tools.json 里 一行声明 一个MCP server，Agent启动时 自动发现并注册工具 。 8.1 什么是MCP？ MCP是Anthropic在2024年推出的开放协议，让LLM应用以统一方式发现并调用外部工具。 AgentScope 2.0把MCP server作为Agent工具的一种“来源”——你在 tools.json 里声明一个MCP server，Agent启动时通过stdio或sse协议连上它， 自动把server暴露的工具当作Agent自己的tool 。 8.2 第一个MCP集成 workspace/tools.json ： { &quot;mcpServers&quot; : { &quot;github&quot; : { &quot;command&quot; : &quot;npx&quot; , &quot;args&quot; : [ &quot;-y&quot; , &quot;@modelcontextprotocol/server-github&quot; ], &quot;env&quot; : { &quot;GITHUB_PERSONAL_ACCESS_TOKEN&quot; : &quot; ${env:GITHUB_TOKEN} &quot; } } } } HarnessAgent.builder().workspace(path) 启动时会自动扫描 workspace/tools.json 的 mcpServers 段、连接每个server、把工具注册到Agent—— 不需要额外开关 ： HarnessAgent agent = HarnessAgent.builder() .name( &quot;GitHubAssistant&quot; ) .model(model) .workspace(Path.of( &quot;./workspace&quot; )) // 自动加载tools.json .build(); 跑起来后，Agent就能调用GitHub MCP server暴露的 create_issue 、 list_repos 、 search_code 等工具了。 8.3 三种连接方式 MCP支持三种传输协议： 协议 适用场景 声明方式 stdio 本地进程，最常见 command + args sse 远程HTTP SSE server url + headers ws 双向WebSocket url + headers stdio示例 （接入本地文件系统）： { &quot;mcpServers&quot; : { &quot;filesystem&quot; : { &quot;command&quot; : &quot;npx&quot; , &quot;args&quot; : [ &quot;-y&quot; , &quot;@modelcontextprotocol/server-filesystem&quot; , &quot;./data&quot; ] } } } sse示例 （接入远程知识库）： { &quot;mcpServers&quot; : { &quot;remote-knowledge&quot; : { &quot;url&quot; : &quot;https://mcp.example.com/sse&quot; , &quot;headers&quot; : { &quot;Authorization&quot; : &quot;Bearer ${env:MCP_TOKEN} &quot; } } } } 8.4 不想写JSON？Java代码直接配 有时候你想在代码里动态拼参数——比如token从环境变量读、超时按环境切换。 这时候可以直接在Java代码里配： import io.agentscope.harness.agent.tools.McpServerConfig; import io.agentscope.harness.agent.tools.ToolsConfig; ToolsConfig cfg = new ToolsConfig(); Map&lt;String, McpServerConfig&gt; servers = new LinkedHashMap&lt;&gt;(); McpServerConfig github = new McpServerConfig(); github.setTransport( &quot;stdio&quot; ); github.setCommand( &quot;npx&quot; ); github.setArgs(List.of( &quot;-y&quot; , &quot;@modelcontextprotocol/server-github&quot; )); github.setEnv(Map.of( &quot;GITHUB_PERSONAL_ACCESS_TOKEN&quot; , System.getenv( &quot;GITHUB_TOKEN&quot; ))); servers.put( &quot;github&quot; , github); cfg.setMcpServers(servers); // 然后通过HarnessAgent的toolsConfig()方法传入 效果和 tools.json 完全一样。 8.5 常见的MCP Server MCP Server 用途 安装命令 server-github GitHub操作（创建Issue、搜索代码等） npx -y @modelcontextprotocol/server-github server-filesystem 本地文件系统读写 npx -y @modelcontextprotocol/server-filesystem server-postgres PostgreSQL数据库查询 npx -y @modelcontextprotocol/server-postgres server-slack Slack消息发送 npx -y @modelcontextprotocol/server-slack server-puppeteer 浏览器自动化（网页抓取、截图） npx -y @modelcontextprotocol/server-puppeteer 接入MCP生态后，AgentScope的Agent能力边界被极大地扩展了—— 只要能通过MCP暴露的工具，Agent都能调用 。 九、优缺点 优点 1. Java生态无缝集成 AgentScope完美兼容Spring Boot、Spring Cloud、Maven等Java主流技术栈。对于Java团队来说，学习曲线非常平缓。 2. 双Agent架构，覆盖全场景 ReActAgent满足轻量级需求，HarnessAgent覆盖生产级工程需求。从原型到生产，一套框架全搞定。 3. 完善的工具系统 通过 @Tool 注解即可将任意Java方法注册为Agent工具，Agent在ReAct循环中自主决定调用时机。 4. 原生多Agent协作 内置orchestrator + workers模式，主Agent可以委派任务给多个子Agent，支持同步和异步两种模式。 5. 生产级工程能力 工作区、长期记忆、会话持久化、上下文压缩、沙箱隔离——HarnessAgent把企业级Agent需要的工程能力全部打包。 6. 分布式部署原生支持 支持Redis、MySQL、PostgreSQL等多种状态存储后端，支持Kubernetes水平扩展。 7. 多模型支持 内置OpenAI协议（DeepSeek、GLM、Ollama等）、DashScope（通义千问）、Anthropic Claude、Google Gemini。 8. MCP/A2A协议支持 支持Model Context Protocol和Agent-to-Agent协议，可以接入MCP生态的工具和服务。 缺点 1. 相对较新 AgentScope-Java 1.0于2025年12月发布，2.0于2026年7月GA。相比Spring AI等成熟框架，社区积累较少。 2. 学习曲线 HarnessAgent的工程化概念（工作区、记忆、子Agent等）需要一定的学习成本。 3. 生态不如Spring AI丰富 目前第三方集成和扩展的数量不如Spring AI Alibaba。 4. 文档偏英文 虽然官方提供了中文文档，但部分深度内容仍以英文为主。 十、适用场景 场景 推荐程度 理由 智能客服系统 强烈推荐 多Agent协作+知识库RAG 运维诊断Agent 强烈推荐 自主推理+工具调用+日志分析 金融分析Agent 强烈推荐 结构化输出+多步推理 代码辅助Agent 推荐 工具调用+代码执行沙箱 企业内部知识助手 推荐 RAG+长期记忆 简单聊天机器人 可能过度设计 用Spring AI Alibaba即可 已有Spring AI生态 需评估 两者可以配合使用 更多项目实战在Java突击队网：susan.net.cn/project 总结 回到最初的问题： Java开发者怎么做AI Agent？ AgentScope-Java给出了一个非常完整的答案。 它不是“把Python框架翻译成Java”的简单移植，而是 从Java生态的实际情况出发，专门为Java开发者设计的Agent框架 。 ReActAgent 让你快速跑通Agent原型， HarnessAgent 让你把原型变成生产级应用。 @Tool注解 让工具定义像写普通Java方法一样自然， 子Agent系统 让多Agent协作变得清晰可控。 最关键的是—— 它让Java开发者不需要为了做Agent去学Python 。</description><pubDate>Wed, 26 Aug 2026 07:51:26 +0000</pubDate></item><item><title>Web Components 为什么火不起来？</title><link>https://juejin.cn/post/7677868279283171368</link><guid isPermaLink="false">juejin-20505f97700f3d6a</guid><description>一听 Web Components ，看起来是很高大上的技术🤔。 它是 W3C 的亲儿子，是浏览器原生支持的组件化标准，由 Custom Elements 、 Shadow DOM 和 HTML Templates 三大底层 API 组成。从 2013 年 Google 首次提出概念到今天，已经过去了整整 13 年。 按理说，一个有着浏览器原生支持、不依赖任何第三方框架、天然跨框架复用的组件标准，应该早就统治前端世界了🤷‍♂️。 但现实是：绝大多数前端工程师依然在用 React 或者 Vue 写组件， Web Components 在实际的商业项目中的采用率，低得令人尴尬。 为什么？因为技术上的正确，从来不等于工程上的好用。接下来我们聊一聊它👇。 开发体验的断崖式落后 Web Components 最大的敌人不是 React ，而是它自己那极其原始的开发体验。 我们直接看同一个需求—— 一个可点击计数器组件 ——在两套体系下的代码对比： React 的写法： function Counter ( ) { const [count, setCount] = useState ( 0 ); return &lt; button onClick = {() =&gt; setCount(c =&gt; c + 1)}&gt;点击了 {count} 次 &lt;/ button &gt; ; } 就只有 3 行代码，逻辑清晰，状态驱动视图，任何初级前端都能秒懂🙌。 Web Components 的原生写法： class MyCounter extends HTMLElement { constructor ( ) { super (); this . _count = 0 ; this . _shadow = this . attachShadow ({ mode : &apos;open&apos; }); this . _render (); } _render ( ) { this . _shadow . innerHTML = ` &lt;style&gt; button { padding: 8px 16px; cursor: pointer; } &lt;/style&gt; &lt;button&gt;点击了 ${ this ._count} 次&lt;/button&gt; ` ; // 每次重新渲染后，必须手动重新绑定事件，因为 innerHTML 会销毁旧的 DOM 节点 this . _shadow . querySelector ( &apos;button&apos; ). addEventListener ( &apos;click&apos; , () =&gt; { this . _count ++; this . _render (); // 手动触发重新渲染，没有任何自动化的响应式机制 }); } } customElements. define ( &apos;my-counter&apos; , MyCounter ); 同样的功能，原生 Web Components 的代码量是 React 的 5 倍以上 。而且这里面充斥着极其原始的手动操作：手动拼接 HTML 字符串、手动绑定事件、手动触发重新渲染、手动管理状态。 这不是在写现代化的组件，这是在用 2026 年的浏览器 API 写 2010 年风格的 jQuery 代码😖。 没有内置的响应式系统 React 有 useState ， Vue 有 ref 和 reactive ，它们的核心卖点都是 状态驱动视图 ：你只管改数据，框架自动帮你更新 DOM 。 但 Web Components 的标准里， 没有任何内置的响应式机制 。 你修改了一个属性， DOM 不会自动更新。你必须自己实现 attributeChangedCallback ，自己手动去找到对应的 DOM 节点，自己手动去改它的 textContent 。当组件的状态变得复杂（比如一个包含 20 个联动字段的表单），你需要手写的同步逻辑会像野草一样疯狂蔓延，最终变成一团无法维护的意大利面条。 有人会说： 你可以用 👉 Lit 啊，它给 Web Components 加上了响应式。 没错， Lit 确实极大地改善了开发体验。但问题是： 当你必须依赖一个第三方库才能让原生标准变得好用时，这个原生标准的意义在哪里🤷‍♂️？ 你用 Lit 写 Web Components ，和你用 React 写组件，本质上都是在依赖一个框架。只不过 React 的生态比 Lit 大了几百倍。 Shadow DOM 的样式隔离 Shadow DOM 是 Web Components 最引以为傲的特性：它提供了真正的样式隔离，组件内部的 CSS 不会泄漏到外部，外部的 CSS 也无法侵入内部。 这在理论上非常美好。但在真实的业务开发中，这种 绝对隔离 会迅速变成不稳定因素。 当你的设计师说全站的按钮统一用品牌蓝时，你发现你根本无法用一个全局的 CSS 变量去穿透 Shadow DOM 的边界（虽然 CSS Custom Properties 可以穿透，但这需要组件内部主动配合暴露接口，大量第三方 Web Components 并没有做这个工作）。 当你想用 Tailwind CSS 的工具类来快速调整组件的样式时，你发现这些 class 在 Shadow DOM 里完全失效，因为 Tailwind 生成的样式表根本注入不到 Shadow Root 里面去😖。 隔离是好事，但不可控的隔离是灾难。 React 和 Vue 的组件没有 Shadow DOM ，但通过 CSS Modules 、 Scoped CSS 或者 Tailwind 就能实现足够好的样式隔离，同时保留了全局主题覆盖的灵活性。 不支持 SSR 在 2026 年，服务端渲染（ SSR ）已经从可选方案变成了项目的标配。 React 有 Next.js ， Vue 有 Nuxt ，它们的 SSR 生态极其成熟。 但 Web Components 的 SSR 支持，至今依然是一个极其尴尬的半成品。 Custom Elements 本质上依赖浏览器的 JavaScript 引擎来注册和执行。在服务端的 Node.js 环境里，根本没有 customElements.define 这个 API。这意味着你的 Web Components 在服务端只能输出一个空壳标签（如 &lt;my-counter&gt;&lt;/my-counter&gt; ），所有的内容都必须等到客户端 JavaScript 加载并执行后才能渲染。 对于一个重视 SEO 和首屏性能的商业项目来说，这是不可接受的硬伤🤔。 框架复用是一个伪需求 Web Components 最大的宣传卖点是： 写一次，在任何框架里复用。 这听起来极其诱人。但你冷静想一想：在真实的项目里，你的团队有多少个项目是同时混用 React 和 Vue 的？ 答案几乎是零，使用场景非常少。 绝大多数公司的前端技术栈是统一的。一旦选定了 React ，所有项目都用 React ；一旦选定了 Vue ，所有项目都用 Vue 。在单一技术栈的团队里，跨框架复用是一个根本不存在的需求。你为了满足一个伪需求，去承受 Web Components 那极其糟糕的开发体验，这笔账怎么算都不划算🫵。 那它到底适合在哪里呢？ 说了这么多缺陷， Web Components 是不是毫无价值？也并不是🖐️。 在一个极其特定的场景下，它依然是无可替代的最优解： 大型企业的跨团队基础设计系统。 当一家公司有几十个前端团队，分别在用 React 、 Vue 、甚至 Angular 时，底层的基础 UI 组件（按钮、输入框、对话框）如果用某个特定框架来写，就必然会排斥其他框架的团队。这个时候，用 Web Components 来构建一套框架无关的基础设计系统，是唯一能让所有团队都无痛接入的方案。 GitHub 的 Primer 、 Adobe 的 Spectrum 、 SAP 的 UI5 ——这些顶级企业的设计系统，底层都选择了 Web Components 。 但请注意，这是一个极少数大型企业才会遇到的问题。对于 99% 的中小型团队来说，直接用 React 或 Vue 的组件库，永远是性价比最高的选择😀。 一些思考🤔 Web Components 的结果，给所有技术人说明了一件事。 一个技术方案能不能成功，从来不取决于它在标准层面有多正确。 W3C 的背书、浏览器的原生支持、理论上的完美架构——这些东西在面对 开发日常 时，全部苍白无力。 开发者用脚投票。谁的开发体验好、谁的生态强、谁能让我在 deadline 之前把需求交出去，我就用谁🖐️。 React 和 Vue 赢了，不是因为它们比 Web Components 更正确，而是因为它们更好用，更灵活！ 你们同意吗😀？ 喜欢我的文章，也欢迎关注我的微信公众号：【前端技术官】。 主要分享： 前端架构 · AI 编程 · 职场认知 · 开发者成长 微信扫码关注 👆 不定期更新，不刷屏，聊点真正有用的干货。</description><pubDate>Wed, 26 Aug 2026 02:30:21 +0000</pubDate></item><item><title>两个半月的时间如何赚到 3000 块钱。</title><link>https://juejin.cn/post/7677842844808052776</link><guid isPermaLink="false">juejin-2d6a603511da28ed</guid><description>先来说下最近两个月情况，我尝试了七八种渠道去做副业。最终得到的结论是，适合普通人好上手的就是公众号。如果你想继续听我继续分析，如果不感兴趣可以划走了。 我挑几个我收益最好的项目说吧，目前做的这个公众号这两个月，总阅读量是107w，总收益是2600。没有额外的支出费用。 小程序收益300，服务器+域名+认证费+token费用目前做到收支平衡吧。 个人咨询费用200，负责解答一些问题，提供AI工具。 以上是我最近两个月总的收入。 下面解答几个问题 每个月多赚1000多块钱？为什么别人都是日入几百上千的？ 我只是个普通人，我副业日入最多是245，我看到过日入大几百的，但是我也看到过每天几毛收益的人也还在坚持。我不是说坚持就有用，方向错了，再坚持也是没用的。我会继续探索，等有一天我的副业能赶上我的主业收入，那我可以考虑更自由生活。 2. AI重要吗，体现在哪里？我该如何更好地使用呢？ 普通人用AI写文案，是反复修改提示词、手动润色，一篇稿子磨两小时；程序员用AI写文案，是预设好模板和参数，使用agent去流程化，自由组合调用流程，灵活的同时操作了个性化的内容产出。 并且我做了自己产品，帮助自媒体人更好更快的产出内容。 如何开始自己的副业？遇到瓶颈了怎么办？ 大多数人是想的太多做的太少，我曾经也是这样，通过不断的可以训练我现在会随时随地使用ai记下自己的想法。然后按照短期收益和长期收益的原则去选择要做的事。也就是如果这个事现在做了，今天就能给我带来收益我就立马做，否则可以延后。 所以可以先开始写作或者视频制作，等遇到问题了再调整。如果实在不知道做什么的可以看看我的小程序有十几种赛道+100多种模板。帮助你快速生产内容，先动起来比什么都强。 然后说道遇到问题，这方面我还是比较有话语权的，我遇到了四次限流。而且每次限流以后我都能做到上万甚至几万的流量。关于踩坑和问题可以留言或者si我。 是不是粉丝越多越牛逼？ 答案是否定的，我公众号几百粉丝的时候最高收益是245一天，但是我现在粉丝已经翻倍了，一般就几十一天。甚至有些大几千的粉丝博主，每天只有几块几毛的都有。不要刻意追求粉丝，不要为了涨粉去互关。 小红书也是同样的，我一百粉不到已经写了两篇10w+的文章了。所以粉丝多是好事，但不必刻意追求。 为什么选择我的工具？ 这个工具适合新手快速验证想法，或者有多个账号的人批量去生成内容。当然如果你有自己的想法，觉得我这个工具无法满足你的需求，都可以定制自己的需求。我相信能用AI解决的问题，就应该用AI去实现。当然工具本身也是可以免费试用的，领积分，或者看广告都是可以的。 为什么做要做副业？主业不行了吗？ AI时代各岗各业都在被重塑，作为程序员群体，没有哪个行业比我们更懂如何使用AI，在时代的浪潮下去使用AI帮助他人提效，也能自己获得一部分回报。只要时间允许，在主业之外拥有一个副业是目前打工人比较好的选择。 最后 选择探索副业是给自己多一条路，当褪去公司光环，褪去平台能力，我们还剩下什么。希望你依然有能力去热爱，至少可以为自己点亮一盏前行的灯。</description><pubDate>Wed, 26 Aug 2026 01:28:35 +0000</pubDate></item><item><title>Gradle 9.7.0 将提速 Android 构建，Sync 提升接近一倍</title><link>https://juejin.cn/post/7677854475684020267</link><guid isPermaLink="false">juejin-b7611c3000d9281b</guid><description>Android 最近和 Gradle 官方合作，在 9.7.0 版本推出了全新的 Gradle Isolated Projects ， 简单来说就是 Gradle 现在开始把大型多模块工程的「配置阶段」改成能安全并行运行，同时未来还能按模块增量缓存的架构 。 现在对 Android 开发来说， app 、 feature-home 、 core-network 、 core-ui 这些 Gradle Project，以前虽然看起来都是独立 module，但配置阶段其实可以互相读写状态，所以 Gradle 很难安全地同步配置它们， 然后现在 Isolated Projects 给这些 Project 之间加了一道强约束边界，然后就可以并行配制了 ，在 Gradle 9.7 开始，这个支持从 experimental 提升到了 incubating，未来还会变成为默认模式。 以后 Gradle 一次构建大体是： Initialization ↓ Configuration ↓ Execution 读 settings 执行每个 module 的 build.gradle javac/kotlinc/package... 创建 Task 配置 Plugin 解析各种 Project 状态 也就是大家平时说的 Android Studio Sync Project with Gradle Files 很大一部分时间都花在 Configuration 上，比如一个 500 个 module 的大型项目，过去逻辑上接近： :app │ ├─ configure ↓ :core │ ├─ configure ↓ :network │ ├─ configure ↓ :feature-home │ ├─ configure ↓ ... 但是在过去这个配置行为不能简单地丢进线程池，因为 Gradle 历史上的 Build DSL 存在这种玩法： // :app/build.gradle.kts val version = rootProject. version project ( &quot;:network&quot; ). tasks . named ( &quot;xxx&quot; ) { ... } rootProject. subprojects { // 修改其他 project } 也就是说 A 配置到一半可以摸 B，B 又可能摸 C ： Project A configuration │ ├──────────► Project B mutable state │ └──────────► Root Project mutable state 如果把他们放到多线程里同步支持，但是出现这种情况的话： Thread 1 → configure A Thread 2 → configure B Thread 3 → configure C 搜一 Gradle 保证不了执行顺序和状态一致性，callback 顺序和插件行为也会混乱，task 的注册也可能出现问题，所以 Gradle 多模块工程长期存在一个很严重的「配置阶段共享可变状态」问题 ，而 Isolated Projects 做的核心事情实际上也很简单，它主要规定了： :app ┌─────────┐ │ mutable │ │ state │ └─────────┘ X │ 禁止直接碰 X ┌─────────┐ │ mutable │ │ state │ └─────────┘ :network 每个 Project 的配置逻辑只能安全操作自己的 mutable state ，例如： rootProject.version project ( &quot;:foo&quot; ) .tasks project ( &quot;:foo&quot; ) .dependencies project ( &quot;:foo&quot; ) .configurations project ( &quot;:foo&quot; ) .extensions 这类跨 Project 「读取/修改可变状态」会被 Gradle 判成 Isolated Projects violation，官方甚至建议可以简单理解成： 除少数 immutable getter 外，基本别去调用另一个 Project 的东西。 而诸如另一个 Project 的 name 、 path 、 projectDir 、 buildFile 这种在配置开始前已经确定的 immutable 信息还是可以读取， 所以实际上这是在 Gradle DSL 上建立一个新的 concurrency contract ，有了这个 contract 就可以： 以前 Project A ──► B │ ▲ ▼ │ Project C ◄──┘ 不知道谁依赖谁的状态 → 不敢并行 Isolated Projects ┌ A ┐ ┌ B ┐ ┌ C ┐ ┌ D ┐ └───┘ └───┘ └───┘ └───┘ │ │ │ │ CPU1 CPU2 CPU3 CPU4 → 可以安全并行配置 所以 Gradle 9.7 大的实际收益就是 Parallel Project Configuration。 而在 Android Studio Sync 的时候，Studio 会让 Gradle 构建大量 Tooling Model，例如： Gradle :app ├─ Android model ├─ dependency model ├─ source sets └─ variants :featureA ├─ Android model ├─ dependency model └─ variants :featureB ... │ ▼ Android Studio 以前 Project configuration 本身存在共享状态，IDE 获取这些 model 的并行度一直受到限制，但是现在 Isolated Projects 开启以后： CPU 1 → configure :app → model CPU 2 → configure :featureA → model CPU 3 → configure :featureB → model CPU 4 → configure :core → model CPU 5 → configure :network → model Gradle 官方自己的测试工程效果就很显著： 如果是 5000+ Project Android monorepo ，甚至可以做到 5m09s -&gt; 2m44s 这种优化，所以这东西对十几个 module 的普通 App 感知可能一般，但是对几百到几千 module 的 Android monorep 影响还是挺大的。 而且它和 Configuration Cache 其实是一条路线，可以粗略理解成： Configuration Cache │ │ 解决： ▼ 上次配置没变？ → 那这次整个 Configuration 都别跑了 Isolated Projects │ │ 解决： ▼ 配置必须重新跑？ → 那各个 Project 并行跑 所以整体架构情况大概类似： Configuration Cache │ ┌────────┴────────┐ │ │ Cache Hit Cache Miss │ │ Configuration Isolated 全跳过 Projects │ Project A ─┬─ Project B ├─ Project C └─ Project D 并行配置 也就是 Isolated Projects 其实是直接建立在 Configuration Cache 基础设施之上 ，所以开启 org.gradle.isolated-projects=true ，Gradle 就会自动启用 Configuration Cache，显式关掉 Configuration Cache 甚至会报错。 不过它和 org.gradle.parallel=true 完全不是一回事，parallel 主要解决的是： Execution Phase :app :compile :libA :compile :libB :compile ↓ 任务并行 而 Isolated Projects 解决的是： Configuration Phase configure :app configure :libA configure :libB ↓ Project 配置并行 这两者还不太一样，不过 Gradle 官方也说了，这是他们和 Google、JetBrains 合作一起实现的支持，也就是从项目到 IDE 到参与了这次优化。 但是老项目可能会有一些风险，比如自定义 Gradle 脚本或者第三方 plugin 如果存在 cross-project access ， 打开 Gradle Isolated 可能就直接配置失败了。 不过说真的，对于国区来说，Sync 最大耗时根本不在这里，实际上这个优化大部分时候聊胜于无，而且升级 Gradle 9.7.0，还是很需要勇气的。</description><pubDate>Wed, 26 Aug 2026 00:57:14 +0000</pubDate></item><item><title>「速通Shell」常用命令（中）——进程、网络与系统信息</title><link>https://juejin.cn/post/7677883485022388267</link><guid isPermaLink="false">juejin-446e0ae60f65fb95</guid><description>上一篇我们聊了文件操作和文本处理，那是 shell 工程师日常使用频率最高的一组命令。这一篇换个主题，聊聊另外三块同样重要的内容：进程管理、网络工具、系统信息。 这三块是日常运维的另一半。写脚本时除了处理文件，你大概率还要跟运行中的程序打交道——查进程、查网络、查系统状态。掌握好这一篇的命令，你的 shell 脚本就能覆盖 90% 的运维场景。 一、三类工具的共同点 进程、网络、系统信息这三类命令看似不相关，其实有一个共同特点：它们都在&quot;观察和控制运行中的系统&quot;。跟上一篇的&quot;操作静态文件&quot;不同，这里的命令往往： 输出的内容是动态变化的（每秒都在刷新） 跨进程、跨主机、跨网络边界 误用可能影响线上服务（kill 错进程、网络断连） 所以这一篇的命令要更谨慎地使用。我们按&quot;观察 → 控制&quot;的顺序来：先学怎么看，再学怎么动。 二、进程管理 进程是 shell 工程师打交道最多的实体。你的脚本启动后是一个进程，调用外部命令会创建子进程，部署的服务也是进程。理解进程的状态和它们之间的关系，是写出可靠脚本的基础。 2.1 ps：看一眼进程 ps 是 process status 的缩写，它把当前系统里的进程列出来。语法是 ps [options] 。 最常用的几个组合： # 查看当前用户的所有进程 ps # 查看所有进程（包括其他用户） ps -ef # 或者 ps aux # 查找特定进程 ps -ef | grep nginx ps -ef 和 ps aux 是两种主流写法，前者是 System V 风格，后者是 BSD 风格。两者的输出列不同，但都能用。日常推荐 ps -ef，跨平台兼容性好。 输出字段解读（以 ps -ef 为例）： UID PID PPID C STIME TTY STAT TIME CMD root 1 0 0 8 月 22 ? 00 : 00 : 01 / sbin / init root 123 1 0 8 月 22 ? 00 : 00 : 00 nginx: master process UID：进程所有者 PID：进程 ID PPID：父进程 ID STAT：进程状态（R 运行、S 睡眠、Z 僵尸、T 停止等） CMD：启动命令 几个实用的查询组合 ： # 按进程名过滤 ps -ef | grep -v grep | grep nginx # 按用户过滤 ps -u www-data # 按 PID 查详情 ps -p 1234 -o pid,ppid,cmd # 自定义输出列 ps -eo pid,ppid,user,pcpu,pmem,cmd -- sort =-pcpu | head 最后一行尤其常用—— 找出最占 CPU 的几个进程 。 2.2 top / htop：动态看 ps 是一次的快照，top 是动态刷新的&quot;实时监控器&quot;。 top # 启动 top ，默认 3 秒刷新 top - p 1234 # 只监控指定 PID top -u www-data # 只监控指定用户 top -n 1 # 只刷新一次（适合脚本里用） top 的输出分两部分：上半部分是系统整体（负载、CPU、内存），下半部分是进程列表。几个看重点： load average：三个数字分别是 1 分钟、5 分钟、15 分钟的平均负载 %Cpu(s)：用户态、内核态、空闲 CPU 占比 KiB Mem：总内存、已用、空闲、缓存 top 里几个交互命令 ： P：按 CPU 排序 M：按内存排序 k：kill 进程（输入 PID） 1：展开多核 CPU q：退出 htop 是 top 的升级版 ——支持鼠标、彩色、树形视图。如果有 htop 就用 htop，体验比 top 好太多。安装： apt install htop 或 yum install htop 。 2.3 jobs / fg / bg：前后台切换 这是 shell 自己的进程管理，跟操作系统的进程管理是分开的。 # 启动一个后台任务 sleep 100 &amp; # 查看当前 shell 的后台任务 jobs # 输出：[1]+ Running sleep 100 &amp; # 把后台任务切到前台 fg %1 # 把前台任务切到后台（先用 Ctrl+Z 暂停） # 按 Ctrl+Z bg %1 &amp; 是&quot;后台启动&quot;。 jobs 列出当前 shell 的后台任务（注意是当前 shell，子 shell 的任务看不到）。 关键概念 ：前台任务会占用终端，你按 Ctrl+C 它就死了。后台任务不占用终端，Ctrl+C 不会影响它。 所以长时间运行的任务应该用 &amp; 放后台 。 但放后台的任务在终端关闭时可能被 SIGHUP 杀掉——解决方案是 nohup，后面讲。 2.4 nohup：脱离终端 nohup long_running.sh &amp; nohup 让进程忽略 SIGHUP 信号——终端断开时不会被杀， &amp; 让它在后台跑。这是&quot;启动一个能跑很久的服务&quot;的标准组合。 输出默认会写到 nohup.out，除非你重定向： nohup long_running.sh &gt; /var/log/myservice.log 2&gt;&amp;1 &amp; 注意 nohup 不是万能的，它只处理 SIGHUP，不处理其他信号。 真正的守护进程需要用 systemd 或专门的 daemon 工具 。 2.5 kill / pkill / killall：发信号 kill 的本意是&quot;发信号&quot;，不是&quot;杀进程&quot;——只是发 TERM 信号是它的默认行为。前面信号那篇讲过细节，这里只讲实战用法。 # 发 TERM（默认，优雅退出） kill 1234 # 发 KILL（强制，9 号信号） kill -9 1234 # 发 HUP（重载配置，常见于 nginx/apache） kill -HUP 1234 # 按名字杀 pkill nginx # 强杀所有匹配 pkill -9 nginx # 杀整个进程组（比如杀某个脚本启动的所有子进程） kill -- -1234 # 注意负号 几个安全实践 ： 永远先 kill（默认 TERM），给进程 5-10 秒清理时间，再 kill -9 pkill 之前先用 pgrep 看看会杀哪些进程： pgrep nginx 先确认 不要 pkill 名字太通用的进程（比如 pkill sh 会杀掉很多不相关的东西） 2.6 nice / renice：调整优先级 nice 调整进程优先级（niceness 值），范围是 -20 到 19。值越大优先级越低，越不抢CPU。普通用户只能调正值（降低优先级），root 可以调任何值。 # 启动时设置优先级 nice -n 19 long_running.sh &amp; # 调整已运行进程的优先级 renice -n 10 -p 1234 什么时候用 ：批量任务、定时任务、备份脚本等不影响主业务的进程，调高 nice 值让它们礼貌地跑。 三、网络工具 shell 工程师的另一大任务是跟网络打交道——调 API、传文件、查连接、查 DNS。这一节把最常用的网络工具过一遍。 3.1 ping：基本连通性 # 简单 ping ping example.com # 限制次数 ping - c 4 example.com # 设置间隔 ping - i 0.5 example.com # 不解析域名（纯 IP ping） ping - n 8.8 .8.8 ping 的实战用法 ： # 等待服务启动（轮询 + ping） until ping -c 1 -W 1 myservice.local &amp;&gt;/dev/null; do sleep 1 done echo &quot;服务已就绪&quot; 但注意 ping 不一定反映服务的真实状态，主机能 ping 通不代表端口能连上（防火墙可能禁 ICMP）。测试服务可用性用 curl 或 nc。 3.2 curl / wget：HTTP 客户端 curl 和 wget 都是 HTTP 客户端，但curl 更通用，wget 更专注下载。 # 简单 GET curl https://example.com # 跟随重定向 curl -L https://example.com # 输出 HTTP 头 curl -I https://example.com # POST 请求 curl -X POST -d &apos;name=alice&amp;age=25&apos; https://api.example.com/users # JSON POST curl -X POST -H &apos;Content-Type: application/json&apos; \ -d &apos;{&quot;name&quot;:&quot;alice&quot;,&quot;age&quot;:25}&apos; \ https://api.example.com/users # 携带 cookie curl -b cookies.txt -c cookies.txt https://example.com # 显示详细请求 curl -v https://example.com # 下载文件 curl -O https://example.com/file.zip curl -o myfile.zip https://example.com/file.zip curl 的常用选项 ： -X：指定 HTTP 方法 -H：自定义 header -d：请求体 -o：输出到文件 -O：保持远程文件名 -I：只显示响应头 -L：跟随重定向 -k：忽略证书错误（不推荐生产用） -s：静默模式（不显示进度） -v：详细模式（调试用） curl 的几个实战组合 ： # 测试 API 健康 curl -sf https://api.example.com/health || echo &quot;API 不健康&quot; # 调 API 并解析 JSON（配合 jq） curl -s https://api.example.com/users | jq &apos;.[] | .name&apos; # 下载并限速 curl --limit-rate 1M -O https://example.com/bigfile.zip # 跟踪重定向 curl -L -o file.html https://bit.ly/shortlink -f 是&quot;失败时返回非零&quot; 。配合 set -e 用，curl 失败时脚本会立即退出。这是 shell 脚本里调 API 的标准做法。 wget 跟 curl 的取舍：curl 功能更强、支持更多协议（FTP、SFTP、SMTP 等），wget 专注 HTTP 下载、递归下载更简单。日常 API 调用用 curl，批量下载用 wget。 3.3 ssh / scp / rsync：远程操作 ssh 是最常用的远程登录工具，scp 是基于 ssh 的文件传输，rsync 是更强大的同步工具。 # 登录远程 ssh user @host # 指定端口 ssh -p 2222 user @host # 执行远程命令 ssh user @host &apos;ls -la&apos; # 拷贝文件到远程 scp file.txt user @host :/remote/path/ # 从远程拷贝文件 scp user @host :/remote/file .txt ./ # 拷贝目录 scp -r dir/ user @host :/remote/path/ ssh 的几个实用技巧 ： # 免密码登录（用密钥） ssh-keygen -t ed25519 # 生成密钥 ssh-copy-id user@host # 上传公钥 # SSH 配置文件（~/.ssh/config） Host myserver HostName 192.168 . 1.100 User alice Port 2222 IdentityFile ~ /.ssh/ work_key # 配置后直接用别名 ssh myserver scp file.txt myserver: /tmp/ rsync 比 scp 强大得多 ——增量同步、保留权限、断点续传。 # 本地同步 rsync -av src/ dst/ # 远程同步 rsync -av -e ssh src/ user @host :/remote/path/ # 删除目标里多余的文件 rsync -av --delete src/ dst/ # 排除某些文件 rsync -av --exclude= &apos;*.tmp&apos; src/ dst/ # 模拟运行（看看会同步哪些文件，但不真做） rsync -avn src/ dst/ rsync 的关键选项 ： -a：归档模式（保留所有属性，等于 -rlptgoD） -v：详细输出 -z：传输时压缩 --progress：显示进度 --delete：删除目标端多余文件 -n：模拟运行（dry-run） rsync 的经典用法 ： # 备份脚本（每天跑） rsync -av --delete /data/ /backup/data/ # 部署到多台服务器 for host in server1 server2 server3; do rsync -az -e ssh ./app/ $host :/opt/app/ done 3.4 ss / netstat：网络连接 ss 是新一代的网络连接查看工具（替代 netstat）。 netstat 在新系统上逐步被淘汰 ——如果系统有 ss 就用 ss。 # 查看所有 TCP 连接 ss -t # 查看所有监听端口 ss -tlnp # 查看所有 UDP 连接 ss -u # 查看建立的连接 ss -t state established # 按端口过滤 ss -tlnp &apos;sport = :80&apos; # 统计各状态的连接数 ss -tan | awk &apos;{print $1}&apos; | sort | uniq -c ss 跟 netstat 的输出差别 ：netstat 把&quot;地址&quot;和&quot;端口&quot;合成一列，ss 分开两列。 机器可读性 ss 更好 ，awk 解析更简单。 3.5 dig / nslookup / host：DNS 查询 # 查询 A 记录 dig example.com # 简短输出 dig + short example.com # 查询特定记录类型 dig example.com MX dig example.com TXT # 指定 DNS 服务器 dig @ 8.8 .8 .8 example.com # 反向 DNS（IP 查域名） dig -x 8.8 .8 .8 host 是更简洁的版本 ，输出更友好： host example.com host 8.8.8.8 nslookup 是最老的 ，现在一般不推荐用。 dig 输出的信息最全 ，是排查 DNS 问题时的首选。 3.6 nc / ncat：网络瑞士军刀 nc（netcat）是一个底层网络工具，可以读写 TCP/UDP 连接。 调试网络问题时特别有用 。 # 监听端口（作为 server） nc -l 12345 # 连接端口（作为 client） nc example.com 80 # 端口扫描 nc -zv example.com 80-100 # 文件传输（简单的两端） # 接收端 nc -l 12345 &gt; file.txt # 发送端 nc host 12345 &lt; file.txt nc 的实战用法 ： # 测试端口是否开放 nc -zv example.com 80 # 调试 HTTP 请求（手动发） printf &quot;GET / HTTP/1.0\r\nHost: example.com\r\n\r\n&quot; | nc example.com 80 # 等待服务启动（比 ping 更靠谱） until nc -z myservice 8080; do sleep 1 done 四、系统信息 最后一组命令是查系统状态——查内核、查内存、查磁盘、查用户。日常脚本里经常用这些做&quot;环境检查&quot;。 4.1 uname：系统信息 # 系统信息 uname -a # 单独看内核 uname -r # 单独看主机名 uname -n 4.2 hostname：主机名 hostname # 显示主机名 hostname -I # 显示所有 IP 地址 hostname -f # 显示完整域名（FQDN） 4.3 date / uptime：时间信息 date # 当前时间 date +%Y-%m-%d # 格式化输出 date +%s # Unix 时间戳 date -d &quot;2024-01-15&quot; # 解析字符串为日期 date -d &quot;@1704067200&quot; # 时间戳转日期 date -u # UTC 时间 date -R # RFC 2822 格式 uptime # 系统运行时间和负载 date 在脚本里的常见用法 ： # 日志文件加日期后缀 log_file= &quot;app_ $(date +%Y%m%d) .log&quot; # 计算时间差 start=$( date +%s) do_something end=$( date +%s) echo &quot;耗时 $((end - start) ) 秒&quot; # 7 天前的时间 date -d &quot;7 days ago&quot; +%Y-%m-%d 4.4 who / whoami / id：用户信息 whoami # 当前用户名 id # 详细信息（UID、GID、组） who # 登录的用户 w # 更详细的登录信息 id 在脚本里最常用 ——判断当前是不是 root： if [ &quot; $(id -u) &quot; -ne 0 ]; then echo &quot;请用 root 权限运行&quot; exit 1 fi 4.5 free：内存 free # 内存使用情况 free -h # 人类可读 free -m # 以 MB 为单位 free -s 5 # 每 5 秒刷新 输出字段：total 总内存、used 已用、free 空闲、shared 共享、buff/cache 缓存、available 可用。 注意 available 才是&quot;真正能用的&quot; ——buff/cache 算上才准。 4.6 df / du：磁盘 上一篇讲过 df 和 du 的基础用法，这里补充几个组合： # 看磁盘 IO 统计 iostat # 看文件系统类型 df -T # 找出最大的 5 个文件 find /var - type f - exec du -h {} + | sort -rh | head -5 # 找出大于 1G 的文件 find / - type f -size +1G 2&gt;/dev/null 4.7 lscpu / lsblk / lspci：硬件信息 lscpu # CPU 信息 lsblk # 块设备（磁盘）信息 lspci # PCI 设备 lsusb # USB 设备 这些命令在排查硬件问题时偶尔用到， 日常脚本里用得不多 。 4.8 /proc 文件系统 Linux 把进程和内核信息暴露在 /proc 下—— 很多命令的输出其实就是从这里读的 。 # 当前进程信息 cat /proc/self/status # CPU 信息 cat /proc/cpuinfo # 内存信息 cat /proc/meminfo # 系统启动时间 cat /proc/uptime # 进程的命令行 cat /proc/1234/cmdline 脚本里读 /proc 比调用命令更高效 ——不用 fork 进程。比如判断系统启动时间： uptime_seconds =$(awk &apos;{print $1}&apos; /proc/uptime) 五、总结 这一篇我们覆盖了另外三组常用命令： 进程管理：ps、top、jobs、fg、bg、nohup、kill、pkill、nice 网络工具：ping、curl、wget、ssh、scp、rsync、ss、dig、nc 系统信息：uname、hostname、date、uptime、who、id、free、df 跟上一篇一样，重点不是记住每个命令的参数，而是知道什么时候用哪个、怎么组合。 下一篇是这个系列的最后一部分——讲归档、压缩、权限、用户、还有几个进阶工具（xargs、tee、cron、screen/tmux）。这是 shell 工具箱的最后一组拼图，写完这三篇，你对 shell 日常命令的掌握就完整了。</description><pubDate>Tue, 25 Aug 2026 09:44:14 +0000</pubDate></item></channel></rss>