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 三类记忆的职责边界

-
Sensory Memory:单次 Agent Run 的工作区。感官记忆适合保存本轮执行中的中间结果。它按 Key 隔离,由 Flink State 承载,在运行期间会进入 Checkpoint,但一次 Agent Run 完成后会自动清理。
-
Short-Term Memory:同一 Key 的跨轮记忆。短期记忆同样基于 Flink Keyed State,但不会在本轮结束时自动清除,适合保存对话历史、最近一次决策和会话属性。它保存的是原始、精确数据,并支持 TTL。
-
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 框架中几乎都是缺失的,或者需要开发者手写大量代码。