【大数据 & AI】Flink Agents 源码解读 --- (2) --- 核心架构

【大数据 & AI】Flink Agents 源码解读 --- (2) --- 核心架构

0x00 摘要

Flink Agents 框架的核心是 “事件驱动 + 状态隔离 + 多语言协作”:通过 Agent/AgentPlan 实现业务逻辑的声明式定义,借助 Flink 原生的分布式、高并发能力实现可靠执行,同时支持 Python 生态的工具 / 模型集成,兼顾了开发灵活性与运行时效率,适用于复杂 AI 代理任务的分布式部署与执行。

具体而言,Flink Agents 的组件是对原生 Flink 组件在 “Agent 业务场景” 下的语义化封装,而非全新发明。因此,本文将先介绍Flink Agents的基本组件,然后将其组件与Flink 原生组件做对比,最后给出一个详细的例子,这样读者可以更好的理解其设计精要。

可以把 Flink Agents 的整个执行流程比作 “做一道菜”,我们借此进行分析。

1.1 主要组件

Flink Agents 是基于原生 Flink 分布式流处理能力封装的上层框架。其中四个主要组件代表了 Flink Agents 框架中的四个层次:

  • Agent(顶层设计,定义了“做什么”):用户定义的智能实体,类似 “餐厅菜单 + 规则手册”,包含业务逻辑、动作(Action)和资源(工具、模型等)定义,明确 “做什么”。
  • AgentPlan(中间编译层,确定了“怎么做”):将 Agent 编译后的可执行计划,类似 “详细操作流程图”,明确动作触发规则、资源映射关系,确定 “怎么做”。
  • ActionExecutionOperator(运行时执行层,是执行环境,负责“协调调度”):Flink 集群中的执行核心,在 Flink 流处理环境中实际执行操作,类似 “餐厅首席大厨”,负责接收数据、调度任务、管理状态,协调整体执行流程。
  • ActionTask(最小执行单元,负责“具体实施”):具体的执行任务,类似 “员工的单个服务步骤”,分为 JavaActionTask 和 PythonActionTask,处理单个事件并返回结果。

具体可以参见下图。

[Agent] 菜单手册
    ↓(编译)
[AgentPlan] 详细流程图
    ↓(运行时实例化)
[ActionExecutionOperator] 餐厅首席大厨
    ↓(分配任务)
[ActionTask] 员工具体任务

这样的设计使得系统既灵活又高效,能够处理复杂的AI代理任务,同时保证了良好的扩展性和维护性。

1.2 内部成员变量映射关系

Agent、AgentPlan 和 ActionExecutionOperator 之间的关系以及它们内部成员变量的映射关系如下。

1.2.1 Agent 到 AgentPlan

Agent 到 AgentPlan 的映射如下:

Agent 成员 AgentPlan 对应成员 说明
_actions(装饰器定义) actions, actions_by_event Agent 中通过 @action 装饰器定义的动作被编译到 AgentPlan 的动作映射中
_actions(add_action 添加) actions, actions_by_event 通过 add_action 方法添加的动作同样编译到动作映射中
_resources resource_providers Agent 中注册的资源被转换为资源提供者

1.2.2 AgentPlan 到 ActionExecutionOperator

AgentPlan 到 ActionExecutionOperator 的映射如下:

AgentPlan 成员 ActionExecutionOperator 对应成员 说明
actions 通过 getActionsTriggeredBy() 方法调用 Operator 根据事件类型查找对应的动作
resource_providers 在 RunnerContextImpl 中使用 提供运行时所需的资源
config metricGroup, builtInMetrics 等 用于配置指标和其他运行时行为
详细映射分析
动作执行映射

ActionExecutionOperator 中:

// 根据事件类型获取触发的动作
private List<Action> getActionsTriggeredBy(Event event) {
    if (event instanceof PythonEvent) {
        return agentPlan.getActionsTriggeredBy(((PythonEvent) event).getEventType());
    } else {
        return agentPlan.getActionsTriggeredBy(event.getClass().getName());
    }
}

// 创建 ActionTask 来执行动作
private ActionTask createActionTask(Object key, Action action, Event event) {
    if (action.getExec() instanceof JavaFunction) {
        return new JavaActionTask(
            key, event, action, getRuntimeContext().getUserCodeClassLoader());
    } else if (action.getExec() instanceof PythonFunction) {
        return new PythonActionTask(key, event, action);
    }
    // ..
}
资源管理映射

AgentPlan 中的资源提供者在 ActionExecutionOperator 中通过 RunnerContextImpl 访问:

// 在 RunnerContextImpl 中
public Resource getResource(String name, ResourceType type) {
    return agentPlan.getResource(name, type);
}
配置映射

AgentPlan 中的配置信息被用于初始化ActionExecutionOperator的各种运行时组件:

// 在 ActionExecutionOperator.open() 中
metricGroup = new FlinkAgentsMetricGroupImpl(getMetricGroup());
builtInMetrics = new BuiltInMetrics(metricGroup, agentPlan);

// ActionStateStore 初始化也依赖于配置
if (actionStateStore == null && KAFKA.getType().equalsIgnoreCase(agentPlan.getConfig().get(ACTION_STATE_STORE_BACKEND))) {
    actionStateStore = new KafkaActionStateStore(agentPlan.getConfig());
}
状态管理映射

ActionExecutionOperator 中的各种状态与 AgentPlan 的执行需求相对应:

// 短期内存状态用于动作间的数据共享
private transient MapState<String, MemoryObjectImpl.MemoryItem> shortTermMemState;

// 动作任务状态用于异步执行管理
private transient ListState<ActionTask> actionTasksState;

// 待处理事件状态用于流控
private transient ListState<Event> pendingInputEventsState;

1.3 执行流程

具体流程如下:

Action Code → Agent → AgentPlan → ActionExecutionOperator → ActionTask → Flink Runtime

以 ReActAgent 为例,其流程如下:

  • 用户定义 ReActAgent,包含 start_actionstop_action

  • 通过 AgentPlan.from_agent() 编译成计划:

    • actions:包含 start_actionstop_action

    • actions_by_event:映射 InputEventstart_actionChatResponseEventstop_action

  • 在运行环境中,AgentPlan 被传递给 ActionExecutionOperatorFactory。ActionExecutionOperatorFactory 创建 ActionExecutionOperator 实例。

  • ActionExecutionOperator 在运行时根据 AgentPlan 执行具体操作。在 ActionExecutionOperator 中:

    • 接收到 InputEvent 后,查找并执行 start_action
    • start_action 发送 ChatRequestEvent
    • 查找并执行处理 ChatRequestEvent 的内置动作(如 CHAT_MODEL_ACTION)
    • PythonActionTask 执行工具函数,若为生成器则创建 PythonGeneratorActionTask 持续执行;
    • 工具执行完成生成 ChatResponseEvent
    • 查找并执行 stop_action
    • stop_action 产生 OutputEvent 并发送到下游

这种设计实现了关注点分离:用户只需关注业务逻辑定义,框架负责将其编译为高效的运行时执行计划。

0x02 与原生Flink比对

Flink Agents 是原生 Flink 的 “领域封装”:所有组件都能映射到原生 Flink 核心组件,未脱离 Flink 流处理的核心架构。我们接下来看看 Flink Agents 里的 Agent、AgentPlan、ActionExecutionOperator、ActionTask 这些核心组件,分别对应原生 Flink 中的哪些核心组件,以及它们的相似性逻辑。

2.1 核心

我们先用一张表来概括比对关系。

Flink Agents 组件 原生 Flink 对应组件 核心角色
Agent StreamGraph / 用户 DataStream 代码 高层业务逻辑声明(做什么)
AgentPlan JobGraph 编译后的可执行计划(怎么拆)
ActionExecutionOperator KeyedProcessOperator/StreamOperator 运行时核心执行算子(核心载体)
ActionTask 算子内处理单元 / AsyncFunction 任务 原子执行任务(最小执行单元)

核心映射逻辑如下:「声明层(Agent)→ 编译层(AgentPlan)→ 执行算子(ActionExecutionOperator)→ 原子任务(ActionTask)」对应原生 Flink 的「StreamGraph → JobGraph → StreamOperator → 算子内处理单元」;

回到 “做一道菜” ,我们进行分拆对比。

  • Agent = 你写的 “菜谱逻辑”(比如 “做番茄炒蛋,先炒蛋再炒番茄”)→ 对应 Flink StreamGraph(纯逻辑);
  • AgentPlan = 餐厅后厨把菜谱转换成的 “可执行工单”(明确 “谁来炒、用哪个锅、放多少盐”)→ 对应 Flink JobGraph(可执行计划);
  • ActionExecutionOperator = 掌勺厨师(执行工单)→ 对应 Flink StreamOperator;
  • ActionTask = 厨师的 “单个翻炒动作”→ 对应 Flink 算子内的原子处理任务;
  • Flink ExecutionGraph = 厨师实际站在哪个灶台、用哪套厨具 → 物理执行层,和 AgentPlan 无关。

2.2 具体比对

2.2.1 Agent

  • 相似组件:Flink Job 的业务逻辑定义(如 DataStream 算子链)、StreamGraph(流式作业的逻辑拓扑)
  • 相似性核心:Agent 是用户定义的 “智能实体”(包含业务逻辑、动作、资源),本质是业务层面的 “作业逻辑声明”;这和原生 Flink 中用户编写的 DataStream 代码(定义 “数据怎么处理”)、StreamGraph(描述逻辑拓扑)的角色完全一致 —— 都是 “告诉系统要做什么” 的顶层逻辑定义,不涉及具体执行。
  • 差异:Agent 更聚焦 “Agent 行为 / 工具调用” 的语义,而原生 StreamGraph 是通用的数据流拓扑。

2.2.2 AgentPlan

  • 相似组件:Flink 的 JobGraph(StreamGraph 编译后的可执行拓扑)
  • 相似性核心:AgentPlan 是 Agent 编译后的 “可执行计划”(明确动作触发规则、资源映射),本质是将高层业务逻辑转换为系统可识别的执行计划;这和原生 Flink 中 StreamGraph 编译为 JobGraph 的过程一致 ——JobGraph 把用户的逻辑拓扑转换为包含并行度、算子链、中间结果传递的可执行拓扑,AgentPlan 则把 Agent 的 “行为规则” 转换为系统可调度的 “动作执行规则”。
  • 关键共性:都是 “编译层” 产物,连接高层定义与底层执行。

2.2.3 ActionExecutionOperator

  • 相似组件:Flink 核心的 StreamOperator(如 ProcessOperator、FlatMapOperator)、KeyedProcessOperator

  • 相似性核心:ActionExecutionOperator 是 Flink 集群中执行 Agent 逻辑的核心算子,本质是运行时的核心执行单元;这和原生 Flink 的 StreamOperator 完全对应 ——StreamOperator 是处理数据流的核心载体(如 ProcessOperator 处理 KeyedStream、实现状态管理),ActionExecutionOperator 就是针对 “Agent 动作执行” 场景定制的 StreamOperator:

    • 都运行在 TaskManager 中,处理流数据(Event);
    • 都支持键控状态(Keyed State),按 key 隔离执行上下文;
    • 都依赖 Mailbox 机制调度任务,避免阻塞;
    • 都负责状态管理、事件处理、结果输出。
  • 典型对应:ActionExecutionOperator 最接近原生的 KeyedProcessOperator(键控处理、状态管理、异步任务调度)。

2.2.3 ActionTask

  • 相似组件:Flink 的「算子内的处理任务」(如 ProcessFunction 中的单个元素处理逻辑)、Async I/O 中的 AsyncFunction 任务

  • 相似性核心:ActionTask 是 “最小执行单元”(如单个 Python/Java 动作执行),本质是算子内的原子处理任务;这和原生 Flink 中:

    • ProcessOperator 处理单个 StreamElement(如一条数据)的逻辑单元;
    • Async I/O 中 AsyncFunction 封装的异步任务(如异步查询数据库);

    角色完全一致 —— 都是 “算子内的最小可执行任务”,执行完成后返回结果,支持异步 / 分阶段执行。

  • 关键共性

    • 都是 “原子执行单元”,不可再拆分;
    • 支持异步执行(如 PythonActionTask 对应 Async I/O);
    • 执行结果可触发后续任务(如 ActionTaskResult 生成新任务,对应 Async I/O 回调触发后续处理)。
  • 细分对应

    • JavaActionTask → 原生同步处理任务(如 ProcessFunction 中的同步逻辑);
    • PythonActionTask → 原生 Async I/O 任务(异步调用外部服务 / 脚本)。

0x03 实例拆解

我们来进一步解释 Flink Agents 这几个核心概念的关系和配合。为了更好的说明,我们可以把整个Flink Agents系统从”做一道菜“拓展到”一个智能餐厅自动化服务系统“。同时,结合实例也深入下技术细节。

3.1 第一层级:Agent – 餐厅菜单和规则手册(Blueprint)

Flink Agents 中的 Agent 是用户定义的智能实体,负责协调各种资源和行为来完成特定的任务。我们把 Agent 想象成餐厅的菜单和运营手册:

  • 定义了餐厅能提供哪些菜品和服务(动作)

  • 规定了需要用到哪些设备和食材(资源),如同为新员工配备工具箱,对应程序员实际用到的,可以是:

    • 各种 API 接口(相当于工具)
    • 数据库访问权限(相当于参考资料)
    • 决策逻辑代码(相当于培训材料)
  • 遇到什么情况应该做什么(对应事件监听)

  • 描述了顾客点餐到上菜的完整流程

    • 有一个 start_action(接待员)负责接收顾客问题
    • 有一个 stop_action(收银员)负责最终结账输出结果

这些都被封装在 Agent 对象中。

3.2 第二层级:AgentPlan – 餐厅的详细操作流程图(Compiled Plan,计划编译层)

Flink Agent框架会把用户的 Agent 转换为一个更通用、更适合系统处理的形式 —— AgentPlan。在本例中,AgentPlan 相当于将餐厅运营手册编译成的详细工作流程图(抽象化的工作蓝图):

  • 把菜单上的每道菜分解成具体的步骤
  • 明确每个岗位(动作)在什么情况下被触发,即遇到啥事件(Event)该干啥活(Action)
  • 准备好所有需要的工具和材料清单(Resource)

就像餐厅经理会制定详细的操作手册,告诉服务员什么时候该做什么事。

结构化解析

AgentPlan 包含几个关键部分:

Actions 映射表

  • 明确列出所有可以执行的动作及其触发条件
  • 例如:“收到订单查询请求” → “执行订单查询动作”

资源提供者目录

  • 记录所有可用资源的位置和获取方式
  • 类似于工厂里各个供应部门的联系方式清单

配置参数集

  • 存储运行所需的各项设置选项
  • 像是设备操作规程和技术规范

平台适配性

通过这种抽象化,无论底层是 Java 还是 Python 实现,都可以统一用同一份 AgentPlan 来进行调度管理。

3.3 第三层级:ActionExecutionOperator – 餐厅的执行管理层(Runtime Executor,执行引擎层)

在大规模生产环境中,不能只靠一个管家干活,而是需要一条自动化流水线。ActionExecutionOperator 就像是一条智能化的工厂装配线,它按照操作手册(AgentPlan)组织多个工作站来协同完成任务。或者说,ActionExecutionOperator 是餐厅的现场执行管理层,是整个系统的“大脑 + 中枢控制室”:

  • 负责接待进店的顾客(接收数据)
  • 根据流程图分配任务给不同的员工(执行动作)
  • 协调各个岗位之间的工作(监控进度状态 / 管理状态和内存)
  • 确保服务流程顺畅(处理并发和容错)
  • 餐厅经理拿着详细流程图,指挥各个员工协同工作。

3.4 第四层级:ActionTask – 具体的服务步骤(Execution Unit)

ActionTask 就像是员工执行的具体服务步骤,是生产线上的最小工位:一次只干一件具体活(调用 Python 函数、查数据库、发 HTTP、写 Kafka …):

  • 每个服务员接到的任务就是一个 ActionTask
  • 可能是简单一步完成(普通服务员)
  • 可能是需要多步完成(需要回访的技术员),即如果活太多(异步、长耗时),服务员会把自己「切分」成新任务,等下次轮询再接着干,实现非阻塞的协作式调度。
  • 完成后报告结果,并可能触发下一步工作,即干完就能立即把结果(Event)扔回传送带,继续下一轮

3.5 工作流程举例

假设顾客来到餐厅点了一份复杂的套餐:

  • 顾客进店(InputEvent)
    • 餐厅经理(ActionExecutionOperator)接待顾客
    • 根据流程图知道应该让接待员(start_action)处理
  • 接待员处理(ActionTask)
    • 接待员(PythonActionTask)接到任务
    • 理解顾客需求,生成厨房订单(ChatRequestEvent)
  • 厨房制作(内置动作)
    • 厨师(CHAT_MODEL_ACTION)收到订单开始制作
    • 制作完成后通知服务员(ChatResponseEvent)
  • 收银结账(ActionTask)
    • 收银员(stop_action)处理最终结果
    • 顾客打包带走(OutputEvent)

「用户只管把管家训好,框架负责把管家变成手册,再把手册变成并行生产线,最后让无数worker按 key 隔离、事件驱动、可检查点的方式跑在 Flink 上。」。

0x04 并发性和并行性

Flink Agents 系统在设计时已经充分考虑了并发性和并行性,比如:

  • 事件驱动模型:基于事件流转触发动作,初始为 InputEvent,经多轮事件(如 ChatRequestEvent、ChatResponseEvent)触发,最终输出 OutputEvent。
  • 并发与隔离设计:利用 Flink 键控流(KeyedStream)实现按 key 分区,每个 key 拥有独立状态;通过邮箱线程模型(MailboxExecutor)实现协作式多任务,避免阻塞。
  • 任务切分机制:长时间运行的任务(如 Python 生成器)会被切分为多个 ActionTask,执行后通过 ActionTaskResult 传递下一个任务,确保并发效率。
  • 本地与远程执行:本地用 LocalRunner 模拟执行,远程基于 Flink 集群的 ActionExecutionOperator 分布式执行,保持 API 一致性。

以下是具体实现方式:

4.1 Flink原生并发模型

系统充分利用了Apache Flink的原生并发机制:

  • 键控流(Keyed Streams):使用Flink的KeyedStream按key对数据进行分区,确保相同key的事件由同一个并行实例处理
  • 并行操作符:ActionExecutionOperator可以在Flink集群中的多个实例上并行运行
// 在CompileUtils.java中 - 将代理连接到键控流
public static <IN, K> DataStream<Object> connectToAgent(
    DataStream<IN> inputStream, KeySelector<IN, K> keySelector, AgentPlan agentPlan) {
    return connectToAgent(inputStream.keyBy(keySelector), agentPlan);
}

在 RemoteExecutionEnvironment 中有:

@Override
public DataStream<Object> toDataStream() {
    if (agentPlan == null) {
        throw new IllegalStateException("Must apply agent before calling toDataStream");
    }

    if (outputDataStream == null) {
        if (keySelector != null) {
            outputDataStream =
                    CompileUtils.connectToAgent(inputDataStream, keySelector, agentPlan);
        } else {
            // If no key selector provided, use a simple pass-through key selector
            outputDataStream =
                    CompileUtils.connectToAgent(inputDataStream, x -> x, agentPlan);
        }
    }

    return outputDataStream;
}

4.2 每个Key的状态管理

每个key都维护独立的状态隔离:

// 在ActionExecutionOperator.java中 - 维护每个key的状态
private transient ListState<ActionTask> actionTasksKState;
private transient ListState<Event> pendingInputEventsKState;
private transient ValueState<Long> sequenceNumberKState;

这些都是键控状态(keyed state),意味着每个key都有自己的独立状态实例,防止不同key之间的竞争条件。

4.3 邮箱线程模型

系统使用Flink的邮箱线程模型实现协作式多任务处理:

// 在ActionExecutionOperator.java中
private final transient MailboxExecutor mailboxExecutor;

// 提交任务进行处理
mailboxExecutor.submit(() -> tryProcessActionTaskForKey(key), "process action task");

这允许长时间运行的操作让出控制权,防止阻塞整个操作符。

4.4 异步任务处理

支持异步执行并具备适当的状态管理:

// 在 ActionTask.java 中 - 支持基于延续的执行
public class ActionTaskResult {
    private final boolean finished;
    private final List<Event> outputEvents;
    private final Optional<ActionTask> generatedActionTaskOpt;

    // 允许一个动作生成延续任务
    public Optional<ActionTask> getGeneratedActionTask() {
        return generatedActionTaskOpt;
    }
}

4.5 内存一致性

具有缓存状态访问的线程安全内存管理:

// 在 ActionExecutionOperator.java 中
private void createAndSetRunnerContext(ActionTask actionTask) {
    // 正确管理 RunnerContext 的创建和重用
    if (actionTask.getRunnerContext() != null) {
        return;
    }
    // ... 创建适当的 context
}

4.6 水印和事件时间处理

具备并发意识的水印处理:

    private transient SegmentedQueue keySegmentQueue;
    
    keySegmentQueue = new SegmentedQueue();

    @Override
    public void processWatermark(Watermark mark) throws Exception {
        keySegmentQueue.addWatermark(mark);
        processEligibleWatermarks();
    }

4.7 检查点和恢复

用于容错的线程安全状态快照

    @Override
    public void snapshotState(StateSnapshotContext context) throws Exception {
        if (actionStateStore != null) {
            Object recoveryMarker = actionStateStore.getRecoveryMarker();
            if (recoveryMarker != null) {
                recoveryMarkerOpState.update(List.of(recoveryMarker));
            }
        }

        HashMap<Object, Long> keyToSeqNum = new HashMap<>();
        getKeyedStateBackend()
                .applyToAllKeys(
                        VoidNamespace.INSTANCE,
                        VoidNamespaceSerializer.INSTANCE,
                        new ValueStateDescriptor<>(MESSAGE_SEQUENCE_NUMBER_STATE_NAME, Long.class),
                        (key, state) -> keyToSeqNum.put(key, state.value()));
        checkpointIdToSeqNums.put(context.getCheckpointId(), keyToSeqNum);

        super.snapshotState(context);
    }

4.8 总结

设计采用了多种并发机制:

  • Flink并行处理:利用Flink原生的并行性和键控流
  • 每个Key的隔离:每个key维护独立状态,消除key之间的竞争
  • 协作式线程:使用邮箱执行器实现非阻塞执行
  • 异步延续:通过任务链支持长时间运行的操作
  • 线程安全状态管理:对共享资源进行适当的同步
  • 容错机制:检查点机制确保故障情况下的数据一致性

这种方法使系统能够在保持一致性和支持水平扩展的同时处理高吞吐量,可在 Flink 集群的多个节点间扩展。

0xFF 参考

posted @ 2025-12-29 20:24  罗西的思考  阅读(8)  评论(0)    收藏  举报