AgentScope4J 深度学习指南 - 从狼人杀项目掌握多智能体框架
以狼人杀多智能体游戏为实战案例,系统学习 AgentScope4J 的核心概念,并补充 2026 年最新的 Harness Framework、长记忆、分布式会话与多 Agent 编排能力。
前言
AgentScope4J 是阿里巴巴开源的多智能体框架,提供了完整的 Agent 开发基础设施。本文以 werewolf-hitl 项目为案例,这是一个带人机交互(HITL)的狼人杀多智能体游戏,先从项目里实际出现的通信、结构化输出、人机交互与多智能体协作讲起,再延伸到 AgentScope Java 1.0.10 之后补强的 Nacos、A2A、长记忆、可观测与 1.1 系列 Harness Framework。
学习路径:
- Msg 消息体系 — 理解通信单元
- Agent 体系 — 理解代理机制
- 工具调用与结构化输出 — 理解 LLM 交互
- WebUserInput 人机交互 — 理解异步等待
- MsgHub 多智能体通信 — 理解协作机制
- Hook 生命周期 — 理解执行拦截
- Pipeline 编排 — 理解多 Agent 组合
- Formatter、流式输出、Prompt、持久化 — 理解框架生产化基础
- A2A、MCP、RAG、多模态 — 理解扩展生态
- Harness Framework — 理解最近新增的工作区、记忆、会话、沙箱和子 Agent 运行时
一、Msg 消息体系
1.1 Msg 结构总览
Msg 是 AgentScope 的唯一通信单元。Agent 之间、Agent 与 Model 之间、Agent 与 Tool 之间,传递的都是 Msg。
┌─────────────────── Msg 结构 ───────────────────┐
│ │
│ id: UUID(自动生成) │
│ name: 发送者名字("张三"、"Moderator") │
│ role: USER / ASSISTANT / SYSTEM / TOOL │
│ content: List<ContentBlock>(内容块列表) │
│ metadata: Map<String, Object>(附带数据) │
│ timestamp: "2026-05-10 14:30:00.123" │
│ │
└────────────────────────────────────────────────┘
关键设计点:
- Msg 不可变(immutable) — 构造后不可修改,需要变更时通过 Builder 重新创建
- content 是 List<ContentBlock> — 支持多模态内容(文本、图片、音频、工具调用等)
- metadata 是 Map — 承载结构化输出、Token 用量统计等"暗数据"
1.2 MsgRole 四种角色
| MsgRole | 含义 | 狼人杀中的例子 |
|---|---|---|
SYSTEM | 系统提示词 | 每个 Agent 的人设 prompt |
USER | 用户/外部输入 | 真人玩家发言、裁判公告、其他 Agent 的话 |
ASSISTANT | AI Agent 的回复 | agent.call() 的返回值 |
TOOL | 工具执行结果 | 狼人杀中未使用 |
一个容易混淆的设计:在 MsgHub 广播时,其他 Agent 的消息到达当前 Agent 时,role 仍然是 USER。因为从当前 Agent 的视角来看,“别人说的话"就是"用户输入”。
1.3 ContentBlock 多态内容
ContentBlock 是一个 sealed class,只允许 7 种子类型:
| Block 类型 | JSON type | 用途 | 狼人杀使用 |
|---|---|---|---|
TextBlock | "text" | 纯文本 | ✅ 到处用 |
ThinkingBlock | "thinking" | Agent 推理过程 | ✅ 开启 thinking |
ImageBlock | "image" | 图片(URL/Base64) | ❌ |
AudioBlock | "audio" | 音频 | ❌(狼人杀用 TTS 模型直接生成语音,不通过 AudioBlock) |
VideoBlock | "video" | 视频 | ❌ |
ToolUseBlock | "tool_use" | 工具调用请求 | ❌ |
ToolResultBlock | "tool_result" | 工具调用结果 | ❌ |
一条 Msg 的 content 可以同时包含多个 Block。比如 LLM 的回复可能同时有 ThinkingBlock(思考过程)+ TextBlock(最终回答)。
1.4 Msg.builder() 实战用法
狼人杀项目中 Msg.builder() 只用了 3 种模式:
模式 1:最简用法 — 纯文本消息(裁判公告)
// MessageSourcePromptProvider.java:46-52
private Msg buildMsg(String text) {
return Msg.builder()
.name("Moderator Message") // 谁说的
.role(MsgRole.USER) // 角色
.content(TextBlock.builder()
.text(text).build()) // 内容
.build();
}
模式 2:人类玩家的消息(HITL 输入)
// 发言 — WerewolfWebGame.java:411-418
Msg.builder()
.name(humanPlayer.getName())
.role(MsgRole.USER)
.content(TextBlock.builder().text(humanInput).build())
.build()
// 投票 — 需要 metadata 与 AI 投票格式统一
Msg.builder()
.name(player.getName())
.role(MsgRole.USER)
.content(TextBlock.builder().text(voteTarget).build())
.metadata(Map.of(
MessageMetadataKeys.STRUCTURED_OUTPUT,
Map.of("targetPlayer", voteTarget, "reason", "")
))
.build();
模式 3:直接注入 Memory 的裁判密令
// 女巫看到受害者 — WerewolfWebGame.java:521-527
Msg victimMsg = Msg.builder()
.name("Moderator Message")
.role(MsgRole.USER)
.content(TextBlock.builder().text(victimInfo).build())
.build();
witchAgent.getMemory().addMessage(victimMsg);
1.5 Metadata — Msg 里的暗数据
metadata 字段(Map<String, Object>)承载了很多非内容信息:
| Key | 含义 | 使用场景 |
|---|---|---|
agentscope_structured_output | 结构化输出数据 | 投票、决策 |
agentscope_chat_usage | Token 用量统计 | 成本追踪 |
agentscope_generate_reason | 生成原因 | HITL 中断检测 |
二、Agent 体系
2.1 接口继承关系
Agent (接口,三合一)
/ | \
/ | \
CallableAgent StreamableAgent ObservableAgent
(能调用) (能流式) (能观察)
| |
call(msgs) observe(msg)
call(msgs, Class) ↓
↓ memory.addMessage(msg)
返回 Mono<Msg>
AgentBase (抽象基类)
/ \
/ \
StructuredOutputCapableAgent UserAgent
|
ReActAgent
Agent 接口是三合一的(Agent.java:41):
public interface Agent extends CallableAgent, StreamableAgent, ObservableAgent
| 能力 | 接口 | 核心方法 | 含义 |
|---|---|---|---|
| 调用 | CallableAgent | call(List<Msg>) → Mono<Msg> | 处理消息,生成回复 |
| 流式 | StreamableAgent | stream(List<Msg>, StreamOptions) → Flux<Event> | 流式返回执行过程 |
| 观察 | ObservableAgent | observe(Msg) → Mono<Void> | 静默接收消息,不回复 |
2.2 三种 Agent 的本质区别
| ReActAgent | UserAgent | |
|---|---|---|
| 本质 | AI 自动推理+行动 | 人机桥接器 |
| doCall() | ReAct 循环(reason→act→observe) | 调用 UserInputBase 等人输入 |
| doObserve() | memory.addMessage(msg) | 空操作(Mono.empty()) |
| 有 Memory? | ✅ 有 | ❌ 没有 |
| 有 Model? | ✅ 有 | ❌ 没有 |
| 有 Toolkit? | ✅ 有(可以是空的) | ❌ 没有 |
| 创建方式 | ReActAgent.builder()...build() | UserAgent.builder()...build() |
| 狼人杀中 | AI 玩家(张三、李四…) | 真人玩家 |
2.3 ReActAgent — 核心中的核心
ReAct 是什么
ReAct = Reason(推理)+ Act(行动)。循环过程:
┌──────────────────────────┐
│ │
▼ │
┌──────────┐ │
│ Reasoning │ ← 调用 LLM 推理 │
└────┬─────┘ │
│ │
LLM 返回了工具调用? │
/ \ │
是 否 │
│ │ │
┌────▼────┐ ┌────▼────┐ │
│ Acting │ │ 返回结果 │ ← 结束循环 │
│ 执行工具 │ └─────────┘ │
└────┬────┘ │
│ │
工具结果加入 memory │
│ │
└──────────────────────────────────┘
继续下一轮 Reasoning
doCall() 的入口逻辑
ReActAgent.java:264-298,这是 call() 的实际实现:
@Override
protected Mono<Msg> doCall(List<Msg> msgs) {
Set<String> pendingIds = getPendingToolUseIds();
// 情况 1:没有待处理的工具 → 正常的 Reason→Act 循环
if (pendingIds.isEmpty()) {
addToMemory(msgs); // 输入消息加入 memory
return executeIteration(0); // 从第 0 轮开始
}
// 情况 2:有待处理工具 + 没有新输入 → 直接执行待处理工具
if (msgs == null || msgs.isEmpty()) {
return acting(0);
}
// 情况 3:有待处理工具 + 用户提供了工具结果 → 验证并加入
// ...
}
prepareMessages() — 上下文组装
ReActAgent.java:821-833:
private List<Msg> prepareMessages() {
List<Msg> messages = new ArrayList<>();
if (sysPrompt != null && !sysPrompt.trim().isEmpty()) {
messages.add(Msg.builder()
.name("system")
.role(MsgRole.SYSTEM)
.content(TextBlock.builder().text(sysPrompt).build())
.build());
}
messages.addAll(memory.getMessages()); // 所有历史消息
return messages;
}
2.4 UserAgent — 人机桥接器
UserAgent 的设计极其精简:
// UserAgent.java:101-103
@Override
protected Mono<Msg> doCall(List<Msg> msgs) {
return getUserInput(msgs, null);
}
// UserAgent.java:246-248
@Override
protected Mono<Void> doObserve(Msg msg) {
return Mono.empty(); // 空操作!不存储任何消息
}
UserAgent 不管理 Memory。 它的作用只是"等人的输入,转成 Msg 返回"。
2.5 狼人杀中 Agent 的创建
WerewolfWebGame.java:212-242:
for (int i = 0; i < roles.size(); i++) {
AgentBase agent;
boolean isHuman = (i == humanPlayerIndex);
if (isHuman) {
// 真人玩家 → UserAgent
agent = UserAgent.builder()
.name(name)
.inputMethod(userInput) // WebUserInput
.build();
} else {
// AI 玩家 → ReActAgent
agent = ReActAgent.builder()
.name(name)
.sysPrompt(prompts.getSystemPrompt(role, name)) // 人设
.model(model) // DashScopeChatModel
.memory(new InMemoryMemory()) // 对话记忆
.toolkit(new Toolkit()) // 空工具箱
.structuredOutputReminder(StructuredOutputReminder.PROMPT) // 结构化输出策略
.build();
}
}
三、工具调用与结构化输出
3.1 工具调用的真实流程
关键澄清:LLM 不会直接调用工具,只会返回"工具调用请求"。
LLM 返回: "我想调用 generate_response 工具,参数是 {...}"
↓
Agent(应用层)收到这个请求
↓
Agent 执行工具,拿到结果
↓
把结果发回给 LLM
↓
LLM 基于结果继续推理
3.2 Hook 检查机制
StructuredOutputHook.java 的 handlePostReasoning() 方法检查的是:
boolean hasCall = !msg.getContentBlocks(ToolUseBlock.class).isEmpty();
// ↑ 如果为空,说明 LLM 没有请求调用任何工具
// ↑ 如果不为空,说明 LLM 请求调用了某个工具
为什么检查 ToolUseBlock 而不是"工具是否执行"?
因为 Hook 在 PostReasoning 阶段触发,此时:
- LLM 刚返回结果
- Agent 还没执行工具
- Hook 需要检查的是"LLM 是否请求了工具调用"
3.3 getContentBlocks 的实现:Java 类型系统检查
不是字符串匹配,是 Java 类型系统(instanceof)检查。
public <T extends ContentBlock> List<T> getContentBlocks(Class<T> blockClass) {
return content.stream()
.filter(blockClass::isInstance) // ← 关键:用 instanceof 检查
.map(b -> TypeUtils.safeCast(b, blockClass))
.collect(Collectors.toList());
}
因为 ContentBlock 是 sealed class,每个子类在创建时就有明确的 Java 类型,所以 isInstance() 检查是可靠的。
3.4 LLM 不会直接输出 JSON
关键澄清:LLM 输出的是 token 序列,不是 JSON。 API 层负责把这些 token 解析成结构化的 JSON。
DashScope API 返回的实际格式
{
"request_id": "xxx-xxx",
"output": {
"choices": [{
"message": {
"role": "assistant",
"content": "我觉得应该投张三",
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "generate_response",
"arguments": "{\"targetPlayer\":\"张三\",\"reason\":\"...\"}"
}
}]
}
}]
}
}
真实流程
调用 LLM API,传入 tools 参数
↓
LLM 生成 token 序列(包含特殊 token [TOOL_CALL_START] 和 [TOOL_CALL_END])
↓
API 层解析 token 序列,验证 JSON 格式,生成结构化 tool_calls
↓
AgentScope 解析 tool_calls,执行工具
↓
把工具结果发回给 LLM
↓
LLM 基于结果继续推理
如果 LLM 输出的不是有效 JSON?
API 层会处理。 有几种情况:
- LLM 输出的 arguments 不是有效 JSON → API 层会尝试修复或报错
- LLM 输出的 arguments 不符合 schema → API 层不验证 schema,由工具自己验证
- LLM 不想调用工具,只输出文本 → AgentScope 的 StructuredOutputHook 检测到没有工具调用,触发重试
3.5 结构化输出的完整机制
核心思路:把 JSON 验证问题转化为工具调用问题
传统方案(不推荐):
String response = llm.call("请按以下格式输出: {\"name\": \"xxx\", \"age\": n}");
try {
JSONObject json = new JSONObject(response);
} catch (JSONException e) {
// 格式不对,重试?很难让 LLM 理解错在哪里
}
AgentScope 的方案:
- 生成 JSON Schema
- 注册临时工具
generate_response,参数 schema 就是目标格式 - LLM 调用工具时,底层会验证参数是否符合 schema
- 如果不符合 → 重试/强制 tool_choice
完整流程
agent.call("请投票", VoteModel.class)
│
├─ 生成 JSON Schema
├─ 注册临时工具 generate_response
├─ 注册 StructuredOutputHook
│
└─ 进入 ReAct 循环
│
├─ reasoning: LLM 返回 ToolUseBlock("generate_response", {...})
│ └─ Hook 检查:✅ 调用了 generate_response
│
├─ acting: 执行工具,拿到结果
│
└─ 返回最终结果(metadata 含结构化数据)
两种重试策略
| 策略 | 第一次失败后 | 第二次失败后 | 第三次失败后 |
|---|---|---|---|
TOOL_CHOICE | 强制 tool_choice = "generate_response" | 再强制一次 | 抛异常 |
PROMPT | 提醒"请调用工具" | 再提醒一次 | 抛异常 |
四、WebUserInput 人机交互
4.1 核心挑战
狼人杀项目的核心挑战是:AI Agent 的 call() 是同步阻塞的,但人的输入要通过 REST API 异步传来。
4.2 Sinks.One 的等待/唤醒机制
游戏线程(阻塞) REST 线程(异步)
│ │
▼ │
waitForInput("VOTE") │
│ 创建 Sinks.One<String> │
│ 存入 Map<"vote_123", sink> │
│ emitWaitUserInput("VOTE") │ ← 前端收到"请投票"提示
│ sink.asMono().block() │ ← 游戏线程阻塞在这里!
│ ↓ 挂起 │
│ ↓ │
│ ↓ 用户投票,POST /api/game/input
│ ↓ │
│ ↓ submitInput("VOTE", "张三")
│ ↓ │
│ ↓ sink.tryEmitValue("张三") ← 发射值!
│ ↓ │
│ ← Mono 拿到 "张三",解除阻塞 │
│ 继续执行游戏逻辑 │
4.3 核心代码
WebUserInput.waitForInput()(等输入):
// 1. 创建一个只能发射一次的发布者
Sinks.One<String> sink = Sinks.one();
// 2. 用唯一 key 存起来(inputType + timestamp)
String key = "vote_" + System.currentTimeMillis();
pendingInputs.put(key, sink);
// 3. 发 SSE 事件告诉前端"请让用户投票"
emitter.emitWaitUserInput("VOTE", "请选择要投出的玩家", candidates);
// 4. 把 Mono 包装返回,调用方用 .block() 阻塞等待
return sink.asMono();
WerewolfWebController.submitInput()(唤醒):
@PostMapping("/api/game/input")
public Map<String, Object> submitInput(@RequestBody Map<String, String> request) {
String inputType = request.get("inputType"); // "VOTE"
String content = request.get("content"); // "张三"
// 找到对应的 sink,发射值
WebUserInput.matchingSink(inputType, content);
// 内部调用: sink.tryEmitValue("张三")
return Map.of("success", true);
}
4.4 Sinks.One 的关键特性
| 特性 | 说明 |
|---|---|
| 只能发射一次 | tryEmitValue() 只能调用一次,第二次会返回 FAIL_TERMINATED |
| 无缓冲 | 没人等时发射,值会丢失(但我们的场景是先等后发) |
| 线程安全 | 可以从任意线程调用 tryEmitValue() |
| block() 会挂起 | 调用 asMono().block() 会释放当前线程,不占 CPU |
五、MsgHub 多智能体通信
5.1 MsgHub 解决什么问题?
没有 MsgHub 时,多 Agent 通信是这样的:
// 狼人讨论:3 个狼人互相交流
Msg a1 = wolfA.call().block();
wolfB.observe(a1).block();
wolfC.observe(a1).block();
Msg b1 = wolfB.call().block();
wolfA.observe(b1).block();
wolfC.observe(b1).block();
Msg c1 = wolfC.call().block();
wolfA.observe(c1).block();
wolfB.observe(c1).block();
// ... 每个 Agent 的消息都要手动广播给其他所有 Agent
有 MsgHub 后:
try (MsgHub hub = MsgHub.builder()
.name("WolfDiscussion")
.participants(wolfA, wolfB, wolfC)
.enableAutoBroadcast(true) // 自动广播
.build()) {
hub.enter().block();
wolfA.call().block(); // A 的回复自动广播给 B 和 C
wolfB.call().block(); // B 的回复自动广播给 A 和 C
wolfC.call().block(); // C 的回复自动广播给 A 和 B
}
5.2 核心机制:订阅关系
MsgHub 的核心是 resetSubscribers()(MsgHub.java:323-333):
private void resetSubscribers() {
if (enableAutoBroadcast) {
for (AgentBase agent : participants) {
// 每个 Agent 订阅其他所有 Agent(不订阅自己)
List<AgentBase> others = participants.stream()
.filter(a -> !a.equals(agent))
.collect(Collectors.toList());
agent.resetSubscribers(name, others);
}
}
}
图解:
参与者: [WolfA, WolfB, WolfC]
WolfA 的订阅者: [WolfB, WolfC] ← A 的消息会发给 B 和 C
WolfB 的订阅者: [WolfA, WolfC] ← B 的消息会发给 A 和 C
WolfC 的订阅者: [WolfA, WolfB] ← C 的消息会发给 A 和 B
5.3 autoBroadcast 的工作原理
当 enableAutoBroadcast=true 时,每个 Agent 的 call() 返回值会自动广播给订阅者。
在 AgentBase.call() 内部(简化版):
public Mono<Msg> call(List<Msg> msgs) {
return doCall(msgs)
.flatMap(result -> {
// 自动广播给订阅者
if (autoBroadcastEnabled) {
return broadcastToSubscribers(result).thenReturn(result);
}
return Mono.just(result);
});
}
5.4 observe(msg) 做了什么?
对于 ReActAgent,observe() 的实现(ReActAgent.java:1048-1050):
@Override
protected Mono<Void> doObserve(Msg msg) {
return memory.addMessage(msg); // 就这一行!把消息加入记忆
}
是的,就是添加记忆。 但关键在于:什么时候添加,添加后怎么用。
5.5 Agent 调用 LLM 前怎么知道对方的消息?
答案是 通过 Memory。完整时序图:
时间线:
─────────────────────────────────────────────────────────────────────────────
MsgHub.enter()
│
├─ WolfA.observe(announcement) → WolfA.memory = [announcement]
├─ WolfB.observe(announcement) → WolfB.memory = [announcement]
└─ WolfC.observe(announcement) → WolfC.memory = [announcement]
WolfA.call()
│
├─ prepareMessages()
│ │
│ └─ messages = [system_prompt, ...WolfA.memory.getMessages()]
│ = [system_prompt, announcement]
│
├─ model.stream(messages) → LLM 返回 "我觉得应该杀张三"
│
└─ broadcastToSubscribers("我觉得应该杀张三")
│
├─ WolfB.observe("我觉得应该杀张三")
│ → WolfB.memory = [announcement, "我觉得应该杀张三"]
│
└─ WolfC.observe("我觉得应该杀张三")
→ WolfC.memory = [announcement, "我觉得应该杀张三"]
WolfB.call()
│
├─ prepareMessages()
│ │
│ └─ messages = [system_prompt, ...WolfB.memory.getMessages()]
│ = [system_prompt, announcement, "我觉得应该杀张三"]
│ ↑ WolfA 的发言!
│
├─ model.stream(messages) → LLM 返回 "同意,我也觉得应该杀张三"
│
└─ broadcastToSubscribers("同意,我也觉得应该杀张三")
│
├─ WolfA.observe("同意,我也觉得应该杀张三")
│ → WolfA.memory = [announcement, "我觉得应该杀张三", "同意,我也觉得应该杀张三"]
│
└─ WolfC.observe("同意,我也觉得应该杀张三")
→ WolfC.memory = [announcement, "我觉得应该杀张三", "同意,我也觉得应该杀张三"]
5.6 前面调用的 Agent 怎么知道后面的人的消息?
答案是:不知道!
这是 MsgHub 的时序设计:
WolfA.call() 时:
- WolfA.memory = [announcement]
- WolfA 不知道 WolfB 和 WolfC 会说什么(因为还没发生)
WolfB.call() 时:
- WolfB.memory = [announcement, WolfA 的发言]
- WolfB 不知道 WolfC 会说什么(因为还没发生)
WolfC.call() 时:
- WolfC.memory = [announcement, WolfA 的发言, WolfB 的发言]
- WolfC 知道所有前面的发言
这是因果关系的保证: Agent 只能看到在它调用之前已经发生的对话,不能看到未来。
5.7 enableAutoBroadcast 的两种模式
| 模式 | 行为 | 适用场景 | 狼人杀中 |
|---|---|---|---|
true | Agent 的 call() 返回值自动广播给其他参与者 | 公开讨论 | 白天讨论、投票 |
false | 需要手动调用 hub.broadcast(msg) | 受控通信 | 狼人讨论 |
5.8 狼人杀中的 3 个 MsgHub 实例
1. 狼人讨论 MsgHub(WerewolfWebGame.java:381-428)
MsgHub wolfHub = MsgHub.builder()
.name("WolfDiscussion")
.participants(wolfAgents) // 只有狼人参与
.announcement(wolfAnnouncement) // "狼人请讨论要杀谁"
.enableAutoBroadcast(false) // ← 关闭自动广播!
.build();
2. 白天讨论 MsgHub(WerewolfWebGame.java:825-898)
MsgHub dayHub = MsgHub.builder()
.name("DayDiscussion")
.participants(aliveAgents) // 所有活着的玩家
.announcement(nightResultMsg) // "昨晚死的是..."
.enableAutoBroadcast(true) // ← 开启自动广播
.build();
3. 投票 MsgHub(WerewolfWebGame.java:909-998)
MsgHub voteHub = MsgHub.builder()
.name("Voting")
.participants(aliveAgents) // 所有活着的玩家
.announcement(voteStartMsg) // "请投票"
.enableAutoBroadcast(true) // ← 开启自动广播
.build();
5.9 MsgHub 的生命周期
try (MsgHub hub = MsgHub.builder()
.name("DayDiscussion")
.participants(alice, bob, charlie)
.announcement(announcementMsg) // 进入时的公告
.enableAutoBroadcast(true)
.build()) {
// 1. enter() — 建立订阅关系,广播公告
hub.enter().block();
// 2. 每个 Agent 独立调用 call()
// - A 的回复自动广播给 B 和 C
// - B 的回复自动广播给 A 和 C
// - ...
for (Player p : alivePlayers) {
p.getAgent().call().block();
}
} // 3. close() — 清理订阅关系
六、Hook 生命周期
6.1 Hook 是什么?
Hook 是 Agent 执行过程中的拦截器,可以在关键节点插入自定义逻辑。
核心接口:
public interface Hook {
<T extends HookEvent> Mono<T> onEvent(T event);
default int priority() { return 100; }
}
Hook 的设计类似于 Servlet Filter 或 Spring Interceptor,在 Agent 执行流程的特定阶段进行拦截和处理。
6.2 5 个生命周期事件
ReActAgent 的执行过程中有 5 个 Hook 可以拦截的事件:
| 事件 | 触发时机 | 可修改内容 |
|---|---|---|
PreCallEvent | call() 开始前 | inputMessages |
PreReasoningEvent | LLM 推理前 | inputMessages、generateOptions |
PostReasoningEvent | LLM 推理后 | reasoningMessage,支持 stopAgent() 和 gotoReasoning() |
PreActingEvent | 工具执行前 | toolUse |
PostActingEvent | 工具执行后 | toolResult,支持 stopAgent() |
执行流程图:
agent.call(msgs)
│
├─ PreCallEvent ──────────── 可修改输入消息
│
├─ PreReasoningEvent ─────── 可修改推理输入
│
├─ LLM 推理
│
├─ PostReasoningEvent ────── 可修改推理结果,可中断 Agent
│
├─ PreActingEvent ────────── 可修改工具调用
│
├─ 执行工具
│
├─ PostActingEvent ───────── 可修改工具结果,可中断 Agent
│
└─ 返回结果
6.3 事件分类
| 类别 | 事件 | 可修改 |
|---|---|---|
| 可修改 | PreCallEvent, PreReasoningEvent, PostReasoningEvent, PreActingEvent, PostActingEvent, PostCallEvent | 有 setter |
| 只读 | ReasoningChunkEvent, ActingChunkEvent, ErrorEvent | 无 setter |
可修改事件允许 Hook 改变 Agent 的行为,比如:
- 修改输入消息(添加上下文、过滤敏感词)
- 修改推理结果(强制工具调用、格式化输出)
- 中断 Agent 执行(
stopAgent())
只读事件用于监控和日志,不能改变 Agent 行为。
6.4 Hook 优先级
Hook 的 priority() 方法决定执行顺序,数值越小越先执行:
| 优先级范围 | 用途 | 示例 |
|---|---|---|
| 0-50 | 系统级 | 认证、安全检查 |
| 51-100 | 高优先级 | 验证、预处理 |
| 101-500 | 普通 | 业务逻辑 |
| 501-1000 | 低优先级 | 日志、监控 |
public class SecurityHook implements Hook {
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
// 安全检查逻辑
return Mono.just(event);
}
@Override
public int priority() {
return 10; // 高优先级,先执行
}
}
6.5 狼人杀中的使用
狼人杀项目没有自定义 Hook,但框架内置的 StructuredOutputHook 在幕后工作:
// StructuredOutputHook 的核心逻辑(简化版)
public class StructuredOutputHook implements Hook {
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (event instanceof PostReasoningEvent) {
// 检查 LLM 是否调用了 generate_response 工具
// 如果没有调用,触发重试机制
}
return Mono.just(event);
}
}
这个 Hook 在结构化输出流程中自动注册,确保 LLM 按照指定格式返回结果。
七、Pipeline 编排
7.1 Pipeline 解决什么问题?
当需要组合多个 Agent 协作时,Pipeline 提供了简洁的编排方式。
没有 Pipeline 时:
Msg result1 = agentA.call(input).block();
Msg result2 = agentB.call(result1).block();
Msg result3 = agentC.call(result2).block();
有 Pipeline 时:
Msg result = Pipelines.sequential(List.of(agentA, agentB, agentC), input).block();
7.2 两种 Pipeline 类型
SequentialPipeline — 串行流水线
Input → Agent1 → Agent2 → ... → Output
// 语法
Msg result = Pipelines.sequential(List.of(agentA, agentB), input).block();
// 等价于
Msg temp = agentA.call(input).block();
Msg result = agentB.call(temp).block();
适用场景:
- 有依赖关系的任务链
- 需要前一个 Agent 的输出作为后一个 Agent 的输入
- 流水线式处理(如:提取 → 分析 → 生成)
FanoutPipeline — 并行扇出
┌→ Agent1 → Output1
Input → ├→ Agent2 → Output2
└→ Agent3 → Output3
// 语法
List<Msg> results = Pipelines.fanout(List.of(agentA, agentB, agentC), input).block();
// results = [agentA 的结果, agentB 的结果, agentC 的结果]
适用场景:
- 多个 Agent 独立处理同一输入
- 需要汇总多个视角的结果
- 并行执行提高效率
7.3 两种执行模式
Pipeline 支持两种执行模式,通过 ExecutionMode 配置:
| 模式 | 行为 | 实现方式 |
|---|---|---|
Concurrent | 真并行 | subscribeOn(scheduler) |
Sequential | 独立但有序 | Flux.concat() |
Concurrent 模式:
// 多个 Agent 在不同线程并行执行
Flux.fromIterable(agents)
.subscribeOn(Schedulers.boundedElastic())
.flatMap(agent -> agent.call(input))
Sequential 模式:
// 多个 Agent 按顺序执行,但不共享状态
Flux.fromIterable(agents)
.concatMap(agent -> agent.call(input))
7.4 错误处理
两种 Pipeline 对错误的处理策略不同:
SequentialPipeline
任何一个失败,整个 Pipeline 失败。
// AgentA 成功,AgentB 失败
Msg result = Pipelines.sequential(List.of(agentA, agentB, agentC), input).block();
// 抛出 AgentB 的异常,AgentC 不会执行
FanoutPipeline
所有 Agent 都会执行,失败时抛出 CompositeAgentException。
try {
List<Msg> results = Pipelines.fanout(List.of(agentA, agentB, agentC), input).block();
} catch (CompositeAgentException e) {
// e.getExceptions() 包含所有失败的异常
// 成功的 Agent 结果仍然可用
}
7.5 狼人杀为什么不用 Pipeline?
狼人杀项目没有使用 Pipeline,原因是:
MsgHub 更适合平等对话
- 狼人杀中的 Agent 是平等的参与者,不是流水线上的工人
- MsgHub 提供了广播机制,所有 Agent 都能收到其他人的发言
没有依赖关系
- 每个 Agent 的发言不依赖于其他 Agent 的输出
- 每个 Agent 基于自己的记忆(Memory)独立决策
需要 HITL(人机交互)
- Pipeline 不支持在执行过程中等待人类输入
- 狼人杀需要在每个 Agent 执行前/后可能等待真人玩家操作
Pipeline 更适合的场景:
- RAG 检索增强生成(检索 → 重排 → 生成)
- 多步推理(分析 → 规划 → 执行)
- 并行评估(多个评估 Agent 同时打分)
八、Formatter 格式化
8.1 为什么需要 Formatter?
AgentScope 使用统一的 Msg 格式,但各家 LLM API 的格式都不同。Formatter 自动完成 Msg ↔ LLM API 格式的双向转换。
8.2 4 个核心职责
- 消息格式化:Msg → 厂商格式(format 方法)
- 响应解析:厂商响应 → ChatResponse(parseResponse 方法)
- 参数应用:temperature、max_tokens 等(applyOptions 方法)
- 工具应用:ToolSchema → 厂商格式(applyTools 方法)
8.3 完整转换流程
1. Agent 准备消息(prepareMessages)
2. Formatter.format() 转换为厂商格式
3. applyTools() 应用工具 schema
4. applyOptions() 应用生成参数
5. 调用 HTTP API
6. 厂商响应返回
7. Formatter.parseResponse() 解析响应
8.4 ThinkingBlock 的处理
- ThinkingBlock 存储在 Memory,但不发给 LLM API
- 原因:节省 token、避免暴露推理过程、保持对话历史整洁
8.5 DashScopeMultiAgentFormatter 的消息合并
- 把多条消息合并为一条带
<history>标签的消息 - 优点:减少消息数量、节省 token、保持对话历史完整性
8.6 各家 Formatter 的差异
| Formatter | 特点 |
|---|---|
| DashScopeMultiAgentFormatter | 消息合并,适合多 Agent 对话 |
| DashScopeChatFormatter | 标准 DashScope 格式 |
| OpenAIChatFormatter | 标准 OpenAI 格式 |
| AnthropicChatFormatter | System prompt 不在 messages 中 |
| GeminiChatFormatter | Google 特殊格式 |
8.7 狼人杀中的使用
model = DashScopeChatModel.builder()
.formatter(new DashScopeMultiAgentFormatter()) // 一行创建,自动工作
.build();
8.8 总结
Formatter 是 AgentScope 的翻译层,屏蔽各家 LLM API 的差异,提供统一的接口。
九、流式输出
9.1 为什么需要流式输出?
LLM 生成响应需要时间(几秒到几十秒),流式输出让用户实时看到 LLM 生成的内容,提升用户体验。
9.2 核心接口
StreamableAgent.stream()返回Flux<Event>Event包含type(事件类型)、message(消息内容)、isLast(是否最后一个)StreamOptions配置事件类型、增量模式、Reasoning 过滤等
9.3 EventType 枚举
| 事件类型 | 含义 |
|---|---|
REASONING | LLM 推理过程 |
TOOL_RESULT | 工具执行结果 |
SUMMARY | 总结生成 |
9.4 isLast 字段的含义
| 值 | 含义 | 使用场景 |
|---|---|---|
true | 最终结果 | 可以保存到数据库 |
false | 中间块 | 用于实时更新 UI |
9.5 增量模式 vs 累积模式
| 模式 | 行为 | 配置 |
|---|---|---|
| 增量模式 | 每次只返回新内容 | incremental=true |
| 累积模式 | 每次返回所有累积内容 | incremental=false |
9.6 底层原理:SSE(Server-Sent Events)
流式输出的底层是通过 HTTP 的 SSE 协议实现的:
- 客户端发送 HTTP 请求,设置请求头
X-DashScope-SSE: enable - 服务器返回持续的响应流,每个事件以
"data:"前缀开头 - 遇到
"[DONE]"标记表示结束
9.7 实现层次
1. Agent.stream() — 返回 Event 流
2. Model.stream() — 调用 HttpClient 发送 HTTP 请求
3. HttpClient.stream() — 设置 SSE 请求头,解析 SSE 流
4. HttpTransport.stream()— 使用 JDK HttpClient 发送异步请求
5. JDK HttpClient — sendAsync() 获取 InputStream
9.8 为什么用 SSE 而不是 WebSocket?
| 协议 | 方向 | 复杂度 | 重连 | 适用场景 |
|---|---|---|---|---|
| SSE | 单向(服务器→客户端) | 简单 | 自动 | LLM 流式输出 |
| WebSocket | 双向 | 复杂 | 手动 | 聊天室、游戏 |
LLM 流式输出只需要单向推送,SSE 更简单高效。
9.9 狼人杀中的使用
狼人杀项目关闭了流式输出(stream=false),因为它需要等待完整结果再继续游戏逻辑。
9.10 总结
流式输出是 AgentScope 的实时反馈机制,通过 SSE 协议实现服务器到客户端的单向推送,让用户实时看到 LLM 生成的内容。
十、阶段性总结:狼人杀项目覆盖的核心能力
10.1 核心概念回顾
| 概念 | 核心要点 |
|---|---|
| Msg | 统一消息格式,ContentBlock 支持多模态,metadata 承载结构化数据 |
| Agent | ReActAgent = Memory + Model + Toolkit,UserAgent = 人机桥接器 |
| 工具调用 | LLM 返回 ToolUseBlock(请求),Agent 执行工具,结果发回 LLM |
| 结构化输出 | 把 JSON 验证问题转化为工具调用问题,用 tool_choice 强制保证格式 |
| WebUserInput | Sinks.One + block() 阻塞游戏线程,REST 端点 tryEmitValue() 唤醒 |
| MsgHub | 自动广播机制,订阅关系管理,因果时序保证 |
| Hook | 执行拦截器,5 个生命周期事件,优先级控制,可修改/只读两类 |
| Pipeline | SequentialPipeline 串行依赖,FanoutPipeline 并行扇出,两种执行模式 |
10.2 学习路径建议
阶段 1:狼人杀项目覆盖的核心概念(当前)
- ✅ Msg 消息体系
- ✅ Agent 体系
- ✅ Structured Output
- ✅ 工具调用
- ✅ WebUserInput
- ✅ MsgHub
- ✅ Hook 生命周期
- ✅ Pipeline 编排
阶段 2:框架核心补充
- Formatter 格式化(Msg → 各家 LLM API 格式的转换)
阶段 3:能力扩展
- RAG(检索增强生成)
- 多模态支持
- 流式输出
阶段 4:生产化
- 持久化与状态管理
- Prompt 管理
基础案例基于 AgentScope Java 的 werewolf-hitl 示例,后半部分补充 AgentScope Java 1.0.10 至 1.1.0-RC1 的公开更新。 代码路径:agentscope-java/agentscope-examples/werewolf-hitl
十一、Prompt 管理
11.1 Prompt 管理解决什么问题?
Agent 需要各种 Prompt(系统提示、任务提示、结果提示等),这些 Prompt 的管理面临以下挑战:
- 集中管理:所有 Prompt 文本需要集中管理,而不是散落在各个代码文件中
- 国际化支持:同一个 Prompt 需要支持多种语言(中文、英文等)
- 动态参数:Prompt 中需要插入变量(如玩家名、游戏状态等)
11.2 PromptProvider 接口
PromptProvider 接口定义了所有 Prompt 的获取方法,是 Prompt 管理的核心抽象:
public interface PromptProvider {
// 系统提示(角色人设)
String getSystemPrompt(Role role, String playerName);
// 任务提示(指令)
String createDiscussionPrompt(GameState state);
// 结果提示(反馈)
String createSeerResultPrompt(Player target);
}
| 方法 | 类型 | 用途 | 示例 |
|---|---|---|---|
getSystemPrompt() | 系统提示 | 定义 Agent 的人设和行为准则 | “你是狼人,你的目标是…” |
createDiscussionPrompt() | 任务提示 | 告诉 Agent 当前要做什么 | “请讨论今晚要杀谁” |
createSeerResultPrompt() | 结果提示 | 告诉 Agent 执行结果 | “你查验的结果是:狼人” |
11.3 MessageSourcePromptProvider 实现
MessageSourcePromptProvider 是使用 Spring MessageSource 实现的国际化 Prompt 提供者:
@Component
public class MessageSourcePromptProvider implements PromptProvider {
private final MessageSource messageSource;
@Override
public String getSystemPrompt(Role role, String playerName) {
String key = "prompt.system." + role.name().toLowerCase();
return messageSource.getMessage(key, new Object[]{playerName}, LocaleContextHolder.getLocale());
}
@Override
public String createDiscussionPrompt(GameState state) {
return messageSource.getMessage(
"prompt.discussion",
new Object[]{state.getAlivePlayers()},
LocaleContextHolder.getLocale()
);
}
}
核心优势:
- 使用 Spring 的
MessageSource机制,自动根据 Locale 加载对应语言 - 支持参数替换(
{0},{1}占位符) - 与 Spring 生态无缝集成
11.4 国际化消息文件
Prompt 文本存储在 properties 文件中,按语言区分:
messages_zh_CN.properties(中文):
prompt.system.werewolf=你是狼人 {0},你的目标是在天亮前杀死所有村民。
prompt.system.villager=你是村民 {0},你的目标是找出并投票淘汰所有狼人。
prompt.discussion=当前存活玩家:{0},请发表你的看法。
prompt.seer.result=你查验 {0} 的结果是:{1}。
messages_en_US.properties(英文):
prompt.system.werewolf=You are werewolf {0}, your goal is to kill all villagers before dawn.
prompt.system.villager=You are villager {0}, your goal is to find and vote out all werewolves.
prompt.discussion=Current alive players: {0}, please share your thoughts.
prompt.seer.result=Your investigation on {0} shows: {1}.
参数替换规则:
{0}→ 第一个参数{1}→ 第二个参数- 以此类推
11.5 Prompt 的 3 种类型
Agent 在执行过程中需要 3 种类型的 Prompt:
| 类型 | 作用 | 时机 | 示例 |
|---|---|---|---|
| 系统提示(System Prompt) | 定义 Agent 的人设和行为准则 | Agent 创建时 | “你是狼人,擅长伪装和欺骗” |
| 任务提示(Task Prompt) | 告诉 Agent 当前要做什么 | 每次调用时 | “请讨论今晚要杀谁” |
| 结果提示(Result Prompt) | 告诉 Agent 执行结果 | 工具执行后 | “你查验的结果是:张三是狼人” |
三者的配合:
System Prompt(人设) + Task Prompt(任务) + 历史记忆 → LLM 推理 → 输出
↓
工具执行 → Result Prompt(反馈)
↓
继续推理或返回结果
11.6 狼人杀中的使用
狼人杀项目完整实现了 PromptProvider,使用 Spring MessageSource 支持中英文:
// WerewolfWebGame.java:212-242
for (int i = 0; i < roles.size(); i++) {
Role role = roles.get(i);
String name = playerNames.get(i);
// 从 PromptProvider 获取系统提示
String systemPrompt = promptProvider.getSystemPrompt(role, name);
AgentBase agent = ReActAgent.builder()
.name(name)
.sysPrompt(systemPrompt) // 使用集中管理的 Prompt
.model(model)
.memory(new InMemoryMemory())
.build();
}
实际使用场景:
- 狼人杀人任务:
createWolfKillPrompt()生成"请讨论今晚要杀谁"的提示 - 预言家查验:
createSeerCheckPrompt()生成"请查验一名玩家"的提示 - 投票阶段:
createVotePrompt()生成"请投票淘汰一名玩家"的提示
11.7 总结
Prompt 管理是 AgentScope 的配置化机制:
- 集中管理:所有 Prompt 文本集中在 properties 文件中,便于维护和修改
- 国际化支持:通过 Spring
MessageSource自动支持多语言切换 - 动态参数:使用占位符
{0},{1}等插入运行时变量 - 类型清晰:系统提示、任务提示、结果提示三种类型各司其职
设计原则:Prompt 文本不应该硬编码在代码中,而应该通过配置文件管理,这样可以:
- 快速调整 Agent 行为(修改 properties 文件即可)
- 支持多语言部署
- 便于 A/B 测试不同的 Prompt 策略
十二、持久化与状态管理
12.1 持久化解决什么问题?
Agent 的状态默认存储在内存中,这带来了一个严重问题:应用重启后,所有状态都会丢失。
内存存储的问题:
应用启动 → Agent 创建(内存) → 对话进行中 → 应用崩溃/重启
↓
所有状态丢失!
对话历史、记忆、配置全部消失
Session 机制的解决方案:
应用启动 → Agent 创建 → 对话进行中 → 保存到外部存储
↓
应用崩溃/重启 → 从外部存储加载 → 继续对话
12.2 核心接口
AgentScope 的持久化体系由 4 个核心接口组成:
State — 可持久化的状态对象
public interface State extends Serializable {
// 标记接口,实现此接口的对象可以被持久化
}
Session — 会话存储接口
public interface Session {
// 保存状态
Mono<Void> save(SessionKey key, State state);
// 加载状态
<T extends State> Mono<T> get(SessionKey key, Class<T> stateClass);
// 删除状态
Mono<Void> delete(SessionKey key);
}
StateModule — 有状态组件接口
public interface StateModule {
// 保存当前状态到 State 对象
State saveTo();
// 从 State 对象加载状态
void loadFrom(State state);
}
StatePersistence — 状态持久化配置
public class StatePersistence {
private final Session session;
private final SessionKey sessionKey;
// 是否自动保存(在 Agent 调用后自动保存)
private boolean autoSave = true;
// 是否自动加载(在 Agent 创建时自动加载)
private boolean autoLoad = true;
}
12.3 Session 的实现
AgentScope 提供了多种 Session 实现,适应不同的存储需求:
| 实现 | 存储方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
JsonSession | JSON 文件 | 简单、无需外部依赖 | 性能一般、不支持分布式 | 开发测试 |
RedisSession | Redis | 高性能、支持分布式 | 需要 Redis 服务 | 生产环境 |
MysqlSession | MySQL | 可靠、支持复杂查询 | 性能相对较低 | 需要持久化查询 |
JsonSession 示例:
Session session = new JsonSession("/data/agent-states");
// 保存
SessionKey key = new SessionKey("user-123", "chat-456");
State state = agent.saveTo();
session.save(key, state).block();
// 加载
State loadedState = session.get(key, State.class).block();
if (loadedState != null) {
agent.loadFrom(loadedState);
}
RedisSession 示例:
Session session = new RedisSession(redisTemplate);
// 使用方式与 JsonSession 相同
// 底层使用 Redis 的 Hash 结构存储
12.4 完整使用流程
使用持久化的完整流程包括 5 个步骤:
// 1. 创建 Session 和 SessionKey
Session session = new JsonSession("/data/states");
SessionKey sessionKey = new SessionKey(userId, conversationId);
// 2. 创建 Agent(带 StatePersistence 配置)
StatePersistence persistence = StatePersistence.builder()
.session(session)
.sessionKey(sessionKey)
.autoSave(true)
.autoLoad(true)
.build();
ReActAgent agent = ReActAgent.builder()
.name("assistant")
.model(model)
.memory(new InMemoryMemory())
.statePersistence(persistence)
.build();
// 3. 加载状态(如果存在)
// autoLoad=true 时自动执行
// agent.loadFrom(session.get(sessionKey, State.class).block());
// 4. 使用 Agent
Msg response = agent.call(userMessage).block();
// 5. 保存状态
// autoSave=true 时自动执行
// session.save(sessionKey, agent.saveTo()).block();
自动保存的触发时机:
- Agent 的
call()方法执行完成后 - Agent 的
observe()方法执行完成后 - 显式调用
agent.save()方法
12.5 狼人杀中的使用
狼人杀项目没有使用持久化,原因如下:
- 游戏是一次性的:每局游戏独立,不需要跨会话保存状态
- 状态生命周期短:一局游戏通常在 30 分钟内结束
- 无历史对话需求:游戏结束后,对话历史没有保留价值
- 简化实现:使用内存存储,避免外部依赖
// 狼人杀项目直接使用内存存储
ReActAgent agent = ReActAgent.builder()
.name(name)
.sysPrompt(systemPrompt)
.model(model)
.memory(new InMemoryMemory()) // 内存存储,不持久化
.toolkit(new Toolkit())
.build();
何时需要持久化:
- 对话机器人(需要记住用户偏好)
- 客服系统(需要跨会话保持上下文)
- 长期任务(需要分多次执行)
12.6 总结
持久化与状态管理是 AgentScope 的生产化机制:
- 解决的核心问题:应用重启后状态丢失
- 核心接口:State、Session、StateModule、StatePersistence
- 多种存储实现:JsonSession(文件)、RedisSession(Redis)、MysqlSession(MySQL)
- 灵活配置:支持自动保存/加载,支持自定义存储实现
设计原则:
- 透明持久化:通过 StatePersistence 配置,业务代码无感知
- 可插拔存储:Session 接口抽象,可以轻松切换存储实现
- 状态可序列化:State 接口确保对象可以被正确序列化和反序列化
生产环境建议:
- 开发测试:使用 JsonSession(简单、无依赖)
- 生产环境:使用 RedisSession(高性能、支持分布式)
- 需要查询:使用 MysqlSession(支持 SQL 查询)
十三、PlanNotebook 任务规划
13.1 PlanNotebook 是什么?
PlanNotebook 是 Agent 的任务计划管理工具,让 Agent 能够创建复杂任务的执行计划、分解为多个子任务、跟踪执行状态。
核心能力:
- 创建计划(Plan)并设定预期结果
- 将计划分解为多个子任务(SubTask)
- 按顺序执行子任务,自动跟踪状态
- 支持历史计划恢复,中断后可继续执行
13.2 核心概念
Plan(计划)
Plan 是任务的顶层容器,包含以下字段:
| 字段 | 类型 | 含义 |
|---|---|---|
name | String | 计划名称 |
description | String | 计划描述 |
expectedOutcome | String | 预期结果 |
subtasks | List<SubTask> | 子任务列表 |
state | PlanState | 计划状态(ACTIVE/DONE/ABANDONED) |
SubTask(子任务)
SubTask 是计划的执行单元,包含以下字段:
| 字段 | 类型 | 含义 |
|---|---|---|
name | String | 子任务名称 |
description | String | 子任务描述 |
expectedOutcome | String | 预期结果 |
state | SubTaskState | 子任务状态 |
outcome | String | 实际执行结果 |
SubTaskState(子任务状态)
TODO → IN_PROGRESS → DONE
→ ABANDONED
| 状态 | 含义 |
|---|---|
TODO | 待执行 |
IN_PROGRESS | 执行中 |
DONE | 已完成 |
ABANDONED | 已放弃 |
13.3 10 个工具函数
PlanNotebook 为 Agent 提供了 10 个工具函数,覆盖计划的完整生命周期:
| 工具函数 | 用途 | 说明 |
|---|---|---|
create_plan | 创建计划 | 创建新的执行计划,包含名称、描述、预期结果 |
update_plan_info | 更新计划信息 | 修改计划的名称、描述、预期结果 |
revise_current_plan | 修订计划 | 添加、修改或删除子任务 |
update_subtask_state | 更新子任务状态 | 将子任务状态改为 TODO/IN_PROGRESS/DONE/ABANDONED |
finish_subtask | 完成子任务 | 标记当前子任务为 DONE,自动激活下一个 |
view_subtasks | 查看子任务详情 | 查看所有子任务的名称、状态、结果 |
get_subtask_count | 获取子任务数量 | 返回当前计划的子任务总数 |
finish_plan | 完成或放弃计划 | 标记整个计划为 DONE 或 ABANDONED |
view_historical_plans | 查看历史计划 | 查看之前创建的所有计划 |
recover_historical_plan | 恢复历史计划 | 从历史计划中恢复,继续执行 |
工具函数的调用流程:
create_plan("开发网站", "创建一个博客网站", "网站可正常访问")
│
├─ revise_current_plan → 添加子任务: ["设计数据库", "实现后端", "实现前端"]
│
├─ update_subtask_state(0, IN_PROGRESS) → 开始第一个子任务
│
├─ ... 执行任务 ...
│
├─ finish_subtask("数据库设计完成") → 自动激活下一个子任务
│
├─ ... 继续执行 ...
│
└─ finish_plan(DONE) → 计划完成
13.4 状态约束
PlanNotebook 对子任务的执行有 3 个关键约束:
约束 1:只能顺序执行
子任务必须按顺序完成,不能跳过前面的子任务直接执行后面的。
✅ 正确:先完成子任务 0,再执行子任务 1
❌ 错误:跳过子任务 0,直接执行子任务 1
约束 2:只能有一个 IN_PROGRESS
同一时刻只能有一个子任务处于 IN_PROGRESS 状态,不允许同时执行多个子任务。
✅ 正确:子任务 0 = IN_PROGRESS,其他 = TODO
❌ 错误:子任务 0 = IN_PROGRESS,子任务 1 = IN_PROGRESS
约束 3:finish_subtask 自动激活下一个
调用 finish_subtask 完成当前子任务后,系统自动将下一个子任务的状态从 TODO 改为 IN_PROGRESS。
执行前:[DONE, IN_PROGRESS, TODO, TODO]
执行 finish_subtask()
执行后:[DONE, DONE, IN_PROGRESS, TODO]
↑ 自动激活
13.5 Hint 自动注入机制
PlanNotebook 通过 Hook 自动注入提示,引导 Agent 按照计划执行。Agent 无需主动查询计划状态,提示会自动出现在每轮推理的上下文中。
注入的提示内容:
=== 当前计划 ===
计划名称:开发网站
计划描述:创建一个博客网站
预期结果:网站可正常访问
=== 当前子任务 ===
子任务 2/5:实现后端 API
描述:实现用户注册、登录、文章 CRUD 接口
预期结果:所有 API 可正常调用
=== 执行规则 ===
1. 专注于当前子任务,不要跳到其他子任务
2. 完成当前子任务后,调用 finish_subtask
3. 所有子任务完成后,调用 finish_plan
注入时机:在 PreReasoningEvent 阶段,Hook 将提示注入到 Agent 的输入消息中。
13.6 历史计划恢复
PlanNotebook 支持历史计划恢复,适用于需要分多次执行的长任务。
恢复流程
第 1 次执行:
create_plan("开发网站")
├─ 子任务 0: 设计数据库 → DONE
├─ 子任务 1: 实现后端 → DONE
├─ 子任务 2: 实现前端 → IN_PROGRESS ← 中断(应用重启)
└─ 子任务 3: 部署上线 → TODO
第 2 次执行(恢复):
recover_historical_plan("开发网站")
├─ 子任务 0: 设计数据库 → DONE(保持)
├─ 子任务 1: 实现后端 → DONE(保持)
├─ 子任务 2: 实现前端 → IN_PROGRESS ← 从这里继续
└─ 子任务 3: 部署上线 → TODO
注意事项
- 子任务状态不会自动重置:恢复后,已完成的子任务仍然是 DONE
- 想重新执行已完成的子任务:需要手动调用
update_subtask_state将状态重置为 TODO - 恢复的是最后一次活跃的计划:如果有多个历史计划,恢复最近的一个
13.7 狼人杀中的使用
狼人杀项目没有使用 PlanNotebook,原因是:
- 任务简单:狼人杀是回合制投票游戏,每轮的任务是"发言"和"投票",不需要复杂规划
- 流程固定:游戏流程由裁判控制(夜晚→白天→投票→结算),Agent 不需要自己规划
- 无长任务:每个 Agent 的单次调用都是独立的,不存在需要跨多轮执行的复杂任务
PlanNotebook 适合的场景:
- 开发网站(需要设计、编码、测试、部署)
- 写研究报告(需要收集资料、分析、撰写、审校)
- 游戏开发(需要设计、实现、测试、发布)
13.8 总结
PlanNotebook 是 AgentScope 的任务规划系统:
- 核心能力:创建计划、分解子任务、跟踪状态、历史恢复
- 10 个工具函数:覆盖计划的完整生命周期
- 状态约束:顺序执行、单一 IN_PROGRESS、自动激活下一个
- Hint 注入:通过 Hook 自动引导 Agent 执行
- 适用场景:复杂任务(开发网站、游戏等),不适合简单任务
十四、持久化细节补充
14.1 持久化保存的内容
AgentScope 的持久化系统保存了 4 类状态数据:
| 保存内容 | 说明 | 示例 |
|---|---|---|
| Memory(对话历史) | 包括所有 Msg,包括 ToolUseBlock 和 ToolResultBlock | 用户提问、AI 回复、工具调用及结果 |
| PlanNotebook(任务状态) | 包括每个 SubTask 的状态(DONE/IN_PROGRESS/TODO) | 计划进度、子任务执行结果 |
| Toolkit(工具分组) | activeGroups 状态 | 当前激活的工具组 |
| 有状态工具 | 工具的内部状态 | 数据库连接状态、缓存内容 |
Memory 保存的 Msg 类型:
Memory 中的 Msg 列表:
├─ SYSTEM: "你是狼人,你的目标是..."
├─ USER: "请讨论今晚要杀谁"
├─ ASSISTANT: "我觉得应该杀张三"(含 ThinkingBlock)
├─ TOOL: ToolUseBlock("generate_response", {...})
├─ TOOL: ToolResultBlock("generate_response", "执行成功")
├─ USER: "张三已被淘汰"
└─ ASSISTANT: "好的,我继续观察..."
14.2 Tool 结果的持久化
关键澄清:Tool 结果是通过 Memory 持久化的,不是单独持久化。
ToolResultBlock 作为 TOOL 角色的 Msg 存储在 Memory 中:
// 工具执行后,结果被封装为 Msg 存入 Memory
Msg toolResultMsg = Msg.builder()
.name("generate_response")
.role(MsgRole.TOOL)
.content(ToolResultBlock.builder()
.toolName("generate_response")
.result("执行成功")
.build())
.build();
// 存入 Memory
memory.addMessage(toolResultMsg);
持久化时:
Session.save(key, agent.saveTo())
│
├─ Memory → List<Msg>(包含所有 ToolUseBlock 和 ToolResultBlock)
├─ PlanNotebook → Plan 列表(包含所有 SubTask 状态)
├─ Toolkit → activeGroups 状态
└─ 有状态工具 → 工具内部状态
14.3 为什么这样设计?
将 Tool 结果通过 Memory 持久化,而不是单独存储,有 4 个设计考量:
| 设计考量 | 说明 |
|---|---|
| 一致性 | 工具结果是对话历史的一部分,与其他消息保持一致 |
| 简化 | 统一用 Memory 管理所有消息,不需要额外的存储机制 |
| 顺序维护 | 保持工具调用的时间顺序,与对话历史自然衔接 |
| 完整性 | 整个对话历史作为一个单元保存,不会丢失任何上下文 |
对比:如果 Tool 结果单独存储
❌ 单独存储方案:
Memory: [用户提问, AI 回复, AI 回复, AI 回复]
ToolResults: [工具结果1, 工具结果2, 工具结果3]
问题:无法知道哪个工具结果对应哪次 AI 回复
✅ Memory 存储方案:
Memory: [用户提问, AI 回复, ToolUseBlock, ToolResultBlock, AI 回复, ...]
优势:完整的调用链,顺序清晰,一一对应
14.4 计划恢复后能否重新执行?
恢复计划:从上次停的地方继续
// 恢复历史计划
agent.recoverHistoricalPlan("开发网站");
// 自动恢复到中断时的状态:
// - 已完成的子任务保持 DONE
// - 中断时的 IN_PROGRESS 子任务保持 IN_PROGRESS
// - 后续的 TODO 子任务保持 TODO
重新执行已完成的子任务:不能直接重新执行
恢复计划后,已完成的子任务(DONE)不能直接重新执行。需要手动重置状态:
// 查看当前子任务状态
agent.viewSubtasks();
// 输出:
// 子任务 0: 设计数据库 → DONE
// 子任务 1: 实现后端 → DONE
// 子任务 2: 实现前端 → IN_PROGRESS
// 子任务 3: 部署上线 → TODO
// 如果想重新执行子任务 1,需要手动重置
agent.updateSubtaskState(1, SubTaskState.TODO);
// 重置后:
// 子任务 0: 设计数据库 → DONE
// 子任务 1: 实现后端 → TODO ← 已重置
// 子任务 2: 实现前端 → IN_PROGRESS
// 子任务 3: 部署上线 → TODO
为什么不自动重置?
- 避免重复工作:已完成的子任务可能已经产出了有价值的结果
- 保持灵活性:用户可以选择性地重新执行某些子任务
- 防止意外:自动重置可能导致意外丢失已完成的工作
14.5 总结
持久化保存了完整的执行记录,包括步骤是否执行完毕:
- Memory 保存所有 Msg:包括 ToolUseBlock 和 ToolResultBlock,保证对话历史完整
- Tool 结果通过 Memory 持久化:不是单独存储,而是作为 TOOL 角色的 Msg 存入 Memory
- 设计优势:一致性、简化、顺序维护、完整性
- 计划恢复:从上次停的地方继续,已完成的子任务不会自动重置
- 手动重置:需要调用
update_subtask_state将状态重置为 TODO
关键理解:持久化不仅仅是"保存数据",更是保存了完整的执行上下文,包括对话历史、工具调用链、任务进度,让 Agent 能够在中断后无缝继续。
十五、A2A 协议
15.1 A2A 解决什么问题?
A2A(Agent-to-Agent)协议提供跨进程的 Agent 通信能力。MsgHub 只能用于进程内通信,A2A 支持跨进程/跨机器的 Agent 通信。
对比:
| 通信方式 | 范围 | 协议 | 适用场景 |
|---|---|---|---|
| MsgHub | 进程内 | 内存中的方法调用 | 同一个应用内的多 Agent 协作 |
| A2A | 跨进程/跨机器 | HTTP/JSON-RPC | 分布式 Agent 微服务 |
15.2 核心概念
A2A 协议有 3 个核心组件:
AgentCard(代理名片)
AgentCard 描述 Agent 的能力、URL、skills 等信息,是 Agent 的"身份证":
AgentCard agentCard = AgentCard.builder()
.name("weather-agent")
.description("天气查询 Agent")
.url("http://localhost:8080")
.skills(List.of(
Skill.builder().name("query_weather").description("查询天气").build()
))
.build();
A2aAgent(A2A 客户端)
A2aAgent 用于调用远程 Agent,是 A2A 协议的客户端实现:
A2aAgent remoteAgent = A2aAgent.builder()
.agentCard(remoteAgentCard) // 远程 Agent 的名片
.build();
// 调用远程 Agent
Msg response = remoteAgent.call(userMessage).block();
AgentCardResolver(名片解析器)
AgentCardResolver 用于获取远程 Agent 的名片:
AgentCardResolver resolver = new StaticAgentCardResolver();
AgentCard card = resolver.resolve("http://remote-agent:8080").block();
15.3 通信协议
A2A 使用 HTTP/JSON-RPC 协议通信,支持流式响应:
客户端 服务端
│ │
│ POST /a2a │
│ Content-Type: application/json
│ {"jsonrpc":"2.0","method":"send","params":{...}}
│ ───────────────────────────→ │
│ │
│ 200 OK │
│ {"jsonrpc":"2.0","result":{...}}
│ ←─────────────────────────── │
│ │
│ // 流式响应(SSE) │
│ POST /a2a (stream=true) │
│ ───────────────────────────→ │
│ │
│ data: {"chunk":"你好"} │
│ ←─────────────────────────── │
│ data: {"chunk":",今天"} │
│ ←─────────────────────────── │
│ data: [DONE] │
│ ←─────────────────────────── │
15.4 使用场景
A2A 协议适用于以下场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 微服务架构 | 不同服务间的 Agent 互调 | 订单服务的 Agent 调用库存服务的 Agent |
| 跨组织协作 | 不同组织的 Agent 互调 | 企业 A 的 Agent 调用企业 B 的 Agent |
| Agent 市场 | 用户可以调用第三方 Agent | 调用天气 Agent、翻译 Agent 等公共服务 |
15.5 A2A vs MsgHub
| 维度 | MsgHub | A2A |
|---|---|---|
| 通信范围 | 进程内 | 跨进程/跨机器 |
| 通信方式 | 内存中的方法调用 | HTTP/JSON-RPC |
| 性能 | 简单高效 | 有网络开销 |
| 分布式 | 不支持 | 支持 |
| 适用场景 | 单体应用内的多 Agent 协作 | 分布式 Agent 微服务 |
15.6 类比 opencode @ 机制
A2A 可以理解为分布式版本的 @ 机制:
- opencode @:进程内的 Agent 互调,通过内存直接通信
- A2A:跨进程的 Agent 互调,通过 HTTP/JSON-RPC 通信
opencode @ 机制(进程内):
@explore → 调用探索 Agent
@librarian → 调用图书管理员 Agent
A2A 协议(跨进程):
@weather-agent → 调用远程天气 Agent
@translation-agent → 调用远程翻译 Agent
15.7 狼人杀中的使用
狼人杀项目没有使用 A2A,原因是:
- 所有 Agent 都在同一个进程:狼人杀的所有 AI 玩家(张三、李四等)都在同一个 JVM 进程中运行
- MsgHub 足够:进程内的多 Agent 通信通过 MsgHub 即可完成
- 无分布式需求:单体应用不需要跨进程通信
何时需要 A2A:
- Agent 部署在不同的服务/机器上
- 需要调用第三方 Agent 服务
- 构建 Agent 市场/平台
十六、Nacos + A2A 集成
16.1 核心思想
Nacos 提供服务发现和注册功能,A2A 提供 Agent 间通信协议。两者结合实现分布式 Agent 微服务架构。
┌─────────────────────────────────────────────────────────┐
│ Nacos 注册中心 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ weather-agent│ │ translator │ │ calculator │ │
│ │ 192.168.1.1 │ │ 192.168.1.2 │ │ 192.168.1.3 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
↑ ↑ ↑
│ │ │
A2A 协议 A2A 协议 A2A 协议
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Agent A │ │ Agent B │ │ Agent C │
└─────────┘ └─────────┘ └─────────┘
16.2 核心组件
NacosA2aRegistry
把 AgentCard 和 Endpoint 注册到 Nacos,让其他服务能够发现本 Agent:
NacosA2aRegistry registry = new NacosA2aRegistry(nacosClient);
// 注册 Agent
registry.register(agentCard, endpoint).block();
// 注销 Agent
registry deregister(agentCard).block();
NacosAgentCardResolver
从 Nacos 发现远程 Agent 的 AgentCard:
NacosAgentCardResolver resolver = new NacosAgentCardResolver(nacosClient);
// 发现远程 Agent
AgentCard card = resolver.resolve("weather-agent").block();
// 创建 A2A 客户端
A2aAgent remoteAgent = A2aAgent.builder()
.agentCard(card)
.build();
16.3 使用流程
服务端:注册 Agent
// 1. 创建 Agent
ReActAgent agent = ReActAgent.builder()
.name("weather-agent")
.model(model)
.memory(new InMemoryMemory())
.build();
// 2. 创建 AgentCard
AgentCard agentCard = AgentCard.builder()
.name("weather-agent")
.description("天气查询 Agent")
.skills(List.of(
Skill.builder().name("query_weather").description("查询天气").build()
))
.build();
// 3. 注册到 Nacos
NacosA2aRegistry registry = new NacosA2aRegistry(nacosClient);
registry.register(agentCard, endpoint).block();
// 4. 启动 A2A Server
A2aServer server = A2aServer.builder()
.agent(agent)
.agentCard(agentCard)
.build();
server.start().block();
客户端:调用远程 Agent
// 1. 创建 NacosAgentCardResolver
NacosAgentCardResolver resolver = new NacosAgentCardResolver(nacosClient);
// 2. 创建 A2aAgent
AgentCard card = resolver.resolve("weather-agent").block();
A2aAgent remoteAgent = A2aAgent.builder()
.agentCard(card)
.build();
// 3. 调用远程 Agent
Msg userMsg = Msg.builder()
.name("user")
.role(MsgRole.USER)
.content(TextBlock.builder().text("北京今天天气怎么样?").build())
.build();
Msg response = remoteAgent.call(List.of(userMsg)).block();
System.out.println(response.getContent()); // "北京今天晴,气温 25°C"
16.4 优势
| 优势 | 说明 |
|---|---|
| 自动发现 | 从 Nacos 自动发现远程 Agent,无需硬编码地址 |
| 动态更新 | 订阅机制,AgentCard 更新时自动通知 |
| 负载均衡 | Nacos 自动负载均衡,支持多实例部署 |
| 健康检查 | Nacos 自动健康检查,故障实例自动剔除 |
16.5 类比微服务
Nacos + A2A 的组合,类比传统微服务架构:
| 维度 | 传统微服务 | Agent 微服务 |
|---|---|---|
| 服务发现 | Nacos + Feign/gRPC | Nacos + A2A |
| 通信协议 | HTTP/gRPC | HTTP/JSON-RPC |
| 服务描述 | Swagger/OpenAPI | AgentCard |
| 负载均衡 | Ribbon/LoadBalancer | Nacos 内置 |
| 健康检查 | Spring Boot Actuator | Nacos 心跳 |
传统微服务架构:
用户 → API Gateway → 服务A → 服务B → 服务C
↓
Nacos(服务发现)
Agent 微服务架构:
用户 → Agent Gateway → AgentA → AgentB → AgentC
↓
Nacos(服务发现) + A2A(通信协议)
16.6 狼人杀中的使用
狼人杀项目没有使用 Nacos + A2A,原因是:
- 所有 Agent 都在同一个进程:不需要服务发现和跨进程通信
- 单体架构足够:单个 Spring Boot 应用即可运行整个游戏
- 无分布式需求:不需要负载均衡和健康检查
何时需要 Nacos + A2A:
- Agent 部署在多个服务/机器上
- 需要动态扩缩容 Agent 实例
- 构建 Agent 市场/平台
- 需要高可用和故障转移
16.7 总结
Nacos + A2A = Agent 服务发现 + Agent 间通信,实现分布式 Agent 微服务架构:
- Nacos:提供服务注册、发现、负载均衡、健康检查
- A2A:提供跨进程的 Agent 通信协议
- 组合:类似传统微服务的 Nacos + Feign/gRPC,但面向 Agent 场景
架构演进:
单体应用 → MsgHub(进程内通信)
↓
分布式应用 → A2A(跨进程通信)
↓
Agent 微服务 → Nacos + A2A(服务发现 + 跨进程通信)
十七、Tool 定义与实现
17.1 Tool 解决什么问题?
Agent 需要执行各种任务(搜索、计算、调用 API 等),但 LLM 本身无法执行代码。通过 Tool 机制,让 LLM 能够"调用"外部函数。
LLM(大脑) Tool(手脚)
│ │
│ "我想搜索天气" │
│ ─────────────────────────→ │
│ │ 执行搜索
│ "北京今天晴,25°C" │
│ ←───────────────────────── │
│ │
基于结果继续推理
核心价值:Tool 让 LLM 从"只能说话"变成"能做事"。
17.2 核心注解
AgentScope4J 使用注解来定义工具,两个核心注解:
@Tool — 标记工具方法
@Tool(name = "search_weather", description = "查询指定城市的天气信息")
public String searchWeather(String city) {
// 调用天气 API
return weatherService.query(city);
}
| 属性 | 类型 | 含义 |
|---|---|---|
name | String | 工具名称,LLM 调用时使用 |
description | String | 工具描述,帮助 LLM 理解工具用途 |
@ToolParam — 描述工具参数
@Tool(name = "search_weather", description = "查询指定城市的天气信息")
public String searchWeather(
@ToolParam(name = "city", description = "城市名称", required = true) String city,
@ToolParam(name = "date", description = "查询日期,格式 yyyy-MM-dd", required = false) String date
) {
return weatherService.query(city, date);
}
| 属性 | 类型 | 含义 |
|---|---|---|
name | String | 参数名称 |
description | String | 参数描述 |
required | boolean | 是否必填 |
17.3 Toolkit — 工具管理器
Toolkit 是工具的管理中心,负责:
| 职责 | 说明 |
|---|---|
| 注册工具 | 扫描 @Tool 注解,注册到工具注册表 |
| 生成 Schema | 为 LLM 生成工具的 JSON Schema |
| 执行工具 | 调用工具方法,返回结果 |
| 管理分组 | 支持工具分组,动态激活/停用 |
创建 Toolkit
// 空 Toolkit(狼人杀项目使用)
Toolkit toolkit = new Toolkit();
// 注册工具
toolkit.registerTool(new WeatherTool());
toolkit.registerTool(new CalculatorTool());
生成 JSON Schema
Toolkit 会自动为每个工具生成 JSON Schema,供 LLM 理解工具的参数格式:
{
"name": "search_weather",
"description": "查询指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
},
"date": {
"type": "string",
"description": "查询日期,格式 yyyy-MM-dd"
}
},
"required": ["city"]
}
}
17.4 完整执行流程
Tool 的完整执行流程涉及 7 个步骤:
┌─────────────────────────────────────────────────────────────────┐
│ Tool 执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 定义工具类(使用 @Tool 和 @ToolParam 注解) │
│ ↓ │
│ 2. 注册工具到 Toolkit(扫描注解,生成 Schema) │
│ ↓ │
│ 3. 创建 Agent(绑定 Toolkit) │
│ │ │
│ │ ReActAgent.builder() │
│ │ .toolkit(toolkit) ← 绑定 Toolkit │
│ │ .build() │
│ ↓ │
│ 4. Agent 调用,LLM 返回工具调用请求(ToolUseBlock) │
│ │ │
│ │ LLM 输出: ToolUseBlock( │
│ │ toolName = "search_weather", │
│ │ input = {"city": "北京"} │
│ │ ) │
│ ↓ │
│ 5. Toolkit 执行工具(调用实际方法) │
│ │ │
│ │ String result = toolkit.execute("search_weather", │
│ │ Map.of("city", "北京"));│
│ │ // result = "北京今天晴,25°C" │
│ ↓ │
│ 6. 构造工具结果(ToolResultBlock),发回给 LLM │
│ │ │
│ │ ToolResultBlock( │
│ │ toolName = "search_weather", │
│ │ result = "北京今天晴,25°C" │
│ │ ) │
│ ↓ │
│ 7. LLM 看到工具结果,生成最终回复 │
│ │ │
│ │ "北京今天天气晴朗,气温 25°C,适合出行。" │
│ │
└─────────────────────────────────────────────────────────────────┘
17.5 工具分组(Tool Groups)
Toolkit 支持工具分组,可以动态激活/停用工具:
Toolkit toolkit = new Toolkit();
// 创建分组
toolkit.createGroup("weather");
toolkit.createGroup("calculator");
// 注册工具到分组
toolkit.registerTool(new WeatherTool(), "weather");
toolkit.registerTool(new CalculatorTool(), "calculator");
// 激活/停用分组
toolkit.activateGroup("weather");
toolkit.deactivateGroup("calculator");
分组的典型场景:
| 场景 | 说明 |
|---|---|
| 按角色分组 | 不同角色的 Agent 使用不同的工具集 |
| 按阶段分组 | 游戏不同阶段激活不同工具 |
| 动态切换 | 根据上下文动态启用/禁用工具 |
17.6 狼人杀中的使用
狼人杀项目使用了空 Toolkit,没有注册任何工具:
// WerewolfWebGame.java:298
ReActAgent agent = ReActAgent.builder()
.name(name)
.sysPrompt(systemPrompt)
.model(model)
.memory(new InMemoryMemory())
.toolkit(new Toolkit()) // ← 空 Toolkit,没有注册任何工具
.build();
为什么狼人杀不需要工具?
| 原因 | 说明 |
|---|---|
| 决策通过结构化输出 | 投票、杀人等决策通过 call(msgs, VoteModel.class) 实现 |
| 游戏逻辑在裁判端 | 杀人、查验等逻辑由裁判(WerewolfWebGame)控制,不需要 Agent 调用工具 |
| 简单交互模式 | Agent 只需要"发言"和"投票",不需要复杂的功能调用 |
对比:需要工具的场景
// 如果狼人杀需要工具,可能是这样的:
public class WerewolfTool {
@Tool(name = "investigate_player", description = "调查一名玩家的身份线索")
public String investigate(
@ToolParam(name = "playerName", description = "要调查的玩家名称") String playerName
) {
// 返回一些模糊的线索,不是直接的身份信息
return "该玩家昨晚似乎没有睡觉...";
}
}
17.7 总结
Tool 是 AgentScope 的工具调用机制:
- 核心注解:
@Tool标记工具方法,@ToolParam描述工具参数 - Toolkit 管理器:负责工具注册、Schema 生成、执行和分组管理
- 执行流程:定义工具 → 注册到 Toolkit → 绑定 Agent → LLM 返回 ToolUseBlock → Toolkit 执行 → 结果返回 LLM
- 工具分组:支持动态激活/停用,适用于复杂场景
设计原则:
- 声明式定义:通过注解声明工具,无需手写 Schema
- 类型安全:参数类型由 Java 方法签名保证
- 灵活管理:Toolkit 支持分组和动态切换
- 与 LLM 解耦:工具定义与 LLM 调用分离,便于测试和复用
十八、MCP Tool vs 普通 Tool
18.1 两种 Tool 类型
AgentScope 支持两种 Tool 类型:
| 类型 | 定义方式 | 执行位置 | 适用场景 |
|---|---|---|---|
| 普通 Tool | 本地 Java 方法,通过 @Tool 注解定义 | 本地进程内执行 | 简单的本地计算、访问本地资源 |
| MCP Tool | 远程工具,通过 MCP 协议调用 | 远程服务器执行 | 第三方服务、跨组织共享、动态工具发现 |
18.2 核心区别
普通 Tool
// 本地 Java 方法,通过反射调用
@Tool(name = "calculate", description = "计算数学表达式")
public String calculate(@ToolParam(name = "expression") String expression) {
return String.valueOf(new ScriptEngine().eval(expression));
}
// 注册到 Toolkit
toolkit.registerTool(new MathTool());
// 执行流程:LLM → Agent → 反射调用 Java 方法 → 返回结果
MCP Tool
// 远程工具,通过 MCP 协议调用
McpTool tool = new McpTool(mcpClientWrapper, toolSchema);
// 执行流程:LLM → Agent → MCP 客户端 → 网络请求 → 远程服务器执行 → 返回结果
执行流程对比:
普通 Tool:
LLM → Agent → Java 反射 → 本地方法执行 → 结果返回
MCP Tool:
LLM → Agent → MCP Client → HTTP/JSON-RPC → 远程服务器 → 结果返回
18.3 McpTool 实现
McpTool 实现了 AgentTool 接口,核心结构如下:
public class McpTool implements AgentTool {
private final McpClientWrapper mcpClientWrapper; // MCP 客户端包装器
private final ToolSchema toolSchema; // 工具 Schema
private final Map<String, Object> presetArguments; // 预设参数(可选)
@Override
public ToolSchema getSchema() {
return toolSchema;
}
@Override
public Mono<ToolResult> execute(Map<String, Object> args) {
// 合并预设参数和调用参数
Map<String, Object> mergedArgs = new HashMap<>(presetArguments);
mergedArgs.putAll(args);
// 通过 MCP 客户端调用远程工具
return mcpClientWrapper.callTool(toolSchema.getName(), mergedArgs);
}
}
核心组件:
| 组件 | 作用 |
|---|---|
McpClientWrapper | MCP 协议客户端,负责与远程 MCP 服务器通信 |
ToolSchema | 工具的 Schema 定义,包括名称、描述、参数格式 |
presetArguments | 预设参数,每次调用时自动合并,用于固定配置 |
18.4 使用场景
普通 Tool 适用场景
| 场景 | 示例 |
|---|---|
| 本地计算 | 数学计算、数据格式转换、文本处理 |
| 访问本地资源 | 读取文件、查询数据库、调用本地 API |
| 简单逻辑 | 条件判断、数据验证、业务规则 |
// 普通 Tool 示例:本地文件读取
@Tool(name = "read_file", description = "读取本地文件内容")
public String readFile(@ToolParam(name = "path") String path) {
return Files.readString(Path.of(path));
}
MCP Tool 适用场景
| 场景 | 示例 |
|---|---|
| 第三方服务 | 天气查询、地图服务、支付接口 |
| 跨组织共享 | 企业内部工具市场、合作伙伴服务 |
| 动态工具发现 | 运行时发现可用工具,无需预先编码 |
// MCP Tool 示例:调用远程天气服务
McpTool weatherTool = mcpClientWrapper.discoverTool("query_weather");
Agent.call("北京今天天气怎么样?"); // 自动调用远程天气工具
18.5 狼人杀中的使用
狼人杀项目没有使用 MCP Tool,只用了空 Toolkit:
// WerewolfWebGame.java:298
ReActAgent agent = ReActAgent.builder()
.name(name)
.sysPrompt(systemPrompt)
.model(model)
.memory(new InMemoryMemory())
.toolkit(new Toolkit()) // ← 空 Toolkit,没有注册任何工具
.build();
为什么狼人杀不需要 MCP Tool?
| 原因 | 说明 |
|---|---|
| 单体架构 | 所有组件都在同一个进程,不需要远程调用 |
| 无外部服务 | 游戏逻辑完全自包含,不依赖第三方服务 |
| 简单交互 | Agent 只需要发言和投票,不需要调用外部工具 |
18.6 总结
Tool 有两种类型,根据使用场景选择合适的类型:
| 维度 | 普通 Tool | MCP Tool |
|---|---|---|
| 定义方式 | @Tool 注解 | McpTool 类 |
| 执行位置 | 本地进程 | 远程服务器 |
| 通信方式 | Java 反射 | MCP 协议(HTTP/JSON-RPC) |
| 适用场景 | 本地计算、简单逻辑 | 第三方服务、跨组织共享 |
| 复杂度 | 低 | 高(需要 MCP 服务器) |
| 性能 | 高(无网络开销) | 中(有网络延迟) |
选择建议:
- 本地逻辑:用普通 Tool(简单、高效)
- 远程服务:用 MCP Tool(支持分布式、动态发现)
十九、RAG(检索增强生成)
19.1 RAG 解决什么问题?
LLM 的知识是训练时学习的,无法获取最新的、私有的、特定领域的知识。RAG 让 Agent 能够基于外部知识库回答问题。
没有 RAG 时的问题:
用户:公司的请假制度是什么?
LLM:(基于训练数据回答)一般公司请假需要...
↑ 可能不准确,因为不知道你们公司的具体制度
有 RAG 后:
用户:公司的请假制度是什么?
↓
检索:从公司制度文档中找到相关段落
↓
LLM:根据公司制度文档,请假需要提前3天申请...
↑ 基于实际文档回答,准确可靠
19.2 核心流程
RAG 分为离线和在线两个阶段:
离线阶段:文档索引
文档(PDF/Word/网页)
↓
Reader(读取文档内容)
↓
TextChunker(把长文档分成小块)
↓
EmbeddingModel(把文本转换为向量)
↓
VectorStore(存储向量)
在线阶段:检索生成
用户提问
↓
EmbeddingModel(把问题转换为向量)
↓
VectorStore.search()(检索最相关的文档块)
↓
LLM(基于检索结果生成回答)
完整流程图:
离线阶段(一次性):
文档 → Reader → TextChunker → EmbeddingModel → VectorStore
在线阶段(每次提问):
用户提问 → EmbeddingModel → VectorStore.search() → LLM 生成回答
19.3 核心组件
RAG 系统由 4 个核心组件组成:
| 组件 | 作用 | 示例 |
|---|---|---|
| Reader | 读取各种格式的文档 | PDF、Word、网页、Markdown |
| TextChunker | 把长文档分成小块 | 按段落、按字符数、按 token 数 |
| EmbeddingModel | 把文本转换为向量 | DashScope Embedding、OpenAI Embedding |
| VectorStore | 存储和检索向量 | Milvus、Pinecone、内存向量库 |
Reader — 文档读取器
Reader 负责读取各种格式的文档,提取纯文本内容:
// 示例:读取 PDF 文档
Reader pdfReader = new PDFReader();
String content = pdfReader.read("company-policy.pdf");
TextChunker — 文本分块器
TextChunker 把长文档分成小块,便于后续向量化和检索:
// 示例:按段落分块
TextChunker chunker = new ParagraphChunker();
List<String> chunks = chunker.chunk(longDocument);
EmbeddingModel — 向量模型
EmbeddingModel 把文本转换为高维向量,用于语义相似度计算:
// 示例:文本转向量
EmbeddingModel model = new DashScopeEmbeddingModel();
float[] vector = model.embed("请假制度");
// vector = [0.123, -0.456, 0.789, ...](1536 维)
VectorStore — 向量存储
VectorStore 存储向量,并支持相似度检索:
// 存储向量
vectorStore.add("chunk_id_1", vector1, metadata);
vectorStore.add("chunk_id_2", vector2, metadata);
// 检索相似向量
List<SearchResult> results = vectorStore.search(queryVector, topK=3);
19.4 文本分块策略
TextChunker 支持多种分块策略:
| 策略 | 分隔符 | 适用场景 |
|---|---|---|
CHARACTER | 按字符数 | 简单文本,无明显结构 |
PARAGRAPH | \n\n | 有段落结构的文档 |
TOKEN | 按 token 数 | 需要精确控制 token 量 |
SEMANTIC | 语义边界 | 高质量分块(目前回退到段落分块) |
CHARACTER — 按字符数分块
// 每 500 个字符一块
TextChunker chunker = new CharacterChunker(500);
List<String> chunks = chunker.chunk(document);
特点:简单直接,但可能在句子中间切断。
PARAGRAPH — 按段落分块
// 按 \n\n 分割
TextChunker chunker = new ParagraphChunker();
List<String> chunks = chunker.chunk(document);
特点:保持段落完整性,适合有明确段落结构的文档。
TOKEN — 按 token 数分块
// 每 200 个 token 一块
TextChunker chunker = new TokenChunker(200);
List<String> chunks = chunker.chunk(document);
特点:精确控制 token 量,适合有 token 限制的场景。
SEMANTIC — 按语义分块
// 按语义边界分块(目前回退到段落分块)
TextChunker chunker = new SemanticChunker();
List<String> chunks = chunker.chunk(document);
特点:理论上最优,但实现复杂,目前 AgentScope 回退到段落分块。
19.5 重叠分割(Overlap)
防止关键内容在边界处被切断。相邻块共享部分内容,确保关键词完整包含在至少一个块中。
没有重叠时的问题:
块 1: "请假需要提前3天"
块 2: "向直属领导申请"
问题:"提前3天向直属领导申请" 被切断,检索时可能找不到完整信息
有重叠时:
块 1: "请假需要提前3天向直属领导"
块 2: "提前3天向直属领导申请并填写"
重叠部分: "提前3天向直属领导"
优势: 无论检索哪个块,都能找到完整信息
推荐配置:
// overlapSize = chunkSize 的 10-20%
TextChunker chunker = new CharacterChunker(
500, // chunkSize
50 // overlapSize(500 的 10%)
);
示例代码:
// 完整的 RAG 配置
TextChunker chunker = new ParagraphChunker(500, 50);
EmbeddingModel embedding = new DashScopeEmbeddingModel();
VectorStore vectorStore = new InMemoryVectorStore();
// 离线:索引文档
List<String> chunks = chunker.chunk(document);
for (int i = 0; i < chunks.size(); i++) {
float[] vector = embedding.embed(chunks.get(i));
vectorStore.add("chunk_" + i, vector, Map.of("text", chunks.get(i)));
}
// 在线:检索
float[] queryVector = embedding.embed(userQuestion);
List<SearchResult> results = vectorStore.search(queryVector, 3);
String context = results.stream()
.map(r -> r.getMetadata().get("text"))
.collect(Collectors.joining("\n"));
19.6 狼人杀中的使用
狼人杀项目没有使用 RAG,原因是:
| 原因 | 说明 |
|---|---|
| 规则明确 | 狼人杀的游戏规则是固定的,不需要外部知识 |
| 无文档需求 | 游戏逻辑完全由代码控制,不需要检索文档 |
| 简单问答 | Agent 的回答基于游戏状态,不需要知识库 |
RAG 适合的场景:
- 客服系统(需要检索产品文档)
- 知识问答(需要检索企业知识库)
- 法律助手(需要检索法律条文)
- 技术支持(需要检索技术文档)
二十、多模态
20.1 多模态解决什么问题?
传统 Agent 只能处理文本,无法理解图片、音频、视频等多媒体内容。多模态能力让 Agent 能够处理各种类型的输入和输出。
没有多模态时:
用户:[发送一张图片] 这是什么?
Agent:抱歉,我无法看到图片。
有多模态后:
用户:[发送一张图片] 这是什么?
Agent:这是一只金毛犬,看起来很高兴。
20.2 支持的类型
AgentScope 支持 4 种 ContentBlock 类型:
| Block 类型 | JSON type | 用途 | 格式 |
|---|---|---|---|
TextBlock | "text" | 纯文本 | 字符串 |
ImageBlock | "image" | 图片 | URL 或 Base64 |
AudioBlock | "audio" | 音频 | URL 或 Base64 |
VideoBlock | "video" | 视频 | URL 或 Base64 |
TextBlock — 纯文本
TextBlock text = TextBlock.builder()
.text("你好,有什么可以帮助你的?")
.build();
ImageBlock — 图片
// 使用 URL
ImageBlock image = ImageBlock.builder()
.url("https://example.com/photo.jpg")
.build();
// 使用 Base64
ImageBlock image = ImageBlock.builder()
.base64("data:image/jpeg;base64,/9j/4AAQ...")
.build();
AudioBlock — 音频
// 使用 URL
AudioBlock audio = AudioBlock.builder()
.url("https://example.com/audio.mp3")
.build();
// 使用 Base64
AudioBlock audio = AudioBlock.builder()
.base64("data:audio/mp3;base64,SUQzB...")
.build();
VideoBlock — 视频
// 使用 URL
VideoBlock video = VideoBlock.builder()
.url("https://example.com/video.mp4")
.build();
// 使用 Base64
VideoBlock video = VideoBlock.builder()
.base64("data:video/mp4;base64,AAAA...")
.build();
20.3 主要针对输入还是输出?
主要针对输入(用户发送图片/音频/视频给 Agent),但也可以用于输出(Agent 生成图片/音频)。
输入场景(更常见)
用户发送图片 → Agent 理解图片内容 → 生成文本回复
用户发送音频 → Agent 识别语音 → 生成文本回复
用户发送视频 → Agent 分析视频 → 生成文本回复
// 用户发送图片
Msg userMsg = Msg.builder()
.name("user")
.role(MsgRole.USER)
.content(
TextBlock.builder().text("这是什么动物?").build(),
ImageBlock.builder().url("https://example.com/cat.jpg").build()
)
.build();
// Agent 调用
Msg response = agent.call(List.of(userMsg)).block();
// response: "这是一只橘猫,看起来很可爱。"
输出场景(较少见)
用户请求生成图片 → Agent 调用图像生成工具 → 返回图片
用户请求语音回复 → Agent 调用 TTS 工具 → 返回音频
// Agent 生成图片(需要图像生成工具)
@Tool(name = "generate_image", description = "生成图片")
public String generateImage(@ToolParam(name = "prompt") String prompt) {
// 调用图像生成 API
return imageGenerator.generate(prompt); // 返回图片 URL
}
20.4 纠正:狼人杀项目没有使用 AudioBlock
重要澄清:狼人杀项目使用的是 DashScopeRealtimeTTSModel,不是 AudioBlock。
| 组件 | 用途 | 说明 |
|---|---|---|
AudioBlock | 多模态内容块 | 用于在 Msg 中传递音频数据 |
DashScopeRealtimeTTSModel | TTS 模型 | 用于将文本转换为语音 |
狼人杀中的 TTS 实现:
// 创建 TTS 模型
DashScopeRealtimeTTSModel ttsModel = DashScopeRealtimeTTSModel.builder()
.apiKey(apiKey)
.build();
// 使用 TTS 生成语音
String text = "昨晚死的是张三";
byte[] audioData = ttsModel.synthesize(text).block();
关键区别:
- AudioBlock:是 ContentBlock 的一种,用于在 Msg 中传递音频数据(URL 或 Base64)
- TTS 模型:是 Model 的一种,用于将文本转换为语音,不通过 AudioBlock 传递
20.5 狼人杀中的多模态使用
狼人杀项目的多模态使用非常有限:
输入:没有多模态输入
// 狼人杀只有文本输入
Msg userMsg = Msg.builder()
.name("player")
.role(MsgRole.USER)
.content(TextBlock.builder().text("我觉得张三是狼人").build())
.build();
输出:使用 TTS 生成语音
// 使用 DashScopeRealtimeTTSModel 生成语音
DashScopeRealtimeTTSModel ttsModel = DashScopeRealtimeTTSModel.builder()
.apiKey(apiKey)
.build();
// Agent 的文本回复转换为语音
String agentResponse = "我同意,张三确实可疑";
byte[] audioData = ttsModel.synthesize(agentResponse).block();
// 将语音发送给前端播放
emitter.sendAudio(audioData);
为什么狼人杀只用 TTS,不用其他多模态?
| 原因 | 说明 |
|---|---|
| 游戏性质 | 狼人杀是语言类游戏,核心是文字交流 |
| 简化实现 | 只需要 TTS 让 AI 玩家"说话",不需要看图/听音频 |
| 性能考虑 | 多模态处理会增加延迟,影响游戏体验 |
如果狼人杀要用多模态,可能是这样的:
// 玩家发送照片作为"证据"
Msg evidenceMsg = Msg.builder()
.name("player")
.role(MsgRole.USER)
.content(
TextBlock.builder().text("看这张照片,张三昨晚在狼人聚会").build(),
ImageBlock.builder().url("https://example.com/evidence.jpg").build()
)
.build();
// Agent 分析图片
// "我仔细看了这张照片,这似乎是一张PS的图片..."
二十一、学习过程中的关键疑问与解答
疑问 1:工具调用有先后顺序/依赖怎么办?
解答:AgentScope 用 Pipeline 解决工具调用的顺序和依赖问题。
- SequentialPipeline:串行执行,前一个的输出是后一个的输入
- FanoutPipeline:并行执行,所有 Agent 同时处理同一输入
// SequentialPipeline 示例:检索 → 重排 → 生成
Msg result = Pipelines.sequential(
List.of(retrievalAgent, rerankAgent, generationAgent),
query
).block();
狼人杀中的替代方案:狼人杀没用 Pipeline,用 MsgHub 广播机制。因为狼人杀的 Agent 是平等参与者,不是流水线上的工人。
疑问 2:WebUserInput 怎么等待?
解答:使用 Sinks.One + block() 阻塞游戏线程,REST 端点 tryEmitValue() 唤醒。
游戏线程(阻塞) REST 线程(异步)
│ │
▼ │
waitForInput("VOTE") │
│ 创建 Sinks.One<String> │
│ sink.asMono().block() │ ← 游戏线程阻塞
│ ↓ 挂起 │
│ ↓ 用户投票,POST /api/game/input
│ ↓ │
│ ↓ sink.tryEmitValue("张三") ← 唤醒!
│ ← Mono 拿到 "张三",解除阻塞 │
关键点:Sinks.One 只能发射一次,线程安全,block() 不占 CPU。
疑问 3:LLM 输出格式不符合怎么办?
解答:不直接让 LLM 输出 JSON,而是调用工具(参数有 schema 约束),失败时用 tool_choice 强制重试。
传统方案(不推荐):
String response = llm.call("请按格式输出 JSON");
try {
JSONObject json = new JSONObject(response); // 可能失败
} catch (JSONException e) {
// 很难让 LLM 理解错在哪里
}
AgentScope 的方案:
- 注册临时工具
generate_response,参数 schema 就是目标格式 - LLM 调用工具时,底层验证参数是否符合 schema
- 不符合 → 用
tool_choice = "generate_response"强制重试
重试策略:
| 策略 | 第一次失败后 | 第二次失败后 | 第三次失败后 |
|---|---|---|---|
TOOL_CHOICE | 强制 tool_choice | 再强制一次 | 抛异常 |
PROMPT | 提醒"请调用工具" | 再提醒一次 | 抛异常 |
疑问 4:Hook 是怎么检查的?
解答:检查 LLM 返回的 Msg 中是否有 ToolUseBlock,用 instanceof 检查(不是字符串匹配)。
// StructuredOutputHook 的检查逻辑
boolean hasCall = !msg.getContentBlocks(ToolUseBlock.class).isEmpty();
// ↑ 如果为空,说明 LLM 没有请求调用任何工具
为什么检查 ToolUseBlock 而不是"工具是否执行"?
因为 Hook 在 PostReasoning 阶段触发,此时 LLM 刚返回结果,Agent 还没执行工具。Hook 需要检查的是"LLM 是否请求了工具调用"。
底层实现:Java 类型系统(instanceof)检查,因为 ContentBlock 是 sealed class,每个子类有明确的 Java 类型。
疑问 5:LLM 不会直接输出 JSON?
解答:LLM 输出的是 token 序列,不是 JSON。API 层负责把这些 token 解析成结构化的 JSON。
真实流程:
调用 LLM API,传入 tools 参数
↓
LLM 生成 token 序列(包含特殊 token [TOOL_CALL_START] 和 [TOOL_CALL_END])
↓
API 层解析 token 序列,验证 JSON 格式,生成结构化 tool_calls
↓
AgentScope 解析 tool_calls,执行工具
DashScope API 返回的实际格式:
{
"output": {
"choices": [{
"message": {
"tool_calls": [{
"function": {
"name": "generate_response",
"arguments": "{\"targetPlayer\":\"张三\"}" // ← API 层解析的 JSON
}
}]
}
}]
}
}
疑问 6:是否可以重新执行已执行过的计划?
解答:可以恢复计划从上次停的地方继续,但不能直接重新执行已完成的子任务,需要手动重置状态。
恢复计划:
agent.recoverHistoricalPlan("开发网站");
// 自动恢复到中断时的状态:
// - 已完成的子任务保持 DONE
// - 中断时的 IN_PROGRESS 子任务保持 IN_PROGRESS
// - 后续的 TODO 子任务保持 TODO
重新执行已完成的子任务:
// 查看当前状态
agent.viewSubtasks();
// 子任务 0: 设计数据库 → DONE
// 子任务 1: 实现后端 → DONE
// 子任务 2: 实现前端 → IN_PROGRESS
// 想重新执行子任务 1,需要手动重置
agent.updateSubtaskState(1, SubTaskState.TODO);
// 子任务 1: 实现后端 → TODO ← 已重置
为什么不自动重置? 避免重复工作、保持灵活性、防止意外丢失已完成的工作。
疑问 7:为什么 Tool 结果不单独持久化?
解答:Tool 结果通过 Memory 持久化(作为 TOOL 角色的 Msg),保持对话历史的完整性和一致性。
// 工具执行后,结果被封装为 Msg 存入 Memory
Msg toolResultMsg = Msg.builder()
.name("generate_response")
.role(MsgRole.TOOL)
.content(ToolResultBlock.builder()
.toolName("generate_response")
.result("执行成功")
.build())
.build();
memory.addMessage(toolResultMsg);
设计优势:
| 优势 | 说明 |
|---|---|
| 一致性 | 工具结果是对话历史的一部分,与其他消息保持一致 |
| 简化 | 统一用 Memory 管理所有消息,不需要额外的存储机制 |
| 顺序维护 | 保持工具调用的时间顺序,与对话历史自然衔接 |
| 完整性 | 整个对话历史作为一个单元保存,不会丢失任何上下文 |
疑问 8:A2A 是 agent 调用 agent 吗?
解答:是的,A2A 是分布式版本的 @ 机制,支持跨进程的 Agent 互调。
opencode @ 机制(进程内):
@explore → 调用探索 Agent
@librarian → 调用图书管理员 Agent
A2A 协议(跨进程):
@weather-agent → 调用远程天气 Agent
@translation-agent → 调用远程翻译 Agent
对比:
| 维度 | MsgHub | A2A |
|---|---|---|
| 通信范围 | 进程内 | 跨进程/跨机器 |
| 通信方式 | 内存中的方法调用 | HTTP/JSON-RPC |
| 适用场景 | 单体应用内的多 Agent 协作 | 分布式 Agent 微服务 |
疑问 9:ai-nacos 可以和 A2A 协作吗?
解答:是的,NacosA2aRegistry 注册 AgentCard 到 Nacos,NacosAgentCardResolver 从 Nacos 发现远程 Agent。
服务端:注册 Agent
NacosA2aRegistry registry = new NacosA2aRegistry(nacosClient);
registry.register(agentCard, endpoint).block();
客户端:发现远程 Agent
NacosAgentCardResolver resolver = new NacosAgentCardResolver(nacosClient);
AgentCard card = resolver.resolve("weather-agent").block();
A2aAgent remoteAgent = A2aAgent.builder()
.agentCard(card)
.build();
优势:自动发现、动态更新、负载均衡、健康检查。
疑问 10:工具分组的启用和禁用场景?
解答:工具分组适用于以下场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 不同任务使用不同工具集 | 根据任务类型激活相应工具 | 翻译任务激活翻译工具,计算任务激活计算器 |
| 权限控制 | 不同角色的 Agent 使用不同工具 | 管理员有所有工具,普通用户只有基础工具 |
| 动态工具管理 | 运行时根据上下文切换工具 | 游戏不同阶段使用不同工具 |
| MCP 工具管理 | 管理远程 MCP 工具的启用/禁用 | 根据网络状况动态切换本地/远程工具 |
// 创建分组
toolkit.createGroup("basic");
toolkit.createGroup("advanced");
// 注册工具到分组
toolkit.registerTool(new SearchTool(), "basic");
toolkit.registerTool(new AnalysisTool(), "advanced");
// 激活/停用分组
toolkit.activateGroup("basic");
toolkit.deactivateGroup("advanced");
疑问 11:怎么区分 MCP tool 和普通 tool?
解答:通过定义方式和执行位置区分:
| 维度 | 普通 Tool | MCP Tool |
|---|---|---|
| 定义方式 | @Tool 注解定义 Java 方法 | McpTool 类实现 |
| 执行位置 | 本地进程内 | 远程服务器 |
| 通信方式 | Java 反射调用 | MCP 协议(HTTP/JSON-RPC) |
| 注册方式 | toolkit.registerTool(new MyTool()) | toolkit.registerTool(mcpTool) |
代码示例:
// 普通 Tool
@Tool(name = "calculate", description = "计算")
public String calculate(String expr) {
return eval(expr);
}
toolkit.registerTool(new MathTool());
// MCP Tool
McpTool remoteTool = mcpClientWrapper.discoverTool("query_weather");
toolkit.registerTool(remoteTool);
选择建议:
- 本地逻辑:用普通 Tool(简单、高效)
- 远程服务:用 MCP Tool(支持分布式、动态发现)
疑问 12:RAG 中句子分块怎么分?
解答:TextChunker 没有专门的句子分块策略,但可以通过 PARAGRAPH 策略按段落分块,或自定义 SentenceChunker 按句号分割。
// 方案 1:使用段落分块
TextChunker chunker = new ParagraphChunker(300, 30);
// 方案 2:自定义句子分块器
public class SentenceChunker implements TextChunker {
@Override
public List<String> chunk(String text) {
// 按句号、问号、感叹号分割
String[] sentences = text.split("(?<=[。!?.!?])\\s*");
return Arrays.stream(sentences)
.filter(s -> !s.trim().isEmpty())
.collect(Collectors.toList());
}
}
建议:
- 如果文档有明确段落结构,用
PARAGRAPH - 如果需要更细粒度,自定义
SentenceChunker - 通常段落级别的分块效果更好,保持了上下文完整性
疑问 13:如何防止关键内容被分割?
解答:使用重叠分割(Overlap),相邻块共享部分内容,确保关键词完整包含在至少一个块中。
// 推荐:overlapSize = chunkSize 的 10-20%
TextChunker chunker = new CharacterChunker(
500, // chunkSize
50 // overlapSize(500 的 10%)
);
示例:
没有重叠:
块 1: "请假需要提前3天"
块 2: "向直属领导申请"
问题: "提前3天向直属领导申请" 被切断
有重叠(overlap=50 字符):
块 1: "请假需要提前3天向直属领导"
块 2: "提前3天向直属领导申请并填写"
优势: 无论检索哪个块,都能找到完整信息
最佳实践:
- overlap 太小:关键信息可能在边界被切断
- overlap 太大:存储空间浪费,检索效率降低
- 推荐:chunkSize 的 10-20%
疑问 14:多模态是针对输入还是输出?
解答:主要针对输入(用户发送图片/音频/视频给 Agent),但也可以用于输出(Agent 生成图片/音频)。
输入场景(更常见):
// 用户发送图片给 Agent
Msg userMsg = Msg.builder()
.name("user")
.role(MsgRole.USER)
.content(
TextBlock.builder().text("这是什么?").build(),
ImageBlock.builder().url("https://example.com/photo.jpg").build()
)
.build();
// Agent 理解图片并回复
Msg response = agent.call(List.of(userMsg)).block();
// "这是一只金毛犬"
输出场景(较少见):
// Agent 调用图像生成工具
@Tool(name = "generate_image")
public String generateImage(String prompt) {
return imageGenerator.generate(prompt); // 返回图片 URL
}
对比:
| 方向 | 使用频率 | 典型场景 |
|---|---|---|
| 输入 | 高 | 图片理解、语音识别、视频分析 |
| 输出 | 低 | 图像生成、TTS 语音合成 |
疑问 15:狼人杀项目使用了 AudioBlock?
解答:没有。狼人杀项目使用的是 DashScopeRealtimeTTSModel,不是 AudioBlock。
| 组件 | 用途 | 说明 |
|---|---|---|
AudioBlock | 多模态内容块 | 用于在 Msg 中传递音频数据(URL/Base64) |
DashScopeRealtimeTTSModel | TTS 模型 | 将文本转换为语音,直接调用 API |
狼人杀中的 TTS 实现:
// 创建 TTS 模型
DashScopeRealtimeTTSModel ttsModel = DashScopeRealtimeTTSModel.builder()
.apiKey(apiKey)
.build();
// 使用 TTS 生成语音(不是通过 AudioBlock)
String text = "昨晚死的是张三";
byte[] audioData = ttsModel.synthesize(text).block();
// 将语音发送给前端播放
emitter.sendAudio(audioData);
关键区别:
- AudioBlock:是 ContentBlock 的一种,用于在 Msg 中存储音频数据
- TTS 模型:是 Model 的一种,直接调用 API 生成语音,结果不通过 AudioBlock 传递
- 狼人杀项目用的是后者,TTS 生成的语音直接发送给前端,不经过 Msg 体系
二十二、MCP(Model Context Protocol)
22.1 MCP 解决什么问题?
每个 LLM 提供商的工具调用格式都不同,开发者需要为每个提供商单独适配。MCP 提供统一的工具协议,开发者只需要实现一次,所有 LLM 提供商都能调用。
核心问题:
LLM 提供商 A:工具格式 A
LLM 提供商 B:工具格式 B
LLM 提供商 C:工具格式 C
开发者:需要为每个提供商写适配代码 😩
MCP:统一协议
开发者:实现一次,所有提供商通用 🎉
22.2 核心概念
MCP Server:工具提供方,注册工具并监听请求
// MCP Server 注册工具
McpServer server = McpServer.builder()
.name("my-tools")
.version("1.0.0")
.tool(new WeatherTool()) // 注册天气工具
.build();
server.start(); // 启动监听
MCP Client:工具调用方,连接 Server 并调用工具
// MCP Client 连接 Server
McpClient client = McpClient.builder()
.serverUrl("http://localhost:8080")
.build();
// 调用工具
Object result = client.callTool("get_weather", Map.of("city", "北京"));
通信协议:JSON-RPC
// 请求
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {"city": "北京"}
},
"id": 1
}
// 响应
{
"jsonrpc": "2.0",
"result": {"temperature": "25°C"},
"id": 1
}
22.3 工作流程
完整流程:
// 1. 启动 MCP Server,注册工具
McpServer server = McpServer.builder()
.tool(new WeatherTool())
.tool(new CalculatorTool())
.build();
server.start();
// 2. MCP Client 连接 Server
McpClient client = McpClient.connect("http://localhost:8080");
// 3. 获取工具列表
List<ToolInfo> tools = client.listTools();
// [WeatherTool, CalculatorTool]
// 4. 注册工具到 AgentScope
AgentScope.registerMcpTools(tools);
// 5. Agent 调用工具,通过 MCP 协议执行
Agent agent = Agent.builder()
.name("assistant")
.model(model)
.tools(AgentScope.getMcpTools()) // 获取 MCP 工具
.build();
// Agent 调用工具时,自动通过 MCP 协议执行
Msg response = agent.call("北京天气怎么样?").block();
22.4 MCP vs 普通 Tool
| 特性 | 普通 Tool | MCP Tool |
|---|---|---|
| 执行位置 | 本地执行 | 远程执行 |
| 注册方式 | @Tool 注解 | MCP Server 注册 |
| 调用方式 | 反射调用 | MCP 协议调用 |
| 适用场景 | 单体应用 | 分布式系统 |
普通 Tool 示例:
// 本地执行,直接反射调用
@Tool(name = "get_weather")
public String getWeather(String city) {
return "晴天 25°C";
}
MCP Tool 示例:
// 远程执行,通过 MCP 协议调用
public class WeatherTool implements McpTool {
@Override
public String getName() {
return "get_weather";
}
@Override
public Object execute(Map<String, Object> args) {
String city = (String) args.get("city");
return "晴天 25°C";
}
}
22.5 使用场景
第三方工具服务:调用外部 API
// 调用外部天气 API
McpServer server = McpServer.builder()
.tool(new ExternalWeatherApiTool()) // 封装外部 API
.build();
跨组织共享工具:不同组织的工具互调
组织 A 的 MCP Server:提供数据分析工具
组织 B 的 MCP Server:提供机器学习工具
组织 C 的 Client:同时调用 A 和 B 的工具
动态工具发现:运行时动态注册工具
// 运行时动态添加工具
McpServer server = McpServer.builder().build();
server.start();
// 动态注册新工具
server.registerTool(new NewTool());
22.6 狼人杀中的使用
狼人杀项目没有使用 MCP,也没有实际注册普通 Tool。前文提到的投票、杀人、查验等动作,主要由两层机制完成:
- Agent 决策层:AI 玩家通过结构化输出返回目标玩家、理由等字段
- 游戏裁判层:
WerewolfWebGame根据结构化结果更新游戏状态
也就是说,狼人杀里的"工具感"来自业务代码,而不是来自 @Tool 或 MCP Server。这个区别很重要:结构化输出适合让 Agent 做选择,Tool/MCP 适合让 Agent 调用外部能力。
为什么不用 MCP:
- 狼人杀是单体应用,所有游戏逻辑在同一进程
- Agent 只需要发言和做选择,不需要调用外部服务
- 游戏规则由裁判端控制,比让 Agent 直接调用
vote/kill工具更安全
22.7 总结
MCP 是 AgentScope 的外部工具协议,提供统一的工具调用接口,支持跨进程、跨组织的工具共享。
核心价值:
- 统一协议:一次实现,所有 LLM 通用
- 分布式支持:跨进程、跨组织调用
- 动态发现:运行时动态注册工具
与普通 Tool 的关系:
- 普通 Tool:本地执行,适合单体应用
- MCP Tool:远程执行,适合分布式系统
- 两者互补,根据场景选择
二十三、Harness Framework 与近期更新
23.1 Harness 解决什么问题?
如果说 ReActAgent 解决的是单次调用里的 reason → act → observe,那么 Harness 解决的是生产环境里的连续性问题:
- 下一轮对话怎么接上上一轮?
- 明天重启后怎么恢复状态?
- 上下文爆了怎么办?
- 工具结果太大怎么办?
- 多用户、多副本时工作区和记忆放在哪里?
- 复杂任务要不要拆给子 Agent?异步任务怎么回收结果?
- 需要执行 Shell 或代码时,怎么不把宿主机暴露给不可信输入?
官方把 agentscope-harness 定义为基于 agentscope-core 的生产运行层,入口类是 HarnessAgent。它不替换 ReActAgent 的推理循环,而是在关键生命周期点注入 Hook,并补齐一套内置工具和工作区约定。
23.2 一句话理解 Harness
Harness = ReActAgent + Workspace + RuntimeContext + 持久化记忆 + 上下文压缩 + 文件系统抽象 + 子 Agent 编排 + 安全执行边界。
它把前面分散讲过的能力重新组织成一个运行时:
| 前文概念 | Harness 中的落点 |
|---|---|
| Hook | WorkspaceContextHook、MemoryFlushHook、CompactionHook、SessionPersistenceHook 等 |
| Toolkit | 自动注册文件、记忆检索、会话查询、子任务管理、条件性 Shell 执行工具 |
| Memory | 每日记忆流水账 + MEMORY.md 长期记忆 + 全文检索 |
| State / Session | 基于 sessionId 的状态快照和会话日志 |
| Tool | 文件工具、记忆工具、子 Agent 工具、沙箱执行工具 |
| Subagent | 通过 subagents/ 声明或代码配置,支持同步/异步委派 |
23.3 快速使用方式
引入依赖:
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-harness</artifactId>
<version>${agentscope.version}</version>
</dependency>
最小使用形态:
HarnessAgent agent = HarnessAgent.builder()
.name("my-agent")
.model(model)
.workspace(Paths.get(".agentscope/workspace"))
.compaction(CompactionConfig.builder()
.triggerMessages(50)
.keepMessages(20)
.build())
.build();
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("user-session-001") // 相同 sessionId 自动续接上下文
.userId("alice") // 多用户场景用于命名空间隔离
.build();
Msg reply = agent.call(userMessage, ctx).block();
RuntimeContext 是这套机制的身份载体:sessionId 决定会话状态和日志路径,userId 决定多租户命名空间。它不是长期状态本身,而是每次 call() 时传给 Hook 和 Tool 的"这一轮是谁"。
23.4 Workspace:Agent 的唯一事实来源
Harness 的核心是工作区。一个典型工作区长这样:
workspace/
├── AGENTS.md # Agent 人格与行为约定,每轮推理前注入
├── MEMORY.md # 长期记忆,由后台维护
├── knowledge/ # 领域知识
├── skills/ # 可复用技能
├── subagents/ # 子 Agent 规格
└── agents/<agentId>/
├── context/ # 会话状态快照
├── sessions/ # 对话 JSONL 与压缩上下文
└── memory/ # 每日记忆流水账
每轮推理前,WorkspaceContextHook 会把 AGENTS.md、MEMORY.md、knowledge/ 等内容注入 system prompt。对话结束后,MemoryFlushHook 从本轮对话中提炼新事实,写入每日记忆;后台的 MemoryConsolidator 再把流水账合并成更精炼的长期记忆。
这意味着 Agent 不再是"每次重新初始化的聊天窗口",而是有一个会随使用积累经验的工作区。
23.5 上下文管理:压缩、溢出恢复、大工具结果卸载
Harness 把长对话上下文问题拆成三层:
| 问题 | Harness 机制 |
|---|---|
| 历史消息太多 | CompactionHook 超阈值后摘要历史,只保留近期尾部 |
| 模型真的报 context overflow | HarnessAgent 捕获错误,强制压缩后自动重试 |
| 单个工具结果太大 | ToolResultEvictionHook 把大结果写入文件系统,只把 head/tail 预览放回上下文 |
这和普通 Memory 的区别是:Memory 只回答"历史存在哪里",Harness 进一步回答"什么时候该压缩、压缩前要不要提炼事实、压缩后怎么恢复可检索性"。
23.6 文件系统与沙箱:同一套逻辑适配三种环境
Harness 的所有文件操作都经过 AbstractFilesystem,因此 Agent 逻辑不必绑定本地磁盘。官方文档把它分成三种声明式模式:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 本机 + Shell | 默认模式,工作区在本地目录,可按配置开放 Shell | 个人助手、本地 Coding Agent、开发测试 |
| 远端共享存储 | 记忆和会话日志落到共享存储,默认不注册 Shell 工具 | 多副本在线服务、企业业务 Agent |
| 沙箱执行 | 文件读写和命令执行都在隔离沙箱中完成,状态可恢复 | DataAgent、SRE Agent、需要执行不可信代码的场景 |
关键点是:execute 不是永远可用。远端共享存储模式默认不注册 Shell 工具,这是安全设计;只有本地 Shell 或沙箱后端明确支持执行时,框架才暴露执行能力。
23.7 Subagent:声明式子 Agent 与异步任务
Harness 支持把子 Agent 写成工作区里的规格文件,也支持通过代码配置。父 Agent 可以通过工具进行委派:
- 同步委派:父 Agent 阻塞等待子 Agent 结果,适合必须拿到结果才能继续的任务
- 异步委派:父 Agent 拿到任务 ID 后继续工作,之后用
task_output查询结果
为了避免无限递归,子 Agent 默认是叶子形态,框架还会限制最大调用深度。这个设计把"多 Agent 编排"从手写 MsgHub / Pipeline 组合,推进到更工程化的任务分发、状态管理和结果回收。
23.8 最近几个版本的重点变化
截至 2026-05-18,GitHub Releases 中可见的最新版本是 v1.1.0-RC1(2026-05-11),官方博客和文档已经按 AgentScope Java 1.1 Harness 体系介绍。最近几个版本的主线很清晰:从"能写 Agent"走向"能长期运行、能分布式部署、能安全执行工具"。
| 版本 | 重点更新 | 对开发者的意义 |
|---|---|---|
v1.1.0-RC1 | 新增 agentscope-harness、HarnessAgent、Workspace、AbstractFilesystem、会话持久化、长记忆、上下文压缩、子 Agent、内置工具、沙箱模式 | 把生产级 Agent 运行时打包成默认能力 |
v1.0.12 | 百炼长记忆、MCP 集成增强、内置 tracing、AutoContextMemory 稳定性、大消息 offloading、RAG 修复、依赖升级 | 增强长会话、可观测和工具调用稳定性 |
v1.0.11 | 优雅关闭、RocketMQ A2A、MySQL Skill Repository、Redis Session Client、流式动态结构化输出、prompt cache、reasoning token 统计、多模态视频工具 | 补齐分布式消息、持久化生态、成本优化和流式结构化能力 |
v1.0.10 | Nacos AI Registry、Prompt 运行时绑定、Skill 在线/离线加载、TTS 接入狼人杀、qwen3.5-plus、Hook 排序 | 让 Prompt/Skill 能集中治理和热更新,狼人杀示例也加入语音能力 |
23.9 Harness 和狼人杀项目的关系
狼人杀示例主要展示的是 AgentScope 的基础机制:Msg、ReActAgent、UserAgent、结构化输出、MsgHub、HITL。Harness 则是这些机制在生产环境里的封装:
- 如果只是跑一局狼人杀,
ReActAgent + MsgHub足够 - 如果要把狼人杀变成长期运行的在线房间,需要 Harness 的
sessionId、会话日志和状态恢复 - 如果要让 AI 玩家长期记住某个真人玩家的风格,需要 Harness 的长期记忆
- 如果要让 AI 玩家调用脚本分析战局,需要 Harness 的沙箱执行
- 如果要把"法官"、“策略分析师”、“复盘助手"拆成多个 Agent,需要 Harness 的子 Agent 编排
所以可以把 Harness 理解成:不是替代狼人杀项目里学到的核心概念,而是把这些概念推向长期运行、可恢复、可隔离、可扩展的工程形态。
23.10 官方资料
二十四、剩余主题与进阶学习
24.1 还有什么主题没学?
已覆盖的核心主题:
- Msg、Agent、Memory、Model、Formatter
- Tool、Hook、Pipeline、MsgHub
- 结构化输出、流式输出、Prompt 管理
- 持久化、PlanNotebook
- A2A、Nacos 集成、MCP
- RAG、多模态
- WebUserInput(HITL)
- Harness Framework、Workspace、AbstractFilesystem、Session Persistence、Subagent
还没覆盖的主题:
- 测试:如何测试 Agent(单元测试、集成测试)
- 监控:如何监控 Agent 的性能(Tracing、Metrics)
- 安全:如何保护 Agent 的安全(权限、审计)
- 部署:如何部署 Agent 到生产环境(Docker、K8s)
- 性能优化:如何优化 Agent 的性能(缓存、并发)
- 错误处理:如何处理 Agent 的异常(重试、降级)
- 日志:如何记录 Agent 的日志(结构化日志)
24.2 关于学习 langchain4j 和 spring ai
建议:不需要先学它们,直接参与 agentscope4j 开发
原因:
- agentscope4j 已经足够完整
- langchain4j 和 spring ai 是不同框架,设计理念、API 风格都不同
- 直接参与开发是最好的学习方式
框架对比:
- agentscope4j:阿里开源,Java 原生,支持 A2A、MCP,适合企业级 Agent 应用
- langchain4j:LangChain 的 Java 版,社区活跃,适合快速原型开发
- spring ai:Spring 官方,与 Spring 生态集成,适合 Spring 项目集成
二十五、参与 agentscope4j 开发
25.1 选择贡献方向
| 方向 | 难度 | 说明 |
|---|---|---|
| 文档完善 | ⭐ | 补充文档、示例、教程 |
| Bug 修复 | ⭐⭐ | 修复已知 Issue |
| 功能增强 | ⭐⭐⭐ | 添加新特性 |
| 性能优化 | ⭐⭐⭐ | 优化现有功能 |
| 测试补充 | ⭐⭐ | 补充单元测试、集成测试 |
25.2 推荐学习路径
阶段 1:熟悉项目结构(1-2 天)
- 阅读 README.md
- 了解模块划分(core、extensions、examples)
- 运行示例项目
阶段 2:选择贡献方向(1 天)
- 浏览 Issues,选择一个简单的任务
- 阅读相关代码,理解实现
- 与维护者沟通,确认方向
阶段 3:开始贡献(持续)
- Fork 仓库
- 创建分支
- 实现功能
- 提交 PR
- 参与 Code Review
25.3 项目结构
agentscope-java 项目结构:
- agentscope-core:核心模块(Agent、Msg、Memory、Model、Tool 等)
- agentscope-extensions:扩展模块(RAG、A2A、MCP、Nacos 等)
- agentscope-harness:生产级运行时模块(Workspace、长记忆、上下文压缩、会话恢复、沙箱、子 Agent)
- agentscope-examples:示例模块(狼人杀、A2A 示例等)
- agentscope-spring-boot-starters:Spring Boot 集成
25.4 如何开始
- 克隆仓库:
git clone https://github.com/agentscope-ai/agentscope-java.git - 阅读贡献指南:
CONTRIBUTING.md - 浏览 Issues:选择一个简单的任务
- Fork 仓库,创建分支
- 实现功能,提交 PR
25.5 总结
不需要先学 langchain4j 和 spring ai,直接参与 agentscope4j 开发是最好的学习方式。从小任务开始(文档、示例、Bug 修复),逐步深入。
最终总结
已学习的主题
- Msg 消息体系 - 消息的构建、传递和处理
- Agent 体系 - Agent 的创建、配置和生命周期
- Memory 记忆管理 - 短期和长期记忆管理
- Model 模型接入 - LLM 模型的接入和调用
- Formatter 格式化 - 消息格式化和模板处理
- Tool 工具调用 - @Tool 注解和工具调用机制
- Hook 生命周期 - Agent 执行前后的钩子函数
- Pipeline 编排 - 多 Agent 的流程编排
- MsgHub 多智能体通信 - 多 Agent 之间的消息传递
- 结构化输出 - JSON Schema 结构化输出
- 流式输出 - SSE 流式响应
- Prompt 管理 - 提示词模板和管理
- 持久化与状态管理 - 状态保存和恢复
- PlanNotebook 任务规划 - 复杂任务的规划和执行
- A2A 协议 - Agent-to-Agent 通信协议
- Nacos 集成 - 服务发现和负载均衡
- MCP(Model Context Protocol) - 统一的外部工具协议
- RAG(检索增强生成) - 知识检索和增强生成
- 多模态 - 图片、音频、视频等多模态内容处理
- WebUserInput(HITL) - Web 界面的人机交互
- Harness Framework - Workspace、RuntimeContext、上下文压缩、会话恢复、沙箱和子 Agent 编排
- 近期版本演进 - Nacos AI Registry、长记忆、优雅关闭、RocketMQ A2A、prompt cache、tracing 等
学习成果
系统性学习了 AgentScope4J 从基础框架到最新 Harness 运行时的核心概念,包括:
- 基础概念:Msg、Agent、Memory、Model、Formatter
- 通信机制:MsgHub、A2A、MCP
- 扩展能力:RAG、多模态、Hook、Tool
- 生产化:持久化、流式输出、Prompt 管理、PlanNotebook、Harness
- 分布式:Nacos、A2A、MCP、RocketMQ A2A、Redis Session、远端共享存储
- 人机交互:WebUserInput(HITL)
- 长期运行:Workspace、长记忆、上下文压缩、会话恢复、沙箱执行、子 Agent 委派
博客文件
- 路径:
/Users/chinazhouwy/doc/articles/content/posts/AgentScope4J 深度学习指南 - 从狼人杀项目掌握多智能体框架.md - 大小:扩展后约 3900+ 行
- 内容:完整的 AgentScope4J 学习指南,从狼人杀实战,到 A2A/MCP/RAG,再到 Harness 生产运行时
下一步
- 读 Harness 源码:从
HarnessAgent、WorkspaceContextHook、CompactionHook、SessionPersistenceHook开始 - 改造狼人杀:尝试用 Harness 增加长期房间、玩家画像记忆、复盘子 Agent
- 参与开发:围绕文档、示例、Harness 工具、测试补充切入
- 分享交流:发布到技术博客平台,并持续跟进 1.1 正式版之后的新能力