Skip to content

消息与事件

在 AgentScope 2.x 中,MsgEvent 是两套独立但紧密耦合的数据单元:Msg 是 Agent 之间通信、上下文 存储与持久化的基本单元,承载"已经完成"的信息;Eventreply_stream 产出的流式与 Human-in-the-loop (HITL)单元,承载"正在发生"的过程。一次流式回复本质上是若干 Event 增量地拼装出一个 Msg 的过程。

版本基线:本文以 Python agentscope 2.0.4(PyPI,2026-07-07)为准,要求 Python >=3.11。 2.x 文档统一访问 https://docs.agentscope.io/versions/2.0.4/en/...,未带版本号的 /en/... 路径会 404。 本文所有类名、字段与导入路径均已与 src/agentscope/message/__init__.pyevent/__init__.pymessage/_base.pymessage/_block.py 源码核对。

Msg 结构

Msg 是一个 Pydantic BaseModel,定义在 agentscope.message 模块。它不依赖任何运行时状态,可以序列化、 反序列化、跨进程传递,也能直接写入 AgentState 的上下文。

核心字段:

字段类型说明
namestr发送者名称(用户名、Agent 名或 "system"
contentlist[ContentBlock]消息内容,始终是 block 列表;传入纯字符串会自动包装
role"user" | "assistant" | "system"角色,决定允许哪些 block 类型
idstr消息 ID,默认自动生成;流式重建时必须与 reply_id 对齐
metadatadict任意键值元数据,默认空 dict
created_atstr创建时间(ISO 格式字符串)
finished_atstr | None完成时间;assistant 消息在 ReplyEndEvent 时才盖戳
usageUsage | NoneToken 用量,含 input_tokensoutput_tokens

角色与 block 约束

Msgmodel_validator 中按 role 校验 content,约束来自源码而非文档承诺:

  • UserMsg:只允许 TextBlockDataBlock。不能塞 ToolCallBlockThinkingBlock
  • SystemMsg:只允许 TextBlock。系统消息不支持多模态。
  • AssistantMsg:允许全部 ContentBlock 子类型。

试图给 UserMsg 传入 ToolCallBlock 会在构造时抛 ValueError

构造消息

UserMsg / AssistantMsg / SystemMsg 是三个工厂函数,返回的都是 Msg 实例,只是 role 不同。content 既可以是纯字符串(自动包装成单个 TextBlock),也可以是显式的 block 列表。

python
import base64

from agentscope.message import (
    UserMsg,
    AssistantMsg,
    SystemMsg,
    TextBlock,
    DataBlock,
    Base64Source,
)

# 1. 纯文本用户消息:字符串自动包装为 TextBlock
user_msg = UserMsg(name="alice", content="帮我分析这张图")
print(user_msg.role)        # "user"
print(user_msg.content[0])  # TextBlock(type="text", text="帮我分析这张图")

# 2. 多模态用户消息:显式 block 列表,含 DataBlock
with open("chart.png", "rb") as f:
    img_b64 = base64.b64encode(f.read()).decode("ascii")

multimodal_msg = UserMsg(
    name="alice",
    content=[
        TextBlock(text="这张图说明了什么?"),
        DataBlock(
            source=Base64Source(data=img_b64, media_type="image/png"),
            name="chart.png",
        ),
    ],
)

# 3. 系统消息:只能是 TextBlock
system_msg = SystemMsg(name="system", content="你是一个数据分析助手")

# 4. 助手消息:通常由 Agent 产出,也可手动构造
assistant_msg = AssistantMsg(name="analyst", content="这张图展示的是 Q3 营收同比变化。")

读取消息内容

Msg 提供两个常用读取方法:

  • get_text_content(separator="\n") -> str | None:拼接所有 TextBlock 的文本,没有则返回 None
  • get_content_blocks(block_type=None):按类型过滤返回 block 列表,不传则返回全部。
python
from agentscope.message import Msg

# 假设 assistant_msg 已由 Agent 产出
text = assistant_msg.get_text_content()
tool_calls = assistant_msg.get_content_blocks("tool_call")
tool_results = assistant_msg.get_content_blocks("tool_result")

ContentBlock 体系

content 字段里的每个元素都是一个 ContentBlock 子类型。2.x 的 block 体系覆盖了文本、多模态、推理过程、 工具调用与工具结果,是 1.x 旧 Msg 形状的彻底重写。

Block用途关键字段
TextBlock纯文本内容text: strid: str
DataBlock二进制多模态数据(图片、音频、视频)source: Base64Source | URLSourcename: str | None
ThinkingBlock模型推理过程(如 Anthropic extended thinking)thinking: strid: strextra="allow" 容纳 signature
ToolCallBlockAgent 发起的工具调用name: strinput: str(原始 JSON)、state: ToolCallState
ToolResultBlock工具执行结果,与 ToolCallBlock 配对name: stroutput: str | list[TextBlock | DataBlock]state: ToolResultState
HintBlockReAct 循环中给模型的提示,传给 LLM 时转为 user 消息hint: str | list[TextBlock | DataBlock]source: str | None

几点源码级细节:

  • DataBlock.source 既可以是 Base64Source(data, media_type),也可以是 URLSource(url, media_type)。 流式 DataBlockDeltaEvent 到达时,框架会逐块 base64 解码、按字节拼接、再重新编码,避免裸字符串拼接 破坏字节流。
  • ToolCallBlock.input原始 JSON 字符串,不是已解析的 dict。流式过程中 ToolCallDeltaEventdelta 会追加到这个字符串上,全部到达后才得到完整 JSON。
  • ToolCallBlock.state 是一个 StrEnum,状态机为 pending → asking / allowed / submitted → finished, 与 Permission 三态决策直接对应。ToolResultBlock.state 则是 success / error / interrupted / denied / running
  • ThinkingBlock 使用 extra="allow",因此可以承载 provider 特有字段(如 Anthropic 的 signature), 无需子类化。

Msg 与 Event 的关系

下图说明 Msg 如何由 ContentBlock 组成,以及 Event 流如何增量地把一个 Msg 拼装出来。ReplyStartEvent 负责创建一个空的 AssistantMsg,后续每一个 block 级 event 都通过 Msg.append_event 增量更新这个 Msg, 直到 ReplyEndEvent 盖上 finished_at

关键关系:

  • 一个 Msgcontent 是一个有序 block 列表,block 的到达顺序就是列表顺序。
  • Event 不直接持久化进上下文,而是通过 append_event 把变更落到 Msg 上,最终持久化的是 Msg
  • append_event 会校验 event.reply_id == self.id,不匹配的事件会被跳过并告警。因此流式重建时, AssistantMsgid 必须取自 ReplyStartEvent.reply_id,不能自己生成。

Event 类型

agentscope.event 模块导出约 30 个事件类,全部继承自 EventBase,通过 EventType 枚举区分。下表列出 流式与 HITL 场景中最主要的事件类型,完整列表见 event/__init__.py

事件触发时机携带的关键数据
ReplyStartEvent一次 reply_stream 开始namereply_id
ModelCallStartEvent调用模型前reply_id、模型信息
ModelCallEndEvent单次模型调用结束input_tokensoutput_tokens(累积进 Msg.usage
TextBlockStartEvent一段文本块开始block_id
TextBlockDeltaEvent文本块增量block_iddelta
TextBlockEndEvent文本块结束block_id
ToolCallStartEvent模型发起一个工具调用tool_call_idtool_call_name
ToolCallDeltaEvent工具调用入参 JSON 增量tool_call_iddelta
ToolResultEndEvent工具执行完成tool_call_idstatemetadata
ReplyEndEvent一次回复结束reply_idreason: ReplyEndReason
RequireUserConfirmEventPermission 决策为 ask,等待用户确认tool_calls(含 suggested_rules
UserConfirmResultEvent用户确认或拒绝confirm_results
UserInterruptEvent用户主动中断中断上下文
ExceedMaxItersEvent达到最大迭代次数迭代计数
RequireExternalExecutionEvent工具需外部执行(submitted 态)tool_calls
ExternalExecutionResultEvent外部执行结果回传execution_results

HintBlockEvent 是一个"一次性"事件——与文本/数据/思考块的 Start-Delta-End 三段式不同,它的完整内容在 单个事件里到达,append_event 直接追加一个完整的 HintBlock

流式重建 Msg

reply_stream 返回的是一个事件迭代器。要把事件流还原成一个完整的 Msg,标准做法是:在 ReplyStartEvent 时用事件带来的 namereply_id 创建一个空的 AssistantMsg,然后对后续每一个 事件调用 msg.append_event(ev)

python
import asyncio
import os

from agentscope.agent import Agent
from agentscope.model import DashScopeChatModel
from agentscope.credential import DashScopeCredential
from agentscope.message import UserMsg, AssistantMsg
from agentscope.event import (
    ReplyStartEvent,
    ReplyEndEvent,
    ExceedMaxItersEvent,
    RequireUserConfirmEvent,
)

# 模型与凭据通过环境变量注入,不要写死在代码里
agent = Agent(
    name="analyst",
    system_prompt="你是一个数据分析助手",
    model=DashScopeChatModel(
        credential=DashScopeCredential(api_key=os.environ["DASHSCOPE_API_KEY"]),
        model="qwen-plus",
    ),
)

user_input = "帮我总结一下今天的销售数据"


async def main() -> None:
    msg: AssistantMsg | None = None

    # reply_stream 是异步事件流,输入是 Msg 而非字符串
    async for event in agent.reply_stream(
        UserMsg(name="user", content=user_input),
    ):
        if isinstance(event, ReplyStartEvent):
            # 用事件的 name 与 reply_id 创建空 Msg,确保 append_event 的 id 校验通过
            msg = AssistantMsg(name=event.name, content=[], id=event.reply_id)

        elif isinstance(event, RequireUserConfirmEvent):
            # HITL 暂停:工具调用进入 asking 态,需要在外部决策后回灌 UserConfirmResultEvent
            print("等待用户确认的工具调用:", [tc.name for tc in event.tool_calls])

        elif isinstance(event, ExceedMaxItersEvent):
            print("达到最大迭代次数,提前结束")

        elif isinstance(event, ReplyEndEvent):
            print("回复结束,原因:", event.reason)

        # 对所有事件统一调用 append_event,让 Msg 增量更新
        if msg is not None:
            msg.append_event(event)

    # 流结束后,msg 就是一个完整的 AssistantMsg,可直接写入上下文或序列化
    if msg is not None:
        print(msg.get_text_content())
        print(msg.usage)  # 累积的 token 用量


asyncio.run(main())

几个容易踩的细节:

  • AssistantMsg(name=..., content=[], id=ev.reply_id) 中的 id 必须取自事件,不能省略。省略后 AssistantMsg 会自动生成一个新 ID,与事件的 reply_id 不匹配,append_event 会跳过所有事件并告警。
  • append_event 返回 Self(即 msg 本身),是原地变更,可以链式调用,也可以忽略返回值。
  • DataBlockDeltaEvent 的处理做了 base64 字节级拼接,不要自己在外面拼字符串再塞回 block。
  • ToolCallBlock.input 在全部 ToolCallDeltaEvent 到达前是不完整的 JSON 片段,不要中途解析它。

前端类比

如果你来自前端:

  • Msg 类比为 Redux 里的 action 对象 + state 快照:它是一个纯数据结构,记录"谁、说了什么、 用了多少 token",可以被序列化、回放、存档。role 字段就像 action 的 type,决定了它能在上下文里 承载哪些字段。
  • ContentBlock 类比为 React Element 树的节点:一条消息不是一段扁平字符串,而是一棵由 TextBlockDataBlockToolCallBlock 等节点组成的有序列表,每种节点有自己的 props。
  • Event 流类比为 SSE chunkreply_stream 吐出的每个事件就是一个 chunk,前端可以逐帧渲染文本、 工具调用气泡、思考过程,而不是等整条消息拼完才显示。
  • append_event 类比为 reducer:它接收当前 Msg 状态与一个事件,返回更新后的 Msg,是一个 确定性的、可重放的折叠操作。

AgentScope 原生语义Msg 并不是不可变的 action——它是一个 Pydantic BaseModelappend_event 会 原地变更 contentfinished_atusage。这与 Redux reducer "返回新 state"的纯函数语义不同:AgentScope 选择原地变更是为了在流式过程中避免反复拷贝 block 列表,但代价是你不能假设同一个 Msg 对象在 append_event 前后内容一致。如果需要快照,请用 msg.model_dump() 显式序列化。Event 也不是 纯粹的 UI 信号——RequireUserConfirmEventUserInterruptEvent 等会真正暂停 Agent 执行,是 HITL 控制 流的一部分,而不只是通知。

常见错误

  • content 类型混用:给 UserMsgToolCallBlock,或给 SystemMsgDataBlock,会在 model_validator 阶段抛 ValueError。构造前先确认 role 允许的 block 类型。
  • 流式重建时忘记传 idAssistantMsg(name=ev.name, content=[]) 不带 id 会自动生成新 ID, 导致 append_eventreply_id != self.id 跳过全部事件,最终 msg.content 始终为空。
  • 中途解析 ToolCallBlock.input:在 ToolCallEndEvent 之前,input 是不完整的 JSON 片段, json.loads 会失败。需要等 ToolCallEndEventReplyEndEvent 之后再解析。
  • block 顺序假设content 列表的顺序就是事件到达顺序,不要假设"所有 TextBlock 在前、ToolCallBlock 在后"。ReAct 循环中 TextBlock、ThinkingBlock、ToolCallBlock、ToolResultBlock 会交错出现。
  • Event 当持久化单元:持久化的是 Msg,不是 Event。事件流消费完即可丢弃,Msg 才是写入 AgentState 上下文与长期记忆的对象。

先修

下一步

官方参考

学习文档整合站点