Agents 与 Agentic AI
Agentic 系统
尽管目前还没有一个公认的 AI Agent(智能体)的定义,但一些新兴模式表明,可以通过协调和组合多个 AI 服务的能力来创建能够完成更复杂任务的 AI 应用。这些模式通常被称为 Agentic 系统 或 Agentic AI。它们通常涉及使用大语言模型(LLMs)来:
- 协调任务执行
- 管理工具调用
- 在交互中保持上下文
根据 Anthropic 研究人员最近发表的一篇文章,这些 Agentic 系统架构 可以分为两大类:
- 工作流(workflows)
- 纯代理(pure agents)

本教程中讨论的 langchain4j-agentic
模块,提供了一套抽象和工具,帮助你构建工作流型和纯代理型 AI 应用。它允许你定义工作流、管理工具调用,并在不同 LLM 交互中保持上下文。
LangChain4j 中的 Agent
在 LangChain4j 中,Agent 是使用 LLM 执行某个特定任务(或一组任务)的组件。
Agent 可以通过在接口方法上添加 @Agent
注解来定义,写法类似普通 AI 服务。
public interface CreativeWriter {
@UserMessage("""
You are a creative writer.
Generate a draft of a story no more than
3 sentences long around the given topic.
Return only the story and nothing else.
The topic is {{topic}}.
""")
@Agent("Generates a story based on the given topic")
String generateStory(@V("topic") String topic);
}
最佳实践是:在 @Agent
注解中提供简短的描述,尤其在 纯代理模式 下,其他 Agent 需要了解该 Agent 的能力,以便决定何时调用。
可以使用 AgenticServices.agentBuilder()
方法来创建该 Agent 的实例,并指定接口与 Chat 模型:
CreativeWriter creativeWriter = AgenticServices
.agentBuilder(CreativeWriter.class)
.chatModel(myChatModel)
.outputName("story")
.build();
从本质上讲,Agent 与普通 AI 服务功能相同,只是它们可以被组合到更复杂的工作流和 Agentic 系统中。
主要区别在于:
Agent 需要指定 outputName
—— 即结果会存储在共享变量中,以便其他 Agent 使用。
你也可以直接在注解中声明,而不是在代码中设置,例如:
@Agent(outputName = "story", description = "Generates a story based on the given topic")
AgenticServices
类提供了一系列静态工厂方法,用于定义和创建 langchain4j-agentic
框架支持的各种 Agent。
引入 AgenticScope
langchain4j-agentic
模块引入了 AgenticScope 的概念。
AgenticScope 是一个由多个 Agent 共享的数据集合,用于存储共享变量:
- 一个 Agent 可以写入它产生的结果
- 另一个 Agent 可以读取这些结果来完成任务
这样,Agent 之间就能高效协作,按需共享信息与结果。
此外,AgenticScope
会自动记录:
- 各个 Agent 的调用顺序
- 每次调用的响应
当 Agentic 系统的主 Agent 被调用时,AgenticScope
会自动创建,并在需要时通过回调程序化地提供。
在后续介绍不同 Agentic 模式时,会结合实例进一步说明 AgenticScope
的用法。
工作流模式
langchain4j-agentic
提供了一套抽象,用于以编程方式编排多个 Agent,从而构建 Agentic 工作流模式。这些模式可以组合成更复杂的工作流。
顺序工作流(Sequential workflow)
最简单的模式是 顺序调用:多个 Agent 依次执行,每个 Agent 的输出作为下一个 Agent 的输入。
适用于需要按照固定顺序完成一系列任务的场景。
例如,我们可以在 CreativeWriter
基础上,定义一个 AudienceEditor
,将生成的故事修改得更符合特定受众:
public interface AudienceEditor {
@UserMessage("""
You are a professional editor.
Analyze and rewrite the following story to better align
with the target audience of {{audience}}.
Return only the story and nothing else.
The story is "{{story}}".
""")
@Agent("Edits a story to better fit a given audience")
String editStory(@V("story") String story, @V("audience") String audience);
}
类似地,还可以定义一个 StyleEditor
,用于调整风格。
注意:方法参数使用了 @V
注解绑定变量名。
这些值并不是直接传递,而是从 AgenticScope
的共享变量中获取。
如果编译时启用了 -parameters
,就能在运行时保留参数名,此时 @V
可以省略,框架会自动推断。
最终,我们可以将 CreativeWriter
、AudienceEditor
、StyleEditor
组合成一个顺序工作流:
CreativeWriter creativeWriter = AgenticServices
.agentBuilder(CreativeWriter.class)
.chatModel(BASE_MODEL)
.outputName("story")
.build();
AudienceEditor audienceEditor = AgenticServices
.agentBuilder(AudienceEditor.class)
.chatModel(BASE_MODEL)
.outputName("story")
.build();
StyleEditor styleEditor = AgenticServices
.agentBuilder(StyleEditor.class)
.chatModel(BASE_MODEL)
.outputName("story")
.build();
UntypedAgent novelCreator = AgenticServices
.sequenceBuilder()
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputName("story")
.build();
Map<String, Object> input = Map.of(
"topic", "dragons and wizards",
"style", "fantasy",
"audience", "young adults"
);
String story = (String) novelCreator.invoke(input);
这里 novelCreator
实际上是一个 顺序工作流型的 Agentic 系统,由三个子 Agent 依次调用组成。
由于没有提供具体接口,所以返回的是 UntypedAgent
,通过输入 Map
调用。
public interface UntypedAgent {
@Agent
Object invoke(Map<String, Object> input);
}
输入的 Map
会被写入 AgenticScope
,子 Agent 可直接访问。最终输出则取自共享变量 "story"
。
如果想使用强类型接口,可以这样定义:
public interface NovelCreator {
@Agent
String createNovel(@V("topic") String topic, @V("audience") String audience, @V("style") String style);
}
并创建工作流:
NovelCreator novelCreator = AgenticServices
.sequenceBuilder(NovelCreator.class)
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputName("story")
.build();
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
循环工作流(Loop workflow)
常见的一种方式是利用 LLM 反复改进文本(例如故事),直到达到满意效果。
这可以通过 循环工作流模式 实现:一个 Agent 多次执行,直到满足退出条件。
例如,定义一个 StyleScorer
,对故事与目标风格的契合度打分:
public interface StyleScorer {
@UserMessage("""
You are a critical reviewer.
Give a review score between 0.0 and 1.0 for the following
story based on how well it aligns with the style '{{style}}'.
Return only the score and nothing else.
The story is: "{{story}}"
""")
@Agent("Scores a story based on how well it aligns with a given style")
double scoreStyle(@V("story") String story, @V("style") String style);
}
然后把它和 StyleEditor
结合成循环,直到分数 ≥ 0.8,或达到最大迭代次数(如 5 次):
StyleEditor styleEditor = AgenticServices
.agentBuilder(StyleEditor.class)
.chatModel(BASE_MODEL)
.outputName("story")
.build();
StyleScorer styleScorer = AgenticServices
.agentBuilder(StyleScorer.class)
.chatModel(BASE_MODEL)
.outputName("score")
.build();
UntypedAgent styleReviewLoop = AgenticServices
.loopBuilder()
.subAgents(styleScorer, styleEditor)
.maxIterations(5)
.exitCondition(agenticScope -> agenticScope.readState("score", 0.0) >= 0.8)
.build();
styleScorer
的输出存储在共享变量 "score"
中,退出条件基于该值。
接着我们可以将循环与 CreativeWriter
组合,形成一个 StyledWriter
:
public interface StyledWriter {
@Agent
String writeStoryWithStyle(@V("topic") String topic, @V("style") String style);
}
CreativeWriter creativeWriter = AgenticServices
.agentBuilder(CreativeWriter.class)
.chatModel(BASE_MODEL)
.outputName("story")
.build();
StyledWriter styledWriter = AgenticServices
.sequenceBuilder(StyledWriter.class)
.subAgents(creativeWriter, styleReviewLoop)
.outputName("story")
.build();
String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy");
并行工作流(Parallel workflow)
有时,多个 Agent 可以 独立处理相同输入,这时可用 并行工作流模式。
例如,让美食专家与电影专家分别推荐餐点和电影,然后组合成 浪漫之夜计划:
public interface FoodExpert {
@UserMessage("""
You are a great evening planner.
Propose a list of 3 meals matching the given mood.
The mood is {{mood}}.
For each meal, just give the name of the meal.
Provide a list with the 3 items and nothing else.
""")
@Agent
List<String> findMeal(@V("mood") String mood);
}
public interface MovieExpert {
@UserMessage("""
You are a great evening planner.
Propose a list of 3 movies matching the given mood.
The mood is {mood}.
Provide a list with the 3 items and nothing else.
""")
@Agent
List<String> findMovie(@V("mood") String mood);
}
由于两者独立,可以并行调用:
FoodExpert foodExpert = AgenticServices
.agentBuilder(FoodExpert.class)
.chatModel(BASE_MODEL)
.outputName("meals")
.build();
MovieExpert movieExpert = AgenticServices
.agentBuilder(MovieExpert.class)
.chatModel(BASE_MODEL)
.outputName("movies")
.build();
EveningPlannerAgent eveningPlannerAgent = AgenticServices
.parallelBuilder(EveningPlannerAgent.class)
.subAgents(foodExpert, movieExpert)
.executorService(Executors.newFixedThreadPool(2))
.outputName("plans")
.output(agenticScope -> {
List<String> movies = agenticScope.readState("movies", List.of());
List<String> meals = agenticScope.readState("meals", List.of());
List<EveningPlan> moviesAndMeals = new ArrayList<>();
for (int i = 0; i < movies.size(); i++) {
if (i >= meals.size()) break;
moviesAndMeals.add(new EveningPlan(movies.get(i), meals.get(i)));
}
return moviesAndMeals;
})
.build();
List<EveningPlan> plans = eveningPlannerAgent.plan("romantic");
这里的 output
方法用来组合两个子 Agent 的结果,生成 EveningPlan
列表。
此外,可以通过 executorService
自定义线程池,否则系统默认使用缓存线程池。
条件工作流(Conditional workflow)
有时需要 根据条件选择 Agent。例如,先对用户请求分类,再调用对应专家。
定义一个 CategoryRouter
:
public interface CategoryRouter {
@UserMessage("""
Analyze the following user request and categorize it as 'legal', 'medical' or 'technical'.
In case the request doesn't belong to any of those categories categorize it as 'unknown'.
Reply with only one of those words and nothing else.
The user request is: '{{request}}'.
""")
@Agent("Categorizes a user request")
RequestCategory classify(@V("request") String request);
}
枚举类:
public enum RequestCategory {
LEGAL, MEDICAL, TECHNICAL, UNKNOWN
}
定义专家,例如:
public interface MedicalExpert {
@UserMessage("""
You are a medical expert.
Analyze the following user request under a medical point of view and provide the best possible answer.
The user request is {{request}}.
""")
@Agent("A medical expert")
String medical(@V("request") String request);
}
然后组合成 ExpertRouterAgent
:
public interface ExpertRouterAgent {
@Agent
String ask(@V("request") String request);
}
构建条件工作流:
CategoryRouter routerAgent = AgenticServices
.agentBuilder(CategoryRouter.class)
.chatModel(BASE_MODEL)
.outputName("category")
.build();
MedicalExpert medicalExpert = AgenticServices
.agentBuilder(MedicalExpert.class)
.chatModel(BASE_MODEL)
.outputName("response")
.build();
LegalExpert legalExpert = AgenticServices
.agentBuilder(LegalExpert.class)
.chatModel(BASE_MODEL)
.outputName("response")
.build();
TechnicalExpert technicalExpert = AgenticServices
.agentBuilder(TechnicalExpert.class)
.chatModel(BASE_MODEL)
.outputName("response")
.build();
UntypedAgent expertsAgent = AgenticServices.conditionalBuilder()
.subAgents(agenticScope -> agenticScope.readState("category", RequestCategory.UNKNOWN) == RequestCategory.MEDICAL, medicalExpert)
.subAgents(agenticScope -> agenticScope.readState("category", RequestCategory.UNKNOWN) == RequestCategory.LEGAL, legalExpert)
.subAgents(agenticScope -> agenticScope.readState("category", RequestCategory.UNKNOWN) == RequestCategory.TECHNICAL, technicalExpert)
.build();
ExpertRouterAgent expertRouterAgent = AgenticServices
.sequenceBuilder(ExpertRouterAgent.class)
.subAgents(routerAgent, expertsAgent)
.outputName("response")
.build();
String response = expertRouterAgent.ask("I broke my leg what should I do");
错误处理(Error handling)
在复杂系统中,可能发生以下问题:
- Agent 无法生成结果
- 外部工具不可用
- 执行过程中抛出异常
因此,errorHandler
方法允许配置错误处理器,它接收 ErrorContext
:
record ErrorContext(String agentName, AgenticScope agenticScope, AgentInvocationException exception) { }
并返回 ErrorRecoveryResult
,可能是以下三种之一:
ErrorRecoveryResult.throwException()
—— 默认行为,直接抛出异常ErrorRecoveryResult.retry()
—— 重试该 Agent 调用(可在此之前做修复)ErrorRecoveryResult.result(Object result)
—— 忽略异常,返回替代结果
例如,如果输入缺少必要参数 "topic"
:
Map<String, Object> input = Map.of(
// "topic", "dragons and wizards",
"style", "fantasy",
"audience", "young adults"
);
会报错:
dev.langchain4j.agentic.agent.MissingArgumentException: Missing argument: topic
可以通过 errorHandler
修复:
UntypedAgent novelCreator = AgenticServices.sequenceBuilder()
.subAgents(creativeWriter, audienceEditor, styleEditor)
.errorHandler(errorContext -> {
if (errorContext.agentName().equals("generateStory") &&
errorContext.exception() instanceof MissingArgumentException mEx && mEx.argumentName().equals("topic")) {
errorContext.agenticScope().writeState("topic", "dragons and wizards");
errorRecoveryCalled.set(true);
return ErrorRecoveryResult.retry();
}
return ErrorRecoveryResult.throwException();
})
.outputName("story")
.build();
声明式 API(Declarative API)
以上所有模式,也可以用 声明式 API 来定义,更加简洁易读。
例如,并行工作流 EveningPlannerAgent
可以写成:
public interface EveningPlannerAgent {
@ParallelAgent(outputName = "plans", subAgents = {
@SubAgent(type = FoodExpert.class, outputName = "meals"),
@SubAgent(type = MovieExpert.class, outputName = "movies")
})
List<EveningPlan> plan(@V("mood") String mood);
@ExecutorService
static ExecutorService executor() {
return Executors.newFixedThreadPool(2);
}
@Output
static List<EveningPlan> createPlans(@V("movies") List<String> movies, @V("meals") List<String> meals) {
List<EveningPlan> moviesAndMeals = new ArrayList<>();
for (int i = 0; i < movies.size(); i++) {
if (i >= meals.size()) {
break;
}
moviesAndMeals.add(new EveningPlan(movies.get(i), meals.get(i)));
}
return moviesAndMeals;
}
}
在这种情况下,用 @Output
注解的静态方法用于定义如何将子代理(subagents)的输出组合成一个单一结果,这与在 AgenticScope
中通过 output
方法传递一个函数实现的方式完全相同。
一旦定义了这个接口,就可以使用 AgenticServices.createAgenticSystem()
方法创建一个 EveningPlannerAgent
实例,然后像之前一样使用它。
EveningPlannerAgent eveningPlannerAgent = AgenticServices
.createAgenticSystem(EveningPlannerAgent.class, BASE_MODEL);
List<EveningPlan> plans = eveningPlannerAgent.plan("romantic");
需要注意的是,这种方式的一个限制是:同一个 ChatModel
将会被隐式地用于创建所有子代理,因此在同一个工作流中无法混合使用不同聊天模型的代理。这是当前实现的一个限制,但在未来版本中是可以克服的。
为了给出另一个声明式 API 的示例,让我们通过它重新定义在条件工作流部分演示过的 ExpertsAgent
。
public interface ExpertsAgent {
@ConditionalAgent(outputName = "response", subAgents = {
@SubAgent(type = MedicalExpert.class, outputName = "response"),
@SubAgent(type = TechnicalExpert.class, outputName = "response"),
@SubAgent(type = LegalExpert.class, outputName = "response")
})
String askExpert(@V("request") String request);
@ActivationCondition(MedicalExpert.class)
static boolean activateMedical(@V("category") RequestCategory category) {
return category == RequestCategory.MEDICAL;
}
@ActivationCondition(TechnicalExpert.class)
static boolean activateTechnical(@V("category") RequestCategory category) {
return category == RequestCategory.TECHNICAL;
}
@ActivationCondition(LegalExpert.class)
static boolean activateLegal(@V("category") RequestCategory category) {
return category == RequestCategory.LEGAL;
}
}
在这个例子中,@ActivationCondition
注解的值表示当对应的方法返回 true
时,哪些代理类会被激活。
内存与上下文工程
到目前为止讨论的所有代理都是无状态的,意味着它们不会保留任何上下文或之前交互的记忆。然而,与其他 AI 服务一样,我们可以为代理提供 ChatMemory
,使其能够在多次调用之间保持上下文。
要为之前的 MedicalExpert
提供内存,只需要在它的签名中添加一个用 @MemoryId
注解的字段即可。
public interface MedicalExpertWithMemory {
@UserMessage("""
You are a medical expert.
Analyze the following user request under a medical point of view and provide the best possible answer.
The user request is {{request}}.
""")
@Agent("A medical expert")
String medical(@MemoryId String memoryId, @V("request") String request);
}
然后在构建代理时设置一个内存提供者:
MedicalExpertWithMemory medicalExpert = AgenticServices
.agentBuilder(MedicalExpertWithMemory.class)
.chatModel(BASE_MODEL)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.outputName("response")
.build();
通常来说,这样对于单个独立使用的代理就已经足够了,但对于参与到一个代理系统的代理来说可能会有局限性。假设技术专家和法律专家也都被提供了内存,并且 ExpertRouterAgent
也被重新定义为拥有内存:
public interface ExpertRouterAgentWithMemory {
@Agent
String ask(@MemoryId String memoryId, @V("request") String request);
}
当我们顺序调用该代理:
String response1 = expertRouterAgent.ask("1", "I broke my leg, what should I do?");
String legalResponse1 = expertRouterAgent.ask("1", "Should I sue my neighbor who caused this damage?");
并不会得到预期的结果,因为第二个问题会被路由到法律专家,而此时法律专家是第一次被调用,它的记忆中并没有之前的对话。
为了解决这个问题,需要为法律专家提供上下文信息,让它知道在调用之前发生了什么。这就是 AgenticScope
自动存储的信息可以发挥作用的地方。
具体来说,AgenticScope
会跟踪所有代理的调用顺序,并且可以将这些调用拼接成一个完整的对话上下文。这个上下文可以直接使用,或者根据需要进行总结,例如定义一个 ContextSummarizer
代理:
public interface ContextSummarizer {
@UserMessage("""
Create a very short summary, 2 sentences at most, of the
following conversation between an AI agent and a user.
The user conversation is: '{{it}}'.
""")
String summarize(String conversation);
}
通过使用这个代理,可以重新定义法律专家,并为其提供一个之前对话的上下文摘要,使其在回答新问题时能够考虑之前的交互。
LegalExpertWithMemory legalExpert = AgenticServices
.agentBuilder(LegalExpertWithMemory.class)
.chatModel(BASE_MODEL)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.context(agenticScope -> contextSummarizer.summarize(agenticScope.contextAsConversation()))
.outputName("response")
.build();
更一般地说,提供给代理的上下文可以是 AgenticScope
状态的任意函数。在这种设置下,当法律专家被问到是否应该起诉邻居时,它会考虑之前医疗专家的对话,并提供更有依据的回答。
在内部,代理框架会自动重写发送给法律专家的用户消息,把总结过的上下文包含进去。例如在这个案例中,实际传给法律专家的用户消息可能是这样的:
"Considering this context \"The user asked about what to do after breaking their leg, and the AI provided medical advice on immediate actions like immobilizing the leg, applying ice, and seeking medical attention.\"
You are a legal expert.
Analyze the following user request under a legal point of view and provide the best possible answer.
The user request is Should I sue my neighbor who caused this damage?."
这种上下文总结在很多情况下都非常有用,因此可以用更简便的方式在代理上定义它,使用 summarizedContext
方法:
LegalExpertWithMemory legalExpert = AgenticServices
.agentBuilder(LegalExpertWithMemory.class)
.chatModel(BASE_MODEL)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.summarizedContext("medical", "technical")
.outputName("response")
.build();
这样做会在内部自动使用之前定义的 ContextSummarizer
代理,并用与该代理相同的聊天模型执行它。还可以向该方法传递可变参数,指定要总结上下文的代理名称,这样就只会总结特定代理的上下文,而不是整个系统的。
AgenticScope 注册与持久化
AgenticScope
是一个在代理系统执行期间创建和使用的临时数据结构。每个用户在每个代理系统中都会有一个 AgenticScope
。对于无状态执行(没有使用内存),AgenticScope
会在执行结束后自动丢弃,其状态不会持久化。
相反,当代理系统使用了内存时,AgenticScope
会被保存到一个内部注册表中。此时 AgenticScope
会一直保留在注册表里,以便用户能够以有状态、对话式的方式与代理系统交互。因此,当某个特定 ID 的 AgenticScope
不再需要时,必须显式地从注册表中驱逐它。为此,代理系统的根代理需要实现 AgenticScopeAccess
接口,从而可以调用其 evictAgenticScope
方法,传入需要移除的 AgenticScope
ID:
agent.evictAgenticScope(memoryId);
AgenticScope
及其注册表纯粹是内存数据结构。这通常足以应付简单的代理系统,但有时可能需要将 AgenticScope
状态持久化到更耐久的存储,例如数据库或文件系统。为此,langchain4j-agentic
模块提供了一个 SPI,可插入自定义持久化层,该层需实现 AgenticScopeStore
接口。可以通过编程方式设置:
AgenticScopePersister.setStore(new MyAgenticScopeStore());
或者使用标准 Java Service Provider 机制,在 META-INF/services/dev.langchain4j.agentic.scope.AgenticScopeStore
文件中写入实现该接口的类的全限定名。
纯代理式 AI
到目前为止,所有代理都是通过确定性工作流来组合和连接的。然而,有些情况下代理系统需要更灵活和自适应,允许代理根据上下文和之前的结果自主决定下一步如何执行。这通常被称为 纯代理式 AI。
为此,langchain4j-agentic
模块提供了一个开箱即用的监督代理(supervisor agent),它可以被赋予一组子代理,并能够自主生成一个计划,决定下一个要调用的代理,或者判断任务是否已完成。
为了演示其工作方式,我们定义几个代理:可以为银行账户存款或取款,或者将一定金额兑换成另一种货币。
public interface WithdrawAgent {
@SystemMessage("""
You are a banker that can only withdraw US dollars (USD) from a user account,
""")
@UserMessage("""
Withdraw {{amount}} USD from {{user}}'s account and return the new balance.
""")
@Agent("A banker that withdraw USD from an account")
String withdraw(@V("user") String user, @V("amount") Double amount);
}
public interface CreditAgent {
@SystemMessage("""
You are a banker that can only credit US dollars (USD) to a user account,
""")
@UserMessage("""
Credit {{amount}} USD to {{user}}'s account and return the new balance.
""")
@Agent("A banker that credit USD to an account")
String credit(@V("user") String user, @V("amount") Double amount);
}
public interface ExchangeAgent {
@UserMessage("""
You are an operator exchanging money in different currencies.
Use the tool to exchange {{amount}} {{originalCurrency}} into {{targetCurrency}}
returning only the final amount provided by the tool as it is and nothing else.
""")
@Agent("A money exchanger that converts a given amount of money from the original to the target currency")
Double exchange(@V("originalCurrency") String originalCurrency, @V("amount") Double amount, @V("targetCurrency") String targetCurrency);
}
所有这些代理都使用外部工具来执行任务,具体来说是一个 BankTool
,它可以从用户账户存取款:
public class BankTool {
private final Map<String, Double> accounts = new HashMap<>();
void createAccount(String user, Double initialBalance) {
if (accounts.containsKey(user)) {
throw new RuntimeException("Account for user " + user + " already exists");
}
accounts.put(user, initialBalance);
}
double getBalance(String user) {
Double balance = accounts.get(user);
if (balance == null) {
throw new RuntimeException("No balance found for user " + user);
}
return balance;
}
@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 with 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;
}
}
还有一个 ExchangeTool
,它可以用于不同货币之间的兑换,可能会调用 REST 服务来获取最新汇率:
public class ExchangeTool {
@Tool("Exchange the given amount of money from the original to the target currency")
Double exchange(@P("originalCurrency") String originalCurrency, @P("amount") Double amount, @P("targetCurrency") String targetCurrency) {
// Invoke a REST service to get the exchange rate
}
}
接下来,就可以像往常一样使用 AgenticServices.agentBuilder()
方法创建这些代理的实例,配置它们使用这些工具,然后将它们作为监督代理的子代理来使用。
BankTool bankTool = new BankTool();
bankTool.createAccount("Mario", 1000.0);
bankTool.createAccount("Georgios", 1000.0);
WithdrawAgent withdrawAgent = AgenticServices
.agentBuilder(WithdrawAgent.class)
.chatModel(BASE_MODEL)
.tools(bankTool)
.build();
CreditAgent creditAgent = AgenticServices
.agentBuilder(CreditAgent.class)
.chatModel(BASE_MODEL)
.tools(bankTool)
.build();
ExchangeAgent exchange = AgenticServices
.agentBuilder(ExchangeAgent.class)
.chatModel(BASE_MODEL)
.tools(new ExchangeTool())
.build();
SupervisorAgent bankSupervisor = AgenticServices
.supervisorBuilder()
.chatModel(PLANNER_MODEL)
.subAgents(withdrawAgent, creditAgent, exchangeAgent)
.responseStrategy(SupervisorResponseStrategy.SUMMARY)
.build();
注意,子代理也可以是复杂的代理,实现一个工作流,但在监督代理看来它们仍然是单一代理。
最终得到的 SupervisorAgent
通常接收一个用户请求作为输入并生成响应,所以它的签名如下:
public interface SupervisorAgent {
@Agent
String invoke(@V("request") String request);
}
现在假设我们用以下请求调用这个代理:
bankSupervisor.invoke("Transfer 100 EUR from Mario's account to Georgios' one")
在内部,监督代理会分析请求,并生成一个完成任务的计划,该计划由一系列 AgentInvocation
组成:
public record AgentInvocation(String agentName, Map<String, String> arguments) {}
例如,对于上面的请求,监督代理可能会生成如下的调用序列:
AgentInvocation{agentName='exchange', arguments={originalCurrency=EUR, amount=100, targetCurrency=USD}}
AgentInvocation{agentName='withdraw', arguments={user=Mario, amount=115.0}}
AgentInvocation{agentName='credit', arguments={user=Georgios, amount=115.0}}
AgentInvocation{agentName='done', arguments={response=The transfer of 100 EUR from Mario's account to Georgios' account has been completed. Mario's balance is 885.0 USD, and Georgios' balance is 1115.0 USD. The conversion rate was 1.15 EUR to USD.}}
最后一个调用是一个特殊的调用,表示监督代理认为任务已完成,并返回一个操作总结作为响应。
在很多情况下,就像这个例子一样,这个总结就是最终需要返回给用户的响应,但并非总是如此。假设你使用 SupervisorAgent
而不是简单的顺序工作流来创作一个故事,并按照给定风格和受众进行编辑(类似最开始的例子)。在这种情况下,用户只关心最终故事,而不是生成故事过程中的中间步骤总结。
默认情况下,监督代理返回最后一个调用的响应,而不是总结。这种情况实际上更为常见。但在银行转账的例子中,更合适的是返回所有执行过的交易总结,因此 SupervisorAgent
通过 responseStrategy
方法被配置成了这样。
下一节将讨论监督代理的设计和定制化。
监督代理的设计与定制化
更普遍地,有些场景下很难提前知道返回总结还是返回最后一次响应更合适。为此提供了一个额外的代理,它会同时接收这两种候选响应以及原始用户请求,并对它们进行评分,从而决定哪一个更符合用户请求。
SupervisorResponseStrategy
枚举可以启用该评分代理,或者跳过评分过程而始终返回其中一种响应:
public enum SupervisorResponseStrategy {
SCORED, SUMMARY, LAST
}
默认行为是 LAST
,但你可以通过 responseStrategy
方法来配置其它策略实现:
AgenticServices.supervisorBuilder()
.responseStrategy(SupervisorResponseStrategy.SCORED)
.build();
例如在银行转账的例子中,若使用 SCORED
策略,可能得到如下评分:
ResponseScore{finalResponse=0.3, summary=1.0}
于是监督代理会返回总结作为最终响应。
当前所描述的监督代理架构如下图所示:

监督代理决定下一步要执行的动作时所使用的信息,是它的另一大关键点。默认情况下,监督代理仅使用本地对话记忆。但在某些场景下,提供更全面的上下文会更有用,比如通过总结子代理的对话(类似上下文工程章节中讨论的方法),甚至可以结合两者。
这三种可能性由以下枚举表示:
public enum SupervisorContextStrategy {
CHAT_MEMORY, SUMMARIZATION, CHAT_MEMORY_AND_SUMMARIZATION
}
在构建监督代理时,可以通过 contextGenerationStrategy
方法来设置:
AgenticServices.supervisorBuilder()
.contextGenerationStrategy(SupervisorContextStrategy.SUMMARIZATION)
.build();
未来可能还会提供更多监督代理的定制化选项。
为监督代理提供上下文
在很多实际场景中,监督代理会从额外上下文中获益:例如约束、策略或偏好,这些信息会指导计划生成(比如“优先使用内部工具”、“不要调用外部服务”、“货币必须是 USD”)。
这些上下文存储在 AgenticScope
中的 supervisorContext
变量中。你可以通过两种方式提供它:
构建时配置:
SupervisorAgent bankSupervisor = AgenticServices
.supervisorBuilder()
.chatModel(PLANNER_MODEL)
.supervisorContext("Policies: prefer internal tools; currency USD; no external APIs")
.subAgents(withdrawAgent, creditAgent, exchangeAgent)
.responseStrategy(SupervisorResponseStrategy.SUMMARY)
.build();
调用时(类型化的监督代理): 在方法参数上添加 @V("supervisorContext")
:
public interface SupervisorAgent {
@Agent
String invoke(@V("request") String request, @V("supervisorContext") String supervisorContext);
}
// 示例调用(覆盖构建时的值)
bankSupervisor.invoke(
"Transfer 100 EUR from Mario's account to Georgios' one",
"Policies: convert to USD first; use bank tools only; no external APIs"
);
调用时(非类型化的监督代理): 在输入 Map
中设置 supervisorContext
:
Map<String, Object> input = Map.of(
"request", "Transfer 100 EUR from Mario's account to Georgios' one",
"supervisorContext", "Policies: convert to USD first; use bank tools only; no external APIs"
);
String result = (String) bankSupervisor.invoke(input);
如果两种方式都提供了,调用时的值会覆盖构建时的值。
非 AI 代理
目前讨论的代理大多是基于 LLM 的 AI 代理,用于自然语言理解和生成。但 langchain4j-agentic
模块也支持非 AI 代理,它们可用于无需自然语言处理的任务,例如调用 REST API 或执行命令。
这些非 AI 代理更接近工具,但在这个上下文中建模为代理更方便,因为它们能与 AI 代理统一使用,并能混合组成更强大的代理系统。
例如,ExchangeAgent
在前述监督代理示例中可能被错误建模为 AI 代理。更合适的做法是将其定义为一个非 AI 代理,直接调用 REST API 进行货币兑换:
public class ExchangeOperator {
@Agent(value = "A money exchanger that converts a given amount of money from the original to the target currency",
outputName = "exchange")
public Double exchange(@V("originalCurrency") String originalCurrency, @V("amount") Double amount, @V("targetCurrency") String targetCurrency) {
// invoke the REST API to perform the currency exchange
}
}
这样它就可以和其它子代理一起被监督代理使用:
WithdrawAgent withdrawAgent = AgenticServices
.agentBuilder(WithdrawAgent.class)
.chatModel(BASE_MODEL)
.tools(bankTool)
.build();
CreditAgent creditAgent = AgenticServices
.agentBuilder(CreditAgent.class)
.chatModel(BASE_MODEL)
.tools(bankTool)
.build();
SupervisorAgent bankSupervisor = AgenticServices
.supervisorBuilder()
.chatModel(PLANNER_MODEL)
.subAgents(withdrawAgent, creditAgent, new ExchangeOperator())
.build();
本质上,在 langchain4j-agentic
中,一个代理就是任意只有一个方法且带有 @Agent
注解的 Java 类。
人类参与环节(Human-in-the-loop)
在构建代理系统时,一个常见需求是让人类参与进来:允许系统在缺少信息时向用户提问,或在执行某些操作前请求用户确认。
这种人类参与环节也可以被视为一种特殊的非 AI 代理,从而实现。
public record HumanInTheLoop(Consumer<String> requestWriter, Supplier<String> responseReader) {
@Agent("An agent that asks the user for missing information")
public String askUser(String request) {
requestWriter.accept(request);
return responseReader.get();
}
}
这是一个非常简单但通用的实现,基于两个函数:
- 一个
Consumer
,用于将 AI 请求发送给用户 - 一个
Supplier
,用于获取用户输入(可能是阻塞的等待)。
langchain4j-agentic
模块内置了 HumanInTheLoop
代理,允许定义这两个函数、代理描述、AgenticScope
的输入变量(生成要询问的问题)和输出变量(存储用户的回答)。
例如,定义一个 AstrologyAgent
:
public interface AstrologyAgent {
@SystemMessage("""
You are an astrologist that generates horoscopes based on the user's name and zodiac sign.
""")
@UserMessage("""
Generate the horoscope for {{name}} who is a {{sign}}.
""")
@Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.")
String horoscope(@V("name") String name, @V("sign") String sign);
}
然后创建一个 SupervisorAgent
,使用这个 AI 代理和一个 HumanInTheLoop
代理,请求用户输入星座信息:
AstrologyAgent astrologyAgent = AgenticServices
.agentBuilder(AstrologyAgent.class)
.chatModel(BASE_MODEL)
.build();
HumanInTheLoop humanInTheLoop = AgenticServices
.humanInTheLoopBuilder()
.description("An agent that asks the zodiac sign of the user")
.outputName("sign")
.requestWriter(request -> {
System.out.println(request);
System.out.print("> ");
})
.responseReader(() -> System.console().readLine())
.build();
SupervisorAgent horoscopeAgent = AgenticServices
.supervisorBuilder()
.chatModel(PLANNER_MODEL)
.subAgents(astrologyAgent, humanInTheLoop)
.build();
调用时:
horoscopeAgent.invoke("My name is Mario. What is my horoscope?")
监督代理会发现缺少星座信息,于是调用 HumanInTheLoop
代理,输出:
What is your zodiac sign?
>
用户输入后,该信息会被传递给 AstrologyAgent
来生成完整的星座运势。
A2A 集成
额外的 langchain4j-agentic-a2a
模块提供了与 A2A 协议的无缝集成,使得代理系统可以使用远程 A2A 服务器代理,并与本地代理混合。
例如,如果 CreativeWriter
代理是在远程 A2A 服务器上定义的,可以通过以下方式创建一个 A2ACreativeWriter
代理来调用远程代理:
UntypedAgent creativeWriter = AgenticServices
.a2aBuilder(A2A_SERVER_URL)
.inputNames("topic")
.outputName("story")
.build();
代理的能力描述会自动从 A2A 服务器提供的 agent card 中获取。不过 card 不提供输入参数名称,因此需要通过 inputNames
方法显式指定。
你也可以定义一个本地接口来映射 A2A 代理:
public interface A2ACreativeWriter {
@Agent
String generateStory(@V("topic") String topic);
}
这样可以更类型安全,输入名也会自动从方法参数中推导:
A2ACreativeWriter creativeWriter = AgenticServices
.a2aBuilder(A2A_SERVER_URL, A2ACreativeWriter.class)
.outputName("story")
.build();
然后,它就能像本地代理一样使用,并可与它们混合来定义工作流或作为监督代理的子代理。