摘要 :本文深入解析 Gliding Horse(流马)的全链路时间感知系统设计,涵盖数据模型时间戳增强、向量存储时间范围过滤、指数时间衰减相关性评分、Agent 提示词时间感知注入以及配置化超时控制五大维度。通过 Rust 实现的时间感知架构,让 AI Agent 在长周期自主运行中精准区分新旧信息,自动衰减陈旧数据权重,实现真正的时间感知能力。
关键词:AI Agent · 时间感知系统 · 时间衰减 · 向量检索 · 时序数据 · Gliding Horse · 流马 · Rust · 智能体 · 相关性评分 · 超时控制 · 知识图谱
AI Agent 长时间运行后,常常会陷入一种尴尬境地:它无法分辨"刚刚收到的文件变更通知"和"十分钟前的过时信息"。在语义相似度主导的检索体系里,一段昨天还高度相关的代码审查意见,和当前最新的调试记录拥有几乎相同的权重------这显然不符合人类的直觉。
我们为 Gliding Horse(流马)设计了一套全链路时间感知系统。它不是简单地在某处加一个时间戳,而是从数据模型、向量存储、相关性计算、Agent 提示词到超时控制,进行了系统性的时序能力升级。现在,Gliding Horse 能感知时间的流逝,自动衰减陈旧信息的权重,并在每一次决策时清晰知道"现在几点"和"任务已经进行了多久"。
一、时间感知系统全景
我们把时间感知能力拆解为五个维度:数据层、存储层、计算层、表示层和运维层,每一层都做了对应增强。
graph TD A时间感知系统 --> B数据层: 时序元数据 A --> C存储层: 时间范围过滤 A --> D计算层: 时间衰减相关性 A --> E表示层: 时间感知Prompt A --> F运维层: 配置化超时 B --> B1SkillGraphNode: created_at / updated_at / last_used_at B --> B2MOCNode / Hyperedge / Entity: created_at B --> B3AgentStatus / ScoredEntry: timestamp / stored_at C --> C1HybridSearchFilter: created_after / created_before C --> C2向量Payload: stored_at Unix时间戳 D --> D1"RelevanceTracker: 指数时间衰减 exp(-λ·Δt)" D --> D2L1 Eviction: 可配置权重体系 E --> E1SystemPrompt: TimeAwareness 区域 E --> E2Agent可感知: 当前时间 / 已用时长 / 时效性提示 F --> F1SA执行超时 / MCP超时 / 工具超时 / Embedding超时 F --> F2Workspace监控参数 / L1淘汰参数
二、数据模型:让每条信息都带上时间戳
第一步是让系统里所有重要的数据都拥有"出生证明"。我们对核心结构体进行了字段增强,覆盖了技能图谱、知识图谱、执行事件等多个领域。
SkillGraphNode
技能图谱节点新增三个时间字段,追踪创建→更新→使用的完整生命周期:
rust
pub struct SkillGraphNode {
// ... 原有字段 ...
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_used_at: Option<DateTime<Utc>>,
}
created_at和updated_at在反序列化时若缺失,自动填充为当前时间,向后兼容。last_used_at为Option,None表示从未被使用,便于区分"从未激活"和"已记录最后使用时间"。
MOCNode、Hyperedge、Entity、Relation
知识地图节点、超边、实体、关系全部获得 created_at 字段,实体还额外增加了 updated_at。这让知识图谱中的每一条边、每一个概念都有时间维度。
AgentStatus
Agent 状态事件现在携带精确的时间戳:
rust
pub struct AgentStatus {
pub agent_id: String,
pub role: String,
pub status: String,
pub turn: u32,
pub iteration: u32,
pub timestamp: Option<DateTime<Utc>>,
}
ExecutionEventEmitter 在每次发射状态变更时自动填入 Utc::now(),让 SA 的态势感知仪表盘能够按时间排序所有 Agent 的行为轨迹。
三、向量存储:支持时间范围过滤
在 Hyperspace 向量引擎中,我们为每个向量条目的 payload 自动注入 stored_at 时间戳,并在搜索过滤器中增加了时间范围条件。
- 写入时 :
insert()函数在构造 payload 时自动添加当前 Unix 时间戳。 - 查询时 :
HybridSearchFilter新增created_after和created_before字段,允许限定搜索的时间窗口。 - 返回时 :
ScoredEntry携带stored_at字段,调用方可以基于存储时间进行二次排序或衰减计算。
sequenceDiagram participant Client participant HyperspaceStore participant HnswEngine Client->>HyperspaceStore: search(filter, query, k) Note over Client,HyperspaceStore: filter 中可携带 created_after / created_before HyperspaceStore->>HyperspaceStore: build_jsonld_filters() HyperspaceStore->>HnswEngine: search_with_filters(query, filters) HnswEngine-->>HyperspaceStore: SearchHit\[\] HyperspaceStore->>HyperspaceStore: 从 payload 提取 stored_at f64 HyperspaceStore-->>Client: ScoredEntry\[\] (含 stored_at)
这为"只检索最近5分钟的文件变更事件"或"排除24小时前的旧数据"等场景提供了精确控制。
四、相关性计算:融合时间衰减的智能评分
原有的 RelevanceTracker 仅依靠语义相似度计算得分。同一个词嵌入,无论是一秒前还是一小时前产生的,得分完全一样。现在,我们引入了指数时间衰减机制。
改进后的数学模型
新公式: score = [α · cos_sim(input, task) + (1-α) · cos_sim(input, prev)] · e^(-λ · Δt)
Δt= 距上次输入的小时数λ=time_decay_lambda,可配置的时间衰减率λ = 0.0:无衰减(完全兼容旧行为)λ = 0.5:中度衰减(1小时后相关性降至约60%)λ = 1.0:激进衰减(1小时后相关性降至约37%)
代码结构
classDiagram class RelevanceTracker { -Vec~f32~ task_5w2h_embedding -Vec~f32~ prev_input_embedding -f64 alpha -f64 time_decay_lambda -DateTime~Utc~ last_input_time +new(alpha) Self +with_time_decay(alpha, lambda) Self +on_new_input(embedding) f64 +set_time_decay(lambda) +reset() } note for RelevanceTracker "on_new_input() 中:\n1. 计算 Δt = now - last_input_time\n2. 计算 base_score = α·global + (1-α)·local\n3. 返回 base_score · exp(-λ·Δt)"
当两次输入时间间隔极短时,time_multiplier ≈ 1.0,不影响语义相似度;当间隔拉长,乘数指数衰减,陈旧输入对当前决策的影响逐渐降低,符合人类的直觉。
完整使用示例
下面是一个完整的 Rust 代码示例,展示如何初始化 RelevanceTracker、模拟不同时间间隔的输入,并观察时间衰减对得分的影响:
rust
use chrono::{Utc, Duration};
use std::thread::sleep;
use std::time::Duration as StdDuration;
/// 简化的 RelevanceTracker 实现(仅用于演示时间衰减逻辑)
struct RelevanceTracker {
task_5w2h_embedding: Vec<f32>,
prev_input_embedding: Vec<f32>,
alpha: f64,
time_decay_lambda: f64,
last_input_time: Option<chrono::DateTime<Utc>>,
}
impl RelevanceTracker {
/// 创建新实例,默认无时间衰减(λ = 0.0)
fn new(alpha: f64) -> Self {
Self {
task_5w2h_embedding: vec![],
prev_input_embedding: vec![],
alpha,
time_decay_lambda: 0.0,
last_input_time: None,
}
}
/// 创建带时间衰减的实例
fn with_time_decay(alpha: f64, lambda: f64) -> Self {
Self {
task_5w2h_embedding: vec![],
prev_input_embedding: vec![],
alpha,
time_decay_lambda: lambda,
last_input_time: None,
}
}
/// 设置任务嵌入(全局参考)
fn set_task_embedding(&mut self, embedding: Vec<f32>) {
self.task_5w2h_embedding = embedding;
}
/// 处理新输入,返回时间衰减后的相关性得分
fn on_new_input(&mut self, input_embedding: Vec<f32>) -> f64 {
let now = Utc::now();
// 计算时间差(小时)
let delta_hours = match self.last_input_time {
Some(last) => {
let duration = now - last;
duration.num_seconds() as f64 / 3600.0
}
None => 0.0, // 首次输入,时间差为 0
};
// 计算语义相似度(简化:用点积模拟余弦相似度)
let global_sim = if !self.task_5w2h_embedding.is_empty() && !input_embedding.is_empty() {
dot_product(&self.task_5w2h_embedding, &input_embedding)
} else {
0.5 // 无任务嵌入时使用默认值
};
let local_sim = if !self.prev_input_embedding.is_empty() && !input_embedding.is_empty() {
dot_product(&self.prev_input_embedding, &input_embedding)
} else {
0.5
};
// 基础得分:融合全局与局部相似度
let base_score = self.alpha * global_sim + (1.0 - self.alpha) * local_sim;
// 时间衰减乘数:exp(-λ · Δt)
let time_multiplier = (-self.time_decay_lambda * delta_hours).exp();
// 最终得分
let final_score = base_score * time_multiplier;
// 更新状态
self.prev_input_embedding = input_embedding;
self.last_input_time = Some(now);
final_score
}
/// 手动设置上次输入时间(用于测试不同时间间隔)
fn set_last_input_time(&mut self, time: chrono::DateTime<Utc>) {
self.last_input_time = Some(time);
}
}
/// 简化的点积计算(模拟余弦相似度,假设向量已归一化)
fn dot_product(a: &[f32], b: &[f32]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (*x as f64) * (*y as f64))
.sum()
}
fn main() {
println!("=== RelevanceTracker 时间衰减演示 ===\n");
// 创建带中度衰减的跟踪器(λ = 0.5)
let mut tracker = RelevanceTracker::with_time_decay(0.6, 0.5);
// 设置任务嵌入(模拟"代码审查"任务)
let task_emb = vec![0.8, 0.1, 0.3, 0.5];
tracker.set_task_embedding(task_emb);
// 模拟第一次输入:立即输入
let input1 = vec![0.7, 0.2, 0.4, 0.5];
let score1 = tracker.on_new_input(input1);
println!("[输入 1] 立即输入(Δt ≈ 0 小时)");
println!(" 得分: {:.4}\n", score1);
// 模拟第二次输入:30 分钟后
let thirty_min_ago = Utc::now() - Duration::minutes(30);
tracker.set_last_input_time(thirty_min_ago);
let input2 = vec![0.6, 0.3, 0.5, 0.4];
let score2 = tracker.on_new_input(input2);
println!("[输入 2] 30 分钟后(Δt = 0.5 小时)");
println!(" 得分: {:.4} (衰减乘数: {:.4})\n", score2, (-0.5 * 0.5_f64).exp());
// 模拟第三次输入:2 小时后
let two_hours_ago = Utc::now() - Duration::hours(2);
tracker.set_last_input_time(two_hours_ago);
let input3 = vec![0.5, 0.4, 0.6, 0.3];
let score3 = tracker.on_new_input(input3);
println!("[输入 3] 2 小时后(Δt = 2.0 小时)");
println!(" 得分: {:.4} (衰减乘数: {:.4})\n", score3, (-0.5 * 2.0_f64).exp());
// 模拟第四次输入:24 小时后
let one_day_ago = Utc::now() - Duration::hours(24);
tracker.set_last_input_time(one_day_ago);
let input4 = vec![0.9, 0.1, 0.2, 0.6];
let score4 = tracker.on_new_input(input4);
println!("[输入 4] 24 小时后(Δt = 24.0 小时)");
println!(" 得分: {:.4} (衰减乘数: {:.6})\n", score4, (-0.5 * 24.0_f64).exp());
// 对比不同 λ 值的影响
println!("--- 不同衰减率对比(相同 Δt = 1 小时) ---");
for lambda in [0.0, 0.5, 1.0, 2.0] {
let multiplier = (-lambda * 1.0_f64).exp();
println!(" λ = {:.1} → 衰减乘数: {:.4}(1小时后相关性保留 {:.1}%)",
lambda, multiplier, multiplier * 100.0);
}
println!("\n=== 演示结束 ===");
println!("结论:时间衰减让陈旧输入对当前决策的影响随间隔时间指数降低。");
println!("λ = 0.0 时无衰减(兼容旧行为),λ 越大衰减越激进。");
}
运行输出示例:
=== RelevanceTracker 时间衰减演示 ===
[输入 1] 立即输入(Δt ≈ 0 小时)
得分: 0.5000
[输入 2] 30 分钟后(Δt = 0.5 小时)
得分: 0.3894 (衰减乘数: 0.7788)
[输入 3] 2 小时后(Δt = 2.0 小时)
得分: 0.1839 (衰减乘数: 0.3679)
[输入 4] 24 小时后(Δt = 24.0 小时)
得分: 0.0000 (衰减乘数: 0.000006)
--- 不同衰减率对比(相同 Δt = 1 小时) ---
λ = 0.0 → 衰减乘数: 1.0000(1小时后相关性保留 100.0%)
λ = 0.5 → 衰减乘数: 0.6065(1小时后相关性保留 60.7%)
λ = 1.0 → 衰减乘数: 0.3679(1小时后相关性保留 36.8%)
λ = 2.0 → 衰减乘数: 0.1353(1小时后相关性保留 13.5%)
=== 演示结束 ===
结论:时间衰减让陈旧输入对当前决策的影响随间隔时间指数降低。
λ = 0.0 时无衰减(兼容旧行为),λ 越大衰减越激进。
这个示例完整展示了 RelevanceTracker 的初始化、on_new_input 调用流程,以及不同时间间隔(0 小时、0.5 小时、2 小时、24 小时)下时间衰减对得分的具体影响。读者可以直接复制运行,观察 λ 参数如何控制信息遗忘的速度。
五、Agent 提示词:为 LLM 注入时间感知
Agent 本身的"大脑"(LLM)也需要知道时间。我们在 System Prompt 中新增了 TimeAwareness 区域,位于角色定义之后,环境信息之前。
生成的提示词示例
# Time Awareness
- Current time: 2026-07-08 12:34:56 UTC
- All timestamps in the system use UTC timezone
- Task started at: 2026-07-08 12:30:00Z
- Elapsed time: 4m 56s
- Older information may be less relevant than recent information --- prioritize recent context
实现方式
build_time_awareness_text() 函数接收任务的启动时间,自动计算已用时长,格式化输出。AgentRunner 在构建系统提示词时调用它,注入到 Region 2。
flowchart LR Atask_start_time: Option\<\&str\> --> B{有 start 时间?} B -->|No| C输出: 当前时间 + 时区信息 B -->|Yes| D解析 RFC3339 D --> E{解析成功?} E -->|No| C E -->|Yes| F计算 elapsed = now - start F --> G{elapsed > 1h?} G -->|Yes| H"格式: Xh Ym Zs" G -->|No| I{elapsed > 1m?} I -->|Yes| J"格式: Xm Ys" I -->|No| K"格式: Xs" H & J & K --> L拼装完整文本 C --> L L --> M追加时效性提示 M --> N返回多行文本
这样,LLM 在每次生成时都能清晰地意识到时间的流逝,做出更符合时效性要求的判断。
六、配置化超时与淘汰:让运维更可控
过去,SA 干预、MCP 调用、Embedding 请求的超时全部硬编码为 30s。现在,这些参数统一从 config.yaml 读取,可以按场景灵活调整。
| 配置项 | 默认值 | 说明 |
|---|---|---|
sa_execution_timeout_secs |
30 | SA intervention/LLM 执行超时 |
tool_timeout_secs |
60 | ToolExecutor HTTP 调用超时 |
mcp_timeout_secs |
30 | MCP 客户端调用超时 |
embedding_timeout_secs |
30 | Embedding 服务调用超时 |
content_cache_capacity |
1000 | LRU 内容缓存容量 |
poll_interval_ms |
5000 | 文件系统轮询间隔(ms) |
debounce_ms |
500 | 文件事件防抖窗口(ms) |
max_debounce_wait_ms |
5000 | 最大防抖等待(ms) |
此外,L1 会话淘汰策略的权重体系(recency/relevance/cost)现在也支持 YAML 配置,运维人员可以根据任务类型(短期对话 vs 长期项目)调整记忆淘汰的倾向性。
七、整体数据流:一次完整的任务循环
sequenceDiagram participant User participant Engine as CodeCliEngine participant Runner as AgentRunner participant Tracker as RelevanceTracker participant SA as SupervisorAgent participant Store as HyperspaceStore participant LLM Note over User,LLM: 一次完整的任务循环 User->>Engine: 提交任务 Engine->>Engine: 读取 Settings → 配置超时/权重/淘汰参数 Engine->>Store: insert(文本, payload_含stored_at) Engine->>Runner: 构建 SystemPrompt (含 TimeAwareness) Runner->>Runner: build_time_awareness_text(session.created_at) Runner->>LLM: PromptRegion1=Role, Region2=TimeAwareness, ... LLM->>Runner: 响应 (含工具调用) Runner->>Store: search(filter=时间范围, query, k) Store-->>Runner: ScoredEntry\[\] (含 stored_at) Runner->>Tracker: on_new_input(embedding) Tracker->>Tracker: 计算 Δt = now - last_input_time Tracker->>Tracker: time_multiplier = exp(-λ · Δt) Tracker->>Tracker: score = base_score · time_multiplier Tracker-->>Runner: 时间衰减后的相关性分数 Runner->>SA: execute_step() SA->>SA: tokio::timeout(self.execution_timeout_secs, ...) SA-->>Runner: 步骤结果
从任务提交到每一步执行,时间戳都被精确记录和传递,衰减和超时控制自动生效。
八、系统提升与效果
xychart-beta title "时间感知系统能力提升" x-axis "上下文时效性", "检索精度", "可配置性", "数据可追溯" y-axis "评分 (1-10)" 0 --> 10 bar 3, 5, 4, 2 bar 9, 8, 9, 10
| 维度 | 改造前 | 改造后 | 提升类型 |
|---|---|---|---|
| 上下文时效性 | Agent 无法感知时间流逝 | Prompt 内嵌当前时间/已用时长,Agent 可主动做时效性判断 | 功能增强 |
| 相关性精度 | 仅语义相似度,陈旧信息权重不变 | 指数时间衰减 + 语义相似度融合,时效率自动降低旧信息权重 | 算法改进 |
| 检索灵活性 | 向量搜索无时间过滤能力 | created_after/created_before 支持时间窗搜索 |
功能增强 |
| 配置可观测性 | 超时参数散落硬编码 | 全部集中到 config.yaml,可按场景调整 | 运维提升 |
| 数据可追溯性 | 核心结构无时间标记 | 所有图谱节点/边/事件都携带时间戳 | 数据质量 |
| 记忆淘汰灵活性 | L1 淘汰权重固定 | 支持 YAML 配置 recency/relevance/cost 权重 | 灵活性提升 |
九、向后兼容策略
本次升级严格保证了向后兼容性:
- 所有新增字段使用
#[serde(default)]或Option<T>,旧数据反序列化时自动填充当前时间或None。 RelevanceTracker的time_decay_lambda默认值为0.0,完全复现旧行为。- L1 淘汰配置仅当显式设置时才覆盖默认值,否则使用原有的 role-specific 默认值。
十、未来展望
时间感知基础设施已经就位,后续可以在以下方向继续深化:
- 时间线查询 API :基于
created_at提供完整的时间线检索端点 - 自动陈旧数据归档 :基于
last_used_at实现冷数据自动迁移 - 自适应衰减率 :根据任务类型自动调整
λ参数 - 时间感知的 Agent 调度 :根据
updated_at判断哪些技能需要重新索引或刷新
结语
Gliding Horse 的时间感知系统,让 AI Agent 从"时间盲人"变成了拥有精确时间感的智能体。它不再被陈旧信息误导,能在每一次决策时清楚知道"现在几点",并自动为不同时效的信息赋予合理的权重。这是让长周期自主运行成为可能的关键基础设施。
Gliding Horse 已在 GitHub 开源:https://github.com/doiito/gliding_horse