Flink Agents:Python 执行链路与跨语言 Actor 演进分析 (PyFlink Agent)
本篇主要分析 Flink Agents 框架是如何在 Java 编写的 Flink 引擎中,完美嵌入并执行 Python 编写的 Agent 逻辑的。重点解析 PythonEnvironmentManager 的环境隔离机制,以及 PythonActionExecutor 如何通过 Pemja 实现跨语言的 Coroutine (协程) 调度与内存安全。
1. 为什么需要跨语言支持?(痛点)
在数据处理领域,Flink (Java/Scala) 是绝对的王者,但在大模型 (LLM) 和 AI Agent 领域,Python 拥有最丰富的生态(如 LangChain、LlamaIndex、各种大模型 SDK)。
如果让 AI 算法工程师用 Java 去重写所有的 Prompt 模板和工具链,几乎是不可能完成的任务。因此,框架必须支持用 Python 写 Agent 逻辑,用 Flink Java 引擎做分布式调度。
1.1 AgentPlan 的边界:它是蓝图,不是 Java 编译产物
在理解跨语言链路之前,必须先把一个误区打掉:Python Agent 变成 JSON 之后,并不等于"后面就只剩 Java 代码了"。
-
AgentPlan 包含什么:
actions:有哪些 Action,以及每个 Action 的执行体exec是什么;AgentPlan.java#L76-L85actionsByEvent:某个 Event 类型会触发哪些 Action;AgentPlan.java#L79-L80resourceProviders:模型、工具、Prompt、MCP Server 等资源如何解析;AgentPlan.java#L82-L85config:Agent 的运行配置。AgentPlan.java#L85-L85
-
Python 侧是怎么生成 Plan 的:
- Python 不会把函数体编译成 Java 方法,而是把函数包装成
PythonFunction,只记录它的module和qualname;agent_plan.py#L215-L249 - 因此,进入 JSON 的不是"可直接在 JVM 内执行的 Java 逻辑",而是"去哪里找到这个 Python 函数"的引用信息。
- Python 不会把函数体编译成 Java 方法,而是把函数包装成
-
JSON 到 Java 后发生什么:
- Java 反序列化
Action时,会根据func_type判断exec是JavaFunction还是PythonFunction;ActionJsonDeserializer.java#L57-L67 - 如果是
PythonFunction,Java 只会恢复出一个(module, qualName)引用对象;ActionJsonDeserializer.java#L98-L101 - 真正执行时,仍然要通过解释器调用 Python:
interpreter.invoke("function.call_python_function", module, qualName, args)。PythonFunction.java#L42-L49
- Java 反序列化
-
为什么不能"既然有 JSON,就都用 Java":
- 因为 JSON 只序列化了结构和引用,没有把 Python 函数语义翻译成 Java 字节码。
- 更准确地说:
AgentPlan统一的是控制平面:谁监听谁、资源从哪来、运行时如何装配;- Python 函数保留的是执行平面:真正的业务逻辑仍然在 Python 解释器里。
-
一个最小例子:
-
假设 Python 用户写了:
python@action(InputEvent) async def enrich(event, ctx): return await call_model(event.input) -
进入
AgentPlan后,更接近下面这种形态:json{ "name": "enrich", "exec": { "func_type": "PythonFunction", "module": "my_agent", "qualname": "enrich" } } -
所以 Java 运行时拿到的不是"已经变成 Java 的
enrich()",而只是"去 Python 里调用my_agent.enrich的地址"。这就是为什么后面仍然必须回调 Python。
-
2. 隔离与启动:PythonEnvironmentManager
当一个 Flink 集群(TaskManager)被启动时,它可能要运行成百上千个不同的 Python Agent,如何保证它们的依赖不冲突?
参考代码:PythonEnvironmentManager.java。
- 过程 (How) :
框架利用了 Flink 现有的AbstractPythonEnvironmentManager。当用户提交 Job 时,可以通过addPythonFiles或addPythonArchives提交一个带有特定依赖的.zip或venv。
PythonEnvironmentManager在createEnvironment()时,会动态解析这些 Archives,并重新配置PYTHONPATH和PYTHONHOME。 - 原理 (Why) :
这确保了即使在同一个 TaskManager 上,不同的 Flink Job 也能拥有相互隔离的 Python 虚拟环境 。算法工程师可以在本地打好包,丢给 Flink 集群跑,完全不需要让运维去集群机器上pip install。
3. 跨语言通信桥梁:Pemja 与 PythonActionExecutor
有了 Python 环境,接下来的难题是:Java 算子 (ActionExecutionOperator) 怎么去调用 Python 的函数?并且还要支持异步!
框架使用了 Pemja (基于 JNI 的高性能 Java-Python 桥接库),并封装了 PythonActionExecutor。
参考代码:PythonActionExecutor.java。
3.1 环境初始化 (The Setup)
在算子启动阶段 (open):
interpreter.exec(PYTHON_IMPORTS):导入 Python 侧的桥接代码。interpreter.invoke(CREATE_FLINK_RUNNER_CONTEXT, ...):在 Python 解释器中创建一个对应的 Python 侧的 RunnerContext 对象 。这非常关键!它意味着 Python 代码操作的context.memory,底层实际上会通过 Pemja 回调到 Java 的RunnerContextImpl。
3.2 协程 (Coroutine) 的跨语言调度难题
这是整个执行链路中最硬核的部分。
Python 的 Agent 代码通常是 async def (协程) 编写的。但 Java 算子是一个普通的线程,它怎么去"驱动" Python 的协程?
参考 executePythonFunction 方法:PythonActionExecutor.java#L123-L149。
- 第一步:启动协程并拦截 Awaitable
当 Java 调用function.call()时,因为 Python 函数是async的,它并不会立刻执行到底,而是立刻返回一个 Python 的协程对象 (Coroutine/Awaitable)。 - 第二步:Pemja 的引用计数保活陷阱 (The GC Trap)
这里有一个巨大的坑:由于 Pemja 的对象生命周期管理机制,如果 Java 侧只拿到一个指针,Python 解释器可能会认为这个协程没用了,直接触发 GC 把这个协程垃圾回收掉!
解法 :框架在 Java 侧生成了一个唯一的变量名PYTHON_AWAITABLE_VAR_NAME_PREFIX + ID(例如python_awaitable_1)。然后调用interpreter.set(pythonAwaitableRef, calledResult),强行把这个协程对象挂载到 Python 解释器的全局命名空间中。这人为地增加了引用计数,防止了被 GC。 - 第三步:返回引用句柄
Java 侧的executePythonFunction并不返回执行结果,而是返回这个字符串句柄"python_awaitable_1"给上层的PythonActionTask。
3.3 协程的唤醒与驱动 (PythonGeneratorActionTask)
拿到这个句柄后,Java 算子怎么继续推动 Python 协程往下走?
参考 PythonGeneratorActionTask.java 和 callPythonAwaitable。
- Java 算子会将这个 Task 挂起放入信箱。
- 当异步网络回调触发时,算子拿出
PythonGeneratorActionTask并执行。 - 它调用
executor.callPythonAwaitable("python_awaitable_1")。 - 在 Python 侧,这实际上执行了类似
awaitable.send(None)的操作,推动 Python 协程执行到下一个await挂起点,或者直到结束。 - 如果结束了,返回
true,Java 侧就可以去清理全局变量;如果没结束,Java 侧再次生成一个PythonGeneratorActionTask,继续挂起等待下一次唤醒。
4. 架构意义:Actor 模型的跨语言投影
这种设计的本质是**"跨语言的 Actor 状态机投影"**。
- Java 侧的
ActionExecutionOperator是主 Actor。 - 它通过
PythonAwaitableRef这个"线",牵着 Python 侧那个被挂起的协程对象。 - 每次收到事件,Java 扯一下线 (
callPythonAwaitable),Python 协程就动一下。 - 而 Python 协程中调用的
memory.set(),又会通过 Pemja 反向投影回 Java 的MapState中。
这使得 Flink Agents 能够以极低的序列化开销(不需要通过 HTTP 或 RPC 通信,而是直接通过进程内的 JNI 共享内存),实现了分布式流计算引擎 与 Python AI 生态 的完美融合。