# Hermes-Agent 上下文压缩与记忆机制参考
> 项目位置:`backend/hermes-agent/`(第三方仓库 `NousResearch/hermes-agent` 的本地克隆,已通过父项目 `.gitignore` 排除)
> 文档目的:记录 Hermes-Agent 内置的上下文压缩(context compression)与记忆(memory)两大子系统的实现机制、配置项、与 Bridge 模式的实际关系,便于后续评估适配成本、规划启用方案。
> 评估时点:2026-08-11(HEAD `f4604f89`)
---
## 0. 一句话定位
Hermes-Agent 的上下文管理由**两条独立子系统**组成:
- **上下文压缩**——会话内消息历史的 token 治理。当 prompt token 超过阈值时,启动一个 5 阶段流水线(裁剪旧工具结果 → 保护头尾 → LLM 总结中段),把摘要写回 messages 列表替换原中段。
- **记忆机制**——跨会话的持久化。包括四层:会话内 messages、跨会话 Markdown 文件(`MEMORY.md` / `USER.md`)、Skills 库、外部 Memory Provider 插件(Honcho / Mem0 / RetainDB 等)。
**与 Bridge 模式的关键事实**:本项目通过 `backend/hermes-bridge/hermes_bridge.py:228` 显式传入 `skip_memory=True`,**完全禁用了所有跨会话记忆机制**;上下文压缩则保持默认启用状态。
---
## 第一部分:上下文压缩机制
### 1.1 触发条件
**判断入口**:`ContextCompressor.should_compress_info()`
> `agent/context_compressor.py:2554-2585`
```python
def should_compress_info(self, prompt_tokens: int = None) -> "tuple[bool, str | None]":
tokens = prompt_tokens if prompt_tokens is not None else self.last_prompt_tokens
if tokens < self.threshold_tokens:
return False, None
if self._automatic_compression_blocked():
return False, self._compression_block_reason() or "blocked"
return True, None
```
**触发依据:基于 token 阈值**,不是消息条数,不是 LLM 自主决定。
- 比较对象:`prompt_tokens` 与 `threshold_tokens`
- `prompt_tokens` 两个来源:
- 粗估(请求前):`estimate_messages_tokens_rough(messages)`,字符数 / 4
- 精确(响应后):API 返回的 `usage.prompt_tokens` 写入 `last_prompt_tokens`(`conversation_loop.py:3272`)
### 1.2 阈值与默认值
| 参数 | 默认值 | 含义 | 源码位置 |
|---|---|---|---|
| `threshold_percent` | `0.50` | 触发比例(占上下文窗口) | `context_compressor.py:2208` |
| `_MIN_CTX_TRIGGER_RATIO` | `0.85` | 触发比例硬上限 | `context_compressor.py:2090` |
| `_SMALL_CTX_THRESHOLD_PERCENT` | `0.75` | 小上下文地板(窗口 < 512K 时强制 ≥75%) | `context_compressor.py:664` |
| `_SMALL_CTX_CONTEXT_FLOOR` | `512K` | "小上下文"的判定门槛 | `context_compressor.py` |
| `threshold_tokens_cap` | `None` | 绝对 token 上限(设置后取 min) | `agent_init.py:1985-1992` |
**`threshold_tokens` 计算公式**:`context_length × threshold_percent - max_tokens`(保证不超过 85% 窗口,并预留回答空间),见 `context_compressor.py:2166-2204`。
### 1.3 Anti-thrash 阻断(防止反复无效压缩)
> `agent/context_compressor.py:2647-2726`
```python
def _automatic_compression_blocked_locally(self) -> bool:
# 1. Summary LLM 失败冷却期内:不触发
if self._summary_failure_cooldown_until - time.monotonic() > 0:
return True
# 2. 连续 2 次压缩无效(节省 <10%)或连续 2 次使用 fallback:不触发
if (self._ineffective_compression_count >= 2
or self._fallback_compression_streak >= 2):
# 300 秒后允许一次"试探性"压缩,失败则再次阻断
return True
return False
```
阻断时通过 `agent._warn_context_overflow_blocked(...)` 向用户告警(`conversation_loop.py:2077-2087`)。
### 1.4 压缩流水线(5 阶段)
> `agent/context_compressor.py:5942-5977`(算法注释)
```
Algorithm:
1. Prune old tool results (cheap pre-pass, no LLM call)
2. Protect head messages (system prompt + first exchange)
3. Find tail boundary by token budget (~20K tokens of recent context)
4. Summarize middle turns with structured LLM prompt
5. On re-compression, iteratively update the previous summary
```
#### 阶段 1:旧工具结果裁剪(无 LLM 调用)
> `context_compressor.py:2732-3005`,四遍扫描:
- **Pass 1**:相同内容去重,旧副本替换为 `"[Duplicate tool output — same content as a more recent call]"`(`context_compressor.py:2819-2843`)
- **Pass 2**:超过 `min_prune_chars`(默认 200)的旧 tool result 替换为一行摘要,如 `[terminal] ran \`npm test\` -> exit 0, 47 lines output`(`_summarize_tool_result`,`context_compressor.py:1128-1187`)
- **Pass 3**:截断超过 500 字符的 `tool_call.arguments` JSON(`context_compressor.py:2906-2924`)
- **Pass 4**:尾部仍超 `protect_tail_tokens × 1.5` 时,对尾部内的 bulky tool 输出降级(`context_compressor.py:2941-3005`)
#### 阶段 2/3:边界确定
> `context_compressor.py:6054-6059`
```python
compress_start = self._protect_head_size(messages) # 头部边界
compress_start = self._align_boundary_forward(messages, compress_start)
compress_end = self._find_tail_cut_by_tokens(messages, compress_start) # 尾部边界
```
- **头部**:`_protect_head_size`(`context_compressor.py:4682-4705`)= system prompt(若有)+ `protect_first_n` 条非 system 消息
- **尾部**:`_find_tail_cut_by_tokens`(`context_compressor.py:5026-5100`)从末尾倒推累计 token,直至达到 `tail_token_budget`,消息数下限 `min(protect_last_n, _MAX_TAIL_MESSAGE_FLOOR=8)`
#### 阶段 4:中段 LLM 总结
> `context_compressor.py:3469-3498`、`3765-3879`
- 通过 `auxiliary_client.call_llm`(`context_compressor.py:3813`)调用独立"摘要模型"(可配置 `auxiliary.compression.provider/model`)
- **结构化模板**:`Goal / Active Task / In Progress / Decisions / Files / Completed Actions / Pending / Remaining Work` 等字段(`_summarizer_preamble`,`context_compressor.py:3637-3648`)
- **迭代更新**:已存在 `_previous_summary` 时改用增量更新提示词(`context_compressor.py:3479-3480`)
- **输入上限**:`_SUMMARY_INPUT_MAX_CHARS = 160_000`(约 40K token),超出则头尾保留、中段省略(`context_compressor.py:395`)
- **输出无 max_tokens 上限**:wire 上显式不加 `max_tokens`(`context_compressor.py:3776-3785`),`max_summary_tokens` 仅用于内部预算(上限 `_SUMMARY_TOKENS_CEILING = 10_000`)
- **剥离 think 块**:摘要 LLM 输出会经过 `strip_think_blocks`,避免思考模型(MiniMax / DeepSeek / QwQ)的 `...` 污染摘要(`context_compressor.py:3854-3863`)
### 1.5 被压缩的内容
| 消息范围 | 处理方式 |
|---|---|
| **system prompt** | 永不压缩(始终在头部保护) |
| **头部 `protect_first_n` 条**(默认 3) | 永不压缩;首次压缩后衰减为 0,避免早期消息"化石"(`context_compressor.py:4693-4700`) |
| **尾部 `protect_last_n` 条**(默认 20,实际由 token 预算主导,下限 8) | 永不压缩 |
| **中段所有消息**(user / assistant / tool) | 进入 LLM 总结输入,最终被摘要消息替换 |
**工具调用结果**是压缩的重点对象,三层处理:
1. 预压缩裁剪(阶段 1)
2. LLM 总结输入:`_serialize_for_summary` 把 tool 消息序列化为文本
3. 图片剥离:`_strip_historical_media`(`context_compressor.py:6686-6692`)把最新图片之前的所有图片 part 替换为文本占位符
### 1.6 压缩结果如何使用
**完全替换原消息**,不附加。`conversation_compression.py:2782` 在调用前用 `copy.deepcopy` 保存原消息:
```python
messages_before_compression = copy.deepcopy(messages)
...
compressed = compress_fn(messages, **compress_kwargs)
```
中段 N 条 → 1 条(或合并入尾部首条)摘要消息。
#### 摘要消息的格式
> `context_compressor.py:6564-6614`
**角色由交替规则动态决定**(user / assistant),保证 OpenAI 兼容后端不会因角色连续而 400。强制规则:若 session 中没有任何真实 user 消息,摘要必须用 `role="user"`。
消息额外带进程内元数据(不上 wire):
```python
{
"role": summary_role,
"content": summary,
COMPRESSED_SUMMARY_METADATA_KEY: True, # "_compressed_summary"
COMPRESSED_SUMMARY_HAS_USER_TURN_KEY: bool(...),
}
```
摘要文本末尾强制追加:
```
--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---
```
system prompt 也会被追加一段说明:`[Note: Some earlier conversation turns have been compacted into a handoff summary...]`(`context_compressor.py:6419`)。
### 1.7 原始消息的保留 / 回滚
| 阶段 | 保留情况 |
|---|---|
| 压缩过程中 | `messages_before_compression` 用于异常回滚(`conversation_compression.py:2858-2905`) |
| 提交后 `in_place=true`(默认) | 同一 `session_id`,直接覆盖,**原始中段消息从内存与 DB 中消失** |
| 提交后 `in_place=false` | 创建子 session,父 session 保留原始消息作为审计轨迹 |
### 1.8 失败降级(4 层)
| 层级 | 触发场景 | 行为 | 源码 |
|---|---|---|---|
| **1. 模型回退** | aux 模型未找到(404/503)/ 超时 / JSON 解析失败 / 流式中断 | 切到主模型重试 | `context_compressor.py:3965-3984` |
| **2. 冷却期** | 任意 LLM 失败 | 600 秒(无 provider 配置)或 60→300→900 秒阶梯(超时),期间 `should_compress` 返回 False | `context_compressor.py:627、1935-1951、4025-4028` |
| **3. 静态 fallback** | LLM 总结失败但未 abort | 从被丢弃的中段本地提取连续性锚点(用户问句、assistant 动作、工具名、文件路径、错误文本),组装成结构化文本(上限 8000 字符) | `context_compressor.py:6429-6454、3203-3332` |
| **4. 整体 abort** | 鉴权失败(401/403)/ 网络断连 / `abort_on_summary_failure=true` | 保留原消息不变,向用户告警 | `context_compressor.py:6354-6402、conversation_compression.py:2947-2956` |
### 1.9 与"思考过程"的关系
**压缩过程不产生 `thinking_callback` 输出**。
证据:在 `conversation_compression.py` 中 grep `thinking|on_reasoning|stream_callback|reasoning_callback` **无任何匹配**。压缩的状态通知走独立通道 `agent._emit_status`(用于 UI "压缩中"提示),与 `thinking_callback` 是两个不同的回调。
前端能看到:
- 压缩开始 / 完成的状态通知(走 `status_callback` → SSE)
- 压缩后插入对话的摘要消息本身(带 `_compressed_summary` 元数据,前端可据此特殊渲染)
前端看不到:
- 压缩 LLM 的思考过程(`` 块已被剥离)
---
## 第二部分:记忆机制
### 2.1 四层记忆结构
| 层级 | 类型 | 存储 | 生命周期 |
|---|---|---|---|
| 1 | 短期(会话内) | `agent.messages` 内存 + SQLite `state.db` | 会话内累加;超限时由压缩摘要后切到新 session_id |
| 2 | 中期(跨会话内置) | `$HERMES_HOME/memories/MEMORY.md` 与 `USER.md`(Markdown) | 跨会话持久,写入即时落盘 |
| 3 | 长期(外部 provider 插件) | Honcho / Mem0 / RetainDB / Supermemory 等后端 | 由插件决定,按 user_id / chat_id 隔离 |
| 4 | 技能库(Skills) | `$HERMES_HOME/skills//`(每个技能一个目录) | 跨会话持久,由后台 review 沉淀 |
### 2.2 短期记忆(会话 DB)
- **存储位置**:内存中的 `agent.messages` 列表 + 磁盘 SQLite SessionDB
- **默认路径**:`hermes_state.py:239`
```python
DEFAULT_DB_PATH = get_hermes_home() / "state.db"
```
- **生命周期**:会话内累加;上下文超限时由 `conversation_compression.py` 摘要后切到新 `session_id`(rotate)
### 2.3 中期记忆(MEMORY.md / USER.md)
> `tools/memory_tool.py:1-24`(模块 docstring)
```
Provides bounded, file-backed memory that persists across sessions. Two stores:
- MEMORY.md: agent's personal notes and observations (environment facts, project
conventions, tool quirks, things learned)
- USER.md: what the agent knows about the user (preferences, communication style,
expectations, workflow habits)
Both are injected into the system prompt as a frozen snapshot at session start.
Mid-session writes update files on disk immediately (durable) but do NOT change
the system prompt -- this preserves the prefix cache for the entire session.
The snapshot refreshes on the next session start.
```
**关键设计**:
- 会话开始时把 MEMORY/USER 快照注入 system prompt
- 会话内的写入立即落盘,但**不更新 system prompt**——保 prefix cache
- 下一会话开始时刷新快照
**字符上限**(默认值):
> `tools/memory_tool.py:165-169`
```python
def __init__(self, memory_char_limit: int = 2200, user_char_limit: int = 1375):
```
- MEMORY: 2200 字符(≈800 token)
- USER: 1375 字符(≈500 token)
**存储路径**:
> `tools/memory_tool.py:53-67`
```python
def get_memory_dir() -> Path:
"""Return the profile-scoped memories directory."""
return get_hermes_home() / "memories"
MEMORY_BLOCK_HEADERS = {
"memory": "MEMORY (your personal notes)",
"user": "USER PROFILE (who the user is)",
}
ENTRY_DELIMITER = "\n§\n"
```
**写入用原子 temp-file + rename**(`tools/memory_tool.py:863-876`),并用 `fcntl` / `msvcrt` 做跨进程文件锁(`_file_lock`,`tools/memory_tool.py:278-320`)。
### 2.4 长期记忆(外部 Memory Provider 插件)
> `agent/memory_provider.py:1-31`
```python
"""Abstract base class for pluggable memory providers.
Memory providers give the agent persistent recall across sessions.
The MemoryManager enforces a one-external-provider limit to prevent
tool schema bloat and conflicting memory backends.
"""
```
**只允许一个外部 provider 同时运行**。已内置的插件位于 `plugins/memory/`:
| 插件 | 类型 |
|---|---|
| `honcho/` | 云端用户建模 |
| `hindsight/` | — |
| `mem0/` | — |
| `retaindb/` | SQLite write-behind 队列 + 语义检索 |
| `supermemory/` | — |
| `holographic/`、`byterover/`、`openviking/` | — |
**编排**:`agent/memory_manager.py` 的 `MemoryManager` 负责 prefetch / sync / shutdown。
### 2.5 技能库(Skills)
- 存放在 `$HERMES_HOME/skills/` 下
- 每个技能是一个目录,含 `SKILL.md` + 可选 `references/`、`templates/`、`scripts/`
- 由后台 review(`background_review.py`)把"用户纠正 / 教训"沉淀到 skill 中
- 触发条件基于工具迭代计数 `_iters_since_skill`
### 2.6 后台 Review(background_review.py)
> `agent/background_review.py:1-17`
```python
"""Background memory/skill review — fork the agent to evaluate the turn.
After every turn, ``AIAgent.run_conversation`` may call
:func:`spawn_background_review` to fire off a daemon thread that replays
the conversation snapshot in a forked :class:`AIAgent` and asks itself
"should any skill/memory be saved or updated?". Writes go straight to
the memory + skill stores. Main conversation and prompt cache are never
touched.
```
#### 触发方式:**事件触发**(不是定时任务)
> `agent/turn_context.py:582-590`
```python
# Track memory nudge trigger (turn-based, checked here).
should_review_memory = False
if (agent._memory_nudge_interval > 0
and "memory" in agent.valid_tool_names
and agent._memory_store):
agent._turns_since_memory += 1
if agent._turns_since_memory >= agent._memory_nudge_interval:
should_review_memory = True
agent._turns_since_memory = 0
```
> `agent/turn_finalizer.py:698-724`
```python
# Check skill trigger NOW — based on how many tool iterations THIS turn used.
_should_review_skills = False
if (agent._skill_nudge_interval > 0
and agent._iters_since_skill >= agent._skill_nudge_interval
and "skill_manage" in agent.valid_tool_names):
_should_review_skills = True
agent._iters_since_skill = 0
# External memory provider: sync the completed turn + queue next prefetch.
agent._sync_external_memory_for_turn(...)
# Background memory/skill review — runs AFTER the response is delivered
if final_response and not interrupted and (_should_review_memory or _should_review_skills):
try:
agent._spawn_background_review(
messages_snapshot=list(messages),
review_memory=_should_review_memory,
review_skills=_should_review_skills,
)
except Exception:
pass # Background review is best-effort
```
**默认间隔**:
- Memory:每 10 轮用户消息触发(`nudge_interval: 10`)
- Skill:每 15 次工具迭代触发(`skills.creation_nudge_interval: 15`)
#### Fork 的安全隔离
Review fork 共享父 agent 的 `_memory_store`,但**显式 `skip_memory=True`** 避免触碰外部 provider,并 `_persist_disabled=True` 防止把审查 prompt 写入用户的 `state.db`:
> `agent/background_review.py:716-830`
```python
review_agent._memory_store = agent._memory_store
review_agent._memory_enabled = agent._memory_enabled
review_agent._user_profile_enabled = agent._user_profile_enabled
review_agent._memory_nudge_interval = 0
review_agent._skill_nudge_interval = 0
review_agent = AIAgent(..., skip_memory=True, ...)
review_agent._persist_disabled = True
review_agent._session_db = None
review_agent._session_json_enabled = False
```
工具白名单仅 `memory` / `skill_manage`(`background_review.py:893-909`),其他工具被拒绝。
### 2.7 context_references.py(与记忆无关)
`@file:` / `@folder:` / `@diff` / `@staged` / `@git` / `@url:` 的**用户消息内联展开机制**——不是 RAG 检索,是用户显式 @ 引用的精确解析。
> `agent/context_references.py:18-21`
```python
REFERENCE_PATTERN = re.compile(
rf"(?diff|staged)\b|(?Pfile|folder|git|url):(?P...))"
)
```
- 直接读工作区文件 / 跑 git 命令 / 抓 URL
- 注入位置:附加到当前用户消息尾部(`--- Attached Context ---`)
- Token 保护:`hard_limit = context_length × 0.50`,`soft_limit = context_length × 0.25`
- 路径白名单 / 敏感文件黑名单(`_SENSITIVE_HOME_FILES`、`_SENSITIVE_HOME_DIRS`)
### 2.8 跨会话状态汇总
| 类型 | 保留内容 | 存储位置 |
|---|---|---|
| **MEMORY.md** | agent 个人笔记:环境事实、约定、工具怪癖、学到的教训 | `$HERMES_HOME/memories/MEMORY.md` |
| **USER.md** | 用户画像:偏好、沟通风格、期望、工作习惯 | `$HERMES_HOME/memories/USER.md` |
| **Skills** | "如何做这类任务"的类级技能库 | `$HERMES_HOME/skills//` |
| **state.db** | 完整会话历史(SQLite,可通过 `session_search` 工具检索) | `$HERMES_HOME/state.db` |
| **外部 provider** | Honcho / Mem0 / RetainDB 等后端自己的存储 | 各自后端 |
**写入方式**:
- MEMORY / USER:LLM 通过 `memory` 工具自主决策调用
- 后台 review:每 N 轮自动触发
- `session_search`:LLM 显式调用工具检索历史
- 外部 provider 的 `sync_turn`:每轮后台自动执行
### 2.9 隐私 / 安全机制
| 机制 | 实现 | 源码 |
|---|---|---|
| **写入前注入 / 渗出扫描** | `first_threat_message(content, scope="strict")` | `tools/memory_tool.py:83-88` |
| **加载时再扫** | 命中则替换为占位符(保留原文本到 live state 让用户能看到并删除) | `tools/memory_tool.py:242-276` |
| **写入审批门**(write_approval) | `stage`(暂存待批)/ `block` / `allow` 三档 | `tools/memory_tool.py:911-965` |
| **Drift 检测** | 检测外部 patch / shell append 污染,命中则备份 | `tools/memory_tool.py:807-861` |
| **CLI `/reset`** | `hermes memory` 子命令重置 | `hermes_cli/subcommands/memory.py`、`hermes_cli/memory_reset.py` |
---
## 第三部分:配置项汇总
### 3.1 上下文压缩(`compression.*`)
> `cli-config.yaml.example:425-556`、`agent/agent_init.py:1860-2009、2455-2499`
| YAML 字段 | 默认值 | 作用 |
|---|---|---|
| `enabled` | `true` | 总开关,`false` 完全关闭自动压缩 |
| `threshold` | `0.50` | 触发阈值占上下文比例 |
| `model_thresholds` | `{}` | 按模型子串匹配覆盖阈值,如 `"glm-5.2": 0.40` |
| `threshold_tokens` | `null` | 绝对 token 上限,与比例阈值取 min |
| `target_ratio` | `0.20` | 尾部保留比例 = threshold × ratio × context_length |
| `protect_first_n` | `3` | 头部额外保护消息数(首次压缩后衰减为 0) |
| `protect_last_n` | `20` | 尾部最小保护消息数(实际受 token 预算与 floor=8 约束) |
| `min_tail_user_messages` | `1` | 尾部至少保留的真实 user 消息数 |
| `max_attempts` | `3`(硬上限 10) | 单轮内压缩重试次数 |
| `in_place` | `true` | true=同 session 覆盖,false=分裂子 session |
| `abort_on_summary_failure` | `false` | true=LLM 总结失败时整体放弃压缩 |
| `proactive_prune_tokens` | `0`(禁用) | 独立的旧工具结果裁剪触发阈值 |
| `proactive_prune_min_result_chars` | `8000` | 工具结果裁剪的最小字符门槛 |
| `proactive_prune_min_reclaim_tokens` | `4096` | 裁剪必须至少回收这么多 token 才提交 |
| `progress_notices` | `false` | 是否在聊天平台显示压缩进度 |
| `idle_compact_after_seconds` | `0`(禁用) | 闲置 N 秒后下次回复前主动压缩 |
| `micro_compact` | `false`(opt-in) | 每轮滚动微压缩(实验性,默认关) |
**摘要模型独立配置** 在 `auxiliary.compression.provider / model`(默认 `"auto"`:OpenRouter → Nous Portal → 主模型)。
**是否可完全关闭**:可以。`compression.enabled: false`。运行时所有触发点都先检查 `agent.compression_enabled`(`conversation_loop.py:1966、2063、6360、6396`)。
### 3.2 记忆(`memory.*`)
> `cli-config.yaml.example:665-683`
| YAML 字段 | 默认值 | 作用 |
|---|---|---|
| `memory_enabled` | `true` | 关闭则不加载 MEMORY.md |
| `user_profile_enabled` | `true` | 关闭则不加载 USER.md |
| `memory_char_limit` | `2200` | MEMORY 字符上限(≈800 token) |
| `user_char_limit` | `1375` | USER 字符上限(≈500 token) |
| `nudge_interval` | `10` | 0=禁用后台 memory review 触发 |
| `flush_min_turns` | `6` | 0=禁用退出前 flush |
| `provider` | `""`(空字符串=禁用外部 provider) | 选哪个插件:`honcho` / `mem0` / `retaindb` / `supermemory` 等 |
**Skills**(同位置 yaml 第 785 行):
| YAML 字段 | 默认值 | 作用 |
|---|---|---|
| `skills.creation_nudge_interval` | `15` | 0=禁用后台 skill review |
### 3.3 启用条件(双层 gate)
> `agent/agent_init.py:1655-1669`
```python
_memory_toolset_requested = "memory" in (agent.enabled_toolsets or [])
if not skip_memory or _memory_toolset_requested:
mem_config = _agent_cfg.get("memory", {})
agent._memory_enabled = mem_config.get("memory_enabled", False)
agent._user_profile_enabled = mem_config.get("user_profile_enabled", False)
agent._memory_nudge_interval = int(mem_config.get("nudge_interval", 10))
if agent._memory_enabled or agent._user_profile_enabled:
from tools.memory_tool import MemoryStore
agent._memory_store = MemoryStore(
memory_char_limit=mem_config.get("memory_char_limit", 2200),
user_char_limit=mem_config.get("user_char_limit", 1375),
)
agent._memory_store.load_from_disk()
```
---
## 第四部分:Bridge 模式下的实际状态
### 4.1 关键事实:Bridge 显式禁用所有 Memory 机制
> `backend/hermes-bridge/hermes_bridge.py:219-231`
```python
kwargs = dict(
base_url=effective_base_url or None,
api_key=effective_api_key,
model=effective_model or None,
max_iterations=max_iterations,
quiet_mode=True,
tool_progress_mode="off",
skip_context_files=True,
load_soul_identity=False,
skip_memory=True, # ← 关键
)
agent = AIAgent(**kwargs)
```
`skip_memory=True` 的影响,对照 `agent_init.py:1673-1739`:
| 机制 | Bridge 模式下是否生效 | 原因 |
|---|---|---|
| **外部 Memory Provider** | **完全禁用** | `_memory_manager = None`(整段被 `if not skip_memory` 包裹) |
| **后台 memory review 触发** | **不触发** | `agent._memory_store` 默认值 `None` + `valid_tool_names` 无 `memory` + 三个 AND 条件全失效 |
| **后台 skill review 触发** | **不触发** | 同上 |
| **sync_all / queue_prefetch_all** | **不执行** | `_memory_manager` 为 None,`_sync_external_memory_for_turn` 早 return |
| **MEMORY.md / USER.md 加载** | **看 `_memory_enabled` 配置** | 若 `enabled_toolsets` 含 `"memory"`(默认 toolset 解析后含 memory),仍会建 store |
| **MEMORY/USER 的 LLM 工具调用** | **取决于 `memory_enabled` 默认值** | 默认 `False`(`mem_config.get("memory_enabled", False)`) |
### 4.2 Bridge 上下文压缩的实际状态
Bridge 没有传任何 `compression.*` 参数,走 AIAgent 默认:
- `compression.enabled = true`(启用)
- `compression.threshold = 0.50`(50% 上下文窗口)
- `compression.target_ratio = 0.20`
- 其他配置看 `$HERMES_HOME/config.yaml` 是否存在
**实际行为**:会话内消息累积到约一半上下文窗口时,自动触发压缩流水线。
### 4.3 启用完整 Memory 的步骤
1. **去掉 `skip_memory=True`**(或改为 `False`)
2. **在 `$HERMES_HOME/config.yaml` 配置**:
```yaml
memory:
memory_enabled: true
user_profile_enabled: true
memory_char_limit: 2200
user_char_limit: 1375
nudge_interval: 10
```
3. **外部 provider**(可选):在 config.yaml 设 `memory.provider: honcho` + 配套 env(如 `HONCHO_API_KEY`)
4. **per-user 隔离**(可选):传 `user_id` / `chat_id` / `gateway_session_key`,参考 `agent_init.py:1707-1723`
### 4.4 启用的潜在影响
| 影响项 | 评估 |
|---|---|
| **system prompt 长度** | MEMORY 2200 字符 + USER 1375 字符 ≈ 1300 token 永久占用上下文 |
| **prefix cache** | 会话内不变(设计上保证 cache 命中) |
| **后台 LLM 调用成本** | 每 10 轮触发一次 background review fork(额外 LLM 调用) |
| **磁盘空间** | `MEMORY.md` / `USER.md` / `skills/` 都很小(KB 级) |
| **隐私** | 用户对话内容会被沉淀到文件(已有注入扫描、写入审批、drift 检测三重防护) |
| **Bridge session 隔离** | Bridge 按 `session_id::hermes_home::model_id` 缓存 agent(最多 8 个 LRU),不同 session_id 不共享 memory store |
---
## 第五部分:核心文件清单
### 上下文压缩
| 文件 | 作用 |
|---|---|
| `agent/context_compressor.py` | 主实现:阈值判断、5 阶段流水线、4 层失败降级 |
| `agent/context_engine.py` | 基类,token 估算 |
| `agent/context_breakdown.py` | 上下文构成分析(用于诊断) |
| `agent/conversation_compression.py` | 压缩调用入口、回滚、用户告警 |
| `agent/conversation_loop.py:1972, 3272, 6360` | 触发点(请求前预检 + 响应后更新) |
| `agent/agent_init.py:1860-2009` | 配置加载与 `ContextCompressor` 实例化 |
| `agent/auxiliary_client.py` | 独立的摘要模型客户端 |
| `cli-config.yaml.example:425-556` | 配置项示例 |
### 记忆机制
| 文件 | 作用 |
|---|---|
| `tools/memory_tool.py` | 内置 `MEMORY.md` / `USER.md` 存储 + `memory` 工具 |
| `agent/memory_provider.py` | `MemoryProvider` ABC 接口 |
| `agent/memory_manager.py` | `MemoryManager`(编排 + prefetch / sync / shutdown) |
| `agent/background_review.py` | 后台 fork 自评 |
| `agent/turn_context.py:582-590` | Memory nudge 触发条件 |
| `agent/turn_finalizer.py:698-724` | Review spawn / sync_all 调用点 |
| `agent/agent_init.py:1655-1739` | Memory 初始化(双层 gate) |
| `agent/context_references.py` | `@file:` / `@diff` 等内联引用展开(与 memory 无关) |
| `plugins/memory/` | 外部 provider 插件目录 |
| `hermes_state.py:239` | `state.db` 默认路径 |
| `cli-config.yaml.example:659-683` | Memory 配置示例 |
### Bridge 模式
| 文件 | 作用 |
|---|---|
| `backend/hermes-bridge/hermes_bridge.py:228` | Bridge 显式 `skip_memory=True` |
| `backend/hermes-bridge/hermes_bridge.py:219-231` | agent 创建参数 |