# 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 创建参数 |