langchain4j - agent 编排和观测

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) { }

/**
* Indicates whether this listener should be used only to the agent where it is registered (default)
* or also inherited by its subagents.
*
* @return true if the listener should be inherited by sub-agents, false otherwise
*/
default boolean inheritedBySubagents() {
return false;
}
}

需要注意 inheritedBySubagents() 方法,默认是 false,只作用于当前注册的 agent,如果是 true,则会被子 agent 继承

因此如果要同时观测主 agent 和子 agent,最好分别实现两个 listener

完整代码

  1. 用到的 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);
}
  1. 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;

/**
* @Author: xiaoen
* @Description: Agent 观测
* @Date: Created in 19:16 2026/4/2
*/
public class MyAgentListener implements AgentListener {

/**
* agent 调用前
*/
@Override
public void beforeAgentInvocation(AgentRequest agentRequest) {
System.out.println("beforeAgentInvocation: " +
"[agentName: " + agentRequest.agentName() +
" agentId: " + agentRequest.agentId() +
" agentInput: " + agentRequest.inputs() + "]");
}

/**
* agent 调用后
*/
@Override
public void afterAgentInvocation(AgentResponse agentResponse) {
System.out.println("afterAgentInvocation: " +
"[agentName: " + agentResponse.agentName() +
" agentId: " + agentResponse.agentId() +
" agentInput: " + agentResponse.inputs() +
" agentOutput: " + agentResponse.output() + "]");
}

/**
* agent 调用错误时
*/
@Override
public void onAgentInvocationError(AgentInvocationError agentInvocationError) {

System.out.println("onAgentInvocationError: " +
"[agentName: " + agentInvocationError.agentName() +
" agentId: " + agentInvocationError.agentId() +
" agentInput: " + agentInvocationError.inputs() +
" error: " + agentInvocationError.error().getMessage() + "]");
}

/**
* agenticScope 销毁后
*/
@Override
public void afterAgenticScopeCreated(AgenticScope agenticScope) {
System.out.println("afterAgenticScopeCreated: " +
"[agenticScope: " + agenticScope.toString() + "]");
}

/**
* agenticScope 销毁前
*/
@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 + "]");
}

/**
* 指示此监听器是仅作用于注册它的那个 Agent(默认行为),
* 还是也会被其子 Agent 继承。
*
* @return 如果监听器应被子 Agent 继承则返回 true,否则返回 false
*/
@Override
public boolean inheritedBySubagents() {
return true;
}

}
  1. 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);

// loopAgent(model);
}

/**
* 循环 agent
*
* @param 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);
}

/**
* 顺序 agent
*
* @param model 模型
*/
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

参考文档: