Message、Artifact 与异步交接
正在读取学习记录…
Message是交互中说了什么,例如“已经查完资料”。Artifact是任务真正交出的产物,例如退款规则报告、结构数据或文件。异步交接让生产者和接收者可以在不同时间工作:先保存产物,再通知接收者取走并确认。
异步的意思是双方不必一直等在同一条调用链上。可是连接中断、通知重复或接收器重新加载,都可能发生。要让结果可靠,不能只在聊天里发一句“完成了”,然后希望对方及时看到。
通知像快递短信,产物像包裹,收据像签收记录。短信来两遍,不应该让同一个包裹出库两遍。
保存结果,再用交付编号确认
生产者生成“O-17可申请退款120元;依据R-1”,保存为JSON产物文件。文件包含任务编号、正文、产物编号和内容哈希。
哈希是把内容计算成固定长度摘要。程序使用SHA-256;相同内容得到相同摘要,修改正文后原摘要对不上,可以发现内容变化。交付事件E-1则用于标识这一次通知。
接收者核对任务归属和哈希,再把E-1与产物摘要写入receipts.json,即收据文件。第二个Receiver实例重新从文件加载收据,看到相同交付返回duplicate。程序用真实本地文件观察持久化与重新加载,文件放在当前学习目录的临时子目录,正常退出后清理。
保存完整程序并运行
在自己的学习目录打开PowerShell,创建一个新的子目录:
mkdir l6-artifacts
cd l6-artifacts把下面完整代码保存为 artifacts_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。
"""本地文件产物、内容校验与持久交付收据。"""
import hashlib
import json
import tempfile
from pathlib import Path
def digest(text):
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def create_artifact(root, content):
# 内容寻址:相同内容得到相同编号,文件变更能被哈希检测。
artifact_id = digest(content)
artifact = {"artifact_id": artifact_id, "task_id": "T-17",
"content": content, "sha256": artifact_id}
path = Path(root) / (artifact_id + ".json")
path.write_text(json.dumps(artifact, ensure_ascii=False), encoding="utf-8")
return path
class Receiver:
def __init__(self, root):
self.receipt_path = Path(root) / "receipts.json"
self.receipts = json.loads(self.receipt_path.read_text(encoding="utf-8")) if self.receipt_path.exists() else {}
def receive(self, event_id, artifact_path, expected_task):
artifact = json.loads(Path(artifact_path).read_text(encoding="utf-8"))
if artifact["task_id"] != expected_task:
raise ValueError("产物任务不匹配")
actual = digest(artifact["content"])
if artifact["sha256"] != actual or artifact["artifact_id"] != actual:
raise ValueError("产物内容校验失败")
if event_id in self.receipts:
if self.receipts[event_id] != actual:
raise ValueError("同一交付编号携带不同产物")
return "duplicate", artifact["content"]
# 收据写盘后,新建接收者仍能识别这次交付。
self.receipts[event_id] = actual
self.receipt_path.write_text(json.dumps(self.receipts), encoding="utf-8")
return "accepted", artifact["content"]
def main():
# 临时目录放在当前学习目录;退出时清理固定演示的临时文件。
with tempfile.TemporaryDirectory(prefix="handoff-", dir=".") as root:
path = create_artifact(root, "O-17可申请退款120元;依据R-1")
print("进度消息:资料已查完,正在整理")
status, content = Receiver(root).receive("E-1", path, "T-17")
print("首次交付:" + status)
print("产物内容:" + content)
status, _ = Receiver(root).receive("E-1", path, "T-17")
print("重新加载后重送:" + status)
artifact = json.loads(path.read_text(encoding="utf-8"))
artifact["content"] = "金额被改为999元"
path.write_text(json.dumps(artifact, ensure_ascii=False), encoding="utf-8")
try:
Receiver(root).receive("E-2", path, "T-17")
except ValueError as error:
print("篡改产物:" + str(error))
if __name__ == "__main__":
main()保存后执行 dir,确认能看到 artifacts_demo.py,再运行:
ls
python -X utf8 artifacts_demo.py程序输出:
进度消息:资料已查完,正在整理
首次交付:accepted
产物内容:O-17可申请退款120元;依据R-1
重新加载后重送:duplicate
篡改产物:产物内容校验失败从进度消息读到最终产物
“资料已查完,正在整理”是进度消息,告诉界面当前在做什么。下一行accepted说明接收器成功读取并确认这份文件;紧接着的产物内容才是可供后续任务使用的结果。
重新加载后重送输出duplicate,来自磁盘上的收据,不是继续使用第一个Receiver的内存字典。重新实例化接收器保留了哪些状态,由receipts.json决定。
最后程序修改文件正文,却保留原哈希。接收器重新计算digest,发现正文与sha256、artifact_id不一致,拒绝产物。重复通知和产物篡改由不同条件处理,不应都简化为“忽略”。
三类编号分别对应什么
task_id说明这份结果属于T-17;artifact_id标识产物内容;event_id标识一次交付通知。相同产物可能有多次合法通知,但同一个event_id不应突然携带另一份内容。
这里采用内容摘要作为本地artifact_id,方便观察内容变化。跨服务协议通常还定义自己的artifactId与内容结构;A2A用Artifact与Part表达输出,并通过任务关联传递。上面的snake_case文件结构是应用内部格式,接入A2A时要转换为该协议对象。
接收器若已见E-1,再收到E-1与同一摘要,返回duplicate;若E-1带新摘要,报“同一交付编号携带不同产物”。这避免发送者错误复用编号,让新版本悄悄覆盖旧结果。
真实报告还可记录产物版本、生成者、引用和媒体类型。产物修订应使用新版本或新内容标识,让接收者明确它是一次更新,而不是原来的通知再次到达。
哈希与收据各有什么限制
哈希检查内容完整性,不验证作者身份。攻击者如果既能改正文,又能改哈希,就能重新计算一组一致的数据。可信来源需要受验证的连接、签名或受权限保护的存储,不能只靠SHA-256证明“官方资料”。
收据使正常写盘后的状态可重新读取,但文件write_text没有提供跨进程事务或崩溃原子性。接收者在“业务操作成功、收据尚未写入”之间崩溃,通知重送可能再次触发业务操作。
例子的接收行为只是读取并返回文本,没有外部付款副作用。若消费产物会更新订单或付款,应把业务幂等标识和收据放在同一事务里,或用可对账的状态机处理。仅有一个去重字典不能保证外部动作恰好一次。
接收进度的流式连接断开后,也应能重新查询任务或已保存产物。进度消息可能丢失,但交付状态和结果文件需要有可恢复的读取路径。
改交付编号,看去重的单位
把main中第二次receive("E-1", path, "T-17")的E-1改为E-2,运行python -X utf8 artifacts_demo.py。重新加载后的重送结果变为accepted,因为这是新交付编号;产物内容仍是同一份。
先恢复第二次交付的E-1,再把main里首次调用的Receiver(root).receive("E-1", path, "T-17")改为Receiver(root).receive("E-1", path, "T-other"),运行同一命令。程序会报“产物任务不匹配”,此时尚未写入收据。把T-other恢复为T-17,正常结果回到accepted与duplicate。任务归属检查发生在去重之前。
参考阅读:A2A流式与异步操作比较持续连接与断开后的通知;Python hashlib阅读SHA-256摘要的生成方式。
运行配套检查
把下面测试完整保存为 test_artifacts_demo.py,与主程序放在同一目录。5项检查覆盖正常内容、收据重新加载、任务错配、正文变更和交付编号复用。
查看测试代码:test_artifacts_demo.py
"""验证重启去重、任务关联与内容变更。"""
import json
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import artifacts_demo as demo
class ArtifactTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="artifact-test-", dir=".")
self.root = self.temp.name
self.path = demo.create_artifact(self.root, "退款120元")
def tearDown(self):
self.temp.cleanup()
def test_normal_delivery_and_content(self):
self.assertEqual(demo.Receiver(self.root).receive("E1", self.path, "T-17"),
("accepted", "退款120元"))
def test_new_receiver_reads_old_receipt(self):
demo.Receiver(self.root).receive("E1", self.path, "T-17")
self.assertEqual(demo.Receiver(self.root).receive("E1", self.path, "T-17")[0], "duplicate")
def test_wrong_task_rejected_without_receipt(self):
with self.assertRaises(ValueError):
demo.Receiver(self.root).receive("E1", self.path, "T-other")
self.assertFalse((Path(self.root) / "receipts.json").exists())
def test_tampered_content_rejected(self):
value = json.loads(self.path.read_text(encoding="utf-8"))
value["content"] = "999元"
self.path.write_text(json.dumps(value), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "校验失败"):
demo.Receiver(self.root).receive("E1", self.path, "T-17")
def test_reused_event_with_new_artifact_is_rejected(self):
receiver = demo.Receiver(self.root)
receiver.receive("E1", self.path, "T-17")
other = demo.create_artifact(self.root, "退款90元")
with self.assertRaisesRegex(ValueError, "不同产物"):
receiver.receive("E1", other, "T-17")
if __name__ == "__main__":
unittest.main()python -X utf8 -m unittest -v test_artifacts_demo.py测试名称后应显示 ok,末尾显示 Ran 5 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · “正在查订单”和最终金额表分别属于什么
“正在查订单”和最终金额表分别属于什么?
对应知识点:L6-artifacts
简答题 · 解释机制
异步交接怎样把后续结果接回原任务?
对应知识点:L6-artifacts
代码题 · 核对给定数据
用任务编号过滤事件列表,只保留task_id等于t1的事件。
对应知识点:L6-artifacts
场景题 · 分析具体变化
产物分成两段先后返回,怎样避免只保存最后一段?
对应知识点:L6-artifacts
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。