如何构建 AI 智能体(2025 完全指南)

🎯 内容提要
AI 智能体是能够自主决策并采取行动以完成任务的系统。与聊天机器人不同,它们不遵循预定义的工作流程——它们会进行推理、规划、使用工具并动态适应。本指南将通过真实示例和代码,向你具体展示如何使用如 LangChain 和 AutoGen 等现代框架来构建可工作的智能体。

2025 年正被誉为"AI 智能体之年",其在企业中的应用正在加速。微软 CEO 萨提亚·纳德拉称其为一根本性转变:"请将智能体视为 AI 时代的应用。"但问题在于——大多数教程向你展示的都是伪装成智能体的聊天机器人,或者更糟的是,那些在演示中有效但在生产环境中失败的复杂系统。
在构建了多个生产级智能体并分析了最新框架之后,我将确切地向你展示如何创建真正有效的 AI 智能体。不掺水分,不搞噱头——只有由真实代码和经过验证的架构支持的实践实现细节。

AI 智能体与聊天机器人有何不同?

让我们立刻澄清这一点。智能体没有预定义的工作流程——它不仅仅是遵循第一步、第二步、第三步。相反,它会在不确定的步骤数量中动态做出决策,并根据需要进行调整。

特性 传统聊天机器人 AI 智能体
决策能力 遵循预定义规则 自主决策
工作流程 固定的、线性的步骤 动态的、自适应的规划
记忆 仅限于会话 跨任务持久化
工具使用 无或硬编码 动态选择和使用工具
错误处理 失败或请求帮助 尝试替代方法

真实示例: 要求一个聊天机器人"预订下周二飞往纽约的航班",它要么会失败,要么会向你询问更多信息。而一个智能体会检查你的日历、搜索航班、比较价格,甚至处理预订——根据发现的情况调整其方法。

每个 AI 智能体所需的 5 个核心组件

基于广泛的研究和生产部署,每个可工作的 AI 智能体都需要以下五个组件:

1. 大语言模型 – 大脑

LLM 充当推理引擎。在 2025 年,你有多种优秀选择(参见我们的详细比较):

  • Claude 4 Opus: 最适合复杂推理和扩展思考
  • GPT-4.1: 在编码和工具使用方面表现出色,拥有 100 万令牌上下文
  • Gemini 2.5 Pro: 强大的多模态能力

💡 专业提示: 不要默认使用最昂贵的模型。对于智能体任务,每百万令牌 2 美元的 GPT-4.1-mini 通常表现不俗,尤其是在结合良好提示的情况下。

2. 记忆系统 – 上下文

由于 LLM 默认是无状态的,你需要管理它们的历史和上下文。现代框架提供几种记忆类型:

# 示例:LangChain 记忆实现
from langchain.memory import ConversationSummaryBufferMemory

memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=2000,
    return_messages=True
)

# 智能体现在可以跨多次交互进行记忆

记忆类型:

  • 缓冲区记忆: 存储原始对话历史
  • 摘要记忆: 压缩长对话
  • 实体记忆: 跟踪特定实体及其属性
  • 知识图谱记忆: 构建概念间的关系

3. 工具 – 双手

工具允许你的智能体与外部世界交互。正确的工具配置与提示工程同等重要。

# 示例:定义一个用于网络搜索的工具
from langchain.tools import Tool

def search_web(query: str) -> str:
    """搜索网络以获取最新信息。"""
    # 此处为实现代码
    return search_results

web_search_tool = Tool(
    name="WebSearch",
    func=search_web,
    description="搜索网络以获取最新信息。当你需要最新数据时使用。"
)

⚠️ 关键点: 你的工具描述直接影响智能体性能。要具体说明何时以及如何使用每个工具。模糊的描述会导致工具选择不当。

4. 规划系统 – 策略

智能体必须能够提前规划和思考。2025 年最成功的方法是 ReAct 范式(推理 + 行动)

# ReAct 风格智能体循环
while not task_complete:
    # 1. 观察当前状态
    observation = get_current_state()

    # 2. 思考下一步行动
    thought = llm.think(f"给定 {observation},我下一步该做什么?")

    # 3. 决定行动
    action = llm.decide_action(thought, available_tools)

    # 4. 执行行动
    result = execute_action(action)

    # 5. 反思结果
    reflection = llm.reflect(result)

    # 更新状态并继续

5. 执行循环 – 引擎

执行循环负责协调一切。现代框架以不同方式处理此问题:

  • LangChain/LangGraph: 使用基于图的执行模型
  • AutoGen: 实现事件驱动的参与者模型
  • CrewAI: 专注于基于角色的智能体协作

逐步指南:构建你的第一个可工作智能体

让我们构建一个能够研究主题并撰写报告的实用智能体。此示例展示了所有五个核心组件的实际运作。

步骤 1:设置环境

# 安装所需的包
pip install langchain langchain-openai tavily-python

# 设置环境变量
export OPENAI_API_KEY="你的密钥"
export TAVILY_API_KEY="你的密钥"

步骤 2:初始化核心组件

from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent, AgentExecutor
from langchain.memory import ConversationBufferMemory
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain.tools import Tool
from langchain import hub

# 1. 初始化 LLM
llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)

# 2. 设置记忆
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True
)

# 3. 配置工具
search = TavilySearchResults(max_results=5)
tools = [
    Tool(
        name="Search",
        func=search.run,
        description="搜索关于任何主题的最新信息。返回相关结果。"
    )
]

# 4. 加载 ReAct 提示(处理规划)
prompt = hub.pull("hwchase17/react")

步骤 3:创建智能体

# 创建 ReAct 智能体
agent = create_react_agent(
    llm=llm,
    tools=tools,
    prompt=prompt
)

# 5. 设置执行循环
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    memory=memory,
    verbose=True,  # 查看智能体的思考过程
    handle_parsing_errors=True,
    max_iterations=10  # 防止无限循环
)

步骤 4:运行你的智能体

# 示例:研究并报告 AI 智能体
result = agent_executor.invoke({
    "input": "研究 2025 年 7 月 AI 智能体的最新发展,并撰写一份简要报告,重点介绍前 3 大趋势。"
})

print(result["output"])

成功关键: 该智能体将自主搜索多次,综合信息,并生成连贯的报告。它并非遵循脚本——而是根据发现的内容动态决定搜索什么。

真正提升性能的高级技巧

在分析了数千次智能体交互后,以下是真正能提高智能体性能的技巧:

1. 问题分解优于角色扮演

  • 无效的方法: 角色提示(例如,"你是一位专家研究员……")对准确性影响甚微或没有影响。
  • 有效的方法: 要求智能体将问题分解为子任务:
    decomposition_prompt = """
    将此任务分解为更小的步骤:
    1. 首先,识别关键组成部分
    2. 然后,分别处理每个组成部分
    3. 最后,综合结果

    任务:{task}
    """
 ```

### 2. 自我批评与反思

添加自我批评步骤能显著提高输出质量:
```python
reflection_prompt = """
审查你之前的回应并识别:
1. 任何逻辑错误或不一致之处
2. 遗漏的重要信息
3. 可以更清晰的领域

之前的回应:{response}
"""

3. 上下文重于指令

上下文的重要性被严重低估。仅仅提供更多相关的背景信息,比复杂的提示技术更能提高性能:

# 效果较差
prompt = "写一份关于 AI 智能体的报告"

# 效果更好
prompt = """写一份关于 AI 智能体的报告。

上下文:AI 智能体是可以规划并执行任务的自主系统。
它们与聊天机器人的不同之处在于做出动态决策而非遵循脚本。
关键框架包括 LangChain、AutoGen 和 CrewAI。
该报告面向熟悉 AI 概念的技术读者。
"""

导致 AI 智能体失效的常见错误

以下是我反复看到的常见错误:

1. 无限循环且无限制

  • 始终设置 max_iterations: 智能体可能陷入循环。设置合理的限制并实现超时处理。

2. 工具描述不清

# 差:描述模糊
Tool(name="search", description="搜索东西")

# 好:包含用例的具体描述
Tool(
    name="WebSearch",
    description="搜索网络以获取最新信息。用于:近期新闻、时事、事实数据、公司信息。返回 5 个最相关的结果。"
)

3. 忽略错误状态

智能体会遇到错误。要为它们做好计划:

try:
    result = agent_executor.invoke({"input": user_query})
except Exception as e:
    # 不要只是失败 - 帮助智能体恢复
    recovery_prompt = f"先前的操作因错误而失败:{e}。请尝试另一种方法。"
    result = agent_executor.invoke({"input": recovery_prompt})

4. 忽视令牌成本

智能体可能快速消耗令牌。需监控并优化:

  • 尽可能使用较小的模型(GPT-4.1-mini vs GPT-4.1)
  • 对长对话实施摘要记忆
  • 缓存工具结果以避免重复调用

生产就绪的智能体架构

对于生产系统,根据你的需求选择架构:

框架 最适合 架构 关键优势
LangChain + LangGraph 复杂的单一智能体 基于图的执行 模块化,工具丰富
AutoGen 多智能体系统 事件驱动的参与者 智能体协作
CrewAI 基于团队的工作流 基于角色的智能体 自然的团队动态
自定义 特定需求 你的选择 完全控制

LangChain + LangGraph 架构

LangChain 已发展成为单智能体系统的事实标准。2025 年 LangGraph 的加入带来了复杂的状态管理:

from langgraph.graph import StateGraph, State
from typing import TypedDict

class AgentState(TypedDict):
    messages: list
    current_task: str
    completed_tasks: list

# 定义图
workflow = StateGraph(AgentState)

# 为不同的智能体能力添加节点
workflow.add_node("researcher", research_node)
workflow.add_node("writer", writing_node)
workflow.add_node("reviewer", review_node)

# 定义流程
workflow.add_edge("researcher", "writer")
workflow.add_edge("writer", "reviewer")

AutoGen 多智能体架构

微软的 AutoGen 在你需要多个专业智能体协同工作时表现出色:

import autogen

# 定义专业智能体
researcher = autogen.AssistantAgent(
    name="Researcher",
    system_message="你是一名研究专家。查找并验证信息。"
)

writer = autogen.AssistantAgent(
    name="Writer",
    system_message="你是一名技术文档工程师。创建清晰、准确的内容。"
)

critic = autogen.AssistantAgent(
    name="Critic",
    system_message="你审查工作的准确性和清晰度。要有建设性但要彻底。"
)

可工作的 AI 智能体真实案例

让我们看看当今在生产环境中实际使用的智能体(查看更多真实可工作的 AI 智能体示例):

1. 客户服务智能体(电子商务)

该智能体自主处理完整的客户交互(在我们的客户服务自动化指南中了解更多):

  • 在数据库中检查订单状态
  • 处理退货和退款
  • 更新送货地址
  • 将复杂问题升级给人工处理
  • 关键创新: 根据客户需求动态选择使用多个专业工具(数据库查询、支付处理、运输 API)。

2. 代码审查智能体(软件开发)

自动审查拉取请求:

  • 分析代码变更
  • 运行安全扫描
  • 提出改进建议
  • 检查是否符合编码标准

3. 研究助手智能体(内容创作)

进行综合研究:

  • 搜索多个来源
  • 事实核查信息
  • 综合发现
  • 生成引用

AI 智能体的安全考量

⚠️ 关键警告: 基于智能体的 AI 系统比聊天机器人更容易受到攻击。随着智能体开始预订航班、发送邮件和执行代码,风险呈指数级增长。

基本安全措施

  • 工具权限: 为每个工具实施细粒度权限
  • 操作验证: 对不可逆操作要求确认
  • 提示注入防御: 验证并清理所有输入
  • 审计日志: 记录每个操作以确保可追溯性
  • 人工监督: 维持紧急停止开关和审批工作流
# 示例:安全的工具执行
def execute_with_permission(action, requires_approval=True):
    if requires_approval and action.risk_level == "high":
        approval = request_human_approval(action)
        if not approval:
            return "操作被安全策略拒绝"

    # 记录操作
    audit_log.record(action, user, timestamp)

    # 带超时执行
    return execute_with_timeout(action, timeout=30)

测试和调试 AI 智能体

测试智能体需要不同于传统软件的方法:

1. 基于场景的测试

# 测试各种场景
test_scenarios = [
    {
        "input": "预订明天飞往纽约的航班",
        "expected_tools": ["calendar_check", "flight_search", "price_compare"],
        "expected_outcome": "flight_options"
    },
    {
        "input": "取消我的订阅并退还上个月的费用",
        "expected_tools": ["account_lookup", "subscription_cancel", "refund_process"],
        "expected_outcome": "confirmation"
    }
]

for scenario in test_scenarios:
    result = agent_executor.invoke({"input": scenario["input"]})
    assert all(tool in result["tool_calls"] for tool in scenario["expected_tools"])

2. 调试工具

启用详细日志记录以查看智能体的决策过程:

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,  # 显示思考过程
    return_intermediate_steps=True  # 返回所有步骤
)

10 个即用型智能体系统提示

以下是为常见智能体类型准备的、经过实战检验的系统提示:

1. 研究智能体

你是一个可以访问网络搜索和文档分析工具的研究智能体。

对于每个研究任务:

  1. 将主题分解为关键问题
  2. 从多个来源搜索信息
  3. 通过交叉引用验证事实
  4. 将发现综合成连贯的摘要
  5. 为所有主张包含引用

始终优先考虑近期信息和权威来源。

2. 客户支持智能体

你是一个客户支持智能体,帮助用户处理他们的账户和订单。

可用工具:order_lookup, refund_process, ticket_create, knowledge_base_search

指南:

  • 在访问账户信息前始终验证客户身份
  • 在升级前搜索知识库
  • 要有同理心并以解决方案为导向
  • 遇到以下情况升级至人工支持:法律问题、威胁或超出你工具范围的请求

切勿对你无法直接实现的功能做出承诺。

3. 数据分析智能体

你是一个专长于商业智能的数据分析智能体。

对于每个分析请求:

  1. 澄清业务问题
  2. 识别相关数据源
  3. 使用适当的统计方法执行分析
  4. 可视化关键发现
  5. 提供可操作的建议

始终在你的分析中注明数据局限性和置信水平。

4. 代码助手智能体

你是一个可以访问文件系统和执行工具的代码助手智能体。

能力:

  • 阅读和分析代码
  • 提出改进建议
  • 实施更改
  • 运行测试
  • 调试问题

切勿:

  • 未经明确许可删除文件
  • 修改系统文件
  • 执行可能有害的命令
  • 在代码中存储凭证

在进行重大更改前始终创建备份。

5. 内容创作智能体

你是一个专注于病毒式内容策略的内容创作智能体。

流程:

  1. 研究指定领域的趋势话题
  2. 分析成功的内容模式
  3. 生成多个内容创意
  4. 创建带有吸引点的详细内容
  5. 建议分发策略

关注真实性和价值,而非点击诱饵。

未来:AI 智能体的下一步是什么?

基于当前轨迹和内部知识,以下是将要发生的事情:

近期(未来 6 个月)

  • 视觉智能体: 能够查看并与 UI 交互的智能体
  • 语音优先智能体: 自然对话取代文本界面
  • 智能体市场: 针对特定行业的预构建智能体
  • 改进的安全性: 内置沙盒和权限系统

中期(2026 年)

  • 物理世界智能体: 与机器人技术集成
  • 监管框架: 为智能体行为设定法律边界
  • 智能体间经济: 智能体雇佣其他智能体
  • 个人 AI 操作系统: 管理整个数字生活的智能体

关键要点

构建真正有效的 AI 智能体需要理解五个核心组件:用于推理的 LLM、用于上下文的记忆、用于行动的工具、用于策略的规划以及一个健壮的执行循环。与聊天机器人的关键区别在于自主决策和动态适应。
从经过验证的框架开始,如用于单智能体的 LangChain 或用于多智能体系统的 AutoGen。专注于清晰的工具描述、适当的错误处理和全面的测试。最重要的是,记住上下文和问题分解比复杂的提示技巧更重要。
AI 智能体革命才刚刚开始。虽然炒作是真实的,但机遇也是真实的。通过遵循本指南并避免常见陷阱,你今天就可以构建出能够交付真正价值的智能体,同时为即将到来的自主未来做好准备。


【注】本文译自:I Agents (Complete 2025 Guide) – Superprompt.com

Java智能体框架的繁荣是一种代码异味

停止构建编排框架,开始构建智能体。未来属于那些掌握生态系统的人,而不是那些被困在构建特定语言引擎中的人。

我需要坦白。我是一个框架狂热者。我的职业生涯建立在 Apache Camel 之上,我人生中的大部分成功都归功于企业集成模式的优雅。我懂。如果有一个社区值得获得诺贝尔框架奖,那就是 Java 社区。从早年在红帽公司到整个大数据生态系统,框架 15 年来一直是 JVM 世界的引擎。我们是抽象的大师。

因此,当智能体时代来临而 Java 在奋力追赶时,我的第一本能是原始的:构建一个框架。我甚至开始了一个,驱动力是这样一个想法:"AI 智能体的 Apache Camel 在哪里?"

三个月前,可能只有一个严肃的 Java 智能体框架。现在,包括 Embabel 在内,已经有了三个。竞赛开始了。但目睹这场爆炸式增长,我不得不提出一个难题:框架本身是否正在成为一种反模式?我们是否在为自己创造负担,而不是专注于真正重要的事情:构建智能体

最近 Java 智能体框架的繁荣并非一个健康、成熟生态系统的标志。它是一种症状。一种架构层面的代码坏味道,告诉我们我们的方法存在根本性问题。

我们最初为什么要构建框架?

让我们回顾一下。为什么像 Spring 和 Camel 这样的框架变得如此主流?原因清晰且合理:

  • 开发人员生产力: 我们当时淹没在样板代码中。框架将其抽象掉了。
  • 代码质量与治理: 它们提供了标准化的模式,防止每个开发人员重新发明轮子。
  • 可重用性: 它们为我们提供了经过实战检验的构造来构建,节省了大量的时间和精力。

目标是优化生产力、质量和治理。但这些是我们今天应该优化的相同参数吗?感觉我们像是在用 2010 年的方法解决 2025 年的问题,完全忽视了房间里的大象:AI 驱动的开发工具

这头大象有个名字:Cursor(及其伙伴)

在我们忙于将 LangChain 移植到 Java 时,情况发生了变化:

Cursor 和 Copilot 生成样板代码的速度比你输入 import 语句还快。你正在构建的那个复杂的链式抽象?Cursor 三秒钟就能写出来。你正在标准化的那个工具注册模式?Copilot 已经知道五种变体。

但在这里,我们需要停下来问一个更根本的问题:你的最终用户实际需要什么?

你真正需要构建什么?

让我们具体点。我们大多数人面临两种情况:

  • 场景 1: 你在未来 12 个月内构建一个关键智能体。也许它是一个每天处理 10,000 次对话的客户服务智能体。或者一个需要理解你公司特定标准的代码审查智能体。或者一个绝不能对监管要求产生幻觉的合规智能体。
  • 场景 2: 你正在构建一个智能体平台。成百上千个智能体,每个都有不同的上下文、不同的领域、不同的需求。也许你在一家咨询公司,为多个客户构建智能体。或者你正在创建一个内部平台,不同团队可以在上面启动自己的智能体。你需要可重用、适应性强、可演进的东西。一种能让你快速创建新智能体,同时保持所有智能体一致性和质量的东西。

在这两种情况下,诚实地问自己:你的用户需要一个代码框架吗?

还是他们需要完全不同的东西?

重新定义框架

在放弃我的框架并实际交付智能体之后,我学到了:我们不需要消除框架。我们需要重新定义在 AI 时代框架实际意味着什么。

  • 旧框架定义: 一种可重用的代码抽象,提供结构并处理横切关注点。你导入并在其之上构建的东西。
  • 新框架定义: 构建智能体的完整环境,一组协同工作的相互依赖的层,其中代码层只是更大拼图的一部分。

以下是现代智能体框架中真正重要的层次:

第 1 层:语言本身

Java(或你选择的语言)及其构造、类型和模式。不包装在抽象中,直接使用。语言已经是你的逻辑结构框架。你不需要在 Java 之上再加一个代码框架。Java 本身就是框架。

第 2 层:模型

一个真正好的大语言模型:GPT-5、Claude、Gemini、Grok。这不仅仅是你调用的 API。它是你技术栈的核心组件。模型的能力直接决定了你能构建什么。像选择编程语言一样仔细地选择它。

第 3 层:开发人员生产力工具

Cursor、Copilot 以及下一代 AI 驱动的开发工具。这些不是可选的。它们是关键基础设施。你的框架应设计成与这些工具无缝协作。如果 Cursor 不能轻松地按照你的模式生成代码,那么你的模式是错误的,或者你可能需要很好地描述你的模式。

第 4 层:提示词包与指南

你经过版本控制、测试、治理的提示词。你的组织语音。你的领域知识。你的合规规则。这是你的业务逻辑存在的地方——不在代码中,而在精心策划的上下文和指令中。将这些视为你的依赖构件,就像 JAR 包,但用于智能体行为。

第 5 层:生态系统 API

对新兴的专业化平台及其 API 的上下文感知。用于知识检索的向量数据库。上下文存储和内存管理系统,如 Zep 或 Cognee。工具执行平台,如 Arcade。用于智能体监控的可观测性平台,如 Langfuse。提示词管理和版本控制工具。这些大多暴露 REST API 或标准协议,大多提供 LLM.txt 用于上下文导入。你的框架需要知道这些存在,并知道如何连接到它们。

第 6 层:架构与设计模式

作为指南和模式捕获的架构思维。关于这些层如何在不同用例中组合在一起的可重用蓝图。不是代码抽象——关于路由逻辑、版本控制策略、部署模式和生态系统集成的文档化知识,这些知识成为你组织上下文的一部分。

想想看。当你构建那个关键的客户服务智能体时,真正决定其成功的是什么?

  • 调用 LLM 的 Java 代码吗?(那是 20 行代码,Cursor 写的)
  • 复杂的链式编排吗?(标准控制流)
  • 重试逻辑和错误处理吗?(Java 已经有这方面的模式)

还是:

  • 选择的模型以及它处理你领域的能力
  • 教导它你的升级规则和语气的提示词
  • 让你能快速迭代这些提示词的工具
  • 与像 Arcade(工具)和 Zep(内存)这样的平台的集成
  • 让你能够对变更进行版本控制、测试和部署的架构
  • 让你能在多个智能体中重用这种方法的设计模式

那就是你的框架。所有六层,协同工作。

实践中的框架

让我向你展示在构建智能体时的实际示例:

第 4 层(提示词包) 是版本化的构件,不是你代码中的字符串:

prompts/
  customer-service/
    v1.2/
      system-prompt.md
      escalation-rules.md
      tone-guidelines.md
      product-context.md
      examples/
        refund-scenarios.yaml
        technical-issues.yaml

第 5 层(生态系统 API) 配置在你的环境中:
你的生态系统上下文嵌入在指南中:

# 生态系统集成指南

## 工具发现
- 调用 Arcade API 列出可用工具: GET /tools
- 参考: 查看 Arcade LLM.txt 位于 https://docs.arcade.dev/llms.txt

## 内存管理
- Zep 会话 API: https://api.getzep.com/v2/sessions/{session_id}
- 参考: 查看 Zep API 文档位于 https://docs.getzep.com

## 基础设施与存储
- 用于提示词构件的对象存储: S3, GCS, 或 Azure Blob
- 用于长时间运行工作流的状态持久化

第 1 层(Java) 提供结构,干净简单:

public class CustomerServiceAgent {
    private final Model model;
    private final PromptPack prompts;
    private final ArcadeClient tools;
    private final ZepMemory memory;

    public Response handle(CustomerQuery query) {
        // 检索会话内存
        var history = memory.getSession(query.sessionId());

        // 从 Arcade 获取可用工具
        var availableTools = tools.listTools();

        // 使用上下文渲染提示词
        var context = prompts.render("main", query, history, availableTools);

        return model.complete(context);
    }
}

第 3 层(Cursor) 在几秒钟内生成这段代码。你专注于架构。

第 6 层(架构) 指南:

# 智能体架构指南

## 工作流路由
- 为多节点智能体工作流设计路由逻辑
  - 分类节点 → 路由到专家节点(支持、销售、技术)
  - 复杂性分析 → 路由到适当的模型层级(GPT-4o vs GPT-3.5)
  - 工具选择节点 → 根据用户意图路由到工具执行节点
- 通过 Arcade 网关路由工具执行:集中认证、速率限制、工具发现
- 提示词版本的 A/B 路由:10% 到 v2.0,90% 到 v1.5,监控质量

## 速率限制与节流
- 每用户令牌预算:10K 令牌/天(免费),100K(付费)
- 队列管理:最大 100 个并发请求,溢出到 SQS...
..
..

为什么这个框架能扩展

  • 对于一个关键智能体: 选择你的模型(第 2 层),编写清晰的代码(第 1 层),用 Cursor 迭代(第 3 层),优化提示词(第 4 层),集成生态系统 API(第 5 层),遵循架构模式(第 6 层)。
  • 对于一千个智能体: 相同的模型,相同的架构模式,相同的生态系统 API,但每个智能体都有自己的提示词包。Cursor 生成样板代码。你的语言提供结构。生态系统处理难题。

美妙之处何在?各层协同工作。Cursor 生成代码是因为模式简单。提示词是独立版本控制的。集成使用 REST API。架构无需抽象即可实现重用。

不需要编排框架。这就是框架。

引擎与 SDK 的问题

让我澄清一下:我并不是说所有框架都应该消失。我对 LangChain、LangGraph、Mastra、CrewAI、Autogen 等团队所构建的东西怀有极大的敬意。但我们需要理解一个在急于将所有东西移植到 Java 的过程中被忽视的关键区别。

不要混淆引擎SDK

我的意思是:我迫不及待地想用 Java 开发完整的智能体。我热爱 Java。但我不想仅仅因为我想用 Java 开发智能体就要一个 Java 引擎

考虑这些例子:

  • LangChain4J? 作为连接更广泛的 LangChain 生态系统的 SDK,这是一个很好的开始。你用 Java 编写,但你正在利用一个经过验证的引擎。
  • 带有 Java SDK 的 Crew AI? 完美。在 Python 中掌握编排模式,然后给我一个 Java 接口来使用它们。
  • 支持多语言的 Mastra? 正是正确的方向。构建一次引擎,为每种语言提供 SDK。
  • 为使用 Go 构建的 Not7 添加 Java SDK 或任何语言 SDK?

这里的模式是?用你喜欢的语言开发,而无需用该语言重建整个引擎。

编排层正在变薄

这就是为什么我认为即使是 SDK 方法也可能是暂时的,或者至少变得非常精简的原因:

  • 一方面: 模型正变得 dramatically 更好。GPT-5、Claude 4.5、Gemini 2.5 Pro、Grok 的推理能力使得复杂的编排模式过时了。它们可以用简单的提示词处理多步骤工作流,而这在六个月前需要复杂的链。
  • 另一方面: 真正的工程问题正在由专业平台解决。以 Arcade 为例:工具发现、认证、大规模执行、处理速率限制、管理工具版本。这才是艰难的工程工作所在。工具管理不再是编排问题;它是在平台层解决的基础设施问题。
  • 在中间: 编排框架正被挤压得越来越薄。

当你的模型能够推理工作流,并且平台处理复杂的工程问题(工具、内存、上下文)时,编排还剩下什么?

答案是:非常少。这就是为什么工程重点需要从编排转向更广泛的智能体开发挑战——提示词管理、生态系统集成、工具决策可审计性、成本优化。真正的问题已不在编排之中。

新现实:AI 原生框架

代码坏味道不仅仅是我们构建了太多框架。而是我们正在为一个不复存在的世界构建框架。以下是 2025 年构建框架实际意味着什么:

方面 过去的框架思维模式 (2005-2024) 下一代框架思维模式 (2025+)
定义 需要导入的代码库 跨越6个层级的完整环境
业务逻辑 位于代码抽象中 位于版本化提示词与指南中
关键构件 JAR 文件、软件包 提示词、上下文、API 知识
可重用性 代码继承与组合 架构模式与蓝图
开发工具 用于编写代码的 IDE 用于生成代码的 AI 工具(如 Cursor)
生态系统 自包含、单体式 集成专业化平台
样板代码 由框架抽象处理 由 AI 在几秒内生成
你导入/使用什么 Spring、Camel、框架 JAR 包 无需导入——你只需组合这些层级
  1. 接受 AI 驱动的开发现实 每个构建智能体的开发人员都将使用 Cursor、Copilot 或类似工具。这不是趋势——这是新的基线。设计你的框架以与 AI 代码生成无缝协作,而不是背道而驰。如果 Cursor 无法理解你的模式,那你的模式就是错的。
  2. 你的框架是纯文本英语,而不仅仅是代码 你的框架最关键部分将是精心设计的提示词、清晰的指南和结构化的上下文——而不是聪明的抽象。这些是你的版本化构件。这些决定了智能体行为。像对待代码一样严格对待它们。
  3. 当你需要 SDK 时,不要重新发明引擎 是的,Java SDK 至关重要。但你不需要仅仅为了用 Java 编写智能体就重建整个编排引擎。生态系统已经有平台在解决难题:内存(Zep, Mem0)、工具(MCPs, Arcade)、向量(Weaviate, Pinecone, Qdrant)、可观测性等。集成,不要重建。
  4. 框架仍然至关重要——但不是为了编排 如果你正在解决真正的问题——提示词版本控制、决策可审计性、生态系统集成模式、成本优化——那就构建这些。但编排?生态系统已经向前发展了。内存、工具、上下文、可观测性正由专业平台解决。将你的创新重点放在其他地方。
  5. 相信你的语言 如果你觉得你选择的语言中缺少一个框架,请退后一步。现代语言——Java、Python、TypeScript、Go——非常强大。凭借它们的最新特性加上 AI 代码生成工具,你可以用干净、简单的代码构建复杂的智能体。你的语言不是限制——试图用不必要的抽象包装它才是。

未来的框架不是你导入的代码库。它是对六个相互依赖层的掌握:你的语言、你的模型、你的开发工具、你的提示词、你的生态系统集成和你的架构。

也许我们不需要另一个智能体框架。也许我们所需要的只是一个智能体,一个能用你选择的语言创建智能体的智能体。一个开源的就更好了。


【注】本文译自:Java’s Agentic Framework Boom is a Code Smell

Java 运行时安全:输入验证、沙箱机制、安全反序列化

你的 Java 应用程序刚刚被攻破了。攻击者发送了一个精心构造的 JSON 载荷,你的反序列化代码"尽职尽责"地执行了它,现在他们正在下载你的客户数据库。这并非假设场景——它曾在 Equifax、Apache 以及无数其他公司真实发生过。

运行时安全与防火墙或身份验证无关。它关注的是不受信任的数据进入你的应用程序之后会发生什么。攻击者能否诱使你的代码执行你从未打算做的事情?答案通常是"可以",除非你刻意提高了攻击难度。

Java 为你提供了自卫的工具。大多数开发者忽略了它们,因为这些工具看起来偏执或过于复杂。然后生产环境就遭到了入侵,突然间那些"偏执"的措施就显得相当合理了。

为何运行时安全被忽视

你专注于功能。安全评审即使有,也往往在后期进行。代码在测试中能工作,于是就发布了。然后有人发现你的公共 API 未经验证就接受了用户输入,或者发现你正在反序列化不受信任的数据,或者意识到你的插件系统以完全权限运行第三方代码。

问题在于,大多数漏洞在你编写它们时看起来并不危险。一个简单的 ObjectInputStream.readObject() 调用看似无害,直到有人解释它如何实现远程代码执行。跳过输入验证节省了五分钟的开发时间,却在六个月后让你付出安全事件的代价。

安全不吸引人,它不会在演示中体现,而且在出事之前很难量化。但运行时安全问题是在生产系统中最常被利用的漏洞之一。让我们来谈谈三大要点:输入验证、沙箱机制和反序列化。

输入验证:万物皆不可信

每一个从外部进入你应用程序的数据都是潜在的攻击向量。用户输入、API 请求、文件上传、来自共享数据库的数据库记录、配置文件——所有这些都是。

规则很简单:在边界验证一切。不要等到业务逻辑中再验证。不要假设前端已经验证过了。在数据进入你的系统时进行验证。

糟糕的验证示例

以下是我在生产环境中经常看到的代码:

@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody UserRequest request) {
    User user = new User();
    user.setEmail(request.getEmail());
    user.setAge(request.getAge());
    user.setRole(request.getRole());

    userRepository.save(user);
    return ResponseEntity.ok(user);
}

看起来没问题,对吧?这是一场灾难。攻击者可以发送:

  • 邮箱:"admin@evil.com<script>alert('xss')</script>"
  • 年龄:-1999999
  • 角色:"ADMIN"(提升自己的权限)

你的应用程序会欣然接受所有这一切,因为你信任了输入。

正确的输入验证

以下是正确的做法:

public class UserRequest {
    @NotNull(message = "Email is required")
    @Email(message = "Must be a valid email")
    @Size(max = 255, message = "Email too long")
    private String email;

    @NotNull(message = "Age is required")
    @Min(value = 0, message = "Age must be positive")
    @Max(value = 150, message = "Age unrealistic")
    private Integer age;

    @NotNull(message = "Role is required")
    @Pattern(regexp = "^(USER|MODERATOR)$", message = "Invalid role")
    private String role;
}

@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserRequest request) {
    // 如果验证失败,Spring 自动返回 400 Bad Request

    User user = new User();
    user.setEmail(sanitizeEmail(request.getEmail()));
    user.setAge(request.getAge());
    user.setRole(request.getRole());

    userRepository.save(user);
    return ResponseEntity.ok(user);
}

private String sanitizeEmail(String email) {
    // 额外防护层:清除任何 HTML/脚本标签以防万一
    return email.replaceAll("<[^>]*>", "");
}

注意这种分层方法。Bean 验证注解捕获明显的问题。然后即使在验证之后,你还要对输入进行清理。这种深度防御方法意味着即使一层失效,你仍然受到保护。

验证复杂对象

真实的应用程序处理的是嵌套对象、列表和复杂结构:

public class OrderRequest {
    @NotNull
    @Valid  // 这很关键 - 验证嵌套对象
    private Customer customer;

    @NotEmpty(message = "Order must contain items")
    @Size(max = 100, message = "Too many items")
    @Valid
    private List<OrderItem> items;

    @NotNull
    @DecimalMin(value = "0.01", message = "Total must be positive")
    private BigDecimal total;
}

public class OrderItem {
    @NotBlank
    @Size(max = 50)
    private String productId;

    @Min(1)
    @Max(999)
    private Integer quantity;

    @DecimalMin("0.01")
    private BigDecimal price;
}

嵌套对象上的 @Valid 注解很容易被忘记,但至关重要。没有它,嵌套对象会完全绕过验证。

用于业务规则的自定义验证器

有时 Bean 验证还不够。你需要业务逻辑:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SafeFilenameValidator.class)
public @interface SafeFilename {
    String message() default "Unsafe filename";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class SafeFilenameValidator implements ConstraintValidator<SafeFilename, String> {
    private static final Pattern DANGEROUS_PATTERNS = Pattern.compile(
        "(\\.\\./|\\.\\.\\\\|[<>:\"|?*]|^\\.|\\.$)"
    );

    @Override
    public boolean isValid(String filename, ConstraintValidatorContext context) {
        if (filename == null) {
            return true; // 单独使用 @NotNull
        }

        // 防止路径遍历攻击
        if (DANGEROUS_PATTERNS.matcher(filename).find()) {
            return false;
        }

        // 白名单方法:只允许安全字符
        if (!filename.matches("^[a-zA-Z0-9_.-]+$")) {
            return false;
        }

        return true;
    }
}

现在你可以在任何文件上传参数上使用 @SafeFilename。这可以捕获攻击者试图上传到 ../../../etc/passwd 的路径遍历攻击。

白名单与黑名单的陷阱

在验证输入时,开发者常常试图阻止"坏"字符。这是黑名单方法,而且几乎总是错误的:

// 不好:黑名单方法
public boolean isValidUsername(String username) {
    return !username.contains("<") && 
           !username.contains(">") && 
           !username.contains("'") &&
           !username.contains("\"") &&
           !username.contains("script");
           // 你永远无法列出所有危险模式
}

攻击者很有创造力。他们会使用 Unicode 字符、URL 编码、双重编码以及你没想到的技巧来绕过你的黑名单。

相反,应该对你允许的内容使用白名单:

// 好:白名单方法
public boolean isValidUsername(String username) {
    return username.matches("^[a-zA-Z0-9_-]{3,20}$");
    // 只允许字母数字、下划线、连字符,3-20个字符
}

如果不在明确允许的范围内,就拒绝。这样安全得多。

沙箱机制:限制损害

输入验证阻止坏数据进入。沙箱机制则限制代码即使攻击成功也能做的事情。如果你的应用程序运行不受信任的代码——插件、用户脚本、动态类加载——沙箱机制至关重要。

Java 安全管理器(传统方法)

多年来,Java 使用安全管理器进行沙箱处理。它在 Java 17 中已被弃用并将被移除,但理解它有助于掌握概念:

// 旧方法(已弃用)
System.setSecurityManager(new SecurityManager());

// 在策略文件中定义权限
grant codeBase "file:/path/to/untrusted/*" {
    permission java.io.FilePermission "/tmp/*", "read,write";
    permission java.net.SocketPermission "example.com:80", "connect";
    // 权限非常有限
};

安全管理器可以限制代码能做什么:文件访问、网络访问、系统属性访问等。它功能强大但复杂,并且有性能开销。

现代沙箱方法

没有安全管理器,你需要替代策略。

在独立进程中隔离。 最可靠的沙箱是进程边界:

public class PluginExecutor {
    public String executePlugin(String pluginPath, String input) throws Exception {
        ProcessBuilder pb = new ProcessBuilder(
            "java",
            "-Xmx256m",  // 限制内存
            "-classpath", pluginPath,
            "com.example.PluginRunner",
            input
        );

        // 限制进程能做的事情
        pb.environment().clear();  // 无环境变量
        pb.directory(new File("/tmp/sandbox"));  // 受限目录

        Process process = pb.start();

        // 超时保护
        if (!process.waitFor(10, TimeUnit.SECONDS)) {
            process.destroyForcibly();
            throw new TimeoutException("Plugin execution timeout");
        }

        return new String(process.getInputStream().readAllBytes());
    }
}

插件在它自己的、资源受限的进程中运行。如果它崩溃或行为不端,你的主应用程序不会受到影响。你可以使用容器或虚拟机实现更强的隔离。

使用带有限制的自定义 ClassLoader:

public class SandboxedClassLoader extends ClassLoader {
    private final Set<String> allowedPackages;

    public SandboxedClassLoader(Set<String> allowedPackages) {
        super(SandboxedClassLoader.class.getClassLoader());
        this.allowedPackages = allowedPackages;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) 
            throws ClassNotFoundException {
        // 阻止危险的类
        if (name.startsWith("java.lang.Runtime") ||
            name.startsWith("java.lang.ProcessBuilder") ||
            name.startsWith("sun.misc.Unsafe")) {
            throw new ClassNotFoundException("Access denied: " + name);
        }

        // 仅白名单特定的包
        boolean allowed = allowedPackages.stream()
            .anyMatch(name::startsWith);

        if (!allowed) {
            throw new ClassNotFoundException("Package not whitelisted: " + name);
        }

        return super.loadClass(name, resolve);
    }
}

// 用法
Set<String> allowed = Set.of("com.example.safe.", "org.apache.commons.lang3.");
ClassLoader sandboxed = new SandboxedClassLoader(allowed);
Class<?> pluginClass = sandboxed.loadClass("com.example.safe.UserPlugin");

这可以防止插件加载危险的类。它并非无懈可击——坚定的攻击者可能会找到基于反射的变通方法——但它显著提高了攻击门槛。

限制资源消耗:

public class ResourceLimitedExecutor {
    private final ExecutorService executor = Executors.newFixedThreadPool(4);

    public <T> T executeWithLimits(Callable<T> task, 
                                   long timeoutSeconds,
                                   long maxMemoryMB) throws Exception {
        // 通过超时限制 CPU/时间
        Future<T> future = executor.submit(task);

        try {
            return future.get(timeoutSeconds, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            future.cancel(true);
            throw new RuntimeException("Task exceeded time limit");
        }

        // 内存限制更难——最好在 JVM 级别使用 -Xmx 处理
        // 或者使用如前所示的进程隔离
    }
}

如果你强制执行超时,即使是不受信任的代码也无法消耗无限的 CPU。内存更棘手——进程隔离或容器限制比尝试在 JVM 内强制执行效果更好。

真实世界的沙箱示例

假设你正在构建一个运行用户提交的数据转换脚本的系统:

public class ScriptSandbox {
    private static final long MAX_EXECUTION_TIME_MS = 5000;
    private static final String SANDBOX_DIR = "/tmp/script-sandbox";

    public String executeScript(String script, String data) {
        // 1. 验证脚本没有明显的恶意
        if (containsDangerousPatterns(script)) {
            throw new SecurityException("Script contains forbidden patterns");
        }

        // 2. 将脚本写入隔离目录
        Path scriptPath = Paths.get(SANDBOX_DIR, UUID.randomUUID().toString() + ".js");
        Files.writeString(scriptPath, script);

        try {
            // 3. 在具有资源限制的独立进程中执行
            ProcessBuilder pb = new ProcessBuilder(
                "timeout", String.valueOf(MAX_EXECUTION_TIME_MS / 1000),
                "node",
                "--max-old-space-size=100",  // 100MB 内存限制
                scriptPath.toString()
            );

            pb.directory(new File(SANDBOX_DIR));
            pb.redirectErrorStream(true);

            Process process = pb.start();

            // 4. 通过 stdin 传递数据,从 stdout 读取结果
            try (OutputStream os = process.getOutputStream()) {
                os.write(data.getBytes());
            }

            String result = new String(process.getInputStream().readAllBytes());

            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("Script failed with exit code: " + exitCode);
            }

            return result;

        } finally {
            // 5. 清理
            Files.deleteIfExists(scriptPath);
        }
    }

    private boolean containsDangerousPatterns(String script) {
        // 检查明显的攻击
        return script.contains("require('child_process')") ||
               script.contains("eval(") ||
               script.contains("Function(") ||
               script.matches(".*\\brequire\\s*\\(.*");
    }
}

这个例子结合了多种防御措施:静态分析、进程隔离、资源限制和清理。没有单一的防御是完美的,但层层设防使得利用难度大大增加。

安全反序列化:最大的隐患

Java 反序列化漏洞是历史上一些最严重安全漏洞的罪魁祸首。问题在于其根本性质:反序列化可以在对象构造期间执行任意代码。

为何反序列化是危险的

当你反序列化一个对象时,Java 会调用构造函数、readObject 方法和其他代码。控制序列化数据的攻击者可以精心构造对象来执行任意命令:

// 危险代码 - 请勿在生产环境中使用
public void loadUserSettings(byte[] data) {
    try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
        UserSettings settings = (UserSettings) ois.readObject();
        applySettings(settings);
    }
}

这看起来无害。但攻击者可以发送包含你类路径上(如 Apache Commons Collections)库中对象的序列化数据,这些对象在反序列化期间会执行系统命令。他们甚至根本不需要接触你的 UserSettings 类。

臭名昭著的"工具链"就是利用这一点。通过以特定方式链式组合标准库类,攻击者实现了远程代码执行。像 ysoserial 这样的工具可以自动创建这些载荷。

切勿反序列化不受信任的数据

最安全的方法很简单:不要对来自不受信任来源的数据使用 Java 序列化。绝不。

改用 JSON、Protocol Buffers 或其他仅包含数据的格式:

// 安全:使用 JSON
public UserSettings loadUserSettings(String json) {
    ObjectMapper mapper = new ObjectMapper();
    return mapper.readValue(json, UserSettings.class);
}

像 Jackson 这样的 JSON 解析器在解析期间不会执行任意代码。它们只是填充字段。攻击面急剧缩小。

当你必须反序列化时

有时你无法摆脱 Java 序列化——遗留协议、缓存库或分布式计算框架。如果你绝对必须反序列化不受信任的数据,请使用防御措施。

使用 ObjectInputFilter (Java 9+):

public Object safeDeserialize(byte[] data) throws Exception {
    try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
        // 白名单允许的类
        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
            "com.example.UserSettings;" +
            "com.example.UserPreference;" +
            "java.util.ArrayList;" +
            "java.lang.String;" +
            "!*"  // 拒绝其他所有类
        );

        ois.setObjectInputFilter(filter);

        return ois.readObject();
    }
}

该过滤器明确地将安全的类加入白名单,并拒绝其他所有类。这阻止了依赖于意外可用类的工具链。

验证对象图:

public class SafeObjectInputStream extends ObjectInputStream {
    private final Set<String> allowedClasses;
    private int maxDepth = 10;
    private int currentDepth = 0;

    public SafeObjectInputStream(InputStream in, Set<String> allowedClasses) 
            throws IOException {
        super(in);
        this.allowedClasses = allowedClasses;
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) 
            throws IOException, ClassNotFoundException {
        // 检查深度以防止深度嵌套的对象
        if (++currentDepth > maxDepth) {
            throw new InvalidClassException("Max depth exceeded");
        }

        String className = desc.getName();

        // 白名单检查
        if (!allowedClasses.contains(className)) {
            throw new InvalidClassException("Class not allowed: " + className);
        }

        return super.resolveClass(desc);
    }

    @Override
    protected ObjectStreamClass readClassDescriptor() 
            throws IOException, ClassNotFoundException {
        ObjectStreamClass desc = super.readClassDescriptor();
        currentDepth--;
        return desc;
    }
}

这个自定义实现通过跟踪反序列化深度和执行严格的白名单来增加另一层防御。

对序列化数据进行签名:

public class SignedSerializer {
    private final SecretKey signingKey;

    public byte[] serialize(Object obj) throws Exception {
        // 序列化对象
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(obj);
        }
        byte[] data = baos.toByteArray();

        // 创建签名
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);
        byte[] signature = mac.doFinal(data);

        // 合并签名和数据
        ByteBuffer buffer = ByteBuffer.allocate(signature.length + data.length);
        buffer.put(signature);
        buffer.put(data);

        return buffer.array();
    }

    public Object deserialize(byte[] signedData) throws Exception {
        ByteBuffer buffer = ByteBuffer.wrap(signedData);

        // 提取签名和数据
        byte[] signature = new byte[32];  // HmacSHA256 产生 32 字节
        buffer.get(signature);

        byte[] data = new byte[buffer.remaining()];
        buffer.get(data);

        // 验证签名
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);
        byte[] expectedSignature = mac.doFinal(data);

        if (!MessageDigest.isEqual(signature, expectedSignature)) {
            throw new SecurityException("Signature verification failed");
        }

        // 签名有效则反序列化
        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
            return ois.readObject();
        }
    }
}

签名可以防止攻击者篡改序列化数据。没有签名密钥,他们无法注入恶意对象。这在数据可能暴露但不受攻击者直接控制时(如客户端存储或缓存系统)有效。

替代序列化库

有几个库提供了更安全的序列化:

Kryo 提供更好的性能,并且可以配置为使用白名单:

Kryo kryo = new Kryo();
kryo.setRegistrationRequired(true);  // 拒绝未注册的类
kryo.register(UserSettings.class);
kryo.register(ArrayList.class);

// 序列化
Output output = new Output(new FileOutputStream("file.bin"));
kryo.writeObject(output, userSettings);
output.close();

// 反序列化 - 只允许注册的类
Input input = new Input(new FileInputStream("file.bin"));
UserSettings settings = kryo.readObject(input, UserSettings.class);
input.close();

Protocol BuffersApache Avro 使用基于模式的序列化。它们设置起来比较繁琐,但完全避免了代码执行风险:

message UserSettings {
  string theme = 1;
  int32 fontSize = 2;
  repeated string favorites = 3;
}

这些格式只反序列化数据,从不反序列化代码。通过 protobuf 反序列化实现代码执行是不可能的。

真实世界安全事件:一个警示故事

我曾咨询过的一家公司有一个管理门户,用于接受文件上传以进行批处理。代码看起来像这样:

@PostMapping("/admin/import")
public String importData(@RequestParam("file") MultipartFile file) {
    try {
        byte[] data = file.getBytes();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
        DataImport importData = (DataImport) ois.readObject();

        processImport(importData);
        return "Import successful";
    } catch (Exception e) {
        return "Import failed: " + e.getMessage();
    }
}

开发人员认为这是安全的,因为该端点需要管理员身份验证。他们遗漏的是:

  • 攻击者通过钓鱼攻击攻陷了一个低级别管理员账户
  • 攻击者使用 ysoserial 上传了一个恶意的序列化载荷
  • 在反序列化期间,载荷执行了系统命令
  • 攻击者获得了应用程序服务器的 shell 访问权限
  • 从那里,他们横向移动到数据库并窃取了客户数据

修复需要多次更改:

@PostMapping("/admin/import")
public String importData(@RequestParam("file") MultipartFile file) {
    // 验证文件类型
    if (!file.getContentType().equals("application/json")) {
        return "Only JSON imports allowed";
    }

    // 验证文件大小
    if (file.getSize() > 10 * 1024 * 1024) {  // 10MB 限制
        return "File too large";
    }

    try {
        // 使用 JSON 代替 Java 序列化
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        DataImport importData = mapper.readValue(
            file.getInputStream(), 
            DataImport.class
        );

        // 验证导入的数据
        validateImportData(importData);

        // 在受限上下文中处理
        processImportSafely(importData);

        return "Import successful";
    } catch (Exception e) {
        log.error("Import failed", e);
        return "Import failed - check logs";
    }
}

这次事件使他们付出了事件响应、法律费用和声誉损失方面的数百万代价。全都是因为一个不安全的反序列化调用。

实用安全检查清单

以下是你在每个 Java 应用程序中都应该做的事情:

输入验证:

  • 在所有 DTO 上使用 Bean 验证注解
  • 使用 @Valid 验证嵌套对象
  • 白名单允许的模式,不要黑名单危险模式
  • 即使在验证之后也要清理数据
  • 验证文件上传:类型、大小、内容
  • 绝不只依赖客户端验证

沙箱机制:

  • 在独立进程或容器中运行不受信任的代码
  • 使用自定义 ClassLoader 来限制类访问
  • 强制执行资源限制:内存、CPU 时间、磁盘空间
  • 清理临时文件和资源
  • 记录所有沙箱违规行为

反序列化:

  • 优先使用 JSON/Protocol Buffers 而非 Java 序列化
  • 没有过滤器的情况下切勿反序列化不受信任的数据
  • 使用 ObjectInputFilter 将类加入白名单
  • 可能时对序列化数据进行签名
  • 定期审计类路径依赖项以查找已知的工具类
  • 考虑使用需要注册模式的 Kryo

通用实践:

  • 保持依赖项更新(漏洞利用针对特定版本)
  • 使用静态分析工具捕获安全问题
  • 记录安全相关事件以进行监控
  • 使用恶意输入进行测试,而不仅仅是正常路径
  • 假设一切都可以被攻击

有用的工具

SpotBugsFindSecBugs 插件可在构建时捕获常见安全问题:

<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <configuration>
        <plugins>
            <plugin>
                <groupId>com.h3xstream.findsecbugs</groupId>
                <artifactId>findsecbugs-plugin</artifactId>
                <version>1.12.0</version>
            </plugin>
        </plugins>
    </configuration>
</plugin>

OWASP Dependency-Check 识别易受攻击的依赖项:

<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

SnykDependabot 在漏洞披露时自动更新依赖项。

思维模式的转变

安全不是你最后添加的功能。它是你从一开始就为之设计的约束。每次你接受外部输入时,问问自己:"攻击者利用这个能做的最坏的事情是什么?" 每次你反序列化数据时,问问:"我是否完全信任这个数据的来源?"

在代码审查中偏执是一种美德。当某人的 PR 包含反序列化或动态类加载时,积极地提出质疑。当缺少输入验证时,把它打回去。在代码审查中显得迂腐,也比在漏洞发生后显得疏忽要好。

运行时安全是关于减少信任。不要信任用户输入。不要信任插件。不要信任序列化数据。不要信任你的验证是完美的。层层设防,这样当一层失效时——它会的——其他层可以捕获攻击。

好消息是,一旦你内化了这些模式,它们就会成为第二天性。输入验证变得自动进行。你会本能地避免 Java 序列化。你会带着隔离的思想进行设计。安全成为你编码风格的一部分,而不是事后附加的东西。

有用资源


【注】本文译自:
Runtime Security in Java: Input Validation, Sandboxing, Safe Deserialization

Java 21 虚拟线程 vs 缓存线程池与固定线程池

探索 Java 并发如何从 Java 8 的增强发展到 Java 21 的虚拟线程,从而实现轻量级、可扩展且高效的多线程处理。

引言

并发编程仍然是构建可扩展、响应式 Java 应用程序的关键部分。多年来,Java 持续增强了其多线程编程能力。本文回顾了从 Java 8 到 Java 21 并发的演进,重点介绍了重要的改进以及 Java 21 中引入的具有重大影响的虚拟线程。

从 Java 8 开始,并发 API 出现了显著的增强,例如原子变量、并发映射以及集成 lambda 表达式以实现更具表现力的并行编程。

Java 8 引入的关键改进包括:

  • 线程与执行器
  • 同步与锁
  • 原子变量与 ConcurrentMap

Java 21 于 2023 年底发布,带来了虚拟线程这一重大演进,从根本上改变了 Java 应用程序处理大量并发任务的方式。虚拟线程为服务器应用程序提供了更高的可扩展性,同时保持了熟悉的"每个请求一个线程"的编程模型。

或许,Java 21 中最重要的特性就是虚拟线程。
在 Java 21 中,Java 的基本并发模型保持不变,Stream API 仍然是并行处理大型数据集的首选方式。
随着虚拟线程的引入,并发 API 现在能提供更好的性能。在当今的微服务和可扩展服务器应用领域,线程数量必须增长以满足需求。虚拟线程的主要目标是使服务器应用程序能够实现高可扩展性,同时仍使用简单的"每个请求一个线程"模型。

虚拟线程

在 Java 21 之前,JDK 的线程实现使用的是操作系统线程的薄包装器。然而,操作系统线程代价高昂:

  • 如果每个请求在其整个持续时间内消耗一个操作系统线程,线程数量很快就会成为可扩展性的瓶颈。
  • 即使使用线程池,吞吐量仍然受到限制,因为实际线程数量是有上限的。

虚拟线程的目标是打破 Java 线程与操作系统线程之间的 1:1 关系。
虚拟线程应用了类似于虚拟内存的概念。正如虚拟内存将大的地址空间映射到较小的物理内存一样,虚拟线程允许运行时通过将它们映射到少量操作系统线程来制造拥有许多线程的假象。

平台线程是操作系统线程的薄包装器。
而虚拟线程并不绑定到任何特定的操作系统线程。虚拟线程可以执行平台线程可以运行的任何代码。这是一个主要优势——现有的 Java 代码通常无需修改或仅需少量修改即可在虚拟线程上运行。虚拟线程由平台线程承载,这些平台线程仍然由操作系统调度。

例如,您可以像这样创建一个使用虚拟线程的执行器:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

对比示例

虚拟线程仅在主动执行 CPU 密集型任务时才消耗操作系统线程。虚拟线程在其生命周期内可以在不同的载体线程上挂载或卸载。

通常,当虚拟线程遇到阻塞操作时,它会自行卸载。一旦该阻塞任务完成,虚拟线程通过挂载到任何可用的载体线程上来恢复执行。这种挂载和卸载过程频繁且透明地发生——不会阻塞操作系统线程。

示例 — 源代码

Example01CachedThreadPool.java

在此示例中,使用缓存线程池创建了一个执行器:

var executor = Executors.newCachedThreadPool()
package threads;

import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

public class Example01CachedThreadPool {

    public void executeTasks(final int NUMBER_OF_TASKS) {

        final int BLOCKING_CALL = 1;
        System.out.println("Number of tasks which executed using 'newCachedThreadPool()' " + NUMBER_OF_TASKS + " tasks each.");

        long startTime = System.currentTimeMillis();

        try (var executor = Executors.newCachedThreadPool()) {

            IntStream.range(0, NUMBER_OF_TASKS).forEach(i -> {
                executor.submit(() -> {
                    // 模拟阻塞调用
                    Thread.sleep(Duration.ofSeconds(BLOCKING_CALL));
                    return i;
                });
            });

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        long endTime = System.currentTimeMillis();
        System.out.println("For executing " + NUMBER_OF_TASKS + " tasks duration is: " + (endTime - startTime) + " ms");
    }

}
package threads;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class Example01CachedThreadPoolTest {

    @Test
    @Order(1)
    public void test_1000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(1000);
    }

    @Test
    @Order(2)
    public void test_10_000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(10_000);
    }

    @Test
    @Order(3)
    public void test_100_000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(100_000);
    }

    @Test
    @Order(4)
    public void test_1_000_000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(1_000_000);
    }

}

我 PC 上的测试结果:

Example02FixedThreadPool.java

使用固定线程池创建执行器:

var executor = Executors.newFixedThreadPool(500)
package threads;

import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

public class Example02FixedThreadPool {

    public void executeTasks(final int NUMBER_OF_TASKS) {

        final int BLOCKING_CALL = 1;
        System.out.println("Number of tasks which executed using 'newFixedThreadPool(500)' " + NUMBER_OF_TASKS + " tasks each.");

        long startTime = System.currentTimeMillis();

        try (var executor = Executors.newFixedThreadPool(500)) {

            IntStream.range(0, NUMBER_OF_TASKS).forEach(i -> {
               executor.submit(() -> {
                   // 模拟阻塞调用
                  Thread.sleep(Duration.ofSeconds(BLOCKING_CALL));
                  return i;
               });
            });

        }   catch (Exception e) {
            throw new RuntimeException(e);
        }

        long endTime = System.currentTimeMillis();
        System.out.println("For executing " + NUMBER_OF_TASKS + " tasks duration is: " + (endTime - startTime) + " ms");
    }

}
package threads;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class Example02FixedThreadPoolTest {

    @Test
    @Order(1)
    public void test_1000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(1000);
    }

    @Test
    @Order(2)
    public void test_10_000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(10_000);
    }

    @Test
    @Order(3)
    public void test_100_000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(100_000);
    }

    @Test
    @Order(4)
    public void test_1_000_000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(1_000_000);
    }

}

我 PC 上的测试结果:

Example03VirtualThread.java

使用虚拟线程每任务执行器创建执行器:

var executor = Executors.newVirtualThreadPerTaskExecutor()
package threads;

import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

public class Example03VirtualThread {

    public void executeTasks(final int NUMBER_OF_TASKS) {

        final int BLOCKING_CALL = 1;
        System.out.println("Number of tasks which executed using 'newVirtualThreadPerTaskExecutor()' " + NUMBER_OF_TASKS + " tasks each.");

        long startTime = System.currentTimeMillis();

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {

            IntStream.range(0, NUMBER_OF_TASKS).forEach(i -> {
               executor.submit(() -> {
                   // 模拟阻塞调用
                  Thread.sleep(Duration.ofSeconds(BLOCKING_CALL));
                  return i;
               });
            });

        }   catch (Exception e) {
            throw new RuntimeException(e);
        }

        long endTime = System.currentTimeMillis();
        System.out.println("For executing " + NUMBER_OF_TASKS + " tasks duration is: " + (endTime - startTime) + " ms");
    }

}
package threads;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class Example03VirtualThreadTest {

    @Test
    @Order(1)
    public void test_1000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(1000);
    }

    @Test
    @Order(2)
    public void test_10_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(10_000);
    }

    @Test
    @Order(3)
    public void test_100_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(100_000);
    }

    @Test
    @Order(4)
    public void test_1_000_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(1_000_000);
    }

    @Test
    @Order(5)
    public void test_2_000_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(2_000_000);
    }

}

我 PC 上的测试结果:

结论

您可以清楚地看到用于处理所有 NUMBER_OF_TASKS 的不同执行器实现之间的执行时间差异。值得尝试不同的 NUMBER_OF_TASKS 值以观察性能变化。

虚拟线程的优势在处理大量任务时变得尤其明显。当 NUMBER_OF_TASKS 设置为较高的数值时——例如 1,000,000——性能差距是显著的。如下表所示,虚拟线程在处理大量任务时效率要高得多:

我确信,在澄清这一点之后,如果您的应用程序使用并发 API 处理大量任务,您会认真考虑迁移到 Java 21 并利用虚拟线程。在许多情况下,这种转变可以显著提高应用程序的性能和可扩展性。

源代码:GitHub Repository – Comparing Threads in Java 21


【注】本文译自:Java 21 Virtual Threads vs Cached and Fixed Threads

Java数据库应用原型

一个使用 Spring Boot 和容器进行测试、Keycloak 提供安全、PostgreSQL 提供数据持久化的,带有 REST 和安全功能的 Java 数据库应用原型。

在工作中开发时,我多次需要一个简单应用的模板,以便基于此模板开始为手头的项目添加特定代码。
在本文中,我将创建一个简单的 Java 应用程序,它连接到数据库,暴露一些 REST 端点,并使用基于角色的访问来保护这些端点。
目的是拥有一个最小化且功能齐全的应用程序,然后可以针对特定任务进行定制。
对于数据库,我们将使用 PostgreSQL;对于安全,我们将使用 Keycloak,两者都通过容器部署。在开发过程中,我使用 podman 来测试容器是否正确创建(作为 docker 的替代品——它们在大多数情况下可以互换)作为一次学习体验。
应用程序本身是使用 Spring Boot 框架开发的,并使用 Flyway 进行数据库版本管理。
所有这些技术都是 Java EE 领域业界标准,在项目中被使用的可能性很高。

我们构建原型的核心需求是一个图书馆应用程序,它暴露 REST 端点,允许创建作者、书籍以及它们之间的关系。这将使我们能够实现一个多对多关系,然后可以将其扩展用于任何可以想象的目的。
完整可用的应用程序可以在 https://github.com/ghalldev/db_proto 找到。
本文中的代码片段取自该代码库

在创建容器之前,请确保使用您偏好的值定义以下环境变量(教程中故意省略了它们,以避免传播多个用户使用的默认值):

DOCKER_POSTGRES_PASSWORD
DOCKER_KEYCLOAK_ADMIN_PASSWORD
DOCKER_GH_USER1_PASSWORD

配置 PostgreSQL:

docker container create --name gh_postgres --env POSTGRES_PASSWORD=$DOCKER_POSTGRES_PASSWORD --env POSTGRES_USER=gh_pguser --env POSTGRES_INITDB_ARGS=--auth=scram-sha-256 --publish 5432:5432 postgres:17.5-alpine3.22
docker container start gh_postgres

配置 Keycloak:
首先是容器的创建并启动:

docker container create --name gh_keycloak --env DOCKER_GH_USER1_PASSWORD=$DOCKER_GH_USER1_PASSWORD --env KC_BOOTSTRAP_ADMIN_USERNAME=gh_admin --env KC_BOOTSTRAP_ADMIN_PASSWORD=$DOCKER_KEYCLOAK_ADMIN_PASSWORD --publish 8080:8080 --publish 8443:8443 --publish 9000:9000 keycloak/keycloak:26.3 start-dev
docker container start gh_keycloak

在容器启动并运行后,我们可以继续创建领域、用户和角色(这些命令必须在正在运行的容器内部执行):

cd $HOME/bin
./kcadm.sh config credentials --server http://localhost:8080 --realm master --user gh_admin --password $KC_BOOTSTRAP_ADMIN_PASSWORD
./kcadm.sh create realms -s realm=gh_realm -s enabled=true
./kcadm.sh create users -s username=gh_user1 -s email="gh_user1@email.com" -s firstName="gh_user1firstName" -s lastName="gh_user1lastName" -s emailVerified=true -s enabled=true -r gh_realm
./kcadm.sh set-password -r gh_realm --username gh_user1 --new-password $DOCKER_GH_USER1_PASSWORD
./kcadm.sh create roles -r gh_realm -s name=viewer -s 'description=Realm role to be used for read-only features'
./kcadm.sh add-roles --uusername gh_user1 --rolename viewer -r gh_realm
./kcadm.sh create roles -r gh_realm -s name=creator -s 'description=Realm role to be used for create/update features'
./kcadm.sh add-roles --uusername gh_user1 --rolename creator -r gh_realm
ID_ACCOUNT_CONSOLE=$(./kcadm.sh get clients -r gh_realm --fields id,clientId | grep -B 1 '"clientId" : "account-console"' | grep -oP '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}')
./kcadm.sh update clients/$ID_ACCOUNT_CONSOLE -r gh_realm -s 'fullScopeAllowed=true' -s 'directAccessGrantsEnabled=true'

用户 gh_user1 在领域 gh_realm 中被创建,并拥有 viewercreator 角色。

您可能已经注意到,我们没有创建新的客户端,而是使用了 Keycloak 自带的一个默认客户端:account-console。这是为了方便起见,在实际场景中,您会创建一个特定的客户端,然后将其更新为具有 fullScopeAllowed(这会导致领域角色被添加到令牌中——默认情况下不添加)和 directAccessGrantsEnabled(允许通过 Keycloak 的 openid-connect/token 端点生成令牌,在我们的例子中使用 curl)。

创建的角色随后可以在 Java 应用程序内部使用,以根据我们约定的契约来限制对某些功能的访问——viewer 只能访问只读操作,而 creator 可以执行创建、更新和删除操作。当然,同样地,可以根据任何原因创建各种角色,只要约定的契约被明确定义并被所有人理解。
角色还可以进一步添加到组中,但本教程不包含这部分内容。

但是,在实际使用这些角色之前,我们必须告诉 Java 应用程序如何提取角色——这是必须的,因为 Keycloak 将角色添加到 JWT 的方式是其特有的,所以我们必须编写一段自定义代码,将其转换为 Spring Security 可以使用的东西:

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    // 遵循与 org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter 相同的模式
    Converter<Jwt, Collection<GrantedAuthority>> keycloakRolesConverter = new Converter<>() {
        private static final String DEFAULT_AUTHORITY_PREFIX = "ROLE_";
        //https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java#L901
        private static final String KEYCLOAK_REALM_ACCESS_CLAIM_NAME = "realm_access";
        private static final String KEYCLOAK_REALM_ACCESS_ROLES = "roles";

        @Override
        public Collection<GrantedAuthority> convert(Jwt source) {
            Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            Map<String, List<String>> realmAccess = source.getClaim(KEYCLOAK_REALM_ACCESS_CLAIM_NAME);
            if (realmAccess == null) {
                logger.warn("No " + KEYCLOAK_REALM_ACCESS_CLAIM_NAME + " present in the JWT");
                return grantedAuthorities;
            }
            List<String> roles = realmAccess.get(KEYCLOAK_REALM_ACCESS_ROLES);
            if (roles == null) {
                logger.warn("No " + KEYCLOAK_REALM_ACCESS_ROLES + " present in the JWT");
                return grantedAuthorities;
            }
            roles.forEach(
                    role -> grantedAuthorities.add(new SimpleGrantedAuthority(DEFAULT_AUTHORITY_PREFIX + role)));

            return grantedAuthorities;
        }

    };
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(keycloakRolesConverter);

    return jwtAuthenticationConverter;
}

AppConfiguration 类中还完成了其他重要配置,例如启用方法安全性和禁用 CSRF。

现在我们可以在 REST 控制器中使用 @org.springframework.security.access.prepost.PreAuthorize 注解来限制访问:

@PostMapping("/author")
@PreAuthorize("hasRole('creator')")
public void addAuthor(@RequestParam String name, @RequestParam String address) {
  authorService.add(new AuthorDto(name, address));
}

@GetMapping("/author")
@PreAuthorize("hasRole('viewer')")
public String getAuthors() {
  return authorService.allInfo();
}

通过这种方式,只有成功通过身份验证且拥有 hasRole 中列出的角色的用户才能调用端点,否则他们将收到 HTTP 403 Forbidden 错误。

在容器启动并配置完成后,Java 应用程序可以启动了,但在启动之前需要添加数据库密码——这可以通过环境变量完成(下面是一个 Linux shell 示例):

export SPRING_DATASOURCE_PASSWORD=$DOCKER_POSTGRES_PASSWORD

现在,如果一切正常启动并运行,我们可以使用 curl 来测试我们的应用程序(以下所有命令均为 Linux shell 命令)。

使用之前创建的用户 gh_user1 登录并提取身份验证令牌:

KEYCLOAK_ACCESS_TOKEN=$(curl -d 'client_id=account-console' -d 'username=gh_user1' -d "password=$DOCKER_GH_USER1_PASSWORD" -d 'grant_type=password' 'http://localhost:8080/realms/gh_realm/protocol/openid-connect/token' | grep -oP '"access_token":"\K[^"]*')

创建一个新作者(这将测试 creator 角色是否有效):

curl -X POST --data-raw 'name="GH_name1"&address="GH_address1"' -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" 'localhost:8090/library/author'

检索库中的所有作者(这将测试 viewer 角色是否有效):

curl -X GET -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" 'localhost:8090/library/author'

至此,您应该拥有了创建自己的 Java 应用程序所需的一切,可以根据需要对其进行扩展和配置。


【注】本文译自:Java Spring Boot Template With PostgreSQL, Keycloak Securit

单体架构中的事件驱动架构:Java应用程序的渐进式重构

传统观点认为事件驱动架构属于微服务架构范畴,服务通过消息代理进行异步通信。然而,事件驱动模式一些最具价值的应用恰恰发生在单体应用程序内部——在这些地方,紧密耦合已造成维护噩梦,而渐进式重构则提供了一条通往更好架构的路径,且无需分布式系统的运维复杂性。

为何在单体应用中使用事件有意义

传统的分层单体应用存在一个特定问题:直接的方法调用在组件之间创建了僵化的依赖关系。您的订单处理代码直接调用库存管理,库存管理又调用仓库系统,继而触发电子邮件通知。每个组件都了解其他几个组件,从而形成一个纠缠不清的网,更改其中一部分需要理解并测试它所触及的所有内容。

事件驱动模式引入了间接性。当下单时,订单服务发布一个"OrderPlaced"事件。其他对订单感兴趣的组件——库存、发货、通知——订阅此事件并独立响应。订单服务不知道也不关心谁在监听。即使这些组件存在于同一个代码库并共享同一个数据库,它们也变得松散耦合。

这种方法提供了立竿见影的好处,而无需将应用程序拆分为微服务。您在保持单体应用运维简单性的同时,获得了可测试性、灵活性和更清晰的边界。当您最终需要提取服务时,事件驱动的结构使得过渡更加平滑,因为组件已经通过定义良好的消息进行通信,而不是直接的方法调用。

起点:一个紧密耦合的订单系统

考虑一个使用 Spring Boot 构建的典型电子商务单体应用。订单创建流程如下所示:

@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final ShippingService shippingService;
    private final LoyaltyService loyaltyService;
    private final EmailService emailService;
    private final AnalyticsService analyticsService;

    public OrderService(
        OrderRepository orderRepository,
        InventoryService inventoryService,
        PaymentService paymentService,
        ShippingService shippingService,
        LoyaltyService loyaltyService,
        EmailService emailService,
        AnalyticsService analyticsService
    ) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
        this.shippingService = shippingService;
        this.loyaltyService = loyaltyService;
        this.emailService = emailService;
        this.analyticsService = analyticsService;
    }

    public Order createOrder(CreateOrderRequest request) {
        // 验证库存
        for (OrderItem item : request.getItems()) {
            if (!inventoryService.checkAvailability(item.getProductId(), item.getQuantity())) {
                throw new InsufficientInventoryException(item.getProductId());
            }
        }

        // 处理支付
        PaymentResult payment = paymentService.processPayment(
            request.getCustomerId(),
            calculateTotal(request.getItems()),
            request.getPaymentDetails()
        );

        if (!payment.isSuccessful()) {
            throw new PaymentFailedException(payment.getErrorMessage());
        }

        // 创建订单
        Order order = new Order(
            request.getCustomerId(),
            request.getItems(),
            payment.getTransactionId()
        );
        order.setStatus(OrderStatus.CONFIRMED);
        Order savedOrder = orderRepository.save(order);

        // 预留库存
        for (OrderItem item : request.getItems()) {
            inventoryService.reserveInventory(item.getProductId(), item.getQuantity());
        }

        // 创建发货单
        shippingService.createShipment(savedOrder);

        // 更新忠诚度积分
        loyaltyService.addPoints(
            request.getCustomerId(),
            calculateLoyaltyPoints(savedOrder)
        );

        // 发送确认邮件
        emailService.sendOrderConfirmation(savedOrder);

        // 跟踪分析
        analyticsService.trackOrderPlaced(savedOrder);

        return savedOrder;
    }
}

这段代码可以工作,但存在严重问题。OrderService 知道七个不同的服务。测试需要模拟所有这些服务。添加新的订单后操作意味着要修改此方法。如果电子邮件服务缓慢,订单创建就会变慢。如果分析跟踪失败,整个订单就会失败并回滚。

事务边界也是错误的。所有操作都在单个数据库事务中发生,这意味着即使电子邮件服务临时停机也会阻止订单创建。库存预留和发货单创建在事务上耦合,尽管它们在逻辑上是独立的操作。

引入 Spring 应用事件

Spring Framework 提供了一个内置的事件系统,在单个 JVM 内工作。默认情况下它是同步的,这使得它易于推理和调试。首先定义领域事件:

public abstract class DomainEvent {
    private final Instant occurredAt;
    private final String eventId;

    protected DomainEvent() {
        this.occurredAt = Instant.now();
        this.eventId = UUID.randomUUID().toString();
    }

    public Instant getOccurredAt() {
        return occurredAt;
    }

    public String getEventId() {
        return eventId;
    }
}

public class OrderPlacedEvent extends DomainEvent {
    private final Long orderId;
    private final Long customerId;
    private final List<OrderItem> items;
    private final BigDecimal totalAmount;

    public OrderPlacedEvent(Order order) {
        super();
        this.orderId = order.getId();
        this.customerId = order.getCustomerId();
        this.items = new ArrayList<>(order.getItems());
        this.totalAmount = order.getTotalAmount();
    }

    // Getters
}

事件应该是不可变的,并包含订阅者需要的所有信息。避免直接传递实体——而是复制相关数据。这可以防止订阅者意外修改共享状态。

重构 OrderService 以发布事件,而不是直接调用服务:

@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final ApplicationEventPublisher eventPublisher;

    public OrderService(
        OrderRepository orderRepository,
        InventoryService inventoryService,
        PaymentService paymentService,
        ApplicationEventPublisher eventPublisher
    ) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
        this.eventPublisher = eventPublisher;
    }

    public Order createOrder(CreateOrderRequest request) {
        // 验证库存
        for (OrderItem item : request.getItems()) {
            if (!inventoryService.checkAvailability(item.getProductId(), item.getQuantity())) {
                throw new InsufficientInventoryException(item.getProductId());
            }
        }

        // 处理支付
        PaymentResult payment = paymentService.processPayment(
            request.getCustomerId(),
            calculateTotal(request.getItems()),
            request.getPaymentDetails()
        );

        if (!payment.isSuccessful()) {
            throw new PaymentFailedException(payment.getErrorMessage());
        }

        // 创建并保存订单
        Order order = new Order(
            request.getCustomerId(),
            request.getItems(),
            payment.getTransactionId()
        );
        order.setStatus(OrderStatus.CONFIRMED);
        Order savedOrder = orderRepository.save(order);

        // 同步预留库存(仍在关键路径上)
        for (OrderItem item : request.getItems()) {
            inventoryService.reserveInventory(item.getProductId(), item.getQuantity());
        }

        // 为非关键操作发布事件
        eventPublisher.publishEvent(new OrderPlacedEvent(savedOrder));

        return savedOrder;
    }
}

现在 OrderService 仅依赖四个组件,而不是八个。更重要的是,它只了解对订单创建至关重要的操作——库存验证、支付处理和库存预留。其他所有操作都通过事件发生。

为解耦的操作创建事件监听器:

@Component
public class OrderEventListeners {
    private static final Logger logger = LoggerFactory.getLogger(OrderEventListeners.class);

    private final ShippingService shippingService;
    private final LoyaltyService loyaltyService;
    private final EmailService emailService;
    private final AnalyticsService analyticsService;

    public OrderEventListeners(
        ShippingService shippingService,
        LoyaltyService loyaltyService,
        EmailService emailService,
        AnalyticsService analyticsService
    ) {
        this.shippingService = shippingService;
        this.loyaltyService = loyaltyService;
        this.emailService = emailService;
        this.analyticsService = analyticsService;
    }

    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        try {
            shippingService.createShipment(event.getOrderId());
        } catch (Exception e) {
            logger.error("Failed to create shipment for order {}", event.getOrderId(), e);
            // 不要重新抛出 - 其他监听器仍应执行
        }
    }

    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateLoyaltyPoints(OrderPlacedEvent event) {
        try {
            int points = calculatePoints(event.getTotalAmount());
            loyaltyService.addPoints(event.getCustomerId(), points);
        } catch (Exception e) {
            logger.error("Failed to update loyalty points for order {}", event.getOrderId(), e);
        }
    }

    @EventListener
    public void sendConfirmationEmail(OrderPlacedEvent event) {
        try {
            emailService.sendOrderConfirmation(event.getOrderId());
        } catch (Exception e) {
            logger.error("Failed to send confirmation email for order {}", event.getOrderId(), e);
        }
    }

    @EventListener
    public void trackAnalytics(OrderPlacedEvent event) {
        try {
            analyticsService.trackOrderPlaced(event.getOrderId(), event.getTotalAmount());
        } catch (Exception e) {
            logger.error("Failed to track analytics for order {}", event.getOrderId(), e);
        }
    }
}

每个监听器在它自己的事务中运行(在适当的时候)并独立处理故障。如果发送电子邮件失败,发货单创建仍然会发生。即使分析跟踪抛出异常,订单创建事务也会成功提交。

理解事务边界

@Transactional(propagation = Propagation.REQUIRES_NEW) 注解至关重要。没有它,所有监听器都会参与订单创建事务。如果任何监听器失败,整个订单都会回滚——这正是我们试图避免的情况。

使用 REQUIRES_NEW,每个监听器都会启动一个新的事务。当监听器运行时,订单已经提交。这意味着:

  • 监听器无法阻止订单创建
  • 监听器故障不会回滚订单
  • 每个监听器的工作是独立原子性的

但这有一个权衡。如果监听器失败,订单存在但某些后处理没有发生。您需要处理这些部分故障的策略:

@EventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleOrderPlaced(OrderPlacedEvent event) {
    try {
        shippingService.createShipment(event.getOrderId());
    } catch (Exception e) {
        logger.error("Failed to create shipment for order {}", event.getOrderId(), e);

        // 记录失败以便重试
        failedEventRepository.save(new FailedEvent(
            event.getClass().getSimpleName(),
            event.getEventId(),
            "handleOrderPlaced",
            e.getMessage()
        ));
    }
}

一个单独的后台作业可以重试失败的事件:

@Component
public class FailedEventRetryJob {
    private final FailedEventRepository failedEventRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Scheduled(fixedDelay = 60000) // 每分钟
    public void retryFailedEvents() {
        List failures = failedEventRepository.findRetryable();

        for (FailedEvent failure : failures) {
            try {
                // 重建并重新发布事件
                DomainEvent event = reconstructEvent(failure);
                eventPublisher.publishEvent(event);

                failure.markRetried();
                failedEventRepository.save(failure);
            } catch (Exception e) {
                logger.warn("Retry failed for event {}", failure.getEventId(), e);
                failure.incrementRetryCount();
                failedEventRepository.save(failure);
            }
        }
    }
}

这种模式提供了最终一致性——系统可能暂时不一致,但通过重试自行恢复。

转向异步事件

Spring 的 @EventListener 默认是同步的。事件处理发生在发布事件的同一线程中,发布者等待所有监听器完成。这提供了强有力的保证,但限制了可扩展性。

通过启用异步支持并注解监听器来使监听器异步:

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean(name = "eventExecutor")
    public Executor eventExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("event-");
        executor.initialize();
        return executor;
    }
}

@Component
public class OrderEventListeners {
    // ... 依赖 ...

    @Async("eventExecutor")
    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        shippingService.createShipment(event.getOrderId());
    }

    @Async("eventExecutor")
    @EventListener
    public void sendConfirmationEmail(OrderPlacedEvent event) {
        emailService.sendOrderConfirmation(event.getOrderId());
    }
}

使用 @AsynccreateOrder() 方法在发布事件后立即返回。监听器在线程池中并发执行。这显著提高了响应时间——订单创建不再等待电子邮件发送或分析跟踪。

但异步事件引入了新的复杂性。当监听器执行时,订单创建事务可能尚未提交。监听器可能尝试从数据库加载订单,但由于事务仍在进行中而找不到它。

Spring 提供了 @TransactionalEventListener 来处理这种情况:

@Component
public class OrderEventListeners {
    @Async("eventExecutor")
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        // 这仅在订单创建事务成功提交后运行
        shippingService.createShipment(event.getOrderId());
    }
}

AFTER_COMMIT 阶段确保监听器仅在发布事务成功提交后运行。如果订单创建失败并回滚,监听器永远不会执行。这可以防止处理实际上不存在的订单的事件。

实现事件存储

随着事件驱动架构的成熟,存储事件变得有价值。事件存储提供了审计日志,支持调试,并支持更复杂的模式,如事件溯源。

创建一个简单的事件存储:

@Entity
@Table(name = "domain_events")
public class StoredEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String eventId;

    @Column(nullable = false)
    private String eventType;

    @Column(nullable = false, columnDefinition = "TEXT")
    private String payload;

    @Column(nullable = false)
    private Instant occurredAt;

    @Column(nullable = false)
    private Instant storedAt;

    @Column
    private String aggregateId;

    @Column
    private String aggregateType;

    // 构造器、getter、setter
}

@Repository
public interface StoredEventRepository extends JpaRepository<StoredEvent, Long> {
    List<StoredEvent> findByAggregateIdOrderByOccurredAt(String aggregateId);
    List<StoredEvent> findByEventType(String eventType);
}

拦截并存储所有领域事件:

@Component
public class EventStoreListener {
    private final StoredEventRepository repository;
    private final ObjectMapper objectMapper;

    public EventStoreListener(StoredEventRepository repository, ObjectMapper objectMapper) {
        this.repository = repository;
        this.objectMapper = objectMapper;
    }

    @EventListener
    @Order(Ordered.HIGHEST_PRECEDENCE) // 在其他监听器之前存储
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void storeEvent(DomainEvent event) {
        try {
            StoredEvent stored = new StoredEvent();
            stored.setEventId(event.getEventId());
            stored.setEventType(event.getClass().getSimpleName());
            stored.setPayload(objectMapper.writeValueAsString(event));
            stored.setOccurredAt(event.getOccurredAt());
            stored.setStoredAt(Instant.now());

            // 如果可用,提取聚合信息
            if (event instanceof OrderPlacedEvent) {
                OrderPlacedEvent orderEvent = (OrderPlacedEvent) event;
                stored.setAggregateId(orderEvent.getOrderId().toString());
                stored.setAggregateType("Order");
            }

            repository.save(stored);
        } catch (JsonProcessingException e) {
            throw new EventStoreException("Failed to serialize event", e);
        }
    }
}

现在,每个领域事件在业务逻辑处理之前都会持久化。您可以通过重放事件来重建系统中发生的情况:

@Service
public class OrderHistoryService {
    private final StoredEventRepository eventRepository;

    public List<OrderEvent> getOrderHistory(Long orderId) {
        List<StoredEvent> events = eventRepository.findByAggregateIdOrderByOccurredAt(
            orderId.toString()
        );

        return events.stream()
            .map(this::deserializeEvent)
            .collect(Collectors.toList());
    }

    private OrderEvent deserializeEvent(StoredEvent stored) {
        // 根据事件类型反序列化
        try {
            Class<?> eventClass = Class.forName("com.example.events." + stored.getEventType());
            return (OrderEvent) objectMapper.readValue(stored.getPayload(), eventClass);
        } catch (Exception e) {
            throw new EventStoreException("Failed to deserialize event", e);
        }
    }
}

这实现了强大的调试能力。当客户报告其订单问题时,您可以准确看到发生了什么事件以及发生的顺序。

Saga 和补偿操作

某些工作流需要跨多个步骤进行协调,其中每个步骤都可能失败。传统方法使用分布式事务,但这些方法扩展性不佳且增加了复杂性。Saga 使用编排事件和补偿操作提供了一种替代方案。

考虑一个更复杂的订单流程,您需要:

  1. 预留库存
  2. 处理支付
  3. 创建发货单

如果在预留库存后支付失败,您需要释放预留。通过补偿事件实现这一点:

public class InventoryReservedEvent extends DomainEvent {
    private final Long orderId;
    private final List<ReservationDetail> reservations;

    // 构造器、getter
}

public class PaymentFailedEvent extends DomainEvent {
    private final Long orderId;
    private final String reason;

    // 构造器、getter
}

@Component
public class InventorySagaHandler {
    private final InventoryService inventoryService;

    @EventListener
    public void handlePaymentFailed(PaymentFailedEvent event) {
        // 补偿操作:释放预留库存
        inventoryService.releaseReservation(event.getOrderId());
    }
}

Saga 通过事件而不是中央协调器进行协调:

@Service
public class OrderSagaService {
    private final ApplicationEventPublisher eventPublisher;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public void processOrder(Order order) {
        // 步骤 1: 预留库存
        List<ReservationDetail> reservations = inventoryService.reserve(order.getItems());
        eventPublisher.publishEvent(new InventoryReservedEvent(order.getId(), reservations));

        try {
            // 步骤 2: 处理支付
            PaymentResult payment = paymentService.processPayment(order);

            if (payment.isSuccessful()) {
                eventPublisher.publishEvent(new PaymentSucceededEvent(order.getId(), payment));
            } else {
                // 触发补偿
                eventPublisher.publishEvent(new PaymentFailedEvent(order.getId(), payment.getReason()));
                throw new PaymentException(payment.getReason());
            }
        } catch (Exception e) {
            // 触发补偿
            eventPublisher.publishEvent(new PaymentFailedEvent(order.getId(), e.getMessage()));
            throw e;
        }
    }
}

这种模式在没有分布式事务的情况下保持了一致性。每个步骤发布记录所发生事件的事件。当发生故障时,补偿事件会触发撤销先前步骤的操作。

桥接到外部消息代理

随着单体应用的增长,您可能希望与外部系统集成或为最终的服务提取做准备。Spring Cloud Stream 提供了对 RabbitMQ 或 Kafka 等消息代理的抽象,同时保持相同的事件驱动模式:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>

application.yml 中配置绑定:

spring:
  cloud:
    stream:
      bindings:
        orderPlaced-out-0:
          destination: order.placed
        orderPlaced-in-0:
          destination: order.placed
          group: order-processors
      kafka:
        binder:
          brokers: localhost:9092

创建内部事件和外部消息之间的桥接:

@Component
public class EventPublisher {
    private final StreamBridge streamBridge;

    public EventPublisher(StreamBridge streamBridge) {
        this.streamBridge = streamBridge;
    }

    @EventListener
    public void publishToExternalBroker(OrderPlacedEvent event) {
        // 将内部事件发布到外部消息代理
        streamBridge.send("orderPlaced-out-0", event);
    }
}

@Component
public class ExternalEventConsumer {
    private final ApplicationEventPublisher eventPublisher;

    public ExternalEventConsumer(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    @Bean
    public Consumer<OrderPlacedEvent> orderPlaced() {
        return event -> {
            // 将外部事件重新发布为内部事件
            eventPublisher.publishEvent(event);
        };
    }
}

这种模式让您可以选择性地将事件发布到外部,同时将内部事件保留在本地。关键的实时操作使用内部事件以实现低延迟。跨服务通信使用消息代理以实现可靠性和可扩展性。

监控与可观测性

事件驱动系统引入了新的可观测性挑战。理解正在发生的情况需要跨多个异步处理步骤跟踪事件。实施全面的日志记录和指标:

@Aspect
@Component
public class EventMonitoringAspect {
    private static final Logger logger = LoggerFactory.getLogger(EventMonitoringAspect.class);
    private final MeterRegistry meterRegistry;

    public EventMonitoringAspect(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Around("@annotation(org.springframework.context.event.EventListener)")
    public Object monitorEventListener(ProceedingJoinPoint joinPoint) throws Throwable {
        String listenerName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        DomainEvent event = (DomainEvent) args[0];

        Timer.Sample sample = Timer.start(meterRegistry);

        try {
            logger.info("Processing event {} in listener {}", 
                event.getEventId(), listenerName);

            Object result = joinPoint.proceed();

            sample.stop(Timer.builder("event.listener.duration")
                .tag("listener", listenerName)
                .tag("event_type", event.getClass().getSimpleName())
                .tag("status", "success")
                .register(meterRegistry));

            meterRegistry.counter("event.listener.processed",
                "listener", listenerName,
                "event_type", event.getClass().getSimpleName(),
                "status", "success"
            ).increment();

            return result;
        } catch (Exception e) {
            sample.stop(Timer.builder("event.listener.duration")
                .tag("listener", listenerName)
                .tag("event_type", event.getClass().getSimpleName())
                .tag("status", "failure")
                .register(meterRegistry));

            meterRegistry.counter("event.listener.processed",
                "listener", listenerName,
                "event_type", event.getClass().getSimpleName(),
                "status", "failure"
            ).increment();

            logger.error("Error processing event {} in listener {}", 
                event.getEventId(), listenerName, e);

            throw e;
        }
    }
}

这个切面自动跟踪每个事件监听器的执行时间和成功率。结合 Prometheus 和 Grafana 等工具,您可以可视化事件处理模式并识别瓶颈。

添加关联 ID 以跟踪系统中的事件:

public abstract class DomainEvent {
    private final Instant occurredAt;
    private final String eventId;
    private final String correlationId;

    protected DomainEvent(String correlationId) {
        this.occurredAt = Instant.now();
        this.eventId = UUID.randomUUID().toString();
        this.correlationId = correlationId != null ? correlationId : UUID.randomUUID().toString();
    }

    // Getters
}

通过事件链传播关联 ID:

@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
    MDC.put("correlationId", event.getCorrelationId());

    try {
        // 执行工作

        // 发布具有相同关联 ID 的后续事件
        eventPublisher.publishEvent(new ShipmentCreatedEvent(
            event.getOrderId(),
            event.getCorrelationId()
        ));
    } finally {
        MDC.clear();
    }
}

现在,与单个订单流相关的所有日志消息共享一个关联 ID,使得跨多个异步操作跟踪整个工作流变得微不足道。

测试事件驱动代码

事件驱动架构需要不同的测试策略。传统的单元测试适用于单个监听器,但集成测试对于验证事件流变得更加重要:

@SpringBootTest
@TestConfiguration
public class OrderEventIntegrationTest {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private ShippingService shippingService;

    @Autowired
    private EmailService emailService;

    @Test
    public void shouldProcessOrderPlacedEventCompletely() throws Exception {
        // 给定
        Order order = createTestOrder();
        OrderPlacedEvent event = new OrderPlacedEvent(order);

        // 当
        eventPublisher.publishEvent(event);

        // 等待异步处理
        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
            // 然后
            verify(shippingService).createShipment(order.getId());
            verify(emailService).sendOrderConfirmation(order.getId());
        });
    }
}

对于单元测试,注入一个间谍事件发布器以验证事件是否正确发布:

@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
    @Mock
    private OrderRepository orderRepository;

    @Mock
    private InventoryService inventoryService;

    @Mock
    private PaymentService paymentService;

    @Spy
    private ApplicationEventPublisher eventPublisher = new SimpleApplicationEventPublisher();

    @InjectMocks
    private OrderService orderService;

    @Test
    public void shouldPublishOrderPlacedEventAfterCreatingOrder() {
        // 给定
        CreateOrderRequest request = createValidRequest();

        when(inventoryService.checkAvailability(any(), anyInt())).thenReturn(true);
        when(paymentService.processPayment(any(), any(), any()))
            .thenReturn(PaymentResult.successful("txn-123"));
        when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        // 当
        orderService.createOrder(request);

        // 然后
        verify(eventPublisher).publishEvent(argThat(event -> 
            event instanceof OrderPlacedEvent
        ));
    }
}

迁移之旅

将单体应用重构为使用事件驱动架构并非全有或全无的命题。从一个工作流开始——通常是造成最多痛苦的那个。识别可以事件驱动的直接服务调用,并逐步引入事件。

从同步事件开始,以最小化行为变更。一旦事件正确流动,为非关键监听器切换到异步处理。当您需要审计跟踪或调试能力时,添加事件存储。仅当您需要跨服务通信或准备提取微服务时,才集成外部消息代理。

目标不是实现完美的事件驱动架构。而是减少耦合、提高可测试性,并在组件之间创建更清晰的边界。即使是部分采用也能提供价值——具有一些事件驱动模式的单体应用比完全没有的模式更易于维护。

这种渐进式方法使您能够持续交付价值,而不是投入一个需要数月时间、直到完全结束时才能交付任何成果的重构项目。您能够了解在特定领域和团队中哪些方法有效,根据实际经验而非理论理想来调整实施策略。

有用的资源


【注】本文译自: Event-Driven Architecture in Monoliths: Incremental Refactoring for Java Apps – Java Code Geeks

氛围编程:IT领导者须知

执行摘要

  • 氛围编程能加速开发与创新,但企业高管必须加强治理、安全与审查流程以保护业务。
  • 团队能快速测试想法并交付最小可行产品,从而缩短上市时间并提升对业务需求的响应能力。
  • 开发人员与非技术人员能更高效地协作,降低入门门槛并促进创新。

想象一下,您可以通过摩擦一盏神灯,用简单直白的语言向精灵描述您的需求,它就能为您生成一个功能齐全的应用程序。虽然神灯并不存在,但AI编程助手在很大程度上实现了这个愿望——无论其影响是好是坏。借助大语言模型,开发人员可以输入自然语言提示,并生成任何编程语言的代码。OpenAI联合创始人安德烈·卡帕西在2025年创造了"氛围编程"一词,用以描述"完全沉浸在感觉中,拥抱指数级增长,甚至忘记代码本身的存在"。

这种新范式标志着从深思熟虑、逐行编写代码,转向更流畅、更直观的人机意图与执行之间的协作。

氛围编程并非要取代开发人员,而是一种加速数字化转型的战略推动器,它能提高生产率,并且是打造快速上市工具的一种经济高效的选择。然而,IT高管必须将治理与赋能相结合,以最大化其价值,同时控制氛围编程带来的风险。

氛围编程如何运作?

开发人员首先选择一个AI编程助手,并描述他们想要的功能或特性。接着,AI会回应代码建议,开发人员可以审查、接受或优化这些建议。然后,开发人员继续迭代,通过向AI发出具体指令来添加新功能或进行调整,从而创建一个动态的、对话式的工作流程。

氛围编程 vs. 传统编程

传统上,编程过程非常结构化和有条不紊,而氛围编程描述的则是一种更具创造性或基于流程的方法。以下是这两种方法差异的细目分类:

氛围编程 传统编程
语言 自然语言 编程语言
焦点 宏观大局 / "感觉" 细节导向
审查流程 信任AI 同行代码审查
界面 AI代理 键入代码 / IDE
开发速度 几分钟到几小时 数天到数周甚至更长
入门门槛 无需具备代码知识 需要懂得如何编写所有代码
创作过程 探索与实验,如同即兴弹奏吉他 有计划、精确且可重复,如同创作交响乐

氛围编程的优势

氛围编程提供了若干关键优势,特别是对于那些希望快速将想法付诸实施并减少重复性任务的开发人员。

  • 更快的开发速度。 经验丰富的开发人员使用氛围编程可以在几小时内完成一个应用程序,而传统的开发时间则需要数天或数周。
  • 更低的入门门槛。 开发人员进行氛围编程所需的唯一语言是他们自己的自然口语。氛围编程使开发人员能够在不懂编码的情况下启动一个功能正常的项目。AI对于正在学习编码或理解应用程序工作原理的开发人员来说,也是一个强有力的工具。
  • 快速原型制作。 氛围编程的速度使开发团队能够快速创建功能性的最小可行产品。这使得氛围编程非常适合在争抢市场先机时向投资者展示项目。此外,它还通过实验实现了更快的功能迭代。
  • 爱好或内部项目。 如果无需考虑公共访问或安全问题,氛围编程是理想选择。其速度和易用性使开发人员能够快速解决问题并构建解决方案。
  • 多模态编程。 氛围编程将代码生成扩展到集成开发环境之外的键入方式,包括语音到文本提示。
  • 员工协作与生产力。 开发人员从编写代码转向审查和优化代码。其他员工,如分析师和产品经理,也可以对编程提供意见,从而实现业务和IT部门的跨职能协作。

氛围编程的局限性

氛围编程听起来是否好得令人难以置信?这取决于它的使用方式。使其成为小型应用程序和原型强大工具的特点,在大型代码库或安全性优先的场景中,却可能成为其负担。

  • 错误与幻觉。 生成代码的AI与任何其他流行的AI工具一样容易出现幻觉。几位计算机科学研究人员的一项研究发现,商业AI模型平均有5.2%的情况下会推荐不存在的软件包。相比之下,开源模型的这一比例跃升至21.7%。
  • 有限的技术复杂性。 提供给AI的每个提示都有一个有限的上下文窗口——类似于内存——其中包含大量关于您环境的数据,例如您打开的标签页内容。这为AI提供了上下文,使其能够做出明智的决策。然而,不同AI模型的上下文窗口大小不同,并且较大的上下文大小可能会影响AI的性能。项目越复杂,AI理解项目所需的上下文就越多。
  • 难以调试和维护。 未经审查就接受AI生成的代码,可能会导致创建一个无人理解代码作用及缘由的代码库。如果AI引入了其自身无法修复的错误,而开发人员又无法理解其输出,那么进展将完全受阻。
  • 缺乏原创性。 编码AI基于现有的代码示例进行训练,只能生成它所知道的内容。它无法完全靠自己提出革命性的过程或想法。

企业高管应将氛围编程生成的代码视为快速原型。然而,程序仍然需要经过审查。对于面向客户的服务,必须进行审查;如果涉及其他敏感数据,则必须检查其是否符合法规要求。

氛围编程的安全顾虑

一位名叫Leo的开发人员在X上宣布,他发布了一个完全通过氛围编程构建的SaaS应用程序。两天内,他的应用程序就遭到了黑客的攻击,Leo发帖称出现了各种随机问题。在整个项目中如此重度依赖AI会导致安全问题层出不穷。原因如下:

  • LLM或平台中的漏洞。 任何依赖外部组件的软件产品都会继承潜在的漏洞。AI编码平台也不例外。最近,安全研究人员在氛围编程平台Base44中发现了暴露的API端点,使得攻击者能够使用非机密的app_id值创建新账户来访问私有应用程序,从而绕过所有身份验证机制。
  • 开发人员错误。 氛围编程工具会精确地生成开发人员所要求的内容。如果开发人员在其提示中未包含安全实践,AI将不会生成遵循最佳安全实践的代码。
  • 数据隐私。 LLM通过摄取数据作为训练数据来改进模型。如果项目涉及敏感数据,例如支付信息、健康记录、专有代码或商业机密,则AI工具必须实施严格的数据隔离,以防止AI在其他应用程序中使用受保护的信息。

如何实施氛围编程

考虑到其局限性,在将氛围编程集成到项目中时最好谨慎行事,以充分利用其优势。

  1. 规划项目。 氛围编程和传统编程共有的一个特点是,两者在从一开始就有清晰计划指导时最为有效。确定您要构建什么,并将步骤分解为易于消化的小部分。牢记您希望为项目采用的安全和代码标准。
  2. 决定您的"氛围"策略。 真正的氛围编程定义为将所有决定交给AI。AI辅助编码是一种混合方法,开发人员向AI提示代码,然后在批准前仔细检查输出。找到最符合您优先事项的平衡点。
  3. 选择AI编程助手。 并非所有模型都构建得一样。有些专门用于代码生成,而其他则能解决更复杂的问题。不同模型在数据隔离、隐私以及成本方面有不同的政策。请仔细选择最适合您项目的AI代理。
  4. 使用源代码控制。 这对任何类型的编码都是个好主意,但对于氛围编程尤其重要。当您的项目处于良好工作状态时,为自己创建检查点,以便您可以根据需要轻松调整。
  5. 迭代。 一次创建一个功能,并在每个提示中提供尽可能多的细节和上下文。优化和重构您的代码,直到它符合您的设想。
  6. 测试。 确保您的项目在每个步骤中都正常工作。AI非常擅长生成自动化测试,但请确保您也执行手动测试,包括依赖项验证和自动化测试,以阻止合并未知/无效的软件包。
  7. 设定防护措施。 务必建立安全审查和编码标准。氛围编程项目仍应审查其准确性和合规意识,因此审批工作流是必要的。

IT高管可追踪的指标

这些指标应衡量交付速度、缺陷率和生产率的改进。以下是高管可用于追踪氛围编程的几个指标示例:

  • 原型制作时间(引入氛围编程工具前后对比)。

  • AI生成的拉取请求在自动化关卡失败的比例,例如测试、代码 lint 检查和软件成分分析。

  • 幻觉检测率,包括无效软件包或不良依赖项。

  • 每月归因于AI生成代码的安全事件

  • 每个正常工作的原型的成本,以客观显示投资回报率。

Java有哪些优势?

Java 的价值

当具有开创性的 Java 白皮书在 1995 年推出该语言时,它列出了七项使其超越竞争对手的核心价值。如今,Java 为在 AWS 和 Google Cloud 等主要云上运行的大规模系统提供动力,这使得这些价值对于现代部署和认证路径更具现实意义。
那份白皮书撰写至今已过去近 30 年,虽然其中许多价值仍然有效,但在 2025 年,选择 Java 作为您的部署平台的理由比以往任何时候都多。如果您关注 Java 路线图或热门技术博客,您会看到 Java 出现在云架构师、开发人员和数据领域的各个路径中。

Java 的优势

以下是 Java、JVM 和 JDK 的十大现代优势:

  1.  Java 是开源的
  2.  Java 是由社区驱动的
  3.  Java 快速且高性能
  4.  Java 易于学习
  5.  Java 是静态类型的
  6.  Java 拥有专家领导
  7.  Java 功能添加迅速
  8.  Java 是面向对象的
  9.  Java 支持函数式编程
  10. Java 优先考虑向后兼容性

    Java 是开源的

    Java 自 2011 年起已开源。任何人都可以查看 JDK 的源代码并创建定制化和优化的构建版本。这种开放性与 AWS 开发者GCP 专业云开发者等云学习路径非常契合,在这些路径中,基于 Java 的微服务很常见。
    流行的 OpenJDK 和 JVM 发行版包括:

    •   Azul 的高性能实现
    •   Oracle 的授权版本
    •   AdoptOpenJDK(现称为 Adoptium)
    •   IBM 的 Java 运行时
    •   Amazon Corretto
    •   Red Hat 的 OpenJDK 发行版
    •   微软构建的 OpenJDK
    •   高性能的 GraalVM
      谷歌甚至不惜借用 Java 源代码来构建自己的移动操作系统。这样做在道德上可能值得商榷,但美国最高法院表示,为构建 Android 操作系统而侵犯 Oracle 的版权是完全公平合理的。

      Java 是由社区驱动的

      Oracle 拥有 Java 商标这一事实在技术社区中引发了无休止的、任性的焦虑。然而,事实是 Java 通过 Java 社区进程向前发展,而非根据 Larry Ellison 的个人意愿。社区驱动的学习也体现在认证项目中,如 AWS 云从业者AWS 解决方案架构师GCP 助理云工程师
      JCP 是向 Java 编程语言添加新功能、新规范和新 API 的途径。在过去的 20 年里,JCP 完成了以下工作:

    •   增加了 1000 多名成员
    •   欢迎了 200 多家公司
    •   鼓励独立开发人员加入

社区支持和贡献是 Java 为软件开发社区带来的巨大优势之一,这种精神您同样可以在 AWS DevOps 工程师GCP DevOps 工程师圈子里找到。

Java 快速且高性能

Java 虚拟机是一个抽象层,使得 Java 程序能够跨平台运行。这种可移植性与 AWS 安全专家AWS 数据工程师GCP 专业数据工程师路径中的云工作负载非常匹配。
JVM 架构中立是 Java 的一大优势,但人们总是担心所需的抽象层可能会严重影响性能。但事实并非如此
在 JVM 上运行的 Java 可能无法达到与 C++ 或 Rust 等编译语言相同的性能。然而,垃圾收集器工作方式的改进、即时编译器的使用以及大量其他底层优化为 Java 平台带来了接近原生的性能。

Java 易于学习

1995 年的 Java 白皮书曾夸耀 Java 易于学习,因为它采用了该语言发布时流行的、类似 C 的熟悉语法。如果您喜欢结构化的目标和问责制,来自 Scrumtuous 的 Scrum 式冲刺可以帮助您规划 Java 学习节奏。
2023 年,JDK 拥有了 JShell,这使得 Java 对 Python 和 JavaScript 开发人员来说变得熟悉且易于学习。应试耐力可以通过像 Udemy 实践考试合集这样的题库来培养,即使它针对的是 AWS。这种训练方法可以很好地迁移到 Java 考试和云认证中。
此外,像 Replit 和 OneCompiler 这样的在线编译器允许学习者无需安装 IDE 或配置 JAVA_HOME 即可开始使用 Java。如果您的最终目标包括云角色,请参阅基础的 AWS 云从业者GCP 助理云工程师页面。

Java 是静态类型的

与 Python 或 JavaScript 等语言不同,Java 是静态类型的。
在 Java 中,您需要指定变量是 float、double、int、Integer、char 还是 String。这比动态类型语言提供了两个显著好处:

  •   它使得管理大型代码库更加容易,这对于 AWS 解决方案架构师GCP 云架构师非常重要。
  •   它使得优化运行时环境成为可能,这对 AWS 数据工程师GCP 数据库工程师等数据密集型角色有所帮助。
    Java 在 Python 和 JavaScript 失败的情况下仍能扩展的原因,通常可以追溯到 Java 的静态类型特性。

    Java 语言的静态类型特性是其主要优势。

    Java 拥有专家领导

    虽然该语言通过 Java 社区进程向前发展,但有两位杰出的软件架构师在 Oracle 内部指导着 Java 平台的演进。领导力和管理也是云项目中的主题,例如 AWS 专业级解决方案架构师以及专注于安全的路径,如 AWS 安全专家GCP 安全工程师

    功能采纳迅速

    与其他语言相比,Java 的优势之一是采纳新功能和响应社区需求的速度非常快。同样的迭代速度也反映在实践角色中,如 AWS DevOps 工程师GCP DevOps 工程师,这些角色会持续部署 Java 服务。

    Java 是面向对象的

    Java 用户认为这是理所当然的,但讨论 Java 的优势不能忽视 Java 是完全面向对象的,它实现了重要的 OOA&D 概念,例如:

  •   继承
  •   组合
  •   多态
  •   封装
  •   接口

对于使用 Scrum 主管产品负责人角色等框架组织工作的团队来说,Java 的对象建模自然地契合了映射到领域驱动设计的待办事项项。

Java 支持函数式编程

软件开发行业出现了向函数式编程的重大转变,而 Java 一直是这一趋势的重要组成部分。如果您旨在将 ML 服务与 Java 微服务融合,请探索 AWS 机器学习AWS AI 从业者路径。
函数式编程和不可变类型的使用可以使程序更快、更简洁且更易于理解。Java 在 Java 8 中进行了重大转变,引入了 Java Streams 和 lambda 表达式,这开启了 Java 函数式编程的新时代。您可以使用该语言同时进行函数式编程和面向对象编程,这是一个主要优势。

向后兼容性

随着 Java 社区推动 API 的重大更改,该语言的维护者始终优先考虑向后兼容性和非破坏性功能的添加。稳定性是 Java 在准备 AWS 助理级解决方案架构师GCP 专业云架构师角色的架构师中保持首选的原因之一。
即使引入了作为函数式编程的默认接口和 lambda 表达式,Java 平台也保持了向后兼容性。早期版本编写的代码可以在更新的环境中运行,无需重新编译。
在 2025 年,Java 的价值众多,因为 JDK 和 JVM 对于包含 AWS 云从业者解决方案架构师开发者数据工程师安全专家,以及高级角色如 AWS 专业级解决方案架构师,还有 GCP 路径如 GCP 数据从业者GCP 专业云网络工程师GCP Workspace 管理员GCP 机器学习工程师GCP 生成式 AI 负责人GCP 数据库工程师在内的多云职业而言,比以往任何时候都更具现实意义。

Java、JVM 和 JDK 的诸多优势持续推动着该编程语言的采用。


【注】本文译自:What are the advantages of Java?

AI智能体是加速器,而非开发者替代品

将AI集成到应用开发中的核心挑战,不在于其协助能力,而在于我们能在多大程度上放心地将控制权委托给它。

尽管AI智能体可以完美地执行那些曾被认为人类专属的任务,但它们同样可能在紧接着的下一段代码中犯下令人震惊的错误。

这些错误尖锐地提醒我们,即使是最先进的AI编程助手,仍然缺乏对世界运行方式的理解。这一根本区别将当前的生成式AI与通用人工智能(AGI)的愿景区分开来。考虑到这一点,让我们来看看AI智能体如何成为出色的开发加速器,却无法取代人类开发者。

LLM的推理并非逻辑推理

即使是复杂的智能体AI——构建于拥有日益庞大的上下文窗口和复杂工作流程的大型语言模型(LLM)之上——也依赖于语义模式匹配。它们无法对底层的因果关系和相互依赖提供真正的洞见。

让人类难以理解这一点的是,LLM在阐述其决策过程时具有令人信服的方式,常常模仿一种逻辑递进,暗示其对因果关系的理解,而实际上它们并不具备这种理解。它们通过拼凑统计上可能性高的人类推理文本片段来实现这一点。虽然这看起来像是逻辑推理,但它基于从训练数据中得出的概率计算,而非对步骤之间因果关系的直接理解。

LLM模仿逻辑推理,但无法掌握因果关系。

将这比作一位主演医疗电视剧的演员,他多年来记住了数千小时的对话、纪录片和真实咨询记录。他可以完美地进行鉴别诊断,像经验丰富的医生一样自信地用专业词汇滔滔不绝地说出症状、检测结果和治疗方案。他知道"向左臂放射的胸痛"通常出现在关于心脏病的场景中,"全血细胞计数和代谢指标组"跟在"我们来做些检查"之后,而担忧的表情伴随着关于肿瘤的讨论。

一位对医疗话题表现出表面理解的医疗剧演员,是比喻AI无法掌握因果关系的绝佳例子。

他们的表演如此令人信服,以至于任何观众都会相信他们懂医。但他们根本不知道阿司匹林为什么能稀释血液,心脏病发作时会发生什么,或者为什么一种治疗有效而另一种会致命。他们只是在背诵他们记住的各种医疗对话的变体,拼凑那些在统计上共同出现的片段,却不理解这些模式代表了真实的生物过程,其中顺序和因果关系 literally 意味着生与死。翻译到应用开发中,这通常意味着出色的结果之后紧接着灾难性的失败,反之亦然。

统计模式而非因果真相

LLM非常擅长在难以想象的大量文本中寻找并连接模式。尽管这些文本中有许多描述了世界的运作方式,但LLM并不理解这些描述的实际含义。相反,它将文本转换成数字——向量——这些数字捕获的是统计关系,而非因果真相。然后,模型将这些数字翻译回人类语言,而在这一切之下,它始终只是在跟踪和 shuffling 数字,而不是意义。例如,"charge"、"payment"和"credit card"这些词可能在向量空间中位置接近,因为它们经常在文本中共同出现,而"profile"、"lookup"和"fetch"则形成另一个集群——但模型实际上并不知道一组涉及金钱,而另一组不涉及。

LLM只处理词组之间的统计关系。

事物并非表面所见

由于编程语言是高度结构化的,这种数值上的 shuffling 可以产生优秀的代码。虽然AI模型并不像开发者那样"理解"代码,但它可以可靠地将输入模式映射到输出,将框架映射到样板代码,将语法映射到语义,其方式常常看起来与人类代码无异。例如,当被要求"用Python和Flask构建一个REST API"时,模型无法推理HTTP或数据库——它只是回忆起@app.route通常出现在函数定义之前,GET请求常常映射到返回jsonify,而错误处理经常涉及try/except块。结果往往是结构良好的Flask代码,即使它源于模式回忆而非真正的理解。

人类需要保持在循环中,以应对AI缺失的上下文和推理能力。

例如,为重试逻辑加固微服务听起来很简单——然而实际情况并非如此。要求AI助手"在失败时添加重试",你可能会得到一段在任何错误时都重试所有操作的代码。这对于幂等的(或无状态的)读取操作(例如"获取配置文件")来说没问题,因为重复调用只会返回相同的数据。
将相同的逻辑应用于非幂等操作——扣款、创建订单、发送电子邮件、查询数据库——你就会招致灾难:重复扣款、重复订单、通知风暴、数据库中的重复记录。解决方法并非魔术,而是判断力。人类首先对操作进行分类——幂等与非幂等——仅在瞬态错误时重试,并且对于任何有副作用的操作,都需要幂等性密钥和服务端去重。虽然这仍然为人类开发者节省大量时间,但他们仍然需要将其技能和专业知知融入其中,否则灾难可能并且将会随机发生。

理解模式匹配的局限性很棘手

原则上,模式匹配难道不能识别出对信用卡扣款需要采用与检索客户资料或产品信息不同的API调用重试方法吗?是的,它可以,但人类无法事先知道这一点,因为这取决于该特定模型的训练数据是否包含了执行标准POST或GET请求的重试函数。
模型未能建立操作类型与其现实后果之间的联系;它仅仅回忆统计关联。为了让模型避免这个错误,训练数据需要包含清晰、一致且重复出现的配对,将操作类型与重试策略及其后果联系起来。
理想情况下,数据会明确对比可以安全重试的代码与必须避免重试的代码。或许它还包括了事后分析或警告,描述了误用重试时发生的情况。然而,模型是否摄入了足够的训练数据来做出这种区分,我们人类无法确定。更棘手的是,由于其概率性质,模型可能在某一次做出了区分,但在接下来的三次尝试中却没有。
这个例子说明了为什么简单地添加更多训练数据通常不是答案,因为必要的数据可能并不以书面形式存在。或者更糟的是,训练数据可能包含了强化错误概括的内容。无论哪种情况,人类用户都无法知道是否如此,并且需要全面理解特定问题应如何解决。

AI的价值是真实的,开发团队可以受益

只要清楚地理解其局限性,AI智能体可以显著提高人类开发者在整个开发生命周期中的生产力。从收集需求并将其转化为用户故事,一直到检测并部署应用程序,AI智能体可以为人类提供建议、自动化验证和快速原型设计,从而显著缩短迭代周期。
AI智能体应被视为力量倍增器,可以处理开发的机械性方面,例如基于现有示例和文档生成样板代码、编写测试用例和记录API。另一方面,人类则负责真正理解业务影响、决定架构权衡,以及解决需要应用抽象逻辑能力的复杂问题。

AI对SDLC的生产力影响

下表分析了AI对软件开发生命周期(SDLC)中不同活动的生产力影响,以及AI对每项活动的能力、所需的人力参与程度和每项活动的风险水平。

AI对SDLC活动的生产力影响

活动 AI的生产力影响 AI能力描述 人力参与需求 风险水平
需求收集 低 – 中 根据笔记、会议记录、电子邮件和其他材料生成用户故事。 高 – 确保故事在成本、风险和回报方面与当前业务优先级保持一致。 高 – 被误解的需求将贯穿整个项目。
架构与设计 建议模式、识别瓶颈并生成初始图表,作为人类构建的坚实起点。 关键 – 考虑系统范围影响、做出战略权衡并监控技术趋势。 高 – 糟糕的架构决策难以逆转且成本高昂。
代码生成 构建定义良好的样板代码并解决精确定义的问题。保持文档更新。 中 – 掌控业务逻辑和边缘情况。 中 – 通常难以完全掌控AI编写的代码。
代码审查 捕获语法错误、发现安全漏洞、发现性能问题并建议优化。 高 – AI会遗漏依赖于上下文的问题和架构问题。 中 – 人类需要对审查负全责。
测试 创建单元测试、集成测试、自动化回归测试并发现边缘情况。 低 – 对于测试生成,但 高 – 对于测试策略。 中 – 人类必须对测试的完整性和相关性负责。
调试 分析堆栈跟踪并就已知错误建议修复方法。 中 – 指导调试过程。 低 – 错误的修复通常很容易发现。
文档编写 生成API文档、自述文件、内联注释、用户指南和变更日志。 低 – 对于面向用户的文档。 低 – 不正确的文档通常可以在没有重大影响的情况下得到纠正。
部署与CI/CD 创建部署清单、构建IaC模板、生成流水线配置。 高 – 生产部署需要仔细检查。 高 – 任何问题都会直接影响生产环境。
监控 添加检测、分析日志并生成警报规则。 中 – AI在没有上下文的情况下难以确定优先级。 中 – 误报会浪费时间。

来源:Torsten Volk, Omdia • 获取数据 • 使用 Datawrapper 创建

结论

宣布AI智能体正在接管开发者工作的技术领导者们,对AI当前能力产生了不切实际的期望。这导致许多企业高管认为开发者工时不再是他们所能构建内容的限制因素。金融分析师可以创建自己的投资组合再平衡工具;医疗保健管理员可以构建患者排班系统;供应链经理可以开发库存优化仪表板;或者营销总监可以构建个性化的活动自动化平台,而无需编写一行代码。虽然他们可以为许多此类业务任务实现概念验证,但架构、开发和交付企业级软件仍然极大地依赖于人类开发者的技能和经验。
然而,AI智能体可以通过为人类开发者完成大量基础性工作来显著加速SDLC。创建测试用例、用监控代理自动检测复杂软件、记录数万行主机代码以及精确定义复杂的基础设施清单,仅仅是AI智能体可以帮助人类开发者的几个例子。
人类与AI智能体之间的SDLC必须是协作的、迭代的并接受持续监督。确定如何最优地调整流程、开发工具和企业文化以满足这些要求,是智能体辅助应用开发的下一个前沿领域。弄清楚如何为人类编码者提供最佳AI支持,其回报有望带来显著的生产力提升,使人类开发团队能够更快、更高质量地交付更多功能。


[注]本文译自: AI agents are accelerators, not developer replacements

构建复合AI系统以实现可扩展工作流

了解如何利用复合AI系统架构化模块化且安全的智能体工作流,以实现可扩展的企业自动化。

生成式AI、大语言模型和多智能体编排的融合催生了一个变革性的概念:复合AI系统。这些架构超越了单个模型或助手,代表了智能代理的生态系统,它们通过协作来大规模交付业务成果。随着企业追求超自动化、持续优化和个性化参与,设计智能体工作流已成为关键的差异化因素。

本文探讨复合AI系统的设计,重点聚焦模块化AI代理、安全编排、实时数据集成和企业治理。旨在为解决方案架构师、工程领导者和数字化转型高管提供一个实用的蓝图,用于在各个领域(包括客户服务、IT运营、营销和现场自动化)构建和扩展智能代理生态系统。

复合AI的兴起

传统的AI应用通常是孤立的,一个机器人专用于服务,另一个专注于分析,还有一个用于营销。然而,真实世界的工作流是相互关联的,需要共享上下文、移交意图并进行自适应协作。复合AI系统通过以下方式解决这一问题:

  • 启用自主但协作的代理(例如,规划器、检索器、执行器)
  • 促进多模态交互(文本、语音、事件)
  • 支持企业级的可解释性、隐私和控制指南

这反映了复杂系统在人类组织中的运作方式:每个单元(代理)都有其角色,但它们共同创造了一个价值链。

企业级智能体工作流的设计原则

设计有效的复合AI系统需要深思熟虑的方法,以确保模块化、可扩展性并与企业目标保持一致。以下是指导智能体工作流开发的关键原则:

1. 模块化代理设计

每个AI代理都应遵循单一职责原则,设计为具有特定、明确界定的职责。这种模块化使维护、测试和可扩展性变得更加容易。例如:

  • 规划器代理:将总体目标分解为可管理的子任务。
  • 检索器代理:从不同来源检索和收集相关数据。
  • 执行器代理:根据规划器的指令执行操作。
  • 评估器代理:评估结果并提供反馈以持续改进。

通过明确定义职责,代理可以独立运作,同时在系统内协同工作。

2. 事件驱动和以意图为中心的架构

从静态的、同步的工作流转向动态的、事件驱动的架构,可增强响应能力和适应性。实施以意图为中心的设计使系统能够有效解释用户或系统意图并据此行动。关键组件包括:

  • 意图路由器:对意图进行分类并将其引导至相应的代理。
  • 事件代理:通过事件消息促进代理之间的通信。
  • 记忆模块:随时间推移保存上下文,使代理能够基于历史数据做出明智决策。

这种架构实现了可扩展性和弹性,这对企业环境至关重要。

3. 企业数据集成与检索增强生成

集成结构化和非结构化数据源可确保AI代理在全面的上下文中运行。利用检索增强生成技术使代理能够访问外部知识库,从而提高其决策能力。策略包括:

  • 数据连接器:创建与企业数据库和API的安全连接。
  • 向量数据库:增强语义搜索和相关信息的检索。
  • 知识图谱:提供数据实体之间关系的结构化表示。

这种集成确保了代理信息灵通、具有上下文意识,并能提供准确的结果。

4. 安全与治理框架

确保智能体系统的安全性和合规性至关重要。实施强大的治理框架有助于维持信任和问责制。关键实践包括:

  • 访问控制:建立并强制执行数据和代理交互的权限。
  • 审计追踪:记录代理活动以实现透明度和合规性。
  • 合规性检查:根据GDPR和HIPAA等监管标准定期评估系统。

结构良好的治理模型可以防范风险,并确保AI的合乎道德的部署。

5. 可观测性与持续监控

实施可观测性实践能够实时监控和诊断代理行为及系统性能。关键组件包括:

  • 日志记录:记录代理行动和决策的全面日志。
  • 指标收集:收集性能指标,如响应时间和错误率。
  • 警报系统:及时向利益相关者通知异常或系统故障。

持续监控允许进行主动维护和持续改进。

6. 人在回路机制

纳入人工监督可确保AI代理在可接受的范围内运行,并适应细微的场景。HITL方法包括:

  • 审批工作流:确保关键决策或行动得到人工验证。
  • 反馈循环:使用户能够就代理性能提供输入,指导其未来行为。
  • 干预协议:允许人员在必要时修改或调整代理行动。

平衡自动化与人工判断可增强系统可靠性并建立用户信任。

7. 可扩展性与性能优化

设计能够有效扩展以处理不断增长工作负载的系统至关重要。实现这一目标的策略包括:

  • 负载均衡:在代理和资源之间均匀分配工作负载。
  • 异步处理:使代理能够独立运行,最大限度地减少瓶颈。
  • 资源管理:有效监控和分配计算资源以维持性能。

针对可扩展性进行优化可确保系统在需求增加时保持响应能力和有效性。

通过遵循这些设计原则,企业可以创建稳健、高效、可靠的智能体工作流,这些工作流既符合组织目标,又能适应不断变化的挑战。

实际应用案例:现场服务代理网格

场景:一家公用事业组织可以利用三个专门的AI代理来增强现场响应操作:

  • 规划器代理:评估收到的用户投诉并制定解决计划。
  • 检索器代理:获取资产位置、历史工单数据和合规性检查清单。
  • 执行器代理:安排技术人员并向移动服务团队发送警报。

影响:提高任务分配效率、缩短解决周期并提高技术人员生产率。

结论

复合AI系统正在通过促进智能、适应性强且可扩展的工作流来改变企业架构。设计模块化、可编排的智能体系统有助于组织:

  • 加速AI驱动的转型
  • 增强运营弹性和灵活性
  • 为客户和员工体验提供更好的结果

未来在于从孤立的AI任务转向复合的代理生态系统,这是一种将创新与强大治理及领域相关性相结合的战略。


【注】本文译自:Architecting Compound AI Systems for Scalable Workflows