LangChain4j从入门到精通-8-智能体
本文深度解析了LangChain4j框架中的智能体(Agents)系统,揭秘如何通过@Agent注解将AI能力模块化封装为可复用的协作单元。核心创新在于引入AgenticScope共享数据空间,使多个智能体能像“汽车研发团队”般协同工作——创意生成、受众编辑、风格优化等智能体各司其职,通过顺序、循环、并行、条件四大工作流模式灵活编排。文章通过故事创作、客服路由等场景演示了声明式API如何简化复杂任
sidebar_position: 7
LangChain4j从入门到精通-8-智能体
本文深度解析了LangChain4j框架中的智能体(Agents)系统,揭秘如何通过@Agent注解将AI能力模块化封装为可复用的协作单元。核心创新在于引入AgenticScope共享数据空间,使多个智能体能像“汽车研发团队”般协同工作——创意生成、受众编辑、风格优化等智能体各司其职,通过顺序、循环、并行、条件四大工作流模式灵活编排。文章通过故事创作、客服路由等场景演示了声明式API如何简化复杂任务编排,并详解了流式响应、错误恢复、执行监控等生产级特性。尤为亮眼的是监督智能体(Supervisor Agent)架构,它可自主规划任务链路,动态调用工具子系统,实现“转账100欧元”这类需多步骤协调的复杂指令。这套体系让Java开发者能用熟悉的Spring Bean式编程模型,构建具备记忆、推理、协作能力的下一代AI应用。
#Java人工智能 #LangChain4j #智能体系统 #AI应用开发 #工作流引擎
:::注意
本节介绍如何使用langchain4j-agentic模块构建智能代理AI应用。请注意,该模块目前仍处于实验阶段,未来版本可能会有所改动。
:::
系统智能体
尽管目前对AI智能体尚无普遍认可的定义,但一些新兴模式展示了如何协调和整合多个AI服务的能力,以创建能够完成更复杂任务的AI增强型应用。这些模式通常被称为"智能体系统"或"智能体AI"。它们通常涉及使用大型语言模型(LLM)来编排任务执行、管理工具使用,并在交互过程中保持上下文连贯。
根据一个 Anthropic研究人员最近发表的一篇文章, 这些代理系统架构可以分为两大类:工作流和纯代理。

本教程中讨论的langchain4j-agentic模块提供了一系列抽象和实用工具,旨在帮助您构建工作流和纯代理式AI应用。它使您能够定义工作流程、管理工具使用,并在与不同大型语言模型交互时保持上下文一致性。
LangChain4j智能体使用
LangChain4j中的代理(agent)通过大语言模型(LLM)执行特定任务或一系列任务。定义代理时可采用与常规AI服务类似的方式——只需为包含单一方法的接口添加@Agent注解即可。
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);
}
最佳实践是在该注解中同时提供关于代理目的的简短描述,特别是在纯代理模式中使用时尤为重要——其他代理需要了解该代理的能力,才能明智地决定如何使用及何时调用。开发者也可在构建代理时通过代理构建器的description方法以编程方式提供此描述。
代理在代理系统中还必须有一个唯一标识的名称。这个名称可以通过@Agent注解指定,也可以通过代理构建器的name方法以编程方式指定。如果未指定名称,则使用带有@Agent注解的方法名称作为代理名称。
现在可以通过使用AgenticServices.agentBuilder()方法来构建此代理的实例,指定要使用的接口和聊天模型。
CreativeWriter creativeWriter = AgenticServices
.agentBuilder(CreativeWriter.class)
.chatModel(myChatModel)
.outputKey("story")
.build();
本质上,代理是纯粹的AI服务,提供相同的功能,但能够与其他代理结合,以创建更复杂的工作流程和代理系统。
AI服务的另一个主要区别是存在outputKey参数,该参数用于指定共享变量的名称,代理调用的结果将存储在该变量中,以便同一代理系统中的其他代理可以使用。另外,输出名称也可以直接在@Agent注解中声明,而不是像本例中那样以编程方式声明,这样就可以在代码中省略并在此处添加。
@Agent(outputKey = "story", description = "Generates a story based on the given topic")
AgenticServices类提供了一组静态工厂方法,用于创建和定义 langchain4j-agentic框架提供的各类代理。
介绍AgenticScope
langchain4j-agentic模块引入了AgenticScope的概念,这是一个在参与代理系统的各代理之间共享的数据集合。AgenticScope用于存储共享变量,代理可以通过写入这些变量来传递其产生的结果,其他代理则可以读取这些变量以获取执行任务所需的信息。这使得代理能够高效协作,根据需要共享信息和结果。
AgenticScope还会自动记录其他相关信息,例如所有代理的调用序列及其响应。当代理系统的主代理被调用时,它会自动创建,并在必要时通过回调以编程方式提供。在讨论langchain4j-agentic实现的代理模式时,将通过实际示例阐明AgenticScope的不同可能用途。
工作流模式
langchain4j-agentic模块提供了一系列抽象概念,用于以编程方式协调多个代理并创建代理工作流模式。这些模式可以组合起来创建更复杂的工作流。
顺序工作流
顺序工作流是最简单的模式,多个代理按顺序依次调用,每个代理的输出作为下一个代理的输入。当需要按特定顺序执行一系列任务时,这种模式非常有用。
例如,可以补充之前定义的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,它执行相同的任务,但针对特定样式。
public interface StyleEditor {
@UserMessage("""
You are a professional editor.
Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style.
Return only the story and nothing else.
The story is "{{story}}".
""")
@Agent("Edits a story to better fit a given style")
String editStory(@V("story") String story, @V("style") String style);
}
请注意,此代理的输入参数已用变量名进行标注。实际上,传递给代理的参数值并非直接提供,而是从AgenticScope共享变量中按同名获取。这种机制使得代理能够访问工作流中先前代理的输出结果。若代理类在编译时启用了-parameters选项(从而在运行时保留方法参数名),则可省略@V注解,系统将自动根据参数名推断对应的变量名。
此时可以创建一个顺序工作流,将这三个智能体结合起来,其中CreativeWriter的输出将作为AudienceEditor和StyleEditor的输入,最终输出经过编辑的故事
CreativeWriter creativeWriter = AgenticServices
.agentBuilder(CreativeWriter.class)
.chatModel(BASE_MODEL)
.outputKey("story")
.build();
AudienceEditor audienceEditor = AgenticServices
.agentBuilder(AudienceEditor.class)
.chatModel(BASE_MODEL)
.outputKey("story")
.build();
StyleEditor styleEditor = AgenticServices
.agentBuilder(StyleEditor.class)
.chatModel(BASE_MODEL)
.outputKey("story")
.build();
UntypedAgent novelCreator = AgenticServices
.sequenceBuilder()
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputKey("story")
.build();
Map<String, Object> input = Map.of(
"topic", "dragons and wizards",
"style", "fantasy",
"audience", "young adults"
);
String story = (String) novelCreator.invoke(input);
这里的novelCreator代理实际上是一个代理系统,它实现了一个顺序工作流程,依次调用三个子代理。由于该代理的定义没有提供类型化接口,序列代理构建器返回了一个UntypedAgent实例,这是一个可以通过输入映射调用的通用代理。
public interface UntypedAgent {
@Agent
Object invoke(Map<String, Object> input);
}
该输入映射中的值会被复制到AgenticScope共享变量中,以便子代理可以访问它们。novelCreator代理的输出也是从名为"story"的AgenticScope共享变量中获取的,该变量在小说创作和编辑工作流执行过程中已被所有其他代理重写过。
可选地,工作流代理还可以提供类型化接口,以便可以使用强类型的输入和输出进行调用。在这种情况下,UntypedAgent接口可以被更具体的接口替代,例如:
public interface NovelCreator {
@Agent
String createNovel(@V("topic") String topic, @V("audience") String audience, @V("style") String style);
}
这样就能创建并使用 novelCreator代理,如下所示:
NovelCreator novelCreator = AgenticServices
.sequenceBuilder(NovelCreator.class)
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputKey("story")
.build();
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
循环工作流
为了更好地利用大型语言模型(LLM)的能力,一种常见的方法是让它们通过多次调用能够编辑或改进文本的智能体,来迭代优化一段文字(例如故事)。这可以通过循环工作流模式实现,即反复调用智能体,直到满足特定条件为止。
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),或达到最大迭代次数。
StyleEditor styleEditor = AgenticServices
.agentBuilder(StyleEditor.class)
.chatModel(BASE_MODEL)
.outputKey("story")
.build();
StyleScorer styleScorer = AgenticServices
.agentBuilder(StyleScorer.class)
.chatModel(BASE_MODEL)
.outputKey("score")
.build();
UntypedAgent styleReviewLoop = AgenticServices
.loopBuilder()
.subAgents(styleScorer, styleEditor)
.maxIterations(5)
.exitCondition( agenticScope -> agenticScope.readState("score", 0.0) >= 0.8)
.build();
在这里,styleScorer代理将其输出写入名为“score”的AgenticScope共享变量中,并且在循环的退出条件中访问和评估了相同的变量。
exitCondition方法接收一个Predicate<AgenticScope>参数,默认情况下会在每次代理调用后评估该条件,一旦条件满足就立即退出循环,以尽可能减少代理调用的次数。
不过,也可以通过配置循环构建器的testExitAtLoopEnd(true)方法,仅在循环结束时检查退出条件,从而强制所有代理在测试该条件之前都被调用。
此外,exitCondition方法还可以接收一个BiPredicate<AgenticScope, Integer>参数,其第二个参数是当前循环迭代的计数器。例如,以下循环定义:
UntypedAgent styleReviewLoop = AgenticServices
.loopBuilder()
.subAgents(styleScorer, styleEditor)
.maxIterations(5)
.testExitAtLoopEnd(true)
.exitCondition( (agenticScope, loopCounter) -> {
double score = agenticScope.readState("score", 0.0);
return loopCounter <= 3 ? score >= 0.8 : score >= 0.6;
})
.build();
如果在前3次迭代中得分至少为0.8,将退出循环;否则会降低质量预期,以至少0.6的得分终止循环,即使在满足退出条件后,仍会强制最后一次调用styleEditor代理。
配置好这个styleReviewLoop后,可以将其视为一个独立智能体,与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)
.outputKey("story")
.build();
StyledWriter styledWriter = AgenticServices
.sequenceBuilder(StyledWriter.class)
.subAgents(creativeWriter, styleReviewLoop)
.outputKey("story")
.build();
String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy");
并行工作流
有时并行调用多个代理很有用,尤其是当它们可以独立处理相同输入时。这可以通过使用并行工作流模式来实现,即同时调用多个代理,并将它们的输出合并为单一结果。
例如,让我们借助电影和美食专家的建议,为营造特定氛围的美好夜晚设计几套方案,将电影与契合该氛围的餐食巧妙搭配。
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);
}
由于两位专家的工作是独立的,可以使用AgenticServices.parallelBuilder()方法并行调用他们,如下所示:
FoodExpert foodExpert = AgenticServices
.agentBuilder(FoodExpert.class)
.chatModel(BASE_MODEL)
.outputKey("meals")
.build();
MovieExpert movieExpert = AgenticServices
.agentBuilder(MovieExpert.class)
.chatModel(BASE_MODEL)
.outputKey("movies")
.build();
EveningPlannerAgent eveningPlannerAgent = AgenticServices
.parallelBuilder(EveningPlannerAgent.class)
.subAgents(foodExpert, movieExpert)
.executor(Executors.newFixedThreadPool(2))
.outputKey("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");
在EveningPlannerAgent中定义的AgenticScope的output函数允许组合两个子代理的输出,创建一个包含与给定情绪相匹配的电影和餐食的EveningPlan对象列表。output方法虽然特别适用于并行工作流,但实际上可以在任何工作流模式中使用,用于定义如何将子代理的输出组合成单一结果,而不是简单地从AgenticScope返回一个值。executor方法还允许可选地提供一个Executor,用于并行执行子代理,否则默认情况下将使用内部缓存的线程池。
条件工作流
另一个常见需求是仅在满足特定条件时才调用某个代理。例如,在处理用户请求之前对其进行分类可能很有用,这样可以根据请求的类别由不同的代理进行处理。这可以通过使用以下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);
}
返回一个 RequestCategory枚举值。
public enum RequestCategory {
LEGAL, MEDICAL, TECHNICAL, UNKNOWN
}
这样,定义了一个MedicalExpert代理后:
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);
}
以及类似的“法律专家”和“技术专家”代理,可以创建一个“专家路由代理”
public interface ExpertRouterAgent {
@Agent
String ask(@V("request") String request);
}
根据用户请求的类别调用相应代理的条件工作流实现。
CategoryRouter routerAgent = AgenticServices
.agentBuilder(CategoryRouter.class)
.chatModel(BASE_MODEL)
.outputKey("category")
.build();
MedicalExpert medicalExpert = AgenticServices
.agentBuilder(MedicalExpert.class)
.chatModel(BASE_MODEL)
.outputKey("response")
.build();
LegalExpert legalExpert = AgenticServices
.agentBuilder(LegalExpert.class)
.chatModel(BASE_MODEL)
.outputKey("response")
.build();
TechnicalExpert technicalExpert = AgenticServices
.agentBuilder(TechnicalExpert.class)
.chatModel(BASE_MODEL)
.outputKey("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)
.outputKey("response")
.build();
String response = expertRouterAgent.ask("I broke my leg what should I do");
异步代理
默认情况下,所有智能体的调用都在调用智能体系统根智能体的同一线程中执行,因此它们是同步的,这意味着智能体系统会等待每个智能体完成后再继续执行下一个。然而,在许多情况下,这并非必要,以异步方式调用智能体可能更为实用,这样智能体系统的执行可以无需等待该智能体完成而继续推进。
因此,可以通过代理构建器的async方法将代理标记为异步。这样操作时,该代理的调用会在单独的线程中执行,而代理系统的运行不会等待该代理完成。异步代理的结果一旦完成就会在AgenticScope中可用,并且只有当该结果作为后续不同代理调用的输入时,AgenticScope才会被阻塞以等待该结果。
例如,由于它们彼此独立,将并行工作流部分讨论的FoodExpert和MovieExpert代理标记为异步,即使在顺序工作流中使用时,也会使它们同时执行。
FoodExpert foodExpert = AgenticServices
.agentBuilder(FoodExpert.class)
.chatModel(BASE_MODEL)
.async(true)
.outputKey("meals")
.build();
MovieExpert movieExpert = AgenticServices
.agentBuilder(MovieExpert.class)
.chatModel(BASE_MODEL)
.async(true)
.outputKey("movies")
.build();
EveningPlannerAgent eveningPlannerAgent = AgenticServices
.sequenceBuilder(EveningPlannerAgent.class)
.subAgents(foodExpert, movieExpert)
.executor(Executors.newFixedThreadPool(2))
.outputKey("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");
Streaming 智能体
为了支持流式传输,也可以创建一个返回 TokenStream的代理
public interface StreamingCreativeWriter {
@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")
TokenStream generateStory(@V("topic") String topic);
}
然后将其配置为使用 StreamingChatModel,这样可以在生成结果的同时进行消费,而无需等待代理调用的完成。
StreamingCreativeWriter creativeWriter = AgenticServices.agentBuilder(StreamingCreativeWriter.class)
.streamingChatModel(streamingBaseModel())
.outputKey("story")
.build();
TokenStream tokenStream = creativeWriter.generateStory("dragons and wizards");
在代理系统中使用时,流式代理只有在作为最后一个被调用的代理时,才能将其流式响应传播至整个系统。在其他所有情况下,它的行为类似于异步代理,因此后续代理需要等待其流式响应完成才能获取并使用其结果。
例如,以下StreamingReviewedWriter代理
public interface StreamingReviewedWriter {
@Agent
TokenStream writeStory(@V("topic") String topic, @V("audience") String audience, @V("style") String style);
}
通过3个流式代理序列实现
StreamingCreativeWriter creativeWriter = AgenticServices.agentBuilder(StreamingCreativeWriter.class)
.streamingChatModel(streamingBaseModel())
.outputKey("story")
.build();
StreamingAudienceEditor audienceEditor = AgenticServices.agentBuilder(StreamingAudienceEditor.class)
.streamingChatModel(streamingBaseModel())
.outputKey("story")
.build();
StreamingStyleEditor styleEditor = AgenticServices.agentBuilder(StreamingStyleEditor.class)
.streamingChatModel(streamingBaseModel())
.outputKey("story")
.build();
StreamingReviewedWriter novelCreator = AgenticServices.sequenceBuilder(StreamingReviewedWriter.class)
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputKey("story")
.build();
当调用这个novelCreator代理时
TokenStream tokenStream = novelCreator.writeStory("dragons and wizards", "young adults", "fantasy");
前两个代理的流式响应在内部完全消耗后,才能开始调用后续代理,并且只有最后一个StyleEditor代理的流式响应会作为整个novelCreator代理的流式响应进行传播。
错误处理
在一个复杂的代理系统中,可能会出现许多问题,例如代理未能产生结果、外部工具不可用,或在代理执行过程中发生意外错误。
因此,errorHandler方法允许为代理系统提供一个错误处理器,该处理器是一个函数,用于转换定义为 ErrorContext的上下文
record ErrorContext(String agentName, AgenticScope agenticScope, AgentInvocationException exception) { }
转换为一个ErrorRecoveryResult,它可以是以下三种情况之一
ErrorRecoveryResult.throwException()这是默认行为,只是将导致问题的 Exception传播到根调用者ErrorRecoveryResult.retry()该操作会重试代理调用,可能是在采取了一些纠正措施之后。ErrorRecoveryResult.result(Object result)忽略问题并将提供的结果作为失败代理的结果返回。
例如,如果在顺序工作流的第一个示例中省略了一个必要的参数
UntypedAgent novelCreator = AgenticServices
.sequenceBuilder()
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputKey("story")
.build();
Map<String, Object> input = Map.of(
// missing "topic" entry to trigger an error
// "topic", "dragons and wizards",
"style", "fantasy",
"audience", "young adults"
);
执行将失败并抛出类似异常
dev.langchain4j.agentic.agent.MissingArgumentException: Missing argument: topic
为了解决这个问题,在这种情况下,可以通过配置一个适当的errorHandler来处理此错误并从中恢复,该处理程序将为agenticScope提供缺失的参数,具体如下。
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();
})
.outputKey("story")
.build();
可观测性
跟踪和记录代理的调用对于调试和理解这些代理参与的整个代理系统的聚合行为至关重要。因此,langchain4j-agentic模块允许通过代理构建器的listener方法注册一个AgentListener,该监听器会收到所有代理调用及其结果的通知,其定义如下:
public interface AgentListener {
default void beforeAgentInvocation(AgentRequest agentRequest) { }
default void afterAgentInvocation(AgentResponse agentResponse) { }
default void onAgentInvocationError(AgentInvocationError agentInvocationError) { }
default void afterAgenticScopeCreated(AgenticScope agenticScope) { }
default void beforeAgenticScopeDestroyed(AgenticScope agenticScope) { }
default void beforeToolExecution(BeforeToolExecution beforeToolExecution) { }
default void afterToolExecution(ToolExecution toolExecution) { }
default boolean inheritedBySubagents() {
return false;
}
}
请注意,该接口的所有方法都提供了默认的空实现,因此只需实现感兴趣的方法即可。这样也允许在未来版本中添加新方法,而不会破坏现有的实现。
例如,以下CreativeWriter代理的配置会在调用时记录到控制台,并显示它生成的故事内容。
CreativeWriter creativeWriter = AgenticServices.agentBuilder(CreativeWriter.class)
.chatModel(baseModel())
.outputKey("story")
.listener(new AgentListener() {
@Override
public void beforeAgentInvocation(AgentRequest request) {
System.out.println("Invoking CreativeWriter with topic: " + request.inputs().get("topic"));
}
@Override
public void afterAgentInvocation(AgentResponse response) {
System.out.println("CreativeWriter generated this story: " + response.output());
}
})
.build();
这些监听方法分别接收一个AgentRequest和AgentResponse作为参数,它们提供了有关代理调用的有用信息,例如其名称、接收的输入和生成的输出,以及用于该调用的AgenticScope实例。请注意,这些方法在与执行代理调用相同的线程中被调用,因此它们是同步的,不应执行长时间阻塞的操作。
AgentListener有两个重要特性,分别是:
- 可组合的,这意味着你可以通过多次调用listener方法为同一个代理注册多个监听器,它们将按照注册顺序被通知;
- 可选层级结构,意味着默认情况下它们仅在直接注册的代理中本地生效,但也可以通过将其inheritedBySubagents方法设置为返回true来被所有子代理继承。在这种情况下,注册在顶级代理上的监听器也会收到对其所有层级子代理调用的通知,并与这些子代理可能自行注册的所有监听器共同作用。
监控
利用AgentListener接口提供的可观测性功能,langchain4j-agentic模块还内置了该接口的实现类AgentMonitor。该实现默认被所有子代理继承,其核心功能是将所有代理调用记录在内存树形结构中,便于在智能体系统运行期间或执行结束后检查调用序列及其结果。开发者可通过代理构建器的listener方法,将该监控器注册为智能体系统根代理的监听器。
为了提供一个更全面的示例,让我们重新考虑旨在生成并迭代优化故事直到其达到所需风格质量的循环工作流程,并在其上注册几个监听器,包括一个AgentMonitor。
AgentMonitor monitor = new AgentMonitor();
CreativeWriter creativeWriter = AgenticServices.agentBuilder(CreativeWriter.class)
.listener(new AgentListener() {
@Override
public void beforeAgentInvocation(AgentRequest request) {
System.out.println("Invoking CreativeWriter with topic: " + request.inputs().get("topic"));
}
})
.chatModel(baseModel())
.outputKey("story")
.build();
StyleEditor styleEditor = AgenticServices.agentBuilder(StyleEditor.class)
.chatModel(baseModel())
.outputKey("story")
.build();
StyleScorer styleScorer = AgenticServices.agentBuilder(StyleScorer.class)
.name("styleScorer")
.chatModel(baseModel())
.outputKey("score")
.build();
UntypedAgent styleReviewLoop = AgenticServices.loopBuilder()
.subAgents(styleScorer, styleEditor)
.maxIterations(5)
.exitCondition(agenticScope -> agenticScope.readState("score", 0.0) >= 0.8)
.build();
UntypedAgent styledWriter = AgenticServices.sequenceBuilder()
.subAgents(creativeWriter, styleReviewLoop)
.listener(monitor)
.listener(new AgentListener() {
@Override
public void afterAgentInvocation(AgentResponse response) {
if (response.agentName().equals("styleScorer")) {
System.out.println("Current score: " + response.output());
}
}
})
.outputKey("story")
.build();
这里直接在creativeWriter代理上注册了第一个监听器,因此只有当该代理被调用时,它才会记录要生成故事的请求主题。第二个监听器注册在顶层的styledWriter代理上,因此对于该代理层次结构中任何级别的所有子代理,它也会被调用。这就是为什么该监听器的afterAgentInvocation方法会检查被调用的代理是否是styleScorer,并且只有在这种情况下,它才会记录为生成故事风格分配的当前分数。
最后,AgentMonitor实例也被注册,并自动与其他两个监听器组合,作为styledWriter顶级代理的另一个监听器,以便它能跟踪整个代理系统中所有代理的调用。
当如下调用 styledWriter代理时:
Map<String, Object> input = Map.of(
"topic", "dragons and wizards",
"style", "comedy");
String story = styledWriter.invoke(input);
AgentMonitor以树形结构记录所有代理的调用情况,同时跟踪每次代理调用的开始时间、结束时间、持续时间、输入和输出。此时可以从监控器中检索记录的调用执行情况,例如将其打印到控制台进行检查。
MonitoredExecution execution = monitor.successfulExecutions().get(0);
System.out.println(execution);
因此,它将揭示生成和完善故事所需的代理调用的嵌套序列,如下所示:
AgentInvocation{agent=Sequential, startTime=2025-12-04T17:23:45.684601233, finishTime=2025-12-04T17:25:31.310476077, duration=105625 ms, inputs={style=comedy, topic=dragons and wiz...}, output=In the shadowy ...}
|=> AgentInvocation{agent=generateStory, startTime=2025-12-04T17:23:45.687031946, finishTime=2025-12-04T17:23:53.216629832, duration=7529 ms, inputs={topic=dragons and wiz...}, output=In the shadowed...}
|=> AgentInvocation{agent=reviewLoop, startTime=2025-12-04T17:23:53.218004760, finishTime=2025-12-04T17:25:31.310442197, duration=98092 ms, inputs={score=0.85, topic=dragons and wiz..., style=comedy, story=In the shadowy ...}, output=null}
|=> AgentInvocation{agent=scoreStyle, startTime=2025-12-04T17:23:53.218606335, finishTime=2025-12-04T17:23:58.900747685, duration=5682 ms, inputs={style=comedy, story=In the shadowed...}, output=0.25}
|=> AgentInvocation{agent=editStory, startTime=2025-12-04T17:23:58.901041911, finishTime=2025-12-04T17:24:58.130857588, duration=59229 ms, inputs={style=comedy, story=In the shadowed...}, output=In the shadowy ...}
|=> AgentInvocation{agent=scoreStyle, startTime=2025-12-04T17:24:58.130980855, finishTime=2025-12-04T17:25:31.310076714, duration=33179 ms, inputs={style=comedy, story=In the shadowy ...}, output=0.85}
声明式API
目前为止讨论的所有工作流模式都可以通过声明式API来定义,这使您能够以更简洁、更易读的方式定义工作流。langchain4j-agentic模块提供了一组注解,可用于以更声明式的方式定义代理及其工作流
例如,EveningPlannerAgent实现了前一节中以编程方式定义的并行工作流,可以使用声明式API重写如下:
public interface EveningPlannerAgent {
@ParallelAgent( outputKey = "plans",
subAgents = { FoodExpert.class, MovieExpert.class })
List<EveningPlan> plan(@V("mood") String mood);
@ParallelExecutor
static Executor 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注解的静态方法来定义如何将子代理的输出合并为单一结果,其方式与向output方法传递AgenticScope函数时的处理逻辑完全一致。
一旦定义了这个接口,就可以使用AgenticServices.createAgenticSystem()方法创建一个EveningPlannerAgent实例,然后像之前一样使用它。
EveningPlannerAgent eveningPlannerAgent = AgenticServices
.createAgenticSystem(EveningPlannerAgent.class, BASE_MODEL);
List<EveningPlan> plans = eveningPlannerAgent.plan("romantic");
在这种情况下,AgenticServices.createAgenticSystem()方法还提供了一个默认用于创建该代理系统中所有子代理的ChatModel。不过,也可以选择为特定子代理指定不同的ChatModel,只需在其定义中添加一个用@ChatModelSupplier注解的静态方法,该方法返回与该代理一起使用的ChatModel。例如,FoodExpert代理可以按如下方式定义自己的ChatModel:
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(outputKey = "meals")
List<String> findMeal(@V("mood") String mood);
@ChatModelSupplier
static ChatModel chatModel() {
return FOOD_MODEL;
}
}
以非常类似的方式,在代理接口中注释其他static方法,可以声明式地配置代理的其他方面,如聊天记忆、可使用的工具等。除非下表中另有规定,否则这些方法不得有参数。以下是可用于此目的的注释列表:
| 注解名称 | 描述 |
|---|---|
@ChatModelSupplier |
返回此代理将使用的 ChatModel。 |
@ChatMemorySupplier |
返回此代理将使用的 ChatMemory。 |
@ChatMemoryProviderSupplier |
返回此代理要使用的 ChatMemoryProvider。此方法需要一个 Object作为参数,用作所创建内存的 memoryId。 |
@ContentRetrieverSupplier |
返回此代理使用的ContentRetriever。 |
@AgentListenerSupplier |
返回此代理要使用的 AgentListener。 |
@RetrievalAugmentorSupplier |
返回此代理使用的 RetrievalAugmentor。 |
@ToolsSupplier |
返回此代理要使用的工具或工具集。它可以返回单个Object或Object[]。 |
@ToolProviderSupplier |
返回此代理要使用的 ToolProvider |
再举一个声明式 API 的例子,让我们通过它重新定义在条件工作流部分演示的 ExpertsAgent。
public interface ExpertsAgent {
@ConditionalAgent(outputKey = "response",
subAgents = { MedicalExpert.class, TechnicalExpert.class, LegalExpert.class })
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时被激活的一组代理类。
请注意,也可以混合使用编程式和声明式风格来定义智能体和智能体系统,这样可以通过注解和智能体构建器部分配置智能体。此外,也允许完全以声明式定义一个智能体,然后以编程方式使用该智能体的类作为子智能体来实现智能体系统。例如,可以如下声明式地定义CreativeWriter和AudienceEditor智能体:
public interface CreativeWriter {
@UserMessage("""
You are a creative writer.
Generate a draft of a story long no more than 3 sentence around the given topic.
Return only the story and nothing else.
The topic is {{topic}}.
""")
@Agent(description = "Generate a story based on the given topic", outputKey = "story")
String generateStory(@V("topic") String topic);
@ChatModelSupplier
static ChatModel chatModel() {
return baseModel();
}
}
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(description = "Edit a story to better fit a given audience", outputKey = "story")
String editStory(@V("story") String story, @V("audience") String audience);
@ChatModelSupplier
static ChatModel chatModel() {
return baseModel();
}
}
然后通过编程方式简单地按照它们的类别作为子代理依次连接起来。
UntypedAgent novelCreator = AgenticServices.sequenceBuilder()
.subAgents(CreativeWriter.class, AudienceEditor.class)
.outputKey("story")
.build();
Map<String, Object> input = Map.of(
"topic", "dragons and wizards",
"audience", "young adults"
);
String story = (String) novelCreator.invoke(input);
强类型的输入和输出
到目前为止,所有用于在智能体之间传递数据的输入和输出键都通过简单的String来标识。然而,这种方法容易出错,因为它依赖于这些键的正确拼写。此外,这种方式无法将这些变量强绑定到特定类型,因此在从AgenticScope读取它们的值时需要进行类型检查和转换。为了避免这些问题,可以选择使用TypedKey接口来定义强类型的输入和输出键。
例如,按照这种方法,在介绍条件工作流时所讨论的专家路由示例中使用的输入和输出键可以定义如下:
public static class UserRequest implements TypedKey<String> { }
public static class ExpertResponse implements TypedKey<String> { }
public static class Category implements TypedKey<RequestCategory> {
@Override
public Category defaultValue() {
return Category.UNKNOWN;
}
}
在这里,UserRequest和 ExpertResponse键都被严格类型化为 String,而 Category键则被类型化为 RequestCategory枚举,并且还提供了一个默认值,以便在 AgenticScope中不存在该键时使用。通过这些类型化的键,用于分类用户请求的 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: '{{UserRequest}}'.
""")
@Agent(description = "Categorizes a user request", typedOutputKey = Category.class)
RequestCategory classify(@K(UserRequest.class) String request);
}
classify方法的参数现在被标注为@K注解,表明其值必须取自由UserRequest类型键标识的AgenticScope变量。同样地,该智能体的输出会被写入由Category类型键标识的AgenticScope变量。请注意,提示模板也已更新为使用类型键的名称,默认情况下,该名称对应于实现TypedKey接口的类的简单名称,在本例中为{{UserRequest}},但这一惯例也可以通过实现TypedKey接口的name()方法来覆盖。类似地,三个专家智能体之一的MedicalExpert可以重新定义如下:
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 {{UserRequest}}.
""")
@Agent("A medical expert")
String medical(@K(UserRequest.class) String request);
}
此时可以使用这些类型化键来识别AgenticScope中的输入和输出变量,从而构建整个代理系统。
CategoryRouter routerAgent = AgenticServices.agentBuilder(CategoryRouter.class)
.chatModel(baseModel())
.build();
MedicalExpert medicalExpert = AgenticServices.agentBuilder(MedicalExpert.class)
.chatModel(baseModel())
.outputKey(ExpertResponse.class)
.build();
LegalExpert legalExpert = AgenticServices.agentBuilder(LegalExpert.class)
.chatModel(baseModel())
.outputKey(ExpertResponse.class)
.build();
TechnicalExpert technicalExpert = AgenticServices.agentBuilder(TechnicalExpert.class)
.chatModel(baseModel())
.outputKey(ExpertResponse.class)
.build();
UntypedAgent expertsAgent = AgenticServices.conditionalBuilder()
.subAgents(scope -> scope.readState(Category.class) == Category.MEDICAL, medicalExpert)
.subAgents(scope -> scope.readState(Category.class) == Category.LEGAL, legalExpert)
.subAgents(scope -> scope.readState(Category.class) == Category.TECHNICAL, technicalExpert)
.build();
ExpertChatbot expertChatbot = AgenticServices.sequenceBuilder(ExpertChatbot.class)
.subAgents(routerAgent, expertsAgent)
.outputKey(ExpertResponse.class)
.build();
String response = expertChatbot.ask("I broke my leg what should I do");
routerAgent不需要通过编程方式指定输出键,因为其接口中已通过@Agent注解的typedOutputKey属性定义,而3个专家代理仍需编程指定,因其接口未定义该属性,因此仍可任选两种方式之一。另外值得注意的是,当从AgenticScope读取值时(例如在条件工作流定义中),无需执行任何类型检查或转换,因为类型化键已提供必要的类型信息。
记忆与上下文工程
到目前为止讨论的所有代理都是无状态的,这意味着它们不会保留任何先前交互的上下文或记忆。然而,与任何其他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))
.outputKey("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()))
.outputKey("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")
.outputKey("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服务提供者接口,创建一个名为META-INF/services/dev.langchain4j.agentic.scope.AgenticScopeStore的文件,其中包含实现AgenticScopeStore接口的类的完全限定名。
纯粹的代理型人工智能
到目前为止,所有智能体都是通过确定性工作流程进行连接和组合,以构建智能体系统。然而,在某些情况下,智能体系统需要更具灵活性和适应性,允许智能体根据上下文和先前交互的结果来决定后续行动。这通常被称为"纯智能体AI"。
为此目的,langchain4j-agentic模块提供了一个开箱即用的监督代理,它可以配备一组子代理,并能够自主生成计划,决定接下来调用哪个代理或判断分配的任务是否已完成
为了举例说明其工作原理,我们定义几个代理,它们可以对银行账户进行存款或取款操作,或者将一定金额从一种货币兑换成另一种货币。
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 exchangeAgent = 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而非简单的顺序工作流来创作故事,并根据特定风格和受众进行编辑(如最初示例所示)。此时用户只会对最终故事感兴趣,而不会关心创作过程中间步骤的摘要。
实际上,最常见的情况是返回最后调用代理生成的响应而非摘要,因此这也是监督者代理的默认行为。但对于当前场景而言,返回所有执行事务的摘要更为合适,因此已通过responseStrategy方法对SupervisorAgent进行了相应配置。
下一节将讨论这一点以及监督者代理的其他可能定制方式。
主管设计与定制
更普遍的情况是,有时无法提前确定两个响应中哪个更适合返回:一个是监督者生成的摘要,另一个是最后调用的代理的最终响应。针对这种情况,我们引入了一个辅助代理机制。该代理会接收原始用户请求以及这两个可能的响应,并对它们进行评分,从而决定哪个响应更符合请求要求,进而确定最终返回哪个结果。
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();
未来可能会逐步实现并开放对监督代理的其他定制点。
向主管提供上下文信息
在许多实际场景中,监督者可以从一个可选的上下文中获益:这些上下文包括应指导规划的约束、策略或偏好(例如,“优先使用内部工具”、“不要调用外部服务”、“货币必须为美元”等)。
该上下文存储在名为supervisorContext的AgenticScope变量中。您可以通过两种方式提供它:
- 构建时配置
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);
}
// Example call (overrides the build-time value for this invocation)
bankSupervisor.invoke(
"Transfer 100 EUR from Mario's account to Georgios' one",
"Policies: convert to USD first; use bank tools only; no external APIs"
);
- 调用(无类型监管者):在输入映射中设置 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);
如果两者都提供了,调用值会覆盖构建时的 supervisorContext。
自定义代理模式
到目前为止讨论的代理模式都是由langchain4j-agentic模块开箱即用的,但如果它们都不符合您应用程序的特定需求怎么办?在这种情况下,可以创建自己的自定义模式,以根据您的要求协调一组子代理之间的交互。
public interface Planner {
default void init(InitPlanningContext initPlanningContext) { }
default Action firstAction(PlanningContext planningContext) {
return nextAction(planningContext);
}
Action nextAction(PlanningContext planningContext);
}
该接口包含三个方法:init、firstAction和 nextAction。init方法在执行开始时仅调用一次,可用于初始化规划器所需的任何状态或数据结构。firstAction方法用于确定由代理模式执行的第一个动作,而 nextAction方法则在每次代理执行后调用,根据 AgenticScope的当前状态和上一次代理执行的结果来确定下一步要执行的动作。
需要注意的是,引入 firstAction方法主要是因为在许多情况下,定义一个单独的回调来指定 Planner调用的第一个代理非常方便。然而,在不需要这种区分的情况下,它提供了一个默认实现,只需将调用转发给 nextAction方法,因此并不严格要求重写它。
firstAction和 nextAction方法返回的 Action类表示代理模式要执行的下一步操作,可以是要调用的一个或多个子代理的列表,也可以是表示执行已完成的信号。如果动作仅指定一个子代理调用,则它将按顺序执行,并在执行规划器本身的同一线程中运行;如果有多个子代理,则将使用提供的 Executor或 LangChain4j 默认的执行器并行执行。
所有内置的代理模式也是基于这个 Planner抽象编写的,查看它们的实现可以帮助理解其工作原理,并为创建自定义模式提供一个良好的起点。例如,并行工作流可能是这些实现中最简单的一个,其定义如下:
public class ParallelPlanner implements Planner {
private List<AgentInstance> agents;
@Override
public void init(InitPlanningContext initPlanningContext) {
this.agents = initPlanningContext.subagents();
}
@Override
public Action firstAction(PlanningContext planningContext) {
return call(agents);
}
@Override
public Action nextAction(PlanningContext planningContext) {
return done();
}
}
这里的init方法只是存储了并行工作流配置的子代理列表,而firstAction方法返回一个并行调用所有这些代理的动作。一旦这个并行执行完成,就没有其他动作需要执行了,因此nextAction方法直接返回done()来表示执行终止。
实现顺序工作流的Planner稍微复杂一些,因为它需要使用内部游标来跟踪接下来要调用的子代理,然后在nextAction方法中返回适当的动作,或者当所有子代理都被调用后发出执行终止的信号。
public class SequentialPlanner implements Planner {
private List<AgentInstance> agents;
private int agentCursor = 0;
@Override
public void init(InitPlanningContext initPlanningContext) {
this.agents = initPlanningContext.subagents();
}
@Override
public Action nextAction(PlanningContext planningContext) {
return agentCursor >= agents.size() ? done() : call(agents.get(agentCursor++));
}
}
要从规划器实现的角度理解如何定义一个代理系统,可以按照以下方式创建一个实例:先生成针对某个主题的小说,然后根据特定风格和受众进行编辑,如前所述。
UntypedAgent novelCreator = AgenticServices.plannerBuilder()
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputKey("story")
.planner(SequentialPlanner::new)
.build();
这完全等同于使用专为顺序工作流设计的API:
UntypedAgent novelCreator = AgenticServices.sequenceBuilder()
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputKey("story")
.build();
plannerBuilder()方法与所有其他代理构建器类似,唯一区别在于它需要提供一个Supplier<Planner>,该供应器需返回此代理系统将使用的特定规划器新实例。当然,实现自定义规划器的代理系统可以与langchain4j-agentic模块开箱即提供的任何其他代理模式无缝结合。
在明确了Planner抽象的工作原理后,现在可以通过实现它来创建自定义的代理模式。接下来的章节将讨论langchain4j-agentic-patterns模块中提供的两个自定义模式示例,这些示例在不同场景下可能很有用。其他自定义模式可以按照相同的方法创建,并可能贡献回LangChain4j项目。
目标导向的代理模式
工作流模式和监督代理代表了可能的代理系统的两个极端:前者是完全确定性和刚性的,需要预先决定调用代理的顺序;而后者是完全灵活和自适应的,但将调用代理序列的决策委托给一个非确定性的大语言模型(LLM)。然而,在某些情况下,介于这两个极端之间的折中方案可能更为合适,既允许代理以相对灵活的方式朝着特定目标工作,又能以算法方式确定这些代理应如何被调用。
为了将这种方法付诸实践,不仅整个代理系统需要定义一个目标,而且每个子代理还需要声明自己的前置条件和后置条件。这对于计算能以最快方式实现目标的代理调用序列是必要的。然而,所有这些信息实际上已经隐含在代理系统中,因为这些前置条件和后置条件不过是每个代理所需的输入和产生的输出,而最终目标就是整个代理系统期望的输出。
基于这一思路,可以计算参与代理系统的所有子代理的依赖图,然后实现一个规划器(Planner),它能够分析AgenticScope的初始状态,将其与期望目标进行比较,并利用该依赖图确定能够实现该目标的代理调用序列。
public class GoalOrientedPlanner implements Planner {
private String goal;
private GoalOrientedSearchGraph graph;
private List<AgentInstance> path;
private int agentCursor = 0;
@Override
public void init(InitPlanningContext initPlanningContext) {
this.goal = initPlanningContext.plannerAgent().outputKey();
this.graph = new GoalOrientedSearchGraph(initPlanningContext.subagents());
}
@Override
public Action firstAction(PlanningContext planningContext) {
path = graph.search(planningContext.agenticScope().state().keySet(), goal);
if (path.isEmpty()) {
throw new IllegalStateException("No path found for goal: " + goal);
}
return call(path.get(agentCursor++));
}
@Override
public Action nextAction(PlanningContext planningContext) {
return agentCursor >= path.size() ? done() : call(path.get(agentCursor++));
}
}
正如预期的那样,这里的目标与基于规划器的自主模式本身的最终输出一致,而从初始状态到目标的路径则是通过分析所有子代理的输入和输出键构建的目标导向搜索图来计算的。然后,在该图上从当前状态到期望目标的最短路径即为需要调用的代理序列。
为了具体说明其工作原理,让我们尝试构建一个目标导向的自主系统,该系统可以从提示中提取一个人的姓名和星座,生成该星座的运势,在互联网上查找相关故事,最后将所有信息整合成一篇漂亮的文章。我们可以通过以下五个代理来完成这一系列任务:
public interface HoroscopeGenerator {
@SystemMessage("You are an astrologist that generates horoscopes based on the user's name and zodiac sign.")
@UserMessage("Generate the horoscope for {{person}} who is a {{sign}}.")
@Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.")
String horoscope(@V("person") Person person, @V("sign") Sign sign);
}
public interface PersonExtractor {
@UserMessage("Extract a person from the following prompt: {{prompt}}")
@Agent("Extract a person from user's prompt")
Person extractPerson(@V("prompt") String prompt);
}
public interface SignExtractor {
@UserMessage("Extract the zodiac sign of a person from the following prompt: {{prompt}}")
@Agent("Extract a person from user's prompt")
Sign extractSign(@V("prompt") String prompt);
}
public interface Writer {
@UserMessage("""
Create an amusing writeup for {{person}} based on the following:
- their horoscope: {{horoscope}}
- a current news story: {{story}}
""")
@Agent("Create an amusing writeup for the target person based on their horoscope and current news stories")
String write(@V("person") Person person, @V("horoscope") String horoscope, @V("story") String story);
}
public interface StoryFinder {
@SystemMessage("""
You're a story finder, use the provided web search tools, calling it once and only once,
to find a fictional and funny story on the internet about the user provided topic.
""")
@UserMessage("""
Find a story on the internet for {{person}} who has the following horoscope: {{horoscope}}.
""")
@Agent("Find a story on the internet for a given person with a given horoscope")
String findStory(@V("person") Person person, @V("horoscope") String horoscope);
}
利用之前开发的GoalOrientedPlanner,这些智能体可以按照以下方式组合成一个目标导向的智能系统:
HoroscopeGenerator horoscopeGenerator = AgenticServices.agentBuilder(HoroscopeGenerator.class)
.chatModel(baseModel())
.outputKey("horoscope")
.build();
PersonExtractor personExtractor = AgenticServices.agentBuilder(PersonExtractor.class)
.chatModel(baseModel())
.outputKey("person")
.build();
SignExtractor signExtractor = AgenticServices.agentBuilder(SignExtractor.class)
.chatModel(baseModel())
.outputKey("sign")
.build();
Writer writer = AgenticServices.agentBuilder(Writer.class)
.chatModel(baseModel())
.outputKey("writeup")
.build();
StoryFinder storyFinder = AgenticServices.agentBuilder(StoryFinder.class)
.chatModel(baseModel())
.tools(new WebSearchTool())
.outputKey("story")
.build();
UntypedAgent horoscopeAgent = AgenticServices.plannerBuilder()
.subAgents(horoscopeGenerator, personExtractor, signExtractor, writer, storyFinder)
.outputKey("writeup")
.planner(GoalOrientedPlanner::new)
.build();
正如预期的那样,这个代理系统的总体目标是生成一份报告,这也是基于GOAP的规划器本身的输出键。考虑到所有子代理的输入和输出,由GoalOrientedSearchGraph构建的依赖关系图将如下所示:

当用“我叫马里奥,我的星座是双鱼座”这样的提示调用这个代理系统时
Map<String, Object> input = Map.of("prompt", "My name is Mario and my zodiac sign is pisces");
String writeup = horoscopeAgent.invoke(input);
GoalOrientedPlanner将分析AgenticScope的初始状态,该状态仅包含prompt变量,然后计算从该初始状态到期望目标writeup在依赖图中的最短路径,从而得到代理调用的序列顺序为:
Agents path sequence: [extractPerson, extractSign, horoscope, findStory, write]
请注意,正如预期的那样,这种以目标为导向的代理模式可以与任何其他现有的代理模式混合和组合。例如,可以利用这种可能性来克服该方法的一个明显局限性:由于它在结构上被优化为遵循最短路径实现特定目标,因此不允许循环。因此,在某些情况下,将循环代理模式作为这种目标导向模式的子代理可能会很有用。
点对点代理模式
迄今为止讨论的所有代理系统都基于集中式和分层架构。实际上,所有工作流模式都有一个明确定义的顶层代理,以程序预定的方式协调多个子代理的活动。即使是更灵活、动态的监督者模式(得益于其基于LLM的规划代理),仍然依赖于一个协调代理来控制各个子代理之间的交互。这类架构适用于许多应用和场景,但也存在一些局限性,特别是在可扩展性和容错性方面。因此,我们可能需要为多代理系统提供一种替代的对等(peer-to-peer)方法,通过采用更去中心化和分布式的策略来克服这些限制。
在对等代理系统中,不存在任何顶层代理,所有代理都是平等的对等体,通过AgenticScope的状态进行协调。具体来说,当一个代理所需的输入作为状态变量出现在AgenticScope中时,该代理就会被触发。随后,由其他代理的输出导致这些变量中的一个或多个发生变化时,可能会再次触发该代理的调用。当AgenticScope达到稳定状态且无法再调用任何代理时,或满足预定义的退出条件时,或达到最大代理调用次数时,该过程终止。实现这种对等代理模式的Planner可以按如下方式编写:
public class P2PPlanner implements Planner {
private final int maxAgentsInvocations;
private final BiPredicate<AgenticScope, Integer> exitCondition;
private int invocationCounter = 0;
private Map<String, AgentActivator> agentActivators;
public P2PPlanner(int maxAgentsInvocations, BiPredicate<AgenticScope, Integer> exitCondition) {
this(null, maxAgentsInvocations, exitCondition);
}
@Override
public void init(InitPlanningContext initPlanningContext) {
this.agentActivators = initPlanningContext.subagents().stream().collect(toMap(AgentInstance::agentId, AgentActivator::new));
}
@Override
public Action nextAction(PlanningContext planningContext) {
if (terminated(planningContext.agenticScope())) {
return done();
}
AgentActivator lastExecutedAgent = agentActivators.get(planningContext.previousAgentInvocation().agentId());
lastExecutedAgent.finishExecution();
agentActivators.values().forEach(a -> a.onStateChanged(lastExecutedAgent.agent.outputKey()));
return nextCallAction(planningContext.agenticScope());
}
private Action nextCallAction(AgenticScope agenticScope) {
AgentInstance[] agentsToCall = agentActivators.values().stream()
.filter(agentActivator -> agentActivator.canActivate(agenticScope))
.peek(AgentActivator::startExecution)
.map(AgentActivator::agent)
.toArray(AgentInstance[]::new);
invocationCounter += agentsToCall.length;
return call(agentsToCall);
}
private boolean terminated(AgenticScope agenticScope) {
return invocationCounter > maxAgentsInvocations || exitCondition.test(agenticScope, invocationCounter);
}
}
在这里,P2PPlanner会记录到目前为止执行的代理调用次数,并使用AgentActivator为每个子代理判断其是否可以根据AgenticScope的当前状态被调用。nextAction方法会检查退出条件是否满足或是否已达到最大调用次数,如果没有,它会根据当前状态识别所有可被激活的代理,将它们标记为已启动,并返回一个调用它们的操作。
为了具体说明其工作原理,让我们尝试构建一个能够进行科学研究并就给定主题提出新假设的点对点代理系统,这样该服务的API可能类似于:
public interface ResearchAgent {
@Agent("Conduct research on a given topic")
String research(@V("topic") String topic);
}
为此目的,可以定义以下5个代理:
public interface LiteratureAgent {
@SystemMessage("Search for scientific literature on the given topic and return a summary of the findings.")
@UserMessage("""
You are a scientific literature search agent.
Your task is to find relevant scientific papers on the topic provided by the user and summarize them.
Use the provided tool to search for scientific papers and return a summary of your findings.
The topic is: {{topic}}
""")
@Agent("Search for scientific literature on a given topic")
String searchLiterature(@V("topic") String topic);
}
public interface HypothesisAgent {
@SystemMessage("Based on the research findings, formulate a clear and concise hypothesis related to the given topic.")
@UserMessage("""
You are a hypothesis formulation agent.
Your task is to formulate a clear and concise hypothesis based on the research findings provided by the user.
The topic is: {{topic}}
The research findings are: {{researchFindings}}
""")
@Agent("Formulate hypothesis around a give topic based on research findings")
String makeHypothesis(@V("topic") String topic, @V("researchFindings") String researchFindings);
}
public interface CriticAgent {
@SystemMessage("Critically evaluate the given hypothesis related to the specified topic. Provide constructive feedback and suggest improvements if necessary.")
@UserMessage("""
You are a critical evaluation agent.
Your task is to critically evaluate the hypothesis provided by the user in relation to the specified topic.
Provide constructive feedback and suggest improvements if necessary.
If you need to, you can also perform additional research to validate or confute the hypothesis using the provided tool.
The topic is: {{topic}}
The hypothesis is: {{hypothesis}}
""")
@Agent("Critically evaluate a hypothesis related to a given topic")
String criticHypothesis(@V("topic") String topic, @V("hypothesis") String hypothesis);
}
public interface ValidationAgent {
@SystemMessage("Validate the provided hypothesis on the given topic based on the critique provided.")
@UserMessage("""
You are a validation agent.
Your task is to validate the hypothesis provided by the user in relation to the specified topic based on the critique provided.
Validate the provided hypothesis, either confirming it or reformulating a different hypothesis based on the critique.
The topic is: {{topic}}
The hypothesis is: {{hypothesis}}
The critique is: {{critique}}
""")
@Agent("Validate a hypothesis based on a given topic and critique")
String validateHypothesis(@V("topic") String topic, @V("hypothesis") String hypothesis, @V("critique") String critique);
}
public interface ScorerAgent {
@SystemMessage("Score the provided hypothesis on the given topic based on the critique provided.")
@UserMessage("""
You are a scoring agent.
Your task is to score the hypothesis provided by the user in relation to the specified topic based on the critique provided.
Score the provided hypothesis on a scale from 0.0 to 1.0, where 0.0 means the hypothesis is completely invalid and 1.0 means the hypothesis is fully valid.
The topic is: {{topic}}
The hypothesis is: {{hypothesis}}
The critique is: {{critique}}
""")
@Agent("Score a hypothesis based on a given topic and critique")
double scoreHypothesis(@V("topic") String topic, @V("hypothesis") String hypothesis, @V("critique") String critique);
}
这些代理都将配备一个能够进行科学文献研究的工具,例如从arXiv下载学术论文,然后被添加到P2P代理系统中:
ArxivCrawler arxivCrawler = new ArxivCrawler();
LiteratureAgent literatureAgent = AgenticServices.agentBuilder(LiteratureAgent.class)
.chatModel(baseModel())
.tools(arxivCrawler)
.outputKey("researchFindings")
.build();
HypothesisAgent hypothesisAgent = AgenticServices.agentBuilder(HypothesisAgent.class)
.chatModel(baseModel())
.tools(arxivCrawler)
.outputKey("hypothesis")
.build();
CriticAgent criticAgent = AgenticServices.agentBuilder(CriticAgent.class)
.chatModel(baseModel())
.tools(arxivCrawler)
.outputKey("critique")
.build();
ValidationAgent validationAgent = AgenticServices.agentBuilder(ValidationAgent.class)
.chatModel(baseModel())
.tools(arxivCrawler)
.outputKey("hypothesis")
.build();
ScorerAgent scorerAgent = AgenticServices.agentBuilder(ScorerAgent.class)
.chatModel(baseModel())
.tools(arxivCrawler)
.outputKey("score")
.build();
ResearchAgent researcher = AgenticServices.plannerBuilder(ResearchAgent.class)
.subAgents(literatureAgent, hypothesisAgent, criticAgent, validationAgent, scorerAgent)
.outputKey("hypothesis")
.planner(() -> new P2PPlanner(10, agenticScope -> {
if (!agenticScope.hasState("score")) {
return false;
}
double score = agenticScope.readState("score", 0.0);
System.out.println("Current hypothesis score: " + score);
return score >= 0.85;
}))
.build();
String hypothesis = researcher.research("black holes");
在这种配置下,researcher点对点协调器会接收研究主题作为输入。此时唯一可调用的智能体是literatureAgent(文献代理),因为它是当前唯一满足所有输入条件的智能体——在这个案例中,AgenticScope中已存在其必需的topic(主题)参数。调用该智能体会生成researchFindings(研究发现)变量,该变量会被加入AgenticScope状态库,进而触发HypothesisAgent(假设代理)的调用。随后生成的hypothesis(假设)又会激活criticAgent(批评代理)。最终ValidationAgent(验证代理)会同时接收hypothesis和critique(批评意见)作为输入,生成新版hypothesis并再次触发其他智能体的连锁反应。与此同时,ScorerAgent(评分代理)会对假设进行score(评分),当评分≥0.85或智能体调用次数达到10次上限时,整个流程终止。下图完整呈现了本次执行过程中涉及的智能体与变量关系。

例如,这个示例的典型运行可能会因为ScorerAgent产生的分数超过了预定阈值而终止。
Current hypothesis score: 0.95
最终输出可能类似于:
Based on the provided references, here are some key points about stochastic gravitational wave backgrounds (SGWBs) from primordial black holes (PBHs):
1. **Detection Rates and Sources:**
- The detection rate of gravity waves emitted during parabolic encounters of stellar black holes in globular clusters was estimated by Kocsis et al. [85].
- Gravitational wave bursts from PBH hyperbolic encounters were discussed by García-Bellido and Nesseris [93].
2. **Energy Emission:**
- The energy spectrum of gravitational waves from hyperbolic encounters was studied by De Vittori, Jetzer, and Klein [88].
- Gravitational wave energy emission and detection rates for PBH hyperbolic encounters were analyzed by García-Bellido and Nesseris [90].
3. **Template Banks:**
- Template banks for gravitational waveforms from coalescing binary black holes (including non-spinning binaries) were developed by Ajith et al. [92].
4. **Constraints on PBHs:**
- Constraints on primordial black holes were reviewed by Carr, Kohri, Sendouda, and Yokoyama [98].
- Universal gravitational wave signatures of cosmological solitons were discussed by Lozanov, Sasaki, and Takhistov [100].
5. **Induced SGWBs:**
- Doubly peaked induced stochastic gravitational wave backgrounds were tested for baryogenesis from primordial black holes by Bhaumik et al. [101].
- Distinct signatures of spinning PBH domination and evaporation, including doubly peaked gravitational waves, dark relics, and CMB complementarity, were explored by Bhaumik et al. [101].
6. **Future Detectors:**
- Future detectors like Taiji, LISA, DECIGO, Big Bang Observer, Cosmic Explorer, Einstein Telescope, and KAGRA are expected to contribute significantly to the detection of SGWBs from PBHs.
7. **Pulsar Timing Arrays:**
- Pulsar timing arrays have been used to search for an isotropic stochastic gravitational wave background [73-75].
8. **Template Banks and Simulations:**
- Template banks like those developed by Ajith et al. are crucial for matching observed signals with theoretical predictions.
非AI代理
到目前为止讨论的所有代理都是AI代理,这意味着它们基于大型语言模型(LLM),可以被调用来执行需要自然语言理解和生成的任务。然而,langchain4j-agentic模块也支持非AI代理,这些代理可用于执行不需要自然语言处理的任务,例如调用REST API或执行命令。这些非AI代理实际上更类似于工具,但在这种情况下,将它们建模为代理更为方便,这样它们就可以像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",
outputKey = "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中的代理可以是任何Java类,只要它有一个且仅有一个带有@Agent注解的方法。
最后,非AI代理也可以用于读取AgenticScope的状态或对其执行小规模操作,因此AgenticServices提供了一个agentAction工厂方法,用于从Consumer<AgenticServices>创建一个简单的代理。例如,假设有一个scorer代理生成一个String类型的score值,而后续的reviewer代理需要以double类型消费该score。在这种情况下,这两个代理将不兼容,但可以通过使用agentAction将第一个代理的输出适配为第二个代理所需的格式,如下所示重写AgenticScope的score状态:
UntypedAgent editor = AgenticServices.sequenceBuilder()
.subAgents(
scorer,
AgenticServices.agentAction(agenticScope -> agenticScope.writeState("score", Double.parseDouble(agenticScope.readState("score", "0.0")))),
reviewer)
.build();
人工循环
构建智能代理系统时,另一个常见需求是引入人工监督机制,即在执行特定操作前允许系统向用户请求缺失信息或操作授权。这种人机协同能力可视为一种特殊的非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();
}
}
这个实现虽然相当简单,但非常通用,它基于两个函数的使用:一个用于将AI请求转发给用户的Consumer,以及一个可能以阻塞方式等待用户提供的响应的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")
.outputKey("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
horoscopeAgent.invoke("My name is Mario. What is my horoscope?")
主管代理会注意到用户的星座信息缺失,并调用HumanInTheLoop代理向用户询问,产生以下输出:
What is your zodiac sign?
>
等待用户提供答案,该答案随后将用于调用AstrologyAgent并生成星座运势。
由于用户可能需要一些时间来提供答案,因此可以将HumanInTheLoop代理配置为异步代理,这实际上是推荐的。这样,在代理系统等待用户提供答案的同时,不需要用户输入的代理可以继续执行。但需要注意的是,监督者始终强制所有代理以阻塞方式执行,以便在规划下一步行动时能够考虑到AgenticScope的完整状态。因此,在前面的示例中,将HumanInTheLoop代理配置为异步模式不会产生任何效果。
A2A 集成
新增的 langchain4j-agentic-a2a模块提供了与 A2A 协议的无缝集成,使得可以构建能够使用远程 A2A 服务器代理的系统,并最终将它们与其他本地定义的代理混合使用。
例如,如果在第一个示例中使用的 CreativeWriter代理是在远程 A2A 服务器上定义的,那么可以创建一个 A2ACreativeWriter代理,它可以像本地代理一样使用,但实际调用的是远程代理。
UntypedAgent creativeWriter = AgenticServices
.a2aBuilder(A2A_SERVER_URL)
.inputKeys("topic")
.outputKey("story")
.build();
代理能力的描述是从A2A服务器提供的代理卡中自动获取的。然而,该卡片并未提供输入参数的名称,因此需要使用inputKeys方法明确指定这些参数。
或者,也可以为A2A代理定义一个本地接口,例如:
public interface A2ACreativeWriter {
@Agent
String generateStory(@V("topic") String topic);
}
以便以更类型安全的方式使用,并且输入名称会自动从方法参数中派生。
A2ACreativeWriter creativeWriter = AgenticServices
.a2aBuilder(A2A_SERVER_URL, A2ACreativeWriter.class)
.outputKey("story")
.build();
然后,在定义工作流程或将其用作监督者的子代理时,可以像使用本地代理一样使用该代理,并与它们混合使用。
远程A2A代理必须返回一个 Task
The remote A2A agent must return a Task type.
更多推荐


所有评论(0)