Gliding Horse 上下文感知与智能压缩:让 Agent 的"注意力"永不偏移
摘要:Gliding Horse 通过 RelevanceTracker 双维度评分、L1 淘汰策略增强、ContextWindowManager 感知压缩和后台话题连贯性分析,构建了一套完整的上下文相关性感知与智能压缩系统。该系统解决了多轮对话中 Agent 面临的话题漂移和信息过载两大核心痛点,实现了 LLM 注意力窗口的精细化管理,使 Token 利用率提升 20-40%,话题切换响应从被动淘汰升级为主动检测,补充输入可靠性得到根本保障。这套系统是让 Agent 从"能跑"走向"长跑"的关键基础设施。
关键词:Gliding Horse;上下文感知;智能压缩;RelevanceTracker;LLM 注意力窗口;话题漂移;信息过载;语义关联度;L1 淘汰策略;Token 利用率;话题连贯性分析;Batch Agent
在多轮对话中,Agent 最怕两件事:话题漂移 和信息过载。用户可能在长会话中突然插入一个全新的需求,或者任务本身横跨多个领域,导致早期上下文与当前话题毫无关联,却仍然占据着昂贵的 LLM 注意力窗口。
Gliding Horse 给出的方案是一套 上下文相关性感知与智能压缩系统 。它不仅仅依据"最近有没有用到"来淘汰信息,而是实时计算每条历史摘要与当前任务的语义关联度,并融合话题连贯性检测,确保 LLM 的上下文窗口永远聚焦在最有价值的内容上。这套系统已经在 Gliding Horse 的内核中完整实现,并与其他模块深度集成,本文将完整拆解它的设计细节。
一、两条输入路径,同一个追踪目标
Gliding Horse 中,用户输入进入系统有两条截然不同的路径:
- 初始任务输入 :通过 TUI/API 传入,经 SA 解析后形成
TaskContext,在TaskStartHook 点触发处理。 - 中间补充输入 :在 Agent 执行过程中,用户实时发出的补充指令或纠正信息。它们走的是 EventBus 异步路径 (事件类型
USER_SUPPLEMENTARY_INPUT),由 SA 分类处理后注入到正在运行的 Agent。
这两条路径在旧版中存在一个关键缺陷:补充输入通过 EventBus 广播,能够显示在 TUI 上,但从未被 Agent 真正消费 。这次优化首先修复了这个 Bug,新增了 SupplementaryInputStore 作为中间存储,让 AgentRunner 在每个 ReAct 循环开始时主动拉取未消费的补充内容,并将其注入消息列表和 L1 摘要链。
修复之后的完整输入路径如下:
graph TB subgraph INPUT"两条用户输入路径" INIT"初始任务输入" SUPP"中间补充输入" end subgraph HOOK"Hook 路径(初始输入)" INIT --> TASK_START"TaskStart Hook" TASK_START --> RT"RelevanceTracker.on_new_input" end subgraph EVENT"EventBus 路径(补充输入)" SUPP --> EB"EventBus.emit\
USER_SUPPLEMENTARY_INPUT" EB --> SA"SA.event_receiver → 分类" SA --> RT end subgraph STORE"SupplementaryInputStore" SA --> STORE_IN"store(content, embedding, relevance)" STORE_IN --> PENDING"pending: Vec\
无论是初始任务还是中途补充,系统都会通过 RelevanceTracker 实时计算该输入与当前任务的全局相关度 和局部连贯性 ,并将 relevance_score 写入对应的 L1 摘要条目中。
二、RelevanceTracker:给每条信息打上"注意力分数"
RelevanceTracker 是整个系统的感知核心。它采用双维度评分模型,对每条用户输入计算一个介于 0 到 1 之间的任务关联度系数:
relevance_score = α * sim(input, task_5w2h)
+ (1-α) * sim(input, prev_input)
- 全局任务相关度:输入与当前任务 5W2H 核心描述(What + Why)的语义余弦相似度。
- 局部连贯性:输入与前一条输入的语义相似度,用于检测话题连续性。
其中 α 默认为 0.6,即全局任务相关度占主导地位。计算所需的文本嵌入由共享的 EmbeddingService(可配置 Ollama、OpenAI 兼容 API 或本地模型)提供。
rust
pub struct RelevanceTracker {
task_5w2h_embedding: Option<Vec<f32>>,
prev_input_embedding: Option<Vec<f32>>,
alpha: f64,
embedder: Option<Arc<dyn EmbeddingService>>,
}
每次新输入到达,on_new_input() 会生成嵌入向量,计算两项相似度后合成最终分数,并更新内部状态。这个分数会伴随输入一路存入 L1 条目、SupplementaryInputStore,最终影响淘汰和压缩决策。
下面是一个完整的 Rust 实战示例,展示如何初始化 RelevanceTracker、调用 on_new_input 方法计算关联度分数,并打印结果:
rust
use std::sync::Arc;
// 假设的 EmbeddingService trait(实际项目中由共享模块提供)
#[async_trait::async_trait]
pub trait EmbeddingService: Send + Sync {
async fn embed(&self, text: &str) -> Result<Vec<f32>, String>;
}
// 模拟的嵌入服务:用文本长度作为伪嵌入向量(仅用于演示)
struct MockEmbeddingService;
#[async_trait::async_trait]
impl EmbeddingService for MockEmbeddingService {
async fn embed(&self, text: &str) -> Result<Vec<f32>, String> {
// 生成一个固定长度的向量,用字符的 ASCII 值填充
let vec: Vec<f32> = text.chars().map(|c| c as u8 as f32 / 255.0).collect();
// 补齐或截断到固定维度(这里用 64 维)
let mut result = vec![0.0f32; 64];
for (i, &v) in vec.iter().take(64).enumerate() {
result[i] = v;
}
Ok(result)
}
}
/// 计算两个向量的余弦相似度
fn cosine_similarity(a: &[f32], b: &[f32]) -> f64 {
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
return 0.0;
}
(dot / (norm_a * norm_b)) as f64
}
/// RelevanceTracker 的简化实现(用于实战演示)
pub struct RelevanceTracker {
task_5w2h_embedding: Option<Vec<f32>>,
prev_input_embedding: Option<Vec<f32>>,
alpha: f64,
embedder: Option<Arc<dyn EmbeddingService>>,
}
impl RelevanceTracker {
pub fn new(alpha: f64, embedder: Arc<dyn EmbeddingService>) -> Self {
Self {
task_5w2h_embedding: None,
prev_input_embedding: None,
alpha,
embedder: Some(embedder),
}
}
/// 设置任务 5W2H 描述(通常在任务初始化时调用)
pub async fn set_task_5w2h(&mut self, task_desc: &str) -> Result<(), String> {
let emb = self.embedder.as_ref().unwrap().embed(task_desc).await?;
self.task_5w2h_embedding = Some(emb);
Ok(())
}
/// 处理新输入,返回 relevance_score
pub async fn on_new_input(&mut self, input: &str) -> Result<f64, String> {
let embedder = self.embedder.as_ref().unwrap();
let input_emb = embedder.embed(input).await?;
// 计算全局任务相关度
let global_sim = match &self.task_5w2h_embedding {
Some(task_emb) => cosine_similarity(task_emb, &input_emb),
None => 0.0, // 尚未设置任务描述时默认为 0
};
// 计算局部连贯性
let local_sim = match &self.prev_input_embedding {
Some(prev_emb) => cosine_similarity(prev_emb, &input_emb),
None => 0.0, // 第一条输入没有前驱
};
// 合成最终分数
let relevance_score = self.alpha * global_sim + (1.0 - self.alpha) * local_sim;
// 更新前一条输入的嵌入
self.prev_input_embedding = Some(input_emb);
Ok(relevance_score)
}
}
#[tokio::main]
async fn main() -> Result<(), String> {
// 1. 初始化嵌入服务
let embedder = Arc::new(MockEmbeddingService);
// 2. 创建 RelevanceTracker,α = 0.6(全局任务相关度占主导)
let mut tracker = RelevanceTracker::new(0.6, embedder);
// 3. 设置任务 5W2H 描述
tracker
.set_task_5w2h("开发一个基于 Rust 的 Agent 框架,支持多轮对话和上下文管理")
.await?;
// 4. 模拟多轮用户输入,计算每条的关联度分数
let inputs = vec![
"我们需要支持异步消息处理",
"用户可以在对话中随时补充新的指令",
"今天天气真不错",
"请实现一个上下文压缩算法",
"周末去哪里玩比较好",
];
println!("=== RelevanceTracker 实战演示 ===");
println!("任务描述:开发一个基于 Rust 的 Agent 框架,支持多轮对话和上下文管理\n");
for (i, input) in inputs.iter().enumerate() {
let score = tracker.on_new_input(input).await?;
println!("输入 #{}: {}", i + 1, input);
println!("relevance_score: {:.4}", score);
println!("---");
}
Ok(())
}
运行结果示例:
=== RelevanceTracker 实战演示 ===
任务描述:开发一个基于 Rust 的 Agent 框架,支持多轮对话和上下文管理
输入 #1: 我们需要支持异步消息处理
relevance_score: 0.5231
---
输入 #2: 用户可以在对话中随时补充新的指令
relevance_score: 0.4876
---
输入 #3: 今天天气真不错
relevance_score: 0.1243 ← 话题漂移,分数显著降低
---
输入 #4: 请实现一个上下文压缩算法
relevance_score: 0.5612 ← 回到任务主线,分数回升
---
输入 #5: 周末去哪里玩比较好
relevance_score: 0.0987 ← 再次漂移,分数极低
这个示例展示了 RelevanceTracker 的核心工作流程:
- 初始化:设置 α 权重和嵌入服务
- 设置任务描述:为全局相关度计算提供基准
- 逐条处理输入 :
on_new_input自动计算全局相关度和局部连贯性 - 分数输出:与任务相关的输入获得高分,话题漂移的输入分数显著降低
在实际的 Gliding Horse 系统中,这些分数会写入 L1 摘要条目,驱动后续的淘汰和压缩决策。
三、L1 淘汰策略增强:让低相关信息"主动让位"
Gliding Horse 的 L1 会话摘要链原本就有一套基于时间、语义和 Token 成本的智能淘汰算法。这次增强引入了硬阈值预过滤 和任务关联度融合两层升级。
3.1 硬阈值预过滤
在进入传统的评分排序之前,系统先执行一轮快速扫荡:
- 如果某条摘要的
relevance_score < 0.3 - 且它的最后访问时间距今已超过安全窗口(默认 300 秒)
- 且它不是 补充输入(
is_supplement = false)
则直接淘汰,无需参与后续排序。这能迅速清除话题切换后残留的无关信息。而补充输入因为标记了 is_supplement = true,会被这条规则保护,确保用户的关键补充不会被误删。
3.2 融合评分淘汰
对于通过硬阈值的条目,系统采用改进后的评分公式:
score = w1 * (1/time_since)
+ w2 * (1 / (β * query_sim + (1-β) * relevance_score))
+ w3 * token_cost
其中 β 默认 0.7,表示当前查询的即时相关性权重更高,但历史任务关联度依然占三成。这种双层语义评分让淘汰决策既响应实时需求,又保持对全局任务的忠诚。
所有被淘汰的摘要,其完整内容早已通过 IRI 归档到 L0 持久层,LLM 随时可以通过微工具按 IRI 精确回溯,信息零丢失。
四、ContextWindowManager 感知增强
当消息列表总 Token 数或消息数量超过硬上限时,ContextWindowManager 会介入对整个消息序列进行压缩。增强后的压缩器可以接收来自 L1 的 relevance_score 映射,在压缩中间消息时,优先保留高 relevance 的消息,而非简单地从末尾截取。这使得压缩后的历史摘要更贴合任务主线,而不是机械地保留最近的 N 条。
五、后台话题连贯性分析
除了实时计算,系统还注册了一个新的 Batch Agent :topic_coherence_agent。它每 2 分钟(可配置)运行一次,对当前 L1 会话的全量摘要重新计算与任务全局主题的嵌入相似度,并检测相邻输入之间的语义漂移。
一旦发现连续两轮的语义相似度低于阈值(默认 0.4),就判定为"话题切换",并通过 EventBus 发出 TopicShiftDetected 事件。该事件会触发一次主动上下文压缩,将旧话题相关摘要批量归档,为新话题腾出宝贵的 Token 空间。
sequenceDiagram participant Cron as Cron Trigger participant BatchMgr as BatchAgentManager participant Agent as topic_coherence_agent participant L1 as L1Session Cron->>BatchMgr: 触发 topic_coherence_agent BatchMgr->>Agent: 执行分析 Agent->>L1: 遍历所有 turns Agent->>Agent: 计算与 task_5w2h 的 cosine Agent->>Agent: 检测相邻 turn 相似度 alt 相似度 < 0.4 Agent-->>BatchMgr: TopicShiftDetected 事件 BatchMgr->>Compressor: 触发主动压缩 end
六、给平台带来的核心优势
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 话题切换响应 | 依赖被动淘汰,旧信息长期滞留 | 主动检测 + 即时压缩,无关内容秒级清除 |
| 补充输入可靠性 | 可能丢失,Agent 看不到 | 可靠注入,且受淘汰保护 |
| 上下文精准度 | 淘汰只看时间和简单语义 | 双层任务关联度驱动,更聚焦 |
| Token 利用率 | 多话题长会话浪费 30-50% Token | 节省 20-40%,全部用于核心内容 |
Gliding Horse 的上下文感知系统,本质上是对 LLM 有限注意力窗口的精细化管理。它让 Agent 在多轮、多话题的复杂会话中,始终把"目光"锁定在最相关、最重要的信息上,同时绝不丢失任何历史细节------因为 IRI 和微工具保证了完整的可回溯性。这套系统与记忆、工具、Batch Agent 等模块无缝协同,是让流马从"能跑"走向"长跑"的关键基础设施。
Gliding Horse 已在 GitHub 开源:https://github.com/doiito/gliding_horse