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

Message、Artifact 与异步交接

我的学习 →

正在读取学习记录…

Message是交互中说了什么,例如“已经查完资料”。Artifact是任务真正交出的产物,例如退款规则报告、结构数据或文件。异步交接让生产者和接收者可以在不同时间工作:先保存产物,再通知接收者取走并确认。

异步的意思是双方不必一直等在同一条调用链上。可是连接中断、通知重复或接收器重新加载,都可能发生。要让结果可靠,不能只在聊天里发一句“完成了”,然后希望对方及时看到。

通知像快递短信,产物像包裹,收据像签收记录。短信来两遍,不应该让同一个包裹出库两遍。

保存结果,再用交付编号确认

生产者生成“O-17可申请退款120元;依据R-1”,保存为JSON产物文件。文件包含任务编号、正文、产物编号和内容哈希。

哈希是把内容计算成固定长度摘要。程序使用SHA-256;相同内容得到相同摘要,修改正文后原摘要对不上,可以发现内容变化。交付事件E-1则用于标识这一次通知。

接收者核对任务归属和哈希,再把E-1与产物摘要写入receipts.json,即收据文件。第二个Receiver实例重新从文件加载收据,看到相同交付返回duplicate。程序用真实本地文件观察持久化与重新加载,文件放在当前学习目录的临时子目录,正常退出后清理。

保存完整程序并运行

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

Bash
mkdir l6-artifacts
cd l6-artifacts

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

Python
"""本地文件产物、内容校验与持久交付收据。"""
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,再运行:

Bash
ls
python -X utf8 artifacts_demo.py

程序输出:

TXT
进度消息:资料已查完,正在整理
首次交付: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
Python
"""验证重启去重、任务关联与内容变更。"""
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()
Bash
python -X utf8 -m unittest -v test_artifacts_demo.py

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

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

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

正在读取本地记录…

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

练习 1 / 4 · v1.1.0

选择题 · “正在查订单”和最终金额表分别属于什么

“正在查订单”和最终金额表分别属于什么?

对应知识点:L6-artifacts

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

简答题 · 解释机制

异步交接怎样把后续结果接回原任务?

对应知识点:L6-artifacts

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

用任务编号过滤事件列表,只保留task_id等于t1的事件。

对应知识点:L6-artifacts

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

产物分成两段先后返回,怎样避免只保存最后一段?

对应知识点:L6-artifacts

图片查看器
100%

文章配图 · 按 Esc 关闭