【Agent Harness】Gliding Horse 的 L2 作战地图:让多 Agent 协作从“摸黑”变成“透明”

Gliding Horse 的 L2 作战地图:让多 Agent 协作从“摸黑”变成“透明”

摘要:本文深入解析 Gliding Horse(流马)框架的 L2 共享黑板设计,一套专为多 Agent 协作打造的“实时作战地图”。文章详细拆解了 AgentTracker(Agent 生命体征监控)、三级资源锁(并发控制)、跨任务依赖管理(DAG 任务树)以及 SharedZone(Agent 间结构化通信)四大核心组件。通过将操作系统进程管理思想适配到 AI Agent 协作场景,L2 黑板让调度器(SA)能够实时掌握全局态势,实现从“摸黑协作”到“透明调度”的跨越。适合对多 Agent 系统、AI 工程架构感兴趣的开发者阅读。

关键词:多 Agent 系统;共享黑板;Agent 协作;任务调度;资源锁;Gliding Horse;流马;AI 工程架构

在构建多 Agent 协作系统的过程中,我们遇到了一个核心挑战:当多个 Agent 同时执行不同的子任务时,调度器如何实时掌握全局状态? 某个 Agent 是否还活着?它正在操作什么资源?两个并行任务之间是否存在依赖冲突?这些信息如果不能实时可见,整个系统就会陷入“摸黑协作”的尴尬境地。

Gliding Horse(流马)的 L2 共享黑板,正是为了解决这个问题而设计的。它不仅仅是 Agent 之间的共享内存,更是一张实时作战地图——调度器(SA)可以在这里看到所有 Agent 的状态、资源的占用情况、任务之间的依赖关系,以及跨 Agent 的协调消息。本文将深入拆解这套 L2 作战地图的设计细节,展示它如何为多 Agent 协作提供坚实的工程保障。

一、L2 黑板的定位:不只是“共享内存”

在 Gliding Horse 的四层记忆架构中,L2 是承上启下的关键一层:

  • 向下,它缓存 L0 持久化层中的热数据,提供毫秒级的读写性能。
  • 向上,它为 L3 投影引擎提供实时数据源,按需裁剪上下文注入 LLM。
  • 横向,它是多个 Agent 实例唯一共享的工作区,承载着任务状态、节点数据和协调消息。

传统 Agent 框架往往通过消息队列或轮询来传递状态,而流马的 L2 黑板则将这些能力内建在同一个图存储中,所有 Agent 通过 SPARQL 直接读写,无需额外的通信中间件。

graph TB subgraph Agents["Agent 集群"] PA["PA (计划)"] DA1["DA-1 (执行)"] DA2["DA-2 (执行)"] CA["CA (检查)"] end subgraph L2["L2 作战地图 (Blackboard)"] direction TB Nodes["节点缓存 + Oxigraph SPARQL"] AgentTracker["AgentTracker<br/>实时状态跟踪"] TaskTree["任务树 DAG<br/>依赖管理"] Locks["资源锁<br/>并发控制"] SharedZone["SharedZone<br/>协调消息"] end subgraph SA_Panel["SA 态势面板"] Dashboard["实时仪表盘<br/>Agent 状态 / 任务进度 / 资源冲突"] end PA <-->|"读写"| L2 DA1 <-->|"读写"| L2 DA2 <-->|"读写"| L2 CA <-->|"读写"| L2 SA_Panel -->|"SPARQL 查询"| L2

二、AgentTracker:每个 Agent 的“生命体征”

L2 作战地图的核心组件是 AgentTracker——一个实时跟踪所有 Agent 状态的子系统。每当一个 Agent 实例启动、执行任务或结束时,它都会在 L2 中更新自己的“生命体征”。

pub struct AgentStatus {
    pub agent_id: String,           // Agent 唯一标识
    pub agent_role: String,         // PA / DA / CA / AA / SA
    pub task_iri: String,           // 当前执行的任务 IRI
    pub status: AgentActivity,      // Idle / Working / Blocked / Error
    pub started_at: DateTime<Utc>,
    pub last_heartbeat: DateTime<Utc>,  // 最后心跳时间
    pub current_operation: Option<String>, // 当前操作描述
    pub resource_locks: Vec<ResourceLock>, // 持有的资源锁
}

pub enum AgentActivity {
    Idle,
    Working,
    Blocked,
    Error,
}

pub enum AgentActivity {
Idle,
Working,
Blocked,
Error,
}


下面是一个完整的 Python 示例,展示如何创建 `AgentStatus` 对象、更新心跳、以及 SA 如何通过 SPARQL 查询超时 Agent:

```python
import time
from datetime import datetime, timezone
from typing import Optional, List
from enum import Enum


class AgentActivity(Enum):
    """Agent 活动状态枚举"""
    IDLE = "Idle"
    WORKING = "Working"
    BLOCKED = "Blocked"
    ERROR = "Error"


class ResourceLock:
    """资源锁信息(简化版)"""
    def __init__(self, resource_type: str, resource_id: str, lock_type: str):
        self.resource_type = resource_type
        self.resource_id = resource_id
        self.lock_type = lock_type


class AgentStatus:
    """Agent 状态对象,对应 Rust 中的 AgentStatus 结构体"""

    def __init__(self, agent_id: str, agent_role: str):
        self.agent_id = agent_id
        self.agent_role = agent_role          # PA / DA / CA / AA / SA
        self.task_iri: Optional[str] = None   # 当前执行的任务 IRI
        self.status: AgentActivity = AgentActivity.IDLE
        self.started_at: datetime = datetime.now(timezone.utc)
        self.last_heartbeat: datetime = self.started_at
        self.current_operation: Optional[str] = None  # 当前操作描述
        self.resource_locks: List[ResourceLock] = []  # 持有的资源锁

    def update_heartbeat(self):
        """更新心跳时间戳,Agent 定期调用"""
        self.last_heartbeat = datetime.now(timezone.utc)
        print(f"[{self.agent_id}] 心跳已更新: {self.last_heartbeat.isoformat()}")

    def start_task(self, task_iri: str, operation: str):
        """开始执行任务"""
        self.task_iri = task_iri
        self.status = AgentActivity.WORKING
        self.current_operation = operation
        self.update_heartbeat()
        print(f"[{self.agent_id}] 开始任务: {task_iri} → {operation}")

    def mark_blocked(self, reason: str):
        """标记为阻塞状态"""
        self.status = AgentActivity.BLOCKED
        self.current_operation = reason
        self.update_heartbeat()
        print(f"[{self.agent_id}] 阻塞: {reason}")

    def mark_error(self, error_msg: str):
        """标记为错误状态"""
        self.status = AgentActivity.ERROR
        self.current_operation = error_msg
        self.update_heartbeat()
        print(f"[{self.agent_id}] 错误: {error_msg}")


class SchedulerAgent:
    """调度器(SA),负责监控所有 Agent 状态"""

    HEARTBEAT_TIMEOUT = 30  # 心跳超时阈值(秒)

    def __init__(self):
        self.agents: dict[str, AgentStatus] = {}

    def register_agent(self, agent: AgentStatus):
        """注册 Agent 到 SA 的监控列表"""
        self.agents[agent.agent_id] = agent
        print(f"[SA] 注册 Agent: {agent.agent_id} ({agent.agent_role})")

    def detect_stale_agents(self) -> List[str]:
        """
        检测超时 Agent(对应 Rust 中的 detect_stale_agents())
        返回所有超过 30 秒未心跳的 Agent ID 列表
        """
        now = datetime.now(timezone.utc)
        stale = []
        for agent_id, status in self.agents.items():
            elapsed = (now - status.last_heartbeat).total_seconds()
            if elapsed > self.HEARTBEAT_TIMEOUT:
                stale.append(agent_id)
                print(f"[SA] ⚠️ 检测到僵尸 Agent: {agent_id},已离线 {elapsed:.1f} 秒")
        return stale

    def query_working_agents(self) -> List[AgentStatus]:
        """
        模拟 SPARQL 查询:获取所有 Working 状态的 Agent
        对应原文中的 SPARQL:
        SELECT ?agent ?role ?status ?operation WHERE {
          GRAPH <blackboard:shared> {
            ?agent a <http://agent-os.org/type/AgentStatus> ;
                   <http://agent-os.org/prop/status> "Working" ;
                   <http://agent-os.org/prop/current_operation> ?operation .
          }
        }
        """
        return [a for a in self.agents.values() if a.status == AgentActivity.WORKING]


# ========== 使用示例 ==========

if __name__ == "__main__":
    # 1. 创建 SA 调度器
    sa = SchedulerAgent()

    # 2. 创建三个 Agent 并注册
    da1 = AgentStatus(agent_id="da-001", agent_role="DA")
    da1.start_task("task:code-gen", "生成用户模块代码")

    da2 = AgentStatus(agent_id="da-002", agent_role="DA")
    da2.start_task("task:db-migrate", "执行数据库迁移")

    ca1 = AgentStatus(agent_id="ca-001", agent_role="CA")
    ca1.mark_blocked("等待代码审查结果")

    for agent in [da1, da2, ca1]:
        sa.register_agent(agent)

    # 3. 模拟心跳更新
    time.sleep(1)
    da1.update_heartbeat()   # DA-001 正常心跳
    da2.update_heartbeat()   # DA-002 正常心跳
    # CA-001 故意不更新心跳,模拟超时

    # 4. 模拟等待 32 秒后检测超时
    print("\n--- 等待 32 秒后检测超时 ---")
    # 手动将 ca1 的心跳时间调早,模拟超时
    ca1.last_heartbeat = datetime.now(timezone.utc).replace(year=2020)
    stale_agents = sa.detect_stale_agents()
    print(f"超时 Agent 列表: {stale_agents}")

    # 5. 查询当前正在工作的 Agent
    print("\n--- 查询 Working 状态的 Agent ---")
    working = sa.query_working_agents()
    for w in working:
        print(f"  {w.agent_id} ({w.agent_role}) → {w.current_operation}")

心跳超时检测是这个子系统的关键机制。每个 Agent 定期更新自己的心跳时间戳,SA 则通过 detect_stale_agents() 方法扫描所有超过阈值(默认 30 秒)未心跳的 Agent。一旦发现“僵尸” Agent,SA 可以立即回收其持有的资源锁,并将它负责的子任务重新分配给其他 Agent。

所有状态数据同步写入 Oxigraph 的 blackboard:shared 命名图,这意味着 SA 可以通过 SPARQL 直接查询实时态势:

SELECT ?agent ?role ?status ?operation
WHERE {
  GRAPH <blackboard:shared> {
    ?agent a <http://agent-os.org/type/AgentStatus> ;
           <http://agent-os.org/prop/status> "Working" ;
           <http://agent-os.org/prop/current_operation> ?operation .
  }
}

这种设计让 SA 的态势感知从“被动等待通知”变为“主动实时查询”,调度决策不再依赖猜测。

三、资源锁:防止 Agent 互相踩脚

并行 Agent 最常见的冲突场景是资源竞争——两个 DA 同时试图修改同一个文件,或者一个 Agent 正在读数据,另一个 Agent 却开始写。传统的做法是通过消息队列串行化,但这会牺牲并行性。

流马的方案是三级资源锁,直接在 L2 中实现:

pub struct ResourceLock {
    pub resource_type: String,   // "file", "db", "api", "graph"
    pub resource_id: String,     // 如 "file:///data/sales.csv"
    pub acquired_at: DateTime<Utc>,
    pub acquired_by: String,     // agent_id
    pub lock_type: LockType,     // Read / Write / Exclusive
}

pub enum LockType {
    Read,      // 多个 Agent 可同时持有
    Write,     // 仅一个 Agent 可持有,与其他 Write/Exclusive 互斥
    Exclusive, // 仅一个 Agent 可持有,与其他所有锁互斥
}

锁冲突检测是实时的:当一个 Agent 尝试获取资源锁时,系统会检查该资源的当前锁状态。如果锁冲突(例如两个 Agent 同时请求 Write 锁),后来的请求会被拒绝,Agent 需要等待或选择其他方案。所有锁信息同步到 blackboard:shared 图,SA 可以随时查看“哪些资源正在被谁锁定”。

四、跨任务依赖:让任务树从“孤立”到“关联”

在实际的软件工程流程中,任务之间往往存在复杂的依赖关系——“设计文档”完成之后才能开始“编码”,“数据库迁移”完成之后才能执行“API 测试”。如果 L2 只跟踪单个任务的子树,SA 就无法判断跨任务的阻塞状态。

我们在 TaskTreeNode 中新增了跨任务依赖边

pub struct TaskTreeNode {
    pub task_iri: String,
    pub parent: Option<String>,       // 父任务
    pub children: Vec<String>,        // 子任务
    pub dependencies: Vec<String>,    // 依赖的其他任务
    pub dependents: Vec<String>,      // 依赖此任务的其他任务(反向索引)
    pub status: String,
}

通过 add_task_dependency() 方法,任意两个任务之间可以建立依赖关系。get_task_dag() 方法则利用拓扑排序,将任务树展开为层级化的有向无环图(DAG)。

这使得 SA 可以回答关键调度问题:“任务 B 为什么还没开始?”——因为它依赖的任务 A 还在执行。“如果我取消任务 C,哪些任务会受影响?”——查询 dependents 列表即可。

五、SharedZone:Agent 间的“群聊”

除了结构化的状态数据和资源锁,Agent 有时还需要松耦合的沟通——比如一个 DA 发现了某个潜在问题,希望通知 CA 特别关注;或者 SA 广播一条紧急指令让所有 Agent 暂停当前操作。

SharedZone 提供了这样的能力:

pub struct CoordinationMessage {
    pub from_agent: String,           // 发送者
    pub msg_type: CoordinationMsgType, // 消息类型
    pub payload: serde_json::Value,    // 消息内容
    pub timestamp: DateTime<Utc>,      // 时间戳
}

pub enum CoordinationMsgType {
    TaskAnnouncement,   // 任务公告
    ProgressUpdate,     // 进度更新
    ResourceRequest,    // 资源请求
    ConflictWarning,    // 冲突警告
    SyncRequest,        // 同步请求
}

Agent 可以发布协调消息到 blackboard:shared,其他 Agent 则可以按时间戳或发送者过滤读取。这相当于给 Agent 们开了一个“群聊”,但消息是有结构的、可查询的、持久化的——不是简单的文本广播。

六、给平台带来的核心优势

能力 传统方案 L2 作战地图
Agent 状态可见性 通过日志或外部监控间接推断 SA 可实时 SPARQL 查询每个 Agent 的状态、心跳、当前操作
资源冲突处理 事后检测,人工介入 锁冲突实时拒绝,所有锁状态可视
跨任务依赖 靠文档或约定,容易遗漏 结构化依赖关系,拓扑排序,自动化调度
Agent 间通信 消息队列,额外基础设施 同图存储内协调消息,零延迟,可查询
故障恢复 手动排查 心跳超时自动检测僵尸 Agent,自动回收资源和任务

一句话总结:L2 作战地图让多 Agent 协作从“摸黑干活”变成了“开着雷达飞行”。调度器可以实时掌握全局态势,Agent 之间可以在同一个图空间里安全地共享数据和协调行动,而所有这些信息都是可追溯、可审计的。

七、结语

Gliding Horse 的 L2 黑板设计,本质上是一套多 Agent 操作系统的进程管理表。它借鉴了操作系统中进程控制块(PCB)、资源分配表、死锁检测等经典思想,将它们适配到了 AI Agent 的协作场景中。当你的系统需要同时运行数十个 Agent,并且要求它们安全、高效地协作时,这样一张“作战地图”就不再是锦上添花,而是必备的基础设施。

Gliding Horse 已在 GitHub 开源:https://github.com/doiito/gliding_horse

posted @ 2026-06-29 08:13  doiito  阅读(14)  评论(0)    收藏  举报