【Agent Harness】Gliding Horse L2 作战地图深度优化:给多 Agent 上下文装上“精准导航”

Gliding Horse L2 作战地图深度优化:给多 Agent 上下文装上"精准导航"

摘要:本文深入解析 Gliding Horse 多 Agent 系统中 L2 作战地图的上下文污染问题,提出一套基于多维索引(角色、周期、节点类型)的上下文隔离方案。通过 Filtered Query 与 L3 投影引擎的双级回退机制,实现每个 Agent 只获取与其角色相关的精准上下文,Token 消耗降低 60% 以上,彻底解决多角色并行时的注意力混淆问题。适合对多 Agent 系统、Rust 工程化、语义上下文管理感兴趣的开发者阅读。

关键词:Gliding Horse;多 Agent 系统;上下文隔离;L2 作战地图;多维索引;Filtered Query;投影引擎;Agent 调度;Rust;PDCA 循环

在上一篇文章中,我们拆解了 Gliding Horse 的 L2 共享黑板如何化身一张"作战地图",让调度器 SA 实时掌握所有 Agent 的状态、资源锁和协调消息。那张地图解决了"谁能干什么、谁在干什么"的问题,但随着系统承载的任务越来越复杂,一个新的问题浮出水面:多个 Agent 在同一个任务里来回读写,上下文会不会互相污染?

答案是:会,而且非常严重。本文将详细解析我们如何发现这个"上下文污染"问题,以及如何通过一套多维索引和上下文隔离机制,让 L2 从"能看"进化到"能精准筛选",为每个 Agent 提供零噪音的上下文

一、问题浮现:当"全量查询"遇上"多角色并行"

Gliding Horse 的任务由 PA(计划)、DA(执行)、CA(检查)、AA(决策)四种角色接力完成,同一任务可能经历多轮 PDCA 循环。最初的 L2 查询设计非常直接:所有跟某个任务相关的节点全部存入 task_nodes[task_iri],查询时一股脑返回。
flowchart LR A"write_node(node_iri, json_ld)" --> B"提取 task_iri" B --> C"task_nodes\[task_iri.push(iri)"] D"dispatch_agent(role=DA)" --> E"query_nodes(task_iri)" E --> F"返回 ALL nodes\
(PA 计划 + DA 历史 + CA 审查 + AA 决策)"
F --> G"join() 成上下文"

这个"全量返回"的后果是:DA 在编码时,上下文里塞满了 PA 的策略讨论、CA 的挑错记录甚至 AA 的拍板结论。这些信息不仅 Token 白白浪费,更严重的是让 LLM 产生注意力混淆------DA 可能被 PA 的试探性假设带偏,CA 可能被 DA 的中间错误记录误导。

与此同时,我们还发现了另外两个关联问题:

  • L3 投影引擎被旁路 :SA 的 dispatch_agent 直接走 L2 全量查询,没有调用按角色裁剪的投影帧,导致投影系统的语义过滤能力在关键路径上失效。
  • SessionSummary IRI 不规范 :跨会话归档时使用随机 UUID IRI,导致 extract_task_iri 无法将历史摘要关联回原任务,进一步加剧了上下文关联的混乱。

二、优化目标:给每个 Agent 只属于它的视角

我们要实现的核心能力是:当 DA 请求上下文时,只返回历史 DA 轮次的内容,不混入 PA/CA/AA 的数据;当进入第二轮 PDCA 时,第二轮的计划者也只能看到本轮及前一周期相关角色的信息,避免跨周期干扰。

这就需要在原有 task_nodes 的基础上,建立一套多维二级索引,让查询可以按角色、周期、节点类型精准过滤。
graph TB subgraph "dispatch_agent() 优化后路径" SA"SA 调度器" --> Choose{"优先路径"} Choose -->|"角色特定投影帧"| L3"L3 Projection Engine" Choose -->|"降级路径"| L2F"L2 Filtered Query" end subgraph "L2 多维索引层" direction LR RoleIdx"role_index\
按 (task, role) 索引"
CycleIdx"cycle_index\
按 (task, cycle) 索引"
TypeIdx"type_index\
按 (task, node_type) 索引"
BaseStore"task_nodes\
(全量存储)"
end L2F --> RoleIdx L2F --> CycleIdx L2F --> TypeIdx RoleIdx --> BaseStore CycleIdx --> BaseStore TypeIdx --> BaseStore

三、多维度索引设计

我们在 Blackboard 结构体中新增了三张内存 HashMap 索引,分别覆盖 Agent 角色、PDCA 周期和节点类型。

rust 复制代码
pub struct Blackboard {
    // 原有全量存储
    task_nodes: RwLock<HashMap<String, Vec<String>>>,

    // 新增二级索引
    /// role_index[(task_iri, role)] → Vec<node_iri>
    role_index: RwLock<HashMap<(String, AgentRole), Vec<String>>>,

    /// cycle_index[(task_iri, cycle_id)] → Vec<node_iri>
    cycle_index: RwLock<HashMap<(String, String), Vec<String>>>,

    /// type_index[(task_iri, node_type)] → Vec<node_iri>
    type_index: RwLock<HashMap<(String, String), Vec<String>>>,
}

为什么用内存 HashMap 而不是 SPARQL 查询? 因为 dispatch_agent 是高频调用路径,SPARQL 查询涉及序列化/反序列化,延迟较高。二级索引是 O(1) 查找,且与 Oxigraph 中的数据同步更新,兼顾性能与一致性。

索引构建

write_node 时,除了写入全量存储和 Oxigraph,同步更新三维索引:

  • 如果传入 roletask_iri,则加入 role_index
  • 如果传入 cycle_id,则加入 cycle_index
  • 如果节点包含 @type,则加入 type_index

这要求调用 write_node 的地方传递 rolecycle_id,我们沿着调用链一路回溯,在 Agent 每轮 ReAct 循环写入 AgentTurn 节点时附带了这些元数据。

四、Filtered Query:上下文查询的精准投送

新增的 query_nodes_filtered 方法接受一个 QueryFilter 结构体:

rust 复制代码
pub struct QueryFilter {
    pub role: Option<AgentRole>,
    pub cycle_id: Option<String>,
    pub node_type: Option<String>,
}

查询逻辑是先获取全量 IRI 列表,然后与各项过滤条件的索引结果取交集,最后从节点缓存中读取实体。当 DA 请求上下文时,传入 role=DA, cycle_id=当前周期,就能得到仅包含本周期 DA 轮次的节点,彻底隔绝其他角色的信息。

下面是一个完整的调用示例,展示如何实例化 QueryFilter、调用 query_nodes_filtered 并处理查询结果:

rust 复制代码
use std::sync::Arc;
use tokio::sync::RwLock;

/// 假设已有 Blackboard 实例
let blackboard: Arc<Blackboard> = /* 从调度器获取 */;

/// 构造查询过滤器:只查询 DA 角色、当前周期的节点
let filter = QueryFilter {
    role: Some(AgentRole::DA),
    cycle_id: Some("cycle-2026-07-02-001".to_string()),
    node_type: None, // 不过滤节点类型
};

/// 执行过滤查询
let node_iris = blackboard
    .query_nodes_filtered("iri://task/task-42", &filter)
    .await
    .expect("过滤查询失败");

/// 从节点缓存中读取完整实体数据
let mut context_parts: Vec<String> = Vec::new();
for iri in &node_iris {
    if let Some(node) = blackboard.node_cache.read().await.get(iri) {
        // 将节点序列化为 JSON-LD 片段,加入上下文
        context_parts.push(serde_json::to_string_pretty(node).unwrap());
    }
}

/// 组装成最终上下文(按时间戳排序,保证顺序)
let context = context_parts.join("\n---\n");

/// 输出统计信息
println!(
    "DA 角色上下文组装完成:共 {} 个节点,{} 字符",
    node_iris.len(),
    context.len()
);

query_nodes_filtered 的内部实现如下------它利用三维索引做交集运算,避免全量扫描:

rust 复制代码
impl Blackboard {
    pub async fn query_nodes_filtered(
        &self,
        task_iri: &str,
        filter: &QueryFilter,
    ) -> Result<Vec<String>, BlackboardError> {
        // 1. 获取该任务的全量节点列表
        let all_nodes = self.task_nodes.read().await
            .get(task_iri)
            .cloned()
            .unwrap_or_default();

        // 2. 如果没有任何过滤条件,直接返回全量
        if filter.role.is_none() && filter.cycle_id.is_none() && filter.node_type.is_none() {
            return Ok(all_nodes);
        }

        // 3. 收集所有非空过滤条件的索引结果
        let mut candidate_sets: Vec<HashSet<String>> = Vec::new();

        if let Some(role) = &filter.role {
            let key = (task_iri.to_string(), role.clone());
            let set = self.role_index.read().await
                .get(&key)
                .cloned()
                .unwrap_or_default()
                .into_iter()
                .collect::<HashSet<_>>();
            candidate_sets.push(set);
        }

        if let Some(cycle_id) = &filter.cycle_id {
            let key = (task_iri.to_string(), cycle_id.clone());
            let set = self.cycle_index.read().await
                .get(&key)
                .cloned()
                .unwrap_or_default()
                .into_iter()
                .collect::<HashSet<_>>();
            candidate_sets.push(set);
        }

        if let Some(node_type) = &filter.node_type {
            let key = (task_iri.to_string(), node_type.clone());
            let set = self.type_index.read().await
                .get(&key)
                .cloned()
                .unwrap_or_default()
                .into_iter()
                .collect::<HashSet<_>>();
            candidate_sets.push(set);
        }

        // 4. 取所有候选集的交集
        let mut intersection: HashSet<String> = candidate_sets
            .into_iter()
            .reduce(|a, b| a.intersection(&b).cloned().collect())
            .unwrap_or_default();

        // 5. 只保留属于该任务的节点(安全兜底)
        let task_set: HashSet<String> = all_nodes.into_iter().collect();
        intersection.retain(|iri| task_set.contains(iri));

        Ok(intersection.into_iter().collect())
    }
}

这段代码的核心思路是:先按每个非空过滤条件从对应索引中取出候选 IRI 集合,再对所有候选集取交集 。这样当 DA 同时指定 role=DAcycle_id=当前周期 时,只有同时满足两个条件的节点才会被返回,实现了精准的上下文隔离。

五、dispatch_agent 上下文组装重写

原来的 dispatch_agent 中有一段硬编码逻辑:为了避免 AA 看到大量无关数据,直接跳过了 L2 查询,用一个单独的 prev_agent_summary 代替。这种"打补丁"的方式显然不够优雅。

优化后的路径采用双级回退

  1. 优先走 L3 投影引擎scheduler.on_context_request(role, task_iri) 会加载该角色专属的投影帧(如 pa_initda_input),利用 SPARQL 和语义增强生成高度结构化的上下文摘要。
  2. 降级走 Filtered Query :若投影引擎未命中或不可用,则使用 query_nodes_filtered 按角色和周期精准拉取 L2 节点,自行组装上下文。
  3. 兜底使用历史摘要:万一前两级都失败,返回之前保存的精简摘要作为最低保障。

这样,每个 Agent 获得的上下文都经过了"角色过滤 + 周期过滤 + 类型过滤"三重筛选,噪音降到最低。

六、关联修复:让数据"认得家"

我们还修复了两个导致上下文混乱的细节:

  • cycle_id 贯穿全链路 :从 TaskContextAgentTurn 节点,再到 dispatch_agent 查询,cycle_id 作为明确定位符一路携带,确保每个节点都清楚自己属于哪个周期。
  • SessionSummary IRI 规范化 :跨会话归档的 IRI 从 iri://memory/{uuid} 改为 iri://task/{task_id}/session/{session_id},让归档摘要可以正确关联回原任务,不再游离在外。

七、优化成效:从"大杂烩"到"分子料理"

指标 优化前 优化后
DA 单轮上下文大小 包含 PA + DA + CA + AA 全部历史(约 8000 tokens) 仅本周期 DA 历史 + 任务目标(约 2000 tokens)
跨周期干扰 第二轮 PA 能看到第一轮 CA 的审计细节 严格隔离,每个周期初始只保留上一周期决策摘要
投影引擎命中率 0%(被硬编码旁路) > 90%(优先路径)
Agent 注意力准确性 DA 可能被 PA 假设误导,CA 可能被 DA 中间错误干扰 每个 Agent 只看到自己角色的上下文,决策更加专注

在实际测试中,修复后的多角色任务执行不再出现"DA 突然开始质疑计划"或"CA 重复 DA 的工作"这类怪事。上下文 Token 消耗降低了 60% 以上,而任务的完成质量却因为注意力集中而明显提升。

八、结语

L2 作战地图是 Gliding Horse 多 Agent 协作的基石。它从最初的"全局监控"进化到现在的"精准投送",背后是我们对 Agent 系统工程化的一次深刻反思:只解决"看得见"还不够,必须解决"只看该看的"。通过多维索引、上下文过滤和投影引擎深度集成,Gliding Horse 的每个 Agent 终于拥有了一个干净、专注的工作空间,这或许就是 AI 协作从"草台班子"走向"专业团队"的关键一步。

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

相关推荐
花褪残红青杏小9 小时前
Rust图像处理第8节-暗角 & 复古胶片特效:四周衰减中心高亮
rust·webassembly·图形学
妙妙屋(zy)16 小时前
Claude Code+CC-Switch+CC-Connect+飞书使用教程
ai
小七-七牛开发者19 小时前
Coding Agent 规则管理:CLAUDE.md、Skills、Hooks、Subagents 到底怎么选?
ai·大模型·agent·claude·token·loop·mcp·claudecode·ai coding
独孤留白1 天前
从C到Rust:Rust 的 Trait 不是Interface,那是什么?
rust
doiito1 天前
左脚踩右脚:让 LLM 自进化的 Agent 轨迹训练法——为什么它能补上主流范式的最后一块拼图
ai·系统设计
花褪残红青杏小1 天前
Rust图像处理第7节-马赛克像素化:分块取平均色实现打码风格
rust·webassembly·图形学
带刺的坐椅1 天前
从 Claude Code 隐私争议,看 SolonCode 的设计选择
ai·llm·agent·claudecode·soloncode·codingplan
lincats2 天前
Claude Code项目越写越乱?这套清理流程能救你
ai·ai agent·claude code
云燕实验室CloudLab2 天前
《AI开始"抱团"思考了!多智能体 + 思维图到底有多强?》
ai·学习工具·智慧学伴