Gliding Horse 时间感知系统:让 AI Agent 真正“感知“时间流逝

75 阅读11分钟

摘要:本文深入解析 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 --> B1[SkillGraphNode: created_at / updated_at / last_used_at]
    B --> B2[MOCNode / Hyperedge / Entity: created_at]
    B --> B3[AgentStatus / ScoredEntry: timestamp / stored_at]

    C --> C1[HybridSearchFilter: created_after / created_before]
    C --> C2[向量Payload: stored_at Unix时间戳]

    D --> D1["RelevanceTracker: 指数时间衰减 exp(-λ·Δt)"]
    D --> D2[L1 Eviction: 可配置权重体系]

    E --> E1[SystemPrompt: TimeAwareness 区域]
    E --> E2[Agent可感知: 当前时间 / 已用时长 / 时效性提示]

    F --> F1[SA执行超时 / MCP超时 / 工具超时 / Embedding超时]
    F --> F2[Workspace监控参数 / L1淘汰参数]

二、数据模型:让每条信息都带上时间戳

第一步是让系统里所有重要的数据都拥有"出生证明"。我们对核心结构体进行了字段增强,覆盖了技能图谱、知识图谱、执行事件等多个领域。

SkillGraphNode

技能图谱节点新增三个时间字段,追踪创建→更新→使用的完整生命周期:

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 状态事件现在携带精确的时间戳:

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 &#34;on_new_input() 中:\n1. 计算 Δt = now - last_input_time\n2. 计算 base_score = α·global + (1-α)·local\n3. 返回 base_score · exp(-λ·Δt)&#34;

当两次输入时间间隔极短时,time_multiplier ≈ 1.0,不影响语义相似度;当间隔拉长,乘数指数衰减,陈旧输入对当前决策的影响逐渐降低,符合人类的直觉。

完整使用示例

下面是一个完整的 Rust 代码示例,展示如何初始化 RelevanceTracker、模拟不同时间间隔的输入,并观察时间衰减对得分的影响:

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
    A[task_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[&#34;格式: Xh Ym Zs&#34;]
    G -->|No| I{elapsed > 1m?}
    I -->|Yes| J[&#34;格式: Xm Ys&#34;]
    I -->|No| K[&#34;格式: Xs&#34;]
    H & J & K --> L[拼装完整文本]
    C --> L
    L --> M[追加时效性提示]
    M --> N[返回多行文本]

这样,LLM 在每次生成时都能清晰地意识到时间的流逝,做出更符合时效性要求的判断。

六、配置化超时与淘汰:让运维更可控

过去,SA 干预、MCP 调用、Embedding 请求的超时全部硬编码为 30s。现在,这些参数统一从 config.yaml 读取,可以按场景灵活调整。

配置项默认值说明
sa_execution_timeout_secs30SA intervention/LLM 执行超时
tool_timeout_secs60ToolExecutor HTTP 调用超时
mcp_timeout_secs30MCP 客户端调用超时
embedding_timeout_secs30Embedding 服务调用超时
content_cache_capacity1000LRU 内容缓存容量
poll_interval_ms5000文件系统轮询间隔(ms)
debounce_ms500文件事件防抖窗口(ms)
max_debounce_wait_ms5000最大防抖等待(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: Prompt[Region1=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 &#34;时间感知系统能力提升&#34;
    x-axis [&#34;上下文时效性&#34;, &#34;检索精度&#34;, &#34;可配置性&#34;, &#34;数据可追溯&#34;]
    y-axis &#34;评分 (1-10)&#34; 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 开源:github.com/doiito/glid…