两条输入路径,同一个追踪目标
Gliding Horse 中,用户输入进入系统有两条截然不同的路径:
初始任务输入:通过 TUI/API 传入,经 SA 解析后形成 TaskContext,在 TaskStart Hook 点触发处理。
中间补充输入:在 Agent 执行过程中,用户实时发出的补充指令或纠正信息。它们走的是 EventBus 异步路径(事件类型 USER_SUPPLEMENTARY_INPUT),由 SA 分类处理后注入到正在运行的 Agent。
这两条路径在旧版中存在一个关键缺陷:补充输入通过 EventBus 广播,能够显示在 TUI 上,但从未被 Agent 真正消费。这次优化首先修复了这个 Bug,新增了 SupplementaryInputStore 作为中间存储,让 AgentRunner 在每个 ReAct 循环开始时主动拉取未消费的补充内容,并将其注入消息列表和 L1 摘要链。
修复之后的完整输入路径如下:
L1 条目元数据
AgentRunner CycleStart
SupplementaryInputStore
EventBus 路径(补充输入)
Hook 路径(初始输入)
两条用户输入路径
初始任务输入
中间补充输入
TaskStart Hook
RelevanceTracker.on_new_input
EventBus.emit
USER_SUPPLEMENTARY_INPUT
SA.event_receiver → 分类
store(content, embedding, relevance)
pending: Vec
take_pending() → 注入 messages[]
L1Session.add_summary()
L1Turn { relevance_score, embedding, is_supplement }
无论是初始任务还是中途补充,系统都会通过 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 或本地模型)提供。
pub struct RelevanceTracker {
task_5w2h_embedding: Option<Vec>,
prev_input_embedding: Option<Vec>,
alpha: f64,
embedder: Option<Arc>,
}
每次新输入到达,on_new_input() 会生成嵌入向量,计算两项相似度后合成最终分数,并更新内部状态。这个分数会伴随输入一路存入 L1 条目、SupplementaryInputStore,最终影响淘汰和压缩决策。
下面是一个完整的 Rust 实战示例,展示如何初始化 RelevanceTracker、调用 on_new_input 方法计算关联度分数,并打印结果:
use std::sync::Arc;
// 假设的 EmbeddingService trait(实际项目中由共享模块提供)
#[async_trait::async_trait]
pub trait EmbeddingService: Send + Sync {
async fn embed(&self, text: &str) -> Result<Vec, String>;
}
// 模拟的嵌入服务:用文本长度作为伪嵌入向量(仅用于演示)
struct MockEmbeddingService;
#[async_trait::async_trait]
impl EmbeddingService for MockEmbeddingService {
async fn embed(&self, text: &str) -> Result<Vec, String> {
// 生成一个固定长度的向量,用字符的 ASCII 值填充
let vec: Vec = 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::().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::().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>,
prev_input_embedding: Option<Vec>,
alpha: f64,
embedder: Option<Arc>,
}
impl RelevanceTracker {
pub fn new(alpha: f64, embedder: Arc) -> 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 摘要条目,驱动后续的淘汰和压缩决策。