总述
消息(Message)是AgentScope中Agent之间的通信单元,也是整个框架运行的基础。理解消息系统的设计,就能理解AgentScope如何在保持灵活性的同时,提供类型安全和可靠性。
本章将从消息的设计哲学、核心结构、内容块系统、多模态支持、元数据管理等多个维度,深入讲解AgentScope的消息系统。
2.1 Msg 消息模型设计
2.1.1 为什么需要消息模型?
问题背景
在Agent系统中,不同的参与者(用户、AI模型、工具、其他Agent)需要进行通信。简单的做法是直接传递字符串或对象,但这样会导致以下问题:
javascript
问题1:信息丢失
└─ 仅传递内容,丢失了:谁说的(role)、什么时候说的(timestamp)、
说了什么ID(message_id)等元数据
问题2:无法应对多样化内容
└─ 内容可能是纯文本、图像、音频、视频,甚至是工具调用请求
单一的String无法表达
问题3:缺乏结构化数据支持
└─ Agent有时需要返回结构化数据(JSON对象),但无法与文本区分
问题4:通信追踪困难
└─ 没有统一的消息格式,很难进行日志、审计、重放
2.1.2 Msg的核心设计
设计原则
原则1:不可变性(Immutable)
├─ 消息创建后不能修改
├─ 保证线程安全(在响应式架构中至关重要)
└─ 便于消息缓存和比较
原则2:最小化设计
├─ 只包含必要字段,避免冗余
├─ 易于序列化和网络传输
└─ 快速解析和处理
原则3:可扩展性
├─ 支持多种内容类型
├─ 元数据字段可扩展
└─ 便于第三方扩展
Msg的核心字段
java
public class Msg {
private final String id; // ✓ 唯一标识,用于追踪
private final String name; // ✓ 可选的消息名称
private final MsgRole role; // ✓ 消息角色(user/assistant/system/tool)
private final List<ContentBlock> content; // ✓ 消息内容(支持多种类型)
private final Map<String, Object> metadata;// ✓ 元数据(结构化数据、扩展信息)
private final String timestamp; // ✓ 时间戳(ISO 8601格式)
}
字段详解
| 字段 | 类型 | 必需 | 说明 | 示例 |
|---|---|---|---|---|
id |
String | 是 | 消息唯一ID | "msg_123e4567-e89b-12d3-a456" |
name |
String | 否 | 发送者名称 | "user", "assistant", "system" |
role |
MsgRole | 是 | 消息角色 | USER, ASSISTANT, SYSTEM, TOOL |
content |
List | 是 | 内容块列表 | [TextBlock, ImageBlock, ...] |
metadata |
Map<String, Object> | 否 | 元数据 | {"user_id": "123", "session": "abc"} |
timestamp |
String | 是 | 时间戳 | "2024-01-15 10:30:45.123" |
2.1.3 Msg的生命周期
markdown
第1阶段:创建(Creation)
Msg.builder()
.role(MsgRole.USER)
.content(TextBlock.builder().text("Hello").build())
.build()
└─ 自动生成ID和timestamp
第2阶段:传输(Transport)
- JSON序列化(网络传输或存储)
- 不可变,支持并发访问
第3阶段:处理(Processing)
Agent接收消息
│
├─ 添加到Memory(保留历史)
├─ 提取内容并处理
└─ 可能修改元数据(但这会创建新的Msg对象)
第4阶段:响应(Response)
生成新的Msg对象作为响应
└─ 包含原消息ID的引用(通过metadata)
第5阶段:持久化(Persistence)
存储到Session或数据库
└─ 支持后续的消息重放和审计
2.1.4 构建消息的多种方式
方式1:使用Builder构建最小化消息
java
// 方式1a:最简单的纯文本消息
Msg msg = Msg.builder()
.textContent("你好,今天天气怎么样?")
.build();
// 方式1b:明确指定角色
Msg msg = Msg.builder()
.role(MsgRole.USER)
.content(TextBlock.builder().text("Hello").build())
.build();
// 方式1c:指定发送者名称
Msg msg = Msg.builder()
.name("Alice") // 用户名
.role(MsgRole.USER)
.textContent("I have a question")
.build();
方式2:构建多内容消息
java
// 包含文本和图像的消息
Msg msg = Msg.builder()
.role(MsgRole.USER)
.name("user_123")
.content(
TextBlock.builder().text("请帮我分析这张图片").build(),
ImageBlock.builder()
.source(URLSource.builder()
.url("https://example.com/chart.png")
.build())
.build()
)
.build();
// 或使用List.of()
List<ContentBlock> content = List.of(
TextBlock.builder().text("What's in this image?").build(),
ImageBlock.builder().source(urlSource).build()
);
Msg msg = Msg.builder()
.role(MsgRole.USER)
.content(content)
.build();
方式3:携带元数据的消息
java
// 包含结构化数据的消息
Msg msg = Msg.builder()
.role(MsgRole.ASSISTANT)
.textContent("订单已创建")
.metadata(Map.of(
"order_id", "ORD-20240115-001",
"amount", 599.99,
"timestamp", System.currentTimeMillis()
))
.build();
// 获取元数据
if (msg.hasStructuredData()) {
Map<String, Object> data = msg.getMetadata();
String orderId = (String) data.get("order_id");
}
方式4:携带结构化对象的消息
java
// 定义结构化数据类
@Data
@Builder
public class OrderResponse {
private String orderId;
private double amount;
private String status;
private LocalDateTime createdTime;
}
// 创建包含结构化数据的消息
OrderResponse orderResp = OrderResponse.builder()
.orderId("ORD-001")
.amount(599.99)
.status("CREATED")
.createdTime(LocalDateTime.now())
.build();
// 方式A:通过metadata存储(然后通过getStructuredData()获取)
Msg msg = Msg.builder()
.role(MsgRole.ASSISTANT)
.textContent("订单创建成功")
.metadata(Map.of(
"order", orderResp // 直接放入对象
))
.build();
// 获取时
if (msg.hasStructuredData()) {
OrderResponse order = msg.getStructuredData(OrderResponse.class);
}
2.2 消息角色(MsgRole)与通信模式
2.2.1 四种消息角色
1. USER 角色
定义:来自用户或外部输入的消息
java
// 典型用法
Msg userMsg = Msg.builder()
.role(MsgRole.USER)
.name("Alice") // 用户名
.textContent("请帮我写一份会议总结")
.build();
特点:
- 通常是对话的起点
- 可以包含多种内容类型(文本、图像、文件URL等)
- 通常来自前端应用或API客户端
场景示例:
sql
用户在聊天界面输入消息 → 转换为 USER 角色消息 → 送给Agent
2. ASSISTANT 角色
定义:由AI Agent或助手生成的消息
java
// 典型用法
Msg assistantMsg = Msg.builder()
.role(MsgRole.ASSISTANT)
.name("ChatBot") // Agent名称
.textContent("我已经为你准备了会议总结...")
.build();
特点:
- 是Agent的回复
- 可能包含推理过程(ThinkingBlock)、工具调用(ToolUseBlock)等
- 通常会被添加到Memory中,作为后续对话的上下文
生产场景:
Agent推理 → 调用工具 → 获得结果 → 生成最终回复(ASSISTANT消息)
3. SYSTEM 角色
定义:系统指令、提示词或配置消息
java
// 系统提示词(通常在Agent创建时设置)
Msg systemMsg = Msg.builder()
.role(MsgRole.SYSTEM)
.textContent(
"你是一个专业的技术文档撰写员。\n" +
"你的职责是:\n" +
"1. 将复杂的技术概念用简洁的语言解释\n" +
"2. 提供代码示例\n" +
"3. 遵循 Markdown 格式"
)
.build();
// 或在ReActAgent中设置
ReActAgent agent = ReActAgent.builder()
.sysPrompt("你是一个专业的技术文档撰写员...")
.build();
特点:
- 在对话开始时发送给LLM
- 指导Agent的行为和输出格式
- 通常不会改变(除非需要更新Agent的人设)
关键用途:
sql
SYSTEM消息定义的内容包括:
├─ Agent的角色和背景
├─ 行为规则和约束
├─ 输出格式要求
├─ 处理特殊情况的指示
└─ 可信度和安全性指导
4. TOOL 角色
定义:工具执行结果消息
java
// 工具执行后返回的消息
Msg toolResultMsg = Msg.builder()
.role(MsgRole.TOOL)
.name("database_query_tool") // 工具名称
.textContent("SELECT * FROM users WHERE id='123' RETURNED: ...")
.metadata(Map.of(
"tool_call_id", "call_abc123", // 关联到原始工具调用
"execution_time", 45, // 执行耗时(毫秒)
"success", true
))
.build();
特点:
- 由工具执行器生成
- 包含工具的执行结果
- 通常包含关键的元数据(如执行时间、错误信息等)
工具调用-结果流程:
Agent思考:需要查询数据库
↓
Agent发起工具调用(ToolUseBlock)
↓
ToolExecutor执行工具
↓
生成TOOL角色的消息,包含结果
↓
Agent接收结果,继续推理
2.2.2 消息角色在ReAct循环中的应用
csharp
第1轮对话:
┌─────────────────────────────────────┐
│ USER: "计算过去30天的平均销售额" │
└────────────────┬────────────────────┘
↓
[Agent推理阶段]
↓
┌─────────────────────────────────────┐
│ ASSISTANT: 思考过程... │
│ "我需要: │
│ 1. 查询过去30天的销售数据 │
│ 2. 计算平均值 │
│ 调用 query_sales 工具..." │
└────────────────┬────────────────────┘
↓
[Agent行动阶段]
↓
┌─────────────────────────────────────┐
│ TOOL: query_sales 返回结果 │
│ [{"date": "2024-01-01", "amount": ...}] │
└────────────────┬────────────────────┘
↓
[Agent继续推理]
↓
┌─────────────────────────────────────┐
│ ASSISTANT: 最终答案 │
│ "过去30天的平均销售额为 ¥50,000" │
└─────────────────────────────────────┘
2.2.3 多轮对话中的消息组织
java
// 模拟一个完整的多轮对话流程
public void demonstrateMultiRoundConversation() {
ReActAgent agent = ReActAgent.builder()
.name("SalesAnalyst")
.sysPrompt("你是一个销售分析专家。...")
.model(model)
.toolkit(toolkit)
.build();
// 第1轮:用户提问
System.out.println("=== 第1轮对话 ===");
Msg userMsg1 = Msg.builder()
.role(MsgRole.USER)
.textContent("过去7天的销售趋势是什么?")
.build();
Msg response1 = agent.call(userMsg1).block();
System.out.println("Agent: " + response1.getTextContent());
// 内部消息流:
// USER消息 → Memory记录 → Model推理
// → ASSISTANT消息 → 调用工具
// → TOOL消息 → 记录结果
// → ASSISTANT消息(最终) → 返回给用户
// 第2轮:基于第1轮结果的追问
System.out.println("\n=== 第2轮对话 ===");
Msg userMsg2 = Msg.builder()
.role(MsgRole.USER)
.textContent("对比去年同期,增长了多少?")
.build();
Msg response2 = agent.call(userMsg2).block();
System.out.println("Agent: " + response2.getTextContent());
// Agent自动从Memory获得第1轮的对话上下文
// 因此能够理解"去年同期"的含义
}
2.3 内容块(ContentBlock)系统
2.3.1 ContentBlock的设计
为什么需要内容块系统?
arduino
场景:一个对话中可能包含
├─ 用户的文本问题:"请分析这张销售报表"
├─ 用户上传的图表图片:PNG格式,编码为Base64
├─ Agent的推理过程:对图表的理解
├─ Agent调用的工具:数据库查询工具
└─ 工具的返回结果:查询到的数据
问题:如何用统一的方式表达这些不同类型的内容?
方案:使用多态的ContentBlock体系
└─ 每种内容类型都是ContentBlock的实现
通过模式匹配处理不同类型
ContentBlock的继承结构
python
ContentBlock(sealed class,已密封)
├── TextBlock # 纯文本内容
├── ThinkingBlock # Agent的推理/思考过程
├── ImageBlock # 图像内容
├── AudioBlock # 音频内容
├── VideoBlock # 视频内容
├── ToolUseBlock # 工具调用请求
└── ToolResultBlock # 工具执行结果
为什么使用sealed class?
java
优势1:类型安全
└─ 编译器确保只有声明的子类存在
优势2:模式匹配支持
└─ switch表达式可以穷举所有情况
优势3:性能优化
└─ 编译器可以进行更多优化
示例:
ContentBlock block = ...;
String content = switch (block) {
case TextBlock tb -> tb.getText();
case ImageBlock ib -> "[Image: " + ib.getSource().getUrl() + "]";
case ToolUseBlock tub -> "Tool call: " + tub.getName();
// ... 编译器确保所有情况都被处理
};
2.3.2 核心内容块详解
1. TextBlock - 文本内容
java
// 创建文本块
TextBlock textBlock = TextBlock.builder()
.text("这是一个文本块")
.build();
// 简化创建方式
Msg msg = Msg.builder()
.textContent("这是一条消息") // 自动创建TextBlock
.build();
// 实际应用
TextBlock systemPrompt = TextBlock.builder()
.text("你是一个专业的技术顾问。" +
"你的回答应该:\n" +
"1. 准确\n" +
"2. 清晰易懂\n" +
"3. 包含实例代码")
.build();
应用场景:
- 用户输入
- Agent推理结果
- 系统提示词
- 一般的文本响应
2. ThinkingBlock - 推理过程
java
// 某些模型(如Claude 3.5 Sonnet)支持返回思考过程
ThinkingBlock thinkingBlock = ThinkingBlock.builder()
.thinking("让我分析这个问题:\n" +
"1. 首先,我需要理解用户的需求\n" +
"2. 然后,我需要找到相关的信息\n" +
"3. 最后,我需要整合这些信息并生成答案")
.build();
// 消息中的thinking块
Msg msg = Msg.builder()
.role(MsgRole.ASSISTANT)
.content(
thinkingBlock, // 模型的思考过程
TextBlock.builder().text("基于上述分析,答案是...").build()
)
.build();
应用场景:
- 理解Agent的推理逻辑
- 调试Agent行为
- 提高输出的可解释性
3. ImageBlock - 图像内容
java
// 方式1:Base64编码(推荐用于小文件)
String base64Image = Base64.getEncoder().encodeToString(
Files.readAllBytes(Paths.get("chart.png"))
);
ImageBlock imageBlock1 = ImageBlock.builder()
.source(Base64Source.builder()
.data(base64Image)
.mediaType("image/png") // 必需:指定MIME类型
.build())
.build();
// 方式2:URL引用(推荐用于大文件或网络图片)
ImageBlock imageBlock2 = ImageBlock.builder()
.source(URLSource.builder()
.url("https://example.com/dashboard-screenshot.png")
.build())
.build();
// 创建含图的消息
Msg imgMsg = Msg.builder()
.role(MsgRole.USER)
.name("user_123")
.content(
TextBlock.builder().text("请分析这张图片中的数据趋势").build(),
imageBlock2 // 添加图像
)
.build();
支持的图像格式:
bash
image/png # PNG格式
image/jpeg # JPEG格式
image/gif # GIF格式
image/webp # WebP格式(现代浏览器支持)
生产场景示例:
java
// 产品截图分析
public String analyzeProductScreenshot(String imagePath) {
byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
String base64 = Base64.getEncoder().encodeToString(imageBytes);
ImageBlock screenshot = ImageBlock.builder()
.source(Base64Source.builder()
.data(base64)
.mediaType("image/png")
.build())
.build();
Msg msg = Msg.builder()
.role(MsgRole.USER)
.content(
TextBlock.builder().text("请识别这个产品界面中的问题").build(),
screenshot
)
.build();
return agent.call(msg).block().getTextContent();
}
4. AudioBlock 和 VideoBlock
java
// 音频块
AudioBlock audioBlock = AudioBlock.builder()
.source(Base64Source.builder()
.data(base64AudioData)
.mediaType("audio/mp3") // audio/wav, audio/mpeg等
.build())
.build();
// 视频块(通常用URL引用)
VideoBlock videoBlock = VideoBlock.builder()
.source(URLSource.builder()
.url("https://example.com/demo-video.mp4")
.build())
.build();
// 包含音视频的消息
Msg multimodalMsg = Msg.builder()
.role(MsgRole.USER)
.content(
TextBlock.builder().text("请转录这段音频").build(),
audioBlock
)
.build();
支持的格式:
bash
音频:audio/mp3, audio/wav, audio/mpeg
视频:video/mp4, video/mpeg, video/quicktime
5. ToolUseBlock - 工具调用
java
// Agent决定调用工具时生成此块
ToolUseBlock toolUseBlock = ToolUseBlock.builder()
.id("call_12345") // 工具调用ID
.name("query_database") // 工具名称
.arguments(Map.of( // 工具参数
"sql", "SELECT * FROM users WHERE age > 30",
"limit", 100
))
.build();
// 包含工具调用的Agent响应
Msg agentMsg = Msg.builder()
.role(MsgRole.ASSISTANT)
.content(
TextBlock.builder()
.text("我需要查询数据库以获得答案")
.build(),
toolUseBlock // 工具调用请求
)
.build();
// ToolUseBlock的内部结构
public class ToolUseBlock extends ContentBlock {
private final String id; // 唯一标识本次调用
private final String name; // 工具名称
private final Map<String, Object> arguments; // 参数
}
工具调用流程:
javascript
Agent判断需要调用工具
↓
生成ToolUseBlock
↓
ToolExecutor发现该块
↓
根据tool name找到实际工具方法
↓
将arguments转换为方法参数
↓
执行工具方法
↓
将结果包装为ToolResultBlock
6. ToolResultBlock - 工具结果
java
// 工具执行完后生成此块
ToolResultBlock toolResultBlock = ToolResultBlock.builder()
.id("call_12345") // 对应的工具调用ID
.name("query_database") // 工具名称
.content("SELECT returned 42 rows:\n[{\"id\": 1, ...}, ...]") // 结果内容
.isError(false) // 是否出错
.build();
// 在Agent响应中表示工具执行成功
Msg toolResult = Msg.builder()
.role(MsgRole.TOOL)
.name("query_database")
.content(toolResultBlock)
.metadata(Map.of(
"execution_time", 123, // 毫秒
"row_count", 42
))
.build();
// 工具执行失败的情况
ToolResultBlock errorBlock = ToolResultBlock.builder()
.id("call_12346")
.name("query_database")
.content("Error: Database connection timeout after 30s")
.isError(true) // 标记为错误
.build();
2.3.3 内容块的实际应用
场景1:多模态问答
java
// 用户上传多张图表并提问
Msg userQuery = Msg.builder()
.role(MsgRole.USER)
.name("analyst_alice")
.content(
TextBlock.builder()
.text("请对比这两张月度销售报表,找出主要差异和趋势")
.build(),
ImageBlock.builder()
.source(URLSource.builder()
.url("https://cdn.example.com/sales_jan.png")
.build())
.build(),
ImageBlock.builder()
.source(URLSource.builder()
.url("https://cdn.example.com/sales_feb.png")
.build())
.build()
)
.build();
// Agent处理
Msg response = agent.call(userQuery).block();
// Agent看到了两张图片,并理解了对比分析的需求
场景2:包含推理过程的回复
java
// Agent返回思考过程和最终答案
Msg agentResponse = Msg.builder()
.role(MsgRole.ASSISTANT)
.name("AnalysisBot")
.content(
ThinkingBlock.builder()
.thinking(
"让我分析这两个月的数据:\n" +
"1月的销售额在100-150万之间\n" +
"2月的销售额在120-180万之间\n" +
"看起来整体趋势向上...")
.build(),
TextBlock.builder()
.text(
"根据分析:\n" +
"- 2月销售额平均上升15%\n" +
"- 新产品线贡献了30%的增长\n" +
"- 建议加大营销投入")
.build()
)
.build();
场景3:工具调用和结果链
java
// 模拟完整的工具调用流程
Msg agentWithToolCall = Msg.builder()
.role(MsgRole.ASSISTANT)
.content(
TextBlock.builder()
.text("我需要查询最新的库存数据来回答你的问题")
.build(),
ToolUseBlock.builder()
.id("call_001")
.name("get_inventory")
.arguments(Map.of("product_id", "SKU-2024-001"))
.build()
)
.build();
// 工具执行后返回结果
Msg toolExecution = Msg.builder()
.role(MsgRole.TOOL)
.name("get_inventory")
.content(
ToolResultBlock.builder()
.id("call_001")
.name("get_inventory")
.content("{\"product_id\": \"SKU-2024-001\", \"stock\": 5000}")
.isError(false)
.build()
)
.build();
// Agent继续推理并给出最终答案
Msg finalResponse = Msg.builder()
.role(MsgRole.ASSISTANT)
.textContent("SKU-2024-001的库存为5000件,足以满足今年的销售计划。")
.build();
2.4 消息元数据与结构化数据
2.4.1 元数据的设计
元数据的用途
yaml
元数据(Metadata)是关于消息本身的数据,包括:
├─ 消息关系元数据
│ └─ reply_to: 关联到哪条消息的回复
│
├─ 业务元数据
│ ├─ user_id: 消息所属的用户
│ ├─ session_id: 消息所属的会话
│ └─ conversation_id: 消息所属的对话
│
├─ 结构化数据
│ ├─ 订单信息、用户信息等业务对象
│ └─ 直接存储为Map<String, Object>
│
├─ 技术元数据
│ ├─ chat_usage: Token使用统计
│ ├─ cost: API调用成本
│ └─ latency: 响应延迟
│
└─ 扩展字段
└─ 应用特定的任意键值对
2.4.2 使用元数据存储结构化数据
方式1:直接存储Map
java
// 简单的键值对
Msg msg = Msg.builder()
.role(MsgRole.ASSISTANT)
.textContent("订单已确认")
.metadata(Map.of(
"order_id", "ORD-2024-001",
"total_price", 1999.99,
"status", "confirmed",
"items_count", 3
))
.build();
// 获取元数据
Map<String, Object> metadata = msg.getMetadata();
String orderId = (String) metadata.get("order_id");
double price = (double) metadata.get("total_price");
方式2:存储POJO对象
java
// 定义结构化数据类
@Data
public class OrderData {
private String orderId;
private double totalPrice;
private String status;
private List<OrderItem> items;
private LocalDateTime createdTime;
}
@Data
public class OrderItem {
private String productId;
private String productName;
private int quantity;
private double unitPrice;
}
// 创建消息时存储对象
OrderData orderData = new OrderData();
orderData.setOrderId("ORD-2024-001");
orderData.setTotalPrice(1999.99);
orderData.setStatus("confirmed");
// ... 设置其他字段
Msg msg = Msg.builder()
.role(MsgRole.ASSISTANT)
.textContent("订单已确认")
.metadata(Map.of(
"order", orderData, // 存储对象
"operation_timestamp", System.currentTimeMillis()
))
.build();
// 获取时(推荐方式)
if (msg.hasStructuredData()) {
OrderData data = msg.getStructuredData(OrderData.class);
System.out.println("Order: " + data.getOrderId());
}
2.4.3 生产场景:Token使用追踪
java
// AgentScope自动在元数据中记录Token使用
public class TokenUsageExample {
public static void main(String[] args) {
ReActAgent agent = ReActAgent.builder()
.name("GPTAgent")
.model(DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.build())
.build();
// 调用Agent
Msg response = agent.call(
Msg.builder().textContent("用10句话概括人工智能的发展历史")
.build()
).block();
// 获取Token使用统计
ChatUsage usage = response.getChatUsage();
if (usage != null) {
System.out.println("=== Token Usage ===");
System.out.println("Input tokens: " + usage.getInputTokens());
System.out.println("Output tokens: " + usage.getOutputTokens());
System.out.println("Total tokens: " + usage.getTotalTokens());
System.out.println("Time: " + usage.getTime() + "s");
// 计算成本(假设DashScope千问模型的价格)
// 输入:0.0001元/千token,输出:0.0002元/千token
double inputCost = usage.getInputTokens() * 0.0001 / 1000;
double outputCost = usage.getOutputTokens() * 0.0002 / 1000;
double totalCost = inputCost + outputCost;
System.out.printf("Cost: ¥%.4f (Input: ¥%.4f, Output: ¥%.4f)\n",
totalCost, inputCost, outputCost);
}
}
}
// ChatUsage类的结构
@Data
public class ChatUsage {
private long inputTokens; // 输入token数
private long outputTokens; // 输出token数
private long totalTokens; // 总token数
private double time; // 响应时间(秒)
private long cachedTokens; // 缓存token数(可选)
}
2.5 消息的序列化与反序列化
2.5.1 JSON序列化
AgentScope使用Jackson框架处理消息的JSON序列化。通过@JsonTypeInfo和@JsonSubTypes注解,实现了多态JSON的自动处理。
java
// 序列化:Msg → JSON
Msg msg = Msg.builder()
.role(MsgRole.USER)
.name("user_123")
.content(
TextBlock.builder().text("Hello").build(),
ImageBlock.builder()
.source(URLSource.builder()
.url("https://example.com/image.png")
.build())
.build()
)
.metadata(Map.of("user_id", "123"))
.build();
// 自动转为JSON
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(msg);
// 输出的JSON结构
/*
{
"id": "msg_uuid",
"name": "user_123",
"role": "USER",
"content": [
{
"type": "text",
"text": "Hello"
},
{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/image.png"
}
}
],
"metadata": {
"user_id": "123"
},
"timestamp": "2024-01-15 10:30:45.123"
}
*/
// 反序列化:JSON → Msg
Msg deserializedMsg = mapper.readValue(json, Msg.class);
// 自动根据"type"字段选择正确的ContentBlock子类
2.5.2 消息的网络传输
java
// 场景:Agent在不同机器上运行,需要通过网络传输消息
// 发送端:将消息序列化并通过HTTP发送
@PostMapping("/api/agent/call")
public ResponseEntity<String> callAgent(@RequestBody String msgJson)
throws Exception {
ObjectMapper mapper = new ObjectMapper();
Msg userMsg = mapper.readValue(msgJson, Msg.class);
Msg response = agent.call(userMsg).block();
String responseJson = mapper.writeValueAsString(response);
return ResponseEntity.ok(responseJson);
}
// 客户端:创建消息,序列化发送,反序列化接收
public Msg callRemoteAgent(String agentUrl, String userInput)
throws Exception {
// 1. 创建消息
Msg msg = Msg.builder()
.role(MsgRole.USER)
.textContent(userInput)
.build();
// 2. 序列化
ObjectMapper mapper = new ObjectMapper();
String msgJson = mapper.writeValueAsString(msg);
// 3. 通过HTTP POST发送
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(agentUrl)
.post(RequestBody.create(msgJson, MediaType.get("application/json")))
.build();
Response httpResponse = client.newCall(request).execute();
String responseJson = httpResponse.body().string();
// 4. 反序列化响应
Msg response = mapper.readValue(responseJson, Msg.class);
return response;
}
2.6 消息在生产系统中的应用
2.6.1 多轮对话管理
java
public class ConversationManager {
private final ReActAgent agent;
private final Memory conversationMemory;
private final List<Msg> messageHistory;
public ConversationManager(ReActAgent agent) {
this.agent = agent;
this.conversationMemory = new InMemoryMemory();
this.messageHistory = new ArrayList<>();
}
/**
* 添加用户消息并获得Agent回复
*/
public Msg handleUserInput(String userId, String userInput) {
// 1. 创建用户消息
Msg userMsg = Msg.builder()
.role(MsgRole.USER)
.name(userId)
.textContent(userInput)
.metadata(Map.of(
"user_id", userId,
"timestamp", System.currentTimeMillis()
))
.build();
// 2. 记录到历史
messageHistory.add(userMsg);
// 3. Agent处理(Agent内部会使用Memory维护上下文)
Msg agentResponse = agent.call(userMsg).block();
// 4. 记录Agent响应
messageHistory.add(agentResponse);
// 5. 计算成本(如果有Token使用信息)
if (agentResponse.getChatUsage() != null) {
double cost = calculateCost(agentResponse.getChatUsage());
System.out.printf("Total cost: ¥%.4f\n", cost);
}
return agentResponse;
}
/**
* 获取会话历史的摘要(用于展示)
*/
public String getSummary() {
return messageHistory.stream()
.map(msg -> String.format("[%s] %s: %s",
msg.getRole(),
msg.getName() != null ? msg.getName() : "unknown",
msg.getTextContent()))
.collect(Collectors.joining("\n"));
}
/**
* 清空会话(开始新对话)
*/
public void clearHistory() {
messageHistory.clear();
conversationMemory.clear();
}
private double calculateCost(ChatUsage usage) {
// DashScope定价:输入0.0001元/千token,输出0.0002元/千token
return (usage.getInputTokens() * 0.0001 +
usage.getOutputTokens() * 0.0002) / 1000;
}
}
// 使用示例
public static void main(String[] args) {
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.sysPrompt("你是一个有帮助的助手")
.model(model)
.build();
ConversationManager conv = new ConversationManager(agent);
// 第1轮
Msg response1 = conv.handleUserInput("user_001", "Python和Java的区别是什么?");
System.out.println("Assistant: " + response1.getTextContent());
// 第2轮(Agent知道前面讨论过Python)
Msg response2 = conv.handleUserInput("user_001", "那Python最适合用在哪些领域?");
System.out.println("Assistant: " + response2.getTextContent());
// 显示对话历史
System.out.println("\n=== Conversation Summary ===");
System.out.println(conv.getSummary());
}
2.6.2 消息持久化
java
public class MessagePersistence {
private final MongoTemplate mongoTemplate; // MongoDB数据库
/**
* 保存消息到数据库
*/
public void saveMessage(String conversationId, Msg msg) {
Document doc = new Document()
.append("conversation_id", conversationId)
.append("message_id", msg.getId())
.append("role", msg.getRole().toString())
.append("name", msg.getName())
.append("timestamp", msg.getTimestamp())
.append("content_text", msg.getTextContent())
.append("metadata", msg.getMetadata())
.append("saved_at", Instant.now());
mongoTemplate.insert(doc, "messages");
}
/**
* 加载对话历史
*/
public List<Msg> loadConversation(String conversationId) {
List<Document> docs = mongoTemplate.find(
Query.query(Criteria.where("conversation_id").is(conversationId)),
Document.class,
"messages"
);
return docs.stream()
.map(this::documentToMsg)
.collect(Collectors.toList());
}
private Msg documentToMsg(Document doc) {
return Msg.builder()
.id((String) doc.get("message_id"))
.name((String) doc.get("name"))
.role(MsgRole.valueOf((String) doc.get("role")))
.textContent((String) doc.get("content_text"))
.timestamp((String) doc.get("timestamp"))
.metadata((Map<String, Object>) doc.get("metadata"))
.build();
}
}
总结
核心概念回顾
| 概念 | 说明 |
|---|---|
| Msg | 不可变的消息对象,是Agent间通信的基本单位 |
| MsgRole | 消息的四种角色(USER、ASSISTANT、SYSTEM、TOOL) |
| ContentBlock | 支持多种内容类型的内容块系统(文本、图像、音视频等) |
| Metadata | 消息的元数据,用于存储结构化数据和扩展信息 |
| 序列化 | JSON序列化支持网络传输和持久化 |
在下一章中,我们将深入探讨模型接口(Model)与适配器模式,了解如何在AgentScope中无缝使用不同的LLM。