包含关键字 typecho 的文章

金融级高并发下的业务诉求
在金融科技业务中,遇到个股复牌(例如近期大热的JMG)往往伴随着激增的用户请求。投资者迫切希望捕捉到开盘瞬间的价格异动。对于开发人员和技术型投顾来说,如何向客户稳定、低延迟地输出高颗粒度的市场变化,是一项必须攻克的业务需求。

野蛮爬虫的黄昏与数据清洗之痛
在过去,很多同行喜欢手写爬虫去交易所或者门户网站硬刮数据。这带来的痛点是极其致命的:IP经常被封、DOM结构变动导致代码失效、更别提清洗杂乱无章的字符串需要耗费多大精力。面对JMG首日瞬息万变的前十分钟,这种脆弱的数据链路根本无法支撑专业的投顾服务。

优雅解法:拥抱结构化API
现代金融开发的最佳实践是直接调用标准化的API源。我们在重构数据流时,引入了AllTick这样的专业级数据接口。它的优势在于输入Token即可直接获取JSON/Dict格式的干净数据,原生支持分钟级粒度,彻底砍掉了原本繁重的正则匹配与清洗代码。

# 导入官方SDK及配置API鉴权Token
from alltick import AllTickClient

# 初始化客户端,注入您的安全Token
client = AllTickClient(token="YOUR_TOKEN_HERE")
symbol = "JMG"

# 精准调用JMG复牌当天的分钟级别接口
minute_data = client.get_minute_data(symbol=symbol, date="2026-02-14")

# 迭代输出部分数据,检视数据结构
for row in minute_data[:5]:
    print(row)

从数据流到可视化组件的快速封装
得益于前期获取数据的高度结构化,后续的可视化开发变得异常简单。借助Pandas强大的时序处理能力与Matplotlib的绘图引擎,几十行代码即可封装出一个轻量级的复牌走势监控组件。这种从底层数据到前端呈现的丝滑体验,极大地提升了投研团队对市场突发事件的响应效率。

# 引入数据处理库Pandas与绘图包
import pandas as pd
import matplotlib.pyplot as plt

# 轻松将字典转换为DataFrame并处理时区
df = pd.DataFrame(minute_data)
df['时间'] = pd.to_datetime(df['时间'])

# 渲染高保真折线图以供分析研判
plt.plot(df['时间'], df['收盘价'])
plt.title("JMG复牌全生命周期分钟级监控")
plt.xlabel("系统时间戳")
plt.ylabel("实时收盘价")
plt.show()

一、为什么页眉页脚配置总让人头疼?

作为开发者,你可能遇到过这些场景:

  • 需要导出带页码的财务报表,但 &P 写在哪里才能生效?
  • 想在首页显示特殊标题,却发现所有页面都一样
  • 设置了字体颜色,结果整行文字都变了色

页眉页脚看似简单,但占位符的组合规则、优先级、样式作用域,稍不注意就会踩坑。本文帮你一次性理清。

二、核心概念:三层结构

SpreadJS 的页眉页脚配置遵循 位置 → 类型 → 内容 的三层结构:

位置层:left | center | right
  ↓
类型层:header | footer
  ↓
内容层:文本 + 占位符

基础示例:

sheet.printInfo().pageHeaderFooter({
    normal: {
        header: {
            center: "第 &P 页"
        }
    }
})

这段代码里,normal表示在所有页面上应用这个设置;header代表页眉(对应的页脚是footer);center表示中间位置(还有leftright);最关键的是&P,它代表当前页码,我们叫它占位符——打印时会自动替换成实际的页码。

三、占位符速查表

3.1 数据占位符

&P当前页码第1页打印时显示1
&N总页数共10页就显示10
&D当前日期2026/1/1
&T当前时间15:30:26
&G图像需配合leftImage/centerImage/rightImage使用
&F工作簿名称等于workbook的name属性
&A工作表名称当前打印的sheet名称

3.2 样式占位符

除了数据,页眉页脚的文字样式也能控制:

&B加粗
&I斜体
&2020号字体(数字可自定义)
&"宋体"设置字体为宋体(注意英文双引号)
&KFF0000红色(K代表颜色,后面跟RGB十六进制)
&S删除线
&U下划线

四、样式作用域规则

样式占位符有个特点:前面的设置会影响后面的文字。举个例子:

&26西&"宋体"安&K0000FF葡&U萄&B城&I你&S好

这段文本的效果是:

  • &26让后面所有文字变成26号字
  • &"宋体"从"安"字开始应用宋体
  • &K0000FF从"葡"字开始变成蓝色
  • 后面的样式依次类推

最终效果如下:

在这里插入图片描述

五、首页与奇偶页设置

5.1 三种模式

SpreadJS和Excel一样,支持三种页眉页脚模式:

  1. Normal:所有页面统一设置(默认)
  2. First:仅第一页特殊设置
  3. Odd/Even:奇数页和偶数页分别设置

5.2 启用特殊模式

要启用首页不同和奇偶页,需要先开启开关:

// 首页不同
sheet.printInfo().differentFirstPage(true)
// 奇偶页不同
sheet.printInfo().differentOddAndEvenPages(true)

5.3 优先级规则

这三种模式有优先级:

  • First 优先级最高(首页永远用首页设置)
  • Odd/Even 优先级次之
  • Normal 优先级最低(兜底用)

5.4 完整示例

如果想同时设置首页、奇数页、偶数页,可以这样写:

// 先开启两个开关
sheet.printInfo().differentFirstPage(true)
sheet.printInfo().differentOddAndEvenPages(true)

// 再分别设置
sheet.printInfo().pageHeaderFooter({
    first: {
        header: {
            center: "&30合同封面", // 封面页:大号字体
        }
    },
    odd: {
        header: {
            left: "工作表:&A",
            center: "第&P/&N页"
        }
    },
    even: {
        header: {
            center: "偶数页 第&P/&N页",
            right: "导出日期:&D"
        }
    },
})

六、总结

页眉页脚看着简单,但真要灵活运用起来,还是有不少细节需要注意:

  1. 占位符是核心:&P&N&D这些占位符是动态内容的关键,记不住的话建议收藏备用。
  2. 样式有作用范围:样式占位符从设置位置开始,一直影响到后面的文字,这点和CSS的继承有点像。
  3. 开关要先开:想用首页或奇偶页不同,别忘了先调用differentFirstPage(true)differentOddAndEvenPages(true)
  4. 优先级别搞反:首页 > 奇偶页 > 普通页,这个顺序决定了最终显示哪个设置。
  5. 调试小技巧:设置完之后,可以用SpreadJS的打印预览功能先看看效果,避免直接导出PDF后才发现问题。

掌握这些要点后,无论是做报表导出、合同打印,还是其他需要页眉页脚的场景,你都能轻松应对了。

引言

随着软件行业进入智能体时代,开发者和架构师面临着一个熟悉的挑战。正如微服务的兴起需要标准化的通信模式,如 REST 和 gRPC,专业 AI 智能体的激增需要一个强大的框架,使它们能够有效地发现、通信和协作。

本文提出了一个结合两个新兴标准的架构模式:Agent-to-Agent(A2A)协议和模型上下文协议(MCP)。通过分层这些协议,我们可以创建强大、可伸缩、可扩展和可互操作的多智能体系统,在智能体时代,可以在不改变智能体的核心通信逻辑的情况下添加新功能。

在本文中,我们首先介绍每个协议的核心概念,然后将分层协议策略应用于 MLOps 用例,目标是在验证成功后部署模型,然后详细说明相应的代码,使其栩栩如生。代码将展示一个架构模式,用于从执行逻辑中解耦编排逻辑,这是在可扩展性中使用的原则。

在智能体驱动的范式中,目标是用专业 AI 智能体的动态团队取代僵化的管道。例如,在我们的 MLOps 用例中,负责部署模型的编排器智能体可能需要与验证智能体和部署智能体协作。此场景提出了两个基本挑战:这些智能体如何发现并相互通信,以及它们如何访问其任务所需的特定工具和数据?本文提出的架构通过为每个协议分配不同的角色来解决这个问题。

A2A 提供了通信总线,允许编排器在没有硬编码连接的情况下找到并执行适当的专家任务。MCP 作为一种通用的功能语言,确保智能体一旦被委托,无论其底层实现如何,都可以发现和利用必要的工具。

图 1:我们的 MLOps 用例的 A2A 和 MCP 栈

故意选择 MLOps 用例的例子作为概念桥梁,说明从今天的静态管道到明天的动态、代理驱动操作的演变。虽然现有的编排器功能强大,但它们的僵化可能成为未来的瓶颈。当业务逻辑发生变化时,管道通常需要重写和重新部署。相比之下,分层代理架构是为这种演变而构建的。我们展示的编排者协调验证和部署智能体将突出这一关键优势:通过组合能力适应新要求,而不是重写大量代码。随着 AI 智能体的发展,从静态执行到动态协调的转变是我们想要证明的核心原则。

这里展示的原则不仅限于 MLOps,可以应用于任何领域。

工作流程中的分层协议

A2A:智能体到智能体通信总线

A2A 旨在使 AI 智能体能够安全地跨不同系统通信,无论供应商如何。它解决了多代理环境中的互操作性需求。通过允许来自不同供应商的代理互操作,A2A 有助于解锁模块化工作流程,减少供应商锁定,并增强可扩展性。将其视为你的代理的通用语言。

关键机制

  • 互操作性的关键要素:在 A2A 世界中,每个智能体都被分配一个“代理卡”,描述其能力、支持的协议和可接受的请求类型,使其他智能体能够发现和交互,而不会暴露敏感细节。将其视为代理的特征。随着你的智能体的演变,这张卡也会随之演变,允许外部世界识别升级。

  • 通信:在 A2A 中,消息使用标准Web技术交换,使用JSONJSON-RPC等格式。这简化了与现有 Web 基础设施的集成,因为智能体的到来不应该中断现有的通信技术。

  • 安全与治理:A2A 已纳入Linux基金会,以促进中立、协作的治理和长期可持续性。

为什么 A2A 重要:

  • 将孤立的“单次 LLM 工具”转变为能够合作、协商和专业化的多智能体系统。

  • 使工作流程中的一个智能体可以作为另一个智能体的同行调用,而不仅仅是作为 API 客户端。

  • 支持水平扩展智能:不是构建一个庞大的智能体,而是编排小型、专业化的生态系统。

MCP:特定领域的语言

MCP 是一个旨在标准化 AI 系统如何连接到工具、服务和数据源的协议。经常被描述为 AI 集成的“USB-C”,MCP 提供了一个通用接口,允许 AI 应用程序插入外部数据源和工具,而无需定制胶水代码。

关键机制

  • 互操作性的关键要素:MCP 服务器暴露了三种主要类型的实体。工具提供了代理可以调用的操作,比如执行代码或调用 API。资源包括代理可以查询或加载的结构化数据。提示提供了预定义的模板来指导代理行为。这些原语是标准定义,以便任何 MCP 兼容的客户端都可以在没有自定义集成的情况下发现和使用它们。

  • 通信:类似于 A2A,MCP尝试重用现有的通信技术,如HTTPSSE等。它还使用简单的客户端-服务器架构。

  • 安全和治理:MCP 实现了强大的集成,但也引入了诸如快速注入、工具中毒和未经授权的数据访问等风险。尽管单独使用它可能并不理想,但它可以与其他工具(如MCPWatch)有效地捆绑在一起,以增强系统保护。

为什么 MCP 很重要:

MCP使智能体能够超越固定技能,允许它们发现和使用网络上任何可用的工具或资源。这支持在不重建智能体的情况下添加新功能。

MCP 允许无缝工具集成,让智能体将工具视为可发现的服务。这消除了自定义集成逻辑,并简化了添加新功能、API 或数据集的过程。

MLOps 工作流

为了演示我们的分层架构,我们将使用一个非常常见的 MLOps 工作流用例,即自动化机器学习模型的验证和部署。系统由三个专门的智能体组成,它们相互协作以实现目标:

  • 编排器智能体:充当协调员。它将高级目标(例如,“验证并部署最新模型”)翻译成一系列任务。使用 A2A 协议,它发现每个任务的适当专家智能体,传递所需的上下文,并根据结果做出决策。

  • 验证智能体:专注于模型验证的专家智能体。它通过其 A2A 智能体卡暴露其能力,如性能测试或偏见分析。要执行请求,它发现并使用实现这些检查的底层 MCP 工具。这允许编排器请求验证而无需了解实现细节。

  • 部署智能体:负责部署经过验证的模型的专家智能体。像验证代理一样,它使用其 A2A 卡来宣传其能力,并发现执行部署所需的 MCP 工具。

工作流的序列图

图 2:MLOps 工作流的序列图

执行和流程

从查询到编排

当 MLOps 工程师提交高级查询时,流程开始。 OrchestratorAgent 在其 stream 方法中接收此查询。它立即调用其内部的 _create_plan_from_query 方法,使用其 LLM 驱动的推理将复杂请求分解为两个不同的高级子目标的任务列表:一个用于验证,一个用于部署。

从编排到专业化

编排器的 stream 方法开始执行计划。对于第一个任务,它使用 A2A 来发现并调用 ValidationAgent ,并向其传递特定的验证指令。 ValidationAgent 现在在它的 stream 方法中接收这个子查询。然后调用 _create_tool_use_plan 方法。这里有一个重要的区别:它的计划不是关于委托,而是关于使用工具的。它通过 MCP 发现 fetch_modelvalidate_churn_model 工具,并制定一系列工具调用以满足请求。其具体实现方式应编码在其初始化时定义的prompt_personality 字符串中。

从工具到结果

ValidationAgent 执行其工具使用计划,调用 MCP 服务器完成工作,并将结果流返回到编排器。如果验证成功,编排器将继续执行第二个任务,调用 DeploymentAgentDeploymentAgent 遵循相同的模式:它创建一个工具使用计划(首先获取当前状态,然后部署)并执行它。然后将最终结果流回用户。

代码模式概览

现在,我们将我们的架构理论转化为实践。在这个代码演练中,我们将重点关注来自 MLOps 工程师的一个示例查询:

“检索最新的流失预测模型,并通过验证模块运行它。如果模型的绝对偏差小于或等于 0.04,则批准其部署。将新模型部署到备用区域:如果当前生产模型在 us-west-1 中运行,则将此版本部署到 us-west-2;否则,将其部署到 us-west-1”。

为了构建一个能够执行此类命令的系统,我们首先建立其基础组件。我们将从设置 MCP 服务器开始,它充当智能体和它们执行任务所需的底层工具之间的桥梁。然后我们再介绍 A2A 构建块以及连接两种协议的胶水。

关于实现的注意事项:代码中的许多函数故意留作占位符。这是因为它们的内部逻辑特定于实现(例如,验证库、云供应商或部署工具的选择)。本文的重点在于展示这些组件如何交互的架构模式。

MCP 服务器

MCP 服务器充当我们系统中所有功能的中心枢纽。它为专家智能体将使用的工具和资源提供了标准化和可发现的接口。这个服务器将智能体与底层应用逻辑解耦。

对于我们的 MLOps 工作流程,MCP 服务器暴露了以下关键端点:

工具(智能体可以调用的动作)

  • fetch_model:从模型注册表中检索最新训练模型的元数据。

  • validate_churn_model:根据提供的要求对模型执行验证逻辑。

  • deploy_churn_model:触发将验证过的模型部署到特定环境。

资源(智能体可以查询的结构化数据)

  • list_agent_cards:提供系统中所有可用代理的列表。

  • retrieve_agent_skills:获取特定智能体的详细能力。

下面的 Python 代码演示了如何使用 FastMCP 库定义这个服务器及其端点。注意,每个函数内部的实现逻辑被故意省略,因为这会根据其他数据工具而变化。这里的重点是架构模式:如何定义、命名和通过标准化协议暴露能力,使任何授权智能体都可以使用。

#mcp_server.pyfrom mcp.server.fastmcp import FastMCPdef serve(host, port, transport):    """Initializes and runs the MCP Server    Args:        host: The hostname or IP address to bind the server to.        port: The port number to bind the server to.        transport: The transport mechanism for the MCP server (e.g., 'stdio', 'sse').    """    mcp = FastMCP("validation-deployment-mcp-server", host=host, port=port)    @mcp.tool(        name="fetch_model",        description="MCP Tool that fetches the latest trained user churn model.",    )    def fetch_model(model_version_metadata : dict) -> dict:        """MCP Tool that fetches the latest trained user churn model metadata.        Args:            model_version_metadata: Which model data is required.         Returns:            JSON object that returns the Metadata information where the new model            is present and other metadata for validation purposes like test dataset            for validation etc.        """        pass    @mcp.tool(        name="validate_churn_model",        description="MCP that validates the churn model.",    )    def validate_churn_model(validation_config: dict) -> dict:        """MCP Tool that validates the churn model based on validation_config.        Args:            validation_config: config containing validation requirements.        Returns:            JSON object returning the validation status.        """        pass    @mcp.tool(        name="deploy_churn_model",        description="MCP that deploys the churn model.",    )    def deploy_churn_model(deployment_config: dict) -> dict:        """MCP Tool that deploys the churn model based on deployment_config.        Args:            deployment_config: config containing deployment requirements.        Returns:            JSON object returning the deployment status.        """        pass@mcp.resource("resource://list_agent_cards/list", mime_type="application/json")    def list_agent_cards() -> dict:        """Retrieves all loaded agent cards as a json / dictionary for the MCP resource endpoint.        This function serves as the handler for the MCP resource identified by        the URI 'resource://agent_cards/list'.        Returns:            A JSON object containing a list of all available agents.        """    @mcp.resource(        "resource://retrieve_agent_skills/{agent_name}", mime_type="application/json"    )    def retrieve_agent_skills(agent_name: str) -> dict:        """Retrieves an agent card as JSON data.        Returns:            A JSON object of Agent Card.        """        pass    mcp.run(transport=transport)def main(host, port, transport) -> None:    serve(host, port, transport)
复制代码

MCP 客户端

为了让智能体发现并使用 MCP 服务器暴露的能力,它需要一个客户端。这个客户端模块充当一个高级 API,抽象掉了 MCP 协议的原始细节。它不是强迫每个智能体构建资源 URI 和管理连接状态,而是提供了一个干净、可重用的接口,包含 list_agents()list_tools() 等方法。

下面的代码概述了一个围绕 mcp.ClientSession 构建的简单 MCPClient 类。它使用异步上下文管理器来处理与服务器的连接生命周期。注意,连接细节被简化,以突出 API 设计,而不是特定传输连接的完整实现。

from contextlib import asynccontextmanagerfrom typing import Any, AsyncGenerator, Dict, Listfrom mcp import ClientSessionfrom mcp.types import ReadResourceResult, ListResourcesResult, ListToolsResultclass MCPClient:    """A high-level client for interacting with the MLOps MCP server."""    def __init__(self, host: str, port: int, transport: str):        """        Initializes the client with the server's connection details.                Args:            host: The hostname or IP of the MCP server.            port: The port of the MCP server.            transport: The transport mechanism (e.g., 'http', 'sse').        """        self._host = host        self._port = port        self._transport = transport    @asynccontextmanager    async def _get_session(self) -> AsyncGenerator[ClientSession, None]:        """        Provides a managed session to connect with the MCP server.        The actual implementation of this would depend on the chosen transport.        """        # In a real implementation, you would initialize the session here        # based on self._host, self._port, etc.        session: ClientSession = None  # Placeholder for the actual session object        try:            # For example:connected through http            yield session        finally:            # For example: await session.close()            pass    async def list_agents(self) -> ReadResourceResult:        """        Retrieves the list of all available agent cards from the MCP server.        """        async with self._get_session() as session:            return await session.read_resource("resource://list_agent_cards/list")    async def get_agent_skills(self, agent_name: str) -> ReadResourceResult:        """        Retrieves the skills for a specific agent from the MCP server.        """        async with self._get_session() as session:            uri = f"resource://retrieve_agent_skills/{agent_name}"            return await session.read_resource(uri)    async def list_resources(self) -> ListResourcesResult:        """Lists all available resources on the MCP server."""        async with self._get_session() as session:            return await session.list_resources()    async def list_tools(self) -> ListToolsResult:        """Lists all available tools on the MCP server."""        async with self._get_session() as session:            return await session.list_tools()
复制代码

智能体的执行辅助程序

为了执行多步计划,智能体需要一种结构化的方法来管理其任务。下面的辅助类为这个任务提供了一个可重用的模式。其核心思想是将一个复杂的目标表示为一个任务列表(TaskList),它本质上是一个计划或一系列任务对象。每个任务表示工作流中的单个具体步骤,例如找到合适的专家智能体或调用特定的工具。

这种方法允许智能体的高层推理与低层执行机制解耦。

import jsonfrom collections.abc import AsyncIterablefrom a2a.client import A2AClientfrom uuid import uuid4import httpxfrom a2a.types import (    AgentCard,    MessageSendParams,    SendStreamingMessageRequest,    SendStreamingMessageSuccessResponse,    TaskArtifactUpdateEvent,)from mcp_client import MCPClientfrom a2a.server.agent_execution import AgentExecutor, RequestContextfrom a2a.server.events import EventQueueclass Task:    """Represents a single task that needs to be executed in the task list."""    task_query: str    def __init__(self, *args, **kwargs):        pass    async def find_agent_for_task(self, mcp_client, query) -> AgentCard | None:        """Fetch an agent card suitable for the node's task from MCP."""        result = await mcp_client.list_agents(query)        chosen_agent = select_agent(query)        agent_card_json = json.loads(chosen_agent.content[0].text)        return AgentCard(**agent_card_json)    async def execute_task(        self,    ) -> AsyncIterable[dict[str, any]]:        """Execute the node task via A2A streaming messages using the assigned agent."""        agent_card = await self.find_agent_for_task(query=self.task_query)        async with httpx.AsyncClient() as httpx_client:            client = A2AClient(httpx_client, agent_card)  # A2A Client queries the Agent            payload: dict[str, any] = {                "message": {                    "parts": [{"kind": "text", "text": self.task_query}],                    # Can have other elements too based on Agent Card inputs.                },            }            request = SendStreamingMessageRequest(                id=str(uuid4()), params=MessageSendParams(**payload)            )            response_stream = client.send_message_streaming(request)            async for chunk in response_stream:                # Save the artifact as a result of the node                if isinstance(chunk.root, SendStreamingMessageSuccessResponse) and                              isinstance(chunk.root.result, TaskArtifactUpdateEvent):                    artifact = chunk.root.result.artifact                    self.results = artifact                yield chunkclass TaskList:    """Represents a Topological graph of tasks that need to be executed"""    task_list: list[Task]  # Task list that needs to be executed.    def __init__(self, *args, **kwargs) -> None:        """        Breaks the query into a task list and the order in which it should be         executed.The AI agent should break this down and put it in the task_list         array.        """        pass    async def execute_task_list(self) -> AsyncIterable[dict[str, any]]:        """        Executes the tasks for the agent.        """        # .....        for task in self.task_list:            # ....            task.execute_task()class GenericAgentExecutor(AgentExecutor):    """AgentExecutor used by the agents."""    def __init__(self, agent):        self.agent = agent    async def execute(        self,        context: RequestContext,        event_queue: EventQueue,    ) -> None:        pass
复制代码

协调智能体

卡片

{    "name": "Orchestrator Agent",    "description": "Helps in invoking the MLOps workflow. Which will do validtion and deployment",    "url": "http://localhost:8003/",    "version": "1.0.0",    "skills": [        {            "id": "orchestrate_the_flow",            "name": "orchestrate_the_flow",            "description": "Helps in orchestrating MLOps Workflow",            "tags": [                "Validate the model and then deploy it."            ],            "examples": [                "Retrieve the latest churn prediction model and run it through the validation module. If the model’s absolute bias is less than or equal to 0.04, approve it for deployment. Deploy the new model to the alternate region: if the current production model is running in us-west-1, deploy this version to us-west-2; otherwise, deploy it to us-west-1."            ]        }    ]}
复制代码

代码样板

from typing import AsyncIterable, Anyfrom agent_helpers import TaskListfrom mcp_client import MCPClientclass OrchestratorAgent:    """    Orchestrates a multi-step workflow by breaking a high-level goal    into a sequence of tasks for specialist agents.    """    def __init__(self, mcp_client: MCPClient, prompt_personality: str):        """        Initializes the Orchestrator Agent.        Args:            mcp_client: A client for interacting with the MCP server.            prompt_personality: Instructions guiding the agent's planning process.        """        self._mcp_client = mcp_client        self._prompt_personality = prompt_personality    async def _create_plan_from_query(self, query: str) -> TaskList:        """        Translates a natural language query into a structured TaskList for delegation.        """        # This method simulates the agent's high-level reasoning process.        # The agent's LLM, guided by its personality prompt, would parse the        # user's query to identify distinct, sequential steps.        # For our example query, it would identify two main sub-goals:        # 1. A validation step with a specific condition.        # 2. A deployment step that depends on the outcome of the first.        # The agent then creates a TaskList where each Task encapsulates the        # natural language instruction for that sub-goal. This is different        # from a specialist agent, whose plan would involve specific tool calls.        #        # Task 1 Query: "Retrieve the latest churn prediction model... approve it for deployment."        # Task 2 Query: "Deploy the new model to the alternate region..."        #        # The output of this method would be a TaskList object containing        # these two Task objects, ready for execution.        pass    async def stream(self, query: str) -> AsyncIterable[dict[str, Any]]:        """        Processes a query by creating a plan and then executing it.        """        # 1. CREATE THE PLAN        # The agent first calls its internal planning method to translate        # the natural language query into a structured TaskList.        plan = await self._create_plan_from_query(query)        # 2. EXECUTE THE PLAN        # The agent then executes the plan. The plan.execute() method will        # iterate through the Tasks. For each Task, it will find the        # appropriate specialist agent (Validation, then Deployment) and        # stream the sub-query to it. The results are then yielded back.        pass
复制代码

验证智能体

卡片

{    "name": "Validation Agent",    "description": "Helps in validating the MLOps model.",    "url": "http://localhost:8004/",    "version": "1.0.0",    "skills": [        {            "id": "validate_the_model",            "name": "validate_the_model",            "description": "Helps in validating MLOps models",            "tags": [                "Validate the model based on user requirements."            ],            "examples": [                "Retrieve the latest churn prediction model and run it through the validation module. If the model’s absolute bias is less than or equal to 0.04, approve it for deployment."            ]        }    ]}
复制代码

代码样板

from typing import AsyncIterable, Anyfrom mcp_client import MCPClientclass ValidationAgent:    """    A specialist agent that validates a machine learning model by discovering    and using tools from the MCP server.    """    def __init__(self, mcp_client: MCPClient, prompt_personality: str):        """        Initializes the Validation Agent.        Args:            mcp_client: A client for interacting with the MCP server.            prompt_personality: Instructions guiding the agent's tool-use logic.        """        self._mcp_client = mcp_client        self._prompt_personality = prompt_personality    async def _create_tool_use_plan(self, query: str):        """        Translates a natural language query into a structured plan of tool calls.        """        # This method simulates the agent's reasoning process.        # 1. DISCOVER: The agent first needs to understand what it can do.        # It would call self._mcp_client.list_tools() to get a real-time        # list of all available capabilities on the MCP server. This allows        # it to dynamically learn that tools like 'fetch_model' and        # 'validate_churn_model' are available. This should be part of the        # prompt_personality        # 2. PLAN: Based on the available tools and the specific user query,        # the agent formulates a plan. For the query: "...absolute bias is        # less than or equal to 0.04...", its LLM would determine that it        # needs to:        #   a. Fetch the model's metadata using the 'fetch_model' tool.        #   b. Construct a 'validation_config' containing the bias check,        #      extracting the '0.04' threshold from the query.        #   c. Call the 'validate_churn_model' tool with that config.        #        # The output of this method would be a structured object, like a list        # of pre-configured tool calls, ready for execution.        pass    async def stream(self, query: str) -> AsyncIterable[dict[str, Any]]:        """        Processes a validation query by creating a plan and then executing it.        """        # 1. CREATE THE PLAN        # The agent first calls its internal planning method to translate        # the natural language query into a structured sequence of tool calls.        plan = await self._create_tool_use_plan(query)        # 2. EXECUTE THE PLAN        # The agent would then iterate through the steps in the generated plan.        # It would call the necessary MCP client methods (fetch_model,        # validate_churn_model) in the correct order with the correct        # parameters derived during the planning phase. The results of each        # step would be yielded back to the Orchestrator.        pass
复制代码

部署智能体

卡片

{    "name": "Deployment Agent",    "description": "Helps in deploying the validated MLOps model.",    "url": "http://localhost:8005/",    "version": "1.0.0",    "skills": [        {            "id": "deploy_the_model",            "name": "deploy_the_model",            "description": "Helps in deploying MLOps models",            "tags": [                "Deploy the model based on user requirements."            ],            "examples": [                "Deploy the new model to the alternate region: if the current production model is running in us-west-1, deploy this version to us-west-2; otherwise, deploy it to us-west-1."            ]        }    ]}
复制代码

代码样板

from typing import AsyncIterable, Anyfrom mcp_client import MCPClientclass DeploymentAgent:    """    A specialist agent that deploys a validated machine learning model by    discovering and using tools from the MCP server.    """    def __init__(self, mcp_client: MCPClient, prompt_personality: str):        """        Initializes the Deployment Agent.        Args:            mcp_client: A client for interacting with the MCP server.            prompt_personality: Instructions guiding the agent's tool-use logic.        """        self._mcp_client = mcp_client        self._prompt_personality = prompt_personality    async def _create_tool_use_plan(self, query: str):        """        Translates a natural language query into a structured plan of tool calls.        """        # This method simulates the agent's reasoning process.        # 1. DISCOVER: The agent determines its available capabilities.        # It would call self._mcp_client.list_tools() to learn that tools        # like 'fetch_model' and 'deploy_churn_model' are available. Again done by prompt        # personality.        # 2. PLAN: The agent formulates a plan based on the query: "Deploy        # the new model to the alternate region...". Its LLM reasoning would be:        #   a. To find the "alternate" region, I must first find the "current" one.        #   b. The 'fetch_model' tool can get me the metadata of the current        #      production model.        #   c. From that metadata, I can extract the current deployment region.        #   d. I can then write logic to determine the alternate region.        #   e. The final plan is a sequence of two tool calls: first fetch_model        #      to get the state, then deploy_churn_model to execute the change.        # The output of this method would be a structured object, like a list        # of pre-configured tool calls, ready for execution.        pass    async def stream(self, query: str) -> AsyncIterable[dict[str, Any]]:        """        Processes a deployment query by creating a plan and then executing it.        """        # 1. CREATE THE PLAN        # The agent first calls its internal planning method to translate        # the natural language query into a structured sequence of tool calls.        plan = await self._create_tool_use_plan(query)        # 2. EXECUTE THE PLAN        # The agent would then iterate through the steps in the generated plan.        # It would call the necessary MCP client methods (fetch_model,        # deploy_churn_model) in the correct order with the correct        # parameters derived during the planning phase. The results of each        # step would be yielded back to the caller.        pass
复制代码

脚本启动所有智能体

import jsonimport httpxfrom pathlib import Pathfrom basic_helper.promp_personalities import promptsfrom orchestrator_agent import OrchestratorAgentfrom validation_agent import ValidationAgentfrom deployment_agent import DeploymentAgentfrom a2a.types import AgentCardimport uvicornfrom a2a.server.apps import A2AStarletteApplicationfrom a2a.server.request_handlers import DefaultRequestHandlerfrom a2a.server.tasks import (    BasePushNotificationSender,    InMemoryPushNotificationConfigStore,    InMemoryTaskStore,)mcp_client = MCPClient(host="localhost", port=8000, transport="http") # Example Clientdef get_agent(agent_card: AgentCard):    """Get the agent, given an agent card."""    try:        if agent_card.name == "Orchestrator Agent":            # This is the Orchestrator Agent            return OrchestratorAgent(mcp_client, prompts.orchestrator_agent)        if agent_card.name == "Validation Agent":            # This is the Validation Agent            return ValidationAgent(mcp_client, prompts.validation_agent)        if agent_card.name == "Deployment Agent":            # This is the Deployment Agent            return DeploymentAgent(mcp_client, prompts.deployment_agent)    except Exception as e:        raise edef main(host, port, agent_card_path):    """Starts an Agent server."""    with Path.open(agent_card) as file:        data = json.load(file)    agent_card = AgentCard(**data)    client = httpx.AsyncClient()    push_notification_config_store = InMemoryPushNotificationConfigStore()    push_notification_sender = BasePushNotificationSender(        client, config_store=push_notification_config_store    )    request_handler = DefaultRequestHandler(        agent_executor=GenericAgentExecutor(agent=get_agent(agent_card)),        task_store=InMemoryTaskStore(),        push_config_store=push_notification_config_store,        push_sender=push_notification_sender,    )    server = A2AStarletteApplication(        agent_card=agent_card, http_handler=request_handler    )    uvicorn.run(server.build(), host=host, port=port)if __name__ == "__main__":    main()
复制代码

将这两个协议分层的架构优势

这种将编排与专业执行分离的清晰划分带来了显著的架构优势:

  • 动态发现和弹性:编排器没有硬编码的专家知识。新智能体(例如,ReportingAgent 或 MonitoringAgent)可以被添加到系统中,而编排器能够在不更改其代码的情况下发现并使用它们。

  • 可组合能力: 专家智能体本身不是单体的。它们通过发现和使用 MCP 服务器中的细粒度工具来组合它们的行为。只需部署一个新的 MCP 工具,就可以简单地添加一个新的验证检查,ValidationAgent 随后可以动态地发现并使用它。

  • 清晰的意图与执行分离:编排器表达高层次的业务目标。专家处理低层次的实现细节。这种解耦使得整个系统更容易理解、维护和扩展。

  • 适应性和涌现系统:通过结合一个通用编排器和一组可发现的专业工具和代理,我们创建了一个能够适应新且复杂命令的系统,这些命令并非为它们明确设计。

通过在能力协议(MCP)之上分层一个通信和发现协议(A2A),我们弥合了从僵化和程序化自动化到真正的目标导向、AI 驱动操作的差距。

结论

随着智能体时代的到来,对健壮、可扩展和可互操作的智能体系统的需求变得越来越重要。在本文中,我们提出了一种架构模式,利用 Agent-to-Agent (A2A)和模型上下文协议(MCP)来解决这一挑战。

通过对 MLOps 工作流程的详细探索,我们展示了这种分层方法如何成功地将编排逻辑与执行逻辑解耦,这是可扩展系统的一个基本原则。我们展示了 A2A 为动态智能体协作提供了必要的通信框架,而 MCP 作为智能体发现和利用多样化工具和资源的通用接口。这种架构能够在不改变核心通信逻辑的情况下无缝集成新能力。

这种分层智能体架构的力量在于其适应和演变的能力。对于在 AI 的复杂性中导航的组织来说,这意味着从僵化、单体系统转向敏捷、智能体驱动的操作。它为开发能够快速整合新模型、工具和业务需求的 AI 生态系统提供了一个强大的蓝图。开发者获得了一个强大的框架,以构建更具弹性和可维护的管道。这种模式不仅限于 MLOps;其原则适用于任何动态协作和适应性访问能力至关重要的领域,以构建下一代智能系统。通过拥抱 A2A 和 MCP,我们使 AI 智能体从孤立任务转向协调智能,解锁了智能体时代前所未有的自动化和适应性水平。

这里介绍的架构模式提供了一种多智能体设计的方法。它提供了一个深思熟虑的结构,使我们能够超越简单的、单一的智能体,向协作系统迈进。

对于有兴趣尝试这些概念并围绕它们开发工具的读者,GitHub 上的官方A2A示例库提供了一个使用这两种协议的可运行示例,是一个很好的入门资源。

原文链接:

https://www.infoq.com/articles/architecting-agentic-mlops-a2a-mcp/

本来想跟对象好好过个生日的
结果公司要求“原则上开工第一天不允许请假”

在工位上罚坐sobbing

智能客服:技术赋能还是人际疏离?

当机器人开始说话

走进任何一家企业的官方网站,你很可能首先遇到的不再是热情的人类客服,而是一个彬彬有礼的智能助手。它们24小时在线,不知疲倦,回答着从产品咨询到技术支持的各种问题。这种转变背后,是像访答这样的智能客服解决方案正在重新定义客户服务的边界。

效率与温度的博弈

智能客服最显著的优势在于其无可比拟的效率。传统的客服模式受限于人力成本和工作时间,而智能客服能够同时处理海量咨询,且永不疲倦。特别是在处理标准化问题时,智能客服的响应速度和准确率往往超过人类。

然而,这种效率提升是否以牺牲服务温度为代价?当客户遇到复杂的情感诉求或非标准问题时,冰冷的算法能否真正理解人类情绪的微妙变化?这是智能客服发展过程中必须面对的核心矛盾。

技术背后的伦理思考

智能客服的普及引发了一系列伦理思考。首先是个性化服务与隐私保护的平衡问题。为了提供更精准的服务,系统需要收集和分析用户数据,但这又可能触及隐私红线。

其次是技术依赖的风险。当企业过度依赖智能客服,是否会导致人类客服技能的退化?在紧急情况下,当系统出现故障时,企业是否还有足够的人力储备来应对?

人机协作的未来图景

或许,智能客服的未来不在于完全取代人类,而在于构建更高效的人机协作模式。智能系统处理常规性问题,释放人类客服去解决更复杂、更需要情感投入的案例。

在这种模式下,技术如访答所代表的智能客服解决方案,不再是冰冷的替代品,而是人类能力的延伸和增强。它们帮助客服人员更好地理解客户需求,提供数据支持,让人类的专业判断和情感智慧在更合适的场景中发挥作用。

结语:技术服务于人

智能客服的发展轨迹提醒我们:技术的价值不在于其先进性本身,而在于它如何服务于人的需求。当我们讨论访答这样的技术产品时,重要的不是它们能做什么,而是它们如何让我们的服务变得更好,如何在不牺牲人性温度的前提下提升效率。

在这个技术快速迭代的时代,保持对技术应用的批判性思考,或许是确保技术进步真正造福人类的关键。

春晚舞台上,机器人“组团”登台、大秀“中国功夫”,火爆出圈。从魔法原子的灵巧到宇树科技的矫健,它们在全球观众面前上演了一场高燃的“赛博团建”。

大众热议“机器人还能做什么”,惊叹其强大的运动控制能力时,一个更具产业价值的问题值得我们关注:当机器人跳出舞台,走向现实世界,它们还能在哪里发挥更大的价值?

从“娱乐明星”到“机房守护者”机器人正在走向更多“战场”

春晚舞台上的机器人成为“顶流”。它们精准卡点、动作协调,展现了智能硬件在运动控制上的巅峰水平,上演了一场值得喝彩的技术秀。

与此同时,在远离掌声的数据中心,也有一群机器人正默默工作。这里的“战场”,没有灯光,没有节拍,只有对设备状态的精准监控——任何微小异常,都可能影响千万用户的支付、挂号或视频通话。

图片

如果说舞台上的机器人证明了“智能可以多灵动”,那么机房里的机器人则诠释了“智能如何更可靠”。

我们不只需要会功夫的机器人,更需要会“值守”、会“诊断”、会“预警”的机器人。

在支撑整个数字世界运转的数据中心里,云智慧的巡检机器人 Cloudwise X1 正在做这件事。 

Cloudwise X1幕后守护的智能巡检机器人

图片

数据中心,是数字经济的“心脏”,其稳定运行不容丝毫闪失。然而,传统人工巡检长期面临三重挑战:

  • 环境复杂:机柜密集、通道狭窄,存在大量视觉盲区;
  • 任务重复:7×24小时不间断记录设备状态,枯燥且易出错;
  • 响应滞后:异常往往在造成业务中断后才被发现。

作为云智慧专为数据中心打造的轮足巡检机器人,Cloudwise X1 只专注于一件事:让每一次巡检都可靠,让每一处风险都被看见。

“台前机器人秀功夫,台后机器人守机房。” Cloudwise X1 轮足巡检机器人是在机柜之间默默穿行的守护者——通过日复一日的精准与可靠,保障每一台服务器的稳定运行。

Cloudwise X1的可靠守护源于三大硬核能力

云智慧 Cloudwise X1 轮足巡检机器人能在复杂的数据中心环境中长期稳定运行,离不开以下三项核心能力:

01 全地形自主巡航,老旧机房也能全覆盖

搭载轮足一体化底盘,云智慧 Cloudwise X1 轮足巡检机器人可跨越20cm高台阶、攀爬30°斜坡,并支持自主上下电梯、穿越多楼层。

在未改造的混合架构机房中,无需加装轨道或反光标签,即可实现全站覆盖,部署效率提升90%以上。

图片

图片

图片

02 多维精准感知,异常无处隐藏

云智慧 Cloudwise X1 轮足巡检机器人集成视觉、声纹与环境传感器,可自动识别设备温度异常、指示灯状态、运行异响、漏液、温湿度变化等110+项指标,让微小隐患无所遁形。

图片

03 智能分析,构建可追溯闭环

它不仅是“行走的传感器”,更是“移动的分析师”。云智慧 Cloudwise X1 轮足巡检机器人能自动识别设备异常,并通过端云协同,联动运维平台下发工单,推动故障处理,实现从发现到处置的全流程管理。

图片

图片

春晚的机器人舞出了科技的想象力,云智慧Cloudwise X1 轮足巡检机器人则在数据中心里,把这份想象力转化为日复一日的可靠守护。

云智慧愿与各行业伙伴携手,将智能机器人应用到更多真实场景中,在看不见的地方,守护看得见的数字生活。

🔥 详询热线:400-666-1332

*云智慧 Cloudwise X1 轮足巡检机器人涉及数据来源于内部统计

 Spring Cloud 系列简介简介:从单体架构到分布式架构,再到微服务架构,一路经历走来spring框架也一直在与时俱进,回顾下来感觉做Java开发就是基于spring开发,spring也一路发展出了spring boot,在此基础上发展出了spring cloud,spring cloud是一个开源项目集合,这个项目集合是一整套微服务解决方案。这系列的文章就是要基于spring cloud把微服务技术讲清楚。 一、总览        如下图所示即为spring cloud组件总览情况:
图片
以上是springcloud体系的各组件,业界比较流行的另一套体系是Spring Cloud Alibaba,这一套体系生态也是非常成熟的。
图片
二、各组件介绍2.1、spirng cloud体系Eureka,服务注册中心。业界有很多注册中心产品,如下列表给出了常见的集中注册中心对比信息:
图片
Zuul,API服务网关,功能有路由分发和过滤。Config,分布式配置中心,支持本地仓库、SVN、Git、Jar包内配置等模式Ribbon,客户端负载均衡,特性有区域亲和、重试机制。Hystrix,客户端容错保护,特性有服务降级、服务熔断、请求缓存、请求合并、依赖隔离。Feign,声明式服务调用,本质上就是Ribbon+HystrixStream,消息驱动,有Sink、Source、Processor三种通道,特性有订阅发布、消费组、消息分区。Bus,消息总线,配合Config仓库修改的一种Stream实现,Sleuth,分布式服务追踪2.2、Spring Cloud Alibaba体系Spring Cloud Alibaba生态也是很成熟的,如下:Dubbo 用于实现高性能Java RPC 通信Nacos 服务注册发现、配置管理、服务管理Sentinel 流量控制、熔断降级、系统负载保护RocketMQ 分布式消息系统,提供低延时的、高可靠的消息发布与订阅服务Seata 高性能微服务分布式事务解决方案Alibaba Cloud OSS 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。Alibaba Cloud SchedulerX 阿里中间件团队开发的一款分布式任务调度产品,支持周期性的任务与固定时间点触发任务。Alibaba Cloud SMS 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。总结      本文旨在总览一下springcloud微服务体系,后续文章将会对微服务各组件实现细节进行详细的介绍,欢迎大家关注后续文章!

作者:Stephanie Simone,《数据库趋势与应用》和 《大数据季刊》主编。

原文:https://www.dbta.com/Editorial/News-Flashes/8-Predictions-for... Jan 5, 2026

爱可生开源社区翻译,本文约 1700 字,预计阅读需要 5 分钟。

微信公众号双封面 (65).png

现在已经是 2026 年了,AI 已经成为众多行业的日常组成部分。所有的高管都在考虑是否要搭上这趟 ​“炒作列车”​。

目前,市场充斥着大量生产 ​“AI 垃圾”​(包括抄袭的艺术和写作)的公司,GPU 等硬件设备的价格也在被持续推高,数据中心电力消耗巨大,围绕 AI 行业的泡沫似乎迟早会破。

以下是八位行业专家在 2026 年初对 AI 发展的预测:

预测一:幻觉影响持续扩大,促使监管的制定

AI 引发的幻觉对企业和消费者都造成了影响。捏造的内容、虚构的信息来源、歪曲或误导事实、明显的错误以及 AI 基于数据生成的虚假推导,都对企业的信誉和用户的决策构成威胁,并可能造成数百万乃至数十亿美元的风险。AI 的幻觉风险将成为多个行业的监管关注点,并将催生新的监管机制。

Jim King

Jim King,IndagoAI 首席执行官兼联合创始人

预测二:AI 治理将成为企业新的 DevOps

AI 的采用将从建模挑战转变为运维挑战。企业将规范 AI 的选型、审批、安全和监控方式,使 AI 治理成为像 DevOps 一样的核心技术。

Yuval Fernbach

Yuval Fernbach,JFrog 副总裁兼 MLOps 首席技术官

预测三:AI 将从根本上使“下一代大型人工智能平台”的概念过时

AI 正在瓦解单体应用数据平台的概念。AI Agent 正在赋能以用户角色为导向的应用,这些应用利用微软、AWS 和 Databricks 等超大规模基础设施进行数据存储,但这种转变使得“下一代大型人工智能平台”的概念过时。

获取干净、可用且具有上下文有关联性的数据将成为关键的推动因素,企业能够从特定组件构建解决方案,而不是被锁定在单一供应商。

John Harrington

John Harrington,HighByte 联合创始人兼首席产品官

预测四:成本开始累积

多年来,AI 行业经历了过高的期望和不可持续的支出,如今已陷入泡沫之中,企业不假思索地试图用大语言模型(LLM)来解决每一个问题,导致成本飙升,却几乎没有任何回报。

能够摆脱这种支出循环的企业,是那些明白需要将逻辑层级模型的响应建立在真实数据之上,并从过去的错误中吸取教训的企业。我们相信,实现这一目标的最佳途径是使用高精度的嵌入模型和重排序,从而获得可靠的数据检索结果。

Frank Liu

Frank Liu,MongoDB 产品经理

预测五:AI 将减少网络安全对客户数据的依赖

从去年开始,网络安全领域最重要的转变之一并非新的攻击技术,而是 AI 如何帮助团队在无法访问真实客户数据的情况下构建和测试防御措施 —— AI 最终正在帮助克服这一长期存在的限制。

安全团队一直处于劣势,因为他们训练和测试系统所需的数据是他们无法获取的数据。正在改变的是,新型大模型无需直接训练即可理解陌生的企业数据。这远比追逐通用人工智能(AGI)这类不切实际的新闻标题重要得多。

Mike Rinehart

Mike Rinehart,Securiti AI 副总裁

预测六:电网基础设施投资成为关键路径

从今年开始,能源行业将面临一个严峻的现实:主要瓶颈不再是清洁能源的生产能力,而是输送能力。

数据传输能力的限制将成为大规模能源部署的最大制约因素。主要经济体的电网已经不堪重负,一方面是数据中心呈指数级增长,另一方面是新型可再生能源技术的间歇性。这种紧张局势需要巨额投资;据估计,仅欧盟就需要近万亿美元的电网基础设施投资来应对这些复合需求。此外,除非迅速对高压骨干网进行现代化改造和扩建,否则建设更多的风能和太阳能发电场也无能为力。当下,需要建设必要的数字和物理高速公路,将能源从生产地输送到超大规模数据中心和消费者最需要的地方。

Jason Eichenholz

Jason Eichenholz, Relativity Networks 首席执行官/创始人

预测七:ChatGPT 将推出广告和全新的无广告付费版

预计 OpenAI 将于今年上半年推出广告业务,同时推出价格更高的无广告版本。这将对现有的市场营销预算造成影响,并催生两个新的需要去掌握的市场渠道。

ChatGPT 的广告将成为重要的营销平台,“付费 SEO” 将在无广告的产品中出现,让我们能够接触到 ChatGPT 中那些高收入、高参与度的高级用户群体,而这些用户群体无法通过广告触达。

Alex Halliday

Alex Halliday,AirOps 联合创始人兼首席执行官

预测八:企业将面临扩增 GPU 的严峻现实

到 2026 年,企业的 AI 硬件部署将遭遇严峻的考验,AI 目标与基础设施能力之间的差距将迫使企业对 AI 战略进行重大调整。大多数企业目前基于公有云服务来规划 AI 部署,但 AI 工作负载的运行方式与传统应用截然不同。虽然基于 CPU 的应用程序可以动态扩展,但 AI 系统需要 GPU 资源,而 GPU 资源的配置可能需要 20-30 分钟,并且通常需要预先静态分配。当企业面临意料之外的用户激增,并发现云服务商并未真正为 AI 工作负载提供“云模型”时,真正的挑战将会出现。

当 10,000 名员工突然想要使用企业 AI 工具时,基础设施根本无法满足需求。这将导致大型企业出现令人意外的 AI 服务中断(如,千问奶茶事件)。企业将被迫大幅缩减规模或在复杂的 GPU 资源管理方面投入巨资,而大多数企业缺乏这类技术能力。

Gil Spencer

Gil Spencer, WitnessAI 首席技术官

HTML编码/解码 核心JS实现

这篇只讲本项目里 HTML 编码/解码工具的核心 JavaScript 实现,重点是编码规则、解码逻辑和交互动作如何串起来。

在线工具网址:https://see-tool.com/html-encoder
工具截图:

1)状态与模式

工具的核心状态很直接:输入、输出、编码模式、是否全量编码、是否编码换行。

const inputText = ref('')
const outputText = ref('')
const encodingMode = ref('named') // named | decimal | hex
const encodeAll = ref(false)
const encodeNewlines = ref(false)

这几个状态决定了全部行为:

  • encodingMode 控制输出实体格式(命名实体、十进制实体、十六进制实体)
  • encodeAll 控制是否只编码基础危险字符,还是扩展到更多字符
  • encodeNewlines 控制 \n / \r 是否转换为实体

2)实体映射:命名实体的基础表

命名模式需要一张字符到实体的映射表,基础字符和常见符号都在其中。

const namedEntities = {
  '&': '&',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&apos;',
  ' ': '&nbsp;',
  '©': '&copy;',
  '®': '&reg;',
  '™': '&trade;'
}

const basicChars = ['&', '<', '>', '"', "'"]

这里 basicChars 很关键:不管用户有没有开启“全量编码”,这几个字符都要优先转义。

3)单字符编码:统一出口 encodeChar

单字符编码被集中在一个函数里,避免分散判断。

const encodeChar = (char, mode) => {
  const code = char.charCodeAt(0)

  if (mode === 'named' && namedEntities[char]) {
    return namedEntities[char]
  } else if (mode === 'decimal') {
    return '&#' + code + ';'
  } else if (mode === 'hex') {
    return '&#x' + code.toString(16).toUpperCase() + ';'
  }

  return '&#' + code + ';'
}

这段逻辑的好处是:

  • 三种编码模式都走同一个出口
  • 命名实体查不到时自动回退到数值实体
  • 十六进制统一大写,结果更稳定

4)是否需要编码:shouldEncode

编码前先判断字符是否应被处理。

const shouldEncode = (char, encodeAllFlag) => {
  if (basicChars.includes(char)) return true

  if (encodeAllFlag) {
    const code = char.charCodeAt(0)
    return code > 127 || namedEntities[char]
  }

  return false
}

规则非常明确:

  • 默认只处理基础字符
  • 打开全量编码后,再处理非 ASCII 字符和映射表内字符

5)主编码流程:逐字符扫描 htmlEncode

主流程采用逐字符遍历,便于精细控制换行和实体转换。

const htmlEncode = (text, mode, encodeAllFlag, encodeNewlinesFlag) => {
  let result = ''

  for (let i = 0; i < text.length; i++) {
    const char = text[i]

    if (char === '\n') {
      result += encodeNewlinesFlag ? '&#10;' : char
      continue
    }
    if (char === '\r') {
      result += encodeNewlinesFlag ? '&#13;' : char
      continue
    }

    result += shouldEncode(char, encodeAllFlag)
      ? encodeChar(char, mode)
      : char
  }

  return result
}

这里把“换行实体化”作为独立分支,避免和普通字符逻辑混在一起。

6)解码流程:利用浏览器原生能力

解码实现没有手写完整解析器,而是直接借助浏览器对实体的原生解析。

const htmlDecode = (text) => {
  const textarea = document.createElement('textarea')
  textarea.innerHTML = text
  return textarea.value
}

这样可以同时处理命名实体、十进制实体和十六进制实体,代码量很小,行为也一致。

7)动作函数:编码、解码、交换、清空、复制

工具的交互动作都由独立函数管理。

编码与解码入口:

const handleEncode = () => {
  if (!inputText.value.trim()) return
  outputText.value = htmlEncode(
    inputText.value,
    encodingMode.value,
    encodeAll.value,
    encodeNewlines.value
  )
}

const handleDecode = () => {
  if (!inputText.value.trim()) return
  outputText.value = htmlDecode(inputText.value)
}

交换输入输出:

const swapInputOutput = () => {
  const temp = inputText.value
  inputText.value = outputText.value
  outputText.value = temp
}

复制结果(现代 API + 兼容兜底):

const copyResult = async () => {
  if (!outputText.value.trim()) return

  try {
    await navigator.clipboard.writeText(outputText.value)
  } catch {
    const textarea = document.createElement('textarea')
    textarea.value = outputText.value
    document.body.appendChild(textarea)
    textarea.select()
    document.execCommand('copy')
    document.body.removeChild(textarea)
  }
}

这部分让工具在真实使用中具备完整闭环:输入 -> 转换 -> 复制/反向处理。

8)实现小结

本工具的核心 JS 可以归纳为四层:

  1. 状态层:输入输出与编码选项
  2. 规则层:实体映射与编码判定
  3. 转换层:htmlEncodehtmlDecode
  4. 动作层:编码、解码、交换、清空、复制

整体实现没有依赖第三方编码库,完全基于浏览器原生能力和 Vue 响应式状态完成,逻辑清晰,维护成本也低。

大家好啊,我是甲木。

今天是 2026 年,年后复工第一天,先祝大家复工愉快(bushi

春节期间,好几个朋友问我同一个问题:「你不是搞 AI 的吗,我也想学学,从哪开始啊?」做老师的、做销售的、刚毕业的学生、创业十几年的老板,当然也包括后台经常私信我的粉丝朋友们。

背景完全不同,困惑高度一致,想用 AI 却不知从何入手。

一搜「AI 学习路线」,弹出来的东西直接劝退:线性代数、概率论与数理统计、机器学习、深度学习balabla一堆…直接从入门到放弃..

所以趁着复工第一天,我把这两年自己摸索的、观察到的、跟各路高手聊出来的经验,做一次彻底的梳理。给大家一份AI学习路线图

先说清楚:这篇文章不是写给 AI 工程师的。 如果你是计算机科班出身,想去大厂做算法岗,传统路线图对你有用,按部就班学就好。但如果你是运营、老师、创业者、学生、自由职业者,不打算靠「造 AI」吃饭,但想把 AI「用好」,那这篇写给你。

简单说说这篇文章的构成:

全文分四个模块,按需跳读:

  • Part1:方法论 摆正心态,掌握五个不会过时的学习心法
  • Part2:入门篇 选工具、找场景、学会跟 AI 交互,从零上手
  • Part3:进阶篇 底层逻辑、提示词进阶、Agent、Workflow、VibeCoding、信息源推荐
  • Part4:洞察篇 一手信息源、资本风向、行业趋势、职业发展
公众号回复“学习清单”,获取AI学习资源包~

如果你已经在用 AI,日常有了自己的工具和习惯,可以直接跳到第三章「进阶篇」和第四章「洞察篇」,那里有更系统的方法和前沿资源信息。

如果你还没开始用 AI,或者只是偶尔试试,建议从头读起。第一章帮你建立正确心态,第二章手把手带你上手。

好,我们开始。

Part1:摆正心态、掌握心法

你不会被 AI 抛下

我知道很多小伙伴内心有一种隐隐的焦虑,FOMO心态。

朋友圈天天刷到「AI 取代 XX 职业」的标题,公司同事已经在用各种 AI 工具提效了,自己还停留在跟春晚用豆包、千问、元宝帮忙抢几个红包..这种感觉就像一列火车正在加速驶离站台,而你还在找站台入口。

但我想说,这种焦虑大可不必。

一项真正具有革命意义的技术,它的特点恰恰是,不会轻易甩下任何人。你想想电的普及。19 世纪末电灯泡刚发明的时候,普通人也不懂交流电直流电的区别,也不知道发电机的工作原理。但这妨碍你现在用电吗?你每天开灯、充手机、用电脑,从来不会停下来想「我得先搞懂电磁感应定律」。

AI 也一样。它正在变成像水和电一样的基础设施。未来你打开的每一个 App、用的每一个软件,底层都有 AI 在运转。你一直在用,但不会有明显的感知,就像你用电的时候不会去想电流是怎么从发电厂到你家插座的。

所以,随时上车都不晚。今天是 2026 年 2 月,现在开始完全来得及。说真的,AI 应用这件事一直在路上。

那问题来了:既然不需要焦虑,我应该把自己摆在什么位置?

我觉得有一个简单的三层模型可以参考:

  • 底层——研发者:训练大模型的人,研究 Transformer 架构的人,OpenAI、DeepSeek 这些公司的核心算法团队。全球也就那么几万号人。
  • 中间层——开发者:用大模型的 API 去搭建应用、做 Agent、写 Workflow 的人。程序员、产品经理、技术创业者居多。
  • 应用层——使用者:用 AI 产品来解决自己工作和生活问题的人。绝大多数人在这一层。

绝大多数人的定位就是「应用者」,这就够了。

你不需要会训练模型,不需要会写代码,你只需要学会「跟 AI 协作」「让 AI 帮你做事」。就像你不需要会修车才能开车,不需要会做菜才能点外卖。

万维钢老师说过一句话,我印象特别深:

「我们永远不会在 AI 面前失去自我,我们不但应该,而且必须,而且可以,以'我'为主,人要比 AI 凶。」

什么意思呢?就是你别怕它、别仰视它。AI 很强,但做决策的永远是我们,是人。你得主动去驾驭它,而不是被它牵着走。

把心态摆正了,接下来聊方法。

五个学习心法:比任何工具教程都重要

工具会变,产品会迭代,今天的热门 App 明天可能就凉了,在 AI 时代尤其为甚。但方法论不会过时。这五个心法是我之前就在不同场合跟大家反复去讲,验证过的,你可以带着它们去学任何 AI 相关的东西。

心法一:动手为先

这条是我放在第一位的,因为它最重要,也最容易被忽略。

太多人的学习路径是这样的:先买本书→再报个课→看了三天视频→觉得「差不多了解了」→然后就没有然后了。

别这样。

你今天就可以打开 ChatGPT、豆包、Kimi、Claude,随便哪个都行,开始跟它聊。问它一个你工作中真实遇到的问题。让它帮你写一封邮件。让它帮你整理一份会议纪要。让它帮你翻译一篇英文报告。

就这么简单。别把它神秘化。

站在岸边永远学不会游泳。你得先跳下水,呛两口水,才知道手脚该怎么划。

刀哥之前给过一个特别实在的建议:他们公司实习生/应届生进来之后,拉一个群,推荐一款AI产品,用上一个月,之后再换其它,慢慢形成习惯。

ps. 其实你用着用着,很多原理性的东西反而自然就懂了。比「先学原理再动手」高效十倍。

心法二:场景驱动

动手是第一步,但别漫无目的地用。你得从自己的「真实需求」出发。

我见过太多人学 AI 的方式是:今天看到别人说 Midjourney 好,就去画两张图;明天看到 Suno 火了,就去生成一首歌;后天又跑去试 AI 编程。每样都浅尝辄止,最后什么都没留下。

正确的打开方式:想想你日常工作或生活中,有哪些事情是重复的、耗时的、让你头疼的。那就是你的切入场景。

  • 你是学生?那你的场景可能是:论文文献整理、PPT 制作、学习笔记总结、英语写作润色。
  • 你是职场人?那你的场景可能是:会议纪要、周报月报、数据分析报告、客户邮件。
  • 你是内容创作者?那你的场景可能是:选题策划、文章大纲、配图生成、短视频脚本。

找到你的场景,然后针对性地去研究「AI 在这个场景下能怎么帮我」。这比泛泛地「学 AI」有效太多了。

心法三:溯源学习

这条是关于信息获取的。

现在讲 AI 的内容铺天盖地。公众号、小红书、抖音、B 站,到处都是「AI XX 教程」「十分钟学会 XX」。信息不是太少,是太多了。多到你根本分不清哪些靠谱、哪些是营销号在蹭流量。

我的原则是:尽量往信息的源头去找。

信息有一条明确的「食物链」:

官方文档 > 权威科技媒体的一手报道 > 大 V 的深度解析 > 营销号的转述

各大 AI 实验室的官方文档是源头,最准确、最全面,但不一定好读。权威科技媒体(国内的话机器之心、量子位等等,国外的话MIT Technology Review、The Verge、The Batch的原创报道等)基于一手素材做分析,会加入行业判断,信息失真很小。大 V 的深度解析是二次加工,质量取决于作者水平,好的大 V 能帮你消化复杂信息,差的可能带偏你。营销号的转述就不用说了,基本是三四手信息,能避则避。

举个例子。你想学豆包怎么用,最好的方式不是去小红书搜「豆包使用技巧」,而是先看豆包官方的使用指南和帮助文档更为系统化。

好友冷逸说过一段话,讲得特别到位:

「萃取、消化第一手知识,重视第二手知识,轻视甚至无视第三、第四手知识。」

信息经过每一次转手,都会失真一点。到了三手、四手,可能已经面目全非。养成溯源的习惯,你获取信息的质量会比大多数人高一个档次。

心法四:功利学习

这条可能听起来不那么「正确」,但我觉得特别实用。

学东西要功利一点。诺贝尔奖得主赫伯特·西蒙说过:「信息的丰富导致注意力的贫乏。」他本人就是 AI 领域的奠基人之一,同时也研究人类认知的局限性。他的结论是:人的认知资源是有限的,必须做取舍。

别因为焦虑就什么都想学。你的时间和精力是有限的,每学一样新东西都有成本。所以在决定「要不要学这个」之前,先问自己三个问题:

  • 它能帮我省时间吗?
  • 它能帮我赚钱(或者省钱)吗?
  • 它能帮我做到之前做不到的事吗?

三个问题里如果至少有一个答案是「能」,那就值得投入精力去学。如果三个答案都是「好像不太能」或者「不确定」,那先放一放也没关系。

功利一点,学你现在用得上的。用不上的,知道有这么个东西就行了,等需要的时候再深入不迟。

心法五:输出与沉淀

前面四条都在讲「输入」。怎么开始、往哪用、去哪学、学什么。但光输入不输出,你的学习就像往一个没有底的杯子里倒水。

费曼说过一句话,大意是:如果你不能用简单的语言把一件事讲清楚,说明你还没真正理解它。

这条在 AI 学习上特别适用。你可能刷了几十篇教程、试了七八个工具、收藏了一堆「AI 神器」合集。但如果有人问你「AI 到底怎么帮到你了?」,你说不出个一二三来。

怎么破?两个字:输出

试着把你学到的东西讲给别人听。跟同事分享一个你用 AI 提效的小技巧。在朋友圈发一条你用 AI 画的图。甚至只是在微信群里说一句「我今天发现 XX 这么用特别好」。

不用多正式。但说出来的那一刻,你会被迫把模糊的感觉整理成清晰的表达。哪里其实没想通,一讲就露馅了。这个过程本身就是最高效的学习,费曼学习法值得拥有。

而且它有一个额外的好处:你帮别人入了门,在朋友眼里你就成了「懂 AI 的那个人」。这种社交资产,是刷再多教程也换不来的。

输出之外,还有一件事同样重要:沉淀

用到一个好的提示词?存下来。摸索出一个高效的工作流?记录下来。发现某个工具在某个场景下特别好用?标注一下。(或者前两者都可以直接沉淀为Skills方便下次复用..

这些东西单独看很小。但三个月后你打开自己的「AI 工具箱」,里面有二三十条经过验证的提示词、五六个跑通的工作流、十几个场景下的最佳实践。这时候你和那些每次都从零开始的人,差距就出来了。

AI 领域变化快,工具可能一两个月就换一茬。但你积累下来的「怎么跟 AI 协作」的经验不会作废。提示词会变,但你对「怎么把需求说清楚」的理解只会越来越深。工具会换,但你搭过的工作流逻辑可以直接迁移到新工具上。

这就是沉淀的复利效应。今天存下来的每一条经验,都在为未来的你省时间。


五个心法说完了:动手为先,场景驱动,溯源学习,功利学习,输出与沉淀。

当然还有一个就是:持续学习,这点不必多说了..

你可能注意到了,我一个具体的 AI 工具都还没教你用。别急,方法论是地基,地基不稳,上面盖什么都白搭。

地基打好了,接下来就该真正上手了。

第二章:上手就用(入门篇)

道理聊完了,该动手了。

很多人学 AI 的第一步就卡在「选哪个工具」上,打开手机应用商店一搜,几十个 AI 应用齐刷刷排在那儿,每个都说自己最强。然后就开始纠结,纠结着纠结着,一周过去了,啥也没用上。

没必要。

我的建议特别简单:随便选一个,先用起来。 用错了也没关系,又不要你签三年合同。AI 工具之间切换的成本几乎为零,你在这个上面打的字,换一个照样能打。但是你纠结的那一周,是真的浪费了。

好,那我还是帮你捋一捋,省得你连「随便选」都不知道从哪选。

2.1 先挑一个趁手的

这部分内容大家可以参考我之前写过的【2026年开年推荐的AI工具】,里面有详细的说明,

在这里简单说说,现在市面上的 AI 工具,大致可以分成这么几类:

对话类:你的日常首选

这类工具就是你跟 AI 聊天的入口。问问题、写东西、翻译、分析、头脑风暴……基本上你能想到的文字类需求,它们都能接住。

豆包、千问、Kimi、DeepSeek、ChatGPT、Claude、Gemini都可以考虑,各有各的特点。

创作类:按需选,按兴趣选

如果你对视觉创作感兴趣,AI 绘画AI视频都可以玩玩。即梦 AI 和可灵 AI 都是国内的,用着方便,效果也不错。即梦的Seedance2.0已经火爆国内外了。

AI 音乐呢,Suno 是目前最火的,你哼个旋律、写几句歌词,它就能给你生成一首完整的歌,第一次用的时候真的会有点上头。

搜索类:用过就回不去了

秘塔 AI 搜索,Kimi 的搜索模式都很好用,会帮你多轮检索、交叉验证,搜出来的内容很扎实。

用过 AI 搜索之后你会发现,传统搜索引擎那种「输入关键词 → 翻十页链接 → 自己拼凑答案」的模式,属实有点原始了。

海外工具:有条件的话值得体验

ChatGPT、Claude、Gemini,御三家,目前全球范围内综合能力最强的。

所以到底选哪个?

我的建议:先选 1 个对话类 + 1 个你感兴趣的创作类。就两个,够了。

对话类里面,豆包或者通义千问,二选一,装上就行。如果你经常需要处理长文档或者做深度搜索,再加一个 Kimi。创作类的,看你兴趣。喜欢画画就下个即梦,喜欢音乐就试试 Suno。

用上一个月,用熟了,你自然就知道自己还需要什么了。

2.2 从你自己的场景切入

工具选好了,下一个问题来了:用它干嘛?

「用它干嘛」这个问题,答案其实不在 AI 那边,在你这边。你是谁?你每天在干嘛?你有什么事情觉得烦、觉得重复、觉得耗时间?

从那里开始就对了。

如果你是学生

你可以让 AI 当你的「私人 AI 导师」。不是夸张,你想想,一个 24 小时在线、不会不耐烦、你问多少遍都不嫌烦的老师,以前得花多少钱请家教才能有这待遇?

看论文、读文献觉得头大?把 PDF 丢给 Kimi,让它帮你总结核心论点、梳理论文结构、提取关键数据。Kimi 的长文处理能力在这个场景下特别好用,几十页的论文它能一口气吃进去。

期末要做 PPT 和报告?用 AIPPT、Gamma、Dokie,给它一个主题,它能直接帮你生成一份带设计感的 PPT。当然你得自己改改内容、调调逻辑,但至少不用从一张白页开始了。

在大学这个最美好的时期,最纯真的爱情当然不能缺席。你可以让 AI 帮你写三行情诗、分析聊天记录里对方的态度(别笑,真的有用)、甚至帮你策划一个有创意的表白方案。AI 不能替你谈恋爱,但可以帮你成为一个更有趣的人。

如果你是职场人

开完会写纪要是不是很烦?千问、听悟,你开会的时候打开录音,它会自动帮你转文字、提取要点、生成会议纪要。我身边好多朋友用上之后都说回不去了。

周报月报这种东西,真的不值得你花一个小时去憋。把这周做了什么要点告诉 AI,让它帮你润色成一篇结构清晰、措辞得体的周报,五分钟搞定。你省下来的五十五分钟,用来干点真正有价值的事情不好吗?

数据分析也是。你把 Excel 表格丢给 AI,让它帮你找规律、做可视化、写分析报告,这在以前可能得专门找个数据分析师干的活儿。

如果你是创作者或自媒体人

AI 辅助写作是基本操作了。注意我说的是「辅助」,不是「替代」。让 AI 帮你列提纲、扩写段落、换个说法、找案例素材,这些都是很好的用法。但核心观点和个人风格得是你自己的,不然读者为什么要关注你而不是直接去问 AI 呢?

文章配图是个刚需。以前写公众号,找配图要么用免费图库(千篇一律),要么冒着版权风险从网上扒(别这么干),现在用 AI 画一张独一无二的配图,又好看又没版权问题。

日常生活

辅导孩子作业,你未必记得住初中数学公式,但 AI 记得。让它一步一步讲解解题思路,比你吼孩子有效多了。关键是它不会生气。

旅行规划,把你的时间、预算、偏好告诉 AI,让它帮你做攻略。比你在小红书上刷几十篇游记再自己拼凑行程效率高太多了。


说了这么多场景,我知道你可能看完有点懵。没关系,一张速查表帮你对号入座:

这个表不是标准答案,是起跑线。用着用着就会自然迭代出属于自己的工具组合。

2.3 学会跟 AI 沟通

工具有了,场景也有了,但你可能很快会遇到一个问题:怎么 AI 给我的回答这么「平庸」?

大概率不是 AI 的问题,是你「问的方式」的问题。

你跟 AI 说话的那段文字,有个专门的名字,叫「提示词」(Prompt)。说白了就是你输入给 AI 的指令。你给的指令越清晰、越具体,AI 回你的内容就越靠谱。

这跟你跟人沟通是一样的道理。你跟同事说「帮我整理一下那个东西」,对方大概率一脸问号。但你说「帮我把上周五会议的三个待办项整理成表格,列出负责人和截止日期」,对方立刻就能动手了。

AI 也是一样。就这么简单。

几个主要元素:背景、目标、要求、示例,归根结底就是要把上下文说清楚!

当然,关于 Prompt 是一门大的学问,大家可以关注下述内容再学习,这里就不展开了。

想更系统地学提示词?推荐几个资源:

  • LangGPT 结构化提示词(https://langgpt.ai/): 很实用的提示词写作框架
  • 提示词工程指南(https://www.promptingguide.ai/zh):中文提示词教程

多跟 AI 对话,用得多了,你自然就知道怎么「说话」它才听得懂。

当然这里需要注意,不要指望一次就得到完美答案,多追问,迭代几轮,效果会更好。

2.4 试试让 AI 帮你跑完全程

到这里,你已经会跟 AI 聊天了。提个需求,它给你回复,一问一答,很好。

但 AI 能做的不只是「你问我答」。

有些任务不是一个问题就能解决的?比如「帮我调研一下竞品,整理一份分析报告」。这个任务里包含搜索、筛选、对比、整理、排版好几个步骤。如果你一步步跟 AI 聊,也能做,但得来来回回好多轮。

有没有可能,你只说一句「帮我做竞品分析」,它就自己去搜、去查、去比较、去整理,最后给你一份完整的报告?

可以的,智能体(Agent)

最通俗的理解:普通 AI 对话是「你说一句它做一步」,智能体是「你交代一个任务,它自己规划步骤,帮你跑完全程」。就像你交代一个靠谱的助理「帮我订周五从北京到上海的差旅」,你不需要告诉他先查航班、再比价格、再订酒店、再填报销单,他自己就把这些全办了。

去哪儿能体验到?

最简单的方式:去逛逛智能体商店。扣子(Coze,字节做的 coze.cn)上面有大量别人做好的智能体,技能商店,各种功能都有。豆包 App 里也内置了不少智能体,打开就能聊。智谱的 GLMs 商店也类似。

一些很有意思的 AI Agent 产品也值得关注:

  • Manus Agentmanus.im): 全球首款通用型 AI Agent,你给它一个复杂任务,它会自己拆解成多个步骤去执行
  • Lovarthttps://www.lovart.ai/):AI 设计平台,自动完成从概念到成品的设计流程
  • OpenAI Operatoroperator.chatgpt.com):ChatGPT 内置的 Agent 模式,能帮你在网页上完成操作
  • OpenClaw :开源 AI Agent,运行在你自己的设备上,通过 WhatsApp、Telegram、Slack、Signal 等聊天工具进行交互,能执行 shell 命令、浏览器自动化、邮件、日历和文件操作。
  • Zapier Agents:支持用自然语言描述来创建 Agent,并可连接 8000+ 应用,非常适合跨工具自动化。
  • Quizletquizlet.com):AI 融入学习过程,根据你的掌握程度自动调整复习策略

代码领域的 Agent 已经相当成熟了。Claude Code、Cursor、Trae 这些工具,让你用自然语言就能写代码。这个话题展开比较深,放到后面进阶篇再聊。

你现在不需要每个都去试。知道有这么个东西就行,等你用熟了基础的 AI 对话之后,再慢慢探索 Agent 的世界。

2.5 给自己找个学习大本营

AI 这个领域变化太快了。真的是今天的新闻明天就过期的那种快。你需要一个「根据地」,能持续获取新信息、跟上节奏。

第一个要收藏的:通往AGI之路 - 飞书云文档(https://waytoagi.feishu.cn/)

WaytoAGI 知识库是目前互联网上我见过的最全面的 AI 开源知识库。从基础概念到进阶教程,从工具推荐到行业动态,分类清晰,持续更新。你如果只收藏一个网站,就收藏这个。

上面有各路 AI 英雄豪杰的最新的洞察,以及各种实操教程。

大家需要善于使用飞书的搜索功能,在上面搜索自己想要关注的内容,从而找到相关的文章。同时WaytoAGI还有免费的学习社群和免费的线下活动,非常推荐大家参与!

值得关注的博主和公众号

入门阶段,信息源不用多,但要靠谱。推荐几个我觉得质量一直在线的:量子位、机器之心、赛博禅心、歸藏的AI工具箱、数字生命卡兹克、Agent橘。这几个先关注上,日常刷刷就能保持基本的信息敏感度。更完整的推荐列表,我放在后面了。

LangGPT 知识库:feishu.langgpt.ai

里面有大量提示词模板和案例。当你想把提示词技巧再往前推一步的时候,去那里逛逛会很有收获。


好了,到这里「入门篇」差不多了。你现在已经有了工具、有了场景、有了基础的提示词技巧、知道了智能体是什么、还有了持续学习的信息源。

说实话,如果你只是想让 AI 在日常生活和工作中帮上忙,大部分人把这些用熟,效率提升就已经很明显了。

但我猜,你用了一段时间之后,一定会开始好奇更多的东西。为什么有时候 AI 说得特别好,有时候又会一本正经地胡说八道?我能不能自己搭一个智能体?能不能用 AI 写代码、做产品?

这些好奇心特别好。带着它们,我们接着往下聊。

第三章:从「会用」到「用得好」(进阶篇)

会用 AI 和用好 AI,中间隔着什么?

我觉得隔着一层「理解」。你不需要成为 AI 专家,但当你大概知道它怎么工作的、知道怎么更好地跟它协作、知道怎么把零散的用法串成体系,你会发现,同样一个工具,到了你手里,就是比别人顺。

这一章,我想和你聊五件事:AI 的底层逻辑、提示词的进阶玩法、工作流搭建、Vibe Coding,以及怎么建立你自己的 AI 信息网络。

不用怕。都是大白话。

3.1 搞懂 AI 的底层概念

很多人对 AI 有一种隐隐的不安:我天天在用它,但我完全不知道它是怎么运作的。

这种感觉很正常。你不需要去读论文、学数学,但花十分钟了解三个核心概念,会让你对 AI 的能力边界有一个更清晰的判断。知其所以然,你才知道什么时候该信它,什么时候该怀疑它。

Transformer:大语言模型的「发动机」

所有你用过的 ChatGPT、Claude、Kimi、豆包,底层都跑在同一个架构上,叫 Transformer。

一句话解释:它让 AI 学会了「注意力」——在一大段文字里,自动判断哪些词和哪些词之间关系更紧密。就像你读一篇长文章时,大脑会自动在关键信息之间建立连接,Transformer 做的事差不多,只不过它用的是数学。

想深入了解:李沐在 B 站讲的《Attention Is All You Need》论文精读https://www.bilibili.com/video/BV1pu411o7BE。原论文在这里https://arxiv.org/pdf/1706.03762.pdf

Context Engineering:大模型时代的编程
AI 系统效果,很大程度取决于上下文设计。角色、任务规则、示例、知识与工具说明共同构成模型的工作环境。Agent、RAG、本地知识库,本质都是上下文工程。

生成模型三路线:自回归、扩散、潜空间
自回归:逐步预测下一个元素,语言模型与部分生图模型采用,结构稳定、可控性强。
扩散:从噪声逐步去噪生成图像,擅长写实与细节,Stable Diffusion、Midjourney 属此类。
潜空间(VAE/GAN):在低维表示中采样生成,是早期图像与语音生成基础。

Agent:从回答到执行
Agent 让 AI 能拆解目标、调用工具并完成任务。它依赖大模型推理与上下文工程,是系统形态升级。

理解这些,你就知道 AI 在做什么,也知道它何时可靠。

想系统学?这几门公开课够了

中文方面,李宏毅老师的课是我最推荐的https://speech.ee.ntu.edu.tw/~hylee/GenAI-ML/2025-fall.php,当然他在 B 站有一些搬运的视频,讲底层原理讲得最透彻,而且幽默。李沐的「跟李沐学 AI」系列也非常好,他还有一套 D2L 动手学深度学习https://zh.d2l.ai,2025 版有 171 集。

英文方面,吴恩达的 [Deeplearning.ai] https://www.deeplearning.ai/courses 是经典中的经典。

但我要强调一点:这些不是必修课。你不学这些,照样能把 AI 用得很好。它们的定位是「知其所以然」,感兴趣就深入,不感兴趣,跳过这一节完全没问题。

3.2 从提示词到 Skills:让 AI 越用越顺手

入门篇里讲了提示词的基础用法:说清楚你要什么,给点背景信息,指定输出格式。

但用久了你会发现,光靠「说清楚」还不够。你开始想:有没有更系统的方法?有没有办法让我的提示词变成一种可以反复使用的东西?

有的。

提示词进阶:从「随手写」到「有结构」

如果你只学一个进阶技巧,我推荐「结构化提示词」。最典型的是 LangGPT 方法,它把一条提示词拆成几个模块:角色、背景、任务、要求、输出格式。

再往上走,有几个高级技巧值得了解:

多轮迭代优化:不要指望一次就得到完美答案。很多高手的秘诀不是提示词写得多精妙,而是他们特别会追问。

Meta-Prompting:让 AI 帮你写提示词。套娃,但有效。

一条进化线索:Prompt → Context → Skills

大家其实会明显感觉到,圈子里的关键词换得越来越快。前几年大家讲 Prompt。后来大家讲 Context Engineering。再后来,Skills 变成了高频词。

这不是换个名字炒概念。背后有一条清晰的进化线索:

Prompt 时代,你给 AI 的是一次性的显式指令。每次对话都从零开始。

Context 时代,你开始「经营」一个信息场。不只是告诉 AI「做什么」,还把相关的背景、约束、知识一起喂给它。

Skills 时代,你把自己的经验和流程封装成一个「能力包」,让 AI 在需要的时候自动调用,稳定复用。

私以为不管是 Context 或是 Skills,都是 Prompt 的一部分,对于模型来说都是 Token。但从用户的角度看,这三者的区别在于:你和 AI 协作的颗粒度变了。从一句话,到一个场景,到一个可复用的能力单元。

Skills 到底是什么?

说得通俗一点:Skills 就是你把自己做某件事的经验和步骤,用自然语言写成一个「扩展包」,AI 在处理任务时会按需加载它。

门槛低,会写 Prompt 就会写 Skills。它比单条 Prompt 强在哪里?可复用、可分享、可迭代。

这个概念已经在国内落地了。字节的 Trae 有「技能」模块,扣子 2.0 也在往这个方向走。而且它是纯自然语言形式的,比搭积木式的 Workflow 更符合大多数人的习惯。

就这么简单。别把它神秘化。可以看之前发过的一篇文章【插入Skills文章链接】

推荐资源:

  • [LangGPT 开源项目] https://github.com/langgptai/LangGPT:结构化提示词框架
  • [Prompt Engineering Guide] https://www.promptingguide.ai/zh:最全的提示词工程中文指南
  • [Anthropic 官方提示词指南] https://www.anthropic.com/learn:写得很实在
  • [GitHub - Awesome-claude-skills] https://github.com/travisvn/awesome-claude-skills:一些很不错的ClaudeSkills

3.3 Agent & Workflow

提示词和 Skills 解决的是「单次对话怎么更好」的问题。但很快你会遇到另一个需求:我能不能把好几个步骤串起来,让 AI 自动跑完?

能。这就是 Workflow。

想象一条工厂流水线:原材料进去,经过切割、打磨、组装、质检,成品出来。Workflow 就是你给 AI 搭的一条流水线。你设定好每一步该做什么、数据怎么流转,触发之后它自动从头跑到尾。

比如:每天早上自动抓取行业新闻 → AI 总结成摘要 → 推送到你的飞书。整个过程不需要你动手,每天起床就能看到。

从哪里开始?

[扣子(Coze)] https://www.coze.cn是国内首选,拖拖拽拽就能搭。[n8n] https://n8n.io 是开源的,灵活度非常高,免费这一点也很香。Dify 更适合想做 AI 应用的人。

我的建议是:先从一个简单的场景入手。先做一个「每天自动给我推送行业新闻摘要」的小 Workflow,跑通了,你就有感觉了。

在企业的 AI 应用场景中,我们最看重的是其稳定性和确定性。

通过 Workflow 的形式,能够更好地确保 AI 运行的每个节点都不超出预期,让它完全按照我们的想法执行。

3.4 Vibe Coding:每个人都可以是创造者

接下来要聊的这个话题,我个人非常兴奋。

Collins 英语词典把「Vibe Coding」评为了 2025 年度词汇。不是什么 AI 术语、不是什么技术名词。一个关于写代码的概念,成了年度词汇。这本身就说明了一些事情。

什么是 Vibe Coding?

你不写代码。你用中文告诉 AI:「我想做一个番茄钟应用,要能设定工作时长和休息时长,界面简洁,用暖色调。」然后 AI 帮你把整个应用写出来。

就这样。你描述需求,它生成代码,你看效果,给反馈,它修改。来回几轮,一个能用的产品就出来了。

这件事最颠覆的地方在于:它模糊了「用户」和「开发者」的边界。

以前你想做一个小工具,你得学编程。学变量、学循环、学函数,怎么也得几周到几个月。现在呢?用自然语言描述你想要什么,几小时甚至几分钟,一个能跑的应用就出来了。

聊聊 Claude Code

在 Vibe Coding 工具里,我想重点聊聊 Claude Code。

Claude Code 是 Anthropic 推出的命令行 AI 编程工具。它不是一个编辑器插件,本质上是一个 Agent。你把它放到项目里,它能理解整个代码库的结构,自主规划该怎么做,然后一步步执行。

有一点技术门槛,你得会打开终端、敲命令行。如果你连终端是什么都不知道,可以先从 Cursor 或 Trae 这种有图形界面的工具开始。

但如果你有一点点基础,Claude Code 的体验是非常惊艳的。你可以跟它说「帮我把这个项目的登录功能改成支持手机号登录」,它会自己去读代码、理解架构、做修改、跑测试。

想入门的话:

  • [官方中文文档] https://code.claude.com/docs/zh-CN/overview
  • [awesome-claude-code] https://github.com/hesreallyhim/awesome-claude-code(21.6k Stars)
  • B 站 [30 个进阶技巧] https://www.bilibili.com/video/BV1XGbazvEuh/(12 万播放)
  • [刘小排 Claude Code 实战分享] https://mp.weixin.qq.com/s/sOPO_MWq9xsiHzYapszK5Q:全球 Claude Code Token 消耗量「榜一大哥」的实操经验

Vibe Coding 工具全景

工具公司特点费用
CursorAnysphere最流行的 AI 编程编辑器$20/月
Trae字节跳动部分免费,全中文,600 万+用户免费 & 收费
CodeBuddy腾讯完全免费,插件+IDE+CLI免费 & 收费
WindsurfCognition自有模型速度是 Sonnet 的 13 倍有免费额度
GPT CodexOpenAI可独立工作超 7 小时$20/月起

如果你在国内,入门推荐 Trae 和 CodeBuddy,免费额度、中文好、不用折腾网络。

普通人用 Vibe Coding 做了什么?

这部分是我最想写的,因为每一个案例都在打破「编程是程序员的事」这个旧观念。

CNBC 有个记者,完全没有技术背景,参加了一个 2 天的 Vibe Coding 训练营,出来的时候手里拿着一个完整的 App。两天。一个记者。

菲律宾有个叫 Pablo 的白领,直到成年才开始用电脑。他花了 2 小时,做出了一个费用管理 App

一个做营销的女生 Maddy Osman,只有最基础的编码知识,用 Vibe Coding 独立做出了好几个产品

创造的门槛正在被拉平。你脑子里那些「要是有这么一个工具就好了」的念头,现在真的有可能自己动手实现。

目前全球约 41% 的代码由 AI 生成。Lovable(一个 Vibe Coding 平台)上线 8 个月就达到了 1 亿美元的年化收入。

这,是一场正在发生的变革。

3.5 你的第一个「数字员工」:OpenClaw

前面聊了 Vibe Coding,你可以用自然语言造产品。接下来说说:用自然语言雇一个 24 小时在线的数字员工。

2026 年初,AI 圈有一个项目彻底出圈了,叫 OpenClaw(openclaw.bot)。GitHub Stars 飙到了 22 万+,各家云厂商争相接入,可以说是今年 Agent 领域最值得关注的现象级项目。

OpenClaw 是什么?

一句话说:它是一个开源的 AI Agent,运行在你自己的电脑或云服务器上,能通过 Discord、Telegram、飞书、WhatsApp 等聊天工具跟你交互。你在手机上发一条消息,它就在服务器上帮你干活。

跟前面 2.4 提到的那些 Agent 产品不同,OpenClaw 更像是一个你「自己养」的 AI 助手。它跑在你自己的环境里,能读文件、执行命令、浏览网页、操作各种工具,而且 7×24 小时在线。

如果说前面聊的那些 Agent 产品是"别人家的员工帮你跑腿",OpenClaw 更像是"你自己雇了一个实习生,住在你办公室里,随叫随到"。

它的灵魂在于 Skills

OpenClaw 最核心的设计是 Skills 生态。你给它装什么技能,它就能干什么活。这跟我们前面 3.2 聊的 Skills 概念一脉相承,只不过在 OpenClaw 里,Skills 变成了 Agent 的「能力包」,装上就能用。

比如你给它装上搜索 Skill,它就能帮你每天定时抓取行业新闻,整理成摘要推送给你。装上代码执行 Skill,它就能帮你写网页、跑脚本。装上金融数据 Skill,它甚至能帮你监控行情,有重大波动立刻通知你。

而且 Skills 是纯自然语言写的,会写提示词就会写 Skills。社区里已经有大量现成的 Skill 库可以直接用:

  • [ClawHub Skills] https://www.clawhub.ai/skills:官方精选 Skill 市场
  • [awesome-openclaw-skills] https://github.com/VoltAgent/awesome-openclaw-skills:社区整理的精选合集

能用它做什么?

举几个我自己跑通的场景:

  • 自动新闻监控:设好关键词和信息源,它每隔一段时间自动帮你抓取、筛选、总结,推送到你的聊天工具里。起床就能看到。
  • 自然语言建站:手机上发一句「帮我做个番茄闹钟的网页,要有不错的交互」,它直接在服务器上写代码、部署上线,几分钟后甩给你一个链接。
  • 文件处理助手:丢给它一个 PDF 或表格,让它分析、提取、转换格式,全程不用开电脑。
  • 定时任务执行:每天早上八点自动执行某个流程,周报自动生成,数据自动备份……设一次,跑很久。

为什么说它代表了一个重要趋势?

OpenClaw 最有意思的一个设计叫 Gateway——它在 Agent 和各种通讯工具之间搭了一座桥。这意味着你不需要打开电脑、不需要登录某个网站,直接在微信、飞书、Discord 这些你每天都在用的工具里,就能跟 Agent 对话,让它帮你干活。

这件事的意义在于:Agent 第一次真正融入了你的日常通讯流。它不再是一个你需要专门去访问的工具,而是像一个同事一样,住在你的群聊里,随时待命。

更深一层,OpenClaw 的 Skills 还能自举——Agent 可以在执行任务的过程中,自己发现可复用的经验,自己封装成新的 Skill,自己装上。也就是说,它会越用越好用,这跟我们前面心法五讲的「沉淀复利」是同一个道理,只不过这次沉淀经验的不是你,是 Agent 自己。

怎么开始?

【插入之前OpenClaw Kimi的文章】

  • [OpenClaw 官网] https://openclaw.ai/
  • [GitHub 仓库]https://github.com/openclaw/openclaw

一句话总结:OpenClaw 把"AI 会聊天"这件事,推进到了"AI 能干活"。 如果说 2025 年的关键词是 Vibe Coding,那 2026 年你一定会反复听到的词,就是 Agent。
而 OpenClaw,是目前这个方向上最值得体验的项目之一。

3.6 建好你的「雷达系统」:AI 信息网络

AI 领域变化太快了。你不可能靠一次性学习就一劳永逸。

你需要的不是「学完」,而是建立一个持续获取信息的网络。

下面这些是我个人关注多年、反复筛选后留下来的信源,很多都是线下的好朋友。

因为我看公众号比较多,所以大多数都是以微信生态为主的..小红书、B站了解的朋友可以评论区见!

中文博主推荐(几大分类)

① AI General

量子位、机器之心、数字生命卡兹克、赛博禅心、Agent 橘子、特工宇宙、卡尔的AI沃茨、沃垠 AI、夕小瑶科技说、张咋啦。这几个覆盖了 AI 领域大部分重要动态,选 1-2 个固定关注就够了。

② AI Prompt & Agent

宝玉AI、李继刚、云中江树、向阳乔木、一泽、云舒的AI实践日记、甲木等。还有一个必须提的是「归藏的 AI 工具箱」,信息密度极高。

③ AI Design

汗青、海辛和阿文、阿真Irene、TATALAB等,对非设计师也很有启发。

④ AI Tech

苍何、袋鼠帝、刘聪 NLP、花叔、饼干哥哥。技术含量高,但不至于看不懂。

⑤ AI Product

AI 产品阿颖、洛小山、AI 产品银海、AI产品黄叔等。

⑥ AI Tutorial

Rico 有三猫、栗噔噔、摸鱼小李、AIGC新知等,风格轻松,跟着做就能出活。

💡 关于 Claude Code 和 Vibe Coding,特别推荐看刘小排的分享

海外信息源

X(Twitter)是最快的,OpenAI、Anthropic 的重要发布基本都先在 X 上出来。YouTube 上 Lex Fridman 的深度访谈质量很高https://www.youtube.com/@lexfridman。The Information 和 TechCrunch 是产业分析的标配。

X的推荐博主,在第四章给大家列出来

「可是我英文不好怎么办?」装一个「沉浸式翻译」浏览器插件,英文网页秒变双语对照,语言不再是信息壁垒。

播客推荐

英文:Lex Fridman、20VC。中文:Founder Park、十字路口 Crossing。

最后一点:不要试图关注所有信源。

信息过载和信息匮乏一样有害。固定 1-2 个每天看的,1-2 个每周看的,其余放收藏夹,有需要的时候再翻。用 AI 帮你做信息筛选和摘要,不要跟信息流拼体力。


到这里,你已经有了一张比较完整的能力地图。但你可能已经开始想一个更深的问题了:AI 这么猛,它到底会走向哪里?钱在往哪流?我该怎么提前站位?

这些问题,我们下一章聊。

第四章:看得远——源头信息与前沿洞察(洞察篇)

前面三章,我们聊的是怎么把 AI 用起来、用好、用出花来。

但你有没有想过一个问题:你用的这些工具,半年后还在不在?你学的这些技巧,一年后还管不管用?

AI 这个领域变化太快了。你今天觉得很厉害的功能,三个月后可能变成免费标配。你今天花大力气学的某个平台,半年后可能已经被收购或者关停了。

所以光会「用」还不够。你得学会「看」。

看懂 AI 正在往哪走,你才能提前站到对的位置上。

4.1 别喝二手水:去源头获取一手信息

我有一个习惯,看到任何 AI 相关的新闻,我都会去找原始出处。

不是因为我不信媒体,而是这个领域的信息衰减太严重了。一篇论文发出来,经过英文科技媒体报道、国内媒体翻译、自媒体二次加工、短视频三次加工……到你手里的时候,可能已经面目全非。

跟对人:AI 领域值得关注的头脑

第一类是技术大佬,他们的观点值得深度消化。

图灵奖三巨头:Yoshua Bengio、Geoffrey Hinton、Yann LeCun。有意思的是,Hinton 现在是「AI 末日派」,LeCun 是坚定的「AI 乐观派」,Bengio 在中间偏谨慎。三个人经常互相怼。看他们争论,比看任何分析文章都有营养。

李飞飞和吴恩达。李飞飞对 AI 与人文的交叉思考非常有深度。吴恩达是 AI 教育界的传奇,而且他一直在一线做项目,不是那种脱离实践的学者。

Ilya Sutskever,OpenAI 的联合创始人。这个人很少公开发言,但每次说话都值得反复品味。

国内这边,梁文锋,DeepSeek 背后的人。他很少接受采访,但 DeepSeek 的技术路线本身就是他最好的表达。

第二类是商业领袖,他们的观点要带「滤镜」看。

Sam Altman、马斯克、黄仁勋、扎克伯格、李彦宏……这些名字你肯定不陌生。

但冷逸有个观点我很认同:「他们的采访都是有商业目的的,要么提高公司估值,要么寻找投资人,以及迷惑竞争对手。」

所以听他们说什么,更要看他们做什么。把钱投到哪里,比嘴上说什么诚实得多。

获取渠道推荐:

  • X(原 Twitter):AI 领域最重要的信息广场,没有之一
  • Lex Fridman 播客:长对话,动不动三四个小时,但深度是真的深度
  • 20VC:偏投资和商业视角

建议关注的X账号列表

官方科研博客

海外:Anthropic Research(写得最好https://www.anthropic.com/research)、OpenAI Newshttps://openai.com/news、Google DeepMindhttps://deepmind.google/research/publications、Meta AI Bloghttps://ai.meta.com/blog

国内:Qwen 博客(通义千问团队https://qwenlm.github.io/blog/)、Seed(字节跳动https://seed.bytedance.com)、腾讯混元、Kimi/智谱/DeepSeek 的公众号。

养成习惯,每周花 20 分钟扫一遍这些源。不需要每篇都精读,扫标题就行。

还有 Reddit,国外版小红书。很多越狱提示词、时髦玩法都来自这里
https://www.reddit.com/

论文没你想的那么可怕

你不需要看懂每一个公式。你只需要知道:这篇论文在解决什么问题?效果怎么样?对我有什么影响?

而且现在有 AI 帮你。Kimi 学术搜索可以帮你快速理解一篇论文的核心内容。BabelDOC 可以把整篇论文翻译成排版精美的中文版。

如果你只读三篇论文:

  • 《Attention Is All You Need》:Transformer 的开山之作,改变了整个 AI 的发展轨迹
  • 斯坦福小镇(Generative Agents):让 25 个 AI 角色在虚拟小镇里自主生活
  • ReAct:推理和行动结合,当前 AI Agent 框架的理论基础

4.2 想深入?这些课和书值得花时间

好消息是,全世界最顶级的 AI 课程,绝大多数都是免费的。不是打折,是真的免费。

课程列表

课程来自一句话描述费用
Stanford CS336斯坦福带你从零构建一个语言模型免费材料
Berkeley CS294UC Berkeley高阶 LLM Agent 课程免费材料
DeepLearning.AI 短课DeepLearning.AI每门 1-2 小时的主题短课免费
Hugging Face 全家桶Hugging Face6 门课覆盖 NLP 到多模态免费
LangChain AcademyLangChain专注 Agent 开发基础免费
fast.aifast.ai代码优先的实践派免费

如果你完全没有编程基础,从 DeepLearning.AI 的短课开始。如果你有一点编程基础,fast.ai 非常适合。如果你是在校学生想往 AI 方向走,Stanford CS336 和 Berkeley CS294 是天花板级别。

书单:这几本值得放在手边

  • 《深度学习》 Ian Goodfellow 著。圈内人叫它「AI 圣经」。不需要从头读到尾,当工具书翻就行。
  • 科普类:《这就是ChatGPT》《深度学习革命》《AI未来进行式》都挺不错
  • 哲思应用类:《拐点》《人比AI凶》——万维钢《关于说话的一切》——写Prompt有帮助
  • 轻度技术类:《GPT图解》《智能体设计指南》《动手做AI Agent》
  • 技术进阶类:《RAG实战课》《这就是MCP》等
  • llya推荐的30个阅读清单
    https://arc.net/folder/D0472A20-9C20-4D3F-B145-D2865C0A9FEE

还有两本免费电子书特别推荐:黄叔的《AI 编程蓝皮书》2.0,以及姚金刚和向阳乔木的《GEO 白皮书》(21 万字)。

4.3 钱往哪里流,行业就往哪里走

这个小节,我想跟你聊点不一样的。不聊技术,聊钱。

因为资本的流向,是预判未来最诚实的信号。投资人可以在采访里说漂亮话,但他们不会拿真金白银开玩笑。

值得追踪的 AI 投资风向标

  • 红杉资本(美国红杉 Sequoia Capital):sequoiacap.com
  • a16z:a16z.com,每年发布的《State of AI》报告是行业必读
  • Y Combinatorycombinator.com,每期批次里 AI 项目的比例就是行业温度计
  • 奇绩创坛miracleplus.com,陆奇创办的,可以理解为「中国版 YC」

2026 年初,我们站在哪里?

先看钱的规模。2025 年,全球 AI 领域的私人融资总额达到了 2258 亿美元。科技四巨头 2026 年计划的资本支出合计达到 6350 到 6650 亿美元,其中大部分用于 AI 基础设施建设。六千多亿美元砸下去建数据中心和买 GPU,这种级别的投入,在人类商业史上也是罕见的。

再看企业采用。McKinsey 2025 年的报告显示,88% 的组织已经在使用 AI。但其中只有 6% 真正从 AI 中获得了显著商业价值。大多数公司还处于「用了,但没用好」的阶段。这恰恰是巨大的机会。谁能帮企业把 AI 从「用了」变成「用好」,谁就能吃到最大的红利。

Agent 方向全面爆发。2025 年 AI Agent 市场规模 78.4 亿美元,预计 2030 年达到 526 亿美元。YC 最近几期的批次里超过 80% 的项目跟 AI 相关,Agent 是最热的方向。Gartner 预测 2026 年 40% 的企业应用会嵌入 Agent。

垂直行业的渗透速度也让我很惊讶。医疗领域,医生使用 AI 的比例从 38% 飙升到 66%。法律行业,律所 AI 采用率从 19% 涨到 79%。金融领域,银行的生成式 AI 采用率从 8% 蹿到 78%。如果你在这些行业里,AI 能力已经不是加分项了,它是基本功。

国内外的差异也值得关注。目前美国占全球 AI 投资的 79%。海外侧重基础模型研发和开发者生态,国内侧重应用落地和场景创新。简单说:海外在造「发动机」,国内在造「车」。

几份报告值得收藏

  • 红杉资本《AI in 2026》 (https://sequoiacap.com/article/ai-in-2026-the-tale-of-two-ais/)
  • a16z《State of AI》 (https://a16z.com/state-of-ai/)
  • Stanford HAI《AI Index 2025》(https://hai.stanford.edu/assets/files/hai_ai_index_report_2025.pdf)(400+页)
  • McKinsey《The State of AI》(https://www.mckinsey.com/capabilities/quantumblack/our-insights/the-state-of-ai)
  • Anthropic《2026 Agentic Coding Trends Report》(https://resources.anthropic.com/2026-agentic-coding-trends-report)

对普通人意味着什么?

选什么技能?看资本在追什么。Agent 开发、AI 应用集成、提示工程,这些方向在未来两三年确定性很高。

进什么行业?看 AI 在哪些垂直领域渗透最快。医疗、法律、金融、教育,这些行业的 AI 改造才刚刚开始。

做什么产品?看 YC 和奇绩创坛每期在孵化什么。它们的项目列表就是一份「未来热门方向预告片」。

4.4 职业发展:用作品说话

聊完了大趋势,回到一个很现实的问题:这些东西怎么变成我的职业竞争力?

我的建议只有四个字:用作品说话。

在 AI 领域,没人在乎你的简历上写了什么。他们在乎的是:你做了什么?

在 Coze 上打造一个爆款智能体,用户量破万。在 GitHub 上开源一个 AI 项目,拿到几百个 Star。在 ProductHunt 上发布一个 AI 小工具,被社区推荐到首页。这些事情,每一件都比任何证书有说服力。

说到证书,说句实话:目前市面上还没有一张 AI 证书是被行业真正认可的。了解一下可以,但不要把主要精力放在考证上。

与其花时间考证,不如花时间在这几个平台上建立存在感:

  • X:关注行业动态,参与讨论,分享你的思考
  • GitHub:你的代码作品集
  • HuggingFace:AI 领域的「GitHub」,分享模型和数据集
  • ProductHunt:发布你的 AI 产品,获取真实用户反馈
  • 飞书:国内很多 AI 社群在飞书上,信息密度高

在一个快速变化的领域里,你的学习能力和实际产出,永远比一纸证书更有价值。

结语

写到这里,一万多字了。

如果你从头读到这儿,辛苦了。如果你是跳着读的,也完全没问题,这本来就是给不同阶段的人准备的。

你不需要从线性代数开始。你不需要学会写代码。你不需要读完所有论文。你只需要打开一个 AI 工具,开始跟它对话。

我一直觉得四个阶段:了解 AI,走进 AI,驾驭 AI,「超越 AI」。

超越不是说你比 AI 厉害。而是你知道怎么跟它协作,让 1+1 大于 2。你知道什么时候该依赖它,什么时候该相信自己。你能用它放大你的优势,而不是被它取代。

万维钢说过:「我们永远不会在 AI 面前失去自我,我们不但应该,而且必须,而且可以,以'我'为主。」

是的,以「我」为主。

AI 是工具,是伙伴,是放大器。但掌舵的那个人,始终是你自己。

人要比 AI 凶。

知易行难,从 0 到 1 最难。但开始了,路就清晰了。

如果你今天只做一件事,我的建议是:打开一个 AI 工具,跟它聊 10 分钟。随便聊什么都行。

不要等到「准备好了」再开始。没有人准备好了才开始的。都是开始了,才慢慢准备好的。

说不定下一个惊艳世界的点子,就来自你此时此刻的好奇心。

以上。


我是甲木,热衷于分享一些AI干货内容,同时也会分享AI在各行业的落地应用,我们下期再见👋🏻

参考文章与资源

行业报告

参考文章

本文由mdnice多平台发布

PingCastle 3.5.0.40 - Active Directory 安全检测和评估

活动目录域安全分析工具

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

作者主页:sysin.org


在 20% 的时间内获得 80% 的 Active Directory 安全性

PingCastle

Active Directory 正迅速成为任何大型公司的关键故障点,因为它的安全既复杂又成本高昂。

人员和流程

PingCastle 的诞生基于一个发现:仅基于技术的安全是行不通的。这就是为什么 公司关注流程和人员 而不仅仅是技术。我们不卖产品!

PingCastle

使用我们的工具并应用我们的方法或查看我们的合作伙伴如何为您带来更多价值。

基于成熟度的方法论

我们不提供保护您的基础设施的解决方案 (sysin)。相反,我们提供工具来发现您必须保护的内容、评估其安全级别并提供有关分配的预算是否得到有效利用的见解。

PingCastle

对于 IT 运营

帮助检测关键安全问题、了解技术状况并提供解决问题的指导和建议。

对于 IT 管理

评估当前的安全级别,指出是否存在严重风险,并就行动计划的优先事项提供建议。

成熟度和结果

建立 IT 管理和 IT 运营之间的通用词汇并提供成熟度评估

可交付成果

我们专注于您需要的可交付成果。您的关键程度是多少?您知道您有多少个域名吗?你能制作一个用于管理的仪表板吗?

健康检查

可以快速收集 Active Directory 最重要的信息以对其进行概述。它根据模型和规则评估Active Directory子进程的分数。然后基于此评估,报告其风险评估。

PingCastle

示意图

绘制地图是通过“信任”链接的活动目录的表示 (sysin)。根据信息的新鲜度和信任链接的深度,它可能更不准确或更准确。事实上,当开始这个过程时,没有太多可用的信息,PingCastle 使用了一组技巧来尽可能地扩展它。

PingCastle

概览

当上下文信息可用时,PingCastle 可以生成一个仪表板以方便表示其收集的数据。这种视图既有全局层面的,也有局部层面的。

PingCastle

新增功能

PingCastle 3.5.0.40

此版本仅错误修复。

PingCastle 3.5.0.37

发布说明

🔐 特权模式更新

  • S-Vuln-MS14-068
  • S-Vuln-MS17-010

    • 检测逻辑现在会检查域控制器上已安装的热修复程序(hotfix)
如果未启用特权模式,这些规则将不再被评估。

🛠️ 规则更新与修复

DNS 区域规则

A-DnsZoneUpdate1 & A-DnsZoneUpdate2

  • _msdcs.* 区域现在被归类为关键基础设施
  • 报告内容已扩展 (sysin),新增包含:

    • 区域名称
    • 域名
    • 可分辨名称(Distinguished Name)
    • 分区(Partition)

这使 DNS 相关信息更加清晰,并简化了后续的修复与整改规划。

P-Kerberoasting

  • 修复了当用户同时属于多个特权组时产生重复发现项的问题,使结果更加聚焦。
  • 报告现在显示:

    • 每个存在风险的用户仅一行
    • 所有关联的组和 SPN 进行汇总展示

T-SIDFiltering

  • 修复了在旧版 Windows 2000 林内信任关系中的误报问题。
  • 这些信任关系通常由于历史域升级,导致 TrustAttributes = 0
  • 新增基于 CrossRef 的过滤逻辑 (sysin),能够正确识别林内信任关系,并且不再将其错误标记为不安全。

Microsoft Defender 攻击面缩减(ASR)

  • Microsoft 在 Windows Server 2025 中更改了 ASR 策略的位置。
  • PingCastle 现在会检查全部三种可能的 GPO 路径
  • 确保在混合服务器版本环境中,ASR 检测依然可靠。

其他规则修复

  • A-DnsZoneAUCreateChild

    • 修复了当域控制器上不存在 DNS 分区时出现的漏报问题。
    • 之前由于一段无法到达的代码路径,部分环境会被完全跳过。
  • S-FolderOptions

    • 修复建议现在指向正确的 GPO 路径

🚀 平台更新:升级至 ASP.NET 8

PingCastle 已升级至 ASP.NET 8,以与 PingCastle Enterprise 保持一致,并希望借此减少过去几个月在部分环境中观察到的杀毒软件误报问题

你可以预期的变化

  • 可执行文件体积更大(约 200 MB)

    • ASP.NET 8 被直接打包进可执行文件中,以简化运行方式。
    • 不再需要任何外部运行时依赖 (sysin)。
  • 配置文件变更

    • 配置文件从 PingCastle.exe.config
      迁移至:appsettings.console.json
  • 自动更新行为变更

    • 如果你使用 PingCastleAutoUpdater.exe,则需要执行两次

      1. 第一次运行:下载新版本
      2. 第二次运行:自动将现有配置迁移到 appsettings.console.json

更新(2026 年 2 月 5 日)
经确认,PingCastle 在发布时由于构建与发布流水线中的顺序问题,错误地发布了未签名的二进制文件。因此,发布版本号已从 3.5.0.33 更新为 3.5.0.37
这两个版本之间没有任何代码改动,仅对构建与发布流水线进行了调整。

下载地址

PingCastle 3.5.0.7 Professional for Windows (updated February 2026)

相关产品:Windows 下载汇总


先说结论:一定要买尊享卡!!!

一大早 5 点多被对象拉起床,赶着地铁去到迪士尼站,到了园区入口已经快 7 点了,园区是 8 点半开始入园,以为提前一个小时能快速免排队玩一个项目,结果好家伙,前面游湖已经是人山人海了

毫无悬念,第一个排队玩全球唯一的疯狂动物城,排了快一小时,感觉还行一个小时多点,问题不大,结果原来是排队入的疯狂动物城的园区?!然后项目还要排两个小时?我人都麻了!

然后我发现一个巨 bug 的地方,原来尊享卡是插队的模式!也就是尊享卡所谓的优先通道是通过插队普通票的方式,还有那个排队方式是精心设计过的,让你们感觉准备排完了,结果里面还有九曲十八弯的队伍(没想到吧)

最后一天才玩了五个项目,(不过有一说一,项目质量十分高,推荐一试)烟花还没看全,体验有点差,下次有机会买尊享卡再去吧

引言

大数据系统的快速扩张,暴露了传统优化技术的局限性,尤其是在分布式架构、动态工作负载以及信息不完全等环境中。如今,各类组织每天处理海量数据以提取业务洞察,例如分析客户行为、预测设备故障、优化供应链以及检测欺诈行为。这些分析任务通常依赖多种分布式数据处理框架来执行,而这些框架都提供大量配置参数,这些参数对性能有着至关重要的影响。论文《大数据处理系统自动参数调优综述》(Herodotou、Yuxing 和 Jiaheng 于 2020 年著)指出了同样的问题,并强调需要能够适应动态负载与环境变化的智能自动调优系统。

 

为应对这一需求,本文提出了一种强化学习(Reinforcement Learning,RL)方法,使分布式计算系统能够像“学徒工程师”通过实践学习那样,自主学习最优配置。我们实现了一个轻量级智能体,作为驱动端组件部署,在任务运行前利用强化学习选择配置参数。

 

本文以Apache Spark为实践基础。Spark 是一种具有代表性的分布式计算框架,可将计算任务分布到数百台机器上执行。Spark 的性能高度依赖配置参数,而这些参数通常使用静态默认值,或由领域专家手动调优。然而,当工作负载特征和数据分布发生变化时,这种方式难以适应。如果配置选择不当,本应在数分钟内完成的分析可能会延长至数小时,同时显著增加云计算成本。随着数据集日益多样化、工作负载愈加动态,依赖静态或手动调优的方式变得脆弱且在经济上难以持续。

 

在处理数百个作业之后,智能体逐渐形成对模式的“直觉”:数据集较小且类别较少时,只需较少的工作节点;数据规模较大且类别丰富时,则需要更多资源。智能体能够完美记住每一次实验结果,从不遗忘经验教训,并将这些累积的知识自动应用到新的工作负载中,相当于把数月专家调优经验转化为一种 24/7 即时可用的智能能力。工程师无需在数据特征变化时反复重新配置系统,智能体会随着每一次作业的执行而不断变得更加智能。

 

Q-learning 智能体是一种强化学习智能体,它通过迭代估计在特定状态下采取某一行动所能获得的长期期望回报,从而学习最优策略。在实际应用中,智能体会观察数据集特征(例如行数、数据规模、基数以及数据倾斜情况),尝试不同的配置参数组合,测量执行性能,并逐步学习在特定数据模式下哪些参数选择效果最佳。

 

本文比较了 Apache Spark 中三种优化策略:内置的自适应查询执行(AQE)、基于 Q-learning 的独立智能体,以及结合两者的混合策略。比较结果表明,混合策略优于单独使用任一方法,因为它将执行前的智能决策(由强化学习选择最优初始配置)与运行时的动态调整(由 AQE 执行)结合在一起。

 

在单智能体实验结果的基础上,本文进一步讨论了一种多智能体强化学习系统的概念扩展方案。该系统由多个相互独立、具备专业化分工的智能体组成,每个智能体专注于优化不同的配置领域,例如内存分配、CPU 核数或缓存策略。每个智能体在其专属领域内成为专家,同时共同协作以优化整体工作负载性能。通过将强化学习理念与分布式系统相结合,本研究为构建能够从经验中学习、无需依赖静态规则或人工干预的智能自调优大数据基础设施奠定了基础。

 

问题背景:Spark 配置优化

Spark 的性能在很大程度上依赖于诸如 shuffle 分区数、内存分配以及并行度设置等配置参数。静态默认值(例如 200 个 shuffle 分区)无法根据数据规模、基数和数据倾斜等不同数据特征进行自适应调整。手动调优不仅需要深厚的领域知识,而且耗时费力,并且针对某一类工作负载优化过的配置,往往在其他负载下表现不佳。

 

以一家虚构的视频分析公司 StreamMetrics 为例。该公司为内容创作者处理视频观看数据,其数据工程团队每天都面临类似挑战:每天早晨,他们运行一个轻量级报表,对前一天按类别(如科学、音乐、娱乐)统计的观看数据进行分析,数据规模仅为数千行。

 

中午,他们会处理每周趋势分析任务,在约 50 万行数据中识别爆款内容。到月底,他们生成综合创作者报告,聚合数百万行数据,涵盖数百个类别,并且数据分布高度倾斜。例如,“游戏”类别可能拥有数百万次观看,而“折纸教程”等小众类别仅有数百次观看。此外,内容创作者还会在全天发起临时分析请求,其数据规模和分布模式都难以预测。

 

在使用 Spark 默认的 200 个 shuffle 分区时,早晨的报表会浪费资源去协调 200 个几乎为空的小任务;每周分析任务可能“误打误撞”运行得还不错;而月底的大规模报告则会因 200 个分区无法有效处理庞大且倾斜的数据而表现不佳。团队当然可以针对每种负载类型手动调优配置,但随着数据模式不断变化,这需要持续维护;例如,上个月的最优配置可能在本月因某个爆款趋势改变类别分布而失效。这正是强化学习能够改变运维方式的典型动态环境。

 

这里,我编写了一个简单的 Spark SQL 查询,用于在一个包含数千行数据的数据集上按类别(如 Science、Music、Entertainment)对视频观看量进行分组统计。

 

默认情况下,这个 groupBy 操作会创建 200 个 shuffle 分区,如图 1 所示。然而,对于小规模数据集而言,这种配置是低效的,因为 Spark 会启动大量微小的 shuffle 任务并生成大量小文件,相对于实际计算量而言,调度、磁盘 I/O 和元数据开销占比过高。大多数分区几乎为空,导致 CPU 和内存资源浪费,同时 driver 和集群将更多时间用于协调任务而非处理数据。

 

图 1:针对一个小文件创建了 200 个任务(Gandhi,2026)

 

在实践中,Spark 开发者通常通过手动设置一个静态分区数来缓解该问题,例如使用经验法则:将分区数设置为 CPU 核数的 2 倍,或执行器数量的 3 倍(参见《Spark 调优》),以确保任务规模足够大,从而提升效率。

 

另一方面,对于超大数据集而言 200 个分区可能又显得不足:这会导致每个分区的数据量过大,处理时间变长,并增加内存溢出的风险。

 

在某些情况下,分区大小只能通过反复试验确定:在不同数据集和工作负载上进行实验,在性能与开销之间寻找平衡。然而,这类配置往往难以在不同数据规模或负载特征之间泛化。自 Spark 3.0 起,引入了自适应查询执行(AQE)。启用后,Spark 会根据执行过程中观察到的实际数据特征动态调整查询计划,而不是完全依赖查询规划阶段的静态估计。

 

然而,AQE 仍然以默认配置(通常是 200 个 shuffle 分区)开始执行,只有在收集到运行时统计信息后才进行合并或调整。这意味着它优化的是 reduce 阶段,但无法避免前期写入大量小 shuffle 文件所带来的初始开销,因此在小规模或中等规模数据集上仍然存在一定低效。此外,如果性能提升需要超过 200 个分区,AQE 也不会自动增加分区数量。

 

强化学习可以在这一场景中发挥关键作用,通过在不同条件下动态调整这些参数,实现跨场景的性能优化。

 

强化学习

正如《强化学习:导论》(Sutton 与 Barto,2018)所定义,强化学习的核心在于学习在不同情境下应采取何种行动,以最大化一个数值奖励信号。与监督学习不同,学习者不会被直接告知应该采取哪些行动,而是必须通过不断尝试,发现哪些行动能够带来最大的回报。试错式搜索以及延迟奖励,是强化学习最重要的两个区别性特征。

 

从形式上讲,强化学习可以被描述为一个 AI 智能体与环境进行交互的过程:智能体感知环境状态,采取行动,并接收奖励,如图 2 所示。随着时间推移,智能体会学习出一项策略(即从状态到行动的映射),以最大化长期期望回报。

 

在本文的场景中,强化学习智能体会观察数据集特征,尝试不同的分区数量,测量执行性能,并逐步积累关于“哪种配置最适合哪种数据模式”的知识。经过多次执行后,智能体会形成类似经验丰富工程师的“直觉”,能够为不同类型的工作负载自动选择合适的分区数量。

 

图 2:标准强化学习中智能体与环境交互循环示意图(Sutton 与 Barto,2018)

 

实现流程:构建一个基于 Q-Learning 的强化学习智能体

我们构建的 Q-Learning 强化学习智能体,是一个在 Apache Spark 之上开发的自定义智能体,部署在 driver 程序中。该实现通过在作业提交流程外层包裹一个智能体层,对 Spark 进行了扩展。

 

以下工作流程展示了我们自定义的 Q-learning 强化学习智能体如何感知 Spark 环境、采取行动、接收反馈,并随着时间推移不断学习。在现实世界中,每天处理数十亿事件的大规模数据平台也面临类似挑战:其数据工程团队需要运行多样化的工作负载,包括基于最新数据的实时仪表盘、针对数百万条记录的周期性聚合报表,以及针对高度倾斜数据分布的综合分析查询。

 

Q-learning 强化学习智能体可以为这些多样化工作负载自动完成配置调优,消除人工干预,通过优化资源分配来降低云计算成本,并加速查询性能,使工程团队能够将更多精力投入到功能开发,而不是反复调整参数。

 

第一步:智能体感知环境(状态观测)

当一个 Spark 作业被提交时,智能体的状态观测模块会拦截该作业,并分析数据集以理解当前环境状态。

print("\nLoading data...")df = spark.read.csv(data_path, header=True, inferSchema=True)row_count = df.count()
复制代码

 

智能体随后提取刻画工作负载的关键特征:

num_rows = df.count()sample_rows = df.limit(1000).collect()from collections import Countercategory_values = [row.category for row in sample_rows]category_counter = Counter(category_values)category_cardinality = len(category_counter)counts = list(category_counter.values())skew_factor = np.std(counts) / np.mean(counts)
复制代码

智能体所观测到的特征包括:

  • 行数:数据量越大,通常需要更多分区

  • 列数:列越多的数据集可能需要更多分区

  • 类别唯一值数量(基数):基数越高,通常意味着需要更多分区

  • 数据大小(MB):数据越大,通常受益于更多分区

  • 平均行大小(字节):用于衡量数据密度

  • 数据倾斜因子(skew factor):衡量数据分布是否不均衡;倾斜严重时需要额外调整

 

智能体的设计选择

智能体仅抽样 1000 行数据(约 100ms),而不是扫描整个数据集,从而在准确性与实时决策之间取得平衡。这种轻量级观测机制,使智能体即便在大规模数据集上也能快速作出决策。

 

第二步:状态编码(为泛化进行离散化)

状态编码模块将连续特征转换为离散状态表示,从而使智能体能够在相似工作负载之间泛化已学到的知识。

# 为智能体设计的自定义离散化分桶row_buckets = [100, 1000, 10000, 100000, 1000000]size_buckets = [1, 10, 100, 1000]card_buckets = [5, 10, 20, 50, 100]skew_buckets = [0.1, 0.3, 0.5, 0.8, 1.0]
复制代码

 

例如,智能体处理一个包含 5000 行、1.23 MB、12 个类别、倾斜度 0.48 的数据集时:

# 智能体离散化逻辑如下::# 行数 5000 → bucket_2# 数据大小 1.23 MB → bucket_1# 基数 12 → 位于 10–20 区间 → bucket_2# 倾斜度 0.48 → 位于 0.3–0.5 区间 → bucket_3state_key = "rows_bucket_2|size_bucket_1|card_bucket_2|skew_bucket_3"
复制代码

 

为什么离散化至关重要?

如果不进行离散化,智能体会将 5000 行与 5001 行的数据集视为完全不同的状态,从而导致学习难以收敛。通过分桶处理,智能体能够识别“1000 到 10000 行的数据集具有相似优化模式”,进而将之前作业中学到的经验应用到新的、但结构相似的工作负载上。

 

第三步:智能体选择行动(ε-贪婪策略)

行动选择模块查询 Q 表,并在“探索”(exploration)与“利用”(exploitation)之间进行权衡,决定尝试哪个分区数。

# 行动空间由若干自定义候选分区数组成actions = [8, 16, 32, 64, 128, 200, 400]# 智能体的探索参数 epsilon = 0.3# 智能体的判断逻辑if random.random() < epsilon:    action = random.choice(actions)  # EXPLORE: Try something new    action_type = "explore" else:   action = max(Q[state_key],key=Q[state_key].get)# EXPLOIT: Use best known action_type = "exploit"
复制代码

 

智能体维护一张 Q 表,用于存储每个“状态-行动”组合的价值估计。例如在某个状态下:

Q["rows_bucket_2|size_bucket_1|card_bucket_2|skew_bucket_3"] = {    8: -0.405,      # Agent tried this, took 0.405 seconds    16: -0.523,     # Agent tried this, took 0.523 seconds    32: -0.650,     # Agent tried this, took 0.650 seconds    64: 0.0,        # Agent hasn't tried this yet    128: 0.0,       # Agent hasn't tried this yet    200: -0.745,    # Agent tried this, took 0.745 seconds (worst so far)    400: 0.0        # Agent hasn't tried this yet}
复制代码

 

智能体的决定: 

智能体会选择 Q 值最高的分区数(在此例中为 8),因为其对应的执行时间最短(-0.405 接近于零)。

 

智能体的学习策略: 

初始 ε = 0.3(30% 探索概率):在学习初期,智能体保持较高的探索率。随着训练过程推进,ε 逐步衰减至 0.05。然而,ε 并未降至 0,而是保留最低 5% 的探索概率,以适应不断演化的工作负载分布,并避免陷入次优策略。

 

第四步:智能体作用于环境(应用配置)

配置管理模块将智能体选定的分区数写入 Spark 配置,然后执行作业。

# Agent injects its learned configuration into Sparkspark.conf.set("spark.sql.shuffle.partitions", "8")# Spark job executes with agent-selected configurationresult_df = df.groupBy("category").count()result_df.show()
复制代码

 

关键点: 

智能体并不修改 Spark 的内部执行逻辑,而是作为一个“智能包装层”,在作业执行前设置最优配置,随后交由 Spark 原生执行引擎完成实际计算。

 

第五步:智能体接收奖励(性能反馈)

当 Spark 完成作业后,奖励计算模块计算执行时间,并将其作为学习信号。在本实现中,奖励函数定义为:reward = -execution_time。也就是说,执行时间越短,奖励越高。需要注意的是,该实现仅以执行时间为优化目标,并未显式考虑运行成本、内存压力、失败风险或资源利用率等多目标因素。更复杂的系统可能会构建多目标奖励函数。

# Agent measures job performancestart_time = time.time()result_df = df.groupBy("category").count().collect()execution_time = time.time() - start_time# Agent's reward signal (negative because lower time is better)reward = -execution_time  # e.g., -0.321 seconds
复制代码

 

第六步:智能体学习(Q 值更新)

学习引擎根据 Q-learning 更新公式更新 Q 表并结合观测奖励

Q(s,a)←Q(s,a)+α(r+γxmax_{a′}Q(s′,a′)−Q(s,a))

 

# Q-learning 更新公式(在智能体学习引擎中实现) alpha = 0.3 # 学习率:根据当前学习需要调整多少 gamma = 0.1 # 折扣因子:未来奖励价值多少 old_q_value = Q[state_key][action] max_future_q = max(Q[state_key].values()) new_q_value = old_q_value + alpha * (reward + gamma * max_future_q -  old_q_value)# 智能体更新记忆Q[state_key][action] = new_q_value
复制代码

学习情况示例: 

如果某一状态下,之前执行时间为 0.4 秒(reward = -0.4),而最新一次执行时间为 0.6 秒(reward = -0.6),那么 Q 值会被向下调整,表示该行动表现不如预期。下一轮中,智能体更可能探索其他分区数。

 

智能体的持续改进机制 

智能体会将 Q 表持久化(例如以 JSON 形式保存),在不同作业之间保留学习结果,从而在数周或数月内逐步积累组织级知识。每一个新作业都是一次学习机会,随着经验积累,智能体的策略将不断精细化,实现真正意义上的自调优系统。

 

实验结果

为验证智能体的有效性,我们在相同工作负载下,对三种优化策略进行了对比实验:

  • 仅 AQE:使用 Spark 内置的自适应查询执行

  • 仅 RL 智能体:使用自定义 Q-learning 智能体,并关闭 AQE

  • 混合策略(AQE + RL):由 Q-learning 智能体选择初始配置,并结合 AQE 进行运行时自适应调整

 

性能对比:

下方图表(图 3)展示了在一个小规模数据集(1000 行)且数据倾斜度较低(0.162)的情况下得到的实验结果。

图 3:小规模数据集的执行时间(Gandhi,2026)

 

实验结果表明,性能得到了显著提升。

 

同样的实验还在一个包含 75,000 行数据、且数据高度倾斜(倾斜度 1.241)的大规模数据集上进行了测试。结果显示,性能提升会随着数据规模和倾斜复杂度的增加而更加明显,如图 4 所示。

图 4:超大规模高倾斜数据集的执行时间(Gandhi,2026)

 

 

关键发现

混合策略优于“仅 AQE”和“仅 RL”两种方法。这验证了一个核心观点:执行前智能决策(RL 选择最优初始配置)与运行时自适应调整(AQE 的动态优化)分别解决了互补的优化问题。

 

对比分析:两个核心洞见

强化学习智能体相比标准的规则驱动型 AQE,能够实现显著更快的执行时间。其优势来源于能够学习并选择最优的初始分区数量(例如在小数据集上选择 8 个分区)。这种前置式配置优化在作业执行开始之前就消除了 shuffle 阶段的额外开销。而 Spark 默认的 AQE 无法完全实现这一点,因为 AQE 只能在 shuffle 块已经写入磁盘之后,才对过多分区进行合并和调整。

 

混合方法实现最佳性能。将 RL 与 AQE 结合,形成了一个两阶段优化机制:

  • 阶段一(执行前):RL 智能体基于历史学习结果设置最优初始配置。

  • 阶段二(运行时):AQE 在执行过程中根据实际观测到的情况(例如运行中发现的数据倾斜、分区大小不均等)进行动态调整。

 

这些实验结果展示了该方法在现实大规模数据平台中的实际价值。对于每天处理数十亿事件的数据系统而言,只需启用 AQE(大多数 Spark 3.0 及以上版本部署已默认支持),并引入 RL 智能体,即有可能在多样化工作负载下获得显著性能提升。这些性能改进可以转化为:通过优化资源分配降低云计算成本、加速查询执行,缩短业务洞察交付时间,以及释放工程团队精力,使其专注于功能开发而非参数调优。

 

扩展至多智能体系统与系统架构

尽管单一的分区优化智能体已经带来了显著性能提升,但在大规模数据平台中,现实情况要复杂得多。例如,在每日聚合作业中,RL 智能体已经成功设置了最优的 shuffle 分区数,但作业仍然因内存溢出而失败,因为执行器内存未针对大规模 join 操作进行合理配置。再例如,实时仪表盘在某个分区数下运行高效,但由于未启用缓存机制,相同的中间数据被反复计算,浪费了大量 CPU 资源。

 

如果仅优化分区数量,仍然会遗留大量可观的性能与成本优化空间。生产级工作负载往往需要在多个维度上进行同步优化,包括:不同操作类型(如 join 与 aggregation)对应的内存分配策略、不同负载强度(I/O 密集型 vs 计算密集型)下的 CPU 并行度配置,以及面向数据复用模式的智能缓存决策。手动协调这些配置的复杂度呈指数级增长。工程师不仅需要调优分区数,还必须考虑:分区数量如何影响内存使用、内存配置如何影响 CPU 利用率,以及缓存策略如何进一步影响上述所有因素。因此,有必要将单一智能体方法扩展为多个独立学习组件,每个组件分别优化特定配置域。

 

单一分区优化智能体已经验证了强化学习在 Spark 配置优化中的可行性,但生产环境中的工作负载通常需要跨多个维度的联合优化。一个自然的扩展方式是在 Spark driver 上部署多个专用智能体,每个智能体负责不同的配置领域,并根据作业执行反馈独立学习。在这种多智能体架构中,引入一个协调器。该协调器是一个轻量级控制层,负责按照固定顺序应用各智能体的决策,但本身不进行学习或策略优化。它与分区智能体协同,额外编排三个专用智能体。

 

内存智能体通过监控内存使用模式、垃圾回收频率以及磁盘溢写事件来优化执行器内存分配。基于工作负载特征(例如大量 join 操作需要构建大型哈希表,对比包含过滤操作的查询语句只占用较低内存),该智能体会动态配置spark.executor.memoryspark.memory.fraction,以及 spark.memory.storageFraction 以在性能与资源浪费之间取得平衡。

 

核心数智能体通过跟踪 CPU 核心利用率、任务等待时间以及线程竞争情况,学习最优的并行度设置。其调整的参数包括: spark.executor.coresspark.task.cpus,以及 spark.executor.instances 。该智能体能够识别 I/O 密集型任务通常受益于更高并行度,以及计算密集型任务在过度并行时会因频繁上下文切换而性能下降。

 

缓存智能体通过测量缓存命中率、缓存淘汰模式以及重复计算成本,学习智能缓存策略。其决策包括:是否缓存中间 DataFrame、选择适当的存储级别(仅内存、内存+磁盘、仅磁盘),以及配置spark.storage.memoryFractionspark.rdd.compress。该决策基于数据复用模式以及可用内存资源进行动态调整。

 

每个智能体均采用与分区优化智能体相同的 Q-learning 框架:提取与其领域相关的状态特征、维护独立的 Q 表,以及基于作业执行性能奖励进行更新。这种解耦设计使得每个智能体能够在自身领域内形成专业能力,同时整个系统实现全面的工作负载优化。

 

图 5 展示了上述多智能体系统的高层架构。

图 5:Apache Spark 的高层多智能体系统架构(Gandhi,2026)

 

结论

本文展示了强化学习如何将传统上依赖人工、且容易出错的 Spark 配置调优过程,转变为一种自主、可自适应的优化系统。通过实现一个基于 Q-learning 的强化学习智能体,使其能够观测数据集特征、尝试不同分区数量,并根据性能反馈持续学习,系统逐步形成了类似资深工程师的优化能力,同时具备完美记忆和系统化探索机制。

 

实验结果验证了该方法的有效性。单独使用 RL 智能体时,其性能优于 Spark 默认的自适应查询执行(AQE);而将 AQE 与 Q-learning 结合的混合策略取得了最佳整体性能。这表明,执行前智能决策(RL 选择最优初始配置)与运行时自适应调整(AQE 的动态优化)分别解决了互补的优化问题。

 

需要指出的是,本研究的实验基于相对较小的数据集(1000 至 75,000 行),相比每天处理数十亿事件的生产级系统仍有差距。尽管结果验证了基于 RL 的配置优化方法在概念层面的可行性,但若能在 PB 级数据规模、更加复杂的查询模式下进行验证,将进一步增强其在生产环境中部署的可信度。此外,当前实现仅聚焦于单一配置维度(shuffle 分区);扩展至涵盖内存、CPU 与缓存等多维度的多智能体优化体系,还需要进一步实验,以验证智能体之间的交互效果并确保稳定收敛。

 

本文提出的多智能体架构,将上述思想扩展至全面的工作负载优化场景。多个专用智能体分别针对内存分配、CPU 核心调度以及缓存策略等领域独立学习优化策略,在各自领域形成专业能力。展望未来,该架构为多个研究方向提供了可能,包括:跨集群环境的迁移学习、用于连续状态空间的深度 Q 网络(DQN),以及融合集群拓扑信息的上下文感知策略。对于管理生产级 Spark 工作负载的数据工程师而言,这一方法提供了一条可行路径:对作业进行性能指标采集,实现一个简化版的 Q-learning 智能体用于 shuffle 分区优化,与现有系统并行部署,并让其在真实生产流量中持续学习。

 

该方法能够积累组织级知识,将数月的调优经验转化为可复用的策略,供未来所有作业使用。强化学习与分布式系统的结合不仅是一种优化技术,更代表着基础设施演进方向的转变——从依赖静态规则的系统,迈向能够基于经验持续学习与自我优化的自主型基础设施。随着大数据系统复杂度不断提升,配置参数成千上万,工作负载持续演化,能够自主学习、适应并优化的智能体将不再只是便利工具,而将成为必需能力。

原文链接:

https://www.infoq.com/articles/agent-reinforcement-learning-apache-spark/

WhatsApp 的工程团队将其媒体处理库从 C++ 重写为 Rust,将代码规模从 16 万行减少至 9 万行,同时引入了内存安全保护机制。该库运行在数十亿台设备上,包括 Android 手机、iPhone、桌面设备、智能手表以及网页浏览器,这使其成为迄今为止规模最大的客户端 Rust 代码部署之一。

 

这一工作可以追溯到 2015 年的Stagefright 漏洞事件。当时人们发现攻击者可以将恶意软件隐藏在看似正常的图片或视频文件中。这些恶意文件利用了 Android 媒体库中的漏洞,而像 WhatsApp 这样的应用无法直接修补底层操作系统。那时,WhatsApp 使用一个名为 “wamedia” 的 C++ 库,在发送前对 MP4 文件进行合规性检查。公司意识到,这段代码处理的是来自潜在恶意来源的非可信数据,因此非常适合使用内存安全语言进行重写。

 

Meta 工程博客 – 安全与隐私专栏

 

尽管 Meta 此次部署在规模上前所未有,但其策略并非没有先例。Mozilla 的首席工程师 Andrew Lilley Brinker 在 Bluesky 上提到

很多人知道 Mozilla 在 Rust 早期发展阶段提供了大量资助,但可能不知道,Firefox 第一个上线的 Rust 组件其实是 2016 年的一个 MP4 解析器!

 

这同样是对 Stagefright 时代所揭示问题的回应——即当 C++ 媒体处理代码在解析非可信二进制数据时,存在固有的安全风险。

 

WhatsApp 并没有采用渐进式替换方式,而是同时构建了完整的 Rust 版本与原 C++ 版本并行运行。团队通过差分模糊测试以及大量集成测试,验证两个版本之间的兼容性,然后才完成迁移。WhatsApp 软件工程师 Daniel Sommermann 和 Baojun Wang表示,这种策略不仅带来了性能提升,还降低了内存使用量。

 

不过,二进制文件大小成为一个现实问题。博客文章提到,Rust 标准库最初增加了文件体积,但并未详细说明解决方案。在 Hacker News 的讨论中,WhatsApp 工程师 Daniel Sommermann 解释道:

 

“我们在构建系统优化方面投入了大量工作,随着时间推移逐步将体积压缩下来。虽然一开始确实接受了大约 200 KiB 的标准库体积开销。”团队将构建系统从 Gradle、CMake 和 Cargo 迁移到 Buck2。Sommermann 表示,Buck2 “在缩小体积方面帮助巨大,比如改进了 LTO(链接时优化)并使用了最新的 clang 工具链优化。”

 

该媒体库已经不仅仅用于基础格式校验。WhatsApp 将这一扩展系统命名为 “Kaleidoscope”。它会检测可疑模式,例如:嵌入文件或脚本的 PDF、文件扩展名与实际内容不匹配的文件、伪装成图片的可执行文件等。一旦发现风险内容,系统会在用户界面中发出警告。这些检测无法拦截所有攻击,但能够阻挡许多常见的利用技术。

 

Meta 表示,这是他们所知规模最大的 Rust 库面向终端用户设备的部署。每个月,这段代码都会通过 WhatsApp、Messenger 和 Instagram 推送到数十亿台设备,包括手机、笔记本电脑、桌面设备、智能手表和各种操作系统上的浏览器。

 

在 Hacker News 的讨论中,还有人探讨了技术细节。例如,用户 Cong-or 指出二进制体积的重要性:

在服务器环境中,Rust 标准库带来的开销通常不算什么,但当你要向数十亿台移动设备发布软件时,每一个 KB 都至关重要。很高兴看到他们投入资源优化构建工具,而不是简单接受体积膨胀。

 

另一位用户 storystarling 则强调测试难度:

这种重写工作最难的部分通常不是 Rust 实现本身,而是要保持与旧版解析器在行为上的‘bug 级兼容’。

 

WhatsApp 的安全策略分为三条路径:尽可能减少攻击面;对仍然使用的 C 和 C++ 代码加强保护(如控制流完整性与强化内存分配器);对于新代码优先使用内存安全语言。使用 C/C++ 的开发者需要接受专门的安全培训,其代码会经过自动化分析工具审查,公司对发现的问题设有严格的修复期限。

 

Meta 的安全团队正在推动公司内部更多团队采用 Rust,并预计未来几年采用速度将加快。这一趋势也反映了整个行业的发展方向。谷歌在 2025 年 11 月的安全博客中表示,Android 中 Rust 代码的引入,使内存安全漏洞占比从 2019 年的 76% 降至 2025 年底的不足 20%,公司将这一变化直接归因于用 Rust 替代新代码中的 C 和 C++。Chrome 已经在字体渲染和图像解码模块中使用 Rust 库,而 Microsoft 自 2023 年起也开始用 Rust重写部分 Windows 组件。

 

原文链接:

https://www.infoq.com/news/2026/02/whatsapp-rust-media-malware/

在 AI 浪潮逐浪全球的当下,SaaS 领域正经历着一场前所未有的“重塑”。从 Notion AI 到 Microsoft 365 Copilot,开发者们都在思考如何将大模型能力融入传统办公场景。

2025 年下半年,一款名为 Shortcut的产品在海外科技圈掀起波澜。其创始人在社交平台发布的 “AI 生成财务模型” 演示视频,在短短 72 小时内播放量突破 300 万,华尔街日报更是评价其 “重新定义了 Excel 的效率边界”。

在这里插入图片描述

实测数据显示,Shortcut 能让新手在 10 分钟内完成原本需资深分析师 2 小时的报表工作,甚至在 Excel 技能挑战赛中以 90% 的胜率击败了新人分析师。这款产品不仅在硅谷获得了极高的关注度,更让人们重新审视了一个核心命题:当传统的电子表格遇上大语言模型(LLM),会产生怎样的化学反应?

本文将深度解析 Shortcut 的成功之道,并探讨其背后的底层技术引擎——SpreadJS,如何在幕后支撑起这类复杂、高性能的 AI 办公应用。

一、 破局者 Shortcut:为什么它能让“老赛道”焕发新生?

电子表格(Spreadsheet)自 1979 年 VisiCalc 诞生以来,其核心形态已经稳定了四十多年。即便是在云端协同时代,Google Sheets 和 Excel Online 依然延续着格子、公式、菜单的经典逻辑。

但 Shortcut 走出了另一条路。它的核心逻辑不再是“用户输入数据,公式计算结果”,而是“用户表达意图,AI 构建逻辑”。

1.1 将 AI 原子化为单元格能力

在 Shortcut 中,最令人惊艳的功能莫过于其内置的 AI() 函数。用户可以在单元格中直接调用大模型的推理能力。例如:

  • =AI("分析这段用户反馈的情绪偏好", A2)
  • =AI("根据 B 列的公司名称,从互联网抓取其最新的融资轮次", B2)

在这里插入图片描述

这种设计将 LLM 的推理能力原子化、组件化,使其能够像求和函数 SUM() 一样,无缝嵌入到业务逻辑的流转中。

1.2 处理非结构化数据的“黑魔法”

传统表格最怕处理文本。但在 Shortcut 面前,杂乱的会议纪要、非标准的地址信息、甚至是一长串的产品描述,都能通过自然语言指令快速转化为结构化数据。正是这种从“模糊意图”到“精确表格”的极速转化,让其在财务建模、市场调研等领域展现出统治级的效率。

1.3 极速的交互响应

很多尝试在 Web 端复刻 Excel 体验的开发者都会遇到一个痛点:随着数据量的增加,页面会变得极其卡顿。Shortcut 能够支撑起海量 AI 推理结果的实时展示,且保持如丝般顺滑的滚动和操作,这说明其底层拥有一套极其强悍的渲染引擎。

二、 深度解析:Shortcut 成功的技术基石——SpreadJS

在分析 Shortcut 的底层架构时,技术社区发现,这款“明日之星”并非从零开始去写每一个单元格的渲染逻辑,而是基于成熟的电子表格控件——SpreadJS

为什么像 Shortcut 这样的创新产品会选择 SpreadJS 作为其“技术底座”?这对于中国开发者在构建类似应用时具有极高的参考价值。

2.1 性能的天花板:Canvas 渲染引擎

在 Web 端,处理数以万计的单元格、复杂的条件格式以及频繁的数据更新,如果采用传统的 DOM 节点模式,浏览器性能会瞬间崩溃。

SpreadJS 采用了高性能的 Canvas 渲染技术。它将表格视为一个整体的画布,只在必要时重绘变化的区域。这种架构确保了 Shortcut 在回填大量 AI 预测数据时,依然能保持 60 帧以上的响应速度。

2.2 像素级的 Excel 兼容性

对于国内企业级开发而言,脱离 Excel 谈电子表格是不现实的。Shortcut 能够赢得资深办公用户的青睐,是因为它完美继承了 Excel 的使用习惯和功能:

  • 公式系统: 450 多种内置函数,完全兼容 Excel 逻辑,这为 AI 生成财务模型提供了坚实的数学底座。
  • 透视表与图表: 让 AI 分析的结果能够直接转化为可视化的看板。
  • 文件解析: 能够零损耗地导入导出的 XLSX 文件。

SpreadJS 这种“原生 Excel 体验”的输出,让开发者可以将精力 100% 投入到 AI 业务场景的创新上,而不必在“如何让表格不走样”这种基础问题上浪费时间。

2.3 极高的 API 开放性与可扩展性

Shortcut 能够定制出独特的 AI 交互界面,得益于 SpreadJS 提供的丰富 API。无论是自定义单元格类型(CellType)、悬浮组件,还是右键菜单的深度定制,SpreadJS 都为开发者留出了足够的发挥空间。

三、 开发者实战:如何利用 AI 插件复刻“Shortcut 模式”?

对于国内开发者来说,复刻一个类似 Shortcut 的系统并不需要从头摸索。SpreadJS 官方推出的 SpreadJS AI 插件,已经将最核心的 AI + 表格交互场景模块化了。

在这里插入图片描述

3.1 NL2Formula:从自然语言到公式

在复杂的报表系统中,普通用户往往记不住复杂的嵌套函数。通过 SpreadJS AI 插件,开发者可以实现“语言驱动计算”:

  • 场景描述: 用户在输入框说“计算去年同期相比的利润增长,并保留两位小数”。
  • 技术实现: AI 插件会自动解析当前表格的列名(Header)和数据结构,将其翻译成精准的公式字符串并填充到目标单元格。

在这里插入图片描述

3.2 智能数据清洗与纠错

中国企业的数据环境往往存在大量的“非标”数据。利用 AI 插件的语义理解能力,开发者可以构建一套自动纠错系统:

  • 案例: 自动识别并统一地址格式、从杂乱的备注中提取金额、发现并标记逻辑异常的数据行(如单价与总价不匹配)。

    • 在这里插入图片描述

3.3 交互式 AI 侧边栏(Chat Assistant)

模仿 Shortcut 的侧边栏助手,开发者可以使用插件快速搭建一个“数据对话框”。用户可以对选中的区域提问:“请基于这些销售数据,帮我写一段下个季度的业务建议”。AI 插件会通过上下文感知(Context Awareness),将选中的单元格内容转化为 LLM 易理解的 Prompt,并生成专业的报告。

在这里插入图片描述

四、 针对开发场景的技术避坑指南

作为长期关注开发者生态的观察者,针对开发者在使用 SpreadJS 构建 AI 表格应用时,有几点针对性的建议:

4.1 灵活对接国产大模型

考虑到数据安全和合规性,国内应用往往首选文心一言、通义千问等模型。SpreadJS AI 插件在设计上保持了高度的灵活性,其底层接口支持标准的 Restful API 调用。开发者可以轻松地通过配置,将 AI 能力从 OpenAI 切换到国产大模型,甚至是在企业内网部署的私有化模型。

4.2 数据安全与隐私保护

在处理财务、医疗等敏感数据时,直接将全量表格发送给 AI 是有风险的。建议采用“数据摘要+脱敏数据”的传输策略。SpreadJS 的 API 允许开发者精细化地控制哪些数据被发送给 AI 插件,从而在保证智能性的同时,守住合规底线。

4.3 异步处理与 UI 反馈优化

AI 的推理通常需要几秒钟的时间。为了避免阻塞用户操作,开发者应充分利用 SpreadJS 的异步加载能力。在 AI 处理期间,利用自定义单元格展示“加载动画”,并在数据返回后使用 suspendPaintresumePaint 进行批量渲染,以达到最佳的交互体验。

五、 结语:电子表格的新纪元

Shortcut 的爆火不是一个偶然现象,它标志着生产力工具从“被动工具”向“主动助手”的跨越。通过实测数据的对比,我们清晰地看到:AI 不再是装饰品,而是切切实实的生产力提升工具。

对于开发者而言,这场变革意味着巨大的机遇。无论是构建企业级的 BI 系统、SaaS 化的协同办公软件,还是内部的自动化审计工具,SpreadJS + AI 的组合都提供了一条已经被验证成功的技术路径。它既保证了电子表格该有的“硬核”性能和兼容性,又通过 AI 赋予了应用“感知”和“思考”的能力。

如果您正准备开发下一代电子表格应用,不妨先站在像 SpreadJS 这样成熟的肩膀上,把精力聚焦在最能产生业务价值的 AI 逻辑创新中。

参考资源:

在很多人看来,SSL证书主要是用于互联网上的网站,比如电商平台、银行网站等,需要保护用户的敏感数据。但你可能不知道,内网IP(如192.168.1.1、10.0.0.1等)同样需要SSL证书。

1. 防止内网数据被窃听

即使你的服务只在内部网运行,数据仍然可能被监听。例如:
如果公司Wi-Fi被入侵,黑客可以嗅探内部HTTP流量,获取账号密码、数据库信息等。
内部员工可能利用抓包工具(如Wireshark)查看未加密的通信内容。
SSL证书的作用:通过HTTPS加密,确保数据在传输过程中无法被窃取或篡改。

内网IP地址SSL证书申请入口

直接访问JoySSL注册一个账号,记得填写注册码230970获取免费安装服务

2. 避免浏览器“不安全”警告

现代浏览器(如Chrome、Edge)会对所有HTTP网站标记为“不安全”,即使是内网IP也不例外。这会导致:
员工访问内部系统时频繁看到警告,影响使用体验。
某些浏览器可能阻止访问HTTP网站,导致内部工具无法正常使用。
SSL证书的解决方案:部署证书后,内网服务将以HTTPS运行,浏览器不再提示“不安全”。

3. 满足安全合规要求

许多行业(如金融、医疗、政府)对数据安全有严格要求,例如:
GDPR(欧盟通用数据保护条例) :要求企业保护用户和员工的隐私数据。
等保2.0(中国网络安全等级保护) :明确要求内部系统采用加密通信。
SSL证书的合规价值:帮助企业在审计时证明内部通信符合安全标准。

4. 防止中间人攻击(MITM)

在内网坏境中,攻击者可能伪装成网关或服务器,进行中间人攻击(MITM),例如:
伪造一个假的登录页面,诱导员工输入账号密码。
篡改内部API请求,导致数据泄露或系统故障。
SSL证书的防护机制:HTTPS通过数字证书验证服务器身份,确保通信双方不被冒充。

先祝大家新年快乐,开工大吉!(快不快乐另说,大吉是真得大吉)

为减少自己久坐,偶尔熬夜写代码的毛病,写了一款不能轻易跳过的 macOS 休息提醒工具:ForceBreak

**先丢 5 个终身码(国区¥18)**,手快有手慢无:

XA9K347K9F9N
M6X9XNM44JN3
PWF3E43YRXW6
MY7PHLHNHTPM
WMWER9MNAJT4

另外,欢迎回帖参与抽奖,48 小时后抽 15 个人发码

App Store: https://apps.apple.com/cn/app/forcebreak/id6758971359

前言

CompletableFuture是jdk8的新特性。CompletableFuture的实现与使用上,处处体现出了函数式异步编程的味道。一个CompletableFuture对象可以被一个环节接一个环节的处理、也可以对两个或者多个CompletableFuture进行组合处理或者等待结果完成。通过对CompletableFuture各种方法的合理使用与组合搭配,可以在很多的场景都可以应付自如。

CompletableFuture实现了CompletionStage接口和Future接口,前者是对后者的一个扩展,增加了异步会点、流式处理、多个Future组合处理的能力,使Java在处理多任务的协同工作时更加顺畅便利。

假设现在需求如下:
从网上查询某个产品的最低价格,例如可以从淘宝、京东、拼多多去获取某个商品的价格、优惠金额,并计算出实际的付款金额,最终返回价格最低的价格信息。

这里假设每个平台获取原价格与优惠券的接口已经实现、且都是需要调用HTTP接口查询的耗时操作,接口每个耗时1s左右。

根据需求理解,可以很自然的写出对应实现代码:

public int getCheapestPlatAndPrice(String product){
    int taoBaoPrice = computeRealPrice(HttpRequestMock.getTaoBaoPrice(product), HttpRequestMock.getTaoBaoDiscounts(product));
    int jingDongPrice = computeRealPrice(HttpRequestMock.getJingDongPrice(product), HttpRequestMock.getJingDongDiscounts(product));
    int pinDuoDuoPrice = computeRealPrice(HttpRequestMock.getPinDuoDuoPrice(product), HttpRequestMock.getPinDuoDuoDiscounts(product));

    // 计算并选出实际价格最低的平台
    return Stream.of(taoBaoPrice, jingDongPrice, pinDuoDuoPrice).min(Comparator.comparingInt(p - > p)).get();
}

运行测试下:

14:58:32.330228700[main]获取淘宝上iphone16的价格完成: 5199
14:58:33.351948100[main]获取淘宝上iphone16的折扣价格完成: 200
14:58:33.352933400[main]计算实际价格完成: 4999
14:58:34.364138900[main]获取京东上iphone16的价格完成: 5299
14:58:35.377258800[main]获取京东上iphone16的折扣价格完成: 150
14:58:35.378257300[main]计算实际价格完成: 5149
14:58:36.392813800[main]获取拼多多上iphone16的价格完成: 5399
14:58:37.405863200[main]获取拼多多上iphone16的折扣价格完成: 99
14:58:37.406712600[main]计算实际价格完成: 5300
4999
耗时:6142ms

结果符合预期,功能正常,但是耗时较长。试想一下,假如你在某个APP操作需要等待6s才返回最终计算结果,那不得直接摔手机?

梳理下代码的实现思路:

可以知道所有的环节都是串行实现的的,由于每个查询接口的耗时都是1s,因此每个环节耗时加到一起,接口总耗时超过6s。

但实际上,每个平台之间的操作是互不干扰的,那其实就可以通过多线程的方式,同时去分别执行各个平台的逻辑处理,最后将各个平台的结果汇总到一起比对得到最低价格。

所以整个执行过程会变成如下的效果:

因此为了提升性能,可以采用线程池来负责多线程的处理操作,因为需要得到各个子线程处理的结果,所以需要使用 Future来实现:

public Integer getCheapestPlatAndPrice2(String product) {
    Future <Integer> taoBaoFuture = threadPool.submit(() -> computeRealPrice(HttpRequestMock.getTaoBaoPrice(product), HttpRequestMock.getTaoBaoDiscounts(product)));
    Future <Integer> jingDongFuture = threadPool.submit(() -> computeRealPrice(HttpRequestMock.getJingDongPrice(product), HttpRequestMock.getJingDongDiscounts(product)));
    Future <Integer> pinDuoDuoFuture = threadPool.submit(() -> computeRealPrice(HttpRequestMock.getPinDuoDuoPrice(product), HttpRequestMock.getPinDuoDuoDiscounts(product)));

    // 等待所有线程结果都处理完成,然后从结果中计算出最低价
    return Stream.of(taoBaoFuture, jingDongFuture, pinDuoDuoFuture)
        .map(price - > {
            try {
                return price.get();
            } catch (Exception e) {
                return null;
            }
        })
        .min(Comparator.comparingInt(p - > p))
        .get();
}

上述代码中,将三个不同平台对应的Callable函数逻辑放入到ThreadPool中去执行,返回Future对象,然后再逐个通过Future.get()接口阻塞获取各自平台的结果,最后经比较处理后返回最低价信息。

执行代码,可以看到执行结果与过程如下:

15:19:25.793891500[pool-1-thread-3]获取拼多多上iphone16的价格完成: 5399
15:19:25.793891500[pool-1-thread-2]获取京东上iphone16的价格完成: 5299
15:19:25.794891500[pool-1-thread-1]获取淘宝上iphone16的价格完成: 5199
15:19:26.816140300[pool-1-thread-2]获取京东上iphone16的折扣价格完成: 150
15:19:26.816140300[pool-1-thread-3]获取拼多多上iphone16的折扣价格完成: 99
15:19:26.816923600[pool-1-thread-3]计算实际价格完成: 5300
15:19:26.816923600[pool-1-thread-2]计算实际价格完成: 5149
15:19:26.817921500[pool-1-thread-1]获取淘宝上iphone16的折扣价格完成: 200
15:19:26.820923400[pool-1-thread-1]计算实际价格完成: 4999
4999
耗时:2085ms

接口总耗时从6s下降到了2s,效果还是很显著的。但是,是否还能再压缩一些呢?

基于上面按照平台拆分并行处理的思路继续推进,我们可以看出每个平台内的处理逻辑其实可以分为3个主要步骤:

  1. 获取原始价格(耗时操作)
  2. 获取折扣优惠(耗时操作)
  3. 得到原始价格和折扣优惠之后,计算实付价格

这3个步骤中,其实第1、2两个耗时操作也是相对独立的,如果也能并行处理的话,响应时长上应该也能继续缩短,即如下的处理流程:

这里当然也可以继续使用上面提到的线程池+Future的方式,但Future在应对并行结果组合以及后续处理等方面显得力不从心,弊端明显:

代码写起来会非常拖沓:先封装Callable函数放到线程池中去执行查询操作,然后分三组阻塞等待结果并计算出各自结果,最后再阻塞等待价格计算完成后汇总得到最终结果。

说到这里呢,就需要CompletableFuture登场了,CompletableFuture可以很轻松的来完成任务的并行处理,以及各个并行任务结果之间的组合再处理等操作。使用CompletableFuture编写实现代码如下:

public Integer getCheapestPlatAndPrice3(String product) {
    CompletableFuture <Integer> taoBao = CompletableFuture.supplyAsync(() -> HttpRequestMock.getTaoBaoPrice(product)).thenCombine(CompletableFuture.supplyAsync(() -> HttpRequestMock.getTaoBaoDiscounts(product)), this::computeRealPrice);
    CompletableFuture <Integer> jingDong = CompletableFuture.supplyAsync(() -> HttpRequestMock.getJingDongPrice(product)).thenCombine(CompletableFuture.supplyAsync(() -> HttpRequestMock.getJingDongDiscounts(product)), this::computeRealPrice);
    CompletableFuture <Integer> pinDuoDuo = CompletableFuture.supplyAsync(() -> HttpRequestMock.getPinDuoDuoPrice(product)).thenCombine(CompletableFuture.supplyAsync(() -> HttpRequestMock.getPinDuoDuoDiscounts(product)), this::computeRealPrice);

    // 排序并获取最低价格
    return Stream.of(taoBao, jingDong, pinDuoDuo)
        .map(CompletableFuture::join)
        .min(Comparator.comparingInt(p - > p))
        .get();
}

看下执行结果符合预期,而接口耗时则降到了1s(因为依赖的每一个查询实际操作的接口耗时都是模拟的1s,所以这个结果已经算是此复合接口能达到的极限值了)。

15:29:04.911516600[ForkJoinPool.commonPool-worker-1]获取淘宝上iphone16的价格完成: 5199
15:29:04.911516600[ForkJoinPool.commonPool-worker-4]获取京东上iphone16的折扣价格完成: 150
15:29:04.911516600[ForkJoinPool.commonPool-worker-2]获取淘宝上iphone16的折扣价格完成: 200
15:29:04.911516600[ForkJoinPool.commonPool-worker-3]获取京东上iphone16的价格完成: 5299
15:29:04.911516600[ForkJoinPool.commonPool-worker-5]获取拼多多上iphone16的价格完成: 5399
15:29:04.911516600[ForkJoinPool.commonPool-worker-6]获取拼多多上iphone16的折扣价格完成: 99
15:29:04.924568[ForkJoinPool.commonPool-worker-2]计算实际价格完成: 4999
15:29:04.924568[ForkJoinPool.commonPool-worker-3]计算实际价格完成: 5149
15:29:04.924568[ForkJoinPool.commonPool-worker-6]计算实际价格完成: 5300
4999
耗时:1071ms

这里CompletableFuture执行时所使用的默认线程池是ForkJoinPool

Future与CompletableFuture

首先,先来理一下Future与CompletableFuture之间的关系。

Future

如果接触过多线程相关的概念,那Future应该不会陌生,早在Java5中就已经存在了。

该如何理解Future呢?举个生活中的例子:

你去咖啡店点了一杯咖啡,然后服务员会给你一个订单小票。 当服务员在后台制作咖啡的时候,你并没有在店里等待,而是出门到隔壁甜品店又买了个面包。 当面包买好之后,你回到咖啡店,拿着订单小票去取咖啡。 取到咖啡后,你边喝咖啡边把面包吃了……嗝~

是不是很熟悉的生活场景? 对比到我们多线程异步编程的场景中,咖啡店的订单小票其实就是Future,通过Future可以让稍后适当的时候可以获取到对应的异步执行线程中的执行结果。

上面的场景,我们翻译为代码实现逻辑:

public void buyCoffeeAndOthers() throws ExecutionException, InterruptedException {
    goShopping();
    // 子线程中去处理做咖啡这件事,返回future对象
    Future<Coffee> coffeeTicket = threadPool.submit(this::makeCoffee);
    // 主线程同步去做其他的事情
    Bread bread = buySomeBread();
    // 主线程其他事情并行处理完成,阻塞等待获取子线程执行结果
    Coffee coffee = coffeeTicket.get();
    // 子线程结果获取完成,主线程继续执行
    eatAndDrink(bread, coffee);
}

Future相关的了解可以看这篇文章:FutureTask是Future的基础实现

CompletableFuture

Future在应对一些简单且相互独立的异步执行场景很便捷,但是在一些复杂的场景,比如同时需要多个有依赖关系的异步独立处理的时候,或者是一些类似流水线的异步处理场景时,就显得力不从心了。比如:

  • 同时执行多个并行任务,等待最快的一个完成之后就可以继续往后处理
  • 多个异步任务,每个异步任务都需要依赖前一个异步任务执行的结果再去执行下一个异步任务,最后只需要一个最终的结果
  • 获取计算结果的 get() 方法为阻塞调用

Java 8 才被引入CompletableFuture 类可以解决Future 的这些缺陷。CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。

可以看到,CompletableFuture 同时实现了 FutureCompletionStage 接口。

CompletableFuture使用方式

创建CompletableFuture并执行

当需要进行异步处理的时候,可以通过CompletableFuture.supplyAsync方法,传入一个具体的要执行的处理逻辑函数,这样就轻松的完成了CompletableFuture的创建与触发执行。

方法名称作用描述
supplyAsync静态方法,用于构建一个CompletableFuture<T>对象,并异步执行传入的函数,允许执行函数有返回值T
runAsync静态方法,用于构建一个CompletableFuture<Void>对象,并异步执行传入函数,与supplyAsync的区别在于此方法传入的是Callable类型,仅执行,没有返回值

使用示例:

public void testCreateFuture(String product) {
    // supplyAsync, 执行逻辑有返回值Integer
    CompletableFuture<Integer> supplyAsyncResult =
            CompletableFuture.supplyAsync(() -> HttpRequestMock.getTaoBaoPrice(product));
    
    // runAsync, 执行逻辑没有返回值
    CompletableFuture<Void> runAsyncResult =
            CompletableFuture.runAsync(() -> System.out.println(product));
}

特别补充:

supplyAsync或者runAsync创建后便会立即执行,无需手动调用触发。

线程串行化方法

使用方法

在流水线处理场景中,往往都是一个任务环节处理完成后,下一个任务环节接着上一环节处理结果继续处理。CompletableFuture用于这种流水线环节驱动类的方法有很多,相互之间主要是在返回值或者给到下一环节的入参上有些许差异,使用时需要注意区分:

具体的方法的描述归纳如下:

方法名称作用描述
thenApplyCompletableFuture的执行后的具体结果进行追加处理,并将当前的CompletableFuture泛型对象更改为处理后新的对象类型,返回当前CompletableFuture对象。
thenComposethenApply类似。区别点在于:此方法的入参函数是一个CompletableFuture类型对象,适用于回调函数需要启动另一个异步计算,并且想要一个扁平化的结果CompletableFuture,而不是嵌套的CompletableFuture<CompletableFuture<U>>
thenAcceptthenApply方法类似,区别点在于thenAccept返回void类型,没有具体结果输出,适合无需返回值的场景。
thenRunthenAccept类似,区别点在于thenAccept可以将前面CompletableFuture执行的实际结果作为入参进行传入并使用,但是thenRun方法没有任何入参,只能执行一个Runnable函数,并且返回void类型

因为上述thenApplythenCompose方法的输出仍然都是一个CompletableFuture对象,所以各个方法是可以一环接一环的进行调用,形成流水线式的处理逻辑:

thenApply

上面任务执行完执行 + 能获取上步返回值 + 自己有返回值

@Test
public void thenApplyAsync() throws ExecutionException, InterruptedException {
    CompletableFuture<String> thenApplyAsync = CompletableFuture.supplyAsync(() -> {
        System.out.println("thenApplyAsync当前线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("thenApplyAsync运行结果:" + i);
        return i;
    }, executor).thenApply(result -> {
        System.out.println("thenApplyAsync任务2启动了。。。。。上步结果:" + result);
        return "hello" + result * 2;
    });
    System.out.println("main.................end....." + thenApplyAsync.get());
}

结果:

thenApplyAsync当前线程:33
thenApplyAsync运行结果:5
thenApplyAsync任务2启动了。。。。。上步结果:5
main.................end.....hello10
thenAccept

上面任务执行完执行 + 能获取上步返回值

@Test
public void thenAcceptAsync() throws ExecutionException, InterruptedException {
    CompletableFuture<Void> thenAcceptAsync = CompletableFuture.supplyAsync(() -> {
        System.out.println("thenAcceptAsync当前线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("thenAcceptAsync运行结果:" + i);
        return i;
    }, executor).thenAccept(result -> {
        System.out.println("thenAcceptAsync任务2启动了。。。。。上步结果:" + result);
    });
}

结果:

thenAcceptAsync当前线程:33
thenAcceptAsync运行结果:5
thenAcceptAsync任务2启动了。。。。。上步结果:5
thenRun

上面任务执行完执行

@Test
public void thenRunAsync() throws ExecutionException, InterruptedException {
    System.out.println("main.................start.....");
    CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("运行结果:" + i);
        return i;
    }, executor).thenRun(() -> {
        System.out.println("任务2启动了。。。。。");
    });
}

结果:

main.................start.....
当前线程:33
运行结果:5
任务2启动了。。。。。
thenCompose

接收返回值并生成新的任务

@Test
public void thenCompose() {
    CompletableFuture cf = CompletableFuture.completedFuture("hello")
            .thenCompose(str -> CompletableFuture.supplyAsync(() -> {
                return str + ": thenCompose";
            },executor));
    System.out.println(cf.join());
}
  • thenApply():转换的是泛型中的类型,相当于将CompletableFuture 转换生成新的CompletableFuture
  • thenCompose():用来连接两个CompletableFuture,是生成一个新的CompletableFuture。

串联示例

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
    System.out.println("supplyAsync first");
    return "first";
}, fixedThreadPool).thenApply(s -> {
    Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
    System.out.println("supplyAsync second");
    return "second " + s;
}).whenComplete((s, t) -> {//s,是上面的返回值,t是上面可能会抛出的Throwable对象
    if (t == null) {
        System.out.println("whenComplete succeed:" + s);
    } else {
        System.out.println("whenComplete error");
    }
});
        
System.out.println(future.get());

//结果:
supplyAsync first
supplyAsync second
whenComplete succeed:secondfirst
second first

线程并联方法

很多时候为了提升并行效率,一些没有依赖的环节我们会让他们同时去执行,然后在某些环节需要依赖的时候,进行结果的依赖合并处理,类似如下图的效果。

CompletableFuture相比于Future的一大优势,就是可以方便的实现多个并行环节的合并处理。相关涉及方法介绍归纳如下:

方法名称作用描述
thenCombine将两个CompletableFuture对象组合起来进行下一步处理,可以拿到两个执行结果,并传给自己的执行函数进行下一步处理,最后返回一个新的CompletableFuture对象。
thenAcceptBoththenCombine类似,区别点在于thenAcceptBoth传入的执行函数没有返回值,即thenAcceptBoth返回值为CompletableFuture<Void>
runAfterBoth等待两个CompletableFuture都执行完成后再执行某个Runnable对象,再执行下一个的逻辑,类似thenRun。
applyToEither两个CompletableFuture中任意一个完成的时候,继续执行后面给定的新的函数处理。再执行后面给定函数的逻辑,类似thenApply。
acceptEither两个CompletableFuture中任意一个完成的时候,继续执行后面给定的新的函数处理。再执行后面给定函数的逻辑,类似thenAccept。
runAfterEither等待两个CompletableFuture中任意一个执行完成后再执行某个Runnable对象,可以理解为thenRun的升级版,注意与runAfterBoth对比理解。
allOf静态方法,阻塞等待所有给定的CompletableFuture执行结束后,返回一个CompletableFuture<Void>结果。
anyOf静态方法,阻塞等待任意一个给定的CompletableFuture对象执行结束后,返回一个CompletableFuture<Void>结果。

使用方法

thenCombine

消费两个结果 + 返回结果

@Test
public void thenCombine() throws ExecutionException, InterruptedException {
    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executor);

    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        System.out.println("任务2运行结果:");
        return "hello";
    }, executor);
    
    CompletableFuture<String> thenCombineAsync = future1.thenCombine(future2, (result1, result2) -> {
        System.out.println("任务5启动。。。结果1:" + result1 + "。。。结果2:" + result2);
        return result2 + "-->" + result1;
    });
    System.out.println("任务5结果" + thenCombineAsync.get());
}

结果:

任务1线程:33
任务1运行结果:5
任务2线程:34
任务2运行结果:
任务5启动。。。结果1:5。。。结果2:hello
任务5结果hello-->5
thenAcceptBoth

消费两个结果 + 无返回

@Test
public void thenAcceptBothAsync() throws ExecutionException, InterruptedException {
    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executor);

    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        System.out.println("任务2运行结果:");
        return "hello";
    }, executor);

    CompletableFuture<Void> thenAcceptBothAsync = future1.thenAcceptBoth(future2, (result1, result2) -> {
        System.out.println("任务4启动。。。结果1:" + result1 + "。。。结果2:" + result2);
    });

}

结果

任务1线程:33
任务1运行结果:5
任务2线程:34
任务2运行结果:
任务4启动。。。结果1:5。。。结果2:hello
runAfterBoth

两个任务都完成后,再接着运行

@Test
public void runAfterBothAsync() {
    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executor);

    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        System.out.println("任务2运行结果:");
        return "hello";
    }, executor);

    CompletableFuture<Void> runAfterBothAsync = future1.runAfterBoth(future2, () -> {
        System.out.println("任务3启动。。。");
    });

}

结果

任务1线程:33
任务1运行结果:5
任务2线程:34
任务2运行结果:
任务3启动。。。
applyToEither

只要有一个执行完就执行 + 获取返回值 + 有返回值

@Test
public void applyToEither() throws ExecutionException, InterruptedException {
    CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(3000);
            System.out.println("任务1运行结果:" + i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i;
    }, executor);

    CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        System.out.println("任务2运行结果:");
        return "hello";
    }, executor);

    CompletableFuture<String> applyToEitherAsync = future1.applyToEither(future2, result -> {
        System.out.println("任务5开始执行。。。结果:" + result);
        return result.toString() + " world";
    });
    System.out.println("任务5结果:" + applyToEitherAsync.get());
}

结果

任务1线程:33
任务2线程:34
任务2运行结果:
任务5开始执行。。。结果:hello
任务5结果:hello world
acceptEither

只要有一个执行完就执行 + 获取返回值

@Test
public void acceptEither() {
    CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(3000);
            System.out.println("任务1运行结果:" + i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i;
    }, executor);

    CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        System.out.println("任务2运行结果:");
        return "hello";
    }, executor);

    CompletableFuture<Void> acceptEitherAsync = future1.acceptEither(future2, result -> {
        System.out.println("任务4开始执行。。。结果:" + result);
    });

}

结果

任务1线程:33
任务2线程:34
任务2运行结果:
任务4开始执行。。。结果:hello
runAfterEither

只要有一个执行完就执行

@Test
public void runAfterEither() {
    CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(3000);
            System.out.println("任务1运行结果:" + i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i;
    }, executor);

    CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        System.out.println("任务2运行结果:");
        return "hello";
    }, executor);

    CompletableFuture<Void> runAfterEitherAsync = future1.runAfterEither(future2, () -> {
        System.out.println("任务3开始执行。。。");
    });
}

结果

任务1线程:33
任务2线程:34
任务2运行结果:
任务3开始执行。。。
allOf

等待全部完成后才执行

@Test
public void allOf() throws ExecutionException, InterruptedException {
    CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1");
        return "任务1";
    }, executor);
    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("任务2");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "任务2";
    }, executor);
    CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务3");
        return "任务3";
    }, executor);

    CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2, future3);
    //等待所有任务完成
    //allOf.get();
    allOf.join();
    System.out.println("allOf" + future1.get() + "-------" + future2.get() + "-------" + future3.get());

}

结果

任务1
任务3
任务2
allOf任务1-------任务2-------任务3
anyOf

等待其中之一完成后就执行

@Test
public void anyOf() throws ExecutionException, InterruptedException {
    CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1");
        return "任务1";
    }, executor);
    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("任务2");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "任务2";
    }, executor);

    CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务3");
        return "任务3";
    }, executor);
    CompletableFuture<Object> anyOf = CompletableFuture.anyOf(future1, future2, future3);
    System.out.println("anyOf--最先完成的是" + anyOf.get());
    //等待future2打印
    System.out.println("等等任务2");
    Thread.sleep(3000);
}

结果

任务1
anyOf--最先完成的是任务1
任务3
等等任务2
任务2

并联示例

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
CompletableFuture<String> firstfuture = CompletableFuture.supplyAsync(() -> {
    Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
    System.out.println("supplyAsync first");
    return "first";
}, fixedThreadPool);
CompletableFuture<String> secondfuture = CompletableFuture.supplyAsync(() -> {
    Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
    System.out.println("supplyAsync second");
    return "second";
}, fixedThreadPool);
CompletableFuture<String> thirdfuture = CompletableFuture.supplyAsync(() -> {
    Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
    System.out.println("supplyAsync third");
    return "third";
}, fixedThreadPool);

CompletableFuture.allOf(firstfuture, secondfuture, thirdfuture)
       .whenComplete((aVoid, t) -> {
             try {
                  System.out.println("whenComplete succeed:" + firstfuture.get() + secondfuture.get() + thirdfuture.get());
              } catch (Exception e) {
                  System.out.println("error");
              }
        });

结果等待与获取

在执行线程中将任务放到工作线程中进行处理的时候,执行线程与工作线程之间是异步执行的模式,如果执行线程需要获取到共工作线程的执行结果,则可以通过get或者join方法,阻塞等待并从CompletableFuture中获取对应的值。

getjoin的方法功能含义说明归纳如下:

方法名称作用描述
get()等待CompletableFuture执行完成并获取其具体执行结果,可能会抛出异常,需要代码调用的地方手动try...catch进行处理。
get(long, TimeUnit)与get()相同,只是允许设定阻塞等待超时时间,如果等待超过设定时间,则会抛出异常终止阻塞等待。
join()等待CompletableFuture执行完成并获取其具体执行结果,可能会抛出运行时异常,无需代码调用的地方手动try...catch进行处理。

从介绍上可以看出,两者的区别就在于是否需要调用方显式的进行try...catch处理逻辑,使用代码示例如下:

public void testGetAndJoin(String product) {
    // join无需显式try...catch...
    PriceResult joinResult = CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouXiXiPrice(product))
            .join();
    
    try {
        // get显式try...catch...
        PriceResult getResult = CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouXiXiPrice(product))
                .get(5L, TimeUnit.SECONDS);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

异常处理

在编排流水线的时候,如果某一个环节执行抛出异常了,会导致整个流水线后续的环节就没法再继续下去了,比如下面的例子:

public void testExceptionHandle() {
    CompletableFuture.supplyAsync(() -> {
        throw new RuntimeException("supplyAsync excetion occurred...");
    }).thenApply(obj -> {
        System.out.println("thenApply executed...");
        return obj;
    }).join();
}

执行之后会发现,supplyAsync抛出异常后,后面的thenApply并没有被执行。

那如果想要让流水线的每个环节处理失败之后都能让流水线继续往下面环节处理,让后续环节可以拿到前面环节的结果或者是抛出的异常并进行对应的应对处理,就需要用到handlewhenCompletable方法了。

先看下两个方法的作用描述:

方法名称作用描述
handlethenApply类似,区别点在于handle执行函数的入参有两个,一个是CompletableFuture执行的实际结果,一个是Throwable对象,这样如果前面执行出现异常的时候,可以通过handle获取到异常并进行处理。
whenCompletehandle类似,区别点在于whenComplete执行后无返回值
exceptionally捕获异常并返回指定值

handle

入参为 结果 或者 异常,返回新结果

@Test
public void handle() throws ExecutionException, InterruptedException {
    System.out.println("main.................start.....");
    final CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 0;
        System.out.println("运行结果:" + i);
        return i;
    }, executor).handleAsync((in, throwable) -> {
        if (throwable != null) {
            return "报错返回";
        }
        return "正确了";
    });
    System.out.println("main.................end....." + completableFuture.get());

}

结果

main.................start.....
当前线程:33
main.................end.....报错返回

whenComplete

whenComplete虽然得到异常信息,但是不能修改返回信息

@Test
public void whenComplete() {
    System.out.println("main.................start.....");
    final CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 0;
        System.out.println("运行结果:" + i);
        return i;
    }, executor).whenComplete((result, throwable) -> {
        //whenComplete虽然得到异常信息,但是不能修改返回信息
        System.out.println("异步完成。。。。结果是:" + result + "...异常是:" + throwable);
    });

    try {
        System.out.println("main.................end..T..." + completableFuture.get());
    } catch (InterruptedException e) {
        System.out.println("报错了1");
    } catch (ExecutionException e) {
        System.out.println("报错了2");
    }
}

结果

main.................start.....
当前线程:33
异步完成。。。。结果是:null...异常是:java.util.concurrent.CompletionException: java.lang.ArithmeticException: 除以零
报错了2

exceptionally

@Test
public void exceptionally() throws ExecutionException, InterruptedException {
    System.out.println("main.................start.....");
    CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 0;
        System.out.println("运行结果:" + i);
        return i;
    }, executor).exceptionally(throwable -> {
        //R apply(T t);
        //exceptionally可以感知错误并返回指定值
        System.out.println("执行了exceptionally");
        return 0;
    });
    System.out.println("main.................end....." + completableFuture.get());
}

结果

main.................start.....
当前线程:33
执行了exceptionally
main.................end.....0

实现超时

由于网络波动或者连接节点下线等种种问题,对于大多数网络异步任务的执行常常会进行超时限制,在异步开发中可以看成是一个常见的问题。

在 Java 9 中,CompletableFuture 引入了支持超时和延迟执行的改进,这两个功能对于控制异步操作行为至关重要。

orTimeout()

允许为 CompletableFuture 设置一个超时时间。如果在指定的超时时间内未完成,CompletableFuture 将以 TimeoutException 完成

  • 示例
@Test
public void orTimeTest() {
    try {
        CompletableFuture completableFuture = CompletableFuture.runAsync(() - > {
            System.out.println("异步任务开始执行....");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).orTimeout(2, TimeUnit.SECONDS);

        completableFuture.join();
    } catch (Exception e) {
        System.out.println(e);
    }
}

completeOnTimeout()

允许在指定的超时时间内如果未完成,则用一个默认值来完成 CompletableFuture。该方法提供了一种优雅的回退机制,确保即使在超时的情况下也能保持异步流的连续性和完整性。

  • 示例
@Test
public void completeOnTimeoutTest() {
    CompletableFuture <String> completableFuture = CompletableFuture.supplyAsync(() - > {
        System.out.println("异步任务开始执行....");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "死磕 Java 新特性";
    }).completeOnTimeout("死磕 Java", 2, TimeUnit.SECONDS);

    System.out.println("执行结果为:" + completableFuture.join());
}

延迟执行

CompletableFuture 提供了delayedExecutor() 来支持延迟执行,该方法创建一个延迟执行的 Executor,可以将任务的执行推迟到未来某个时间点。能够让我们更加精确地控制异步任务的执行时机,特别是在需要根据时间安排任务执行的场景中。

  • 示例
@Test
public void completeOnTimeoutTest() {
    // 创建一个延迟执行的Executor
    Executor delayedExecutor = CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS);

    // 使用延迟的Executor执行一个简单任务
    CompletableFuture <Void> future = CompletableFuture.runAsync(() - > {
        System.out.println("任务延迟后执行...");
    }, delayedExecutor);

    // 等待异步任务完成
    future.join();
}

CompletableFuture的Async版本

在使用CompletableFuture的时候会发现,有很多的方法,都会同时有两个以Async命名结尾的方法版本。以thenCombine方法为例:

  1. thenCombine(CompletionStage, BiFunction)
  2. thenCombineAsync(CompletionStage, BiFunction)
  3. thenCombineAsync(CompletionStage, BiFunction, Executor)

从参数上看,区别并不大,仅第三个方法入参中多了线程池Executor对象。看下三个方法的源码实现,会发现其整体实现逻辑都是一致的,仅仅是使用线程池这个地方的逻辑有一点点的差异:

有兴趣的可以去翻一下此部分的源码实现,这里概括下三者的区别:

  1. thenCombine方法,沿用上一个执行任务所使用的线程池进行处理
  2. thenCombineAsync两个入参的方法,使用默认的ForkJoinPool线程池中的工作线程进行处理
  3. themCombineAsync三个入参的方法,支持自定义线程池并指定使用自定义线程池中的线程作为工作线程去处理待执行任务。

为了更好的理解下上述的三个差异点,通过下面的代码来演示下:

  • 用法1:其中thenCombineAsync指定使用自定义线程池,supplyAsync方法不指定线程池(使用默认线程池)
public PriceResult getCheapestPlatAndPrice4(String product) {
    // 构造自定义线程池
    ExecutorService executor = Executors.newFixedThreadPool(5);
    
    return
        CompletableFuture.supplyAsync(
            () -> HttpRequestMock.getPinDuoDuoPrice(product)
        ).thenCombineAsync(
            CompletableFuture.supplyAsync(() -> HttpRequestMock.getPinDuoDuoDiscounts(product)),
            this::computeRealPrice,
            executor
        ).join();
}

没有指定自定义线程池的supplyAsync方法,其使用了默认的ForkJoinPool工作线程来运行,而指定了自定义线程池的方法,则使用了自定义线程池来执行。

17:23:50.683636700[ForkJoinPool.commonPool-worker-1]获取拼多多上iphone16的价格完成: 5399
17:23:50.683636700[ForkJoinPool.commonPool-worker-2]获取拼多多上iphone16的折扣价格完成: 99
17:23:50.696637100[pool-2-thread-1]计算实际价格完成: 5300
5300
耗时:1079ms
  • 用法2: 不指定自定义线程池,使用默认线程池策略,使用thenCombine方法
public PriceResult getCheapestPlatAndPrice5(String product) {
    return
        CompletableFuture.supplyAsync(
            () -> HttpRequestMock.getPinDuoDuoPrice(product)
        ).thenCombine(
            CompletableFuture.supplyAsync(() -> HttpRequestMock.getPinDuoDuoDiscounts(product)),
            this::computeRealPrice
        ).join();
}

执行结果如下,可以看到执行线程名称与用法1示例相比发生了变化。因为没有指定线程池,所以两个supplyAsync方法都是用的默认的ForkJoinPool线程池,而thenCombine使用的是上一个任务所使用的线程池,所以也是用的ForkJoinPool

17:24:53.840945700[ForkJoinPool.commonPool-worker-2]获取拼多多上iphone16的折扣价格完成: 99
17:24:53.840945700[ForkJoinPool.commonPool-worker-1]获取拼多多上iphone16的价格完成: 5399
17:24:53.850944100[ForkJoinPool.commonPool-worker-1]计算实际价格完成: 5300
5300
耗时:1083ms

现在,我们知道了方法名称带有Async和不带Async的实现策略上的差异点就在于使用哪个线程池来执行而已。那么,对我们实际的指导意义是啥呢?实际使用的时候,应该怎么判断自己应该使用带Async结尾的方法、还是不带Async结尾的方法呢?

上面是Async结尾方法默认使用的ForkJoinPool创建的逻辑,这里可以看出,默认的线程池中的工作线程数是CPU核数 - 1,并且指定了默认的丢弃策略等,这就是一个主要关键点。所以说,符合以下几个条件的时候,可以考虑使用带有Async后缀的方法,指定自定义线程池:

  • 默认线程池的线程数满足不了实际诉求
  • 默认线程池的类型不符合自己业务诉求
  • 默认线程池的队列满处理策略不满足自己诉求

使用注意点

与Stream结合

在涉及批量进行并行处理的时候,通过StreamCompletableFuture结合使用,可以简化很多编码逻辑。但是在使用细节方面需要注意下,避免达不到使用CompletableFuture的预期效果。

需求场景: 在同一个平台内,传入多个商品,查询不同商品对应的价格与优惠信息,并选出实付价格最低的商品信息。

结合前面的介绍分析,我们应该知道最佳的方式,就是同时并行的方式去各自请求数据,最后合并处理即可。所以我们规划按照如下的策略来实现:

先看第一种编码实现:

public int comparePriceInOnePlat(List <String> products) {
    return products.stream()
        .map(product -> CompletableFuture.supplyAsync(() -> HttpRequestMock.getTaoBaoPrice(product))
            .thenCombine(CompletableFuture.supplyAsync(() -> HttpRequestMock.getTaoBaoDiscounts(product)),
                this::computeRealPrice))
        .map(CompletableFuture::join)
        .min(Comparator.comparingInt(p -> p))
        .get();
}

对于List的处理场景,这里采用了Stream方式来进行遍历与结果的收集、排序与返回。看似正常,但是执行的时候会发现,并没有达到我们预期的效果:

16:59:22.384338900[ForkJoinPool.commonPool-worker-2]获取淘宝上iphone16的折扣价格完成: 200
16:59:22.384338900[ForkJoinPool.commonPool-worker-1]获取淘宝上iphone16的价格完成: 5199
16:59:22.396881[ForkJoinPool.commonPool-worker-1]计算实际价格完成: 4999
16:59:23.404683800[ForkJoinPool.commonPool-worker-2]获取淘宝上iphone17的折扣价格完成: 200
16:59:23.404683800[ForkJoinPool.commonPool-worker-1]获取淘宝上iphone17的价格完成: 5199
16:59:23.404683800[ForkJoinPool.commonPool-worker-1]计算实际价格完成: 4999
16:59:24.416418500[ForkJoinPool.commonPool-worker-2]获取淘宝上iphone18的折扣价格完成: 200
16:59:24.417266700[ForkJoinPool.commonPool-worker-1]获取淘宝上iphone18的价格完成: 5199
16:59:24.417266700[ForkJoinPool.commonPool-worker-1]计算实际价格完成: 4999
4999
耗时:3116ms

从上述执行结果可以看出,其具体处理的时候,其实是按照下面的逻辑去处理了:

为什么会出现这种实际与预期的差异呢?原因就在于使用的Stream上面!虽然Stream中使用两个map方法,但Stream处理的时候并不会分别遍历两遍,其实写法等同于下面这种写到1个map中处理,改为下面这种写法,其实也就更容易明白为啥会没有达到我们预期的整体并行效果了:

public int comparePriceInOnePlat1(List < String > products) {
    return products.stream()
        .map(product -> CompletableFuture.supplyAsync(() -> HttpRequestMock.getTaoBaoPrice(product))
            .thenCombine(CompletableFuture.supplyAsync(() -> HttpRequestMock.getTaoBaoDiscounts(product)), this::computeRealPrice).join())
        .min(Comparator.comparingInt(p -> p))
        .get();
}

既然如此,这种场景是不是就不能使用Stream了呢?也不是,其实拆开成两个Stream分步操作下其实就可以了。

再看下面的第二种实现代码:

public int comparePriceInOnePlat2(List < String > products) {
    // 先触发各自平台的并行处理
    List <CompletableFuture <Integer>> completableFutures = products.stream()
        .map(product -> CompletableFuture.supplyAsync(() -> HttpRequestMock.getTaoBaoPrice(product))
            .thenCombine(CompletableFuture.supplyAsync(() -> HttpRequestMock.getTaoBaoDiscounts(product)), this::computeRealPrice))
        .collect(Collectors.toList());
    // 在独立的流中,等待所有并行处理结束,做最终结果处理
    return completableFutures.stream()
        .map(CompletableFuture::join)
        .min(Comparator.comparingInt(p -> p))
        .get();
}

执行结果:

17:08:00.052684200[ForkJoinPool.commonPool-worker-2]获取淘宝上iphone16的折扣价格完成: 200
17:08:00.051681700[ForkJoinPool.commonPool-worker-5]获取淘宝上iphone18的价格完成: 5199
17:08:00.051681700[ForkJoinPool.commonPool-worker-6]获取淘宝上iphone18的折扣价格完成: 200
17:08:00.052684200[ForkJoinPool.commonPool-worker-3]获取淘宝上iphone17的价格完成: 5199
17:08:00.051681700[ForkJoinPool.commonPool-worker-1]获取淘宝上iphone16的价格完成: 5199
17:08:00.051681700[ForkJoinPool.commonPool-worker-4]获取淘宝上iphone17的折扣价格完成: 200
17:08:00.064680500[ForkJoinPool.commonPool-worker-4]计算实际价格完成: 4999
17:08:00.064680500[ForkJoinPool.commonPool-worker-1]计算实际价格完成: 4999
17:08:00.063680100[ForkJoinPool.commonPool-worker-6]计算实际价格完成: 4999
4999
耗时:1083ms

从执行结果可以看出,三个商品并行处理,整体处理耗时相比前面编码方式有很大提升,达到了预期的效果。

归纳下:因为Stream的操作具有惰性执行的特点,且只有遇到终止操作(比如collect方法)的时候才会真正的执行。所以遇到这种需要并行处理且需要合并多个并行处理流程的情况下,需要将并行流程与合并逻辑放到两个Stream中,这样分别触发完成各自的处理逻辑,就可以了。

使用自定义线程池

CompletableFuture 默认使用ForkJoinPool.commonPool() 作为执行器,这个线程池是全局共享的,可能会被其他任务占用,导致性能下降或者饥饿。因此,建议使用自定义的线程池来执行 CompletableFuture 的异步任务,可以提高并发度和灵活性。

private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>());

CompletableFuture.runAsync(() -> {
     //...
}, executor);

尽量避免使用get()

CompletableFutureget()方法是阻塞的,尽量避免使用。如果必须要使用的话,需要添加超时时间,否则可能会导致主线程一直等待,无法执行其他任务。

    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Hello, world!";
    });

    // 获取异步任务的返回值,设置超时时间为 5 秒
    try {
        String result = future.get(5, TimeUnit.SECONDS);
        System.out.println(result);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        // 处理异常
        e.printStackTrace();
    }
}