用 LangGraph4j 编排多 Agent 工作流
本文目录 · 30 节
- 01、ChatClient 处理不了什么场景?
- 02、LangGraph4j 的核心模型是什么?
- 03、求职派的数据工作流长什么样?
- 04、状态怎么在节点之间流转?
- 05、对话型 Agent 和工作流型 Agent 怎么选?
- ending
- 01、ChatClient 处理不了什么场景?
- 02、LangGraph4j 的核心模型是什么?
- 03、求职派的数据工作流长什么样?
- 04、状态怎么在节点之间流转?
- 05、对话型 Agent 和工作流型 Agent 怎么选?
- ending
- 01、ChatClient 处理不了什么场景?
- 02、LangGraph4j 的核心模型是什么?
- 03、求职派的数据工作流长什么样?
- 04、状态怎么在节点之间流转?
- 05、对话型 Agent 和工作流型 Agent 怎么选?
- ending
星球:Java进阶之路&二哥编程星球 | 专栏:求职派 OpenClaw 架构 | 作者:沉默王二 | 发布:2026-08-03
用 LangGraph4j 编排多 Agent 工作流
用 LangGraph4j 编排多 Agent 工作流
Spring AI 的 ChatClient 解决的是“一问一答”的问题。
发 Prompt,走调用链,返回结果。
但求职派的岗位数据采集需要把四个步骤串成一条流水线,先判断输入类型,再调用大模型提取结构化信息,然后按业务规则清洗字段,最后发布到正式岗位库。技术派: https://paicoding.com/jobclaw-langgraph4j-multi-agent-workflow

LangGraph4j 是 Java 生态下的 Agent 工作流引擎,核心思路是把多个处理节点编排成一张有向图,让数据按照条件分支流转。
求职派用它来跑岗位数据的自动采集流水线,四个节点、三个条件分支、一个共享的状态对象。
这篇内容会用求职派的生产源码给大家分析清楚——节点和条件边怎么定义、状态怎么在节点之间传递、什么时候该用工作流引擎而不是对话型 Agent。
01、ChatClient 处理不了什么场景?
ChatClient 的执行模式是线性的。Prompt 进来,经过 Advisor 链处理,调用大模型,如果模型需要调用工具就自动回调,最后返回结果。
这个模式已经足以覆盖绝大多数场景了。问答、翻译、摘要、甚至带工具调用的 ReAct 循环,都是“一进一出”的模式。
但有一类任务超出了它的能力范围。
举个例子,用户提交了一段评价文本,系统需要先判断好评还是差评。好评的话自动回复“感谢好评”;差评则要提取关键信息,转给人工客服跟进售后。

这个场景有三个特征是 ChatClient 不具备的。
-
多步骤。判断、回复、提取、转交,至少三到四步独立处理逻辑。
-
条件分支。好评和差评走的是完全不同的执行路径。
-
步骤间的状态传递。后面的步骤需要读取到前面步骤的输出结果。
把这些逻辑全塞进一个 Prompt 里让大模型自己判断,勉强也能跑。
但逻辑一旦复杂起来,你会发现自己在用 Prompt Engineering 模拟一个工作流引擎,而且每个步骤都没有办法独立测试和监控。
LangGraph4j 就是专门用来解决这类多步骤编排问题的。
02、LangGraph4j 的核心模型是什么?
LangGraph4j 是 Python 社区 LangGraph 的 Java 移植版,用“有向图”来描述工作流。
理解三个概念就能上手。

节点
图里的每一个处理步骤就是一个节点。它接收当前的状态对象,执行一段业务逻辑,返回需要更新的状态字段。
节点之间彼此独立,不会直接调用对方的方法。所有的数据传递都通过状态对象来完成。
边
边决定了节点的执行顺序,分为两种。
固定边,A 执行完之后一定走到 B,没有任何条件判断。
条件边,A 执行完之后,根据当前状态里的数据决定走 B 还是走 C,或者直接走到终点,结束。
条件边是工作流引擎和普通顺序调用的核心区别,它让流程有了“拐弯”的能力。
状态
状态是所有节点共享的数据容器。每个节点从状态里读取上游节点的输出,处理完之后把自己的结果写回去。
整张图自始至终只有一个状态实例,随着节点的依次执行,状态里的数据会逐步丰富起来。
这三个概念对应到 LangGraph4j 的代码里,分别是定义图的 StateGraph、注册节点的 addNode、以及连接节点的 addEdge 和 addConditionalEdges。
求职派使用的版本是 1.6.0-rc4。
03、求职派的数据工作流长什么样?
求职派的管理后台有一个“岗位数据采集”功能。管理员可以提交 CSV 文件、Excel 表格、HTML 页面链接或者纯文本,系统会自动从中提取岗位信息,清洗之后发布到正式的岗位库。
整个流程拆成了四个节点,用 LangGraph4j 编排成一张有向图。
-
任务分类,判断输入数据的类型。如果管理员没有手动指定,系统会根据文件后缀名(
.csv、.xlsx、.png)和文本特征(是否包含 HTML 标签、是否是 HTTP 链接)自动识别 -
数据采集,根据分类结果调用对应的采集服务,内部会调用大模型从非结构化内容中提取结构化的岗位信息,然后写入草稿表
-
数据清洗,对草稿数据执行规则化清洗。公司类型归一化(“私企”和“民企”互相映射)、招聘类型标准化、招聘对象年份修正
-
草稿发布,把清洗通过的记录从草稿表转移到正式岗位表

图的定义在工作流执行器的一个内部类里,用链式 API 一气呵成。
Java
return new StateGraph<>(OcAgentState.serializer())
.addNode("task_classify", node_async(taskClassifyAgent::apply))
.addNode("task_gather", node_async(taskGatherAgent::apply))
.addNode("draft_washer", node_async(draftWasherAgent::apply))
.addNode("draft_publish", node_async(draftPublishAgent::apply))
.addEdge(START, "task_classify")
app/.../agents/AgentExecutor.java,第 129-135 行
四个节点注册完毕后, addEdge(START, ...) 把图的入口连到分类节点。 node_async 把每个节点的 apply 方法包装成异步动作,支持非阻塞执行。
关键在条件边。以分类节点到采集节点之间的过渡为例:
Java
.addConditionalEdges("task_classify",
edge_async(state -> state.getTask() == null ? END : "采集"),
EdgeMappings.builder()
.to("task_gather", "采集").toEND().build())
app/.../agents/AgentExecutor.java,第 136-138 行
分类节点执行完后,lambda 表达式会检查状态里有没有合法的任务对象。
没有就走 END 终止整张图;有的话就返回 "采集" 标签,映射表把这个标签映射到采集节点。
后面两个条件边的思路完全相同。
采集节点执行完后检查提取的数据列表是否为空;清洗节点执行完后检查清洗通过的有效记录数是否大于零。任何一步不通过,就会提前终止。

图编译成可执行对象之后,只需要传入采集任务实体就可以启动整条流水线。
执行过程中,每个节点都会通过 SSE 向前端推送实时进度信息,包括节点名称、执行阶段(开始或结束)以及当前处理的数据摘要。
管理员在浏览器上能看到每一步正在跑什么、跑到了哪里。
条件边里的判断逻辑全部是业务数据驱动的。
判断的是“状态里有没有合法对象”“列表是否为空”这类确定性条件,不是让大模型来决定下一步走哪条路。大模型只在数据采集这一个环节被调用,负责从非结构化文本中提取结构化信息。

04、状态怎么在节点之间流转?
四个节点之间通过一个共享的状态对象来传递数据。
状态对象定义了五个字段,分别对应流水线中的每一个阶段。
Java
public static final String INPUT = "input"; // 外部输入的采集任务
public static final String TASK = "task"; // 分类后的任务对象
public static final String GATHER = "gather"; // 采集结果(新增+更新列表)
public static final String WASHER = "washer"; // 清洗后的有效记录 ID
public static final String PUBLISH = "publish"; // 发布成功的岗位数据
app/.../agents/OcAgentState.java,第 27-47 行
每个节点只读取自己上游的字段,处理完之后把结果写到自己负责的字段里。
比如采集节点从 task 字段里读取分类后的任务信息,调用采集服务完成后,把结果写入 gather 字段。
清洗节点再从 gather 字段里读取采集结果,清洗完之后把有效的记录 ID 写入 washer 字段。
这种设计让每个节点的输入和输出都变成了声明式的。看字段名就知道数据从哪来、到哪去。
不过状态对象的底层其实就是一个 Map。直接用默认的序列化器来处理的话,Java 的复杂 POJO 对象在跨节点传递时会出问题。
求职派的解决办法是给每种业务实体注册了自定义的 JSON 序列化器。
Java
public static StateSerializer serializer() {
var serializer = new ObjectStreamStateSerializer<>(OcAgentState::new);
serializer.mapper().register(GatherTaskEntity.class,
new JsonSerializer<>(GatherTaskEntity.class));
serializer.mapper().register(GatherVo.class,
new JsonSerializer<>(GatherVo.class));
return serializer;
}
app/.../agents/OcAgentState.java,第 78-87 行
每种在节点之间传递的业务实体都需要单独注册序列化器,否则状态在节点边界上可能会丢失数据或者抛出序列化异常。

这是 LangGraph4j 在 Java 生态下的一个实践成本。
Python 版本用 pickle 就能搞定对象序列化,Java 版本则需要为每种自定义类型显式注册序列化器。
多出来的几行代码不是大问题,但如果忘了注册某个类型,运行时的报错信息并不直观,排查起来需要一些耐心。
05、对话型 Agent 和工作流型 Agent 怎么选?
求职派里同时运行着两套 Agent 架构,服务于完全不同的场景。
对话型 Agent 处理的是用户在微信、钉钉、飞书上发来的消息。
消息经过渠道适配和意图分类之后,路由到具体的业务 Agent,比如岗位推荐、岗位检索、身份采集。
这些 Agent 基于 Spring AI 的 ChatClient 和 ChatMemory 来构建,核心能力是理解自然语言、调用工具、维持多轮对话的上下文。
工作流型 Agent 处理的是管理后台提交的岗位数据。
管理员通过 REST 接口触发 LangGraph4j 编排的四步流水线。这条流水线不需要理解自然语言,也不需要记忆对话上下文。
它就是一条数据处理管道。
两套架构在代码层面完全独立运行。
对话型 Agent 走消息路由器和意图分类器,工作流型 Agent 走管理后台的 REST 接口和图执行器。
它们共享的只有底层的数据层。清洗发布后的岗位数据,最终会被对话型的岗位推荐 Agent 检索到并推荐给用户。

选择哪套架构的判断标准如下。
-
任务的核心是“理解自然语言并做出响应” → 对话型 Agent
-
任务的核心是“按固定流程处理数据,每一步有明确的输入输出” → 工作流型 Agent
另一个判断维度是路由的确定性。
对话型 Agent 的路由依赖意图分类器,关键词匹配加上大模型复合分类,结果带有一定的不确定性。工作流型 Agent 的路由则完全是业务数据驱动的条件判断,没有任何不确定性。
ending
求职派用 LangGraph4j 编排的不是智能对话,是一条四步确定性流水线。
分类、采集、清洗、发布,每一步的路由都由业务数据驱动,大模型只在数据采集环节被调用,负责从非结构化内容中提取结构化信息。
工作流引擎的价值不在于让 AI 做更多的事情,而在于让 AI 只做它擅长的那一件事。剩下的,交给确定性的业务代码。
我们下期见。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)—— 多 Agent 智能求职系统
项目简介:基于 Spring Boot 4 + Spring AI 2.0 + LangGraph4j 的多 Agent 求职平台,支持微信/钉钉/飞书三渠道接入,具备意图识别、岗位推荐、身份采集等对话型 Agent,以及基于工作流引擎的岗位数据自动采集流水线。
技术栈:Java 21、Spring Boot 4、Spring AI 2.0、LangGraph4j 1.6、JPA/Hibernate、H2/MySQL、React 19 / Next.js 15
核心职责:
-
基于 LangGraph4j 的 StateGraph API 设计并实现了岗位数据采集的工作流(任务分类→数据采集→数据清洗→草稿发布),通过条件边实现自动校验和提前终止机制,数据采集准确率提升至 95% 以上
-
设计对话型 Agent 和工作流型 Agent 的双架构方案,对话型走意图分类路由处理用户自然语言交互,工作流型走确定性流水线处理结构化数据,两套系统完全解耦
-
基于 Spring AI 的 ChatModel 封装多层 LLM 调用器,按用户维度解析模型偏好,支持GLM、DeepSeek 等多供应商的动态切换
-
实现了自定义的 JSON 状态序列化器,解决了 LangGraph4j 在 Java 生态下 POJO 跨节点传递的序列化问题
-
基于 SSE 实现工作流执行过程的实时进度推送,每个节点的启动、完成和异常状态均可在前端管理面板中实时追踪
Spring AI 的 ChatClient 解决的是“一问一答”的问题。
发 Prompt,走调用链,返回结果。
但求职派的岗位数据采集需要把四个步骤串成一条流水线,先判断输入类型,再调用大模型提取结构化信息,然后按业务规则清洗字段,最后发布到正式岗位库。技术派: https://paicoding.com/jobclaw-langgraph4j-multi-agent-workflow

LangGraph4j 是 Java 生态下的 Agent 工作流引擎,核心思路是把多个处理节点编排成一张有向图,让数据按照条件分支流转。
求职派用它来跑岗位数据的自动采集流水线,四个节点、三个条件分支、一个共享的状态对象。
这篇内容会用求职派的生产源码给大家分析清楚——节点和条件边怎么定义、状态怎么在节点之间传递、什么时候该用工作流引擎而不是对话型 Agent。
01、ChatClient 处理不了什么场景?
ChatClient 的执行模式是线性的。Prompt 进来,经过 Advisor 链处理,调用大模型,如果模型需要调用工具就自动回调,最后返回结果。
这个模式已经足以覆盖绝大多数场景了。问答、翻译、摘要、甚至带工具调用的 ReAct 循环,都是“一进一出”的模式。
但有一类任务超出了它的能力范围。
举个例子,用户提交了一段评价文本,系统需要先判断好评还是差评。好评的话自动回复“感谢好评”;差评则要提取关键信息,转给人工客服跟进售后。

这个场景有三个特征是 ChatClient 不具备的。
-
多步骤。判断、回复、提取、转交,至少三到四步独立处理逻辑。
-
条件分支。好评和差评走的是完全不同的执行路径。
-
步骤间的状态传递。后面的步骤需要读取到前面步骤的输出结果。
把这些逻辑全塞进一个 Prompt 里让大模型自己判断,勉强也能跑。
但逻辑一旦复杂起来,你会发现自己在用 Prompt Engineering 模拟一个工作流引擎,而且每个步骤都没有办法独立测试和监控。
LangGraph4j 就是专门用来解决这类多步骤编排问题的。
02、LangGraph4j 的核心模型是什么?
LangGraph4j 是 Python 社区 LangGraph 的 Java 移植版,用“有向图”来描述工作流。
理解三个概念就能上手。

节点
图里的每一个处理步骤就是一个节点。它接收当前的状态对象,执行一段业务逻辑,返回需要更新的状态字段。
节点之间彼此独立,不会直接调用对方的方法。所有的数据传递都通过状态对象来完成。
边
边决定了节点的执行顺序,分为两种。
固定边,A 执行完之后一定走到 B,没有任何条件判断。
条件边,A 执行完之后,根据当前状态里的数据决定走 B 还是走 C,或者直接走到终点,结束。
条件边是工作流引擎和普通顺序调用的核心区别,它让流程有了“拐弯”的能力。
状态
状态是所有节点共享的数据容器。每个节点从状态里读取上游节点的输出,处理完之后把自己的结果写回去。
整张图自始至终只有一个状态实例,随着节点的依次执行,状态里的数据会逐步丰富起来。
这三个概念对应到 LangGraph4j 的代码里,分别是定义图的 StateGraph、注册节点的 addNode、以及连接节点的 addEdge 和 addConditionalEdges。
求职派使用的版本是 1.6.0-rc4。
03、求职派的数据工作流长什么样?
求职派的管理后台有一个“岗位数据采集”功能。管理员可以提交 CSV 文件、Excel 表格、HTML 页面链接或者纯文本,系统会自动从中提取岗位信息,清洗之后发布到正式的岗位库。
整个流程拆成了四个节点,用 LangGraph4j 编排成一张有向图。
-
任务分类,判断输入数据的类型。如果管理员没有手动指定,系统会根据文件后缀名(
.csv、.xlsx、.png)和文本特征(是否包含 HTML 标签、是否是 HTTP 链接)自动识别 -
数据采集,根据分类结果调用对应的采集服务,内部会调用大模型从非结构化内容中提取结构化的岗位信息,然后写入草稿表
-
数据清洗,对草稿数据执行规则化清洗。公司类型归一化(“私企”和“民企”互相映射)、招聘类型标准化、招聘对象年份修正
-
草稿发布,把清洗通过的记录从草稿表转移到正式岗位表

图的定义在工作流执行器的一个内部类里,用链式 API 一气呵成。
Java
return new StateGraph<>(OcAgentState.serializer())
.addNode("task_classify", node_async(taskClassifyAgent::apply))
.addNode("task_gather", node_async(taskGatherAgent::apply))
.addNode("draft_washer", node_async(draftWasherAgent::apply))
.addNode("draft_publish", node_async(draftPublishAgent::apply))
.addEdge(START, "task_classify")
app/.../agents/AgentExecutor.java,第 129-135 行
四个节点注册完毕后, addEdge(START, ...) 把图的入口连到分类节点。 node_async 把每个节点的 apply 方法包装成异步动作,支持非阻塞执行。
关键在条件边。以分类节点到采集节点之间的过渡为例:
Java
.addConditionalEdges("task_classify",
edge_async(state -> state.getTask() == null ? END : "采集"),
EdgeMappings.builder()
.to("task_gather", "采集").toEND().build())
app/.../agents/AgentExecutor.java,第 136-138 行
分类节点执行完后,lambda 表达式会检查状态里有没有合法的任务对象。
没有就走 END 终止整张图;有的话就返回 "采集" 标签,映射表把这个标签映射到采集节点。
后面两个条件边的思路完全相同。
采集节点执行完后检查提取的数据列表是否为空;清洗节点执行完后检查清洗通过的有效记录数是否大于零。任何一步不通过,就会提前终止。

图编译成可执行对象之后,只需要传入采集任务实体就可以启动整条流水线。
执行过程中,每个节点都会通过 SSE 向前端推送实时进度信息,包括节点名称、执行阶段(开始或结束)以及当前处理的数据摘要。
管理员在浏览器上能看到每一步正在跑什么、跑到了哪里。
条件边里的判断逻辑全部是业务数据驱动的。
判断的是“状态里有没有合法对象”“列表是否为空”这类确定性条件,不是让大模型来决定下一步走哪条路。大模型只在数据采集这一个环节被调用,负责从非结构化文本中提取结构化信息。

04、状态怎么在节点之间流转?
四个节点之间通过一个共享的状态对象来传递数据。
状态对象定义了五个字段,分别对应流水线中的每一个阶段。
Java
public static final String INPUT = "input"; // 外部输入的采集任务
public static final String TASK = "task"; // 分类后的任务对象
public static final String GATHER = "gather"; // 采集结果(新增+更新列表)
public static final String WASHER = "washer"; // 清洗后的有效记录 ID
public static final String PUBLISH = "publish"; // 发布成功的岗位数据
app/.../agents/OcAgentState.java,第 27-47 行
每个节点只读取自己上游的字段,处理完之后把结果写到自己负责的字段里。
比如采集节点从 task 字段里读取分类后的任务信息,调用采集服务完成后,把结果写入 gather 字段。
清洗节点再从 gather 字段里读取采集结果,清洗完之后把有效的记录 ID 写入 washer 字段。
这种设计让每个节点的输入和输出都变成了声明式的。看字段名就知道数据从哪来、到哪去。
不过状态对象的底层其实就是一个 Map。直接用默认的序列化器来处理的话,Java 的复杂 POJO 对象在跨节点传递时会出问题。
求职派的解决办法是给每种业务实体注册了自定义的 JSON 序列化器。
Java
public static StateSerializer serializer() {
var serializer = new ObjectStreamStateSerializer<>(OcAgentState::new);
serializer.mapper().register(GatherTaskEntity.class,
new JsonSerializer<>(GatherTaskEntity.class));
serializer.mapper().register(GatherVo.class,
new JsonSerializer<>(GatherVo.class));
return serializer;
}
app/.../agents/OcAgentState.java,第 78-87 行
每种在节点之间传递的业务实体都需要单独注册序列化器,否则状态在节点边界上可能会丢失数据或者抛出序列化异常。

这是 LangGraph4j 在 Java 生态下的一个实践成本。
Python 版本用 pickle 就能搞定对象序列化,Java 版本则需要为每种自定义类型显式注册序列化器。
多出来的几行代码不是大问题,但如果忘了注册某个类型,运行时的报错信息并不直观,排查起来需要一些耐心。
05、对话型 Agent 和工作流型 Agent 怎么选?
求职派里同时运行着两套 Agent 架构,服务于完全不同的场景。
对话型 Agent 处理的是用户在微信、钉钉、飞书上发来的消息。
消息经过渠道适配和意图分类之后,路由到具体的业务 Agent,比如岗位推荐、岗位检索、身份采集。
这些 Agent 基于 Spring AI 的 ChatClient 和 ChatMemory 来构建,核心能力是理解自然语言、调用工具、维持多轮对话的上下文。
工作流型 Agent 处理的是管理后台提交的岗位数据。
管理员通过 REST 接口触发 LangGraph4j 编排的四步流水线。这条流水线不需要理解自然语言,也不需要记忆对话上下文。
它就是一条数据处理管道。
两套架构在代码层面完全独立运行。
对话型 Agent 走消息路由器和意图分类器,工作流型 Agent 走管理后台的 REST 接口和图执行器。
它们共享的只有底层的数据层。清洗发布后的岗位数据,最终会被对话型的岗位推荐 Agent 检索到并推荐给用户。

选择哪套架构的判断标准如下。
-
任务的核心是“理解自然语言并做出响应” → 对话型 Agent
-
任务的核心是“按固定流程处理数据,每一步有明确的输入输出” → 工作流型 Agent
另一个判断维度是路由的确定性。
对话型 Agent 的路由依赖意图分类器,关键词匹配加上大模型复合分类,结果带有一定的不确定性。工作流型 Agent 的路由则完全是业务数据驱动的条件判断,没有任何不确定性。
ending
求职派用 LangGraph4j 编排的不是智能对话,是一条四步确定性流水线。
分类、采集、清洗、发布,每一步的路由都由业务数据驱动,大模型只在数据采集环节被调用,负责从非结构化内容中提取结构化信息。
工作流引擎的价值不在于让 AI 做更多的事情,而在于让 AI 只做它擅长的那一件事。剩下的,交给确定性的业务代码。
我们下期见。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)—— 多 Agent 智能求职系统
项目简介:基于 Spring Boot 4 + Spring AI 2.0 + LangGraph4j 的多 Agent 求职平台,支持微信/钉钉/飞书三渠道接入,具备意图识别、岗位推荐、身份采集等对话型 Agent,以及基于工作流引擎的岗位数据自动采集流水线。
技术栈:Java 21、Spring Boot 4、Spring AI 2.0、LangGraph4j 1.6、JPA/Hibernate、H2/MySQL、React 19 / Next.js 15
核心职责:
-
基于 LangGraph4j 的 StateGraph API 设计并实现了岗位数据采集的工作流(任务分类→数据采集→数据清洗→草稿发布),通过条件边实现自动校验和提前终止机制,数据采集准确率提升至 95% 以上
-
设计对话型 Agent 和工作流型 Agent 的双架构方案,对话型走意图分类路由处理用户自然语言交互,工作流型走确定性流水线处理结构化数据,两套系统完全解耦
-
基于 Spring AI 的 ChatModel 封装多层 LLM 调用器,按用户维度解析模型偏好,支持GLM、DeepSeek 等多供应商的动态切换
-
实现了自定义的 JSON 状态序列化器,解决了 LangGraph4j 在 Java 生态下 POJO 跨节点传递的序列化问题
-
基于 SSE 实现工作流执行过程的实时进度推送,每个节点的启动、完成和异常状态均可在前端管理面板中实时追踪
Spring AI 的 ChatClient 解决的是“一问一答”的问题。
发 Prompt,走调用链,返回结果。
但求职派的岗位数据采集需要把四个步骤串成一条流水线,先判断输入类型,再调用大模型提取结构化信息,然后按业务规则清洗字段,最后发布到正式岗位库。技术派: https://paicoding.com/jobclaw-langgraph4j-multi-agent-workflow

LangGraph4j 是 Java 生态下的 Agent 工作流引擎,核心思路是把多个处理节点编排成一张有向图,让数据按照条件分支流转。
求职派用它来跑岗位数据的自动采集流水线,四个节点、三个条件分支、一个共享的状态对象。
这篇内容会用求职派的生产源码给大家分析清楚——节点和条件边怎么定义、状态怎么在节点之间传递、什么时候该用工作流引擎而不是对话型 Agent。
01、ChatClient 处理不了什么场景?
ChatClient 的执行模式是线性的。Prompt 进来,经过 Advisor 链处理,调用大模型,如果模型需要调用工具就自动回调,最后返回结果。
这个模式已经足以覆盖绝大多数场景了。问答、翻译、摘要、甚至带工具调用的 ReAct 循环,都是“一进一出”的模式。
但有一类任务超出了它的能力范围。
举个例子,用户提交了一段评价文本,系统需要先判断好评还是差评。好评的话自动回复“感谢好评”;差评则要提取关键信息,转给人工客服跟进售后。

这个场景有三个特征是 ChatClient 不具备的。
-
多步骤。判断、回复、提取、转交,至少三到四步独立处理逻辑。
-
条件分支。好评和差评走的是完全不同的执行路径。
-
步骤间的状态传递。后面的步骤需要读取到前面步骤的输出结果。
把这些逻辑全塞进一个 Prompt 里让大模型自己判断,勉强也能跑。
但逻辑一旦复杂起来,你会发现自己在用 Prompt Engineering 模拟一个工作流引擎,而且每个步骤都没有办法独立测试和监控。
LangGraph4j 就是专门用来解决这类多步骤编排问题的。
02、LangGraph4j 的核心模型是什么?
LangGraph4j 是 Python 社区 LangGraph 的 Java 移植版,用“有向图”来描述工作流。
理解三个概念就能上手。

节点
图里的每一个处理步骤就是一个节点。它接收当前的状态对象,执行一段业务逻辑,返回需要更新的状态字段。
节点之间彼此独立,不会直接调用对方的方法。所有的数据传递都通过状态对象来完成。
边
边决定了节点的执行顺序,分为两种。
固定边,A 执行完之后一定走到 B,没有任何条件判断。
条件边,A 执行完之后,根据当前状态里的数据决定走 B 还是走 C,或者直接走到终点,结束。
条件边是工作流引擎和普通顺序调用的核心区别,它让流程有了“拐弯”的能力。
状态
状态是所有节点共享的数据容器。每个节点从状态里读取上游节点的输出,处理完之后把自己的结果写回去。
整张图自始至终只有一个状态实例,随着节点的依次执行,状态里的数据会逐步丰富起来。
这三个概念对应到 LangGraph4j 的代码里,分别是定义图的 StateGraph、注册节点的 addNode、以及连接节点的 addEdge 和 addConditionalEdges。
求职派使用的版本是 1.6.0-rc4。
03、求职派的数据工作流长什么样?
求职派的管理后台有一个“岗位数据采集”功能。管理员可以提交 CSV 文件、Excel 表格、HTML 页面链接或者纯文本,系统会自动从中提取岗位信息,清洗之后发布到正式的岗位库。
整个流程拆成了四个节点,用 LangGraph4j 编排成一张有向图。
-
任务分类,判断输入数据的类型。如果管理员没有手动指定,系统会根据文件后缀名(
.csv、.xlsx、.png)和文本特征(是否包含 HTML 标签、是否是 HTTP 链接)自动识别 -
数据采集,根据分类结果调用对应的采集服务,内部会调用大模型从非结构化内容中提取结构化的岗位信息,然后写入草稿表
-
数据清洗,对草稿数据执行规则化清洗。公司类型归一化(“私企”和“民企”互相映射)、招聘类型标准化、招聘对象年份修正
-
草稿发布,把清洗通过的记录从草稿表转移到正式岗位表

图的定义在工作流执行器的一个内部类里,用链式 API 一气呵成。
Java
return new StateGraph<>(OcAgentState.serializer())
.addNode("task_classify", node_async(taskClassifyAgent::apply))
.addNode("task_gather", node_async(taskGatherAgent::apply))
.addNode("draft_washer", node_async(draftWasherAgent::apply))
.addNode("draft_publish", node_async(draftPublishAgent::apply))
.addEdge(START, "task_classify")
app/.../agents/AgentExecutor.java,第 129-135 行
四个节点注册完毕后, addEdge(START, ...) 把图的入口连到分类节点。 node_async 把每个节点的 apply 方法包装成异步动作,支持非阻塞执行。
关键在条件边。以分类节点到采集节点之间的过渡为例:
Java
.addConditionalEdges("task_classify",
edge_async(state -> state.getTask() == null ? END : "采集"),
EdgeMappings.builder()
.to("task_gather", "采集").toEND().build())
app/.../agents/AgentExecutor.java,第 136-138 行
分类节点执行完后,lambda 表达式会检查状态里有没有合法的任务对象。
没有就走 END 终止整张图;有的话就返回 "采集" 标签,映射表把这个标签映射到采集节点。
后面两个条件边的思路完全相同。
采集节点执行完后检查提取的数据列表是否为空;清洗节点执行完后检查清洗通过的有效记录数是否大于零。任何一步不通过,就会提前终止。

图编译成可执行对象之后,只需要传入采集任务实体就可以启动整条流水线。
执行过程中,每个节点都会通过 SSE 向前端推送实时进度信息,包括节点名称、执行阶段(开始或结束)以及当前处理的数据摘要。
管理员在浏览器上能看到每一步正在跑什么、跑到了哪里。
条件边里的判断逻辑全部是业务数据驱动的。
判断的是“状态里有没有合法对象”“列表是否为空”这类确定性条件,不是让大模型来决定下一步走哪条路。大模型只在数据采集这一个环节被调用,负责从非结构化文本中提取结构化信息。

04、状态怎么在节点之间流转?
四个节点之间通过一个共享的状态对象来传递数据。
状态对象定义了五个字段,分别对应流水线中的每一个阶段。
Java
public static final String INPUT = "input"; // 外部输入的采集任务
public static final String TASK = "task"; // 分类后的任务对象
public static final String GATHER = "gather"; // 采集结果(新增+更新列表)
public static final String WASHER = "washer"; // 清洗后的有效记录 ID
public static final String PUBLISH = "publish"; // 发布成功的岗位数据
app/.../agents/OcAgentState.java,第 27-47 行
每个节点只读取自己上游的字段,处理完之后把结果写到自己负责的字段里。
比如采集节点从 task 字段里读取分类后的任务信息,调用采集服务完成后,把结果写入 gather 字段。
清洗节点再从 gather 字段里读取采集结果,清洗完之后把有效的记录 ID 写入 washer 字段。
这种设计让每个节点的输入和输出都变成了声明式的。看字段名就知道数据从哪来、到哪去。
不过状态对象的底层其实就是一个 Map。直接用默认的序列化器来处理的话,Java 的复杂 POJO 对象在跨节点传递时会出问题。
求职派的解决办法是给每种业务实体注册了自定义的 JSON 序列化器。
Java
public static StateSerializer serializer() {
var serializer = new ObjectStreamStateSerializer<>(OcAgentState::new);
serializer.mapper().register(GatherTaskEntity.class,
new JsonSerializer<>(GatherTaskEntity.class));
serializer.mapper().register(GatherVo.class,
new JsonSerializer<>(GatherVo.class));
return serializer;
}
app/.../agents/OcAgentState.java,第 78-87 行
每种在节点之间传递的业务实体都需要单独注册序列化器,否则状态在节点边界上可能会丢失数据或者抛出序列化异常。

这是 LangGraph4j 在 Java 生态下的一个实践成本。
Python 版本用 pickle 就能搞定对象序列化,Java 版本则需要为每种自定义类型显式注册序列化器。
多出来的几行代码不是大问题,但如果忘了注册某个类型,运行时的报错信息并不直观,排查起来需要一些耐心。
05、对话型 Agent 和工作流型 Agent 怎么选?
求职派里同时运行着两套 Agent 架构,服务于完全不同的场景。
对话型 Agent 处理的是用户在微信、钉钉、飞书上发来的消息。
消息经过渠道适配和意图分类之后,路由到具体的业务 Agent,比如岗位推荐、岗位检索、身份采集。
这些 Agent 基于 Spring AI 的 ChatClient 和 ChatMemory 来构建,核心能力是理解自然语言、调用工具、维持多轮对话的上下文。
工作流型 Agent 处理的是管理后台提交的岗位数据。
管理员通过 REST 接口触发 LangGraph4j 编排的四步流水线。这条流水线不需要理解自然语言,也不需要记忆对话上下文。
它就是一条数据处理管道。
两套架构在代码层面完全独立运行。
对话型 Agent 走消息路由器和意图分类器,工作流型 Agent 走管理后台的 REST 接口和图执行器。
它们共享的只有底层的数据层。清洗发布后的岗位数据,最终会被对话型的岗位推荐 Agent 检索到并推荐给用户。

选择哪套架构的判断标准如下。
-
任务的核心是“理解自然语言并做出响应” → 对话型 Agent
-
任务的核心是“按固定流程处理数据,每一步有明确的输入输出” → 工作流型 Agent
另一个判断维度是路由的确定性。
对话型 Agent 的路由依赖意图分类器,关键词匹配加上大模型复合分类,结果带有一定的不确定性。工作流型 Agent 的路由则完全是业务数据驱动的条件判断,没有任何不确定性。
ending
求职派用 LangGraph4j 编排的不是智能对话,是一条四步确定性流水线。
分类、采集、清洗、发布,每一步的路由都由业务数据驱动,大模型只在数据采集环节被调用,负责从非结构化内容中提取结构化信息。
工作流引擎的价值不在于让 AI 做更多的事情,而在于让 AI 只做它擅长的那一件事。剩下的,交给确定性的业务代码。
我们下期见。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)—— 多 Agent 智能求职系统
项目简介:基于 Spring Boot 4 + Spring AI 2.0 + LangGraph4j 的多 Agent 求职平台,支持微信/钉钉/飞书三渠道接入,具备意图识别、岗位推荐、身份采集等对话型 Agent,以及基于工作流引擎的岗位数据自动采集流水线。
技术栈:Java 21、Spring Boot 4、Spring AI 2.0、LangGraph4j 1.6、JPA/Hibernate、H2/MySQL、React 19 / Next.js 15
核心职责:
-
基于 LangGraph4j 的 StateGraph API 设计并实现了岗位数据采集的工作流(任务分类→数据采集→数据清洗→草稿发布),通过条件边实现自动校验和提前终止机制,数据采集准确率提升至 95% 以上
-
设计对话型 Agent 和工作流型 Agent 的双架构方案,对话型走意图分类路由处理用户自然语言交互,工作流型走确定性流水线处理结构化数据,两套系统完全解耦
-
基于 Spring AI 的 ChatModel 封装多层 LLM 调用器,按用户维度解析模型偏好,支持GLM、DeepSeek 等多供应商的动态切换
-
实现了自定义的 JSON 状态序列化器,解决了 LangGraph4j 在 Java 生态下 POJO 跨节点传递的序列化问题
-
基于 SSE 实现工作流执行过程的实时进度推送,每个节点的启动、完成和异常状态均可在前端管理面板中实时追踪