链
什么是链
链的概念源于 Python 的 LangChain,在 LCEL 引入之前。链将多个底层组件结合起来,并协调它们之间的交互。
对于链,首先要了解一些内容,就是
LangChain4j 的 Chain
类是「遗留概念(legacy)」。 官方只实现了
ConversationalChain 和
ConversationalRetrievalChain
两个内容,源码注释原文写着 “Chains are not going to be
developed further, it is recommended to use AiServices
instead.”,并明确「暂不打算再增加」。其核心问题是太死板、难定制。AI
服务可以像普通 Java 对象一样使用,同样功能下这比设计各种 Chain
更灵活。
ConversationalChain是最简单的聊天链。其实今天对应的就是Assistant assistant = AiServices.create(Assistant.class, model);
ConversationalRetrievalChain对应的就是RAG我认为 LangChain4j 官方放弃链的很大原因还是来自 Java 本身已经有控制流了,不想重复造轮子,所以说我们这里核心内容还是了解如何编排工作流
官方主推的「链」是「Chaining multiple AI Services」,也就是多个 AI Service 的链式编排。所以,在这里,我们只讨论多个 AI Service 的链式编排,把复杂逻辑拆成小而专一的 AI Service,再用普通 Java 控制流编排起来。这就是本 demo 的主线,legacy 链就简单介绍了
本质上就是,把多个步骤串联起来形成一个工作流,就是多个组件的流水线
链的示例项目
Legacy 链
我写这个例子,主要目的就是一个,LangChain4j 官方为什么放弃 Chain,转而走向 AiServices 的道路上?
它能做什么:ConversationalChain 本质上就是
ChatModel 加上一个 ChatMemory 的固定封装:每次
execute
会自动把用户消息加入记忆、调用模型、再把回复存回记忆。 对比之前
chat 模块里手写的
ChatService.chat(),它省了几行样板,但也仅此而已,这正是它「死板」的体现。
1 | public class LegacyChainService { |
实际上它内部干的事情非常简单。ConversationalChain = ChatModel + ChatMemory
的固定封装
那么,我们的 execute() 内部发生了什么?
假设 chain.execute("你好")
执行,第一步:创建 UserMessage
1 | UserMessage userMessage = UserMessage.from("你好"); |
第二步:加入 ChatMemory
1 | memory.add(userMessage); |
第三步:读取 Memory
1 | List<ChatMessage> messages = |
第四步:调用 ChatModel
1 | ChatResponse response = chatModel.chat(messages); |
发送给大模型
第五步:得到回复
1 | Assistant: |
第六步:存回 Memory
1 | memory.add(aiMessage); |
第七步:返回文本
1 | return aiMessage.text(); |
然会你会发现,你不管是插入 Tool,插入 RAG,返回 POJO,你都没有地方可以很方便的插入了
如果用 AiServices 怎么做?其实也非常简单了
1 | interface Assistant { |
然后构建这个 Assistant 就可以了,你会发现 AI Service 中各种东西你都可以很方便的加进来
所以说,LegacyChainService
最重要的价值并不是它还能不能用,而且它的定位,它解决的是 2023 年早期 LLM
应用带记忆聊天的问题;而现代 LangChain4j 已经把 AI Service 当作 Spring
Bean 一样的组件,通过 Java 的 if/switch/for/while 来编排工作流,因此
Legacy Chain 被历史淘汰了。
顺序链
顺序链是最基础的链式编排模式就是把多个 AI Service 串起来,上一个的输出自动成为下一个的输入。这就是最基本的工作流
1 | /** |
在咱们写的代码中,就是这样的一个形式
1 | 长文本 → Summarizer.summarize() → 摘要 → Translator.translate() → 翻译结果 |
官方文档明确指出:不要把一堆指令塞进同一个超长 System Prompt,包括你从外部引入的提示词,原因很明显,注意力分配问题会导致 LLM 会漏看,指令互相干扰,成本高
这样我们写了两个原子的 AI Service 接口之后,我们就需要给他串起来
1 | /** |
很明显,整个编排的过程中,没有特殊的 Chain 类,就是方法调用的串联。所以说,链这次我们不在具体指一些内容,而是一种 AI Agent 开发的思维,LLM 不再是黑盒的单调用,而是被拆成多个功能单一的 AI 组件,由代码掌控流程
循环链
1 | public class IterativeChainServices { |
我们对这样的两个 AI Service 接口进行编排
1 | public Map<String, Object> iterativeChain(String topic, int targetScore, int maxRounds) { |
这是 Agent 的雏形:行动(生成)→ 观察(评分)→ 反思(改进意见)→ 再行动
很明显,我们用到了 LLM 反思(self-reflection) 的提示词工程的模式
路由链
Boolean 路由链就是 LLM 驱动的 if/else
我们来看代码
1 | public interface GreetingExpert { |
返回类型 boolean 会触发
BooleanOutputParser,框架自动将 LLM 输出解析为
true/false,这里结构化输出那边讲过了
然后还有一个 Enum 路由链,这种有限输出域的情况下,都可以做这种路由链
1 | // RoutingChainServices.java - 定义枚举 |
但是在这里,枚举常量名必须是 LLM 能够理解并输出的词
框架用 EnumOutputParser 的是解析,不需要 JSON
模式,这里自动解析的内容,就又回到了结构化输出的内容了,就是
EnumOutputParser 是如何将 LLM
的自然语言输出解析成你定义的 Java
枚举的。所以这里就不说了
混合 workflow
混合 workflow 就是在一个更加复杂的情况下,使用了各种的路由模式,产生一个更接近真实业务的综合编排。
真实应用往往不是单一模式,而是把顺序、分支等多种编排组合使用。
整个决策流程清晰地写在 Java 代码里,就能可读、可测、可控,这正是确定性编排的价值。
当流程的分支判断越来越多、越来越难以穷举时,你自然会想到让 LLM 自己决定,那就进入 Agent 的世界了
1 | /** |
因为内容上面都提到了,这里就不细说了
AI Agent
Agents 的基础内容
Agent 是什么与 Agentic 系统
就首先,Agent 和 workflow 肯定要分开说的,虽然他们功能相似
那么对于这样的一个流程,用户问题 → LLM → Tool A → Tool B → 结果,其中,Tool A → Tool B这个顺序是谁决定的,是谁给出的操作,如果是提前写死的流程,那么这是
Workflow,不是 Agent。所以说,在 workflow
中,模型没有决策权,因为流程已经确定。
所以说,与 workflow 不同的是,Agent 中 LLM 不再只是生成答案,而是成为执行过程中的决策者。即,LLM有决策权。
AI Agent 称为智能体,或者人工智能代理,本质是自动执行任务的程序,核心在于让模型不只回答问题,而是按步骤完成动作。它能够感知环境、进行决策并执行行动,以达成特定目标的智能软件实体,它不仅仅是回答问题的聊天机器人,更是能够动手做事的智能执行者。
Agent = LLM (大脑) + Planning (规划) + Tool use (执行) + Memory (记忆)。
普通的 LLM 只是 One-shot(一次性) 的响应,而 Agent 的核心在于 Iterative(迭代)。而 Agent 的基础步骤基本就是,推理,决策,行动,观察。现在总提到一个词 Loop,跟这个就有关系,但是根本上,也跟 ReACT(推理 + 行动) 离不开关系,自然很像。
那么,什么是 Agentic System 呢?这个没有一个准确的说法,但是实际上,Agentic System 就是以 Agent 为核心构建的系统。通常情况下,Agentic System 中已经不是单个 Agent,是多 Agent 协作系统
AI Agent 核心组件
一个功能完整的 AI Agent 通常模仿人类的认知和行动循环,包含以下几个关键模块:
或者这么分类也是很常见的
- 感知层,包括用户的输入,环境的状态等等,总之,感知层的输入经过整合,形成 Agent 的当前上下文,送入LLM进行理解和决策。
- 大脑就是 LLM,做意图理解,推理决策,工具调用
- 工具层就是能把决策转化为真实动作的执行单元。
以上各组件并非孤立存在,它们组成一个持续迭代的感知—思考—行动—观察闭环,这就是Agent Loop。Agent 不断重复这个循环,直到任务完成或达到终止条件。
这个循环让 Agent 具备了在失败时自我纠错的能力:如果某一步工具调用返回了错误或意外结果,观察阶段会将这个信息反馈给大脑,大脑在下一轮思考时就会调整策略。
而且一个合格的 AI Agent 通常具备以下特征:
- 自主
- 感知环境
- 主动行动
- 能与人类或其他 Agent 交互
- 学习能力
那么,更具体的组件组织情况和工作情况的图如下,这张图也会对接我们下面提到的 Agent 架构
Agent 架构
Agent 的工作方式本质上是一个循环(Loop)——感知当前状态,推理下一步,执行行动,再次感知……直到任务完成。
不同架构的差异,就在于如何组织和扩展这个基本循环。
单 Agent 循环
最基础也最直观的架构,一个 Agent 从头到尾独立完成所有任务。
单 Agent 循环直接体现了 ReAct 模式,每一步都是先想再做。LLM 充当大脑,工具调用是它的双手。
也就是说,一个 LLM 在一个循环中独立完成所有步骤。
每一步,LLM 都会基于当前上下文进行推理(Reasoning),决定下一步行动(Acting),调用工具或输出结果,然后将观察(Observation)结果作为新上下文,进入下一轮。
使用这种架构的问题都有这样的特点,任务清晰,但是步骤不固定、需要灵活应变,资料依赖比操作以来更强的问题。
规划 + 执行
这是对单Agent循环的一次优化,将思考与行动在时间上彻底解耦。
将想清楚要做什么和实际去做分离为两个独立阶段,提升任务的可预测性和可审计性。
规划 + 执行架构将 Agent 的工作拆分为两个明确阶段:先规划(Plan),再执行(Execute)。
在规划阶段,模型不执行任何操作,只生成一份详细的执行步骤列表。在执行阶段,系统依次完成每个步骤。这种分离让用户可以在执行前审查计划,就像 Claude Code 的 Plan Mode 一样。这种方式在处理复杂长任务能极大程度的提高工作的质量。
在单Agent循环中,想和做交织在一起,容易因中间步骤的结果而偏离原目标。而在Plan-and-Execute中,规划是一次性的,执行是线性的,大大提升了长任务的可预测性和可审计性
多 Agent 协作
一个 Orchestrator协调者,负责任务拆解和调度,多个 Subagent执行者 各司其职,并行或串行地完成子任务,结果汇聚回 Orchestrator 做综合。
当单个 Agent 面临上下文窗口不足,无法容纳多个子任务的巨量信息时,或任务过于复杂时,多 Agent 架构提供了一个解决方案,让多个专门化的子 Agent 并行工作,由一个 Orchestrator(协调者)统筹全局。
每个子 Agent 拥有独立的上下文窗口。例如图中的代码审查 Agent 深度阅读 auth.py 不会影响性能分析 Agent 的判断;安全检测 Agent 产生的大量中间输出不会挤占其他 Agent 的空间。这种方式在 cursor 中被大量使用。
很明显,多 Agent 协作的主要成本是编排开销,如果子任务非常简单,编排开销可能超过实际工作的开销,此时单 Agent 更合适。
上下文隔离是核心价值。例如,安全Agent读取大量日志产生的中间输出,不会挤占代码生成Agent的窗口空间,这对于处理大型代码库(如Cursor)至关重要。
反思与自我修正
在 Agent 的输出环节加入质量评估,不满意则重新生成或修正,形成内部迭代循环。
反思架构为 Agent 增加了一个质检环节,每次生成输出后,都由一个评判者(Critic)来评估质量,如果不达标则要求修正,直到输出满足标准。
这就像开发者写完代码后自己跑一遍测试,在交付之前先自查一遍,然后再给上司进行代码 review。
但是这不仅增加了总耗时和Token消耗,且如果Critic本身能力不足,可能将正确的输出修正为错误的误判。
工作流编排
将整个任务流程预先定义为一张有向无环图(DAG)。图中的每个节点是一个原子操作(可能是一次LLM调用,或一个工具调用),边则定义了数据流向和依赖关系(如“节点A的输出是节点B的输入”)。
Agent(或LLM)的自主决策权被严格限制在单个节点内部(例如,节点内LLM可以决定调用哪个工具来获取数据),但节点间的跳转逻辑是硬编码的,由框架驱动执行,Agent无权跳过或更改DAG结构。
这是最接近传统软件工程的一种 Agent 架构。与前面几种架构的最大区别在于,Agent 的自主决策空间被限制在单个节点内部,节点之间的流转是预先定义好的,不可更改。
这种情况有极高的稳定性和可观测性,每个节点的输入输出都可追踪调试,失败后可精确重跑失败节点。但是缺乏灵活性,无法应对未在DAG中预设的突发情况,开发维护成本高
LangChain4j 中的 Agent
如何在 LangChain4j 中进行 Agent 开发
@Agent 注解
在 LangChain4j 中,Agent 是使用 LLM 执行某个特定任务(或一组任务)的组件。
Agent 可以通过在接口方法上添加 @Agent
注解来定义,写法类似普通 AI
服务。你在接口方法上加上@Agent注解,就定义了一个Agent。这个注解的核心作用是为这个Agent在系统中登记身份和能力。
而且通常情况下会在 @Agent 注解中提供简短的描述,尤其在
纯代理模式 下,其他 Agent 需要了解该 Agent
的能力,以便决定何时调用。
description:这是给协调者看的。 在多Agent协作时,这个描述至关重要。协调者LLM需要根据这个描述,来决定在什么情况下把这个任务交给谁。outputKey:这是Agent的成果代号。 这是 Agent 与其他 Agent 协作的关键。Agent执行完任务,结果不会直接返回给你,而是按照outputKey的名字,存入一个叫AgenticScope的共享变量“仓库”里。后续的Agent可以通过这个名字去仓库里取用这个结果。
1 | // 一个负责生成的Agent |
AgenticScope
AgenticScope 是一个由多个 Agent 共享的数据集合,用于存储共享变量,一个 Agent 可以写入它产生的结果,另一个 Agent 可以读取这些结果来完成任务。这样,Agent 之间就能高效协作,按需共享信息与结果。
这个仓库就是AgenticScope。它在工作流执行时自动创建,用来存放所有
Agent 的输入和输出数据。你可以把它想象成一块所有 Agent
都能看到、能往上写东西、也能从里面拿东西的白板。
当一个Agent需要输入时,它会通过
@V("key")告诉框架,去AgenticScope里找名为key的数据作为参数。同样,它的输出会通过outputKey写回白板。这实现了Agent之间的数据解耦。一个 Agent 不需要知道上一步是谁产生的数据,只需要知道从白板的哪个位置拿就行了。
如果你的Java方法编译时保留了参数名(
-parameters),@V注解可以省略,框架会自动根据参数名去Scope里查找数据。emmm,这个我估计一般都开着。
此外,AgenticScope 会自动记录:
- 各个 Agent 的调用顺序
- 每次调用的响应
- 等等。。。。。。。
当 Agentic 系统的主 Agent 被调用时,AgenticScope
会自动创建,并在需要时通过回调程序化地提供。在后续介绍不同 Agentic
模式时,会结合实例进一步说明 AgenticScope 的用法
创建 Agent 和工作流模式
然后,可以使用 AgenticServices.agentBuilder()
方法来创建该 Agent 的实例,并指定接口与 Chat 模型:
1 | CreativeWriter writer = AgenticServices |
从本质上讲,Agent 与普通 AI 服务功能相同,只是它们可以被组合到更复杂的工作流和 Agentic 系统中。
AgenticServices类就像是一个工具箱,提供了构建不同类型
Agent
和工作流编排的静态工厂方法,上面演示的就是构建单个Agent,最常用的是agentBuilder()。它会读取你定义的接口,并生成一个代理实例。
但是单个 Agent 远远不是我们的最终目的,LangChain4j
在AgenticServices中提供了多种工作流构建器,让你能把单个
Agent 按照上面我们讲述的架构类型组合成复杂的系统。
下面是最核心的三种
顺序工作流
最简单的模式是 顺序调用:多个 Agent 依次执行,每个 Agent 的输出作为下一个 Agent 的输入。适用于需要按照固定顺序完成一系列任务的场景。
这对应了规划+执行架构中的线性执行部分。Agent 们一个接一个地执行,前一个的输出自动成为后一个的输入。例如对应上面的例子,我们生成故事 -> 调整风格 -> 适配受众,一般会经过这样一个过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 1. 分别构建三个Agent(假设它们都已定义好接口)
CreativeWriter writer = ...;
StyleEditor styleEditor = ...;
AudienceEditor audienceEditor = ...;
// 2. 构建顺序工作流
UntypedAgent novelCreator = AgenticServices
.sequenceBuilder()
.subAgents(writer, styleEditor, audienceEditor) // 按顺序执行
.outputKey("story") // 整个流程的最终产出
.build();
// 3. 执行
Map<String, Object> input = Map.of("topic", "dragons", "style", "gothic");
// 执行后,最终的"story"会存放在novelCreator的内部Scope中
novelCreator.invoke(input);如果希望有强类型的输入输出,可以给
sequenceBuilder()传入一个接口类。并行工作流
这对应了多Agent协作架构中的并行执行部分。多个子Agent同时处理同一个任务,结果汇总。这里的关键是每个子Agent都有自己独立的上下文窗口,互不干扰。
例如,HR、经理、团队成员同时评审一份简历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 1. 构建三个评审Agent
HrCvReviewer hrReviewer = ...;
ManagerCvReviewer mgrReviewer = ...;
TeamMemberCvReviewer teamReviewer = ...;
// 2. 构建并行工作流
UntypedAgent parallelReview = AgenticServices
.parallelBuilder()
.subAgents(hrReviewer, mgrReviewer, teamReviewer)
.executor(Executors.newFixedThreadPool(3)) // 配置线程池
.output(scope -> { // 自定义汇总逻辑
// 从scope中读取三个评审结果,计算平均分
CvReview hr = (CvReview) scope.readState("hrReview");
// ... 计算 ...
return new CvReview(avgScore, combinedFeedback);
})
.build();循环工作流
这对应了反思与自我修正架构。一个Agent或一组Agent会反复执行,直到满足某个退出条件。
例如,简历评分 -> 低于标准则修改 -> 再次评分,直到分数达标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 1. 构建“修改”和“评审”两个Agent
ScoredCvTailor cvTailor = ...;
CvReviewer cvReviewer = ...;
// 2. 构建循环工作流
UntypedAgent cvImprovementLoop = AgenticServices
.loopBuilder()
.subAgents(cvTailor, cvReviewer) // 循环执行这两个Agent
.exitCondition(scope -> { // 定义退出条件
CvReview review = (CvReview) scope.readState("cvReview");
return review.score >= 0.8; // 分数达标就退出
})
.maxIterations(3) // 最多循环3次,防止死循环
.build();条件工作流
有时需要 根据条件选择 Agent。在工作流执行过程中,根据共享白板(
AgenticScope)里的某个状态值,决定下一步该走哪条路、执行哪个 Agent。还是以简历评估场景为例,根据评分高低决定是”邀请面试”还是”发拒绝信”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// 1. 定义两个分支Agent
EmailAssistant emailAssistant = AgenticServices
.agentBuilder(EmailAssistant.class)
.chatModel(model)
.tools(new OrganizingTools()) // 发邮件工具
.build();
InterviewOrganizer interviewOrganizer = AgenticServices
.agentBuilder(InterviewOrganizer.class)
.chatModel(model)
.tools(new OrganizingTools()) // 日程安排工具
.contentRetriever(ragProvider.loadHouseRulesRetriever()) // 可附加RAG
.build();
// 2. 构建条件工作流
UntypedAgent candidateResponder = AgenticServices
.conditionalBuilder()
// 分支1:评分 >= 0.8,执行 InterviewOrganizer
.subAgents(
scope -> ((CvReview) scope.readState("cvReview")).score >= 0.8,
interviewOrganizer
)
// 分支2:评分 < 0.8,执行 EmailAssistant
.subAgents(
scope -> ((CvReview) scope.readState("cvReview")).score < 0.8,
emailAssistant
)
.build();
// 3. 执行
Map<String, Object> input = Map.of(
"cvReview", new CvReview(0.6, "技术细节不足") // 模拟低分
);
candidateResponder.invoke(input);
// 此处会路由到 EmailAssistant,发送拒绝邮件如果有多个
subAgents条件,它们会按声明顺序依次判断,命中第一个满足条件的分支后执行。
值得一提的是,工作流中的“节点”不一定是AI Agent,也可以是普通的Java对象或方法,即“非AI智能体”(Non-AI Agent),反正这里不说下面也会说()
一般是对于计算、更新数据库状态等确定性操作,用代码实现更高效、更准确,也更省钱。
而且这部分内容的实现也非常贴合直觉,直接在@Agent注解的类里写普通方法即可,然后在sequenceBuilder()中像使用AI
Agent一样,直接new一个对象放进去。
1 | // 一个纯粹的Java类,不涉及任何LLM调用 |
而且,除了用 Builder 构建,LangChain4j
也支持在接口中通过声明式注解定义条件工作流。例如
@ConditionalAgent
注解可以用来标记路由方法。下面提到了再细说
顺序 / 循环工作流 Agent 示例
首先,我们设计这样的一个 Agent,它是一个内容创作 Agent 集合,场景是:创意写作 → 受众编辑 → 风格润色 →(可选)风格评分,这样的一个循环。
为什么这样设计,LangChain4j 这种以及大部分 Agent 框架的开发,都会遵守一个最小原则,就是每个基础 Agent 只做一件事,且通过 输出键 与其它 Agent 无侵入地协作。
这样设计是有必要的。。。
LangChain4j 在 Agent 编排
方面提供了一种轻量而强大的声明式方式,通过 @Agent
注解将普通接口方法变成可复用节点,所有 Agent 通过
@Agent(outputKey = "...") 将结果写入
共享作用域(AgenticScope),这样一来将它们串成流水线。所以说我们来看看最基本的顺序和循环是怎么编排的。
1 | public class ContentAgents { |
那么其中,
outputKey = "story"是这个顺序工作流工作的关键。它是如何发挥作用的呢??很明显,该方法执行后,返回结果将被存入当前会话的
AgenticScope中,键名为"story"。后续任何 Agent 只需声明同名输入参数,框架就会从作用域自动注入该值。那么具体到例子来说,你像
CreativeWriter这种创作完了故事,输出到了AgenticScope中,然后,AudienceEditor会根据输入参数String story,从作用域查找键为story的值注入。作用域中有前一步生成的故事,这里就能自动衔接。然后输出再次写入键story,形成覆盖更新,实现管道式处理。对于风格评分,这部分是循环 Agent 的部分
对于 StyleScorer 这个风格评分器,输出键为
score,该值可在循环条件中作为判断依据。
所以说,上面的四个接口反复出现 outputKey,它们是
Agent 编排的胶水。当多个 Agent 在同一个
AgenticScope(通常由框架自动创建)中运行时
- 写入:每个 Agent 方法执行完毕,框架将其返回值以
outputKey为键存入作用域。 - 读取:后续 Agent 的方法参数名若与作用域中的键同名,则自动注入最新值;否则视为普通外部入参。
其中的 description 不仅是可给自己看的描述,而且可以给
LLM 去看这个参数的描述
Agent 是如何被编排和调用的呢?
那么,其中的顺序工作流,以 NovelCreator 为例
1 | public interface NovelCreator { |
为什么要写成这样的一个接口呢?就能实现对上面 Agent 的顺序编排?如何做到的?
很明显,NovelCreator 是一个
强类型入口,它本身也是一个
@Agent,但它的实现不代表一次 LLM
调用,而是代表一个完整的子工作流图。而且它的具体编排逻辑不在代码中显式出现,而是通过
LangChain4j 的工作流 DSL
来定义,大致等价于以下步骤:
那么来看循环工作流对应的接口方法,其实跟上面的顺序工作流并没有什么区别,Agent 入口就是这样定义的
1 | public interface StyledWriter { |
那么??这些工作流是怎么样被这样的接口和实现类编排之后,就能够按照需要的 Agent 进行各种方式的调用的呢?
首先,只有接口,实现从哪来??这个之前我们提到了!是动态代理
在 AgentConfig 里,我们这样创建它
1 |
|
AgenticServices.agentBuilder()做了几件事- 读取接口上的
@Agent注解,提取outputKey。 - 读取方法上的
@UserMessage注解,提取 prompt 模板。 - 在运行时,通过 JDK 动态代理 生成一个实现了
CreativeWriter接口的对象。 - 这个代理对象内部持有
ChatModel和AgenticScope(作用域)的引用。
- 读取接口上的
- 当你调用
creativeWriter.generateStory("龙与机器人")时- 代理拦截调用,获取方法签名。
- 将
topic="龙与机器人"填充到@UserMessage模板中,生成完整 prompt。 - 调用
chatModel.generate(prompt),拿到 LLM 返回的文本。 - 将返回值以键
"story"存入当前线程的AgenticScope。 - 返回该文本。
所以,从根源和原理来看,我们可以认为@Agent(outputKey = "story")
的意思是:这个方法执行后,把结果写入作用域,键名为
"story"。
这次再来看顺序工作流 NovelCreator 是如何把三个 Agent
如何串起来的
配置类里的构建方式:
1 |
|
sequenceBuilder生成了一个NovelCreator的代理实例,其内部保存了一个子 Agent 列表:[creativeWriter, audienceEditor, styleEditor]。因此,大体步骤如下
创建
AgenticScope:每次调用入口方法,框架都会新建一个AgenticScope,用于存储中间结果。注入外部参数到作用域:入口方法的参数名
topic、audience、style会被自动写入作用域依次执行子 Agent:按照列表编排的顺序,逐个调用
1
2
3
4
5
6
7
8
9
10
11creativeWriter.generateStory(topic)
此时 topic 参数从哪里来?
→ 框架先检查 scope 里有没有键 "topic",有则注入;没有才从方法参数传过来。
执行完后,scope 里多了 "story": "人类首次登陆火星..."。
audienceEditor.editStory(story, audience)
story 和 audience 都从 scope 中解析(audience 是入口参数,story 是上一步产出)。
执行后,scope 里的 "story" 被覆盖为符合青少年阅读习惯的版本。
styleEditor.editStyle(story, style)
同理,再次覆盖 "story",变成科幻风格版。返回最终结果:整个工作流的输出就是最后一个子 Agent 执行后的
scope中"story"的值。同时,因为NovelCreator自身声明了@Agent(outputKey = "story"),这个最终值也写回了作用域,供更外层使用。
循环工作流中,我们希望故事风格契合度达到 0.8 以上,否则反复润色。配置如下:
1 | UntypedAgent styleReviewLoop = AgenticServices.loopBuilder() |
这里返回的是
UntypedAgent(无强类型接口),它内部维护一个循环体。当这个循环
Agent 被触发时,先进入循环,调用
styleScorer.scoreStyle(story, style),检查条件,如果没达到,继续执行下一个子
AgentstyleEditor.editStyle(story, style)
所以说,这里需要注意子 Agent 的顺序:循环体内的子
Agent 会依次执行,所以 styleScorer
先跑,如果分数不够,styleEditor 才跑;然后进入下一轮再从
styleScorer 开始。这符合先评估、再修改的逻辑。
这里也简单提一下混合编排的基本逻辑,那么混合编排其实很简单,就是把前面编排好的部分的 Agent,拼在一起,例如,上面的我们把顺序和循环的 Agent 拼接在一起,就是混合编排
1 | return AgenticServices.sequenceBuilder(ContentAgents.StyledWriter.class) |
把 creativeWriter 和刚造好的循环 Agent
组合成一个新的顺序工作流。调用
styledWriter.writeStoryWithStyle(topic, style)
时的流程就是先调用第一个编排好的 Agent,再调用第二个编排好的 Agent
并行工作流 Agent 示例
并行工作流顾名思义就是让多个 Agent 同时处理同一个输入,互不干扰,最后将各自的结果合并输出。
例子中我们想象一个晚间活动规划师,用户输入一种心情
mood,系统需要同时推荐,3 道适合的菜品和3
部适合的电影,然后将它们组合成一组 EveningPlan
很明显,这个业务中,两个推荐任务彼此独立,最终需要合并,这正是并行工作流的特点
我们设计这样的两个 AI Service 的 Agent 接口
1 | public interface FoodExpert { |
- 独立输出键:
FoodExpert的结果写入meals,MovieExpert写入movies。两个键互不冲突,是并行执行的基础。 - 统一输入:两个方法都接收同一个
mood参数。在并行工作流中,入口方法的参数会被自动注入到AgenticScope,子 Agent 通过参数名匹配即可获取。 - 返回类型
List<String>:框架会自动将 LLM 的输出(如 “1. 意大利面. 牛排. 沙拉”)解析为List<String>。
而对于这个并行工作流的入口EveningPlannerAgent,这个接口本身也是一个
Agent,它的实现由 parallelBuilder
生成,调用后会返回合并后的计划列表。具体的编排实现是这样的
1 |
|
subAgents(foodExpert, movieExpert)指定需要并行执行的两个 Agent,executor(...)提供一个线程池,子 Agent 会并发运行在这两个线程上,outputKey("plans")代表合并后的最终结果会写回作用域的"plans"键自动合并发生在
output(this::mergeEveningPlans)中,所有子 Agent 都执行完毕后,框架调用此函数从作用域中取出meals和movies,进行自定义合并?如果没有
.output()回调会怎样?那么并行工作流的最终输出会是最后一个子 Agent 的返回值。
mergeEveningPlans 方法从 AgenticScope
中读取并行产生的两个列表,然后按位置一一配对,这个实现是要自己实现的
1 | private List<EveningPlan> mergeEveningPlans(AgenticScope agenticScope) { |
服务层的调用中,在 AgentOrchestrator
中,调用封装得非常简洁,其他 Agent
的封装也类似,它扮演的角色就是之前我们讲 Agent 架构的时候的
Orchestrator(协调者)
1 | public Map<String, Object> parallelWorkflow(String mood) { |
这里梳理一下 parallelBuilder 的内部运行机制
AgenticScope在设计上是线程安全的。当并行任务同时写入meals和movies时,因为键不同,不会发生竞争。这也正是我们刻意将输出键分开的原因。parallelBuilder会:- 为每个子 Agent 创建一个
Callable任务,提交到线程池。 - 使用
ExecutorService.invokeAll(或类似机制)等待所有任务完成。 - 如果某个任务抛出异常,会通过工作流的错误处理机制传播(可以配置
errorHandler)。 - 全部完成后,在主线程(或调用方线程)中执行
output回调进行合并。
- 为每个子 Agent 创建一个
条件路由 Agent 示例
在真实业务中,一个用户请求往往需要先分类,再交由对应领域的专用的 Agent 来处理。
LangChain4j 的 条件路由 正是为这种场景而生的模式,先通过一个分类 Agent 判定请求类型,再根据分类结果动态选择执行某个专家 Agent。
本文以官方示例
ExpertRouterAgent为蓝本,进行改造,代码如下
1 | public class RoutingAgents { |
- 问题分类的入口:首先,分类路由器作为一个分类用的 Agent,返回类型是
ContentRequestCategory的枚举,包含WRITING,SEO,FACT_CHECK,UNKNOWN,分类完成之后,将问题路由给对应的专家 Agent - 三个专家 Agent 接口结构高度一致,参数都是
String request,outputKey都是"response",这正是条件路由的巧妙之处:无论哪个专家被选中,结果都写入同一个键,外层入口可以无差别读取。它们只会在满足条件时被调用,不参与条件判断本身。
对于这个工作流,我们可以这样构建它,分为两步
1 |
|
conditionalBuilder()创建一个条件路由工作流,返回的是UntypedAgent,它代表无特定接口类型,但可作为子 Agent 被其他工作流引用,这个非常重要.subAgents(condition, agent)注册了一对 条件 → Agent 的映射。条件是一个Predicate<AgenticScope>,从作用域读取category的值,判断是否为对应枚举。- 注意,执行时,框架会按注册顺序评估条件,遇到第一个为
true的条件就执行对应的 Agent,后续条件不再评估。
条件工作流只负责根据分类选专家,它本身不包含分类步骤。因此我们需要用 顺序工作流 把分类和条件路由串联起来
1 |
|
在 AgentOrchestrator
这个协调者中,条件工作流的调用被封装为和前面的 Agent 的一样的形式
1 | public Map<String, Object> conditionalWorkflow(String request) { |
其实核心就是,调用方无需关心分类细节,只需输入用户请求,即可获得对应领域的专家建议。
纯 Agentic AI / Supervisor 示例
它本质是多 Agent 协作架构的自动操作版本
之前提到的多 Agent
协作架构中,需要Orchestrator(协调者),而
Supervisor 模式中,Orchestrator 本身也是一个
LLM
Agent。它根据用户的自然语言请求,动态生成一个执行计划,也就是AgentInvocation列表,然后框架按计划依次调用子Agent。
也就是说,Supervisor 是一种 LLM
驱动的自主编排模式。在 Supervisor 模式下,你不需要定义
sequenceBuilder 或
conditionalBuilder,而是把所有可用的 Agent(及 Tool)注册给
Supervisor。LLM 会接收用户请求,自己决定
这就把编排逻辑从代码移到了LLM的推理中。是让 AI 管理 AI 的体现。
例如,我们有这样的一个场景,银行转账中的多 Agent 协作中,假设用户提出一个转账请求
“Transfer 100 EUR from Mario’s account to Georgios’s one”
这个请求看似简单,却涉及多个步骤:
- 货币兑换:EUR 转 USD(假设内部账户只存 USD)。
- 取款:从 Mario 的账户取出对应 USD。
- 存款:向 Georgios 的账户存入 USD。
如果使用顺序工作流,你需要硬编码:“先调用兑换,再取款,再存款”。但 Supervisor 模式可以让 LLM 自己规划这些步骤,甚至根据货币类型动态决定是否兑换、兑换顺序等。
那么,相关代码如下
AI 子 Agent
1 | public interface WithdrawAgent { |
- 每个 Agent 都有一个清晰的
description,这是给 Supervisor 看的“能力说明”,LLM 据此决定何时调用它们。 - 这两个 Agent 都绑定了一个共同的
BankTool(通过.tools(bankTool)配置),它们会利用 Function Calling 实际执行账户操作。这个简单提一下,我们下期再说这个 Tool
非 AI 子 Agent
1 | public class ExchangeOperator { |
- 这个类不需要 LLM,只是普通的 Java
方法。前面也提到过,LangChain4j 的 Agent 支持你编排进去非 LLM
驱动的方法作为类似工具调用的感觉。通过
@Agent注解标注,它就可以作为一个子 Agent 注册到 Supervisor 中。LLM 会将其视为一个可调用的工具,只不过这个工具的输入输出是结构化的参数,而不是自然语言。
那么,Supervisor 的配置组装是如何进行的呢??
1 |
|
前面两个注册对应 Agent 的接口之外,我们来仔细看看
bankSupervisorAgent 是如何组织这个 Agent 的
supervisorContext这是注入给 Supervisor LLM 的策略约束。例如示例中指定了优先使用内部工具,货币用 USD,不调用外部 API。它就会作为系统提示的一部分,影响 LLM 的规划行为。可以把它想象成管理手册,告诉 Supervisor 该遵守的规则。
subAgents(...)是注册所有可用的子 Agent,包括 AI Agent 和非 AI 的
ExchangeOperator。LLM 在规划时会从这些 Agent 的描述中理解各自的功能,并选择恰当的调用。responseStrategy是一个枚举值可选,代表输出的类型
SUMMARY:LLM 在执行完所有子 Agent 后,生成一个最终摘要回复。FULL:返回所有子 Agent 的完整输出,不做摘要。LAST:仅返回最后一个子 Agent 的输出。
在银行转账场景,
SUMMARY最合适,让 LLM 把取款、存款、兑换的结果整合成一段清晰的信息返回给用户。contextGenerationStrategy控制每次调用子 Agent 时,上下文如何传递给 LLM:
CHAT_MEMORY:使用完整的聊天记忆,即之前的所有交互历史都会被 Supervisor LLM 看到,使其能基于全局状态做决策。STATEFUL:仅提供当前作用域的状态(key-value),不保留历史对话。
这里选择
CHAT_MEMORY可以让 Supervisor 更好地处理多轮复杂的交互(例如追问细节)。
在 WithdrawAgent 和 CreditAgent
背后,是一个模拟银行账户操作的
BankTool,作为我们下一个提到的内容,有必要在这里进行衔接
1 |
|
当 LLM 处理 WithdrawAgent 的请求 从用户 Mario 的账户取出
100 USD 时,它会通过 Function Calling 自动调用
BankTool.withdraw("Mario", 100.0)。返回值(新余额)被 LLM
用于生成自然语言回复,同时也写入作用域
这正是 Supervisor 模式的核心优势之一:将确定性业务逻辑(Tool)和 LLM 的推理能力无缝结合。LLM 不需要知道银行账户的具体操作,它只需明白取款这个意图,然后调用相应的工具函数即可。
在 AgentOrchestrator 中,Supervisor 转账被封装成
1 | public Map<String, Object> supervisorTransfer( |
所以说
- 当业务流程稳定且步骤清晰时,用工作流模式更可控;
- 当面对多变的用户意图、需要智能调度多个工具时,Supervisor 是更聪明的选择。






