七层记忆的 全息大脑内核
Memory-OS 跨 7 层架构完成记忆的捕获、存储、评分、检索与权威裁决——让 agent 不再重新发现它早已知道的事实。四个独立记忆源 Fabric、Qdrant、Sessions、Fact Store 协同工作,全部围绕一个核心问题:注入的记忆,凭什么被信任。
# Memory-OS · icarus/hooks.py · icarus/state.py · layers/
基准真相 SOUL.md — 权威分级问题
源码:~/.hermes/SOUL.md 与 rulebook.md。Memory-OS 把记忆注入进每一次提示,但 agent 的身份文档从未告诉它「要信任这些记忆」——没有明确的等级,注入记忆就被隐式当成可选项。
SOUL.md 与 rulebook.md)从未赋予它权威等级。缺少 rank,注入记忆就排在终端输出和官方文档之下——agent 不断重新发现提示里已有的事实。修复前:只有三级的基准真相层级
在 Layer 7 之前,Ground Truth 层级只有三级:
- 1. 终端输出→ 基准真相(运行时系统状态)
- 2. 官方文档→ 权威(API、配置、版本细节)
- 3. 训练知识→ 仅供参考(务必核实)
四个注入源 [qdrant]、[fabric]、[sessions]、[facts] 根本没有被列出。无状态 = 无权威。
团队观察到的症状:
- agent 运行
search_files、read_file、session_search去重新查找[qdrant]早已提供的信息 - 即便答案就在提示里,每个问题都被当成全新问题
- 项目、决策、约束在每个会话里从头重新发现
- 跨会话上下文虽然出现在系统前导里却被默默忽略
修复后:4 级基准真相层级
修复把 SOUL.md 扩展到四级,将注入记忆插入为第 2 级:
- 1. 终端输出→ 系统状态的基准真相(运行时)
- 2. 注入记忆 [qdrant] [fabric] [sessions] [facts]→ 已记录知识与既往决策的基准真相
- 3. 官方文档→ API/配置/版本细节的权威
- 4. 训练知识→ 仅供参考;务必对照 1–3 核实
| 冲突的来源 | 裁决规则 | 胜出方 |
|---|---|---|
| 终端输出 vs 注入记忆 | 系统状态以终端为准;已记录知识以注入记忆为准。 | 终端 / 注入 |
| 注入记忆 vs agent 假设 | 注入记忆永远胜出。答案在提示里时,绝不把问题当作全新问题。 | 注入记忆 |
| 注入记忆 vs 官方文档 | 版本敏感的细节以官方文档为准;项目上下文以注入记忆为准。 | 文档 / 注入 |
| 训练知识 vs 任何来源 | 训练知识永远败北,对照第 1–3 级核实。 | 第 1–3 级 |
变更的文件
| 文件 | 变更 |
|---|---|
~/.hermes/SOUL.md | Ground Truth 段从 3 级扩展到 4 级;新增冲突规则 |
~/.hermes/rulebook.md | 在 Source of Truth 表中新增「Injected memory」行;新增强制核实行为 |
systemctl --user restart hermes-gateway为什么这层重要
没有 Layer 7,基础设施层(01–06)照样捕获并注入记忆,但注入的记忆被无视:
注入却被验证
Qdrant 点已注入,agent 却 curl Qdrant API 去核实它们。
注入却被重查
Fabric 条目已注入,agent 却调用 fabric_recall 去重新查找。
注入却被重搜
会话历史已注入,agent 却运行 session_search 去重新发现。
注入却被探测
Facts 已注入,agent 却 probe fact_store 去确认它们。
每一次重新发现都在烧 token、时间和模型上下文。Layer 07 正是阻止这种浪费的那一层。
结构化事实 — 贝叶斯信任评分(全息记忆)
源码:$HERMES_HOME/memory_store.db(SQLite + HRR + FTS5)。Layer 3 存储持久化的结构化事实,带实体解析与贝叶斯信任评分。
Facts 表结构
| 字段 | 类型 | 说明 |
|---|---|---|
fact_id | INTEGER PK | 唯一事实标识 |
content | TEXT | 事实陈述本身 |
category | TEXT | user_pref | project | tool | general |
entities | JSON ARRAY | 解析出的实体引用(HRR 驱动) |
tags | TEXT | 逗号分隔的标签列表 |
trust_score | REAL | 贝叶斯计算的可信度(0–1) |
retrieval_count | INTEGER | 事实被检索的次数 |
helpful_count | INTEGER | 经 fact_feedback 标记为有用的次数 |
created_at | TIMESTAMP | 创建时间 |
last_accessed_at | TIMESTAMP | 最后检索时间 |
6 个 Fact 动作
| 动作 | 作用 | 示例 |
|---|---|---|
add | 存入新事实 | fact_store(action='add', content='...', entities=['Qdrant']) |
search | 经 FTS5 关键词查询 | fact_store(action='search', query='Qdrant named vectors') |
probe | 某个实体的全部事实 | fact_store(action='probe', entity='Docker') |
reason | 组合式:关联多个实体的事实 | fact_store(action='reason', entities=['Qdrant', 'Docker']) |
contradict | 查找 store 中冲突的论断 | fact_store(action='contradict') |
update / remove | CRUD 维护 | fact_store(action='update', fact_id=87, trust_delta=0.1) |
贝叶斯信任分计算
信任分不是简单比例——它用贝叶斯先验避免在小样本上过拟合。所有事实从 0.50(中性)起步:
trust_score = (helpful_count + α * prior) / (retrieval_count + α)
Where:
α = 2 (strength of the prior, i.e. "virtual observations")
prior = 0.50 (initial belief before any evidence)
例如:
- 新事实(0 检索,0 有用):trust = (0 + 2×0.5) / (0 + 2) = 0.50
- 1 检索,1 有用:trust = (1 + 2×0.5) / (1 + 2) = 0.67
- 10 检索,3 有用:trust = (3 + 2×0.5) / (10 + 2) = 0.33(有用但稀疏)
- 100 检索,80 有用:trust = (80 + 2×0.5) / (100 + 2) = 0.79(高置信)
probe、search 或 reason 检索到事实并在回复中引用它时,必须在同一轮调用 fact_feedback(helpful=True/False)。没有反馈,trust_score 只是摆设——每个事实都衰减回 0.50,事实质量悄然劣化。HRR — 全息约简表示
Layer 3 用 HRR 做实体解析——把不同表面形式的相关事实绑定在一起。例如「Docker」「docker daemon」「containerd」可能通过向量叠加解析到同一个内部实体 ID。这让 probe() 和 reason() 无需精确字符串匹配即可工作。
Icarus 增强:_search_facts()
Icarus 分支新增 _search_facts()——在 pre_llm_call 期间对 memory_store.db 做 FTS5 搜索,仅在首轮注入相关事实(避免每轮成本)。结果在系统提示中标记为 [facts]。
def _search_facts(query, top_k=3):
# Uses FTS5 over memory_store.db
# Tokenizes query, builds OR-query from tokens ≥4 chars
# Returns top-3 facts by rank, truncated to 200 chars
# Fail-open: returns [] on any error
已知坑
retrieval_count
probe()/reason() 之前不递增计数,只有 search() 会——当前代码已修复。
分数不动
信任分需要 fact_feedback 调用才会移动。没有它,每个事实都停在 0.50。
HRR 内部化
HRR 在内部驱动实体解析——agent 不会看到或操作全息向量。
Qdrant 向量库 — 四级回退链
源码:context_enhancer.py 的 search_with_fallback()。Layer 5 对一切受益于向量嵌入的知识做语义搜索——wiki 页面、会话转录、原始文档、技术参考。
技术栈
Qdrant 1.17+
Docker 部署 · Collection: knowledge_base · Endpoint: http://localhost:6333
4096d Cosine + BM25
稠密 4096 维余弦 + BM25 稀疏向量,混合检索。
Qwen3-Embedding-8B
经 OpenRouter 调用,$0.025/1M tokens。
为什么选 Qwen3-Embedding-8B
- 多语言 — 在 50+ 语言(葡萄牙语、西班牙语等)上表现强劲
- 质量 — 4096 维,语义保真度高,接近 MTEB 榜首
- 速度 — 8B 参数推理快,适合每小时增量摄取而不拖累 ARQ worker
- 成本 — 经 OpenRouter $0.025/1M tokens,整库 wiki 重建只需几美分
Qdrant Collection 结构(必须用命名向量)
{
"vectors": {
"dense": { "size": 4096, "distance": "Cosine" }
},
"sparse_vectors": {
"sparse": { "index": { "on_disk": false } }
}
}
{"dense": [...], "sparse": SparseVector(...)}。未命名向量会被静默拒绝。另外 AsyncQdrantClient.search() 在 qdrant-client 1.18 中不存在——改用 REST API POST /collections/{name}/points/search。4 级回退级联
search_with_fallback() 实现一个 4 级 fail-open 级联:
| 级别 | 方法 | 触发条件 | 返回 |
|---|---|---|---|
1 | Hybrid(dense + sparse + RRF) | 正常运行 — 两种向量都可用 | 对 dense 余弦 + BM25 稀疏做 Reciprocal Rank Fusion 的 Top-k |
2 | Dense-only | sparse_vector 为 None(FastEmbed BM25 子进程失败) | 纯 4096d 余弦相似度搜索 |
3 | Lexical(vault markdown) | Qdrant 离线(ConnectionError / Timeout / HTTPError) | glob $VAULT_PATH/wiki/ 下 *.md,按 term 密度 / √word_count 排序 |
4 | SQLite keyword(lineage) | vault 不可访问(无读权限、无文件) | 对 state.db 的 lineage.query 列做 LIKE 查询 |
级联在代码里如何工作
search_with_fallback(dense_vector, sparse_vector, query_text, top_k, score_threshold)
└── t0 = time.perf_counter()
│
├─── [Level 1] if sparse_vector is not None:
│ POST /collections/knowledge_base/points/query
│ prefetch: [dense (top_k*3), sparse (top_k*3)]
│ query: {"fusion": "rrf"}
│ → RRF fusion: score = Σ 1/(k + rank_i) across dense + sparse
│
├─── [Level 2] elif dense_vector is not None:
│ POST /collections/knowledge_base/points/query
│ using: "dense"
│ → Pure cosine similarity, no fusion
│
└─── [Levels 3+4] Exception caught:
lexical_search_in_vault() → sqlite_keyword_search() → []
Returns: (results, fallback_level, qdrant_latency_ms, fallback_latency_ms)
嵌入管线
衰减与去重
| 机制 | 调度 | 作用 |
|---|---|---|
| Decay scanner | 每周 cron | 归档低重要性 + 高龄的 AI 生成点。公式:decay_score = exp(-ln(2) * age_days / half_life)。豁免人类内容与高重要性(>0.7)点。 |
| Semantic dedup | 每月 | 合并近重复点(cosine > 0.92)。标签取并集,按 source_type 定优先级。 |
importance_score、last_accessed_at、confidence_score。缺失 → 所有点 decay_score=1.0 → 什么都不归档。Icarus 注入:_search_qdrant()
def _search_qdrant(query, top_k=2, threshold=0.55):
# 1. embed_query() → dense 4096d vector via OpenRouter
# 2. embed_query_sparse() → BM25 via FastEmbed subprocess
# 3. search_with_fallback() → 4-level cascade
# 4. Results labeled [qdrant] in system prompt
# 5. Per-session dedup by point ID (_injected_qdrant set)
#
# Key pitfall: context_enhancer.py reads OPENROUTER_API_KEY (singular).
# Before importing, hooks.py injects: os.environ["OPENROUTER_API_KEY"] = resolved_key
Social Closer 门控
琐碎消息完全跳过 Qdrant 搜索——没必要为闲聊嵌入向量:
_SOCIAL_CLOSERS = frozenset({
"ok", "obrigado", "valeu", "beleza", "blz", "tks", "thanks",
"👍", "👌", "✅", "feito", "certo", "confirmo", "entendido",
"sim", "não", "claro", "perfeito", "ótimo"
})
def _is_social_close(text):
stripped = text.strip().lower()
if stripped in _SOCIAL_CLOSERS: return True
# Very short ASCII-only without technical markers
if len(stripped) < 6 and stripped.isascii() and not any(
c in stripped for c in "://.@#$_?"): return True
return False
hooks.py — 生命周期钩子与记忆管线
源码:icarus/hooks.py。该模块实现四个生命周期钩子,包裹每一个会话轮次,共同构成完整的捕获 → 注入 → 提取管线。
钩子执行顺序
上下文引导
加载 SOUL、待处理交接、近期条目、创意状态。返回 {"context": "..."} 注入会话前导。
多源召回 + 注入
搜索全部 4 源(Fabric / Qdrant / Sessions / Facts),按会话去重,返回 {"context": "..."} 在 LLM 调用前注入。
决策捕获 + 创意追踪
捕获决策,追踪创意状态(主题、学习、问题)。为重要结果写 fabric 条目。
会话收尾
给会话打分,尝试用 LLM 从转录提取条目,失败回退到旧的截断逻辑,写记忆文件。
on_session_start — 上下文引导
新会话开始时调用。清除每会话去重状态,递增创意周期,然后从四源组装上下文:
def on_session_start(session_id="", platform="", **kwargs):
global _last_query_tokens, _injected_fabric, _injected_qdrant, _injected_sessions
_last_query_tokens = set()
_injected_fabric.clear()
_injected_qdrant.clear()
_injected_sessions.clear()
state.session_id = session_id
state.exchanges = []
# 1. Load SOUL (the agent's identity/values document)
soul = state.load_soul()
if soul: parts.append(soul.strip())
# 2. Pending handoffs from other agents (tasks, reviews, tickets)
open_tasks, reviews, open_tickets = state.read_pending()
if open_tasks: # → [fabric] items assigned to you
if reviews: # → [fabric] reviews of your work
if open_tickets: # → [fabric] tickets assigned to you
# 3. Cross-agent feedback (non-pending items from other agents)
feedback = state.read_cross_agent(3)
# 4. Recent entries from this agent
entries = state.read_recent(limit=5) # → [fabric] recent activity
# 5. Creative state (open questions + recent learnings)
if creative["questions"]: # → [fabric] open questions
if creative["learnings"]: # → [fabric] learnings
return {"context": "\n".join(parts)} if parts else None
pre_llm_call — 多源召回 + 注入
核心记忆注入钩子。搜索全部四源并按会话去重,避免一轮又一轮重复相同上下文:
def pre_llm_call(session_id="", user_message="", is_first_turn=False, **kwargs):
# ── Overlap gate ──
# Only suppress injection on NEAR-LITERAL repetition (>0.85 token overlap).
# The old 0.6 gate killed injection in long single-topic sessions.
tokens = _tokenize(user_message) # alphanumeric tokens ≥4 chars, stopwords removed
if _last_query_tokens:
overlap = len(tokens & _last_query_tokens) / max(len(tokens), 1)
if overlap > 0.85: return None # suppress — near-identical turn
_last_query_tokens = tokens
is_social = _is_social_close(user_message)
# ── 4 independent searches ──
results = state.recall(user_message, max_results=5, agent=agent) # Fabric
qdrant_results = _search_qdrant(user_message, top_k=2, threshold=0.55) # Qdrant
session_results = _search_sessions(user_message, session_id, top_k=2) # Sessions FTS5
fact_results = _search_facts(user_message, top_k=3) if is_first_turn else [] # Facts
# ── Per-source dedup (within session) ──
# Fabric: dedup by entry id (str) — _injected_fabric set
# Qdrant: dedup by point id — _injected_qdrant set
# Sessions: dedup by session_id — _injected_sessions set
# Facts: no dedup needed (first-turn only)
# → Returns {"context": "[fabric]...\n\n[qdrant]...\n\n[sessions]...\n\n[facts]..."}
post_llm_call — 决策捕获 + 创意追踪
def post_llm_call(session_id="", user_message="", assistant_response="", platform="", **kwargs):
state.exchanges.append({"user": user_message[:200], "assistant": assistant_response[:500]})
# ── Decision capture ──
# Requires: decision keyword + outcome keyword + substantial response (>200 chars)
# + substantial user request (>50 chars)
if (DECISION_RE.search(assistant_response)
and OUTCOME_RE.search(assistant_response)
and len(assistant_response) > 200
and len(user_text) > 50):
body = f"Task: {user_text[:300]}\n\nResult: {assistant_response[:500]}"
state.write_entry("decision", body, summary[:80],
platform=plat, status=entry_status, training_value="high")
# ── Creative tracking (broader _THEME_RE — no writes) ──
if _THEME_RE.search(assistant_response):
theme = _extract_theme(assistant_response) # top-3 non-stopword words, ≥4 chars
if theme not in creative["themes"]: creative["themes"].append(theme)
if _EVAL_RE.search(assistant_response):
learning = _extract_sentence(assistant_response, _EVAL_RE) # first matching sentence
if learning and learning not in creative["learnings"]: creative["learnings"].append(learning)
if _QUESTION_RE.search(assistant_response):
question = _extract_sentence(assistant_response, _QUESTION_RE)
if question and question not in creative["questions"]: creative["questions"].append(question)
state.save_creative(creative)
on_session_end — LLM 提取 + 旧逻辑回退
def on_session_end(session_id="", platform="", completed=False, **kwargs):
state.write_memory_file(creative) # persist creative state to disk
if not state.exchanges: return
scores = state.score_session()
if scores["total"] < 0.2: return # too trivial — skip
# ── LLM extraction (primary) ──
transcript = _build_transcript(state.exchanges) # compact [Turn N — User/Agent] format
entries = _llm_extract_entries(transcript) # LLM analyzes → structured entries
if entries:
for entry in entries:
state.write_entry(entry["type"], entry["content"], entry["summary"],
platform=plat, training_value=entry["training_value"],
status="completed")
logger.info("LLM extracted %d entries from session", len(entries))
else:
# ── Legacy fallback ──
_legacy_session_write(platform, scores) # truncated original behavior
LLM 驱动的会话提取
_llm_extract_entries() 把紧凑转录发给聊天 LLM(默认经 OpenRouter 的 DeepSeek),要求它提取值得跨会话保留的重要条目。返回结构化 dict:
{
"type": "decision" | "resolution" | "note",
"summary": "one line, max 80 chars, original language",
"content": "markdown with ## Context, ## Action/Decision, ## Outcome",
"training_value": "high" | "normal" | "low"
}
提取提示要求 LLM 跳过琐碎会话、问候和例行闲聊——只保留值得跨会话保留的内容。该函数用稳健 JSON 解析(_parse_json_robust)处理 markdown 围栏、首尾多余文本,并在解析失败时渐进剥离尾部字符。
FTS5 会话搜索:_search_sessions()
def _search_sessions(query, current_session_id="", top_k=2):
# FTS5 over state.db (Hermes session DB)
# Builds OR-query from meaningful tokens (≥4 chars, stopwords removed)
toks = [t for t in _tokenize(query) if len(t) >= 4]
fts_query = " OR ".join(toks[:8]) # max 8 terms to avoid query complexity
# FTS5 snippet() + GROUP BY are incompatible in same SELECT
# → fetch top-20 ranked rows, dedup by session_id in Python
SELECT m.session_id, s.title, s.started_at,
snippet(messages_fts, 0, '', '', '…', 12) AS snip
FROM messages_fts
JOIN messages m ON m.id = messages_fts.rowid
LEFT JOIN sessions s ON s.id = m.session_id
WHERE messages_fts MATCH ?
AND m.session_id != ?
AND m.role IN ('user','assistant')
ORDER BY rank
LIMIT 20
pre_llm_call 中,若 _is_social_close() 返回 True,则 Qdrant、Sessions、Facts 搜索全部跳过。这避免在「ok」「thanks」「👍」等消息上浪费嵌入 API 调用。提示注入消毒
Memory-OS 从外部源(Qdrant、vault 文件、SQLite)取文本并注入到 agent 上下文。任何文本进入提示前都先经过 _sanitize_context_text()——一个多模式消毒管线,外加启发式安全兜底。
捕获的已知攻击模式
| 模式 | 正则 | 替换为 |
|---|---|---|
| "ignore all previous/prior instructions" | (?i)\bignore\s+all\s+(previous|prior)\s+(instructions|...) | [REDACTED] |
| "you are/will now become/act as AI/assistant" | (?i)\byou\s+(are|will\s+now)\s+(now\s+)?(become|act|acting)\s+as... | [REDACTED] |
| "new instructions follow/above/below" | (?i)\bnew\s+(instructions|directives|commands)\s+(follow|...) | [REDACTED] |
模板注入 {{...}}、${...} | \{\{.*?\}\}|\$\{.*?\} | [REDACTED] |
| 三反引号代码围栏 | ``` | [code] |
| javascript/data: URL | (?i)(javascript|data)\s*: | sanitized: |
| <script>、事件处理器、<iframe> | <\s*script[\s>]|on\w+\s*=<iframe[\s>] | [sanitized] |
| [IMPORTANT:]、[SYSTEM:]、[OVERRIDE:] | (?i)\[IMPORTANT:.*?\]|\[SYSTEM:.*?\]|\[OVERRIDE:.*?\] | [REDACTED] |
| 控制字符(除 \n \t) | [\x00-\x08\x0b\x0c\x0e-\x1f\x7f] | "" |
| 零宽 / 不可见 Unicode | [-
- -] | "" |
启发式安全兜底
除模式匹配外,_validate_safe_content() 通过指令密度分析捕获未知攻击向量:
def _validate_safe_content(text: str) -> str:
if not text or len(text) < 20: return text
directivess = len(re.findall(
r"(?i)\b(ignore|forget|disregard|override|replace|pretend|"
r"act\s+as|you\s+(are|must|will|shall))\b", text))
if directivess >= 3 and directivess / max(len(text), 1) > 0.02:
return "[SANITIZED]"
return text
后处理
- 规整多余换行:4+ 连续
\n→ 3 - 规整多余空格:8+ 空格 → 1
- 结果截断到
max_len(默认 600 字符) - Fail-open:任何异常都返回原始文本
召回遥测 — 度量记忆有效性
源码:state.py 的召回/使用遥测系统。Memory-OS 追踪被召回的条目是否真的被 agent 使用,从而闭环检索质量的度量。
记录的事件
| 事件 | 记录者 | 数据 |
|---|---|---|
recall | state.log_recall() | query, result_count, result_ids, result_summaries, session_id, agent |
usage | state.log_usage() | entry_id, action, session_id, agent |
遥测文件
$HERMES_HOME/.icarus-telemetry.jsonl
{"ts": "2026-06-10T14:23:01Z", "event": "recall",
"source": "pre_llm_call", "query": "how do I configure Qdrant...",
"result_count": 3, "result_ids": ["a1b2", "c3d4", "e5f6"],
"session_id": "sess-abc123", "agent": "hermes"}
{"ts": "2026-06-10T14:23:05Z", "event": "usage",
"action": "referenced", "entry_id": "c3d4",
"session_id": "sess-abc123", "agent": "hermes"}
使用率指标
遥测系统计算使用率:被召回的条目中后续在写操作里被使用的比例。低使用率(如 <0.2)说明 agent 在忽视注入上下文——指向可能的 Ground Truth 层级问题(Layer 7 未生效)。
def _summarize_telemetry_events(events):
recalls = [e for e in events if e.get("event") == "recall"]
usages = [e for e in events if e.get("event") == "usage"]
recalled_ids = set(id for r in recalls for id in r.get("result_ids", []))
used_ids = set(u.get("entry_id", "") for u in usages) - {""}
used_from_recall = used_ids & recalled_ids
return {
"total_recalls": len(recalls),
"total_usages": sum(1 for u in usages if u.get("entry_id", "") in recalled_ids),
"unique_entries_recalled": len(recalled_ids),
"unique_entries_used": len(used_from_recall),
"usage_rate": round(len(used_from_recall) / max(len(recalled_ids), 1), 2),
}
集成:build_brief()
每日 brief 含召回统计,让 agent 自查记忆有效性:
brief = state.build_brief()
# {
# "pending": { "open_tasks": 2, "reviews_of_my_work": 1, "open_tickets": 0, "items": [...] },
# "recent_work": [{ "summary": "...", "timestamp": "2026-06-09T15:30" }],
# "from_others": ["agent-x: Fixed the Qdrant upsert bug"],
# "suggested_action": "Pick up: Review deployment script from hermes (id a1b2c3)",
# "recall_stats": { "total_recalls": 12, "usage_rate": 0.58, ... }
# }
state.get_telemetry(last_n=..., session_id_filter=..., agent_filter=...) 跨所有会话回溯分析召回质量。