什么是链

链的概念源于 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
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
public class LegacyChainService {

private final ConversationalChain chatModel;

public LegacyChainService(@Qualifier("chainChatModel") ChatModel chatModel) {
this.chatModel = chatModel;
}

/**
* 用 legacy 的 {@link ConversationalChain} 进行一次对话。
*
* @param userMessage 用户消息
* @return 模型回复
*/
public String chat(String userMessage) {
// ConversationalChain = ChatModel + ChatMemory 的固定封装
ConversationalChain chain = ConversationalChain.builder()
.chatModel(chatModel)
// 不显式设置时,默认是最多 10 条消息的窗口记忆,MessageWindowChatMemory是窗口记忆
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();

// execute 内部:加入用户消息 → 调用模型 → 回复存回记忆 → 返回回复文本
return chain.execute(userMessage);
}
}

实际上它内部干的事情非常简单。ConversationalChain = ChatModel + ChatMemory 的固定封装

那么,我们的 execute() 内部发生了什么?

假设 chain.execute("你好")

执行,第一步:创建 UserMessage

1
UserMessage userMessage = UserMessage.from("你好");

第二步:加入 ChatMemory

1
memory.add(userMessage);

第三步:读取 Memory

1
2
List<ChatMessage> messages =
memory.messages();

第四步:调用 ChatModel

1
ChatResponse response = chatModel.chat(messages);

发送给大模型

第五步:得到回复

1
2
Assistant:
你好,很高兴见到你...

第六步:存回 Memory

1
memory.add(aiMessage);

第七步:返回文本

1
return aiMessage.text();

然会你会发现,你不管是插入 Tool,插入 RAG,返回 POJO,你都没有地方可以很方便的插入了

如果用 AiServices 怎么做?其实也非常简单了

1
2
3
4
interface Assistant {
@SystemMessage(你是一位Java专家)
String chat(String message);
}

然后构建这个 Assistant 就可以了,你会发现 AI Service 中各种东西你都可以很方便的加进来

所以说,LegacyChainService 最重要的价值并不是它还能不能用,而且它的定位,它解决的是 2023 年早期 LLM 应用带记忆聊天的问题;而现代 LangChain4j 已经把 AI Service 当作 Spring Bean 一样的组件,通过 Java 的 if/switch/for/while 来编排工作流,因此 Legacy Chain 被历史淘汰了。

顺序链

顺序链是最基础的链式编排模式就是把多个 AI Service 串起来,上一个的输出自动成为下一个的输入。这就是最基本的工作流

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
/**
* 示例 1 配套接口集合:顺序链(Sequential Chaining)所需的两个原子 AI Service。
*
* 当一个 LLM 任务很复杂时,与其把一堆指令塞进同一个超长 system prompt\
* 不如把它拆成多个小而专一的 AI Service,每个只干一件事。小组件更容易开发、测试、维护,也更便宜。
*
* 本类中的两个接口将被「链」起来:
* {@link Summarizer#summarize} 的输出,作为 {@link Translator#translate} 的输入。
* 这种「上一步输出 → 下一步输入」就是最基础的顺序链。
*/
public class SequentialChainServices {

private SequentialChainServices()

/**
* 第 1 环:摘要器。把长文本压缩成一句话核心摘要。
*/
public interface Summarizer {
@SystemMessage("你是一个专业的文本摘要专家,擅长抓住核心信息。")
@UserMessage("请用一句话概括以下文本的核心内容,不超过 50 字:\n{{it}}")
String summarize(String text);
}

/**
* 第 2 环:翻译器。把(上一环产出的)摘要翻译成目标语言。
*/
public interface Translator {
@SystemMessage("你是一位专业翻译,译文要准确、自然。")
@UserMessage("请将以下文本翻译为 {{language}},只输出译文:\n{{text}}")
String translate(@V("text") String text, @V("language") String language);
}
}

在咱们写的代码中,就是这样的一个形式

1
长文本 → Summarizer.summarize() → 摘要 → Translator.translate() → 翻译结果

官方文档明确指出:不要把一堆指令塞进同一个超长 System Prompt,包括你从外部引入的提示词,原因很明显,注意力分配问题会导致 LLM 会漏看,指令互相干扰,成本高

这样我们写了两个原子的 AI Service 接口之后,我们就需要给他串起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 顺序链编排
*
* @param longText 原始长文本
* @param language 目标语言(如 "English")
* @return 含每一环产物的 Map(summary + translation)
*/
public Map<String, String> sequentialChain(String longText, String language) {
// 第 1 环:摘要
String summary = summarizer.summarize(longText);

// 第 2 环:把上一环的输出作为输入,翻译
String translation = translator.translate(summary, language);

Map<String, String> result = new LinkedHashMap<>();
result.put("step1_summary", summary);
result.put("step2_translation", translation);
return result;
}

很明显,整个编排的过程中,没有特殊的 Chain 类,就是方法调用的串联。所以说,链这次我们不在具体指一些内容,而是一种 AI Agent 开发的思维,LLM 不再是黑盒的单调用,而是被拆成多个功能单一的 AI 组件,由代码掌控流程

循环链

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
public class IterativeChainServices {

private IterativeChainServices() {}

/**
* 文案生成器:根据主题生成一句营销标语。
*/
public interface SloganWriter {

@SystemMessage("你是一位资深广告文案,擅长写吸引人的营销标语。")
@UserMessage("请为「{{topic}}」写一句营销标语。{{instruction}}")
String write(@V("topic") String topic, @V("instruction") String instruction);
}

/**
* 文案评分器:给标语打分(0-100)。
*
* 返回 {@code int},框架用数值解析器解析模型输出。这个分数将作为外层循环「是否继续优化」的判断依据。
*/
public interface SloganCritic {

@SystemMessage("你是一位严格的广告评审,请只回复一个 0 到 100 之间的整数分数,不要任何其它文字。")
@UserMessage("请从吸引力、简洁度、记忆点三方面给以下营销标语打分(0-100):{{it}}")
int score(String slogan);
}
}

我们对这样的两个 AI Service 接口进行编排

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
36
37
38
39
40
41
public Map<String, Object> iterativeChain(String topic, int targetScore, int maxRounds) {
List<Map<String, Object>> rounds = new ArrayList<>();

String instruction = "";
String bestSlogan = null;
int bestScore = -1;

for (int round = 1; round <= maxRounds; round++) {
// 生成(带上一轮的改进意见)
String slogan = sloganWriter.write(topic, instruction);
// 评分
int score = sloganCritic.score(slogan);

Map<String, Object> roundInfo = new LinkedHashMap<>();
roundInfo.put("round", round);
roundInfo.put("slogan", slogan);
roundInfo.put("score", score);
rounds.add(roundInfo);

// 记录历史最佳
if (score > bestScore) {
bestScore = score;
bestSlogan = slogan;
}

// 达标即停
if (score >= targetScore) {
break;
}

// 未达标把分数作为反馈喂回下一轮,让模型「带着批评意见」改写
// 很明显,这是我们前面提到的 自我反思 的提示词工程
instruction = "上一版得分仅 " + score + " 分,请让它更有冲击力、更易记忆,重写一版。";
}

Map<String, Object> result = new LinkedHashMap<>();
result.put("rounds", rounds);
result.put("bestSlogan", bestSlogan);
result.put("bestScore", bestScore);
return result;
}

这是 Agent 的雏形:行动(生成)→ 观察(评分)→ 反思(改进意见)→ 再行动

很明显,我们用到了 LLM 反思(self-reflection) 的提示词工程的模式

路由链

Boolean 路由链就是 LLM 驱动的 if/else

我们来看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface GreetingExpert {
@UserMessage("以下文本是否是一句问候语?文本:{{it}}")
boolean isGreeting(String text); // 返回 boolean
}

public Map<String, Object> booleanRoutingChain(String userMessage) {
Map<String, Object> result = new LinkedHashMap<>();

// LLM 负责「语义判断」,返回 boolean
boolean greeting = greetingExpert.isGreeting(userMessage);
result.put("isGreeting", greeting);

// 代码负责「流程控制」:if/else
if (greeting) {
// 走预设话术,不调用对话模型
result.put("reply", "你好!这里是「微笑里程」,很高兴为你服务,有什么可以帮你的吗?");
result.put("source", "预设话术(未调用对话模型)");
} else {
// 真正的问题,才交给对话模型
result.put("reply", companyChatBot.reply(userMessage));
result.put("source", "对话模型生成");
}
return result;
}

返回类型 boolean 会触发 BooleanOutputParser,框架自动将 LLM 输出解析为 true/false,这里结构化输出那边讲过了

然后还有一个 Enum 路由链,这种有限输出域的情况下,都可以做这种路由链

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
// RoutingChainServices.java - 定义枚举
public enum TicketCategory {
TECHNICAL, // 技术问题
BILLING, // 账单问题
COMPLAINT, // 投诉
GENERAL // 一般咨询
}

public interface TicketClassifier {
@UserMessage("请判断以下工单属于哪个类别:{{it}}")
TicketCategory classify(String ticket); // 返回 enum
}

public Map<String, String> enumRoutingChain(String ticket) {
Map<String, String> result = new LinkedHashMap<>();

// LLM 负责分类,返回 enum
TicketCategory category = ticketClassifier.classify(ticket);
result.put("category", category.name());

// 代码负责 switch 分发
String action = switch (category) {
case TECHNICAL -> "已转交技术支持团队,并创建技术工单,预计 2 小时内响应。";
case BILLING -> "已转交财务结算组核对账单信息。";
case COMPLAINT -> "已升级为高优先级投诉,安排专员一对一跟进。";
case GENERAL -> "已由通用客服知识库自动答复。";
};
result.put("action", action);
return result;
}

但是在这里,枚举常量名必须是 LLM 能够理解并输出的词

框架用 EnumOutputParser 的是解析,不需要 JSON 模式,这里自动解析的内容,就又回到了结构化输出的内容了,就是 EnumOutputParser 是如何将 LLM 的自然语言输出解析成你定义的 Java 枚举的。所以这里就不说了

混合 workflow

混合 workflow 就是在一个更加复杂的情况下,使用了各种的路由模式,产生一个更接近真实业务的综合编排。

真实应用往往不是单一模式,而是把顺序、分支等多种编排组合使用。

整个决策流程清晰地写在 Java 代码里,就能可读、可测、可控,这正是确定性编排的价值。

当流程的分支判断越来越多、越来越难以穷举时,你自然会想到让 LLM 自己决定,那就进入 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
36
37
38
39
40
41
42
43
44
45
/**
* 混合 workflow:一个更接近真实业务的综合编排。
*
* <p>流程:收到一段用户反馈 →
* <ol>
* <li>用 boolean 判断它是不是纯问候(是则直接礼貌回应,流程结束);</li>
* <li>不是问候 → 用 enum 给它分类;</li>
* <li>同时用摘要器把反馈压缩成一句话,便于工单系统记录。</li>
* </ol>
*
* @param feedback 用户反馈文本
* @return 完整 workflow 的执行轨迹
*/
public Map<String, Object> hybridWorkflow(String feedback) {
Map<String, Object> trace = new LinkedHashMap<>();

// 步骤 1:是否为问候(boolean 路由)
boolean greeting = greetingExpert.isGreeting(feedback);
trace.put("step1_isGreeting", greeting);

if (greeting) {
trace.put("result", "检测到问候语,礼貌回应:你好!很高兴收到你的消息~");
trace.put("flow", "命中问候分支,流程提前结束");
return trace;
}

// 步骤 2:分类(enum 路由)
TicketCategory category = ticketClassifier.classify(feedback);
trace.put("step2_category", category.name());

// 步骤 3:摘要(顺序链的一环,与分类并列执行)
String summary = summarizer.summarize(feedback);
trace.put("step3_summary", summary);

// 步骤 4:按分类给出处理动作
String action = switch (category) {
case TECHNICAL -> "创建技术工单,转技术支持。";
case BILLING -> "转财务核对账单。";
case COMPLAINT -> "升级投诉,专员跟进。";
case GENERAL -> "知识库自动答复。";
};
trace.put("step4_action", action);
trace.put("flow", "问候判断 → 分类 → 摘要 → 分发,全流程由代码编排");
return trace;
}

因为内容上面都提到了,这里就不细说了

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 通常模仿人类的认知和行动循环,包含以下几个关键模块:

img

或者这么分类也是很常见的

img
  • 感知层,包括用户的输入,环境的状态等等,总之,感知层的输入经过整合,形成 Agent 的当前上下文,送入LLM进行理解和决策。
  • 大脑就是 LLM,做意图理解,推理决策,工具调用
  • 工具层就是能把决策转化为真实动作的执行单元。

以上各组件并非孤立存在,它们组成一个持续迭代的感知—思考—行动—观察闭环,这就是Agent Loop。Agent 不断重复这个循环,直到任务完成或达到终止条件。

image-20260615115627947

这个循环让 Agent 具备了在失败时自我纠错的能力:如果某一步工具调用返回了错误或意外结果,观察阶段会将这个信息反馈给大脑,大脑在下一轮思考时就会调整策略。

而且一个合格的 AI Agent 通常具备以下特征:

  • 自主
  • 感知环境
  • 主动行动
  • 能与人类或其他 Agent 交互
  • 学习能力

那么,更具体的组件组织情况和工作情况的图如下,这张图也会对接我们下面提到的 Agent 架构

image-20260616095632759

Agent 架构

Agent 的工作方式本质上是一个循环(Loop)——感知当前状态,推理下一步,执行行动,再次感知……直到任务完成。

不同架构的差异,就在于如何组织和扩展这个基本循环。

单 Agent 循环

最基础也最直观的架构,一个 Agent 从头到尾独立完成所有任务。

单 Agent 循环直接体现了 ReAct 模式,每一步都是先想再做。LLM 充当大脑,工具调用是它的双手。

image-20260616095725251

也就是说,一个 LLM 在一个循环中独立完成所有步骤。

每一步,LLM 都会基于当前上下文进行推理(Reasoning),决定下一步行动(Acting),调用工具或输出结果,然后将观察(Observation)结果作为新上下文,进入下一轮。

使用这种架构的问题都有这样的特点,任务清晰,但是步骤不固定、需要灵活应变,资料依赖比操作以来更强的问题。

规划 + 执行

这是对单Agent循环的一次优化,将思考与行动在时间上彻底解耦。

想清楚要做什么实际去做分离为两个独立阶段,提升任务的可预测性和可审计性。

规划 + 执行架构将 Agent 的工作拆分为两个明确阶段:先规划(Plan),再执行(Execute)。

在规划阶段,模型不执行任何操作,只生成一份详细的执行步骤列表。在执行阶段,系统依次完成每个步骤。这种分离让用户可以在执行前审查计划,就像 Claude Code 的 Plan Mode 一样。这种方式在处理复杂长任务能极大程度的提高工作的质量。

image-20260616170142461

在单Agent循环中,想和做交织在一起,容易因中间步骤的结果而偏离原目标。而在Plan-and-Execute中,规划是一次性的,执行是线性的,大大提升了长任务的可预测性和可审计性

多 Agent 协作

一个 Orchestrator协调者,负责任务拆解和调度,多个 Subagent执行者 各司其职,并行或串行地完成子任务,结果汇聚回 Orchestrator 做综合。

当单个 Agent 面临上下文窗口不足,无法容纳多个子任务的巨量信息时,或任务过于复杂时,多 Agent 架构提供了一个解决方案,让多个专门化的子 Agent 并行工作,由一个 Orchestrator(协调者)统筹全局。

image-20260616095858873

每个子 Agent 拥有独立的上下文窗口。例如图中的代码审查 Agent 深度阅读 auth.py 不会影响性能分析 Agent 的判断;安全检测 Agent 产生的大量中间输出不会挤占其他 Agent 的空间。这种方式在 cursor 中被大量使用。

很明显,多 Agent 协作的主要成本是编排开销,如果子任务非常简单,编排开销可能超过实际工作的开销,此时单 Agent 更合适。

上下文隔离是核心价值。例如,安全Agent读取大量日志产生的中间输出,不会挤占代码生成Agent的窗口空间,这对于处理大型代码库(如Cursor)至关重要。

反思与自我修正

在 Agent 的输出环节加入质量评估,不满意则重新生成或修正,形成内部迭代循环。

反思架构为 Agent 增加了一个质检环节,每次生成输出后,都由一个评判者(Critic)来评估质量,如果不达标则要求修正,直到输出满足标准。

image-20260616100056809

这就像开发者写完代码后自己跑一遍测试,在交付之前先自查一遍,然后再给上司进行代码 review。

但是这不仅增加了总耗时和Token消耗,且如果Critic本身能力不足,可能将正确的输出修正为错误的误判。

工作流编排

将整个任务流程预先定义为一张有向无环图(DAG)。图中的每个节点是一个原子操作(可能是一次LLM调用,或一个工具调用),则定义了数据流向和依赖关系(如“节点A的输出是节点B的输入”)。

Agent(或LLM)的自主决策权被严格限制在单个节点内部(例如,节点内LLM可以决定调用哪个工具来获取数据),但节点间的跳转逻辑是硬编码的,由框架驱动执行,Agent无权跳过或更改DAG结构。

这是最接近传统软件工程的一种 Agent 架构。与前面几种架构的最大区别在于,Agent 的自主决策空间被限制在单个节点内部,节点之间的流转是预先定义好的,不可更改。

image-20260616170119202

这种情况有极高的稳定性和可观测性,每个节点的输入输出都可追踪调试,失败后可精确重跑失败节点。但是缺乏灵活性,无法应对未在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 一个负责生成的Agent
public interface CreativeWriter {
@Agent(
outputKey = "story", // 成果叫 "story"
description = "Generates a story based on the given topic" // 能力描述
)
String generateStory(@V("topic") String topic);
}

// 一个负责编辑的Agent
public interface StyleEditor {
@Agent(
outputKey = "story", // 注意:它也产出 "story",会更新共享仓库里的同名变量
description = "Edits a story to better fit a given style"
)
String editStory(@V("story") String story, // 这里的参数名"story"很重要!
@V("style") String style);
}
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
2
3
4
5
CreativeWriter writer = AgenticServices
.agentBuilder(CreativeWriter.class)
.chatModel(yourChatModel) // 注入LLM
.outputKey("story") // 也可以在这里指定outputKey,覆盖注解里的
.build();

从本质上讲,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一个纯粹的Java类,不涉及任何LLM调用
public class ScoreAggregator {
@Agent(description = "聚合评审分数", outputKey = "combinedCvReview")
public CvReview aggregate(@V("hrReview") CvReview hr,
@V("managerReview") CvReview mgr) {
// ... 用Java代码计算平均分 ...
return new CvReview(avgScore, feedback);
}
}

// 在工作流中使用
UntypedAgent workflow = AgenticServices
.sequenceBuilder()
.subAgents(
// ... AI Agent ...
new ScoreAggregator(), // 非AI Agent直接new
// ...
)
.build();

而且,除了用 Builder 构建,LangChain4j 也支持在接口中通过声明式注解定义条件工作流。例如 @ConditionalAgent 注解可以用来标记路由方法。下面提到了再细说

顺序 / 循环工作流 Agent 示例

首先,我们设计这样的一个 Agent,它是一个内容创作 Agent 集合,场景是:创意写作 → 受众编辑 → 风格润色 →(可选)风格评分,这样的一个循环。

为什么这样设计,LangChain4j 这种以及大部分 Agent 框架的开发,都会遵守一个最小原则,就是每个基础 Agent 只做一件事,且通过 输出键 与其它 Agent 无侵入地协作。

这样设计是有必要的。。。

LangChain4j 在 Agent 编排 方面提供了一种轻量而强大的声明式方式,通过 @Agent 注解将普通接口方法变成可复用节点,所有 Agent 通过 @Agent(outputKey = "...") 将结果写入 共享作用域AgenticScope),这样一来将它们串成流水线。所以说我们来看看最基本的顺序和循环是怎么编排的。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class ContentAgents {

private ContentAgents() {}

public interface CreativeWriter {
@UserMessage("""
你是一位创意作家。
围绕给定主题写一段不超过 3 句话的短篇故事。
只输出故事正文,不要解释。
主题是:{{topic}}。
""")
@Agent(description = "根据主题生成短篇故事", outputKey = "story")
String generateStory(String topic);
}

public interface AudienceEditor {
@UserMessage("""
你是一位专业编辑。
请分析并重写以下故事,使其更符合目标受众「{{audience}}」的阅读习惯。
只输出修改后的故事,不要解释。
原故事:「{{story}}」
""")
@Agent(description = "按目标受众调整故事", outputKey = "story")
String editStory(String story, String audience);
}

public interface StyleEditor {
@UserMessage("""
你是一位风格编辑。
请重写以下故事,使其更符合「{{style}}」风格。
只输出修改后的故事,不要解释。
原故事:「{{story}}」
""")
@Agent(description = "按指定风格润色故事", outputKey = "story")
String editStyle(String story, String style);
}

public interface StyleScorer {
@UserMessage("""
你是一位严格的文学评论家。
请对以下故事与目标风格「{{style}}」的契合度打分,分数范围 0.0 到 1.0。
只输出数字,不要解释。
故事:「{{story}}」
""")
@Agent(description = "评估故事与目标风格的契合度", outputKey = "score")
double scoreStyle(String story, String style);
}

/**
* 顺序工作流的强类型入口
*/
public interface NovelCreator {
@Agent(outputKey = "story")
String createNovel(String topic, String audience, String style);
}

/**
* 顺序 + 循环组合的强类型入口(官方 {@code StyledWriter} 模式)。
*/
public interface StyledWriter {

@Agent(outputKey = "story")
String writeStoryWithStyle(String topic, String style);
}
}
  • 那么其中,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
2
3
4
public interface NovelCreator {
@Agent(outputKey = "story")
String createNovel(String topic, String audience, String style);
}

为什么要写成这样的一个接口呢?就能实现对上面 Agent 的顺序编排?如何做到的?

很明显,NovelCreator 是一个 强类型入口,它本身也是一个 @Agent,但它的实现不代表一次 LLM 调用,而是代表一个完整的子工作流图。而且它的具体编排逻辑不在代码中显式出现,而是通过 LangChain4j 的工作流 DSL 来定义,大致等价于以下步骤:

那么来看循环工作流对应的接口方法,其实跟上面的顺序工作流并没有什么区别,Agent 入口就是这样定义的

1
2
3
4
public interface StyledWriter {
@Agent(outputKey = "story")
String writeStoryWithStyle(String topic, String style);
}

那么??这些工作流是怎么样被这样的接口和实现类编排之后,就能够按照需要的 Agent 进行各种方式的调用的呢?

首先,只有接口,实现从哪来??这个之前我们提到了!是动态代理

AgentConfig 里,我们这样创建它

1
2
3
4
5
6
7
@Bean
public ContentAgents.CreativeWriter creativeWriter(...) {
return AgenticServices.agentBuilder(ContentAgents.CreativeWriter.class)
.chatModel(chatModel)
.outputKey("story")
.build();
}
  • AgenticServices.agentBuilder() 做了几件事
    • 读取接口上的 @Agent 注解,提取 outputKey
    • 读取方法上的 @UserMessage 注解,提取 prompt 模板。
    • 在运行时,通过 JDK 动态代理 生成一个实现了 CreativeWriter 接口的对象。
    • 这个代理对象内部持有 ChatModelAgenticScope(作用域)的引用。
  • 当你调用 creativeWriter.generateStory("龙与机器人")
    • 代理拦截调用,获取方法签名。
    • topic="龙与机器人" 填充到 @UserMessage 模板中,生成完整 prompt。
    • 调用 chatModel.generate(prompt),拿到 LLM 返回的文本。
    • 将返回值以键 "story" 存入当前线程的 AgenticScope
    • 返回该文本。

所以,从根源和原理来看,我们可以认为@Agent(outputKey = "story") 的意思是:这个方法执行后,把结果写入作用域,键名为 "story"

这次再来看顺序工作流 NovelCreator 是如何把三个 Agent 如何串起来的

配置类里的构建方式:

1
2
3
4
5
6
7
8
9
10
@Bean
public ContentAgents.NovelCreator novelCreator(
ContentAgents.CreativeWriter creativeWriter,
ContentAgents.AudienceEditor audienceEditor,
ContentAgents.StyleEditor styleEditor) {
return AgenticServices.sequenceBuilder(ContentAgents.NovelCreator.class)
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputKey("story")
.build();
}
  • sequenceBuilder 生成了一个 NovelCreator 的代理实例,其内部保存了一个子 Agent 列表:[creativeWriter, audienceEditor, styleEditor]

  • 因此,大体步骤如下

    • 创建 AgenticScope:每次调用入口方法,框架都会新建一个 AgenticScope,用于存储中间结果。

    • 注入外部参数到作用域:入口方法的参数名 topicaudiencestyle 会被自动写入作用域

    • 依次执行子 Agent:按照列表编排的顺序,逐个调用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      creativeWriter.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
2
3
4
5
UntypedAgent styleReviewLoop = AgenticServices.loopBuilder()
.subAgents(styleScorer, styleEditor) // 注意顺序:先评分,再润色
.maxIterations(5)
.exitCondition(scope -> scope.readState("score", 0.0) >= 0.8)
.build();

这里返回的是 UntypedAgent(无强类型接口),它内部维护一个循环体。当这个循环 Agent 被触发时,先进入循环,调用 styleScorer.scoreStyle(story, style),检查条件,如果没达到,继续执行下一个子 AgentstyleEditor.editStyle(story, style)

所以说,这里需要注意子 Agent 的顺序:循环体内的子 Agent 会依次执行,所以 styleScorer 先跑,如果分数不够,styleEditor 才跑;然后进入下一轮再从 styleScorer 开始。这符合先评估、再修改的逻辑。

这里也简单提一下混合编排的基本逻辑,那么混合编排其实很简单,就是把前面编排好的部分的 Agent,拼在一起,例如,上面的我们把顺序和循环的 Agent 拼接在一起,就是混合编排

1
2
3
4
return AgenticServices.sequenceBuilder(ContentAgents.StyledWriter.class)
.subAgents(creativeWriter, styleReviewLoop)
.outputKey("story")
.build();

creativeWriter 和刚造好的循环 Agent 组合成一个新的顺序工作流。调用 styledWriter.writeStoryWithStyle(topic, style) 时的流程就是先调用第一个编排好的 Agent,再调用第二个编排好的 Agent

并行工作流 Agent 示例

并行工作流顾名思义就是让多个 Agent 同时处理同一个输入,互不干扰,最后将各自的结果合并输出

例子中我们想象一个晚间活动规划师,用户输入一种心情 mood,系统需要同时推荐,3 道适合的菜品和3 部适合的电影,然后将它们组合成一组 EveningPlan

很明显,这个业务中,两个推荐任务彼此独立最终需要合并,这正是并行工作流的特点

我们设计这样的两个 AI Service 的 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
public interface FoodExpert {
@UserMessage("""
你是一位约会策划师。
根据给定 mood,推荐 3 道适合的晚餐。
mood 是:{{mood}}。
每道菜只写菜名,输出 3 个菜名的列表,不要解释。
""")
@Agent(outputKey = "meals", description = "推荐晚餐")
List<String> findMeal(String mood);
}

public interface MovieExpert {
@UserMessage("""
你是一位约会策划师。
根据给定 mood,推荐 3 部适合的电影。
mood 是:{{mood}}。
输出 3 个电影名的列表,不要解释。
""")
@Agent(outputKey = "movies", description = "推荐电影")
List<String> findMovie(String mood);
}

public interface EveningPlannerAgent {
@Agent(outputKey = "plans")
List<hbnu.project.langchain4jdemo.agent.model.EveningPlan> plan(String mood);
}
  • 独立输出键FoodExpert 的结果写入 mealsMovieExpert 写入 movies。两个键互不冲突,是并行执行的基础。
  • 统一输入:两个方法都接收同一个 mood 参数。在并行工作流中,入口方法的参数会被自动注入到 AgenticScope,子 Agent 通过参数名匹配即可获取。
  • 返回类型 List<String>:框架会自动将 LLM 的输出(如 “1. 意大利面. 牛排. 沙拉”)解析为 List<String>

而对于这个并行工作流的入口EveningPlannerAgent,这个接口本身也是一个 Agent,它的实现由 parallelBuilder 生成,调用后会返回合并后的计划列表。具体的编排实现是这样的

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
@Bean
public PlanningAgents.FoodExpert foodExpert(@Qualifier("agentChatModel") ChatModel chatModel) {
return AgenticServices.agentBuilder(PlanningAgents.FoodExpert.class)
.chatModel(chatModel)
.outputKey("meals") // 将返回值写入作用域的 "meals" 键
.build();
}

@Bean
public PlanningAgents.MovieExpert movieExpert(@Qualifier("agentChatModel") ChatModel chatModel) {
return AgenticServices.agentBuilder(PlanningAgents.MovieExpert.class)
.chatModel(chatModel)
.outputKey("movies") // 将返回值写入作用域的 "movies" 键
.build();
}

@Bean
public PlanningAgents.EveningPlannerAgent eveningPlannerAgent(
PlanningAgents.FoodExpert foodExpert,
PlanningAgents.MovieExpert movieExpert) {
return AgenticServices.parallelBuilder(PlanningAgents.EveningPlannerAgent.class)
.subAgents(foodExpert, movieExpert) // 参与并行执行的子 Agent
.executor(Executors.newFixedThreadPool(2)) // 自定义线程池
.outputKey("plans") // 最终合并结果写入的键
.output(this::mergeEveningPlans) // 合并函数
.build();
}
  • subAgents(foodExpert, movieExpert)指定需要并行执行的两个 Agent,executor(...)提供一个线程池,子 Agent 会并发运行在这两个线程上,outputKey("plans")代表合并后的最终结果会写回作用域的 "plans"

  • 自动合并发生在output(this::mergeEveningPlans)中,所有子 Agent 都执行完毕后,框架调用此函数从作用域中取出 mealsmovies,进行自定义合并?

    如果没有 .output() 回调会怎样?

    那么并行工作流的最终输出会是最后一个子 Agent 的返回值。

mergeEveningPlans 方法从 AgenticScope 中读取并行产生的两个列表,然后按位置一一配对,这个实现是要自己实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
private List<EveningPlan> mergeEveningPlans(AgenticScope agenticScope) {
List<String> movies = agenticScope.readState("movies", List.of());
List<String> meals = agenticScope.readState("meals", List.of());

List<EveningPlan> plans = new ArrayList<>();
for (int i = 0; i < movies.size(); i++) {
if (i >= meals.size()) {
break;
}
plans.add(new EveningPlan(movies.get(i), meals.get(i)));
}
return plans;
}

服务层的调用中,在 AgentOrchestrator 中,调用封装得非常简洁,其他 Agent 的封装也类似,它扮演的角色就是之前我们讲 Agent 架构的时候的 Orchestrator(协调者)

1
2
3
4
5
6
7
8
9
public Map<String, Object> parallelWorkflow(String mood) {
List<EveningPlan> plans = eveningPlannerAgent.plan(mood);
Map<String, Object> result = new LinkedHashMap<>();
result.put("mood", mood);
result.put("plans", plans);
result.put("learningPoint",
"parallelBuilder:并行执行子 Agent,output() 合并 AgenticScope 中的 meals/movies");
return result;
}

这里梳理一下 parallelBuilder 的内部运行机制

  • AgenticScope 在设计上是线程安全的。当并行任务同时写入 mealsmovies 时,因为键不同,不会发生竞争。这也正是我们刻意将输出键分开的原因。
  • parallelBuilder 会:
    1. 为每个子 Agent 创建一个 Callable 任务,提交到线程池。
    2. 使用 ExecutorService.invokeAll(或类似机制)等待所有任务完成。
    3. 如果某个任务抛出异常,会通过工作流的错误处理机制传播(可以配置 errorHandler)。
    4. 全部完成后,在主线程(或调用方线程)中执行 output 回调进行合并。

条件路由 Agent 示例

在真实业务中,一个用户请求往往需要先分类,再交由对应领域的专用的 Agent 来处理。

LangChain4j 的 条件路由 正是为这种场景而生的模式,先通过一个分类 Agent 判定请求类型,再根据分类结果动态选择执行某个专家 Agent。

本文以官方示例 ExpertRouterAgent为蓝本,进行改造,代码如下

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class RoutingAgents {

private RoutingAgents() {
}

public interface CategoryRouter {

@UserMessage("""
分析以下用户请求,将其分类为 writing、seo、fact_check 之一。
若无法归类,则返回 unknown。
只返回其中一个英文单词,不要解释。
用户请求:「{{request}}」
""")
@Agent(outputKey = "category", description = "对用户内容请求进行分类")
ContentRequestCategory classify(String request);
}

public interface WritingExpert {

@UserMessage("""
你是一位资深内容策划。
从写作角度分析以下请求,给出可执行的创作建议(200 字以内)。
用户请求:{{request}}
""")
@Agent(outputKey = "response", description = "内容写作专家")
String advise(String request);
}

public interface SeoExpert {

@UserMessage("""
你是一位 SEO 专家。
从搜索引擎优化角度分析以下请求,给出关键词与结构建议(200 字以内)。
用户请求:{{request}}
""")
@Agent(outputKey = "response", description = "SEO 优化专家")
String advise(String request);
}

public interface FactCheckExpert {

@UserMessage("""
你是一位事实核查编辑。
从信息准确性角度分析以下请求,指出需要核实的关键点(200 字以内)。
用户请求:{{request}}
""")
@Agent(outputKey = "response", description = "事实核查专家")
String advise(String request);
}

/**
* 条件工作流 + 路由器的强类型入口(官方 {@code ExpertRouterAgent} 模式)。
*/
public interface ExpertRouterAgent {

@Agent(outputKey = "response")
String ask(String request);
}
}
  • 问题分类的入口:首先,分类路由器作为一个分类用的 Agent,返回类型是 ContentRequestCategory的枚举,包含 WRITING, SEO, FACT_CHECK, UNKNOWN,分类完成之后,将问题路由给对应的专家 Agent
  • 三个专家 Agent 接口结构高度一致,参数都是 String requestoutputKey 都是 "response",这正是条件路由的巧妙之处:无论哪个专家被选中,结果都写入同一个键,外层入口可以无差别读取。它们只会在满足条件时被调用,不参与条件判断本身。

对于这个工作流,我们可以这样构建它,分为两步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Bean
public UntypedAgent expertsAgent(
RoutingAgents.WritingExpert writingExpert,
RoutingAgents.SeoExpert seoExpert,
RoutingAgents.FactCheckExpert factCheckExpert) {
return AgenticServices.conditionalBuilder()
.subAgents(
scope -> scope.readState("category", ContentRequestCategory.UNKNOWN) == ContentRequestCategory.WRITING,
writingExpert
)
.subAgents(
scope -> scope.readState("category", ContentRequestCategory.UNKNOWN) == ContentRequestCategory.SEO,
seoExpert
)
.subAgents(
scope -> scope.readState("category", ContentRequestCategory.UNKNOWN) == ContentRequestCategory.FACT_CHECK,
factCheckExpert
)
.build();
}
  • conditionalBuilder() 创建一个条件路由工作流,返回的是 UntypedAgent,它代表无特定接口类型,但可作为子 Agent 被其他工作流引用,这个非常重要
  • .subAgents(condition, agent) 注册了一对 条件 → Agent 的映射。条件是一个 Predicate<AgenticScope>,从作用域读取 category 的值,判断是否为对应枚举。
  • 注意,执行时,框架会按注册顺序评估条件,遇到第一个为 true 的条件就执行对应的 Agent,后续条件不再评估。

条件工作流只负责根据分类选专家,它本身不包含分类步骤。因此我们需要用 顺序工作流 把分类和条件路由串联起来

1
2
3
4
5
6
7
8
9
@Bean
public RoutingAgents.ExpertRouterAgent expertRouterAgent(
RoutingAgents.CategoryRouter categoryRouter,
UntypedAgent expertsAgent) {
return AgenticServices.sequenceBuilder(RoutingAgents.ExpertRouterAgent.class)
.subAgents(categoryRouter, expertsAgent)
.outputKey("response")
.build();
}

AgentOrchestrator 这个协调者中,条件工作流的调用被封装为和前面的 Agent 的一样的形式

1
2
3
4
5
6
7
8
9
public Map<String, Object> conditionalWorkflow(String request) {
String response = expertRouterAgent.ask(request);
Map<String, Object> result = new LinkedHashMap<>();
result.put("request", request);
result.put("response", response);
result.put("learningPoint",
"conditionalBuilder + sequenceBuilder:按 AgenticScope 中的 category 选择子 Agent");
return result;
}

其实核心就是,调用方无需关心分类细节,只需输入用户请求,即可获得对应领域的专家建议。

纯 Agentic AI / Supervisor 示例

它本质是多 Agent 协作架构的自动操作版本

之前提到的多 Agent 协作架构中,需要Orchestrator(协调者),而 Supervisor 模式中,Orchestrator 本身也是一个 LLM Agent。它根据用户的自然语言请求,动态生成一个执行计划,也就是AgentInvocation列表,然后框架按计划依次调用子Agent。

也就是说,Supervisor 是一种 LLM 驱动的自主编排模式。在 Supervisor 模式下,你不需要定义 sequenceBuilderconditionalBuilder,而是把所有可用的 Agent(及 Tool)注册给 Supervisor。LLM 会接收用户请求,自己决定

这就把编排逻辑从代码移到了LLM的推理中。是让 AI 管理 AI 的体现。

例如,我们有这样的一个场景,银行转账中的多 Agent 协作中,假设用户提出一个转账请求

“Transfer 100 EUR from Mario’s account to Georgios’s one”

这个请求看似简单,却涉及多个步骤:

  1. 货币兑换:EUR 转 USD(假设内部账户只存 USD)。
  2. 取款:从 Mario 的账户取出对应 USD。
  3. 存款:向 Georgios 的账户存入 USD。

如果使用顺序工作流,你需要硬编码:“先调用兑换,再取款,再存款”。但 Supervisor 模式可以让 LLM 自己规划这些步骤,甚至根据货币类型动态决定是否兑换、兑换顺序等。

那么,相关代码如下

AI 子 Agent

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface WithdrawAgent {
@SystemMessage("你是银行柜员,只能为用户账户执行 USD 取款操作。")
@UserMessage("从用户 {{user}} 的账户取出 {{amount}} USD,并返回新的余额。")
@Agent(description = "从账户取出 USD", outputKey = "withdraw")
String withdraw(String user, Double amount);
}

public interface CreditAgent {
@SystemMessage("你是银行柜员,只能为用户账户执行 USD 存款操作。")
@UserMessage("向用户 {{user}} 的账户存入 {{amount}} USD,并返回新的余额。")
@Agent(description = "向账户存入 USD", outputKey = "credit")
String credit(String user, Double amount);
}
  • 每个 Agent 都有一个清晰的 description,这是给 Supervisor 看的“能力说明”,LLM 据此决定何时调用它们。
  • 这两个 Agent 都绑定了一个共同的 BankTool(通过 .tools(bankTool) 配置),它们会利用 Function Calling 实际执行账户操作。这个简单提一下,我们下期再说这个 Tool

非 AI 子 Agent

1
2
3
4
5
6
7
8
9
public class ExchangeOperator {
@Agent(
value = "A money exchanger that converts ...",
outputKey = "exchange"
)
public Double exchange(String originalCurrency, Double amount, String targetCurrency) {
// 固定汇率兑换逻辑
}
}
  • 这个类不需要 LLM,只是普通的 Java 方法。前面也提到过,LangChain4j 的 Agent 支持你编排进去非 LLM 驱动的方法作为类似工具调用的感觉。通过 @Agent 注解标注,它就可以作为一个子 Agent 注册到 Supervisor 中。LLM 会将其视为一个可调用的工具,只不过这个工具的输入输出是结构化的参数,而不是自然语言。

那么,Supervisor 的配置组装是如何进行的呢??

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
@Bean
public BankAgents.WithdrawAgent withdrawAgent(
@Qualifier("agentChatModel") ChatModel chatModel,
BankTool bankTool) {
return AgenticServices.agentBuilder(BankAgents.WithdrawAgent.class)
.chatModel(chatModel)
.tools(bankTool) // 将 BankTool 的方法作为 Function Calling 工具
.build();
}

@Bean
public BankAgents.CreditAgent creditAgent(
@Qualifier("agentChatModel") ChatModel chatModel,
BankTool bankTool) {
return AgenticServices.agentBuilder(BankAgents.CreditAgent.class)
.chatModel(chatModel)
.tools(bankTool)
.build();
}

@Bean
public BankAgents.BankSupervisorAgent bankSupervisorAgent(
@Qualifier("agentChatModel") ChatModel chatModel,
BankAgents.WithdrawAgent withdrawAgent,
BankAgents.CreditAgent creditAgent,
ExchangeOperator exchangeOperator) {
return AgenticServices.supervisorBuilder(BankAgents.BankSupervisorAgent.class)
.chatModel(chatModel)
.supervisorContext("Policies: prefer internal tools; currency USD; no external APIs")
.subAgents(withdrawAgent, creditAgent, exchangeOperator)
.responseStrategy(SupervisorResponseStrategy.SUMMARY)
.contextGenerationStrategy(SupervisorContextStrategy.CHAT_MEMORY)
.build();
}

前面两个注册对应 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 更好地处理多轮复杂的交互(例如追问细节)。

WithdrawAgentCreditAgent 背后,是一个模拟银行账户操作的 BankTool,作为我们下一个提到的内容,有必要在这里进行衔接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Tool("Credit the given user with the given amount and return the new balance")
Double credit(@P("user name") String user, @P("amount") Double amount) {
Double balance = accounts.get(user);
if (balance == null) {
throw new RuntimeException("No balance found for user " + user);
}
double newBalance = balance + amount;
accounts.put(user, newBalance);
return newBalance;
}

@Tool("Withdraw the given amount from the given user and return the new balance")
Double withdraw(@P("user name") String user, @P("amount") Double amount) {
Double balance = accounts.get(user);
if (balance == null) {
throw new RuntimeException("No balance found for user " + user);
}
double newBalance = balance - amount;
accounts.put(user, newBalance);
return newBalance;
}

当 LLM 处理 WithdrawAgent 的请求 从用户 Mario 的账户取出 100 USD 时,它会通过 Function Calling 自动调用 BankTool.withdraw("Mario", 100.0)。返回值(新余额)被 LLM 用于生成自然语言回复,同时也写入作用域

这正是 Supervisor 模式的核心优势之一:将确定性业务逻辑(Tool)和 LLM 的推理能力无缝结合。LLM 不需要知道银行账户的具体操作,它只需明白取款这个意图,然后调用相应的工具函数即可。

AgentOrchestrator 中,Supervisor 转账被封装成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Map<String, Object> supervisorTransfer(
String fromUser, String toUser, double amount,
String originalCurrency, String supervisorContext) {

String request = "Transfer %s %s from %s's account to %s's one"
.formatted(amount, originalCurrency, fromUser, toUser);

// 记录转账前余额
Map<String, Object> balancesBefore = new LinkedHashMap<>(bankTool.snapshot());

// 调用 Supervisor
String response = supervisorContext == null || supervisorContext.isBlank()
? bankSupervisorAgent.invoke(request)
: bankSupervisorAgent.invoke(request, supervisorContext);

// 打包结果
Map<String, Object> result = new LinkedHashMap<>();
result.put("request", request);
result.put("response", response);
result.put("balancesBefore", balancesBefore);
result.put("balancesAfter", bankTool.snapshot());
result.put("learningPoint", "supervisorBuilder:LLM 生成 AgentInvocation 计划,调度 Tool 与非 AI 子 Agent");
return result;
}

所以说

  • 当业务流程稳定且步骤清晰时,用工作流模式更可控;
  • 当面对多变的用户意图、需要智能调度多个工具时,Supervisor 是更聪明的选择。