当前位置: 首页 > news >正文

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

问题浮现:当“全量查询”遇上“多角色并行”

Gliding Horse 的任务由 PA(计划)、DA(执行)、CA(检查)、AA(决策)四种角色接力完成,同一任务可能经历多轮 PDCA 循环。最初的 L2 查询设计非常直接:所有跟某个任务相关的节点全部存入task_nodes[task_iri],查询时一股脑返回。

write_node(node_iri, json_ld)

提取 task_iri

task_nodes[task_iri].push(iri)

dispatch_agent(role=DA)

query_nodes(task_iri)

返回 ALL nodes
(PA 计划 + DA 历史 + CA 审查 + AA 决策)

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的基础上,建立一套多维二级索引,让查询可以按角色、周期、节点类型精准过滤。

L2 多维索引层

dispatch_agent() 优化后路径

角色特定投影帧

降级路径

SA 调度器

优先路径

L3 Projection Engine

L2 Filtered Query

role_index
按 (task, role) 索引

cycle_index
按 (task, cycle) 索引

type_index
按 (task, node_type) 索引

task_nodes
(全量存储)

三、多维度索引设计

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

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结构体:

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并处理查询结果:

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的内部实现如下——它利用三维索引做交集运算,避免全量扫描:

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% 以上,而任务的完成质量却因为注意力集中而明显提升。

http://www.jsqmd.com/news/1139846/

相关文章:

  • 新手一分钟搞懂 Akka HTTP
  • 引言:加密不等于安全
  • CronTick配置指南:从基础设置到高级优化
  • Tokio 运行时配置:worker 线程数与 blocking 线程池的调参思路
  • 如何通过Open-Shell让Windows开始菜单回归经典:完整配置指南
  • Wayback Machine 浏览器扩展:终极网页时光机指南
  • 【LLM系列】深入理解FlashAttention V4:从计算链路演进看非对称硬件时代的注意力优化
  • YOLOv10模型改进-特定领域应用-第95篇:YOLOv10改进策略【特定领域应用】| YOLOv10在农业检测中的应用
  • PyTorch Conv2d 参数详解:padding=1 与 kernel_size=3 如何保持特征图尺寸不变
  • Java毕设选题推荐:基于 SpringBoot 的校园运动会赛程编排管理系统的设计与实现 基于 SpringBoot+Vue 的大学生运动会成【附源码、mysql、文档、调试+代码讲解+全bao等】
  • [GESP202606 八级] 线网建设题解
  • YOLOv10模型改进-损失函数改进-第89篇:YOLOv10改进策略【损失函数】| Huber Loss损失函数
  • Qt信号槽在使用中避免循环绑定
  • PyTorch 2.0 CIFAR-10 模型优化:3种学习率策略对比,测试集准确率提升至69.78%
  • Python爬虫经典案例第83篇:在线博客平台爬取:掘金数据采集实战
  • CIC研判:数字化越转越重,病根还是在低代码工作流
  • BLIP BLIP-2 超详细入门解析
  • 电商罗盘视频怎么下载?罗盘视频跳转、无水印下载和内容库教程
  • PyTorch nn.LSTM 参数详解与避坑指南:batch_first 等5个关键参数实战解析
  • 如何3秒完成网页图片格式转换:Save Image as Type浏览器扩展完整指南
  • Opus 4.8、GPT-5.5、Gemini 3.1 Pro 测了同一套任务,输赢比你想的复杂
  • 非遗网站平台的设计与实现毕业设计任务书
  • 谷歌 Find Hub 双版本迭代优化软硬件生态
  • TensorFlow 2.10 GPU Windows 终极配置:CUDA 11.2 + cuDNN 8.1 避坑 3 要点
  • 支撑 2500 亿行对话数据:某头部大模型公司如何用 TiDB 托住高频 AI 交互场景
  • 3个关键步骤彻底解决Intel Mac过热问题:Turbo Boost Switcher终极指南
  • MDX-M3-Viewer:游戏模型格式的WebGL解析与可视化引擎
  • 重构石化智能底座——乾元通多链路技术赋能石化生产与数据融合
  • 旧地面翻新
  • Transformer 架构 2023:从 GPT-3 到 Stable Diffusion 的 5 大 AIGC 基石模型对比