Skip to content

11_04_Memory_System

Wyckoff edited this page Jun 21, 2026 · 1 revision

(四) 记忆系统分层与四路召回

记忆系统在 cli/memory.py,只服务 CLI(命令行)/ TUI(终端图形界面)。

它不是聊天记录归档,也不是持仓数据库。它的目的只有一个:让后续交易判断持续遵守用户已经确认过的交易纪律,例如不追涨、偏好尾盘二次确认、跌破确认支撑不买、极端放量冲高回落不买。

因此,记忆系统只沉淀稳定、可复用的信息;每天行情、当前持仓、某只股票今天能不能买、工具调用细节,都不应该进入长期记忆。这些内容应该来自实时工具、推荐表、持仓表、信号表或 chat log。

5.1 分层语义:L1 / L2 / L3 不是优先级

L1、L2、L3 表示信息的抽象方式,不表示谁比谁“更高级”、也不表示 L3 一定覆盖 L2。

flowchart TD
    L1["L1 原子记忆<br/>偏好 / 决策"] --> L2["L2 交易剧本<br/>什么场景下怎么做"]
    L1 --> L3["L3 用户画像<br/>这个用户长期是什么交易风格"]
Loading
层级 存储类型 解决的问题 典型内容 不该放什么
L1 preference / decision 记录原始证据 “用户偏好尾盘买入”;“跌破确认支撑不买” 今日行情、一次性买卖、工具执行过程
L2 playbook 把多个 L1 归纳成可执行流程 “尾盘二次确认买入剧本:14:45 后检查支撑、VWAP、量能;破支撑不买” 泛泛的投资理念、没有触发条件的口号
L3 persona 把多个 L1 归纳成用户画像 “用户重视本金安全,偏好二次确认,不喜欢开盘交易影响情绪” 临时市场观点、某只股票当前结论

三者的使用方式不同:

  • L1 是证据:回答“这条规则从哪里来”。
  • L2 是动作:回答“遇到这个交易场景应该怎么执行”。
  • L3 是约束:回答“这个用户整体是什么风险偏好和交易性格”。

举例:

L1:
- [preference] 用户不想每天开盘买,容易影响心情
- [preference] 用户倾向尾盘买入
- [decision] 当天跌破确认支撑不买
- [decision] 极端放量但冲高回落不买

L2:
- [playbook] 尾盘二次确认买入:适用于已进入候选池且需要二次确认的股票;14:45 后检查支撑、VWAP、收盘位置和量能;跌破确认支撑或极端放量冲高回落时禁止买入。

L3:
- [persona] 用户重视本金安全和交易情绪稳定,偏好二次确认,不喜欢开盘冲动交易。

所以,L2 和 L3 是从同一批 L1 派生出的两个维度:L2 面向交易动作,L3 面向用户画像

5.2 写入流程:先存 L1,再蒸馏 L2/L3

L1 的生成:会话摘要提取

对话结束或新开会话时,save_session_summary() 在后台线程异步执行。触发前先做轻量门控,任一不满足则跳过:

  • 消息总数 ≥ 4 条
  • 本轮对话有工具调用(纯聊天不值得提取)
  • provider(模型适配层)已配置
  • 当前 session(会话)的摘要 hash(哈希)没有处理过;如果 /new、resume(恢复会话)、fork(分叉会话)或退出重复触发同一批消息,直接跳过,不再请求 LLM

满足条件后,系统会把对话消息转换成摘要输入文本,再发给 LLM 用 _SESSION_SUMMARY_PROMPT 提取。摘要输入不是直接取原始 messages[-40:],而是先遍历全部消息,将非空 content 格式化成 [role] content 文本行;其中 role=tool 的工具结果如果超过 200 字,会先截断到 200 字。最后取这些文本行的最后 40 行。只有 tool_calls、没有 content 的 assistant 消息不会进入摘要输入。

Prompt 只允许输出两种格式:

[偏好] 用户偏好尾盘二次确认,不想开盘买入影响情绪
[决策] 当天跌破确认支撑不买

解析后按标签写入 SQLite:[偏好]preference(L1),[决策]decision(L1)。无论提取结果是否写入新 L1,系统都会写入一个 session marker(处理标记),记录 summary_hash,用于避免同一批消息重复摘要;这个 marker 不参与召回注入。

如果一条 L1 内容自身包含 6 位代码或明确股票名称,写入前会用本地股票名称缓存解析成 codes,用于后续确定性召回;没有明确股票的全局交易纪律保持 codes=''。这样可以避免“本轮聊过宁德时代,所以不追涨这条全局偏好也被误绑到宁德时代”。

写入前经过两级去重。第一层是 确定性去重:对同类型已有记忆做规范化文本比较和字符 bigram(双字片段)相似度判断,完全重复或高度相似时直接跳过,不调用 LLM。

第二层是 LLM 语义去重:当同类型已有记忆存在,且确定性规则未判定重复时,系统会使用 fallback provider(不可用时复用当前 provider)把新内容和最近 10 条同类型记忆一起发给 _DEDUP_PROMPT 判断。只有明确判定为 NEW 的内容才写入 L1;如果语义去重调用失败、返回格式无效,或返回了不存在的重复记忆 id,系统会跳过本条写入,避免未经确认的重复记忆进入长期记忆。

L2 / L3 的生成:批量蒸馏,不是单条晋升

每次成功写入至少一条新 L1,且 skip_layers=False 时,会尝试触发 refresh_memory_layers()。触发不等于一定调用 LLM,系统先做增量门控:

atoms = get_recent_L1_preference_and_decision(limit=30)

if len(atoms) < 3:
    return 0

source_hash = hash(atoms)
if source_hash 已经被 L2/L3 记录过:
    return 0

if 不是首次蒸馏 and 新增 L1 数量 < 3:
    return 0

_LAYER_REFRESH_PROMPT 要求 LLM 基于最近 L1 归纳两类高层记忆,每类最多 3 条:

[画像] 用户重视本金安全和情绪稳定,偏好尾盘二次确认
[剧本] 尾盘二次确认买入:适用于已进入候选池的股票;14:45 后检查支撑、VWAP、收盘位置和量能;跌破确认支撑或极端放量冲高回落时禁止买入

解析标签后写入:[画像]persona(L3),[剧本] / [交易剧本]playbook(L2)。历史 scenario 记录仅做兼容召回;新生成的 L2 统一写入 playbook

每条由层级蒸馏生成的 L2/L3 都会在 metadata 中记录:

  • extractor=layer_refresh
  • layer_version
  • source_l1_ids
  • source_hash

关键设计:L2/L3 不是“某条 L1 被升级”,而是“最近一批 L1 被重新归纳”。原始 L1 仍然保留,L2/L3 只是在它们之上生成可复用摘要。系统每次蒸馏仍读取最近 30 条 L1 以保留全局语境,但只有当 source hash 未处理过,且距离上次蒸馏至少新增 3 条 L1 时才真正调用 LLM。这样避免同一批 L1 被反复重算,也降低 L2/L3 被同义改写污染的概率。

完整生命周期

flowchart TD
    A["对话结束 / 新会话"] --> B{"消息 ≥ 4 条\n且有工具调用"}
    B -->|否| Z["跳过"]
    B -->|是| H0{"session summary_hash\n是否已处理"}
    H0 -->|是| Z
    H0 -->|否| C["取最近 40 条消息\n工具结果截断至 200 字"]
    C --> D["LLM 提取\n最多 3 条 [偏好] / [决策]"]
    D --> E{"确定性去重\n规范化文本 / bigram 相似度"}
    E -->|重复| M["写 session marker"]
    E -->|新内容| E2{"同类型已有记忆"}
    E2 -->|否| F["写入 SQLite\nL1 preference / decision"]
    E2 -->|是| E3{"LLM 语义去重\n与已有 10 条对比"}
    E3 -->|重复或失败| M
    E3 -->|NEW| F
    F --> M
    M --> G{"是否退出场景\nskip_layers"}
    G -->|是| Z
    G -->|否| H{"L1 总数 ≥ 3 条"}
    H -->|否| Z
    H -->|是| I{"source_hash 是否处理过\n或新增 L1 不足 3 条"}
    I -->|是| Z
    I -->|否| J["取最近 30 条 L1\n发给 LLM 蒸馏"]
    J --> K["LLM 输出 [画像] / [剧本]"]
    K --> L2["L2 playbook\n条件化交易剧本"]
    K --> L3["L3 persona\n用户稳定画像"]
    L2 --> W["写入 SQLite\nmetadata 记录 source_l1_ids / source_hash"]
    L3 --> W
Loading

容量上限与老化清理

类型 上限 清理策略
preference 50 条 超出后删最旧;永不因时间衰减降权;长期保留
decision 30 条 超出后删最旧;45 天后被 prune_memories 清理
playbook 20 条 超出后删最旧;60 天后清理
persona 5 条 超出后删最旧;永久保留,不受时间清理影响
scenario 兼容类型 旧版本 L2 记录,按 playbook 规则召回和清理

5.3 召回管道设计

召回入口流程总览:

flowchart TD
    U["当前用户消息"] --> C["解析股票代码 / 股票名称"]
    U --> K["抽取中文关键词"]
    U --> F["FTS5 全文检索"]
    U --> A["历史归档检索 (search_context_archives)"]
    C --> S["代码精确匹配"]
    K --> L["关键词 LIKE 匹配"]
    F --> R["混合排序 + 类型半衰期"]
    S --> R
    L --> R
    R --> P["Persona / Preference 置顶"]
    P --> D["按 id 去重"]
    D --> I["注入 relevant-memories"]
    A --> AR["提取匹配归档引用 (archive_recall_lines)"]
    AR --> ARC["追加 # 压缩归档"]
    I --> OUT["合并后的记忆上下文"]
    ARC --> OUT
Loading

召回由 build_memory_context()cli/memory.py)驱动,底层实现在 search_memory_hybrid()integrations/local_db.py)以及 cli/context_archive.py。每次用户发送消息,系统先把 6 位代码和本地股票名称解析成确定性 codes,再并行启动四路独立检索管道,结果进行合并、去重、过滤和追加排序。

股票作用域过滤是防止错召的关键:

  • 当前问题能解析出明确股票时,只保留全局记忆和同代码记忆。例如“我想建仓比亚迪”会解析到 002594,不会召回 300750 宁德时代的单股建仓记忆。
  • 当前问题没有明确股票时,带 codes 的单股记忆不参与召回。例如“我想建仓科技”不会因为“科技”两个字召回“长信科技”的单股记忆。
  • 科技白酒 这类词属于板块/主题,不等同于股票名;它们可以作为未来主题记忆的实体,但不能靠股票代码确定性召回。

四路检索管道

管道一:FTS5 全文检索(权重 0.8)

将用户原始消息直接提交给 SQLite FTS5 虚拟表:

SELECT m.*, bm25(agent_memory_fts) AS rank
FROM agent_memory_fts fts
JOIN agent_memory m ON m.id = fts.rowid
WHERE agent_memory_fts MATCH '用户输入'
ORDER BY rank

FTS5 使用 SQLite 默认 unicode61 tokenizer 建立倒排索引后用 BM25 排序。BM25 同时考虑词频(TF)和逆文档频率(IDF),比单纯 LIKE 匹配更精准,也能容忍部分字段不完整匹配;中文短词覆盖主要由后面的关键词 2-gram 管道兜底。

管道二:股票实体精确匹配(权重 1.2)

正则 (?<!\d)(\d{6})(?!\d) 从用户输入中抽取 6 位股票代码,同时使用本地 stock_list_cache.json 做股票名到代码的最长匹配。例如“宁德时代”映射为 300750,“比亚迪”映射为 002594,然后对 codes 字段做 LIKE 匹配:

WHERE codes LIKE '%300750%'

代码命中说明这条记忆明确与当前股票相关。FTS5 和关键词召回得到的候选也会经过同样的代码作用域过滤,避免同一个“建仓”泛词把其它股票的历史记忆带进来。

管道三:中文关键词 LIKE 匹配(权重 0.25)

_extract_keywords() 对用户输入做轻量分词:

  1. 正则 [一-鿿]{2,4} 抓出所有 2~4 字中文片段。
  2. 超过 2 字的片段按 2-gram 滑窗拆分("建仓位置"→"建仓"+"仓位"+"位置"),提高短词召回率。
  3. 过滤停用词和泛交易词("可以"、"现在"、"建仓"、"买入"、"止损" 等)。
  4. 去重,取前 5 个关键词。

对每个关键词做 content LIKE %keyword%,多词 OR 合并查询,命中任意一词即得分。权重最低,用于兜底覆盖 FTS5 未建索引的边角情况。

管道四:历史归档元数据检索(Context Archive Recall)

在上下文压缩发生后,历史高保真消息已移出活跃上下文,被持久化为本地 context_archive/{session_id}/{compaction_id}.jsonl。为防止信息永久丢失,系统利用元数据进行召回:

  1. 提取关键词:系统将用户当前消息提取股票代码及 2-gram 中文词,构建为查询项集合。
  2. 元数据匹配:扫描当前 session 下(或全局)所有 .json 元数据索引文件,通过 _meta_score 算法计算查询项在元数据的 compaction_idsummarycodesfileskeywords 字段中的匹配交集数量。
  3. 加权排序:匹配交集得分大于 0 的元数据记录,按照匹配得分及 created_at 时间戳降序排序,筛选出排名前 2 的归档元数据记录(通过 archive_recall_lines)。
  4. 注入上下文:归档不参与传统 memory 混排,而是在内存拼接时单独作为一个 # 压缩归档 部分注入:
    # 压缩归档
    - archive://default/ctx_20260621120000_abcd12345678 [002594]:关于比亚迪突破120日均线的建仓位置讨论与策略规划。
  5. 主动恢复(Recovery): 如果 Agent 发现用户提到了归档中涉及的旧内容(例如“我们上次谈到比亚迪突破120日线,那个具体策略是什么”),由于上下文中有 # 压缩归档 下的具体 URI archive://...,Agent 可以主动使用 query_history(source="archive", archive_ref="archive://default/ctx_...") 工具读取该归档下的完整消息流以了解详细历史,实现无缝的历史接力。

结果合并:取最高分,不叠加

针对数据库召回的 L1/L2 记忆(FTS5、代码精确、中文关键词):

candidates: dict[int, dict] = {}   # key = 记忆 id

def _merge(items, source_weight):
    for m in items:
        if m["id"] not in candidates:
            m["_score"] = source_weight
            candidates[m["id"]] = m
        else:
            candidates[m["id"]]["_score"] = max(已有分, source_weight)

同一条记忆被多个管道命中,得分取三者中的最大值,不叠加。这样避免"被多个低质量管道重复命中的普通记忆"压过"只被 FTS5 精准命中的高质量记忆"。

时间衰减(按类型缩短半衰期)

所有候选记忆都乘以时间衰减系数,默认半衰期 14 天;L2 交易剧本稍长,L3 画像和长期偏好不衰减:

$$\mathrm{finalScore} = \mathrm{baseScore} \times 2^{-\mathrm{ageDays}/\mathrm{halfLifeDays}}$$

类型 半衰期 自动清理
decision 14 天 45 天
playbook / scenario 21 天 60 天
stock_opinion / market_view / fact 14 天 30-45 天
preference / persona 不衰减 长期保留

这样做的目的,是让阶段性单股判断和交易剧本更快让位于新反馈;用户的长期偏好和风险边界仍然稳定保留。

什么内容应该进记忆

记忆系统的判断标准不是“这句话重要不重要”,而是“它以后是否能稳定改善交易决策”。

信息 是否进记忆 推荐落点
“我不想开盘买,影响心情” L1 preference,后续可蒸馏进 L3 persona
“跌破确认支撑不买” L1 decision,后续可蒸馏进 L2 playbook
“尾盘二次确认后再买” L1 preference / L2 playbook
“今天泛微网络可以买吗” 当前工具分析 / chat log
“我现在持有 4 只股票” portfolio / Supabase 持仓表
“今天市场资金撤退明显” 市场数据表 / 当日报告
“某次工具调用失败” agent log / scratchpad

阶段性观点与冲突记忆

投资场景里有一类信息变化很快:今天认为黄金有避险价值,明天觉得黄金逻辑失效、科技主线更强,后天又认为白酒长期下跌后有修复机会。这类内容不能简单当成长期画像覆盖,也不能只保留最后一句,否则会丢失用户决策风格中的“切换条件”。

当前实现采用 追加式记忆 + 当前轮召回 ,而不是覆盖式记忆:

  1. 临时交易指令默认不沉淀_SESSION_SUMMARY_PROMPT 明确要求不要提取具体买卖事实、临时操作和当天市场状态。
  2. 阶段性判断优先落在 L1 decision:如果用户表达的是“因为某个逻辑失效,所以从黄金切到科技”,这更像决策逻辑,而不是永久偏好。
  3. 稳定风格才进入 preference / persona:例如“不追涨”、“重视止损”、“单票仓位不超过 15%”,才适合长期置顶。
  4. 旧判断不自动删除:系统保留 created_atsource_ref,让模型看到用户观点变化的时间顺序,也能用 wyckoff memory trace <id> 回看来源。
  5. 召回只作为参考:记忆被包在 `<relevant-memories>` 中,并声明"不代表当前任务进程,仅作为参考",当前问题和工具实时数据仍然优先。

因此,黄金、科技、白酒这类主线切换更适合作为阶段性 decision 或 L2 playbook 的触发条件被召回,而不是永久 persona。当前系统能保留变化过程和来源证据,但还没有显式的 superseded(被新观点取代)状态;如果后续要更严格处理冲突,可以在 agent_memory.metadata 中增加 topic_keyvalid_untilsuperseded_by 等字段,用于把同一主题下的旧观点标记为已被覆盖。

Persona / Preference 置顶,Playbook 相关召回

persona(L3)和 preference(L1)还额外走一条旁路,不参与 hybrid search 的分数竞争,直接按时间取最新的(persona 取 1 条,preference 取 5 条),在最终组装时强制排在最前。

注意这里不是 L3 全量召回:当前只无条件置顶最新 1 条 personaplaybook(L2)走 hybrid search,只有和当前 query 相关时才进入“# 交易剧本”。

置顶后的 persona / preference 会按 memory id 从 hybrid 结果里去重,避免同一条偏好既出现在"# 用户画像",又出现在"# 历史记忆":

# 用户画像(persona + preference,置顶)
# 交易剧本(playbook / legacy scenario,hybrid search 命中,最多 3 条)
# 历史记忆(decision 等,hybrid search 命中)

总量控制

全部内容组装后经 _budget_recall_lines() 做 token 预算截断:每条记忆不超过 200 字符,所有召回内容合计不超过 1200 字符。超出时从末尾截断,保证注入体积不会对上下文窗口造成压力。

注入格式通过 prepend_memory_context() 把召回记忆包在 `<relevant-memories>`(相关记忆)中,再把当前用户消息包在 `<current-user-message>`(当前用户消息)中。这样模型可以参考长期偏好,但不会把记忆误当成当前任务进度。

5.4 记忆注入机制与缓存命中

记忆召回的注入采用 瞬态注入(Transient Injection) 模式:注入→发送→还原,只活在当前轮,不污染历史消息。

完整三步流程

第一步:构建并暂存(_send_messagetui.py

用户发出消息后,先调用 build_memory_context(text) 做 hybrid search 召回记忆,结果暂存在私有字段 _memory_context 里,此时 messages[-1]["content"] 仍是原始用户输入:

user_message = {"role": "user", "content": text}
if mem_ctx:
    user_message["_memory_context"] = mem_ctx  # 暂存,不直接污染 content
self._messages.append(user_message)

第二步:发送前注入(_prepare_turn_memory_contexttui.py

_run_agent() 启动时立即调用,把记忆从 _memory_context 弹出并 prepend 到 content 前,原始文本备份到 _raw_content

self._messages[turn_index]["_raw_content"] = user_text          # 备份原文
self._messages[turn_index]["content"] = prepend_memory_context(user_text, memory_context)

实际发给模型的 content 格式如下:

<relevant-memories>
以下是当前对话召回的相关记忆,不代表当前任务进程,仅作为参考:

# 用户画像
- [persona] 用户重视本金安全和情绪稳定,偏好尾盘二次确认
- [preference] 用户不想开盘买入,避免影响一天心情

# 交易剧本
- [playbook] 尾盘二次确认买入:14:45 后检查支撑、VWAP、收盘位置和量能;破支撑不买

# 历史记忆
- [decision] 极端放量但冲高回落不买
</relevant-memories>

<current-user-message>
宁德时代现在可以建仓吗
</current-user-message>

第三步:回复后还原(_restore_turn_user_messagetui.py

模型回复结束后,把 content 从注入版本恢复为原始用户输入:

msg["content"] = msg.pop("_raw_content")   # 擦除注入,还原原文

为什么不注入到 System Prompt?

方案 问题
改 System Prompt 记忆持续占用所有后续轮次 token;每轮重建 system prompt 会破坏静态基底缓存
Prepend 到当前轮 user message(当前实现) 只影响当前轮;用后擦除;记忆与当前问题绑定,模型关联度更高

与 Prompt Caching 的关系

Section 2 描述的 滑动提示词缓存 依赖前缀稳定性:只有发给 API 的 messages 前缀与上一次请求完全一致,才能命中缓存。

还原后,每轮发出的历史消息结构如下:

第 N+1 轮发出的 messages(还原后):
system | u1 | a1 | u2 | a2 | ... | u(N-1) | a(N-1) | uN(还原后) | aN | uN+1+记忆
←────────── 全部命中第 N 轮的缓存 ──────────────────→ ↑miss(因为缓存里是 uN+记忆)

每轮固定只 miss 上一轮的最后一条 user message(因为那条发出时带了注入的记忆,还原后变成干净原文,与缓存不一致)。其余所有更早的历史消息前缀完全稳定,全部命中缓存。

对话轮次 N 历史消息总数 每轮 miss 条数 miss 比例
2 2 1 50%(短对话损失大)
5 8 1 12.5%
10 18 1 5.6%
20 38 1 2.6%(趋近于零)

缓存 miss 的代价是固定 O(1) 的,不随对话增长而扩大。

与不还原相比(记忆永久残留在历史里):不还原时历史消息同样稳定,缓存命中率反而略好,但代价是每轮记忆文本(~500 token)持续堆积在 context 里,20 轮后额外占用 10,000+ token。

**结论 :「用后擦除」的核心价值是 ** 防止上下文无限膨胀 ,同时把缓存 miss 的代价锁定在固定 1 条,不让脏历史随轮次扩散。缓存方面不是零损耗,但随对话增长趋近无影响。

每条记忆保留 `source_ref=chat_log:<session_id>`(来源引用)。CLI(命令行)可用 `wyckoff memory trace <id>` 回看来源,避免长期摘要变成不可验证黑盒。

注意:agent_memorycontext_archive 是两套不同机制。agent_memory 只保存可跨会话复用的长期偏好、交易纪律和决策剧本;context_archive 保存被上下文压缩移出主窗口的原始消息,用于同一类问题的证据恢复。前者用于“以后怎么做得更符合用户”,后者用于“刚才/之前具体发生了什么还能找回来”。

5.5 与结构化数据的分工

记忆系统只负责“用户长期怎么交易”,不负责“今天发生了什么”。交易系统里的事实数据应继续留在结构化表和工具返回中:

内容 正确来源
当前持仓、成本、盈亏 portfolio / Supabase 持仓表
今日推荐、AI 是否推荐、二次确认原因 recommendation / signal 表
今日市场状态、资金趋势、板块水温 每日漏斗报告 / 市场数据工具
某只股票今日 K 线和分钟线 TickFlow / Tushare / 行情工具
Agent 当时怎么想、调用了什么工具 scratchpad / chat log / agent log
被上下文压缩移出的原始消息 context archive / archive://...
用户稳定偏好、交易禁忌、可复用执行剧本 agent_memory

这个边界很重要:如果把短期行情塞进长期记忆,后续模型会把过期事实误当成当前事实;如果把交易纪律只放在一次对话里,后续又会忘记用户的风控习惯。


5.6 召回排序与时间衰减策略细节

1. 四路混合召回加权评分

每次用户消息进入会话,系统在后台并发执行以下四种召回方式:

  • FTS5 全文检索 (SQLite): 利用 SQLite 倒排索引进行 BM25 评分(基于词频 TF 和逆文档频率 IDF 算法)。权重系数设为 0.8。适用于匹配用户表达的复杂自然语言场景(如“上一次谈到仓位管理时是怎么说的”)。
  • 股票实体精确匹配: 通过正则从用户输入中提取 6 位股票代码(如 603373),或匹配 stock_list_cache.json 缓存在输入中出现的最长股票名称,将其映射为 codes。然后在记忆库中执行 codes LIKE '%ticker%'。权重系数设为 1.2
  • 2-gram 中文滑窗分词: 去除停用词(如“买入”、“现在”等通用交易词)后,将中文片段按 2 字滑窗切分。执行 content LIKE %keyword% 检索。权重系数设为 0.25,用于补充全文检索可能遗漏的边角关联词。
  • 历史归档元数据匹配: 扫描 context_archive 目录下的 .json 元数据索引文件,利用 _meta_score 计算查询项在摘要、标的代码、修改文件以及提取关键字中的命中频次。该路匹配获取的归档不参与传统记忆混排,直接作为 # 压缩归档 部分追加到提示词尾部。

2. 得分合并算法 (Merge Policy)

同一条 L1/L2 记忆如果被多个管道同时命中,系统采用取最大值而不累加的策略:

$$\text{mergedScore} = \max(\text{score}_{\text{fts}}, \text{score}_{\text{code}}, \text{score}_{\text{keyword}})$$

这有效防止了一条记忆仅因为包含了大量普通泛词(被 keyword LIKE 命中多次)而压过了高相关度精确匹配的记忆。

3. 半衰期时间衰减 (Half-Life Decay)

为确保阶段性观点与决策的及时更新,防止过期剧本干扰最新策略,系统对记忆进行时间加权衰减:

$$\text{finalScore} = \text{baseScore} \times 2^{-\frac{\text{ageDays}}{\text{halfLifeDays}}}$$

  • decision (阶段决策):半衰期 14 天,45 天后物理清理。
  • playbook (交易剧本):半衰期 21 天,60 天后物理清理。
  • preference / persona (画像与稳定偏好):不参与衰减,永久保留。

返回 系列索引

Clone this wiki locally