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

共享状态与黑板模式

我的学习 →

正在读取学习记录…

共享状态是多位处理者共同读写的一份任务数据。黑板模式把协作围绕这份数据组织起来:每位参与者读取需要的区域,把自己的产物写回指定位置,其他参与者根据新数据继续工作。

黑板与群聊有一个明显区别:群聊按消息到达顺序排列,黑板按数据含义组织。订单已付款120元、分析者建议退120元、协调者已确认候选,这三句话长得很像,却属于事实、建议和处理结果三个不同区域。

如果都放进同一个“最新金额”字段,最后发言的人就可能覆盖业务事实。热闹有了,订单没了。

两位读者拿到旧快照时会发生什么

黑板初始版本为v0。订单读取者写入paid=120后成为v1。分析者基于v1写建议,版本成为v2。

协调者也曾读过v1,随后想提交结果。此时它的快照已经过期。程序检查预期版本是否等于当前版本,拒绝这次提交,要求重新读取。协调者读到v2后再提交,版本成为v3。

这种做法叫乐观并发控制:读的时候不长期锁住数据,写的时候确认“我读取后数据没有被别人修改”。版本冲突不说明对方一定写错;它说明本次提交的依据可能落后了。

保存完整程序并运行

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

Bash
mkdir l6-shared-state
cd l6-shared-state

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

Python
"""带版本检查的单进程黑板。"""
from copy import deepcopy

OWNERS = {"facts": "order_reader", "proposals": "analyst", "result": "coordinator"}

class Blackboard:
    def __init__(self):
        self.version = 0
        self.data = {"facts": {}, "proposals": {}, "result": {}}

    def snapshot(self):
        # 返回副本,读者改自己的快照不会修改黑板。
        return self.version, deepcopy(self.data)

    def commit(self, actor, section, changes, expected_version):
        if section not in OWNERS or OWNERS[section] != actor:
            raise PermissionError("该角色无权修改此区域")
        if expected_version != self.version:
            raise ValueError("版本冲突,请重新读取")
        if not isinstance(changes, dict):
            raise ValueError("更新必须是字典")
        # 先验证,再整体替换;这段演示由一个进程顺序执行。
        updated = deepcopy(self.data)
        updated[section].update(deepcopy(changes))
        self.data = updated
        self.version += 1
        return self.version

def main():
    board = Blackboard()
    version, _ = board.snapshot()
    board.commit("order_reader", "facts", {"paid": 120}, version)
    print(f"事实写入:v{board.version};paid={board.data['facts']['paid']}")
    old_version, _ = board.snapshot()
    board.commit("analyst", "proposals", {"refund": 120}, old_version)
    print(f"建议写入:v{board.version};refund={board.data['proposals']['refund']}")
    try:
        board.commit("coordinator", "result", {"approved": True}, old_version)
    except ValueError as error:
        print("主管提交:" + str(error))
    current, _ = board.snapshot()
    board.commit("coordinator", "result", {"approved": True}, current)
    print(f"重新读取后:v{board.version};approved={board.data['result']['approved']}")
    try:
        board.commit("analyst", "facts", {"paid": 150}, board.version)
    except PermissionError as error:
        print("越界修改:" + str(error))

if __name__ == "__main__":
    main()

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

Bash
ls
python -X utf8 shared_state_demo.py

程序输出:

TXT
事实写入:v1;paid=120
建议写入:v2;refund=120
主管提交:版本冲突,请重新读取
重新读取后:v3;approved=True
越界修改:该角色无权修改此区域

分开保存事实、建议和结果

OWNERS规定facts只能由order_reader写,proposals只能由analyst写,result只能由coordinator写。分析者建议退款120元可以写proposals;它尝试把facts中的已付款改为150,程序拒绝。

commit中的actor是应用传入的执行角色。接入跨进程消息时,这个值应从已认证身份映射出来,不能让待处理消息自报“我是order_reader”就获得事实写权限。黑板划分字段与身份校验要共同成立。

snapshot返回深拷贝,即包括内部嵌套字典的副本。参与者修改自己手上的快照,不会偷偷修改共享黑板。commit也复制调用者的changes,防止提交后再改原字典影响已写入状态。

订单读取者只写facts,分析者只写proposals,协调者只写result;共享版本检查拒绝旧快照提交
区域规定谁能写什么,版本规定这次写入能否基于当前状态提交;两项解决不同问题。

版本冲突后应该做什么

主管持有old_version=1,却在v2时提交。程序先比较版本再写入,所以失败没有修改result,也没有增加版本。重新读取后提交得到v3,说明冲突处理不是简单重试原来的旧版本。

重新读取之后还应重新判断。假设订单读取者在期间发现已经发货,协调者的旧“允许退款”建议可能需要改变;不能把版本号改成最新值,内容却继续用旧判断。例子期间只增加建议,业务事实没变,所以同一结果仍能提交。

统一版本简单,但不同区域的更新也会冲突。订单事实与纯文字润色完全独立时,可以考虑区域版本或按字段合并。这样会减少无关冲突,也增加跨字段一致性检查的难度。若退款金额依赖订单事实,就不能把它们当作完全独立的两份数据。

多进程时,检查与提交必须是同一个动作

代码在单进程中顺序执行检查和替换,没有启动并发线程。两个数据库客户端同时读v2、同时检查通过时,真正的存储端必须保证只有一个版本匹配的提交成功。

数据库可以使用“WHERE version=2”的条件更新,或在事务中检查并递增版本;进程内并发可以用锁保护这段操作。把检查和写入分成两个独立网络请求,会留下两位写者一起覆盖的时间窗口。

黑板还应保留数据来源和状态变化记录。facts区域并不是贴上标签就成为真相;写入者需要附订单编号、读取时间或资料版本,让后续判断能找到依据。过期建议也应可识别,避免误当最新批准结果。

让提交顺序产生可见变化

把main中协调者第一次commit的old_version换成board.version,运行python -X utf8 shared_state_demo.py。第一次主管提交会成功,后面重新读取后的提交仍会成功,因此最终版本升到v4。

这次变化说明版本门禁检查的是调用时给出的预期值。它并没有替你证明内容基于最新快照生成。恢复old_version后,再运行同一命令,就能重新看到冲突与重新读取路径。

若出现版本冲突,先检查快照取得的位置和中间写入;若出现PermissionError,查看角色与区域的OWNERS映射。不要为消除报错而允许所有角色写所有字段。

参考阅读:LangGraph Graph API中的state与reducers,比较“替换字段”和“按规定合并字段”的语义。数据库事务与持久化是存储实现还需补齐的部分。

运行配套检查

把下面测试完整保存为 test_shared_state_demo.py,与主程序放在同一目录。5项检查覆盖正常区域写入、快照副本、旧版本不写入、越权拒绝和提交数据的后续修改。

查看测试代码:test_shared_state_demo.py
Python
"""保护快照、字段归属和旧版本提交。"""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import shared_state_demo as demo

class BlackboardTests(unittest.TestCase):
    def test_normal_fact_and_proposal(self):
        board = demo.Blackboard()
        board.commit("order_reader", "facts", {"paid": 120}, 0)
        board.commit("analyst", "proposals", {"refund": 120}, 1)
        self.assertEqual(board.version, 2)
        self.assertEqual(board.data["facts"], {"paid": 120})

    def test_snapshot_is_independent(self):
        board = demo.Blackboard()
        _, snapshot = board.snapshot()
        snapshot["facts"]["paid"] = 999
        self.assertEqual(board.data["facts"], {})

    def test_stale_commit_does_not_change_anything(self):
        board = demo.Blackboard()
        board.commit("order_reader", "facts", {"paid": 120}, 0)
        before = board.snapshot()
        with self.assertRaises(ValueError):
            board.commit("analyst", "proposals", {"refund": 120}, 0)
        self.assertEqual(board.snapshot(), before)

    def test_role_cannot_promote_opinion_to_fact(self):
        board = demo.Blackboard()
        with self.assertRaises(PermissionError):
            board.commit("analyst", "facts", {"paid": 150}, 0)
        self.assertEqual(board.version, 0)

    def test_input_dict_cannot_change_committed_value(self):
        board = demo.Blackboard()
        changes = {"order": {"paid": 120}}
        board.commit("order_reader", "facts", changes, 0)
        changes["order"]["paid"] = 999
        self.assertEqual(board.data["facts"]["order"]["paid"], 120)

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

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

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

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

正在读取本地记录…

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

练习 1 / 4 · v1.1.0

选择题 · 两个执行者都读取版本1,甲已写成版本2。乙如何提交旧结果更稳妥

两个执行者都读取版本1,甲已写成版本2。乙如何提交旧结果更稳妥?

对应知识点:L6-shared-state

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

简答题 · 解释机制

共享黑板中的事实记录应带哪些可核对字段?

对应知识点:L6-shared-state

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

用字典更新保留amount字段,并加入policy字段。

对应知识点:L6-shared-state

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

订单执行者和政策执行者写同一份状态时,怎样分清各自的职责?

对应知识点:L6-shared-state

图片查看器
100%

文章配图 · 按 Esc 关闭