阿里开源AgentScope多智能体框架解析系列(二)第2章:消息系统(Message)深入解析

总述

消息(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。

相关推荐
语落心生2 小时前
阿里开源AgentScope多智能体框架解析系列(一)第1章:AgentScope-Java 概述与快速入门
agent
语落心生2 小时前
阿里开源AgentScope多智能体框架解析系列(四)第4章:Agent 接口与 AgentBase 基类
agent
潘锦3 小时前
AI Agent 核心管理逻辑:工具的管理和调度
agent
Jay Kay5 小时前
Agent沙箱执行服务
agent·沙箱服务
小马过河R5 小时前
ReAct和Function Calling之间的纠葛与恩恩怨怨
人工智能·语言模型·agent·智能体
大模型真好玩7 小时前
LangGraph智能体开发设计模式(二)——协调器-工作者模式、评估器-优化器模式
人工智能·langchain·agent
沛沛老爹8 小时前
Web开发者实战AI Agent:基于Dify实现OpenAI Deep Research智能体
前端·人工智能·gpt·agent·rag·web转型
沛沛老爹8 小时前
Web开发者实战AI Agent:基于Dify的多模态文生图与文生视频智能体项目
前端·人工智能·llm·agent·rag·web转型
EdisonZhou21 小时前
MAF快速入门(9)多路分支路由工作流
llm·aigc·agent·.net core