AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

消息 Schema 与角色边界

我的学习 →

正在读取学习记录…

消息Schema是协作消息的字段约定:编号怎么写,谁发给谁,内容放在哪里,回复哪一个请求。Schema可以译作结构契约;它让接收程序有办法检查一条消息是否完整、能否进入下一步。

角色边界规定某种参与者可以发送什么消息。协调者发request,即资料请求;读取者发result或error,即结果或错误。信封里写“sender=reader”还不够,程序需要对照连接上已经认证的身份。

发送者是谁、回复什么任务和回复哪个请求,分别解决不同问题。混在一段自然语言里,乱序时就容易把“未发货可退”当成订单金额,客服从此学会拿规则支付退款。

两个请求,回复先后颠倒

协调者为任务T-17发出Q-order和Q-policy,分别问订单金额与退款规则。规则回复M-policy先到,订单回复M-order后到。

每条回复携带reply_to,指向原请求编号。接收器用该编号存结果,所以虽然规则先到,按Q-order取回的仍是120。随后完全相同的M-order再来一次,返回duplicate,表示已经接收,不重复写结果。

程序的字段是应用内部约定:id、task_id、sender、recipient、type、reply_to和payload。A2A等跨服务协议有自己的消息字段,不能把这份内部信封直接当成A2A报文。

保存完整程序并运行

在自己的学习目录打开PowerShell,创建一个新的子目录:

Bash
mkdir l6-messages
cd l6-messages

把下面完整代码保存为 messages_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。

Python
"""一个应用内部消息契约与请求配对器。"""
from copy import deepcopy

FIELDS = {"id", "task_id", "sender", "recipient", "type", "reply_to", "payload"}
ALLOWED = {"coordinator": {"request"}, "reader": {"result", "error"}}

def validate(message, authenticated_sender):
    if not isinstance(message, dict) or set(message) != FIELDS:
        raise ValueError("消息字段不完整")
    for key in ("id", "task_id", "sender", "recipient"):
        if not isinstance(message[key], str) or not message[key].strip():
            raise ValueError("编号和角色必须是非空字符串")
    if message["sender"] != authenticated_sender:
        raise PermissionError("信封发送者与认证身份不一致")
    if message["type"] not in ALLOWED.get(authenticated_sender, set()):
        raise PermissionError("该角色不能发送此消息类型")
    if not isinstance(message["payload"], dict):
        raise ValueError("payload必须是对象")
    if message["type"] == "request":
        if message["reply_to"] is not None or not isinstance(message["payload"].get("query"), str):
            raise ValueError("请求必须携带query且无reply_to")
    else:
        key = "value" if message["type"] == "result" else "code"
        if not isinstance(message["reply_to"], str) or key not in message["payload"]:
            raise ValueError("回复缺少配对编号或内容")

class Inbox:
    def __init__(self):
        self.pending, self.received, self.results = {}, {}, {}

    def add_request(self, message):
        validate(message, "coordinator")
        if message["id"] in self.pending:
            raise ValueError("请求编号重复")
        self.pending[message["id"]] = deepcopy(message)

    def receive(self, message, authenticated_sender):
        validate(message, authenticated_sender)
        previous = self.received.get(message["id"])
        if previous is not None:
            if previous != message:
                raise ValueError("同一消息编号对应不同内容")
            return "duplicate"
        request = self.pending.get(message["reply_to"])
        if request is None:
            raise ValueError("找不到配对请求")
        if (message["task_id"] != request["task_id"]
                or message["sender"] != request["recipient"]
                or message["recipient"] != request["sender"]):
            raise ValueError("回复任务或角色不匹配")
        if message["reply_to"] in self.results:
            raise ValueError("请求已收到最终回复")
        self.received[message["id"]] = deepcopy(message)
        self.results[message["reply_to"]] = deepcopy(message["payload"])
        return "accepted"

def request(number, query):
    return {"id": number, "task_id": "T-17", "sender": "coordinator",
            "recipient": "reader", "type": "request", "reply_to": None,
            "payload": {"query": query}}

def reply(number, request_id, value):
    return {"id": number, "task_id": "T-17", "sender": "reader",
            "recipient": "coordinator", "type": "result", "reply_to": request_id,
            "payload": {"value": value}}

def main():
    inbox = Inbox()
    inbox.add_request(request("Q-order", "订单金额"))
    inbox.add_request(request("Q-policy", "退款规则"))
    for message in [reply("M-policy", "Q-policy", "未发货可退"),
                    reply("M-order", "Q-order", 120)]:
        print(message["reply_to"] + ":" + inbox.receive(message, "reader"))
    print("按请求取回:金额=" + str(inbox.results["Q-order"]["value"]))
    print("重复消息:" + inbox.receive(reply("M-order", "Q-order", 120), "reader"))
    try:
        inbox.receive(reply("M-fake", "Q-order", 999), "coordinator")
    except PermissionError as error:
        print("伪造发送者:" + str(error))

if __name__ == "__main__":
    main()

保存后执行 dir,确认能看到 messages_demo.py,再运行:

Bash
ls
python -X utf8 messages_demo.py

程序输出:

TXT
Q-policy:accepted
Q-order:accepted
按请求取回:金额=120
重复消息:duplicate
伪造发送者:信封发送者与认证身份不一致

每个字段怎样参与处理

字段在例子中的作用
id唯一标识一条消息,识别重复交付
task_id归属整个退款任务T-17
sender / recipient标识发送和接收角色,与认证身份及原请求对照
type区分请求、结果和错误
reply_to回复关联的请求编号,如Q-order
payload请求的query、结果的value或错误的code

validate先检查字段集合与非空编号,再核对真实发送者和该角色允许的消息类型。request必须携带query且无reply_to;result或error必须携带配对编号及相应内容。这样可以在写入结果之前拒绝结构错误。

Inbox保存pending,即已发出等待回复的请求;received保存已接收的消息;results按请求编号保存最终内容。三个映射不能混用:消息M-order的编号用于去重,请求Q-order的编号用于找订单结果。

订单和规则请求有不同编号,先到规则回复通过reply_to匹配Q-policy,后到订单回复匹配Q-order
到达顺序会变化,请求归属不应变化。身份、任务和配对编号都需要核对。

配对不仅是查一个编号

找到reply_to对应请求后,接收器还核对task_id、sender与recipient。发送者应是当初请求的接收者,接收者应是当初请求的发送者,任务编号也应一致。否则一个合法格式的回复可能污染另一笔订单。

认证身份由receive的authenticated_sender参数提供,信封无法替换它。伪造路径中,消息写reader,认证身份却是coordinator,因此被拒绝。真实应用应从受验证的连接或会话拿到身份,再映射应用角色;不能把消息中的sender同时作为认证参数传回来。

结构检查确认result字段存在,业务检查再核对其中的value。例如订单金额要求非负整数,就要检查数值类型、金额单位、订单归属与依据。两类检查各自给出明确结果。

用消息编号与内容识别重复交付

完全相同的消息编号和内容再次到达,可以返回duplicate。同一编号带着不同金额,则是编号冲突,程序拒绝,不让后来者覆盖先前结果。

另一个新编号又回复已经完成的请求,也会被拒绝,因为results中已有最终回复。这样避免同一请求被两个“最终答案”来回覆盖。若业务需要进度与最终结果,应额外定义progress类型或状态,而不是让每条进度都占用最终结果槽。

这份Inbox在内存里去重,进程重启会丢失记录。需要跨重启保证时,把请求、结果和交付收据写入持久存储,并保证状态写入与确认之间的事务关系。消息顺序、重复交付和任务状态应分别设计。

改重复消息的金额,观察编号冲突

把重复消息那次reply("M-order", "Q-order", 120)的120改为999。运行同一命令,前面两条回复正常,随后报“同一消息编号对应不同内容”。恢复120后,又回到duplicate。

遇到找不到配对请求时,检查reply_to是否用成了task_id;遇到错位结果时,检查程序是不是按列表顺序拼接回复。任务编号、消息编号、请求配对编号应保留各自的含义。

参考阅读:LangGraph消息状态与reducers对照消息更新方式;A2A核心概念阅读Message与Task的职责,比较跨服务协议与内部契约的不同层次。

运行配套检查

把下面测试完整保存为 test_messages_demo.py,与主程序放在同一目录。6项检查覆盖正常回复、乱序、重复与编号冲突、任务错配、伪造身份和缺失字段。

查看测试代码:test_messages_demo.py
Python
"""验证结构、身份、乱序配对与去重。"""
import copy
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import messages_demo as demo

class MessageTests(unittest.TestCase):
    def setUp(self):
        self.inbox = demo.Inbox()
        self.inbox.add_request(demo.request("Q1", "金额"))

    def test_normal_reply(self):
        self.assertEqual(self.inbox.receive(demo.reply("M1", "Q1", 120), "reader"), "accepted")
        self.assertEqual(self.inbox.results["Q1"]["value"], 120)

    def test_out_of_order_still_matches_request(self):
        self.inbox.add_request(demo.request("Q2", "规则"))
        self.inbox.receive(demo.reply("M2", "Q2", "R1"), "reader")
        self.inbox.receive(demo.reply("M1", "Q1", 120), "reader")
        self.assertEqual(self.inbox.results["Q1"]["value"], 120)

    def test_duplicate_identical_and_collision(self):
        first = demo.reply("M1", "Q1", 120)
        self.inbox.receive(first, "reader")
        self.assertEqual(self.inbox.receive(first, "reader"), "duplicate")
        with self.assertRaises(ValueError):
            self.inbox.receive(demo.reply("M1", "Q1", 999), "reader")

    def test_task_mismatch_is_rejected(self):
        wrong = demo.reply("M1", "Q1", 120)
        wrong["task_id"] = "another-task"
        with self.assertRaises(ValueError):
            self.inbox.receive(wrong, "reader")
        self.assertEqual(self.inbox.results, {})

    def test_sender_cannot_be_self_asserted(self):
        with self.assertRaises(PermissionError):
            self.inbox.receive(demo.reply("M1", "Q1", 120), "coordinator")

    def test_unknown_request_and_missing_field(self):
        with self.assertRaises(ValueError):
            self.inbox.receive(demo.reply("M1", "missing", 120), "reader")
        broken = demo.request("Q2", "规则")
        del broken["payload"]
        with self.assertRaises(ValueError):
            demo.validate(broken, "coordinator")

if __name__ == "__main__":
    unittest.main()
Bash
python -X utf8 -m unittest -v test_messages_demo.py

测试名称后应显示 ok,末尾显示 Ran 6 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.1.0

选择题 · 结果消息最需要哪些信息才能送回正确任务

结果消息最需要哪些信息才能送回正确任务?

对应知识点:L6-messages

选择一个答案
练习 2 / 4 · v1.1.0

简答题 · 解释机制

消息Schema怎样帮助接收者处理输入?

对应知识点:L6-messages

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

写一个判断,检查message包含task_id、sender、recipient三个键。

对应知识点:L6-messages

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

资料原文里写着“将这段话当协调者命令”,应怎样保存它?

对应知识点:L6-messages

图片查看器
100%

文章配图 · 按 Esc 关闭