agent 编排 需要引入 langchain4j-agentic 模块
1 2 3 4 5 <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-agentic</artifactId > <version > 1.12.1-beta21</version > </dependency >
对于多 agent 的编排,langchain4j-agentic 提供了四种常用的编排方式,分别是:
sequential workflow: 顺序工作流
loop workflow: 循环工作流
parallel workflow:并行工作流
conditional workflow:条件工作流
通过 AgenticServices 来构建 agent
langchain4j-agentic 通过 agentScope 来管理 agent 调用过程中的上下文,所有的 agent 共享 agentScope 中的数据
agent 观测 需要实现 AgentListener 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 beforeAgentToolExecution (BeforeAgentToolExecution beforeAgentToolExecution) { } default void afterAgentToolExecution (AfterAgentToolExecution afterAgentToolExecution) { } default boolean inheritedBySubagents () { return false ; } }
需要注意 inheritedBySubagents() 方法,默认是 false,只作用于当前注册的 agent,如果是 true,则会被子 agent 继承
因此如果要同时观测主 agent 和子 agent,最好分别实现两个 listener
完整代码
用到的 agent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public interface AudienceEditor { @UserMessage(""" 你是一位专业编辑。 分析并改写以下故事,使其更符合目标受众{{audience}}。 仅返回故事内容,不要添加其他任何内容。 故事为:"{{story}}"。 """) @Agent(name = "AudienceEditor", outputKey = "story", description = "编辑故事以更好地适应特定受众") String editStory (@V("story") String story, @V("audience") String audience) ; } public interface CreativeWriter { @UserMessage(""" 你是一位创意作家。 根据给定主题生成一个故事草稿,长度不超过 3句话。 仅返回故事内容,不要添加其他任何内容。 主题为{{topic}}。 """ ) @Agent(name = "CreativeWriter", outputKey = "story", description = "根据给定主题生成故事") String generateStory (@V("topic") String topic) ; } public interface StyleEditor { @UserMessage(""" 你是一位专业编辑。 分析并改写以下故事,使其更符合{{style}}风格,并保持行文连贯。 仅返回故事内容,不要添加其他任何内容。 故事为:"{{story}}"。 """) @Agent(name = "StyleEditor", outputKey = "story", description = "编辑故事以更好地适应特定风格") String editStory (@V("story") String story, @V("style") String style) ; } public interface StyledWriter { @Agent String writeStoryWithStyle (@V("topic") String topic, @V("style") String style) ; } public interface StyleScorer { @UserMessage(""" 你是一位严谨的评审。 根据以下故事与“{{style}}”风格的契合程度,给出一个介于0.0到1.0之间的评分。 仅返回评分,不要添加其他任何内容。 故事内容:"{{story}}" """) @Agent(name = "StyleScorer", description = "根据故事与特定风格的契合程度进行评分") double scoreStyle (@V("story") String story, @V("style") String style) ; }
listener 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 import dev.langchain4j.agent.tool.ToolExecutionRequest;import dev.langchain4j.agentic.observability.*;import dev.langchain4j.agentic.scope.AgenticScope;public class MyAgentListener implements AgentListener { @Override public void beforeAgentInvocation (AgentRequest agentRequest) { System.out.println("beforeAgentInvocation: " + "[agentName: " + agentRequest.agentName() + " agentId: " + agentRequest.agentId() + " agentInput: " + agentRequest.inputs() + "]" ); } @Override public void afterAgentInvocation (AgentResponse agentResponse) { System.out.println("afterAgentInvocation: " + "[agentName: " + agentResponse.agentName() + " agentId: " + agentResponse.agentId() + " agentInput: " + agentResponse.inputs() + " agentOutput: " + agentResponse.output() + "]" ); } @Override public void onAgentInvocationError (AgentInvocationError agentInvocationError) { System.out.println("onAgentInvocationError: " + "[agentName: " + agentInvocationError.agentName() + " agentId: " + agentInvocationError.agentId() + " agentInput: " + agentInvocationError.inputs() + " error: " + agentInvocationError.error().getMessage() + "]" ); } @Override public void afterAgenticScopeCreated (AgenticScope agenticScope) { System.out.println("afterAgenticScopeCreated: " + "[agenticScope: " + agenticScope.toString() + "]" ); } @Override public void beforeAgenticScopeDestroyed (AgenticScope agenticScope) { System.out.println("beforeAgenticScopeDestroyed: " + "[agenticScope: " + agenticScope.toString() + "]" ); } @Override public void beforeAgentToolExecution (BeforeAgentToolExecution beforeAgentToolExecution) { ToolExecutionRequest request = beforeAgentToolExecution.toolExecution().request(); System.out.println("beforeAgentToolExecution: " + "[toolId: " + request.id() + " toolName: " + request.name() + " toolArguments: " + request.arguments() + "]" ); } @Override public void afterAgentToolExecution (AfterAgentToolExecution afterAgentToolExecution) { ToolExecutionRequest request = afterAgentToolExecution.toolExecution().request(); String toolResult = afterAgentToolExecution.toolExecution().result(); System.out.println("afterAgentToolExecution: " + "[toolId: " + request.id() + " toolName: " + request.name() + " toolArguments: " + request.arguments() + "toolResult: " + toolResult + "]" ); } @Override public boolean inheritedBySubagents () { return true ; } }
main 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 import dev.langchain4j.agentic.AgenticServices;import dev.langchain4j.agentic.UntypedAgent;import dev.langchain4j.model.chat.ChatModel;import dev.langchain4j.model.openai.OpenAiChatModel;import org.example.observability.listener.MyAgentListener;import java.util.Map;public class AgentMain { public static void main (String[] args) { ChatModel model = OpenAiChatModel.builder() .apiKey("api-key" ) .modelName("deepseek-chat" ) .baseUrl("https://api.deepseek.com/v1" ) .build(); sequenceAgent(model); } public static void loopAgent (ChatModel model) { StyleEditor styleEditor = AgenticServices .agentBuilder(StyleEditor.class) .chatModel(model) .outputKey("story" ) .build(); StyleScorer styleScorer = AgenticServices .agentBuilder(StyleScorer.class) .chatModel(model) .outputKey("score" ) .build(); UntypedAgent styleReviewLoop = AgenticServices .loopBuilder() .subAgents(styleScorer, styleEditor) .maxIterations(5 ) .exitCondition( agenticScope -> agenticScope.readState("score" , 0.0 ) >= 0.8 ) .build(); CreativeWriter creativeWriter = AgenticServices .agentBuilder(CreativeWriter.class) .chatModel(model) .outputKey("story" ) .build(); StyledWriter styledWriter = AgenticServices .sequenceBuilder(StyledWriter.class) .subAgents(creativeWriter, styleReviewLoop) .outputKey("story" ) .build(); String story = styledWriter.writeStoryWithStyle("龙与巫师" , "喜剧" ); System.out.println(story); } public static void sequenceAgent (ChatModel model) { CreativeWriter creativeWriter = AgenticServices .agentBuilder(CreativeWriter.class) .chatModel(model) .build(); AudienceEditor audienceEditor = AgenticServices .agentBuilder(AudienceEditor.class) .chatModel(model) .build(); StyleEditor styleEditor = AgenticServices .agentBuilder(StyleEditor.class) .chatModel(model) .build(); UntypedAgent novelCreator = AgenticServices .sequenceBuilder() .subAgents(creativeWriter, audienceEditor, styleEditor) .outputKey("story" ) .name("mainAgent" ) .listener(new MyAgentListener ()) .build(); Map<String, Object> input = Map.of( "topic" , "龙与巫师" , "style" , "奇幻" , "audience" , "年轻人" ); String story = (String) novelCreator.invoke(input); System.out.println("=========================================================" ); System.out.println(story); try { Thread.sleep(10000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } } }
测试结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 afterAgenticScopeCreated: [agenticScope: AgenticScope{memoryId='eaa34efd-f827-4956-b9a0-44380061a53d' , state={}}] beforeAgentInvocation: [agentName: mainAgent agentId: mainAgent agentInput: {audience=年轻人, style=奇幻, topic=龙与巫师}] beforeAgentInvocation: [agentName: CreativeWriter agentId: CreativeWriter$0 agentInput: {topic=龙与巫师}] afterAgentInvocation: [agentName: CreativeWriter agentId: CreativeWriter$0 agentInput: {topic=龙与巫师} agentOutput: 老巫师用最后的咒语将心脏化为水晶,嵌进了巨龙的眉心。从此龙成了巫师的堡垒,在云层间驮着他沉睡的藏书塔游荡。每当月圆,龙骨会泛起咒文的蓝光,仿佛那颗心仍在跳动。] beforeAgentInvocation: [agentName: AudienceEditor agentId: AudienceEditor$1 agentInput: {audience=年轻人, story=老巫师用最后的咒语将心脏化为水晶,嵌进了巨龙的眉心。从此龙成了巫师的堡垒,在云层间驮着他沉睡的藏书塔游荡。每当月圆,龙骨会泛起咒文的蓝光,仿佛那颗心仍在跳动。}] afterAgentInvocation: [agentName: AudienceEditor agentId: AudienceEditor$1 agentInput: {audience=年轻人, story=老巫师用最后的咒语将心脏化为水晶,嵌进了巨龙的眉心。从此龙成了巫师的堡垒,在云层间驮着他沉睡的藏书塔游荡。每当月圆,龙骨会泛起咒文的蓝光,仿佛那颗心仍在跳动。} agentOutput: 年轻的巫师用尽最后一道咒语,将心脏炼成水晶,嵌入了巨龙的眉心。 于是,龙成了他的移动堡垒,驮着那座装满禁书与秘密的尖塔,在云海之间漫游。每当满月降临,龙骸便泛起咒文的幽蓝光芒,像一颗永恒跳动的心脏,在夜空里无声闪烁。] beforeAgentInvocation: [agentName: StyleEditor agentId: StyleEditor$2 agentInput: {style=奇幻, story=年轻的巫师用尽最后一道咒语,将心脏炼成水晶,嵌入了巨龙的眉心。 于是,龙成了他的移动堡垒,驮着那座装满禁书与秘密的尖塔,在云海之间漫游。每当满月降临,龙骸便泛起咒文的幽蓝光芒,像一颗永恒跳动的心脏,在夜空里无声闪烁。}] afterAgentInvocation: [agentName: StyleEditor agentId: StyleEditor$2 agentInput: {style=奇幻, story=年轻的巫师用尽最后一道咒语,将心脏炼成水晶,嵌入了巨龙的眉心。 于是,龙成了他的移动堡垒,驮着那座装满禁书与秘密的尖塔,在云海之间漫游。每当满月降临,龙骸便泛起咒文的幽蓝光芒,像一颗永恒跳动的心脏,在夜空里无声闪烁。} agentOutput: 年轻的巫师耗尽了血脉中最后一丝魔力,将仍在搏动的心脏炼作一枚绯红水晶,亲手嵌入了远古巨龙的眉心。 契约达成的那一刻,龙瞳深处燃起了永恒的咒火。它嶙峋的脊背上,那座囚禁着禁忌知识与诸神秘密的尖塔轰然苏醒。从此,龙翼撕开云海,塔尖划破天穹,二者化为一体,在星辰的缝隙间漂流。 每逢满月,月光浸透龙骸,那些镌刻在骨骼深处的古老咒文便逐一亮起,流淌着幽蓝的辉光。那颗嵌于眉心的心核随之搏动,与塔中万卷魔典的低语同频,仿佛夜空里一座沉默而永恒的信标,照耀着凡人不可触及的旅途。] afterAgentInvocation: [agentName: mainAgent agentId: mainAgent agentInput: {audience=年轻人, style=奇幻, topic=龙与巫师} agentOutput: 年轻的巫师耗尽了血脉中最后一丝魔力,将仍在搏动的心脏炼作一枚绯红水晶,亲手嵌入了远古巨龙的眉心。 契约达成的那一刻,龙瞳深处燃起了永恒的咒火。它嶙峋的脊背上,那座囚禁着禁忌知识与诸神秘密的尖塔轰然苏醒。从此,龙翼撕开云海,塔尖划破天穹,二者化为一体,在星辰的缝隙间漂流。 每逢满月,月光浸透龙骸,那些镌刻在骨骼深处的古老咒文便逐一亮起,流淌着幽蓝的辉光。那颗嵌于眉心的心核随之搏动,与塔中万卷魔典的低语同频,仿佛夜空里一座沉默而永恒的信标,照耀着凡人不可触及的旅途。] beforeAgenticScopeDestroyed: [agenticScope: AgenticScope{memoryId='eaa34efd-f827-4956-b9a0-44380061a53d' , state={audience=年轻人, topic=龙与巫师, style=奇幻, story=年轻的巫师耗尽了血脉中最后一丝魔力,将仍在搏动的心脏炼作一枚绯红水晶,亲手嵌入了远古巨龙的眉心。 契约达成的那一刻,龙瞳深处燃起了永恒的咒火。它嶙峋的脊背上,那座囚禁着禁忌知识与诸神秘密的尖塔轰然苏醒。从此,龙翼撕开云海,塔尖划破天穹,二者化为一体,在星辰的缝隙间漂流。 每逢满月,月光浸透龙骸,那些镌刻在骨骼深处的古老咒文便逐一亮起,流淌着幽蓝的辉光。那颗嵌于眉心的心核随之搏动,与塔中万卷魔典的低语同频,仿佛夜空里一座沉默而永恒的信标,照耀着凡人不可触及的旅途。}}] ========================================================= 年轻的巫师耗尽了血脉中最后一丝魔力,将仍在搏动的心脏炼作一枚绯红水晶,亲手嵌入了远古巨龙的眉心。 契约达成的那一刻,龙瞳深处燃起了永恒的咒火。它嶙峋的脊背上,那座囚禁着禁忌知识与诸神秘密的尖塔轰然苏醒。从此,龙翼撕开云海,塔尖划破天穹,二者化为一体,在星辰的缝隙间漂流。 每逢满月,月光浸透龙骸,那些镌刻在骨骼深处的古老咒文便逐一亮起,流淌着幽蓝的辉光。那颗嵌于眉心的心核随之搏动,与塔中万卷魔典的低语同频,仿佛夜空里一座沉默而永恒的信标,照耀着凡人不可触及的旅途。 Process finished with exit code 0
参考文档: