Hermes Agent v0.8 source analysis — initial
This commit is contained in:
956
index.html
Normal file
956
index.html
Normal file
@@ -0,0 +1,956 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hermes Agent v0.8.0 · 源码解析</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0e14;
|
||||
--bg2: #0f1419;
|
||||
--border: #1c2430;
|
||||
--text: #d4d8e0;
|
||||
--dim: #6b7280;
|
||||
--dim2: #9ca3af;
|
||||
--blue: #60a5fa;
|
||||
--green: #4ade80;
|
||||
--yellow: #fbbf24;
|
||||
--red: #f87171;
|
||||
--purple: #a78bfa;
|
||||
--orange: #fb923c;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.75;
|
||||
font-size: 15px;
|
||||
}
|
||||
.wrap { max-width: 1100px; margin: 0 auto; padding: 48px 32px 96px; }
|
||||
|
||||
/* Header */
|
||||
header { border-bottom: 1px solid var(--border); padding-bottom: 28px; margin-bottom: 40px; }
|
||||
h1 { font-size: 34px; font-weight: 700; color: #fff; display: flex; align-items: center; gap: 14px; }
|
||||
.logo-dot {
|
||||
width: 12px; height: 12px; border-radius: 50%; background: var(--green);
|
||||
box-shadow: 0 0 16px var(--green); animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
.lede { color: var(--dim2); font-size: 15px; margin-top: 12px; max-width: 780px; }
|
||||
.meta-row {
|
||||
display: flex; flex-wrap: wrap; gap: 20px; margin-top: 16px;
|
||||
font-size: 12px; color: var(--dim); font-family: "SF Mono", "Monaco", monospace;
|
||||
}
|
||||
.meta-row span strong { color: var(--blue); font-weight: 500; }
|
||||
|
||||
/* TOC */
|
||||
nav.toc {
|
||||
background: var(--bg2); border: 1px solid var(--border); border-radius: 12px;
|
||||
padding: 20px 24px; margin-bottom: 40px;
|
||||
}
|
||||
nav.toc h4 {
|
||||
font-size: 11px; color: var(--dim); text-transform: uppercase;
|
||||
letter-spacing: 1.5px; margin-bottom: 12px; font-weight: 600;
|
||||
}
|
||||
nav.toc ol {
|
||||
list-style: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 8px 24px; counter-reset: toc-counter;
|
||||
}
|
||||
nav.toc li { counter-increment: toc-counter; }
|
||||
nav.toc li a {
|
||||
color: var(--dim2); text-decoration: none; font-size: 13.5px;
|
||||
display: block; padding: 2px 0;
|
||||
}
|
||||
nav.toc li a::before {
|
||||
content: counter(toc-counter, decimal-leading-zero) " ";
|
||||
color: var(--dim); font-family: "SF Mono", monospace; font-size: 11px;
|
||||
}
|
||||
nav.toc li a:hover { color: var(--blue); }
|
||||
|
||||
/* Sections */
|
||||
section { margin: 56px 0; scroll-margin-top: 24px; }
|
||||
h2 {
|
||||
font-size: 24px; color: #fff; font-weight: 600; margin-bottom: 16px;
|
||||
padding-bottom: 8px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
h2 .num {
|
||||
font-family: "SF Mono", monospace; color: var(--blue); font-size: 14px;
|
||||
font-weight: 400; margin-right: 12px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 17px; color: var(--blue); margin: 28px 0 10px; font-weight: 600;
|
||||
}
|
||||
h4 {
|
||||
font-size: 14px; color: var(--yellow); margin: 20px 0 8px;
|
||||
font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
p { margin-bottom: 14px; color: var(--text); }
|
||||
ul, ol { margin: 10px 0 14px 20px; color: var(--text); }
|
||||
li { margin-bottom: 6px; }
|
||||
strong { color: #fff; font-weight: 600; }
|
||||
|
||||
/* Code + citations */
|
||||
code {
|
||||
background: var(--border); padding: 2px 8px; border-radius: 4px;
|
||||
color: var(--yellow); font-size: 13px;
|
||||
font-family: "SF Mono", "Monaco", "Consolas", monospace;
|
||||
}
|
||||
pre {
|
||||
background: var(--bg2); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 16px 20px; margin: 14px 0 20px; overflow-x: auto;
|
||||
font-size: 12.5px; line-height: 1.7;
|
||||
}
|
||||
pre code { background: transparent; padding: 0; color: var(--text); font-size: 12.5px; }
|
||||
|
||||
.cite {
|
||||
display: inline-block; background: rgba(96, 165, 250, 0.1);
|
||||
border: 1px solid rgba(96, 165, 250, 0.3); color: var(--blue);
|
||||
padding: 1px 8px; border-radius: 4px; font-family: "SF Mono", monospace;
|
||||
font-size: 11.5px; margin: 0 2px;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%; border-collapse: collapse; margin: 14px 0 20px;
|
||||
background: var(--bg2); border: 1px solid var(--border); border-radius: 8px;
|
||||
overflow: hidden; font-size: 13.5px;
|
||||
}
|
||||
th {
|
||||
background: rgba(96, 165, 250, 0.08); color: var(--blue);
|
||||
text-align: left; padding: 10px 14px; font-weight: 600;
|
||||
border-bottom: 1px solid var(--border); font-size: 12.5px;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
td { padding: 10px 14px; color: var(--text); border-bottom: 1px solid var(--border); vertical-align: top; }
|
||||
td:first-child { color: var(--yellow); font-family: "SF Mono", monospace; font-size: 12.5px; white-space: nowrap; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
|
||||
/* Architecture ASCII */
|
||||
.ascii-art {
|
||||
background: var(--bg2); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 20px; margin: 14px 0 20px; white-space: pre;
|
||||
font-family: "SF Mono", "Monaco", monospace; font-size: 12px;
|
||||
line-height: 1.5; color: var(--dim2); overflow-x: auto;
|
||||
}
|
||||
.ascii-art .b { color: var(--blue); }
|
||||
.ascii-art .g { color: var(--green); }
|
||||
.ascii-art .y { color: var(--yellow); }
|
||||
.ascii-art .p { color: var(--purple); }
|
||||
.ascii-art .r { color: var(--red); }
|
||||
|
||||
/* Callouts */
|
||||
.callout {
|
||||
background: rgba(96, 165, 250, 0.06);
|
||||
border-left: 3px solid var(--blue);
|
||||
padding: 14px 20px; margin: 16px 0; border-radius: 4px;
|
||||
color: var(--text); font-size: 14px;
|
||||
}
|
||||
.callout.warn { background: rgba(245, 158, 11, 0.06); border-left-color: var(--yellow); }
|
||||
.callout.danger { background: rgba(248, 113, 113, 0.06); border-left-color: var(--red); }
|
||||
.callout strong { color: var(--yellow); }
|
||||
.callout.warn strong { color: var(--yellow); }
|
||||
.callout.danger strong { color: var(--red); }
|
||||
|
||||
/* Stats grid */
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin: 14px 0 20px; }
|
||||
.stat {
|
||||
background: var(--bg2); border: 1px solid var(--border);
|
||||
border-radius: 8px; padding: 16px 18px;
|
||||
}
|
||||
.stat-label { color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px; }
|
||||
.stat-value { color: #fff; font-size: 22px; font-weight: 600; font-family: "SF Mono", monospace; }
|
||||
|
||||
/* Tags */
|
||||
.tag {
|
||||
display: inline-block; background: var(--border); color: var(--blue);
|
||||
padding: 3px 12px; border-radius: 20px; font-size: 11.5px;
|
||||
margin-right: 6px; margin-bottom: 6px; font-family: "SF Mono", monospace;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin-top: 64px; padding-top: 24px; border-top: 1px solid var(--border);
|
||||
color: var(--dim); font-size: 12.5px; text-align: center;
|
||||
}
|
||||
footer a { color: var(--blue); text-decoration: none; }
|
||||
footer a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<h1><span class="logo-dot"></span>Hermes Agent v0.8.0 · 源码解析</h1>
|
||||
<p class="lede">
|
||||
Nous Research 开源的自进化 AI agent。本文基于本地 <code>git clone</code> 的 v0.8.0 源码
|
||||
(commit <code>d6785dc</code>,2026-04-12),结合 <code>AGENTS.md</code>、<code>RELEASE_v0.8.0.md</code>
|
||||
和 842 个 Python 文件的结构化阅读。所有技术结论都标注了 <span class="cite">file:line</span> 引用。
|
||||
</p>
|
||||
<div class="meta-row">
|
||||
<span>version <strong>0.8.0</strong></span>
|
||||
<span>commit <strong>d6785dc</strong></span>
|
||||
<span>python files <strong>842</strong></span>
|
||||
<span>markdown files <strong>542</strong></span>
|
||||
<span>repo size <strong>162 MB</strong></span>
|
||||
<span>test count <strong>~3000</strong></span>
|
||||
<span>license <strong>MIT</strong></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="toc">
|
||||
<h4>目录</h4>
|
||||
<ol>
|
||||
<li><a href="#tldr">一句话总结</a></li>
|
||||
<li><a href="#evidence">取证方式</a></li>
|
||||
<li><a href="#entry">三个入口脚本</a></li>
|
||||
<li><a href="#arch">整体架构</a></li>
|
||||
<li><a href="#layout">仓库结构地图</a></li>
|
||||
<li><a href="#aiagent">核心:AIAgent 类</a></li>
|
||||
<li><a href="#loop">Agent 主循环</a></li>
|
||||
<li><a href="#registry">工具注册表</a></li>
|
||||
<li><a href="#model_tools">工具编排层</a></li>
|
||||
<li><a href="#session">会话 + FTS5 搜索</a></li>
|
||||
<li><a href="#gateway">消息网关</a></li>
|
||||
<li><a href="#platforms">22 个平台适配器</a></li>
|
||||
<li><a href="#backends">6 种 Terminal 后端</a></li>
|
||||
<li><a href="#skills">Skills 系统</a></li>
|
||||
<li><a href="#slash">Slash 命令中心化</a></li>
|
||||
<li><a href="#profile">Profile 多实例</a></li>
|
||||
<li><a href="#v08">v0.8 重要变化</a></li>
|
||||
<li><a href="#docker">Docker 运行时</a></li>
|
||||
<li><a href="#takeaways">设计洞察</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="tldr">
|
||||
<h2><span class="num">01</span>一句话总结</h2>
|
||||
<p>
|
||||
Hermes Agent 是一个<strong>多平台入口、同一个 agent 循环、可插拔 LLM provider、可插拔 terminal 后端、
|
||||
可插拔 skills + MCP + plugins</strong> 的 Python agent 框架。它的最关键设计不是 LLM 能力,而是
|
||||
<strong>把"跟 agent 说话"、"agent 跑代码"、"agent 用工具"、"agent 记忆"四件事完全正交</strong>:
|
||||
你可以从 CLI / Telegram / Slack / Discord / WhatsApp / Signal / 邮件 / Matrix / 钉钉 / 飞书 / WeChat
|
||||
任意入口发消息,同一个 <code>AIAgent</code> 实例处理,代码执行可以转发到本地 / Docker / SSH 远程 /
|
||||
Daytona / Modal / Singularity 任意沙箱,而模型 provider 从 OpenRouter / Nous / Anthropic / OpenAI /
|
||||
z.ai / Kimi / MiniMax / Google AI Studio / HuggingFace / 任意 OpenAI 兼容 custom endpoint 任选。
|
||||
</p>
|
||||
<p>
|
||||
所有这些都可以在运行时用 <code>/model</code> 切换,不重启,不丢上下文。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="evidence">
|
||||
<h2><span class="num">02</span>取证方式 · 不是 AI 猜的</h2>
|
||||
<p>
|
||||
本文的每一条技术断言都基于对实际源码的阅读,格式是 <span class="cite">file:line</span> 引证。
|
||||
证据来源:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>git clone https://github.com/NousResearch/hermes-agent.git</code> 到
|
||||
<code>~/Projects/research/20260413-hermes-source-analysis/source/</code></li>
|
||||
<li>顶层仓库文件:<code>AGENTS.md</code>(20 KB 开发者指南)、<code>README.md</code>、
|
||||
<code>RELEASE_v0.8.0.md</code>(209 PR / 82 issue 合并日志)、<code>Dockerfile</code>、
|
||||
<code>pyproject.toml</code>、<code>docker/entrypoint.sh</code></li>
|
||||
<li>核心代码文件:<code>run_agent.py</code>(10 578 行)、<code>cli.py</code>(9 920 行)、
|
||||
<code>gateway/run.py</code>(8 844 行)、<code>hermes_state.py</code>(1 238 行)、
|
||||
<code>tools/registry.py</code>、<code>model_tools.py</code></li>
|
||||
</ul>
|
||||
<div class="callout">
|
||||
<strong>为什么要讲取证</strong>:AI 分析源码最容易出的问题是"听起来对但其实瞎编"。
|
||||
本文写作时使用的读法是<em>先读 → 再写 → 不反过来</em>。若发现任何引用错误,以 GitHub 上
|
||||
<code>v0.8.0</code> 标签为准,反馈到项目 issues。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="entry">
|
||||
<h2><span class="num">03</span>三个入口脚本</h2>
|
||||
<p>
|
||||
<code>pyproject.toml</code> <span class="cite">pyproject.toml:112-115</span> 定义了 3 个可执行入口:
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>命令</th><th>指向</th><th>作用</th></tr>
|
||||
<tr><td>hermes</td><td>hermes_cli.main:main</td><td>交互式 CLI + 所有 <code>hermes *</code> 子命令</td></tr>
|
||||
<tr><td>hermes-agent</td><td>run_agent:main</td><td>单次 agent 调用(脚本模式、批处理、数据生成)</td></tr>
|
||||
<tr><td>hermes-acp</td><td>acp_adapter.entry:main</td><td>ACP 协议服务器(给 VS Code / Zed / JetBrains 用)</td></tr>
|
||||
</table>
|
||||
<p>
|
||||
核心类 <code>AIAgent</code> 定义在 <span class="cite">run_agent.py:492</span>。
|
||||
交互 CLI 的 <code>HermesCLI</code> 定义在 <span class="cite">cli.py:1574</span>。
|
||||
消息网关的 <code>GatewayRunner</code> 定义在 <span class="cite">gateway/run.py:512</span>。
|
||||
<strong>这三个类是理解 Hermes 的三把钥匙</strong>。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="arch">
|
||||
<h2><span class="num">04</span>整体架构</h2>
|
||||
<div class="ascii-art"><span class="y"> ┌─────────────────────────────────────────────┐</span>
|
||||
<span class="y"> │</span> <span class="g">用户入口</span> <span class="y">│</span>
|
||||
<span class="y"> │</span> CLI · Telegram · Slack · Discord <span class="y">│</span>
|
||||
<span class="y"> │</span> WhatsApp · Signal · Email · Matrix <span class="y">│</span>
|
||||
<span class="y"> │</span> DingTalk · Feishu · WeCom · WeChat · 22+ <span class="y">│</span>
|
||||
<span class="y"> └──────────────────┬──────────────────────────┘</span>
|
||||
│
|
||||
<span class="b">[gateway/run.py + gateway/platforms/*]</span>
|
||||
│
|
||||
▼
|
||||
<span class="b">┌─────────────────────────────────────────────────────────────────────┐</span>
|
||||
<span class="b">│</span> <span class="g">AIAgent 核心(run_agent.py:492)</span> <span class="b">│</span>
|
||||
<span class="b">│</span> ┌──────────────┬──────────────┬──────────────┬──────────────┐ <span class="b">│</span>
|
||||
<span class="b">│</span> │ 系统提示词 │ 会话存储 │ 内存管理 │ 工具编排 │ <span class="b">│</span>
|
||||
<span class="b">│</span> │ agent/ │ hermes_state │ agent/ │ model_tools │ <span class="b">│</span>
|
||||
<span class="b">│</span> │ prompt_ │ .py (FTS5) │ memory_ │ + tools/ │ <span class="b">│</span>
|
||||
<span class="b">│</span> │ builder.py │ │ manager │ registry.py │ <span class="b">│</span>
|
||||
<span class="b">│</span> └──────────────┴──────────────┴──────────────┴──────────────┘ <span class="b">│</span>
|
||||
<span class="b">│</span> <span class="b">│</span>
|
||||
<span class="b">│</span> <span class="p">主循环</span>:run_conversation() :7528 <span class="b">│</span>
|
||||
<span class="b">│</span> while api_call_count < max_iterations: <span class="b">│</span>
|
||||
<span class="b">│</span> call LLM with tool schemas <span class="b">│</span>
|
||||
<span class="b">│</span> if response.tool_calls: <span class="b">│</span>
|
||||
<span class="b">│</span> handle_function_call(...) <span class="b">│</span>
|
||||
<span class="b">│</span> append tool results <span class="b">│</span>
|
||||
<span class="b">│</span> else: return response.content <span class="b">│</span>
|
||||
<span class="b">└──────────────────┬──────────────────┬───────────────────────────────┘</span>
|
||||
│ │
|
||||
<span class="g">LLM 层</span> <span class="y">工具执行层</span>
|
||||
│ │
|
||||
▼ ▼
|
||||
<span class="g">┌─────────────────────┐</span> <span class="y">┌──────────────────────────────────────────┐</span>
|
||||
<span class="g">│</span> OpenRouter <span class="g">│</span> <span class="y">│</span> tools/*.py(40+ 工具) <span class="y">│</span>
|
||||
<span class="g">│</span> Nous Portal <span class="g">│</span> <span class="y">│</span> ┌────────────────────────────────────┐ <span class="y">│</span>
|
||||
<span class="g">│</span> Anthropic <span class="g">│</span> <span class="y">│</span> │ 终端(terminal_tool.py) │ <span class="y">│</span>
|
||||
<span class="g">│</span> OpenAI + Codex <span class="g">│</span> <span class="y">│</span> │ ↓ 可选 6 种后端 │ <span class="y">│</span>
|
||||
<span class="g">│</span> Google AI Studio <span class="g">│</span> <span class="y">│</span> │ local · docker · ssh · │ <span class="y">│</span>
|
||||
<span class="g">│</span> z.ai / GLM <span class="g">│</span> <span class="y">│</span> │ modal · daytona · singularity │ <span class="y">│</span>
|
||||
<span class="g">│</span> Kimi / Moonshot <span class="g">│</span> <span class="y">│</span> └────────────────────────────────────┘ <span class="y">│</span>
|
||||
<span class="g">│</span> MiniMax <span class="g">│</span> <span class="y">│</span> · 文件(file_tools.py) <span class="y">│</span>
|
||||
<span class="g">│</span> HuggingFace <span class="g">│</span> <span class="y">│</span> · 网络(web_tools, browser_tool) <span class="y">│</span>
|
||||
<span class="g">│</span> xAI / Grok <span class="g">│</span> <span class="y">│</span> · 代码执行(code_execution_tool) <span class="y">│</span>
|
||||
<span class="g">│</span> custom(任意 OAI) <span class="g">│</span> <span class="y">│</span> · 子代理(delegate_tool) <span class="y">│</span>
|
||||
<span class="g">└─────────────────────┘</span> <span class="y">│</span> · MCP 客户端(mcp_tool.py,2195 行) <span class="y">│</span>
|
||||
<span class="y">└──────────────────────────────────────────┘</span></div>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="layout">
|
||||
<h2><span class="num">05</span>仓库结构地图</h2>
|
||||
<p>
|
||||
来源:<code>AGENTS.md</code> 第 12-64 行 + 实际 <code>ls</code> 验证。
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>路径</th><th>行数</th><th>责任</th></tr>
|
||||
<tr><td>run_agent.py</td><td>10 578</td><td>AIAgent 类 + 主循环 + 重试 / fallback / 预算管理</td></tr>
|
||||
<tr><td>cli.py</td><td>9 920</td><td>HermesCLI 类 + 交互 TUI(prompt_toolkit + Rich)</td></tr>
|
||||
<tr><td>gateway/run.py</td><td>8 844</td><td>GatewayRunner + 消息分发 + agent 会话缓存</td></tr>
|
||||
<tr><td>tools/mcp_tool.py</td><td>2 195</td><td>MCP 客户端(OAuth 2.1 PKCE + 动态工具发现)</td></tr>
|
||||
<tr><td>tools/terminal_tool.py</td><td>1 777</td><td>终端执行编排(后台进程、审批、环境切换)</td></tr>
|
||||
<tr><td>hermes_state.py</td><td>1 238</td><td>SessionDB(SQLite + FTS5 全文搜索)</td></tr>
|
||||
<tr><td>tools/delegate_tool.py</td><td>1 103</td><td>子代理派发(并行工作流)</td></tr>
|
||||
<tr><td>model_tools.py</td><td>577</td><td>工具编排层 + sync→async 桥接</td></tr>
|
||||
<tr><td>toolsets.py</td><td>655</td><td>工具集定义 + 平台启用策略</td></tr>
|
||||
<tr><td>tools/registry.py</td><td>335</td><td>单例工具注册表 + dispatch</td></tr>
|
||||
<tr><td>agent/</td><td>—</td><td>prompt_builder, context_compressor, prompt_caching, memory_manager, trajectory, display</td></tr>
|
||||
<tr><td>hermes_cli/</td><td>—</td><td>所有 <code>hermes *</code> 子命令 + 配置 + 皮肤 + setup 向导</td></tr>
|
||||
<tr><td>tools/</td><td>—</td><td>40+ 工具文件(每个自注册到 registry)</td></tr>
|
||||
<tr><td>tools/environments/</td><td>3 285</td><td>6 种终端后端(base + local/docker/ssh/modal/daytona/singularity)</td></tr>
|
||||
<tr><td>gateway/platforms/</td><td>—</td><td>22 个消息平台适配器</td></tr>
|
||||
<tr><td>skills/</td><td>—</td><td>26 个顶层分类 / 78 个 bundled skill</td></tr>
|
||||
<tr><td>acp_adapter/</td><td>—</td><td>ACP 协议服务(VS Code / Zed / JetBrains 集成)</td></tr>
|
||||
<tr><td>cron/</td><td>—</td><td>调度器(jobs.py + scheduler.py)</td></tr>
|
||||
<tr><td>tinker-atropos/</td><td>—</td><td>RL 训练子模块(Tinker + Atropos)</td></tr>
|
||||
<tr><td>tests/</td><td>—</td><td>~3000 个 pytest 测试</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="aiagent">
|
||||
<h2><span class="num">06</span>核心:AIAgent 类</h2>
|
||||
<p>
|
||||
<code>class AIAgent</code> 定义在 <span class="cite">run_agent.py:492</span>。构造函数接受 50+ 参数,
|
||||
但核心字段几个:
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>字段</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>model</td><td>""(运行时注入)</td><td>模型名,如 <code>anthropic/claude-opus-4.6</code></td></tr>
|
||||
<tr><td>max_iterations</td><td>90 <span class="cite">run_agent.py:527</span></td><td>工具调用循环上限</td></tr>
|
||||
<tr><td>iteration_budget</td><td>IterationBudget(90) <span class="cite">run_agent.py:619</span></td><td>跨主 agent + 所有子代理共享的预算</td></tr>
|
||||
<tr><td>platform</td><td>None</td><td>"cli" / "telegram" / "discord" / ...,用于注入平台格式提示</td></tr>
|
||||
<tr><td>session_id</td><td>自动生成</td><td>SessionDB 外键</td></tr>
|
||||
<tr><td>enabled_toolsets / disabled_toolsets</td><td>None</td><td>白/黑名单控制可用工具集</td></tr>
|
||||
<tr><td>fallback_model</td><td>None</td><td>主 provider 失败后的备选</td></tr>
|
||||
<tr><td>credential_pool</td><td>None</td><td>多 key 自动轮替</td></tr>
|
||||
<tr><td>skip_context_files / skip_memory</td><td>False</td><td>批处理/RL 场景不注入用户人格</td></tr>
|
||||
</table>
|
||||
<p>
|
||||
注意 <span class="cite">run_agent.py:504-505</span> 的 <code>_context_pressure_last_warned</code> —
|
||||
这是一个<strong>类级别</strong>的 dict,用来跨实例去重"上下文压力警告"。原因写在注释里:
|
||||
<em>gateway 会为每条消息创建一个新 AIAgent 实例,所以实例级的 flag 每次都重置</em>,需要类级共享状态。
|
||||
这个细节暴露了 Gateway 的真实调用模式:<strong>每消息一个实例</strong>,但实例是从 cache 里拿的(见
|
||||
<span class="cite">gateway/run.py:577-584</span> 的 <code>_agent_cache</code>)。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="loop">
|
||||
<h2><span class="num">07</span>Agent 主循环</h2>
|
||||
<p>
|
||||
主循环在 <code>run_conversation()</code>,入口 <span class="cite">run_agent.py:7528</span>,
|
||||
核心 while 在 <span class="cite">run_agent.py:7850</span>:
|
||||
</p>
|
||||
<pre><code>while (api_call_count < self.max_iterations and self.iteration_budget.remaining > 0) or self._budget_grace_call:
|
||||
self._checkpoint_mgr.new_turn()
|
||||
if self._interrupt_requested:
|
||||
interrupted = True
|
||||
break
|
||||
api_call_count += 1
|
||||
...
|
||||
# 调 LLM
|
||||
response = client.chat.completions.create(model=model, messages=messages, tools=tool_schemas)
|
||||
if response.tool_calls:
|
||||
for tool_call in response.tool_calls:
|
||||
result = handle_function_call(tool_call.name, tool_call.args, task_id)
|
||||
messages.append(tool_result_message(result))
|
||||
else:
|
||||
return response.content</code></pre>
|
||||
<p>
|
||||
这个简化版是 <code>AGENTS.md</code> 第 112-122 行给的示意。真实代码在 7850-10213 之间展开了
|
||||
~2000 行复杂度,处理:
|
||||
</p>
|
||||
<ul>
|
||||
<li>中断(<span class="cite">run_agent.py:7855</span>)— 用户发新消息时打断当前循环</li>
|
||||
<li>迭代预算(<span class="cite">run_agent.py:7871</span>)— <code>consume()</code> 消费 1 次,共享预算</li>
|
||||
<li><code>_budget_grace_call</code>(<span class="cite">run_agent.py:7869</span>)— 预算耗尽给模型 1 次总结机会</li>
|
||||
<li><code>step_callback</code> 发给 gateway 做 <code>agent:step</code> hook(<span class="cite">run_agent.py:7878-7902</span>)</li>
|
||||
<li>Skill 使用计数器 <code>_iters_since_skill</code>(<span class="cite">run_agent.py:7906-7908</span>)—
|
||||
每 N 轮未用 skill 会 nudge 模型考虑 <code>skill_manage</code></li>
|
||||
<li>消息消毒:<code>_sanitize_messages_surrogates()</code>(<span class="cite">run_agent.py:356</span>)
|
||||
和 <code>_sanitize_messages_non_ascii()</code>(<span class="cite">run_agent.py:413</span>)—
|
||||
处理奇怪 unicode 导致的 provider 报错</li>
|
||||
</ul>
|
||||
|
||||
<div class="callout warn">
|
||||
<strong>设计原则</strong>:AGENTS.md 第 339-347 行明确写 <em>Prompt Caching Must Not Break</em>。
|
||||
主循环的一条铁律是——<strong>绝对不能中途改变过去的 context、工具集或系统提示</strong>。
|
||||
破坏 cache 意味着 10x 成本。唯一允许改 context 的时刻是<strong>自动上下文压缩</strong>
|
||||
(<code>agent/context_compressor.py</code>)。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="registry">
|
||||
<h2><span class="num">08</span>工具注册表(插件化核心)</h2>
|
||||
<p>
|
||||
Hermes 的工具系统是<strong>"tool 文件自注册"模型</strong>。每个 <code>tools/*.py</code>
|
||||
在模块 import 时调用 <code>registry.register(...)</code> 把自己挂上去。看 <code>tools/registry.py</code>
|
||||
的类定义:
|
||||
</p>
|
||||
<pre><code><span class="cite">tools/registry.py:48</span>
|
||||
class ToolRegistry:
|
||||
"""Singleton registry that collects tool schemas + handlers from tool files."""
|
||||
|
||||
def __init__(self):
|
||||
self._tools: Dict[str, ToolEntry] = {}
|
||||
self._toolset_checks: Dict[str, Callable] = {}
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
toolset: str,
|
||||
schema: dict, # OpenAI function schema
|
||||
handler: Callable, # 实际执行函数
|
||||
check_fn: Callable = None, # 可用性检查(env 变量等)
|
||||
requires_env: list = None,
|
||||
is_async: bool = False,
|
||||
description: str = "",
|
||||
emoji: str = "",
|
||||
max_result_size_chars: int | float | None = None,
|
||||
): ...</code></pre>
|
||||
<p>
|
||||
关键设计:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>单例</strong>:<span class="cite">tools/registry.py:290</span> <code>registry = ToolRegistry()</code>,
|
||||
模块级对象,任意文件 <code>from tools.registry import registry</code> 拿到同一个</li>
|
||||
<li><strong>check_fn</strong>:<span class="cite">tools/registry.py:117-143</span> <code>get_definitions()</code> 先跑 check,
|
||||
失败的 tool 不会出现在发给 LLM 的 schema 列表里。这是 Hermes 实现"API key 缺失时工具消失"的机制</li>
|
||||
<li><strong>MCP 动态注入</strong>:<span class="cite">tools/registry.py:95-110</span> 有 <code>deregister()</code>,
|
||||
专门给 MCP 服务发 <code>notifications/tools/list_changed</code> 时 nuke-and-repave 用</li>
|
||||
<li><strong>dispatch() 同时支持同步 / 异步</strong>:<span class="cite">tools/registry.py:149-166</span>
|
||||
— 如果 <code>entry.is_async</code>,从 <code>model_tools._run_async()</code> 桥接跑</li>
|
||||
<li><strong>tool_error / tool_result helper</strong>:<span class="cite">tools/registry.py:309-335</span>
|
||||
强制所有 tool handler 返回 JSON 字符串,消除了几百次 <code>json.dumps()</code> 样板</li>
|
||||
</ul>
|
||||
<div class="callout">
|
||||
<strong>import 依赖链(防循环)</strong> <span class="cite">tools/registry.py:7-14</span>:
|
||||
<code>tools/registry.py</code> 不依赖任何其他文件 →
|
||||
<code>tools/*.py</code> 依赖 registry →
|
||||
<code>model_tools.py</code> 依赖 registry + 所有 tool 文件 →
|
||||
上层 <code>run_agent.py</code> / <code>cli.py</code> / <code>batch_runner.py</code> 依赖 model_tools。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="model_tools">
|
||||
<h2><span class="num">09</span>工具编排层(sync/async 桥接)</h2>
|
||||
<p>
|
||||
<code>model_tools.py</code> 的顶部(<span class="cite">model_tools.py:1-22</span>)自述为
|
||||
"thin orchestration layer over the tool registry"。它只做两件事:
|
||||
</p>
|
||||
<ol>
|
||||
<li><strong>触发 tool 发现</strong>:import 所有 <code>tools/*.py</code>,让它们 self-register</li>
|
||||
<li><strong>sync→async 桥接</strong>:<code>_run_async()</code> 位于 <span class="cite">model_tools.py:81-120+</span></li>
|
||||
</ol>
|
||||
<p>
|
||||
<code>_run_async()</code> 背后有一个很讲究的设计 —— <strong>三种 loop 策略</strong>:
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>场景</th><th>策略</th><th>为什么</th></tr>
|
||||
<tr><td>CLI 主线程(无 running loop)</td><td>持久共享 loop <code>_tool_loop</code> <span class="cite">model_tools.py:39-56</span></td><td>缓存的 httpx/AsyncOpenAI 客户端需要绑定到活 loop,<code>asyncio.run()</code> 每次 create-and-close 会触发 "Event loop is closed" GC 报错</td></tr>
|
||||
<tr><td>Gateway 已经在 async 栈里</td><td>开一次性 thread pool,在新线程里 <code>asyncio.run()</code> <span class="cite">model_tools.py:108-113</span></td><td>不跟现有 running loop 冲突</td></tr>
|
||||
<tr><td>并行 tool 执行的 worker 线程</td><td>每线程持久 loop(ThreadLocal)<span class="cite">model_tools.py:59-78</span></td><td>避免多 worker 抢同一个 loop,同时保持客户端缓存有效</td></tr>
|
||||
</table>
|
||||
<p>
|
||||
这段代码读下来能看出 Hermes 是经过大量实战打磨的 —— <code>_get_worker_loop()</code> 的文档注释
|
||||
直接说"这是为了避免某个 PR 引入的 'Event loop is closed' bug"。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="session">
|
||||
<h2><span class="num">10</span>会话存储 + FTS5 搜索</h2>
|
||||
<p>
|
||||
<code>hermes_state.py</code> 的 <code>SessionDB</code> 类是整个 agent 的记忆骨架。路径:
|
||||
</p>
|
||||
<pre><code><span class="cite">hermes_state.py:32</span> DEFAULT_DB_PATH = get_hermes_home() / "state.db"
|
||||
<span class="cite">hermes_state.py:34</span> SCHEMA_VERSION = 6
|
||||
<span class="cite">hermes_state.py:115</span> class SessionDB</code></pre>
|
||||
<h4>表结构</h4>
|
||||
<p>
|
||||
<code>sessions</code> 表 <span class="cite">hermes_state.py:41-69</span> 存每个会话的元数据:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>id, source</code> — 会话 ID + 来源("cli" / "telegram" / "discord" / ...)</li>
|
||||
<li><code>parent_session_id</code> — <strong>父会话链</strong>,上下文压缩时切片新会话,父子关系保持</li>
|
||||
<li><code>input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens</code> — 5 种 token 统计</li>
|
||||
<li><code>estimated_cost_usd, actual_cost_usd, cost_status, cost_source, pricing_version</code> — 成本追踪</li>
|
||||
<li><code>billing_provider, billing_base_url, billing_mode</code> — 计费 provider 单独记录(因为 OAuth pool 会轮换)</li>
|
||||
</ul>
|
||||
<p>
|
||||
<code>messages</code> 表 <span class="cite">hermes_state.py:71-85</span> 存每条消息:
|
||||
<code>session_id, role, content, tool_call_id, tool_calls, tool_name, timestamp, token_count, finish_reason, reasoning, reasoning_details, codex_reasoning_items</code>。
|
||||
</p>
|
||||
<p>
|
||||
注意 <code>reasoning_details</code> 和 <code>codex_reasoning_items</code> 是两种不同 provider 的 thinking 格式
|
||||
—— Anthropic thinking blocks 和 OpenAI Codex reasoning item,各自持久化。
|
||||
</p>
|
||||
<h4>FTS5 全文搜索</h4>
|
||||
<p>
|
||||
<span class="cite">hermes_state.py:93-112</span> 定义了 FTS5 虚拟表 + 3 个触发器:
|
||||
</p>
|
||||
<pre><code>CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
||||
content,
|
||||
content=messages,
|
||||
content_rowid=id
|
||||
);
|
||||
|
||||
CREATE TRIGGER messages_fts_insert AFTER INSERT ON messages BEGIN
|
||||
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
|
||||
END;
|
||||
CREATE TRIGGER messages_fts_delete AFTER DELETE ON messages BEGIN
|
||||
INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.id, old.content);
|
||||
END;
|
||||
CREATE TRIGGER messages_fts_update AFTER UPDATE ON messages BEGIN
|
||||
INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.id, old.content);
|
||||
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
|
||||
END;</code></pre>
|
||||
<p>
|
||||
<code>content=messages, content_rowid=id</code> 是 FTS5 的<strong>外部内容模式</strong>(external content table)—
|
||||
索引数据<strong>不重复存储</strong>,直接引用 messages 表的 id。节省一半磁盘。
|
||||
</p>
|
||||
<p>
|
||||
<code>_sanitize_fts5_query()</code> <span class="cite">hermes_state.py:938</span> 实现了 FTS5 查询消毒
|
||||
—— 原因:FTS5 自己有查询语法,用户输入的 <code>-</code>、<code>"</code>、<code>(</code>、<code>)</code>
|
||||
要么 escape 要么会抛错。这个函数的注释明确说明"wrap unquoted hyphenated and dotted terms in quotes so FTS5's
|
||||
tokenizer doesn't split them"(因为 FTS5 会按点和连字符分词)。
|
||||
</p>
|
||||
<h4>高并发写</h4>
|
||||
<p>
|
||||
<span class="cite">hermes_state.py:123-136</span> 的类常量:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>_WRITE_MAX_RETRIES = 15</code></li>
|
||||
<li><code>_WRITE_RETRY_MIN_S = 0.020</code>(20 ms)</li>
|
||||
<li><code>_WRITE_RETRY_MAX_S = 0.150</code>(150 ms)</li>
|
||||
<li><code>_CHECKPOINT_EVERY_N_WRITES = 50</code>(每 50 次写触发一次 PASSIVE WAL checkpoint)</li>
|
||||
</ul>
|
||||
<p>
|
||||
注释解释:SQLite 自带 busy handler 用确定性 sleep 导致"convoy effect"(高并发时互相卡成一队),
|
||||
所以 Hermes 在应用层做<strong>抖动重试</strong>,让多个 writer 自然错开。这是细节工程能力的体现。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="gateway">
|
||||
<h2><span class="num">11</span>消息网关</h2>
|
||||
<p>
|
||||
<code>class GatewayRunner</code> <span class="cite">gateway/run.py:512</span>。
|
||||
启动入口 <code>async def start_gateway()</code> <span class="cite">gateway/run.py:8634</span>。
|
||||
</p>
|
||||
<h4>关键实例字段</h4>
|
||||
<ul>
|
||||
<li><code>adapters: Dict[Platform, BasePlatformAdapter]</code> <span class="cite">gateway/run.py:536</span>
|
||||
— 每个启用的平台加载一个 adapter(telegram / discord / slack / ...)</li>
|
||||
<li><code>session_store = SessionStore(...)</code> <span class="cite">gateway/run.py:553</span>
|
||||
— 会话持久化,wrap SessionDB</li>
|
||||
<li><code>_running_agents: Dict[str, AIAgent]</code> <span class="cite">gateway/run.py:573</span>
|
||||
— 每 session 的活 agent,用于中断/查询</li>
|
||||
<li><code>_pending_messages</code> <span class="cite">gateway/run.py:575</span>
|
||||
— 中断期间用户发的新消息先排队</li>
|
||||
<li><strong><code>_agent_cache</code></strong> <span class="cite">gateway/run.py:583</span>
|
||||
— <strong>这是 prompt caching 能用的关键</strong>。每 session 缓存 <code>(AIAgent, config_signature)</code>,
|
||||
同一个 session 复用同一个 instance,避免重建 system prompt 和内存,保证 Anthropic prefix cache 命中</li>
|
||||
<li><code>_session_model_overrides</code> <span class="cite">gateway/run.py:588</span>
|
||||
— <code>/model</code> 指令切换模型,每 session 一份</li>
|
||||
<li><code>_pending_approvals</code> <span class="cite">gateway/run.py:591</span>
|
||||
— 危险命令审批的待响应状态</li>
|
||||
</ul>
|
||||
<h4>临时配置注入</h4>
|
||||
<p>
|
||||
<span class="cite">gateway/run.py:540-549</span>:
|
||||
</p>
|
||||
<pre><code>self._prefill_messages = self._load_prefill_messages()
|
||||
self._ephemeral_system_prompt = self._load_ephemeral_system_prompt()
|
||||
self._reasoning_config = self._load_reasoning_config()
|
||||
self._service_tier = self._load_service_tier()
|
||||
self._show_reasoning = self._load_show_reasoning()
|
||||
self._busy_input_mode = self._load_busy_input_mode()
|
||||
self._restart_drain_timeout = self._load_restart_drain_timeout()
|
||||
self._provider_routing = self._load_provider_routing()
|
||||
self._fallback_model = self._load_fallback_model()
|
||||
self._smart_model_routing = self._load_smart_model_routing()</code></pre>
|
||||
<p>
|
||||
这些都标注为<strong>"ephemeral, injected at API-call time only and never persisted"</strong> —
|
||||
典型做法:保留功能,但不污染持久化会话,这样切换配置不会破坏历史 cache。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="platforms">
|
||||
<h2><span class="num">12</span>22 个平台适配器</h2>
|
||||
<p>
|
||||
<code>gateway/platforms/</code> 目录实际文件清单(<code>ls</code> 核对):
|
||||
</p>
|
||||
<pre><code>api_server.py base.py bluebubbles.py dingtalk.py
|
||||
discord.py email.py feishu.py helpers.py
|
||||
homeassistant.py matrix.py mattermost.py signal.py
|
||||
slack.py sms.py telegram.py telegram_network.py
|
||||
webhook.py wecom.py wecom_callback.py wecom_crypto.py
|
||||
weixin.py whatsapp.py</code></pre>
|
||||
<p>
|
||||
<code>base.py</code> 定义 <code>BasePlatformAdapter</code> 抽象基类,其他文件继承实现。
|
||||
<code>ADDING_A_PLATFORM.md</code> 在同目录下给开发者文档。
|
||||
</p>
|
||||
<p>
|
||||
特殊的几个:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>api_server.py</code> — REST API 入口,暴露 OpenAI 兼容 <code>/v1/*</code>(就是你跑起来的那个)</li>
|
||||
<li><code>webhook.py</code> — 通用 webhook(接 Home Assistant、自定义集成)</li>
|
||||
<li><code>wecom.py + wecom_callback.py + wecom_crypto.py</code> — 企业微信,因为要做 XML 加密所以拆 3 个文件</li>
|
||||
<li><code>telegram_network.py</code> — 专门处理 Telegram 网络层的 retry / dedup / fallback IP(中国访问常见问题)</li>
|
||||
<li><code>bluebubbles.py</code> — iMessage 桥接(通过 BlueBubbles 项目)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="backends">
|
||||
<h2><span class="num">13</span>6 种 Terminal 后端</h2>
|
||||
<p>
|
||||
<code>tools/environments/</code> 目录(共 3285 行代码):
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>后端</th><th>文件 / 行数</th><th>用途</th></tr>
|
||||
<tr><td>base</td><td>base.py / 579</td><td>抽象基类 + 共用工具方法</td></tr>
|
||||
<tr><td>local</td><td>local.py / 314</td><td>直接在 Hermes 进程宿主机跑(默认,不安全)</td></tr>
|
||||
<tr><td>docker</td><td>docker.py / 560</td><td>每个 task 起独立容器,用完销毁</td></tr>
|
||||
<tr><td>ssh</td><td>ssh.py / 258</td><td>远程执行(把 Hermes 留本地,命令转发到 sandbox VM)</td></tr>
|
||||
<tr><td>modal</td><td>modal.py / 434 + managed_modal.py / 282</td><td>Modal.com 无服务器沙箱</td></tr>
|
||||
<tr><td>daytona</td><td>daytona.py / 229</td><td>Daytona workspace</td></tr>
|
||||
<tr><td>singularity</td><td>singularity.py / 262</td><td>HPC / 学术 Singularity 容器(给 GPU 集群用)</td></tr>
|
||||
<tr><td>file_sync</td><td>file_sync.py / 168</td><td>本地 ↔ 远程文件同步层(ssh/modal 共用)</td></tr>
|
||||
</table>
|
||||
<div class="callout">
|
||||
<strong>为什么这是 Hermes 的亮点</strong>:传统 agent 把代码执行和 agent 本体绑死,要隔离就整个进程全隔离。
|
||||
Hermes 把它<em>解耦</em>—— agent 跑在便宜的 VPS,代码执行可以转发到贵的 Modal GPU 或远程 SSH 沙箱,
|
||||
按需付费。对比昨天讨论的 Manus 架构,Hermes 更灵活(你能选),Manus 更省心(强制 microVM)。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="skills">
|
||||
<h2><span class="num">14</span>Skills 系统</h2>
|
||||
<p>
|
||||
Skills 是 Hermes 的"过程性记忆"—— markdown 文件描述某类任务的步骤。<code>skills/</code> 目录:
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>顶层分类</th><th>包含</th></tr>
|
||||
<tr><td>apple</td><td>apple-reminders / imessage / findmy / apple-notes / ...</td></tr>
|
||||
<tr><td>research</td><td>blogwatcher / polymarket / llm-wiki / arxiv / research-paper-writing</td></tr>
|
||||
<tr><td>gaming</td><td>minecraft-modpack-server / pokemon-player / ...</td></tr>
|
||||
<tr><td>software-development</td><td>(多个)</td></tr>
|
||||
<tr><td>devops, mlops, data-science</td><td>(多个)</td></tr>
|
||||
<tr><td>creative, diagramming, email, feeds, gifs, github, leisure,
|
||||
media, mcp, note-taking, productivity, red-teaming, smart-home,
|
||||
social-media, autonomous-ai-agents, domain, dogfood, index-cache,
|
||||
inference-sh</td>
|
||||
<td>—</td></tr>
|
||||
</table>
|
||||
<p>
|
||||
26 个顶层 + 78 个 bundled skill 文件(入口日志 <em>"78 total bundled"</em> 匹配)。
|
||||
每个 skill 是 <code>SKILL.md</code>,YAML frontmatter 带 metadata。例:
|
||||
</p>
|
||||
<pre><code>---
|
||||
name: dogfood
|
||||
description: Systematic exploratory QA testing of web applications ...
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [qa, testing, browser, web, dogfood]
|
||||
related_skills: []
|
||||
---
|
||||
|
||||
# Dogfood: Systematic Web Application QA Testing
|
||||
...</code></pre>
|
||||
<p>
|
||||
来源:<span class="cite">skills/dogfood/SKILL.md:1-11</span>
|
||||
</p>
|
||||
<p>
|
||||
Skills 的<strong>注入策略</strong>:<code>agent/skill_commands.py</code> 扫描 <code>~/.hermes/skills/</code>,
|
||||
用户打 <code>/skillname</code> 触发时,注入为<strong>用户消息</strong>(不是系统提示词),
|
||||
这样不破坏 prompt caching —— 见 <code>AGENTS.md:135</code>(原文:
|
||||
<em>"Skill slash commands: scans skills dir, injects as user message (not system prompt)
|
||||
to preserve prompt caching"</em>)。
|
||||
</p>
|
||||
<p>
|
||||
此外,skills 还支持"自我进化"—— 独立仓库
|
||||
<code>hermes-agent-self-evolution</code> 用 DSPy + GEPA 对 skill 做基于轨迹的 prompt 优化。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="slash">
|
||||
<h2><span class="num">15</span>Slash 命令中心化注册</h2>
|
||||
<p>
|
||||
所有 <code>/command</code> 定义在 <code>hermes_cli/commands.py</code> 的
|
||||
<code>COMMAND_REGISTRY</code>(AGENTS.md:137-148)。
|
||||
一次定义,多处消费:
|
||||
</p>
|
||||
<ul>
|
||||
<li>CLI 分发:<code>cli.py</code> <code>HermesCLI.process_command()</code> <span class="cite">cli.py:5192</span></li>
|
||||
<li>Gateway 分发:<code>gateway/run.py</code> + <code>GATEWAY_KNOWN_COMMANDS</code> 冻结集</li>
|
||||
<li>Gateway 帮助:<code>gateway_help_lines()</code> 自动生成 <code>/help</code></li>
|
||||
<li>Telegram 菜单:<code>telegram_bot_commands()</code> 生成 BotCommand 数组</li>
|
||||
<li>Slack 子命令:<code>slack_subcommand_map()</code> 生成 <code>/hermes sub</code> 路由</li>
|
||||
<li>自动补全:<code>COMMANDS</code> flat dict 喂给 <code>SlashCommandCompleter</code></li>
|
||||
<li>CLI 帮助:<code>COMMANDS_BY_CATEGORY</code> 喂给 <code>show_help()</code></li>
|
||||
</ul>
|
||||
<p>
|
||||
加一条 slash 命令只需要在 <code>CommandDef</code> 数组里加一行 +
|
||||
各自 <code>process_command()</code> 里加 <code>elif canonical == "mycommand":</code> 分支。
|
||||
加别名更简单 —— 只动 <code>aliases</code> 元组,<strong>所有下游自动同步</strong>
|
||||
(dispatch、help、Telegram 菜单、Slack 映射、autocomplete)。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="profile">
|
||||
<h2><span class="num">16</span>Profile 多实例支持</h2>
|
||||
<p>
|
||||
Hermes 支持<strong>多个完全隔离的实例</strong>,每个有独立 <code>HERMES_HOME</code>。核心机制:
|
||||
</p>
|
||||
<pre><code><span class="cite">hermes_cli/main.py</span> 中的 _apply_profile_override()
|
||||
在任何模块 import 之前设置 HERMES_HOME 环境变量
|
||||
→ 所有 119+ 个 get_hermes_home() 调用自动 scope 到活 profile</code></pre>
|
||||
<p>
|
||||
安全规则(AGENTS.md:376-420):
|
||||
</p>
|
||||
<ul>
|
||||
<li>禁止硬编码 <code>Path.home() / ".hermes"</code>(会破坏 profiles)</li>
|
||||
<li>用 <code>get_hermes_home()</code> 读路径,<code>display_hermes_home()</code> 展示</li>
|
||||
<li>平台 adapter 连接时用 <code>acquire_scoped_lock()</code> 防两个 profile 共用同一个 bot token</li>
|
||||
<li>Profile 列表本身<em>不</em>存在 HERMES_HOME,存在 <code>Path.home() / ".hermes" / "profiles"</code>,
|
||||
这样 <code>hermes -p coder profile list</code> 能看到所有 profile(包括非活动的)</li>
|
||||
</ul>
|
||||
<div class="callout warn">
|
||||
<strong>真实修过 5 个 bug</strong>:AGENTS.md 第 427 行提到"This was the source of 5 bugs fixed in PR #3575"
|
||||
—— Nous Research 自己踩过坑。硬编码 <code>~/.hermes</code> 会破坏 profile 隔离,
|
||||
测试里也要 mock <code>Path.home()</code> + 设 <code>HERMES_HOME</code> 才能跑 profile 测试
|
||||
(<span class="cite">tests/conftest.py</span> 的 <code>_isolate_hermes_home</code> autouse fixture)。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="v08">
|
||||
<h2><span class="num">17</span>v0.8.0 重要变化</h2>
|
||||
<p>
|
||||
来源:<code>RELEASE_v0.8.0.md</code>(2026-04-08 发布,209 PR / 82 issue 合并)。挑出最关键的:
|
||||
</p>
|
||||
<h4>A. 自我进化的证据</h4>
|
||||
<p>
|
||||
<strong>#6120</strong> — "Self-Optimized GPT/Codex Tool-Use Guidance"。原文:
|
||||
<em>"The agent diagnosed and patched 5 failure modes in GPT and Codex tool calling through
|
||||
automated behavioral benchmarking"</em>。这句话很重:<strong>agent 用自己的行为基准测试发现自己的问题,
|
||||
然后自己写了修复</strong>。不是团队加提示词,是 agent 闭环在做迭代。
|
||||
</p>
|
||||
<h4>B. 后台任务自动通知</h4>
|
||||
<p>
|
||||
<strong>#5779</strong> — <code>notify_on_complete</code>。起一个长任务(训练、测试、部署),
|
||||
agent 不需要 polling,完成时自动收通知。这是长时间任务场景的核心优化。
|
||||
</p>
|
||||
<h4>C. 闲置超时替代墙钟超时</h4>
|
||||
<p>
|
||||
<strong>#5389</strong> — 原文:<em>"Gateway and cron timeouts now track actual tool activity
|
||||
instead of wall-clock time"</em>。这个改动让"跑 20 分钟的 build 不会被超时杀掉",
|
||||
前提是它一直在有工具活动。
|
||||
</p>
|
||||
<h4>D. 实时模型切换</h4>
|
||||
<p>
|
||||
<strong>#5181 / #5742</strong> — <code>/model</code> 从 CLI、Telegram、Discord、Slack 都能切。
|
||||
Telegram 和 Discord 还给了 inline 按钮 picker。
|
||||
</p>
|
||||
<h4>E. 安全加固打包</h4>
|
||||
<p>
|
||||
<strong>#5944 + #5613 + #5629</strong> — "Security Hardening Pass":
|
||||
</p>
|
||||
<ul>
|
||||
<li>SSRF 保护统一化</li>
|
||||
<li>时序攻击(timing attack)缓解</li>
|
||||
<li>tar 遍历防护(解压恶意包)</li>
|
||||
<li>凭证泄漏防护</li>
|
||||
<li>cron 路径遍历加固</li>
|
||||
<li>跨会话隔离</li>
|
||||
<li>所有 terminal 后端的 workdir 消毒</li>
|
||||
</ul>
|
||||
<h4>F. MCP OAuth 2.1 PKCE</h4>
|
||||
<p>
|
||||
<strong>#5420</strong> — 完整的 OAuth 2.1 (带 PKCE) 接入任意 MCP server。
|
||||
<strong>#5305</strong> — 自动 OSV 漏洞库扫描 MCP 扩展包(防供应链投毒)。
|
||||
</p>
|
||||
<h4>G. Google AI Studio 原生 provider</h4>
|
||||
<p>
|
||||
<strong>#5577</strong> — Gemini 直连,不再必须过 OpenRouter。还集成了 models.dev 注册表
|
||||
自动检测任意 provider 的 context length。
|
||||
</p>
|
||||
<h4>H. 集中式日志 + 结构验证</h4>
|
||||
<p>
|
||||
<strong>#5430</strong> — <code>~/.hermes/logs/agent.log + errors.log</code>,
|
||||
<code>hermes logs</code> 命令跟踪过滤。<strong>#5426</strong> — 启动时验证 <code>config.yaml</code> 结构。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="docker">
|
||||
<h2><span class="num">18</span>Docker 运行时</h2>
|
||||
<h4>Dockerfile</h4>
|
||||
<p>
|
||||
来源:<code>Dockerfile:1-46</code>(46 行,简短)。三阶段构建:
|
||||
</p>
|
||||
<ol>
|
||||
<li><code>ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie</code> 作为 uv 源 <span class="cite">Dockerfile:1</span></li>
|
||||
<li><code>tianon/gosu:1.19-trixie</code> 作为 gosu 源 <span class="cite">Dockerfile:2</span>
|
||||
— gosu 是用来丢 root 权限的轻量替代 <code>su</code></li>
|
||||
<li><code>debian:13.4</code> 作为最终 base <span class="cite">Dockerfile:3</span></li>
|
||||
</ol>
|
||||
<p>
|
||||
关键环境变量 <span class="cite">Dockerfile:7-10</span>:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>PYTHONUNBUFFERED=1</code> — 实时刷日志</li>
|
||||
<li><code>PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright</code> —
|
||||
<em>"Store Playwright browsers outside the volume mount so the build-time install survives
|
||||
the /opt/data volume overlay at runtime"</em>。这是典型的 Docker volume 覆盖细节问题 —
|
||||
如果装在 <code>/opt/data</code> 下,runtime mount 覆盖会抹掉安装</li>
|
||||
</ul>
|
||||
<p>
|
||||
APT 包列表 <span class="cite">Dockerfile:14-15</span>:
|
||||
<code>build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps</code>
|
||||
—— 注意:<strong>这里<em>没有</em> git</strong>。这是我们昨天部署时踩到的坑,
|
||||
npm install 需要 git 但 Dockerfile 没装,导致 build 失败,需要 sed 补丁。
|
||||
</p>
|
||||
<h4>entrypoint.sh</h4>
|
||||
<p>
|
||||
<span class="cite">docker/entrypoint.sh:1-64</span>:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>root → hermes 降权</strong>(<span class="cite">docker/entrypoint.sh:11-30</span>):
|
||||
如果以 root 启动,先检查并重映射 hermes 用户的 UID/GID(通过 <code>HERMES_UID</code>/<code>HERMES_GID</code>
|
||||
环境变量),fix 数据卷所有权,然后 <code>exec gosu hermes "$0" "$@"</code></li>
|
||||
<li><strong>目录结构初始化</strong>(<span class="cite">docker/entrypoint.sh:42</span>):
|
||||
<code>mkdir -p $HERMES_HOME/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home}</code>
|
||||
—— 最后那个 <code>home</code> 很巧妙,注释解释:<em>"Without it those tools [git, ssh, gh, npm] write
|
||||
to /root which is ephemeral and shared across profiles"</em></li>
|
||||
<li><strong>首次启动 seed 默认文件</strong>:如果 <code>.env</code> / <code>config.yaml</code> / <code>SOUL.md</code>
|
||||
不存在,从 <code>INSTALL_DIR</code> 的 example 文件复制</li>
|
||||
<li><strong>同步 bundled skills</strong>(<span class="cite">docker/entrypoint.sh:60-62</span>):
|
||||
<code>python3 $INSTALL_DIR/tools/skills_sync.py</code> 基于 manifest,保留用户编辑</li>
|
||||
<li>最后 <code>exec hermes "$@"</code></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ============================== -->
|
||||
<section id="takeaways">
|
||||
<h2><span class="num">19</span>设计洞察(主观)</h2>
|
||||
<p>
|
||||
读完之后几个主观结论 —— 不是源码里写的,是我看源码之后的归纳:
|
||||
</p>
|
||||
<h4>1. 模块化的纪律比任何单点技术重要</h4>
|
||||
<p>
|
||||
Hermes 能支持 22 个消息平台 + 6 种终端后端 + 11 个 LLM provider + 78 个 skill,不是因为每个都写得多好,
|
||||
而是因为<strong>工具注册表 + 抽象基类 + slash 命令中心化</strong>让新增成本低到可接受。
|
||||
看 <code>BasePlatformAdapter</code>、<code>BaseEnvironment</code>、<code>ToolRegistry</code>、
|
||||
<code>COMMAND_REGISTRY</code> 这 4 个抽象,就明白为什么 v0.8 能在一个周期合并 209 个 PR。
|
||||
</p>
|
||||
<h4>2. 真正的性能优化都在"不重建"上</h4>
|
||||
<p>
|
||||
Gateway 的 <code>_agent_cache</code>、<code>_tool_loop</code> 的持久 loop、Anthropic 的 prompt cache 保护……
|
||||
所有优化都是"能不重建就不重建"。这让 Hermes 对 prompt caching 友好的 LLM(Anthropic)比对不友好的
|
||||
便宜 10 倍。这不是理论 —— <code>AGENTS.md:339-347</code> 把这个原则写成<em>铁律</em>。
|
||||
</p>
|
||||
<h4>3. 它把"agent 能跑哪"和"agent 用啥模型"解耦了</h4>
|
||||
<p>
|
||||
传统 agent 把两件事绑一起。Hermes 的终端后端和 LLM provider 是两个独立的抽象:
|
||||
你可以把 agent 放在 $5 VPS 上,代码执行转发到 Modal 上的 GPU 实例,模型调 Anthropic Opus,
|
||||
三者都可以独立换。这个解耦是真正的产品差异化。
|
||||
</p>
|
||||
<h4>4. 它承认自己是"一个会用工具的 LLM",而不是"有 LLM 的应用"</h4>
|
||||
<p>
|
||||
主循环本质就是 <code>while: LLM call → handle tool calls → repeat</code>。
|
||||
Hermes 的所有复杂度都在<em>工具生态、平台适配、状态持久化、安全边界</em>上 ——
|
||||
agent loop 本身 2000 行里大部分是错误处理、重试、审批、中断、预算、流式回调,而不是"AI 逻辑"。
|
||||
这符合"agent 是一个模式,不是一种算法"的设计哲学。
|
||||
</p>
|
||||
<h4>5. 自进化不是魔法,是 DSPy + 基准测试闭环</h4>
|
||||
<p>
|
||||
v0.8 的 "Self-Optimized GPT/Codex Tool-Use Guidance"(#6120)揭示了自进化的实现路径:
|
||||
跑 benchmark → 识别失败模式 → 用 DSPy/GEPA 优化 prompt → 回灌。
|
||||
独立仓库 <code>hermes-agent-self-evolution</code> 印证这点。
|
||||
换句话说,<strong>自进化是工程,不是玄学</strong>。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
· Hermes Agent v0.8.0 源码解析 · 2026-04-13 ·<br>
|
||||
源码取证:<code>~/Projects/research/20260413-hermes-source-analysis/source/</code>(git commit <code>d6785dc</code>)·<br>
|
||||
上游仓库:<a href="https://github.com/NousResearch/hermes-agent" target="_blank" rel="noopener">github.com/NousResearch/hermes-agent</a> ·
|
||||
官方文档:<a href="https://hermes-agent.nousresearch.com/docs/" target="_blank" rel="noopener">hermes-agent.nousresearch.com/docs</a> ·
|
||||
运行实例:<a href="https://hermes.milejoy.com" target="_blank" rel="noopener">hermes.milejoy.com</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user