Flink Agents 深度剖析:这才是生产级 AI Agent 该有的底座!

1.前言

将 LLM Agent 运行在分布式流处理引擎上,这并不是简单的技术缝合,而是一次对Agent 基础设施该长什么样的严肃回答。

早期的AI Agent 的爆发让基础设施面临一个尴尬的问题:流行的LangChain、LangGraph等框架解决了 Agent 的开发,但一旦涉及生产环境的分布式部署、状态持久化、故障恢复、背压控制,开发者就不得不回到手工拼凑 Redis + 消息队列 + 定时任务的原始时代。

那么Flink Agents 给的答案是:把 Agent 直接建模为流处理的一等公民。它不是"又一个 Agent 框架",而是复用了 Flink 过去十年在分布式流处理上沉淀的全部资产:Exactly-Once 语义、增量 Checkpoint、异步 I/O、背压传播,这些特性让 Agent 天然获得生产级的可靠性。在定义 Agent 协作拓扑时,实际上在定义一个 Flink Job Graph,背后是成熟的 TaskManager 调度、RocksDB 状态后端和 Chandy-Lamport 分布式快照算法。

所以这决定了Flink Agent的定位:不抢 LangChain 的快速原型市场,而是瞄准需要确定性、可伸缩、容错的多 Agent 流式处理场景。

2.核心概念与设计理念

2.1 三个核心抽象

Flink Agents 将复杂的 Agent 系统拆解为三层正交抽象:

Agent保存两类信息

  • actions:Action 名称、触发条件、函数描述和 Action 配置;

  • resources:按 ResourceType 分类的模型连接、工具、记忆等资源。

用户通常继承 Agent,通过注解声明静态方法。@Action 的值是事件类型字符串;一个 Action 可以监听多个事件类型,语义是 OR。

AgentPlan:把声明转换成可执行计划,是 API 层和 Runtime 层之间的桥梁。它把 Agent 中的函数提升为可执行 Action,并构造按事件类型索引的 actionsByEvent,运行时据此完成事件分发。
RunnerContext:Action 的运行时能力入口,提是每次 Action 执行时可用的上下文,供了

  • sendEvent:发送新事件;

  • 感官记忆、短期记忆和长期记忆访问;

  • Resource、配置和 MetricGroup 访问;

  • durableExecute 与 durableExecuteAsync:可恢复的细粒度调用。

ActionExecutionOperator:真正运行在 Flink 中的执行单元,是实际接入 Flink Runtime 的算子。它负责事件路由、Action 任务队列、状态访问、异步执行以及输出收集。

2.2 设计哲学:复用而非重建

Flink Agents 的设计有一条隐性原则:不重复造轮子,而是给现有轮子换个好用的接口。其中:

  • 状态管理:直接用 Flink 的Keyed State和 RocksDB 后端,Agent 通过 ctx.getState() 访问,底层自动获得增量 Checkpoint 和 Exactly-Once 保证。

  • 消息传递:直接走 Flink 的 RecordWriter/InputGate 通道,同一 key 的消息天然有序。

  • 异步调用:封装成 AsyncFunction,接入 Flink 的 AsyncIO 算子,内置超时、并发控制、背压传播。

Agent 开发者面对的是一个干净的 Agent 接口和 AgentContext 上下文环境,不必关心 TaskManager 怎么调度、状态怎么序列化、Checkpoint 怎么触发。但如果需要调优,Flink 的整套并行度、状态 TTL、watermark策略仍然暴露在外层。

2.3 LLM 调用的位置

LLM 调用在 Flink Agents 中被定位为一种外部异步 I/O,跟查询数据库、调用 HTTP API 是同一层级的抽象。这个定位是很关键的,它表示了Agent 的推理对 Flink Runtime而言只是一次带有超时和重试机制的异步函数调用,和其他 DataStream 算子中的数据库查询没有本质区别。

这意味着所有针对异步 I/O 的优化(批量合并请求、结果缓存、降级策略)都可以在 Tool 层面统一实现,Agent 核心代码保持纯粹。

3.架构原理深入

3.1 Agent 的生命周期与执行模型

每个 Agent 实例在被 Flink TaskManager 拉起后,进入一个标准的 推理循环(Reasoning Loop):

Agent 从消息中恢复上下文,经 LLM 做出行动决策;工具调用异步执行,状态更新后进入下一轮。

注意这个循环中的几个关键设计决策:

i、单线程执行:每个 Agent 实例是单线程的,消息按序处理。这简化了状态一致性模型,所以不必担心并发读写Keyed State。代价是 LLM 调用会阻塞后续消息,因此异步 I/O 不是可选项,是必需品。

ii、工具调用的异步化:asyncExecute 提交异步请求后立即返回,Agent 线程继续处理其他可执行的任务(如果框架支持交错执行),或者挂起等待回调。Flink 的 AsyncIO 算子在这里维护请求队列、跟踪超时、触发回调,同时向下游传播背压信号。

iii、State与 Checkpoint 的交互:每次推理循环结束后,状态变更(新的对话轮次、更新的中间结果)写入Keyed State。Flink 的 Checkpoint 机制在 Barrier 到达时执行快照。如果故障发生在 LLM 调用过程中,Agent 从上一个 Checkpoint 恢复,重新发起该轮推理------这意味着 LLM 调用需要设计为幂等的(通过请求去重 Key 在 Tool 层实现)。

3.2 消息传递与背压传播

消息传递链路是全异步、全背压感知的:

bash 复制代码
上游 Agent (RecordWriter) → 网络缓冲区 (Netty / Shuffle) → 下游 InputGate → 下游 Agent 反序列化并处理

当下游 Agent 处理缓慢(比如 LLM API 响应慢导致推理循环积压),其输入缓冲区写满,背压信号沿着 InputGate → TCP 连接 → 上游 RecordWriter 反向传播,最终上游 Agent 的 sendMessage 调用被阻塞。这是 Flink 原生的背压机制,不需要 Agent 代码做任何处理。

但这里有一个陷阱:sendMessage 如果一直阻塞,Agent 的推理线程会被挂起,无法处理新的输入消息。对于需要高吞吐的场景,sendMessage 应该设计为非阻塞 API(内部缓冲到队列,异步发送),让 Agent 线程快速释放。

3.3 端到端数据流的完整路径

一个完整的 Agent Pipeline 的数据流如下:

bash 复制代码
Kafka Source → SourceAgent → ObserverAgent → PlannerAgent → ExecutorAgent → SinkAgent → Kafka/DB/WebSocket

其中:SourceAgent 的角色是协议适配:将 Kafka 的 ConsumerRecord 或 HTTP 请求体转换为 Agent 消息格式。SinkAgent 反向操作:将 Agent 的最终输出写回外部系统。中间的 Observer-Planner-Executor 是典型的 感知-规划-执行 协作模式。各 Agent 阶段可独立配置并行度,消息按 key 保序流动,Checkpoint Barrier 贯穿整条处理链路。

每个 Agent 在 Checkpoint 周期内都有自己的Keyed State快照。当整个 Job 从故障恢复时,所有 Agent 从同一个 Checkpoint 坐标恢复,保证全局一致性。

3.4 三类记忆的职责边界

  1. Sensory Memory:单次 Agent Run 的工作区。感官记忆适合保存本轮执行中的中间结果。它按 Key 隔离,由 Flink State 承载,在运行期间会进入 Checkpoint,但一次 Agent Run 完成后会自动清理。

  2. Short-Term Memory:同一 Key 的跨轮记忆。短期记忆同样基于 Flink Keyed State,但不会在本轮结束时自动清除,适合保存对话历史、最近一次决策和会话属性。它保存的是原始、精确数据,并支持 TTL。

  3. Long-Term Memory:外部语义记忆。长期记忆面向跨时间、跨会话的大规模知识,通常落在外部向量存储中,通过语义检索召回。它不是 Keyed State 的简单别名,也不应承载每步都要低延迟读写的临时数据。

在 Action 中读写记忆:

bash 复制代码
@Action("profile_loaded")
public static void remember(Event event, RunnerContext ctx) throws Exception {
    // 本轮执行结束后自动清理
    ctx.getSensoryMemory().set("rawProfile", event.getAttr("profile"));
 
    // 同一业务 Key 的后续运行仍可读取
    ctx.getShortTermMemory().set("lastIntent", event.getAttr("intent"));
 
    Object lastIntent = ctx.getShortTermMemory()
            .get("lastIntent")
            .getValue();
 
    ctx.sendEvent(new OutputEvent(lastIntent));
}

配置短期记忆 TTL:

bash 复制代码
agentsEnv.getConfig().set(AgentExecutionOptions.SHORT_TERM_MEMORY_STATE_TTL_MS,30 * 60 * 1000L);

生产环境还应结合状态大小、Checkpoint 时长和会话活跃度设置 TTL,避免无界增长。

4.案例剖析

4.1 案例一:最小可运行的事件工作流

下面的 Agent 完成"输入文本标准化 → 输出结果"。它只使用当前源码中真实存在的 API。

bash 复制代码
import org.apache.flink.agents.api.Event;
import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
import org.apache.flink.agents.api.annotation.Action;
import org.apache.flink.agents.api.context.RunnerContext;
 
import java.util.Locale;
import java.util.Map;
 
public class NormalizeAgent extends Agent {
 
    @Action(EventType.InputEvent)
    public static void receive(Event event, RunnerContext ctx) {
        String input = (String) InputEvent.fromEvent(event).getInput();
        ctx.sendEvent(new Event("normalize", Map.of("text", input)));
    }
 
    @Action("normalize")
    public static void normalize(Event event, RunnerContext ctx) {
        String text = (String) event.getAttr("text");
        String result = text.trim().replaceAll("\\s+", " ").toLowerCase(Locale.ROOT);
        ctx.sendEvent(new OutputEvent(result));
    }
}

把 Agent 接到 DataStream:

bash 复制代码
public class NormalizeJob {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env =
                StreamExecutionEnvironment.getExecutionEnvironment();
        AgentsExecutionEnvironment agentsEnv =
                AgentsExecutionEnvironment.getExecutionEnvironment(env);
 
        DataStream<String> input = env.fromData("  HELLO   Flink Agents  ");
 
        DataStream<Object> output = agentsEnv
                .fromDataStream(input)
                .apply(new NormalizeAgent())
                .toDataStream();
 
        output.print();
        agentsEnv.execute("normalize-agent-job");
    }
}

在有会话语义的场景中,应按会话或业务主键分区,而不是让不同用户共享同一状态空间。例如:

bash 复制代码
DataStream<Object> output = agentsEnv
        .fromDataStream(requests, Request::getSessionId)
        .apply(new CustomerServiceAgent())
        .toDataStream();

这里的 Key 决定状态隔离边界,也会影响恢复后的事件顺序,因此必须选择稳定、可重放的业务键。

4.2 案例二:调用聊天模型的标准事件链路

Flink Agents 将模型调用也表示成事件。业务 Action 发送 ChatRequestEvent,内置 Action 完成模型调用后产生 ChatResponseEvent,再由业务 Action 处理响应。

bash 复制代码
public class SummaryAgent extends Agent {
 
    @Prompt
    public static Prompt summaryPrompt() {
        return Prompt.fromText("请把下面内容压缩成三条要点:{input}");
    }
 
    @ChatModelSetup
    public static ResourceDescriptor summaryModel() {
        return ResourceDescriptor.Builder
                .newBuilder(ResourceName.ChatModel.OLLAMA_SETUP)
                .addInitialArgument("connection", "ollamaConnection")
                .addInitialArgument("model", "qwen3:8b")
                .addInitialArgument("prompt", "summaryPrompt")
                .build();
    }
 
    @Action(EventType.InputEvent)
    public static void requestSummary(Event event, RunnerContext ctx) {
        String input = (String) InputEvent.fromEvent(event).getInput();
        ChatMessage message = new ChatMessage(MessageRole.USER, "");
 
        ctx.sendEvent(new ChatRequestEvent(
                "summaryModel",
                List.of(message),
                Map.of("input", input),
                null));
    }
 
    @Action(EventType.ChatResponseEvent)
    public static void emitSummary(Event event, RunnerContext ctx) {
        ChatResponseEvent response = ChatResponseEvent.fromEvent(event);
        ctx.sendEvent(new OutputEvent(response.getResponse().getContent()));
    }
}

运行作业时还要向 AgentsExecutionEnvironment 注册名为 ollamaConnection 的 CHAT_MODEL_CONNECTION ResourceDescriptor。连接描述符由所选模型集成模块提供,端点、鉴权和模型名应通过配置或 Secret 注入,不要硬编码在 Agent 逻辑中。

5.总结

Apache Flink Agents 的价值不在于又提供了一种 Agent 开发范式,而在于为 Agent 系统补上了基础设施这一课。Flink Agents 不是在 Flink 上简单封装一次 LLM 调用,也不是一个已经稳定的 AgentGraph 编排框架.它把"流处理引擎的可靠性"和"Agent 的灵活性"这两件看似不搭的事拼在了一起。Checkpoint 让多轮对话状态可以容错,背压机制让过载不会雪崩,异步 I/O 让 LLM 的慢调用不阻塞整条管道------这些能力在早期 Agent 框架中几乎都是缺失的,或者需要开发者手写大量代码。

相关推荐
word1 小时前
AI Agent 工作流实战:从线性脚本到可维护的自动化系统
人工智能·产品
空堂与归1 小时前
Prompt 工程从入门到实战:五大原则让你的提示词效果翻倍
人工智能
Hector_zh1 小时前
逐浪 · 第十三篇 TRAE Work 实战:分分钟让AI"记住"你是谁——一次跨会话长期记忆的完整实践
人工智能·ai编程·vibecoding
冬奇Lab1 小时前
代码库知识库系列(11):跨库场景——当一个服务调用另一个服务
人工智能
Goodwin1 小时前
AI编程Token节流实战
人工智能
冬奇Lab1 小时前
开源项目第181期:Open Code Review — 阿里巴巴内部锤炼的 AI 代码审查工具,token 消耗仅为通用 Agent 的 1/9
人工智能·开源·资讯
海上小飞龙1 小时前
Redis 持久化:RDB 与 AOF
数据库·redis·缓存
hold?fish:palm1 小时前
redis中AOF 重写机制解析
数据库·c++·redis