跳到主要内容

数据截至 (上游 commit daa7624a2755)

agentic:多智能体编排收敛成同一个 Planner 循环

30 秒导读: langchain4j-agentic 是 LangChain4j 后加的多智能体编排层。它最值得学的不是"支持了哪几种工作流", 而是它把所有工作流压成了同一套东西:一个执行循环 + 一块共享黑板 + 一个可替换的 Planner。 顺序、并行、循环、条件四种"工作流",在源码里只是四个几十行的 Planner 实现;连 LLM 自主调度的 supervisor 也是一个 Planner

本章讲编排层。单个 agent 怎么从 Java 接口变成一次 LLM 调用,见 02-ai-services.md; agent 手里的工具怎么来的,见 03-tool-calling.md


1. 这是什么(零基础也能懂)

一句话定义: 让多个 AI agent 按某种编排方式协作完成一个任务的 Java 层。

解决什么问题: 一个 agent 干不完的活,要拆给几个 agent 干。比如"写一篇小说": 先让写手出初稿、再让编辑按受众改写、再让另一个编辑按文体改写、最后让评分员打分,分不够就回炉重来。

这里面有四类烦人的事,都不是"调模型"本身:

烦人的事具体是什么
谁下一个跑顺序?并行?循环到满意为止?看条件分支?
数据怎么传上一个 agent 的输出,怎么变成下一个 agent 的入参
中途挂了怎么办跑到第 3 个 agent 时进程崩了,重启后要能接着跑
人要插一脚中间需要人确认/补信息,流程得停下来等

它能做什么(功能):

  • 五种内置编排:顺序、并行、并行 map、循环、条件路由,外加 LLM 自主调度的 supervisor。
  • 一块所有 agent 共享的键值黑板(AgenticScope),兼作执行轨迹记录仪。
  • 可持久化 + 崩溃恢复:黑板和"跑到第几步"都能落盘,重启后从断点续跑。
  • 人机协同(human-in-the-loop)、异步 agent、流式输出透传。
  • 接外部 agent:A2A 协议远端 agent、MCP 工具都能当成本地 agent 用。
  • 可观测性:监听器、执行监控器、生成 HTML 拓扑图和执行报告。

用起来什么样: 定义两个 agent 接口,拼成一个顺序工作流,一次调用跑完。

// 示意,非源码;接口定义与用法取自 langchain4j-agentic/src/test/java/dev/langchain4j/agentic/Agents.java:137-171
public interface CreativeWriter {
@UserMessage("Generate a story about {{topic}}.")
@Agent(description = "Generate a story", outputKey = "story") // 输出写进黑板的 "story" 键
String generateStory(@V("topic") String topic);
}

public interface AudienceEditor {
@UserMessage("Rewrite the story for {{audience}}: {{story}}")
@Agent(description = "Edit for audience", outputKey = "story") // 读黑板的 story,改完写回 story
String editStory(@V("story") String story, @V("audience") String audience);
}

UntypedAgent novelCreator = AgenticServices.sequenceBuilder() // 顺序编排
.subAgents(creativeWriter, audienceEditor)
.outputKey("story")
.build();

String story = (String) novelCreator.invoke(
Map.of("topic", "dragons and wizards", "audience", "young adults"));

重点看两件事:agent 之间没有直接互相调用,它们只跟黑板打交道;outputKey / @V 就是黑板的写键和读键。 真实用法见测试 langchain4j-agentic/src/test/java/dev/langchain4j/agentic/WorkflowAgentsIT.java:116-157(check_sequential_agents)。

一句话直觉: 把它想成一间会议室——黑板挂在墙上(AgenticScope), 门口站着一个主持人(Planner)决定"下一个谁上台",台上的人只读黑板、写黑板,谁也不认识谁。


2. 顶层全景(它大概怎么转)

一次 novelCreator.invoke(...) 的控制流,从左到右读:

用户调用


┌──────────────┐ 建 proxy ┌────────────────────────────┐
│ AgenticServices│ ──────────▶ │ ① 执行引擎(唯一的一个) │
│ 各种 builder │ │ PlannerBasedInvocationHandler│
└──────────────┘ └──────────┬─────────────────┘
│ 问"下一步调谁"

┌────────────────────┐
│ ② 主持人 Planner │ ← 顺序/并行/循环/条件/supervisor
│ firstAction │ 只在这里不一样
│ nextAction │
└──────────┬─────────┘
返回 Action:call / done / noOp


┌────────────────────┐
│ ③ 调用器 AgentExecutor│ ── 反射调 @Agent 方法
└──────────┬─────────┘
│ 读入参 / 写结果 / 记一笔

┌────────────────────┐
│ ④ 黑板 AgenticScope │ state + agentInvocations
└────────────────────┘

跑完一个 agent 后,③ 会回头通知 ①(onSubagentInvoked),① 再问 ② 要下一个 Action,如此往复直到 done

部件职责一句话:

部件干什么在哪个文件
AgenticServices静态门面,所有 builder 的入口langchain4j-agentic/src/main/java/dev/langchain4j/agentic/AgenticServices.java
PlannerBasedInvocationHandler唯一的执行引擎,JDK 动态代理的 InvocationHandler.../internal/PlannerBasedInvocationHandler.java
Planner决策接口,回答"下一步调谁".../planner/Planner.java
Action决策的词汇表:call / done / noOp.../planner/Action.java
AgentExecutorAction 落地成一次真实的方法反射调用.../internal/AgentExecutor.java
AgentInvoker从黑板取参数、调方法、发监听事件.../internal/AgentInvoker.java
AgenticScope共享黑板 + 调用轨迹.../scope/AgenticScope.java

主线走一遍(高层):

  1. AgenticServices.sequenceBuilder() 拿到一个 builder,.build() 时把 SequentialPlanner::new 交给统一的 build(Supplier<Planner>)(internal/AbstractServiceBuilder.java:170-181)。
  2. 那里 new 出 PlannerBasedInvocationHandler,包成 JDK 动态代理,返回给你当 agent 接口用。
  3. 你调接口方法 → invoke 分发到 executeAgentMethod(internal/PlannerBasedInvocationHandler.java:196-207):把入参写进黑板,建 Planner,启动循环。
  4. 循环反复问 Planner、执行 agent、把结果写回黑板,直到 Action.isDone()
  5. outputKey 从黑板取最终结果返回。

3. 入口:AgenticServices 门面与 @Agent 标注

这一节讲"你写的东西怎么被识别成 agent"。

3.1 一张 builder 清单

AgenticServices 是纯静态门面,私有构造(AgenticServices.java:78),全部能力就是下面这些工厂方法:

方法产出源码位置
agentBuilder() / agentBuilder(Class)单个 agent(无编排)AgenticServices.java:119:129
sequenceBuilder()顺序工作流AgenticServices.java:143:153
parallelBuilder()并行工作流AgenticServices.java:160:170
parallelMapperBuilder()对集合每项复制一份子 agent 并行跑AgenticServices.java:178:189
loopBuilder()循环工作流AgenticServices.java:196:206
conditionalBuilder()条件路由AgenticServices.java:213:223
supervisorBuilder()LLM 自主调度AgenticServices.java:231:241
plannerBuilder()自带 Planner,自定义编排AgenticServices.java:248:257
a2aBuilder(url)把远端 A2A agent 包成本地 agentAgenticServices.java:268:280
humanInTheLoopBuilder()人机协同 agentAgenticServices.java:136
createAgenticSystem(Class)从注解声明式地建整套系统AgenticServices.java:323-384

注意 plannerBuilder() 的存在:它把内部机制直接开放为公开 API——你自己写个 Planner 实现就能定义新编排。 这也是"所有工作流都是 Planner"这条结论最直接的证据。测试里就有一个"两两配对并行"的自定义 planner (langchain4j-agentic/src/test/java/dev/langchain4j/agentic/CustomPlannerIT.java:21-79,ParallelInPairsPlanner)。

工作流 builder 还有一层 SPI 间接:workflowAgentsBuilder()ServiceLoaderWorkflowAgentsBuilder 实现, 找不到才用默认的 WorkflowAgentsBuilderImpl.INSTANCE(AgenticServices.java:83-99)。Quarkus/Spring 集成靠这个换实现。

3.2 @Agent:方法级的标注

agent 不是类,是方法@Agent 只能打在方法上(Agent.java:14-16),关键属性:

属性作用行号
description / value给 LLM 看的能力描述(supervisor 靠它选人)Agent.java:31:39
outputKey结果写进黑板的哪个键Agent.java:46
async异步调用,不阻塞后续 agentAgent.java:55
optional入参在黑板里缺失时静默跳过而不是报错Agent.java:63
summarizedContext哪些别的 agent 参与构造本 agent 的上下文Agent.java:78

3.3 AgentInvoker:方法与黑板之间的适配层

AgentInvoker 负责"把黑板上的键值,凑成这个方法的实参"。它是接口,invoke 是带监听事件的默认实现 (internal/AgentInvoker.java:32-44):调用前发 beforeAgentInvocation,调用后发 afterAgentInvocation

真正反射调用在 internalInvoke(internal/AgentInvoker.java:46-57),里面有个细节: 调用前把黑板塞进 LangChain4jManaged.setCurrent(...),finally 里再清掉——这样被调 agent 内部也能拿到当前 AgenticScope

按 agent 来源分出几个实现:

实现用于取参数的方式
MethodAgentInvoker普通 @Agent 接口方法@V/参数名从黑板取(internal/MethodAgentInvoker.java:14-16)
UntypedAgentInvokerUntypedAgent.invoke(Map)整个 Map 直接进黑板
MapperAgentInvokerparallelMapper 的每个实例固定绑一个集合元素 + 下标
SpecAgentInvoker非 AI agent(普通 Java 方法)AgentSpecsProvider 描述

分发点在 AgentInvoker.fromMethod(internal/AgentInvoker.java:73-79)。


4. 核心:唯一的执行引擎

这节是本章的心脏。 无论你用哪个 builder,跑起来的都是同一个 PlannerLoop.loop()

4.1 它要解决的小问题

编排引擎最容易写成"一个工作流一个执行器":顺序执行器 for 循环、并行执行器 CompletableFuture.allOf、 循环执行器 while、条件执行器 if。四份代码,四套 bug,四套持久化。

LangChain4j 的做法是把"决策"从"执行"里抽出来:执行永远是同一段代码,变的只是"谁来告诉我下一步调谁"。

4.2 决策的词汇表:Action

Planner 只能返回三类东西(planner/Action.java):

Action含义
call(agents...)调这一批 agent(多个 = 并行)Action.AgentCallAction(Action.java:90-115)
done() / done(result)结束,可带最终结果Action.DoneAction(:15-28)、DoneWithResultAction(:30-52)
noOp()空调用,啥也不干但不结束Action.NoOpAction(:81-83)

noOpAgentCallAction 的空列表子类,这让循环可以"这一轮没人可调,但别退出"——并行分支里等其它分支时会走到。

Planner 接口上直接给了这三个的工厂默认方法(planner/Planner.java:78-119),所以实现类里写 return done(); 就够了。

4.3 循环本体

PlannerLoop.loop()(internal/PlannerBasedInvocationHandler.java:417-458),整个引擎就这么点东西:

nextAction = planner.firstAction(new PlanningContext(agenticScope, null));
while (nextAction == null || !nextAction.isDone()) {
if (nextAction == null) { Thread.yield(); continue; }
List<AgentExecutor> agents = ((Action.AgentCallAction) nextAction).agentsToCall();
nextAction = null;
switch (agents.size()) {
case 0 -> Thread.yield();
case 1 -> agents.get(0).execute(agenticScope, this);
default -> parallelExecution(agents);
}
}

这段真源码(:369-382)有四个值得注意的点:

  • nextAction 被置空后才执行 agent(:376)。下一个动作不是循环体算出来的,而是 agent 跑完后回调填进来的。
  • 一个 = 同步调,多个 = 并行调(:377-381)。并行只是同一个 Action 里装了多个 agent,parallelExecutionCompletableFuture.allOf 等齐(:394-407)。
  • nextAction == nullThread.yield() 自旋(:371-374)。异步 agent 还没回调时,主线程在这里让出 CPU 等。
  • nextActionvolatile(:354),因为回调可能发生在并行线程上。

4.4 回调:决策发生在这里

onSubagentInvoked(:423-440)是 PlannerExecutor 接口的实现,由 AgentExecutor 在每个 agent 跑完后调用:

lock.lock();
try {
this.nextAction = composeActions(this.nextAction,
planner.nextAction(new PlanningContext(agenticScope, agentInvocation)));
Map<String, Object> execState = planner.executionState();
if (!execState.isEmpty()) { agenticScope.writeState(executionStateId(), execState); }
if (registry != null) { agenticScope.checkpoint(registry); }
} finally { lock.unlock(); }

三件事一次做完:问下一步 → 存 planner 进度 → 黑板落盘。用 ReentrantLock 串行化(:352), 因为并行分支的多个 agent 会同时回调。

composeActions(:568)处理并行场景:两个分支各自返回一个 call,就把两批 agent 合成一个 AgentCallAction 一起跑; 任一边是 done 或空调用,就取另一边。

4.5 崩溃恢复:两条线各存各的

存什么存在哪谁负责
业务数据(story、score…)AgenticScope.stateagent 的 outputKey
跑到第几步(cursor、iteration…)黑板里的 __planner_state_<agentId>Planner.executionState()

前缀常量 EXECUTION_STATE_PREFIX = "__planner_state_" 定义在 :347,键名由 executionStateId() 拼出(:390-392), 所以嵌套的多个 planner 各存各的、互不覆盖。

恢复时 loop() 开头先读回来喂给 restoreExecutionState(:364-367),跑完再把这个键清掉(:385)。

Planner 接口的注释把契约写得很明白(planner/Planner.java:10-33): executionState() 存的东西,必须满足"restore 之后调 firstAction 能得出正确的续跑动作"。 这解释了为什么 SequentialPlanner 存的是 agentCursor - 1LoopPlanner 存的是 agentCursor 本身——见下一节。


5. 关键结论:四种工作流 = 四个 Planner

顺序、并行、循环、条件不是四个引擎,是四个几十行的类。 逐个看它们有多小。

工作流Planner 实现行数核心逻辑
顺序SequentialPlanner52游标 agentCursor++,越界就 done()
并行ParallelPlanner34firstAction 一次性 call(全部),nextActiondone()
循环LoopPlanner103游标取模回绕 + 迭代计数 + 退出条件
条件ConditionalPlanner47过滤谓词为真的子 agent,一次性 call
并行 mapParallelMapperPlanner105按集合元素克隆出 N 个调用实例,收齐后归并

顺序的全部决策逻辑就一行(workflow/impl/SequentialPlanner.java:23-25):

return terminated() ? done() : call(agents.get(agentCursor++));

并行的更短(workflow/impl/ParallelPlanner.java:21-28):firstAction 返回 call(agents) 整个列表, nextAction 直接 done()——并行不是引擎特性,只是一个 Action 里装了多个 agent,由 loop()default -> 分支去开线程。

条件路由(workflow/impl/ConditionalPlanner.java:16-22)是个 record:按谓词过滤,选中的一起 call,没人中就 done()

5.1 样本细读:LoopPlanner

循环是四者里状态最多的,拿它当样本最有代表性。三个状态字段(workflow/impl/LoopPlanner.java:17-27):

字段作用
agentCursor本轮跑到第几个子 agent
iterationsCounter跑到第几轮(从 1 开始)
exitConditionBiPredicate<AgenticScope, Integer>,拿黑板 + 轮次判断该不该退出
testExitAtLoopEnd退出条件只在一轮结束时测,还是每个 agent 后都测

决策逻辑(LoopPlanner.java:46-57):

agentCursor = (agentCursor + 1) % agents.size(); // 取模 → 一轮跑完自动回到第一个
if (agentCursor == 0) { // 回绕说明一轮结束
if (iterationsCounter >= maxIterations
|| exitCondition.test(planningContext.agenticScope(), iterationsCounter)) {
return done();
}
iterationsCounter++;
} else if (!testExitAtLoopEnd
&& exitCondition.test(planningContext.agenticScope(), iterationsCounter)) {
return done(); // 允许轮中提前退出
}
return call(agents.get(agentCursor));

用图看一轮(3 个子 agent,testExitAtLoopEnd = false):

┌───────────────────────────────────────────────┐
│ │
▼ │
cursor=0 ──▶ cursor=1 ──▶ cursor=2 ──▶ (%3) cursor=0
A 跑完 B 跑完 测退出 C 跑完 测退出 ├─ 轮数够 或 退出条件真 ──▶ done
条件 条件 └─ 否则 iteration++ ──────┘

为什么两个 planner 存的 cursor 差 1? 这是全章最容易看漏的细节,源码注释里写了:

  • SequentialPlanner.executionState()agentCursor - 1(SequentialPlanner.java:38-43),因为它的 firstAction 走默认实现(即 nextAction),恢复后会执行 agents.get(agentCursor++) —— 游标必须指向"需要重跑的那个"。
  • LoopPlanner.executionState()agentCursor 原值(LoopPlanner.java:85-90),因为它覆写了 firstActioncall(agents.get(agentCursor))(:41-43),不推进游标。

ParallelPlannerConditionalPlanner 干脆不实现这两个方法,吃 Planner 的默认空实现——无状态的 planner 天然无需恢复

5.2 topology():结构信息也从 Planner 来

每个 planner 声明自己的拓扑类型(planner/AgenticSystemTopology.java:AI_AGENT / NON_AI_AGENT / HUMAN_IN_THE_LOOP / SEQUENCE / PARALLEL / LOOP / ROUTER / STAR)。 PlannerBasedInvocationHandler.topology() 直接转发给默认 planner 实例(:332-334)。 可观测性那节的 HTML 拓扑图,就是靠递归读这个字段画出来的。

顺带:Planner.as(Class, AgentInstance) 是个向下转型钩子(planner/Planner.java:131-133),默认抛 ClassCastException; LoopPlanner 覆写它返回 DefaultLoopAgentInstance(LoopPlanner.java:65-70),让外部能读到 maxIterations() / exitCondition() 这类循环专属元信息。


6. 共享状态黑板:AgenticScope

上面反复说"黑板",这节讲它到底是什么。

6.1 它要解决的小问题

agent A 的输出要给 agent B 当输入,但 A 和 B 互不认识。传统做法是编排器手动接线; 这里改成约定一个共享命名空间:A 写 outputKey="story",B 的参数标 @V("story"),接线自动完成。

6.2 里面装了什么

DefaultAgenticScope 的四个字段(scope/DefaultAgenticScope.java:49-57):

字段类型装什么
stateConcurrentHashMap键值黑板本体
agentInvocations同步 List每次调用的 (类型, 名字, id, 入参, 输出) 记录
context同步 List<AgentMessage>每个 agent 的最后一问一答,供上下文摘要用
agents / executionContextstransient Map不参与序列化的运行期对象

读写有两个非平凡细节:

  • writeState(key, null)删除语义,不是写空(:132-140)。
  • readState 遇到 DelayedResponse(异步/流式)会阻塞取值,并把取到的实值写回黑板(readStateBlocking,:181-187)——异步 agent 的 join 点藏在这里,谁先读谁负责等。

调用记录由 registerAgentInvocation 落下(:198-203),同时从 agent 的 ChatMemory 里挑出最后一条 UserMessage + AiMessage 存进 context (registerContextFromChatMemory,:246-266)——只留一问一答,agent 内部的多轮工具调用不进共享上下文。

6.3 三种寿命

Kind 枚举分三档(:75-81),决定生命周期和锁策略:

Kind何时用结束时是否加锁
EPHEMERAL调用方法里没有 @MemoryId立刻从注册表 evict
REGISTERED@MemoryId,但没配持久化 store留在内存
PERSISTENT@MemoryId 且配了 storeflush 到 storeReadWriteLock

锁的用法反直觉,注释专门解释了(:103-109):修改状态时拿读锁,持久化时拿写锁。 因为内部结构本身线程安全,并发修改无所谓;不能容忍的是"在半更新状态下被序列化"。

6.4 持久化与恢复

AgenticScopeRegistry ──(有 store 时)──▶ AgenticScopePersister.store ──▶ AgenticScopeStore(SPI)
内存 Map: ServiceLoader 找实现 save / load / delete / getAllKeys
key = (agentId, memoryId)
  • AgenticScopeRegistry(scope/AgenticScopeRegistry.java:35-69):内存 ConcurrentHashMap 缓存,get 未命中才回落到 store 加载(:41-52);create 时按有没有 store 决定 PERSISTENT 还是 REGISTERED(:54-58)。
  • AgenticScopePersister(scope/AgenticScopePersister.java:5-30):单元素枚举,ServiceLoader 找第一个 AgenticScopeStore,没有就 null(即不持久化);也可以 setStore 手动装。
  • AgenticScopeStore(scope/AgenticScopeStore.java:10-42):四方法 SPI,自己实现就能落到文件/DB。
  • 落盘前会做 serializableCopy()(:83-93),用 isSerializable 过滤掉动态代理、TokenStreamFuture 这三类不可序列化的值(:60-73:95-97)。

落盘时机有两个:每次 agent 调用后的 checkpoint(:368-372,只对 PERSISTENT 生效),和根调用结束时的 rootCallEnded(:207-217)。 后者还会先 state.replaceAll(this::readStateBlocking) —— 强制把所有异步结果 join 掉再收工。

6.5 ResultWithAgenticScope:把黑板一起带出来

普通调用只返回结果。若把方法返回类型声明成 ResultWithAgenticScope<T>,引擎会连黑板一起包回来 (internal/PlannerBasedInvocationHandler.java:235-237),定义见 scope/ResultWithAgenticScope.java:21。 用来在外面查中间态、查调用轨迹。


7. supervisor:一个自己造 LLM 来当主持人的 Planner

前面四种工作流是"人写死流程"。supervisor 是"让 LLM 决定流程"——但它依然只是一个 Planner

7.1 自指结构

SupervisorPlanner (是一个 Planner)

│ nextAction() 要决定下一步调谁

AiServices.builder(PlannerAgent.class) ← 用 LangChain4j 自己的 AiServices
│ 造一个 LLM 驱动的"规划 agent"

PlannerAgent.plan(...) ──▶ AgentInvocation{agentName, arguments}


找到同名 agent ──▶ return call(agent) ← 变回一个普通 Action

妙在哪: 编排层用它自己的下层能力(02-ai-services.mdAiServices)实现了自己的调度器。 对执行引擎而言,supervisor 和 SequentialPlanner 没有任何区别。

真源码:buildPlannerAgent 就是一句 AiServices.builder(PlannerAgent.class).chatModel(chatModel) (supervisor/SupervisorPlanner.java:242),外面用 getOrCreateAgent 按 agentId 缓存到黑板上(:219-221)。

7.2 决策一轮

nextAction(supervisor/SupervisorPlanner.java:98-107)先取上一个 agent 的输出当 lastResponse, 超过 maxAgentsInvocations 就强制收尾,否则进 nextSubagent(:145-166):

  1. 组 supervisor context(黑板里的 supervisorContext 键)。
  2. plannerAgent.plan(memoryId, agentsList, request, lastResponse, supervisorContext)
  3. 返回的 agentName 若是 "done"doneAction;否则按名字找到 agent。
  4. 把 LLM 给的参数写进黑板,然后 return call(agent)

agent 清单是拼给 LLM 的"名片"字符串,格式 {'id', 'description', [arg: type, ...]}(toCard,:109-115), 复杂类型会递归展开 record 组件/字段(argumentDescription,:121-143)。@Agent(description=...) 的价值就在这里兑现。

提示词本身在 supervisor/PlannerAgent.java:11-46,里面明确要求模型"不要用自己的知识、只依赖给定 agent、小步走"。

7.3 三个防坑设计

表现对策
LLM 用字符串覆盖结构化状态黑板里本来存着 Person 对象,被 LLM 生成的字符串顶掉writeArgumentToScope 做类型兼容检查,不兼容就不写(:193-209)
上下文越滚越长supervisor 多轮后 chat memory 爆SupervisorContextStrategy 三档:CHAT_MEMORY / SUMMARIZATION / CHAT_MEMORY_AND_SUMMARIZATION(:250-271)
最终返回哪个答案最后一个 agent 的输出 vs LLM 的总结,哪个更好?SupervisorResponseStrategy 三档:LAST / SUMMARY / SCORED(:232-241)

SCORED 最有意思:再造一个 LLM 当裁判ResponseAgent 给两个候选各打一分,取高的那个 (:235-240,配 supervisor/ResponseScore.javascore1/score2)。

supervisor 也存进度:executionStateloopCount(:277-288)。


8. 声明式注解版 API

同一套能力有两副面孔。命令式是"拿 builder 拼",声明式是"在接口上打注解,createAgenticSystem 反射出来"。

8.1 对应关系

命令式 builder声明式注解注解定义
sequenceBuilder()@SequenceAgent(subAgents = {...})declarative/SequenceAgent.java:29
parallelBuilder()@ParallelAgentdeclarative/ParallelAgent.java
parallelMapperBuilder()@ParallelMapperAgent(subAgent=, itemsProvider=)declarative/ParallelMapperAgent.java
loopBuilder()@LoopAgent(maxIterations = 5, subAgents = {...})declarative/LoopAgent.java:31:77
.exitCondition(...)@ExitCondition 静态方法declarative/ExitCondition.java:34
conditionalBuilder().subAgent(pred, a)@ConditionalAgent + @ActivationCondition(X.class)declarative/ConditionalAgent.java:46ActivationCondition.java:42
supervisorBuilder()@SupervisorAgent(contextStrategy=, responseStrategy=)declarative/SupervisorAgent.java:34
plannerBuilder().planner(...)@PlannerAgent + @PlannerSupplierdeclarative/PlannerAgent.java:33PlannerSupplier.java:32
.output(scope -> ...)@Output 静态方法declarative/Output.java:41
.chatModel(m)@ChatModelSupplier 静态方法declarative/ChatModelSupplier.java

一个真实例子(取自 langchain4j-agentic/src/test/java/dev/langchain4j/agentic/DeclarativeAgentIT.java:362-384):

public interface StyleReviewLoopAgent {
@LoopAgent(outputKey = "story", maxIterations = 5,
subAgents = {StyleScorer.class, StyleEditor.class})
String reviewAndScore(@V("story") String story);

@ExitCondition
static boolean exit(@V("score") double score) { return score >= 0.8; }
}

public interface StoryCreatorWithReview {
@SequenceAgent(outputKey = "story",
subAgents = {CreativeWriter.class, StyleReviewLoopAgent.class})
ResultWithAgenticScope<String> write(@V("topic") String topic, @V("style") String style);
}

注意 StyleReviewLoopAgent 既是一个工作流又是另一个工作流的子 agent——编排可以任意嵌套。

8.2 声明式只是 builder 的一层壳

createAgenticSystemcreateComposedAgent(AgenticServices.java:386-437)是一串 if: 按 @SequenceAgent@LoopAgent@ConditionalAgent@ParallelAgent@ParallelMapperAgent@SupervisorAgent@PlannerAgent 的顺序找注解, 命中就转调对应的 buildXxxAgent(如 buildLoopAgent,:448-465),而那些方法里做的第一件事就是调同名的命令式 builder:

var builder = loopBuilder(agentServiceClass)
.subAgents(createSubagents(annotation.subAgents(), chatModel, agentConfigurator))
.maxIterations(annotation.maxIterations());

所以两副面孔背后是同一条路。createBuiltInAgentExecutor(:608-698)是同一串 if 的"当子 agent 用"版本, 末尾多接了 @HumanInTheLoop@A2AClientAgent@McpClientAgent,以及静态方法形式的非 AI agent(:683-695)。

AgentConfigurator(AgenticServices.java:293-313)是给框架集成留的三个钩子:改配置、换子 agent 解析、换实例工厂—— Spring/Quarkus 用它把容器里的 bean 塞进来。


9. 人机协同与异步

三种"结果还没到手"的情况,共用一个 DelayedResponse 抽象,黑板 readState 时统一阻塞取值。

场景结果从哪来源码
AsyncResponse@Agent(async = true)构造时就 supplyAsync 丢线程池跑internal/AsyncResponse.java:7-14
StreamingResponseagent 返回 TokenStream订阅 onCompleteResponse 填 futureinternal/StreamingResponse.java:7-16
PendingResponse人机协同、外部事件阻塞调用线程等外部 complete()(future 语义)internal/PendingResponse.java:6-32

这一层的公共基类换成了 DeferredResponse(internal/DeferredResponse.java:26):responseIdblockingGetcomplete 都在它身上;"反序列化后重建未完成的 future、外部系统凭 responseId 重新接上" 这套跨重启语义也写在它的 javadoc 里(internal/DeferredResponse.java:12-19),PendingResponseSuspendedResponse 只是它的两个落地形态。黑板侧的配套 API 是 completePendingResponse(id, value)pendingResponseIds() (scope/DefaultAgenticScope.java:435:450)。

SuspendedResponse(internal/SuspendedResponse.java:8-26)是另一个落地形态:遇到它时系统打 checkpoint 然后抛 AgenticSystemSuspendedException 释放线程(而不是阻塞);恢复时外部 completePendingResponse 后用同一 memory id 重新调一次 agent 方法。也就是说:同线程等,用 PendingResponse;跨进程崩溃恢复,用 SuspendedResponse

HumanInTheLoop 本身极简——一个 record,@Agent 方法只是把黑板交给 responseProvider 函数 (workflow/HumanInTheLoop.java:13-25)。同步问答就在 responseProvider 里读控制台;要挂起等外部事件,就返回一个 PendingResponse(要跨重启则返回 SuspendedResponse)。

流式还有个透传开关:propagateStreaming()(internal/PlannerBasedInvocationHandler.java:564-565)只在 "允许流式输出 planner 已终止"时为真。AgentExecutor 据此决定把 TokenStream 原样传出去,还是包成 StreamingResponse 先收干 (internal/AgentExecutor.java:131-132)——只有最后一个 agent 的流才能流给用户,中间 agent 的流必须先收完给下一个当输入。

崩溃恢复的完整走查见测试 langchain4j-agentic/src/test/java/dev/langchain4j/agentic/RecoverabilityIT.java:131-188 (workflow_recovers_from_crash_with_human_in_the_loop):清空内存 → 从文件 store 加载 → SequentialPlanner 恢复 cursor=2 → 跳过已跑完的两步。


10. 外部 agent 接入:A2A 与 MCP

核心思路一句话:远端 agent 也做成动态代理,伪装成本地 @Agent 接口。

两个可选模块,都通过 ServiceLoader 挂进来:

模块接的是什么门面缺依赖时
langchain4j-agentic-a2aA2A 协议的远端 agentAgenticServices.a2aBuilder(url)DummyA2AService 抛"请加依赖"(internal/A2AService.java:38-56)
langchain4j-agentic-mcpMCP server 的某个 toolMcpService.get().mcpBuilder(client, cls)同样的 Dummy 兜底

A2A 侧,DefaultA2AClientBuilder 构造时就去拉 agent card(A2A.getAgentCard(url), langchain4j-agentic-a2a/src/main/java/dev/langchain4j/agentic/a2a/DefaultA2AClientBuilder.java:77-101), build() 返回一个实现了你的接口 + A2AClientInstance 的代理(:98-106)。 调用时从黑板按 inputKeys 取参数拼成 A2A Message,回来的 contextId / taskId 再写回黑板(:124-140), 配合 @A2AContextId / @A2ATaskId 参数注解做多轮会话。

MCP 侧同理:DefaultMcpClientBuilder.build() 建代理(langchain4j-agentic-mcp/src/main/java/dev/langchain4j/agentic/mcp/DefaultMcpClientBuilder.java:89-107), 按 toolName 从 client 的工具列表里挑一个,挑不到就报"可用工具有 X、Y"(:114-128)。 一个 MCP tool = 一个 agent,不是一整个 MCP server。


11. 可观测性

三层,从轻到重:

是什么源码
AgentListener8 个默认空方法的钩子接口observability/AgentListener.java:8-30
AgentMonitor一个把所有事件攒起来的 listener 实现observability/AgentMonitor.java:26-40
HtmlReportGenerator把 monitor 的数据渲染成 HTMLobservability/HtmlReportGenerator.java:27-87

AgentListener 覆盖的事件:agent 调用前/后/出错、黑板创建/销毁、agent 内工具执行前/后。 有个关键开关 inheritedBySubagents()(:26-28),默认 false;返回 true 时会被 registerInheritedParentListener 递归安装到所有子 agent 上(internal/PlannerBasedInvocationHandler.java:354-360)。

AgentMonitormemoryId 分桶存 MonitoredExecution,分成进行中/成功/失败三组, 有 maxRetainedSessions 上限(默认 100,AgentMonitor.java:28-34)防内存泄漏;它自己 inheritedBySubagents() 返回 true(:150-153),一挂全树可见。

HtmlReportGenerator 出三种页面:generateTopology(只画结构,不需要跑过)、generateReportgenerateExecution(:31-87)。 拓扑图靠递归 subagents() + topology() 上色画出来(cssCls / label / color,:821-847)。

注:接口若继承 MonitoredAgent,builder 会自动挂一个 AgentMonitor(internal/AbstractServiceBuilder.java:171-179),不用手配。


12. 巧妙之处(可带走的技术)

  • 决策与执行分离到极致。 把 while 循环里的"下一步是什么"抽成一个接口方法,四种工作流就退化成四个几十行的类 (workflow/impl/SequentialPlanner.java:12 vs ParallelPlanner.java:11)。 这个模式对任何"多种执行策略"的引擎都适用。

  • 并行不是引擎特性,是数据形状。 Action.AgentCallAction 里装几个 agent,决定了 loop() 走单线程还是 CompletableFuture (internal/PlannerBasedInvocationHandler.java:485-492)。并行/串行的差别被压进一个 List.size()

  • 下一步动作靠回调填,而不是循环体算。 nextAction = null 后由 onSubagentInvoked 补上(:548-556), 这让异步 agent、并行分支合流(composeActions,:447-459)自然地长在同一套代码里。

  • 持久化的两条线分开管。 业务数据走 outputKey,执行进度走 __planner_state_<agentId> 前缀键(:399:482)。 嵌套编排各存各的,恢复时互不干扰。

  • 读锁保护修改、写锁保护持久化。 反直觉但对(scope/DefaultAgenticScope.java:115-124 的锁初始化、写锁 flush :247-252、读锁 withReadLock :365-372): 内部结构本身线程安全,要防的是"序列化到一半的快照"。

  • 框架用自己的下层实现自己的上层。 supervisor 用 AiServices.builder(PlannerAgent.class) 造调度器 (supervisor/SupervisorPlanner.java:242),对引擎而言它跟顺序 planner 平权。

  • optional agent 静默跳过。 参数在黑板里缺失时,optional=true 的 agent 不炸整条流水线,而是读一下 outputKey 就走人 (internal/AgentExecutor.java:67-80)。条件分支不必显式建模。

  • 流式的"最后一棒"规则。 只有 planner 已终止时才把 TokenStream 透传出去,否则先收干 (:564-565 + internal/AgentExecutor.java:131-132)。


13. 边界与局限

  • Planner 的接口是"一步一问",不是"一次出全图"。 没有内置的"先规划完整 DAG 再执行"。 想要 DAG,得自己在 Planner 实现里维护;langchain4j-agentic-patterns 模块提供了几个现成的 (GoalOrientedPlannerDebatePlannerVotingPlannerBlackboardPlannerP2PPlanner),但那是另一个模块的事。

  • 恢复的粒度是"agent 边界"。 checkpoint 发生在每个 agent 调用之后(:434-436), 一个 agent 执行到一半崩了,重启会整个重跑这个 agent。没有 agent 内部的断点。

  • 黑板是扁平的全局命名空间。 所有 agent 共用一套 key,outputKey 撞名就互相覆盖(顺序改写 story 就是故意利用这点)。 TypedKey 只是给 key 加了类型标注,并没有做作用域隔离。

  • readState 的隐式阻塞。 异步 agent 的结果在被读到那一刻才 join(:181-187)。 收益是写法自然,代价是阻塞点不显式——性能问题不好定位。

  • Thread.yield() 自旋等待。 等异步回调时是忙等让出而非条件变量(:371-374:378)。 简单可靠,但在动作稀疏的长流程上不是最省 CPU 的写法。

  • supervisor 依赖模型遵循提示词。 agent 名字对不上会直接抛 IllegalStateException(supervisor/SupervisorPlanner.java:187-189), 只有"恰好一个同名 agent"时才做一次 name 兜底匹配(:180-186)。


14. 代码地图(导航索引)

主题文件路径(相对克隆根)关键符号
入口门面、所有 builderlangchain4j-agentic/src/main/java/dev/langchain4j/agentic/AgenticServices.javasequenceBuilderloopBuildersupervisorBuilderplannerBuildercreateAgenticSystemcreateComposedAgent
agent 标注.../agentic/Agent.java@Agent(outputKeyasyncoptionaldescription)
执行引擎(先读这个).../agentic/internal/PlannerBasedInvocationHandler.javaPlannerLoop.looponSubagentInvokedcomposeActionsexecuteAgentMethodEXECUTION_STATE_PREFIX
决策接口.../agentic/planner/Planner.javafirstActionnextActionexecutionStaterestoreExecutionStatetopologycall/done/noOp
决策词汇表.../agentic/planner/Action.javaAgentCallActionDoneActionDoneWithResultActionNoOpAction
顺序编排.../agentic/workflow/impl/SequentialPlanner.javaagentCursorterminatedexecutionState
并行编排.../agentic/workflow/impl/ParallelPlanner.javafirstAction
循环编排.../agentic/workflow/impl/LoopPlanner.javaagentCursoriterationsCounterexitConditiontestExitAtLoopEnd
条件路由.../agentic/workflow/impl/ConditionalPlanner.javaconditionalSubagentsfirstAction
并行 map.../agentic/workflow/impl/ParallelMapperPlanner.javaitemsProvidercollectItemscompletedCount
builder → planner 的接线.../agentic/internal/AbstractServiceBuilder.javabuild(Supplier<Planner>)
单次调用落地.../agentic/internal/AgentExecutor.javaexecuteinternalExecutecompleteAgentInvocationagentResponse
参数适配.../agentic/internal/AgentInvoker.javaMethodAgentInvoker.javatoInvocationArgumentsfromMethodinternalInvoke
共享黑板.../agentic/scope/AgenticScope.javaDefaultAgenticScope.javawriteStatereadStateBlockingregisterAgentInvocationcheckpointKindserializableCopy
黑板持久化.../agentic/scope/AgenticScopeRegistry.javaAgenticScopePersister.javaAgenticScopeStore.javaget/create/evictstoresave/load
supervisor.../agentic/supervisor/SupervisorPlanner.javanextSubagentbuildPlannerAgenttoCardwriteArgumentToScopedoneAction
supervisor 提示词.../agentic/supervisor/PlannerAgent.javaplan
supervisor 策略.../agentic/supervisor/SupervisorContextStrategy.javaSupervisorResponseStrategy.javaResponseScore.javaCHAT_MEMORY/SUMMARIZATIONLAST/SUMMARY/SCORED
声明式注解.../agentic/declarative/@SequenceAgent@LoopAgent@ExitCondition@ConditionalAgent@ActivationCondition@SupervisorAgent@PlannerAgent@Output
人机协同.../agentic/workflow/HumanInTheLoop.javaaskUserresponseProviderHumanInTheLoopBuilder
延迟结果langchain4j-agentic/src/main/java/dev/langchain4j/agentic/internal/DeferredResponse(基类)、AsyncResponsePendingResponseSuspendedResponseStreamingResponse;符号 blockingGetcompleteresponseId
可观测性.../agentic/observability/AgentListenerAgentMonitorHtmlReportGenerator.generateTopology
A2A 接入langchain4j-agentic-a2a/src/main/java/dev/langchain4j/agentic/a2a/DefaultA2AClientBuilder.javaagentCardbuildinvokeAgent
MCP 接入langchain4j-agentic-mcp/src/main/java/dev/langchain4j/agentic/mcp/DefaultMcpClientBuilder.javatoolNamebuild
端到端用例langchain4j-agentic/src/test/java/dev/langchain4j/agentic/WorkflowAgentsITDeclarativeAgentITSupervisorAgentITCustomPlannerITRecoverabilityIT