源码级拆解 Hermes Agent:记忆系统、上下文压缩与 MCP 集成的工程实现

为什么要拆这个项目

Hermes Agent 是 Nous Research 开源的 AI 智能体框架,今年 2 月低调发布,4 月 GitHub Stars 就破了 12 万。半年之内从 v0.1.0 迭代到 v0.14.0,社区贡献者超 240 人。

我关注它不是因为 star 多,而是因为它在技术上做了一个很少见的选择:自进化(Self-Evolving)。大多数 Agent 框架的定位是"编排工具"------帮你把 LLM 和工具串起来。Hermes 的定位是"越用越聪明"------Agent 执行完复杂任务后,会自动把解决流程沉淀为可复用的技能文件,下次遇到类似问题直接调用。

这篇文章从源码层面拆解 Hermes 最有技术含量的几个模块:记忆系统、上下文压缩、Prompt 缓存优化、MCP 集成。每个模块都会讲清楚它解决什么问题、代码怎么实现、以及实际使用中的局限。

项目地址:github.com/nousresearc...,MIT 许可证,Python 3.11+,当前版本 v0.14.0。


一、整体架构:三层分离

先看全局。Hermes 采用三层架构,把用户界面、Agent 逻辑、执行后端严格分开:

scss 复制代码
接口层 ─────── CLI(TUI) / Gateway(20+平台) / ACP(IDE协议) / Cron(定时)
                                  │
                                  ▼
核心逻辑层 ──── AIAgent (run_agent.py, ~9200行)
               ├── Transport (4种LLM通信协议)
               ├── ContextEngine (上下文压缩)
               └── MemoryProvider (8种记忆后端)
                                  │
                                  ▼
执行后端层 ──── Local / Docker / SSH / Singularity / Modal / Daytona / Vercel

几个值得注意的设计选择:

单 Agent 模式。和 CrewAI、MetaGPT 这些多 Agent 框架不同,Hermes 核心是一个单体 Agent------一个进程、一个 session 数据库、一个记忆存储、一个工具注册表。它的理由是:多 Agent 实例会导致记忆碎片化和工具状态漂移。需要并行时,主 Agent 启动隔离的 Sub-agent 执行子任务,完成后汇报结果并销毁。

插拔式 Transport 。不同 LLM 供应商的 API 协议差异很大(OpenAI Chat Completions、Anthropic Messages、AWS Bedrock Converse......),Hermes 用 Transport 抽象层屏蔽差异。每个 Transport 负责消息格式转换、工具 Schema 转换、响应归一化。新增 Provider 只需选一个已有 Transport 配上 api_mode 字符串,不用写新 Transport。

7 种执行后端 。Agent 的"思考"和"动手"分离。Docker 后端做了安全硬化------drop 所有 capabilities、禁止特权提升、PID 限制、可选只读根文件系统。这个设计在让 Agent 自主执行 shell 命令的场景下很重要,否则一句 rm -rf / 就完蛋了。


二、记忆系统:三层设计

记忆系统是 Hermes 和其他 Agent 框架拉开差距的核心。分三层,每层解决不同的问题。

Layer 1:冻结文件------跨会话身份与知识

三个 Markdown 文件构成 Agent 的"长期人格":

bash 复制代码
~/.hermes/
├── SOUL.md    # Agent 身份/人格定义,注入 System Prompt 最前端
├── MEMORY.md  # 环境/项目事实(技术栈、约定、踩过的坑),上限 2200 字符
└── USER.md    # 用户沟通风格与偏好,上限 1375 字符

MEMORY.md 的内容大概长这样:

markdown 复制代码
## 项目环境
- 后端:Python 3.11 + FastAPI
- 数据库:PostgreSQL 15,ORM 用 SQLAlchemy 2.0
- 部署:Docker Compose + Nginx

## 约定
- API 路由统一放 routers/ 目录
- 所有数据库操作走 async session
- 提交代码前必须跑 ruff check

## 历史踩坑
- psycopg2 和 asyncpg 不能混用,之前因为这个排查了两小时

关键设计 :这三个文件在会话启动时做一次冻结快照注入到 System Prompt,会话过程中即使 Agent 更新了磁盘上的文件,已构建的 System Prompt 也不变。

为什么要这样做?因为 Prompt Cache 。Anthropic 和 OpenAI 都支持对 System Prompt 的前缀做缓存。如果 System Prompt 在会话中变来变去,缓存就失效了,每次 API 调用都要重新处理完整的前缀。冻结快照保证了前缀稳定,每次调用都能命中缓存,输入 token 成本降 80-90%

这也解释了为什么 MEMORY.md 限制 2200 字符、USER.md 限制 1375 字符------不是技术上做不到更大,而是这些内容会塞进每一次 API 请求的 System Prompt 里。太大了一是影响缓存效率,二是挤占正文的上下文窗口。

Layer 2:程序性记忆------Skills 自进化

这是 Hermes 最有意思的设计。Agent 完成一个复杂任务(5 个以上工具调用)后,会自主把解决过程沉淀为一个 SKILL.md 文件

markdown 复制代码
---
name: deploy-docker-compose
description: 使用 Docker Compose 部署多容器应用
tags: [devops, docker, deployment]
created: 2026-03-15
last_used: 2026-05-10
use_count: 12
---

## 步骤

1. 检查 docker-compose.yml 是否存在
2. 验证所有服务的镜像是否可用
3. 执行 `docker compose up -d`
4. 检查所有容器健康状态
5. 输出服务访问地址

## 注意事项

- 端口冲突时先用 `docker ps` 检查占用
- 生产环境务必设置 `restart: always`

技能文件的生命周期由 Autonomous Curator 管理。它不是一个常驻守护进程,而是在 CLI 启动时或周期性触发。核心逻辑:

  • 30 天没用过的技能标记为"过时"
  • 90 天没用过的技能移入 ~/.hermes/skills/.archive/
  • 永远不自动删除,最坏情况就是归档(可以手动恢复)
  • 只管 Agent 自己创建的技能,不动内置的 118 个 bundled skills

可以先预览再决定要不要执行:

bash 复制代码
hermes curator run --dry-run  # 预览审查结果,不实际操作

这个设计的本质是让 Agent 建立自己的"肌肉记忆"。第一次做某类任务可能要调 5 次工具、花 30 秒 token,第二次直接调技能,快得多也便宜得多。

不过实际用下来也有问题------Agent 自动生成的技能质量不一定靠谱。有时候它会把一个特定场景的解决方案泛化成通用技能,下次用的时候反而出错。Curator 的归档策略只看使用频率,不看技能质量,这块还有优化空间。

Layer 3:会话搜索------SQLite FTS5

所有历史对话存在本地 SQLite 数据库 ~/.hermes/state.db(WAL 模式支持并发读写)。数据库的 messages_fts 是一个 FTS5 虚拟表,支持全文检索。

搜索流程:

css 复制代码
Agent 调用 session_search
    → FTS5 全文检索,按相关性排名
    → 按 session 分组,解析子→父 session 关系
    → 取 Top 3-5 个 session,每个截断到约 100K 字符
    → 用辅助小模型生成摘要
    → 返回摘要(不是原始全文,避免撑爆上下文)

存储增长方面,每轮对话几 KB 加上 FTS5 索引,半年正常使用大概 20-100 MB,不会成为瓶颈。


三、上下文压缩:Head-Middle-Tail 策略

长对话最终会撞上上下文窗口限制。Hermes 的解法在 agent/context_compressor.py,采用三区策略:

scss 复制代码
 ┌──────────────────────────────────────────────────────┐
 │  Head (保护区)     │  Middle (压缩区)   │  Tail (保护区)  │
 │  System Prompt     │  历史对话           │  最近 N 条消息   │
 │  永不压缩           │  超阈值时做摘要      │  永不压缩        │
 └──────────────────────────────────────────────────────┘

默认配置:上下文使用率到 50% 时触发压缩,目标压到 20%,尾部至少保护最近 20 条消息。

源码里有几个巧妙的工程细节:

增量摘要。压缩器会存上一轮的摘要结果,后续压缩在此基础上更新而非从头开始。这样即使经历多次压缩,早期的重要上下文不会被层层摘要稀释掉。

Anti-Thrashing 保护。如果最近两次压缩每次节省不到 10%,直接跳过压缩。这防止了一种死循环:上下文刚好在阈值附近震荡,反复触发压缩但每次只省一点点,白花 LLM 调用的钱。

摘要 Prompt 设计_summarizer_preamble 把摘要生成定位为"独立助手撰写交接文档",而不是"回答问题"。如果不这么做,摘要模型会把对话中用户提的问题当成对自己的提问,试图回答而不是总结。

已知限制:摘要模型的上下文窗口必须大于等于主 Agent 模型的,否则摘要本身就会触发 context-length 错误。这在混合使用不同模型时容易踩坑。


四、Prompt Cache 优化

实现在 agent/prompt_caching.py,策略叫 "system_and_3"

Anthropic 允许每个请求最多 4 个 cache_control 断点。Hermes 的分配方式:

sql 复制代码
断点 1:System Prompt(包含 SOUL.md + MEMORY.md + USER.md 的冻结快照)
断点 2-4:最近 3 条非系统消息的滚动窗口

System Prompt 在整个 session 中是固定的(前面说的"冻结快照"设计),所以断点 1 的缓存命中率接近 100%。后面 3 个断点覆盖最近的对话上下文,大部分情况下也能命中。

实际效果是每次 API 调用只需要处理最新一轮对话的增量 token,历史部分全部走缓存。这个优化对成本控制非常关键------Hermes 号称月运行成本 $7-22,Prompt Cache 是重要原因之一。


五、MCP 集成:1900 行的双向桥梁

MCP 集成是 Hermes 与外部工具生态互联的核心,代码在 tools/mcp_tool.py,有 1900 多行,是所有工具模块里最重的。

5.1 架构设计

Hermes 的 Agent 主循环是同步 Python 代码,但 MCP 协议是异步的。这产生了一个工程问题:怎么从同步代码调用异步协议?

解法是用一个独立的 daemon 线程跑 asyncio 事件循环_mcp_loop),同步侧通过 run_coroutine_threadsafe 提交任务并阻塞等待结果。不优雅,但管用。

python 复制代码
# 简化后的核心逻辑
import asyncio
import threading

class MCPManager:
    def __init__(self):
        self._loop = asyncio.new_event_loop()
        self._thread = threading.Thread(target=self._run_loop, daemon=True)
        self._thread.start()
    
    def _run_loop(self):
        asyncio.set_event_loop(self._loop)
        self._loop.run_forever()
    
    def call_tool_sync(self, server_name: str, tool_name: str, arguments: dict):
        """从同步代码调用 MCP Server 的工具"""
        future = asyncio.run_coroutine_threadsafe(
            self._call_tool_async(server_name, tool_name, arguments),
            self._loop
        )
        return future.result(timeout=30)
    
    async def _call_tool_async(self, server_name, tool_name, arguments):
        session = self._sessions[server_name]
        return await session.call_tool(tool_name, arguments)

5.2 工具命名和动态发现

MCP Server 暴露的工具会被加上 mcp_{server_name}_ 前缀(连字符替换为下划线),避免和内置工具冲突。比如一个叫 finance-data 的 MCP Server 暴露了 query_rating 工具,注册后的名字就是 mcp_finance_data_query_rating

MCP Server 可以通过 notifications/tools/list_changed 通知工具列表变更,Hermes 自动重新拉取并更新注册。所以 Server 侧可以动态增删工具,Client 侧不用重启。

5.3 双向能力

Hermes 不只是 MCP Client,它同时也是 MCP Server。可以把自己的对话历史、session search、附件管理能力暴露给外部 MCP Client(比如 Claude Desktop、Cursor)。

5.4 连接管理

每个 MCP Server 作为长生命周期的 asyncio Task 运行。断线自动重连,指数退避,最多 5 次重试,最大退避 60 秒。

支持两种传输方式:stdio(本地进程间通信,通过 command + args 启动子进程)和 HTTP/StreamableHTTP(远程)。

5.5 安全处理

一个容易被忽略的细节:stdio 方式启动的 MCP Server 子进程,不会继承宿主的完整环境变量 。Hermes 只传递安全基线变量(PATH、HOME、USER 等),过滤掉所有可能包含凭证的变量。这防止了一种常见的安全隐患------MCP Server 代码(可能来自第三方)通过 os.environ 拿到宿主的 API Key。

5.6 自己写一个 MCP Server 接入 Hermes

接入外部工具只需要两步:写一个 MCP Server,然后在配置文件里注册。

以一个查询股票行情的 MCP Server 为例:

python 复制代码
"""stock_server.py --- 股票行情 MCP Server"""
from mcp.server import Server
from mcp.types import Tool, TextContent
import mcp.server.stdio
import json

server = Server("stock-quotes")

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="get_stock_price",
            description="查询 A 股实时股价,返回最新价、涨跌幅、成交量",
            inputSchema={
                "type": "object",
                "properties": {
                    "symbol": {
                        "type": "string",
                        "description": "股票代码,如 600519(贵州茅台)"
                    }
                },
                "required": ["symbol"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "get_stock_price":
        # 实际项目对接行情 API
        data = {"symbol": arguments["symbol"], "price": 1688.00,
                "change_pct": "+1.2%", "volume": "3.2万手"}
        return [TextContent(type="text", text=json.dumps(data, ensure_ascii=False))]
    return [TextContent(type="text", text='{"error": "unknown tool"}')]

async def main():
    async with mcp.server.stdio.stdio_server() as (rs, ws):
        await server.run(rs, ws, server.create_initialization_options())

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

然后在 ~/.hermes/config.yaml 里注册:

yaml 复制代码
mcp:
  servers:
    stock-quotes:
      command: python
      args: ["/path/to/stock_server.py"]

重启 Hermes 后,Agent 就能自动发现并调用 mcp_stock_quotes_get_stock_price 工具了。


六、多平台接入

Hermes 的 Gateway 支持 20 多个消息平台------Telegram、Discord、Slack、钉钉、飞书、企业微信、个人微信(v0.8.0+)、QQ Bot......甚至还有 Home Assistant。

技术上并不复杂,本质是一层适配器模式:每个平台一个 Python 文件(在 gateway/platforms/ 目录下),实现消息收发的标准接口,往下调同一个 AIAgent 核心。社区在 v0.4.0 时用不到 300 行代码就加完了 Mattermost 适配,说明扩展成本很低。

比较有意思的设计是跨平台上下文:所有平台共享同一个 MEMORY.mdUSER.md 和 SQLite session 数据库。你在 Telegram 上教 Agent 的东西,切到飞书上它也记得。切换平台时新 session 通过会话搜索自动恢复最相关的历史上下文。


七、RL 训练集成

Hermes 不只是一个运行时 Agent 框架,它同时也是一个训练数据生成管线,这在开源 Agent 框架里很少见。

三个组件:Atropos(轨迹 API Server + GRPO 优势计算)、Tinker(LoRA 训练/推理)、Environment(任务/评分/奖励)。

流程是:Agent 执行任务时完整记录轨迹(对话历史 + 工具调用 + 结果),计算奖励分数,导出为训练数据,用 LoRA 微调模型,再用微调后的模型跑 Agent,循环迭代。

社区提供了 RealWorldTaskEnv,包含 30 个真实任务(编码、搜索、文件操作、数据分析、运维),支持多维奖励评分,导出 SFT-ready JSONL。

这个设计的野心是把 Agent 的使用数据变成模型改进的飞轮。不过目前来看,RL 训练的门槛还是比较高的,大部分用户停留在直接使用 Agent 的阶段。


八、安全加固

让 Agent 自主执行 shell 命令,安全就是生命线。Hermes v0.8.0 之后做了一轮大幅加强:

执行隔离:Docker 后端 drop 所有 capabilities,禁止特权提升,PID 限制,可选只读根文件系统。Singularity 后端做全命名空间隔离。

命令审批tools/approval.py 维护一个危险命令黑名单。fork bomb、block device 直写这类绝对禁止,不可覆盖。其他危险操作会弹出人工审批确认。

凭证保护:环境变量中含 KEY、TOKEN、SECRET、PASSWORD、CREDENTIAL、AUTH 的一律过滤,防止 Agent 在 shell 输出中暴露凭证。MCP Server 子进程只继承安全基线变量。

v0.8.0 新增:SSRF 保护、时序攻击缓解、tar 遍历防护、跨 session 隔离。


九、成本与资源

Hermes 的资源需求很低:

不跑浏览器自动化的话,1 个 vCPU + 1 GB RAM + 20 GB 存储就够了。默认调用云端 LLM API,不需要 GPU(本地跑 Ollama/vLLM 才需要)。

月成本大概 <math xmlns="http://www.w3.org/1998/Math/MathML"> 7 − 22 : V P S 7-22:VPS </math>7−22:VPS5-7(Hetzner / DigitalOcean),LLM API $2-15(取决于模型选择和用量)。Prompt Cache 优化是控制 API 成本的关键------冻结快照 + system_and_3 策略让大部分请求只处理增量 token。


十、和其他框架的核心差异

和 AutoGPT、CrewAI、LangGraph 这些框架比,Hermes 最大的差异在自进化能力------Skills 自动沉淀 + Curator 生命周期管理 + 会话搜索。其他框架基本没有这个维度。

代价是它押注了单 Agent 模式。如果你的场景需要多个 Agent 角色协作(比如 MetaGPT 的软件团队模拟),Hermes 原生支持不够好,要靠 Parent-Subagent 机制变通。实际上约 20% 的用户同时用 Hermes + 另一个多 Agent 框架,各取所长。

另一个差异是多平台接入。大部分 Agent 框架只提供 CLI 或 API 入口,Hermes 直接支持 20+ 消息平台,可以把 Agent 部署到 Telegram、飞书、企业微信这些日常使用的工具里。这对非技术用户的可达性好很多。


十一、局限和开放问题

用下来也有些问题值得注意:

Skills 质量参差。Agent 自动生成的技能有时会过度泛化------把一个特定场景的解决方案变成"通用技能",下次用的时候可能不适用。Curator 只按使用频率归档,不评估质量。

记忆容量天花板MEMORY.md 限制 2200 字符、USER.md 限制 1375 字符。对于复杂项目,这个容量可能不够。但放宽限制会影响 Prompt Cache 效率和上下文窗口利用率,这是一个 trade-off。

上下文压缩的摘要模型限制。摘要模型的 Context Window 必须大于等于主模型,否则摘要本身就会 context-length 错误。混合使用不同模型时容易踩这个坑。

EvoMap 争议。2026 年 4 月,中国 AI 团队 EvoMap 指控 Hermes 的自进化引擎与其开源项目 Evolver 高度雷同。Nous Research 否认抄袭。此事目前仍有争议,使用时自行判断。


参考

相关推荐
Mr数据杨6 小时前
AIGC工具平台-StoryBoard故事板
人工智能·aigc·php
指掀涛澜天下惊6 小时前
AI 基础知识十六 Decoder-only 训练诗集示例
人工智能·transformer·decoder-only
上海云盾第一敬业销售7 小时前
DDoS防护架构解析与实战经验
架构·ddos
上海云盾-小余7 小时前
业务层 CC 攻击精准研判:行为识别与轻量化拦截方案
运维·服务器·安全·架构
heimeiyingwang7 小时前
【架构实战】MySQL主从复制与读写分离:数据库高可用架构
数据库·mysql·架构
Cosolar7 小时前
2026年全球向量数据库技术全景与架构演进深度解析报告
数据库·人工智能·架构·agent·智能体
__log7 小时前
AI对话系统中集成可视化图表能力的战略价值与实施路径深度分析
人工智能
米高梅狮子7 小时前
03.OpenStack使用
linux·前端·云原生·容器·架构·kubernetes·openstack
货拉拉技术7 小时前
私域转化率翻倍的秘密:我们把多模态Agent融进了私域营销
人工智能·算法·设计模式