乐于分享
好东西不私藏

AgentScope 源码剖析(一):Msg 是如何设计的?

AgentScope 源码剖析(一):Msg 是如何设计的?

一开始看智能体框架,很容易直接去看 Agent、ReActAgent、Tool Calling、Memory 这些模块。它们确实重要,但真正读源码时会发现,这些模块之间都绕不开一个更基础的对象:
如果不先理解 Msg,后面再看 memory.add(msg)、formatter.format(msgs=…)、工具调用结果、RAG 检索结果、长期记忆内容时,很容易只看到函数调用,却看不清信息到底是如何在框架里被组织和传递的。
所以这一篇先看 AgentScope 的 Message 模块。
本文主要讨论一个问题:
AgentScope 是如何设计 Msg 这个消息对象的?

01 为什么先看 Message 模块?

智能体运行的过程,本质上是在不断处理信息。
用户输入是一种信息。
Agent 回复是一种信息。
工具调用是一种信息。
工具返回结果也是一种信息。
RAG 检索内容、长期记忆检索内容,也都需要被放入上下文中,参与后续推理。
因此,在看 Agent 如何推理之前,先要看一个更基础的问题:
AgentScope 如何表示一条消息?
如果消息只是字符串,系统很难区分:
这条内容是谁发出的?
它应该作为 user、assistant 还是 system 进入模型?
它是普通文本,还是工具调用?
它需要被 Memory 保存吗?
它后面能否被序列化和恢复?
AgentScope 对这个问题的回答,就是 Msg。

02 Message 模块的源码结构

AgentScope 的消息模块主要位于:
src/agentscope/message/
这部分源码里,最核心的是两个文件:
_message_block.py
_message_base.py
这两个文件的分工很清晰。
_message_block.py 负责定义消息内容块。
_message_base.py 负责定义 Msg 这个消息对象。
换句话说,AgentScope 并不是直接把消息设计成一个简单的类,而是分成了两层:
第一层是内容块系统,负责说明消息内容里可以放什么。
第二层是消息对象本身,负责说明一条完整消息应该如何组织。
【图1:Message 模块源码结构图】
图注:AgentScope message 模块的源码结构。_message_block.py 定义内容块,_message_base.py 定义 Msg 对象。
这个拆分很重要。
因为如果只看 Msg 这个类,很容易以为它只是一个带有 name、role、content 的普通数据对象。
但从源码结构看,AgentScope 实际上是在表达:
Msg 不是单纯的字符串封装,而是由“消息外壳”和“内容块系统”共同组成的消息抽象。

03 _message_block.py:content 为什么不是简单字符串?

先看 _message_block.py。
这个文件的核心作用,是定义 Msg.content 可以有哪些内容形态。
在普通聊天程序里,消息内容通常就是一段字符串。例如:
“请帮我分析这段代码”
但 AgentScope 面向的是智能体场景。智能体上下文里的内容,往往不只是文本。
它可能是普通文本,也可能是 reasoning 内容。
它可能包含图片、音频、视频。
它也可能包含工具调用请求,或者工具调用结果。
所以 AgentScope 设计了一组 ContentBlock,用来表达不同类型的消息内容。
大体可以分成三类:
第一类是文本类 Block,用来表示文本内容和推理内容。
第二类是多模态类 Block,用来表示图片、音频、视频等内容。
第三类是工具调用类 Block,用来表示工具调用请求和工具返回结果。
【图2:ContentBlock 类型系统图】
图注:ContentBlock 类型系统。Msg.content 不只是字符串,而是一组结构化内容块。
为什么不直接全部转成字符串?
因为字符串只能表达内容本身,却很难稳定表达内容的类型。
比如一段文本写着:
调用 search 工具,参数是 {“query”: “AgentScope”}
这当然可以被人读懂,但对框架来说,它只是一段普通文本。
如果它被表示成 ToolUseBlock,系统就能明确知道:这是一次工具调用,工具名是什么,参数是什么,调用编号是什么。
工具结果也是一样。
如果工具结果只是普通文本,后续模块很难判断它是 assistant 的自然语言回复,还是某个工具调用对应的结果。
所以,_message_block.py 的价值在于:
它为 Msg.content 提供了结构化表达能力。
这也是 AgentScope 能够支持工具调用、多模态内容和复杂智能体推理链路的基础。

04 _message_base.py:Msg 是如何组织一条消息的?

理解了内容块,再看 _message_base.py。
这个文件定义的是 Msg 本体。
如果说 _message_block.py 关注的是“消息内容可以有哪些形态”,那么 _message_base.py 关注的就是“一条完整消息应该包含哪些信息”。
一个 Msg 主要围绕这些字段组织:
name
role
content
metadata
id
timestamp
invocation_id
其中最值得关注的是 name、role、content 和 metadata。
name:消息是谁发出的
在简单场景里,它可能是 user 或 assistant。
但在多 Agent 场景中,它可以是 PlannerAgent、CoderAgent、ReviewerAgent,也可以是 long_term_memory 或某个工具名称。
所以,name 更偏 AgentScope 框架内部的身份标识。
role:模型应该如何理解这条消息
常见的 role 包括:
system
user
assistant
这也解释了为什么 name 和 role 要分开。
AgentScope 内部可能有很多消息发送者,但模型 API 通常只接受有限的角色类型。
所以:
name 表示这条消息是谁发出的。
role 表示模型应该把这条消息当成什么角色。
【图3:name 与 role 的区别图】
图注:name 和 role 的区别。name 是框架内部发送者身份,role 是模型协议中的角色。
content可以是普通字符串,也可以是 ContentBlock 列表。
这就把 _message_block.py 和 _message_base.py 连接起来了。
_message_block.py 定义内容形态,_message_base.py 用 content 字段承载这些形态。
metadata:额外结构化信息,它和 content 的职责不同。
content 更偏消息正文。metadata 更偏附加状态、结构化输出或额外结果。
这样设计之后,Msg 不仅能表示“说了什么”,还能携带一些不直接作为正文展示的结构化信息。
id、timestamp、invocation_id:消息的可追踪性
Msg 还会带有 id、timestamp、invocation_id 这类字段。
这说明 Msg 不是临时传递的一段字符串,而是一个可追踪、可管理、可恢复的信息对象。
比如后面 Memory 删除消息时,就可以围绕 msg.id 进行精确管理。

05 Msg 不只是存数据

如果 Msg 只是把字段收在一起,那它只是一个普通数据类。
但从源码设计看,Msg 还承担了两个重要职责:
统一内容访问。
支持序列化与恢复。
统一内容访问
因为 content 可能是字符串,也可能是 block 列表,所以如果每个模块都自己判断内容类型,代码会非常混乱。
因此,Msg 提供了统一访问方法,例如:
get_text_content()
get_content_blocks()
has_content_blocks()
这些方法屏蔽了底层内容形态的差异。
上层模块不需要关心 content 到底是字符串还是 block 列表,只需要通过这些方法获取文本内容、指定类型的 block,或者判断消息中是否包含某类 block。
比如 ReActAgent 中判断模型是否产生工具调用,就可以通过 has_content_blocks(“tool_use”) 来完成。
序列化与恢复
Msg 还支持:
to_dict()
from_dict()
这一步很关键。
因为消息进入 AgentScope 主流程后,不只是运行时对象。
它还可能被 Memory 保存,被状态系统导出,被持久化到外部存储中,并在后续重新恢复出来。
如果 Msg 不能稳定地转成字典,再从字典恢复,就很难支持 Memory 和状态管理。
【图4:Msg 序列化与 Memory 关系图】
图注:Msg 的序列化与恢复。消息对象通过 to_dict() 和 from_dict() 进入 Memory 和持久化链路。

所以,Msg 并不是简单的数据容器。
它同时具备消息表达、内容访问、状态保存和恢复能力。

06 Msg 如何进入 AgentScope 主链路?

理解了 Msg 本身,再看 AgentScope 其他模块,就会更清楚。
在 AgentScope 中,很多核心模块都围绕 Msg 工作。
Memory 负责保存 Msg。
Formatter 负责读取 Msg,并转换成模型 API 需要的输入格式。
Model 产生新的消息内容。
Agent 负责组织整个过程。
Tool、RAG、Long-term Memory 返回的信息,最终也可以被包装成 Msg,重新进入上下文。
也就是说,Msg 是这些模块之间的统一中间结构。
它把来自不同来源的信息,统一成一种消息对象,然后再交给 Memory、Formatter、Model 和 Agent 处理。
【图5:Msg 进入 AgentScope 主链路图】
图注:AgentScope 中围绕 Msg 展开的信息流闭环。Msg 连接 Memory、Formatter、Model 与后续生成过程。
这也是为什么我觉得 AgentScope 源码学习适合从 Msg 开始。
如果没有先理解 Msg,后面看 Agent 时会觉得很多地方只是在“传对象”。
但理解了 Msg 后,就能看清楚这个对象到底承载了什么信息,以及它为什么能在不同模块之间流动。
从表面看,Msg 像是一个消息类。
但从源码设计来看,它其实由两部分共同构成:
_message_block.py
_message_base.py
前者定义消息内容块,解决 content 里可以放什么。
后者定义消息对象,解决一条完整消息如何组织、访问、序列化和流转。
因此,AgentScope 的 Msg 不是简单的字符串封装,而是一种统一的消息表达与交换协议。
它的设计逻辑可以概括成四点:
第一,消息外壳。
用 name、role、content、metadata、id 等字段表示一条完整消息。
第二,内容块系统。
通过 ContentBlock 支持文本、多模态、工具调用和工具结果。
第三,统一访问接口。
通过 get_text_content()、get_content_blocks()、has_content_blocks() 屏蔽内容形态差异。
第四,状态化能力。
通过 to_dict() 和 from_dict() 支持 Memory、持久化和恢复。
所以,理解 AgentScope,确实很适合从 Msg 开始。
因为只有先搞清楚一条消息在框架里是如何被设计出来的,后面再看 Memory、Formatter、ReActAgent 和 Tool Calling,才会真正看到这套系统的信息流是如何被搭起来的。