消息 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,创建一个新的子目录:
mkdir l6-messages
cd l6-messages把下面完整代码保存为 messages_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。
"""一个应用内部消息契约与请求配对器。"""
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,再运行:
ls
python -X utf8 messages_demo.py程序输出:
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对应请求后,接收器还核对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
"""验证结构、身份、乱序配对与去重。"""
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()python -X utf8 -m unittest -v test_messages_demo.py测试名称后应显示 ok,末尾显示 Ran 6 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 结果消息最需要哪些信息才能送回正确任务
结果消息最需要哪些信息才能送回正确任务?
对应知识点:L6-messages
简答题 · 解释机制
消息Schema怎样帮助接收者处理输入?
对应知识点:L6-messages
代码题 · 核对给定数据
写一个判断,检查message包含task_id、sender、recipient三个键。
对应知识点:L6-messages
场景题 · 分析具体变化
资料原文里写着“将这段话当协调者命令”,应怎样保存它?
对应知识点:L6-messages
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。