.. _build-your-own:
构建你自己的 Agent:从零开始的实践指南
===========================================
这是全书的压轴章节。在前面的章节中,我们深入分析了 Hermes Agent
的架构设计,提炼出了通用的工程模式和教训。现在,让我们把这些知识
转化为实践——从头构建一个生产级 Agent。
本章不是 Hermes 的使用教程。我们将基于从 Hermes 学到的经验,
设计一个更干净的架构,并提供可直接使用的代码模板。
最小可行 Agent:200 行 Python
-------------------------------
在深入复杂架构之前,让我们先实现一个最小可行 Agent(MVA)。
它包含了 Agent 的核心循环,但没有 Hermes 的复杂性。
理解这个 MVA 是理解任何 Agent 系统的基础。
.. mermaid::
:name: mva-core-loop
:caption: 最小可行 Agent 核心循环
flowchart LR
A["系统提示词"] --> B["API 调用"]
C["用户消息"] --> B
B --> D{"有工具调用?"}
D -->|是| E["执行工具"]
E --> B
D -->|否| F["返回响应"]
classDef start fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a
classDef warn fill:#fef9c3,stroke:#ca8a04,color:#854d0e
classDef success fill:#dcfce7,stroke:#16a34a,color:#166534
class A,C start
class D warn
class E success
class F success
完整代码如下:
.. code-block:: python
"""minimal_agent.py — A 200-line AI Agent with tool calling.
Demonstrates the core observe-think-act loop that every Agent needs:
1. Build system prompt with available tools
2. Call LLM API
3. Parse response for tool calls
4. Execute tools and feed results back
5. Repeat until model returns a final text response
"""
import json
import os
import subprocess
from typing import Any
from openai import OpenAI
# ── Tool registry (simplified self-registration pattern) ──────────
_TOOLS: dict[str, dict[str, Any]] = {}
def register_tool(name: str, description: str, parameters: dict, handler):
"""Register a tool with its schema and handler function."""
_TOOLS[name] = {
"schema": {
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": parameters,
},
},
"handler": handler,
}
def get_tool_schemas() -> list[dict]:
"""Return all registered tool schemas for the API call."""
return [t["schema"] for t in _TOOLS.values()]
def dispatch_tool(name: str, arguments: dict) -> str:
"""Execute a tool by name, return result as JSON string."""
tool = _TOOLS.get(name)
if not tool:
return json.dumps({"error": f"Unknown tool: {name}"})
try:
result = tool["handler"](arguments)
return json.dumps(result) if not isinstance(result, str) else result
except Exception as e:
return json.dumps({"error": f"Tool failed: {e}"})
# ── Built-in tools ────────────────────────────────────────────────
def _handle_read_file(args: dict) -> str:
path = args.get("path", "")
try:
return open(path).read()[:50_000] # Truncate large files
except FileNotFoundError:
return json.dumps({"error": f"File not found: {path}"})
def _handle_write_file(args: dict) -> str:
path, content = args.get("path", ""), args.get("content", "")
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "w") as f:
f.write(content)
return json.dumps({"ok": True, "bytes_written": len(content)})
def _handle_terminal(args: dict) -> str:
cmd = args.get("command", "")
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True, timeout=30,
)
output = result.stdout + result.stderr
return output[:20_000] if output else "(no output)"
# Self-register tools (mirrors Hermes pattern)
register_tool(
"read_file", "Read a file from disk.",
{"type": "object", "properties": {"path": {"type": "string"}},
"required": ["path"]},
_handle_read_file,
)
register_tool(
"write_file", "Write content to a file.",
{"type": "object",
"properties": {"path": {"type": "string"}, "content": {"type": "string"}},
"required": ["path", "content"]},
_handle_write_file,
)
register_tool(
"terminal", "Run a shell command.",
{"type": "object", "properties": {"command": {"type": "string"}},
"required": ["command"]},
_handle_terminal,
)
# ── Error classification (simplified from Hermes's 11-way) ────────
def classify_error(error: Exception) -> str:
"""Return recovery strategy: 'retry', 'compress', or 'abort'."""
status = getattr(error, "status_code", None)
msg = str(error).lower()
if status == 429 or "rate limit" in msg:
return "retry"
if status in (500, 502, 503):
return "retry"
if "context" in msg and ("length" in msg or "token" in msg):
return "compress"
if status in (401, 403):
return "abort"
return "retry"
# ── Core Agent Loop ───────────────────────────────────────────────
class MinimalAgent:
"""A minimal but functional AI Agent with tool calling."""
def __init__(self, model: str = "gpt-4o", max_iterations: int = 30):
self.client = OpenAI()
self.model = model
self.max_iterations = max_iterations
def run(self, user_message: str, system_prompt: str = None) -> str:
"""Run the observe-think-act loop until completion."""
messages = []
# Build system prompt
if system_prompt is None:
system_prompt = (
"You are a helpful AI assistant with access to tools. "
"Use tools when needed to complete tasks. "
"Always provide a final text response when done."
)
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": user_message})
# Main loop
for iteration in range(self.max_iterations):
try:
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
tools=get_tool_schemas(),
)
except Exception as e:
strategy = classify_error(e)
if strategy == "retry":
continue # Simplified: no backoff in MVA
if strategy == "abort":
return f"Fatal error: {e}"
# compress — not implemented in MVA
return f"Context too large: {e}"
choice = response.choices[0]
assistant_msg = choice.message
# Append assistant response to history
messages.append(assistant_msg.model_dump())
# Check: final text response (no tool calls)
if not assistant_msg.tool_calls:
return assistant_msg.content or "(empty response)"
# Execute tool calls
for tool_call in assistant_msg.tool_calls:
fn_name = tool_call.function.name
try:
fn_args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
fn_args = {}
result = dispatch_tool(fn_name, fn_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
return "Maximum iterations reached without completion."
# ── Entry point ───────────────────────────────────────────────────
if __name__ == "__main__":
agent = MinimalAgent()
print("Minimal Agent ready. Type your message (Ctrl+C to quit):")
while True:
try:
user_input = input("\n> ")
if user_input.strip():
response = agent.run(user_input)
print(f"\n{response}")
except KeyboardInterrupt:
print("\nGoodbye!")
break
这个 200 行的 Agent 实现了以下核心功能:
- **工具自注册** :简化版的 Hermes 自注册模式,工具通过
``register_tool()`` 声明 schema 和 handler。
- **核心循环** :observe-think-act 循环,包含 API 调用、响应解析、
工具执行。
- **错误分类** :简化版的错误分类器,将错误映射到三种恢复策略。
- **迭代预算** :通过 ``max_iterations`` 限制循环次数。
它缺少 Hermes 的以下功能——这些是后续章节要讨论的:
- 流式响应
- 多提供商支持
- 上下文压缩
- 并行工具执行
- 会话持久化
- 认证与安全
架构选型决策
--------------
当你准备超越最小可行 Agent,构建一个更完整的系统时,
你面临的第一个问题不是"写什么代码",而是"做什么选择"。
单块 vs 微服务
~~~~~~~~~~~~~~~~
.. mermaid::
:name: monolith-vs-microservice
:caption: 单块 vs 微服务架构选型
flowchart TD
Q{"单块还是微服务?"}
Q -->|"单用户 / 小团队"| M["单块架构
Hermes 的选择"]
Q -->|"多租户 / 大规模"| S["微服务架构"]
M --> M1["优点:部署简单
调试方便
延迟最低"]
M --> M2["缺点:单点故障
难以水平扩展"]
S --> S1["优点:独立扩展
故障隔离
技术异构"]
S --> S2["缺点:运维复杂
网络延迟
分布式事务"]
classDef warn fill:#fef9c3,stroke:#ca8a04,color:#854d0e
classDef success fill:#dcfce7,stroke:#16a34a,color:#166534
classDef fail fill:#fee2e2,stroke:#dc2626,color:#991b1b
classDef info fill:#f1f5f9,stroke:#64748b,color:#334155
class Q warn
class M,S info
class M1,S1 success
class M2,S2 fail
**Hermes 的选择** :单块架构(monolith)。整个 Agent 运行在单个进程中。
这个选择是正确的,原因如下:
- Hermes 的典型部署场景是单用户 CLI 或网关(单进程多会话)。
没有多租户需求。
- Agent 的核心循环(API 调用 → 工具执行 → 消息更新)是严格的顺序依赖,
拆分为微服务不会带来性能收益。
- 单进程内的函数调用比跨服务的 RPC 快几个数量级。
在 Agent 的紧密循环中,这个延迟差异是显著的。
**何时选择微服务** :
- 需要 Agent 作为服务同时服务成百上千用户。
- 工具执行需要隔离(不同用户的代码不能在同一进程运行)。
- 不同组件有不同的扩展需求(如工具执行需要大量 CPU,
而 API 调用需要大量网络 I/O)。
同步 vs 异步
~~~~~~~~~~~~~~
**Hermes 的选择** :同步主循环 + 异步桥接。
Hermes 的主循环(``run_conversation()``)是完全同步的。
异步操作(如 httpx 异步客户端)通过 ``_run_async()`` 桥接到同步上下文。
这个看似"不优雅"的选择有其深刻的原因:
1. **调试友好** :同步代码的调用栈是线性的,异常直接传播。
异步代码的异常传播跨越多个协程,调试难度显著增加。
2. **避免事件循环复杂性** :Python 的 ``asyncio`` 在多线程环境中
容易产生微妙的问题(如事件循环所有权、线程安全)。
Hermes 通过"只在需要时桥接到异步"的策略,将异步的复杂性
限制在工具执行层。
3. **工具兼容性** :许多 Python 库(如 subprocess、文件 I/O)
是同步的。使用异步主循环意味着每个同步调用都需要
``loop.run_in_executor()`` ,增加样板代码。
**何时选择全异步** :
- 网关服务需要同时处理数百个并发连接。
- 大部分操作是 I/O 密集型(网络请求、文件读写)。
- 团队对 asyncio 有深入理解。
数据库:SQLite vs PostgreSQL
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Hermes 的选择** :SQLite + WAL 模式。
SQLite 的优势在于零配置、零运维。对于 Hermes 的场景
(单机部署、中等写入频率、读多写少),SQLite 完全足够。
WAL 模式解决了基本的并发读写问题。
**何时选择 PostgreSQL** :
- 多实例部署(多个网关进程共享数据库)。
- 需要复杂的查询(如全文搜索、聚合分析)。
注意 Hermes 通过 SQLite FTS5 实现了全文搜索,
但 FTS5 的功能比 PostgreSQL 的全文搜索有限。
- 写入频率极高(SQLite 的写入吞吐量受 WAL checkpoint 影响)。
通信协议:JSON-RPC vs gRPC vs REST
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Hermes 的选择** :JSON-RPC over stdin/stdout(TUI 网关)。
TUI(Terminal User Interface)与 Agent 之间的通信使用 JSON-RPC,
通过进程的 stdin/stdout 传输。这个选择是因为:
- **简单性** :JSON-RPC 比 gRPC 简单得多,不需要 .proto 文件和代码生成。
- **进程隔离** :TUI 和 Agent 可以是不同的进程,
stdin/stdout 是最简单的跨进程通信方式。
- **可调试性** :JSON-RPC 消息是人类可读的,
方便调试和日志分析。
**何时选择 gRPC** :
- 高频调用、低延迟需求。
- 强类型接口定义重要。
- 需要双向流式通信。
**何时选择 REST** :
- 面向外部 API(第三方集成)。
- 需要通过 HTTP 代理/防火墙。
工具系统设计
--------------
从 Hermes 的经验中,我们可以提炼出设计工具系统的四步法。
第一步:定义工具注册接口
~~~~~~~~~~~~~~~~~~~~~~~~~~
工具注册接口应该包含以下信息:
- **name** :工具的唯一标识符。
- **description** :对 LLM 的描述(这直接影响模型是否正确选择工具)。
- **parameters** :JSON Schema 格式的参数定义。
- **handler** :实际的执行函数。
- **availability_check** (可选):运行时检查工具是否可用。
- **max_result_size** (可选):工具返回结果的最大大小限制。
第二步:实现自注册模式
~~~~~~~~~~~~~~~~~~~~~~~~
每个工具文件在模块级别调用注册函数。使用 Hermes 的 AST 预检查技巧
来高效发现需要导入的文件:
.. code-block:: python
import ast
from pathlib import Path
def discover_tools(tools_dir: Path):
"""Discover and import tool modules that register themselves."""
for path in sorted(tools_dir.glob("*.py")):
if path.name.startswith("_"):
continue
if not _has_register_call(path):
continue
importlib.import_module(f"tools.{path.stem}")
def _has_register_call(path: Path) -> bool:
"""Check if a module contains a top-level register() call."""
tree = ast.parse(path.read_text())
return any(
isinstance(stmt, ast.Expr)
and isinstance(stmt.value, ast.Call)
and isinstance(stmt.value.func, ast.Name)
and stmt.value.func.id == "register"
for stmt in tree.body
)
第三步:参数验证与类型强制转换
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
LLM 返回的工具参数是 JSON 字符串,需要解析和验证。
Hermes 的经验表明,以下类型转换是必要的:
- **字符串 "true"/"false"** → 布尔值(模型经常返回字符串而非布尔值)。
- **数字字符串** → int 或 float(JSON schema 说 "type": "number",
但模型可能返回 "42" 而非 42)。
- **逗号分隔字符串** → 列表(模型可能将列表参数序列化为单个字符串)。
第四步:并行执行策略
~~~~~~~~~~~~~~~~~~~~~~
基于 Hermes 的三级并行策略:
.. code-block:: python
NEVER_PARALLEL = {"clarify", "user_input"}
PARALLEL_SAFE = {"web_search", "read_file", "list_files"}
PATH_SCOPED = {"read_file", "write_file"}
def should_parallelize(tool_calls):
names = [tc.name for tc in tool_calls]
if any(n in NEVER_PARALLEL for n in names):
return False
paths = []
for tc in tool_calls:
if tc.name not in PARALLEL_SAFE and tc.name not in PATH_SCOPED:
return False
if tc.name in PATH_SCOPED:
path = Path(tc.args.get("path", ""))
if any(paths_overlap(path, p) for p in paths):
return False
paths.append(path)
return len(tool_calls) > 1
上下文管理策略
----------------
上下文管理是 Agent 系统中最棘手的问题之一。以下是基于 Hermes 经验的指导原则。
何时压缩
~~~~~~~~~~
不要等到 API 返回上下文溢出错误再压缩。Hermes 的策略是**预压缩** :
在每轮对话开始前,估算当前消息的 token 数。如果超过阈值
(通常是上下文窗口的 70-80%),主动触发压缩。
预压缩的好处是避免了一次注定失败的 API 调用(省时间、省钱),
以及避免了将上下文溢出错误与其他错误混淆的风险。
如何摘要
~~~~~~~~~~
Hermes 的摘要模板包含以下关键部分:
1. **免责声明** :明确告知模型"这是历史摘要,不是当前指令"。
这防止模型执行摘要中提到的已完成任务。
2. **已解决的问题** :列出已经处理完毕的请求,避免重复执行。
3. **待解决的问题** :列出尚未完成的任务,确保模型知道从哪里继续。
4. **当前状态** :文件系统状态、配置变更等,帮助模型理解环境。
5. **活跃任务** :明确标注当前应该继续的任务。
Token 预算
~~~~~~~~~~~~
为摘要分配足够的 token,但不浪费:
.. code-block:: python
# Hermes 的公式:压缩内容的 20%,但有上限和下限
summary_tokens = min(
max(compressed_tokens * 0.20, 2000),
12_000,
)
缓存策略
~~~~~~~~~~
Prompt 缓存的关键原则:
1. **系统提示词必须稳定** :在会话内不要修改系统提示词。
如果必须修改(如加载新记忆),将变更放在用户消息中。
2. **缓存断点位置** :系统提示词 + 最近几条消息。
Anthropic 限制 4 个断点,合理分配。
3. **连续会话的一致性** :当从数据库恢复会话时,
使用存储的系统提示词而非重新构建,确保缓存命中。
多 Provider 支持
------------------
如果你需要支持多个 LLM 提供商,以下是基于 Hermes 经验的指导。
适配器模式实现
~~~~~~~~~~~~~~~~
为每个提供商实现一个适配器,将提供商特有的 API 转换为统一的内部格式:
.. code-block:: python
from types import SimpleNamespace
def normalize_openai_response(response) -> SimpleNamespace:
"""Convert OpenAI response to unified format."""
choice = response.choices[0]
msg = choice.message
return SimpleNamespace(
content=msg.content,
tool_calls=msg.tool_calls,
finish_reason=choice.finish_reason,
)
def normalize_anthropic_response(response) -> SimpleNamespace:
"""Convert Anthropic response to unified format."""
content_blocks = response.content
text = ""
tool_calls = []
for block in content_blocks:
if block.type == "text":
text += block.text
elif block.type == "tool_use":
tool_calls.append(block)
return SimpleNamespace(
content=text or None,
tool_calls=tool_calls or None,
finish_reason=response.stop_reason,
)
``SimpleNamespace`` 的选择是有意为之的——它轻量、灵活、
不需要定义数据类,非常适合作为适配器层的中间格式。
客户端缓存考量
~~~~~~~~~~~~~~~~
不同提供商的 SDK 有不同的客户端生命周期管理。
- OpenAI SDK 的 ``OpenAI()`` 客户端维护连接池,可以复用。
- Anthropic SDK 类似,但有额外的认证刷新逻辑。
- Bedrock 使用 boto3 session,有自己的凭证链。
关键原则:**不要为每次 API 调用创建新的客户端实例** 。
客户端创建涉及连接建立和 TLS 握手,频繁创建会显著增加延迟。
Hermes 通过懒初始化和客户端缓存来避免这个问题。
错误恢复跨提供商
~~~~~~~~~~~~~~~~~~
当主提供商失败时,Hermes 可以自动回退到备用提供商。
跨提供商的错误恢复需要考虑:
- 不同提供商的模型能力不同。回退时可能需要调整提示词
(如移除 Anthropic 特有的 thinking 指令)。
- 上下文格式可能不兼容。OpenAI 的消息格式与 Anthropic 有细微差异。
- 工具 schema 格式需要转换(如 Anthropic 不支持某些 JSON Schema 特性)。
Hermes 通过 ``api_mode`` 分支来处理这些差异。
更优雅的做法是将差异完全封装在适配器层。
部署考量
----------
CLI vs Web vs API
~~~~~~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 20 40 40
* - 部署模式
- 优点
- 缺点
* - CLI
- 开发者友好、零部署、本地访问
- 单用户、无远程访问
* - Web UI
- 用户友好、远程访问
- 需要 Web 服务器、认证、状态管理
* - API 服务
- 可编程集成、可水平扩展
- 需要认证、限流、监控
Hermes 同时支持 CLI 模式(直接在终端运行)和网关模式
(通过 RPC 服务多平台用户)。两种模式共享同一个 ``AIAgent`` 核心,
差异只在"谁调用 ``run_conversation()``"和"回调如何传递"。
进程隔离:SlashWorker 模式
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Hermes 的 TUI 网关使用了一个精巧的进程隔离模式来处理斜杠命令:
.. mermaid::
:name: slash-worker-isolation
:caption: SlashWorker 进程隔离模式
flowchart LR
A["TUI 网关
(主进程)"] -->|"JSON-RPC
via stdin/stdout"| B["SlashWorker
(子进程)"]
B -->|"持久化
HermesCLI"| C["会话状态"]
A -->|"直接调用"| D["Agent 核心
(AIAgent)"]
classDef start fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a
classDef info fill:#f1f5f9,stroke:#64748b,color:#334155
classDef success fill:#dcfce7,stroke:#16a34a,color:#166534
class A start
class B,D info
class C success
SlashWorker(``tui_gateway/slash_worker.py``)是一个持久化的子进程,
负责处理斜杠命令(如 ``/config`` 、``/model`` 、``/tools``)。
它在启动时创建一个 ``HermesCLI`` 实例,然后通过 stdin/stdout
的 JSON-RPC 协议接收命令、返回结果。
这个设计的好处是:
- **隔离** :斜杠命令的崩溃不影响主网关进程。
- **持久化** :``HermesCLI`` 实例在整个会话生命周期内保持,
避免了每次命令都重新初始化的开销。
- **简单性** :stdin/stdout 通信比 socket 或共享内存简单得多。
会话持久化
~~~~~~~~~~~~
Hermes 的会话持久化策略:
1. **SQLite + WAL 模式** :多进程安全,零运维。
2. **批量写入** :消息不是逐条写入,而是在 turn 结束时批量 flush。
3. **压缩触发会话分裂** :上下文压缩会创建新的会话记录,
通过 ``parent_session_id`` 链接。
一个完整的示例架构
--------------------
综合以上所有讨论,以下是一个推荐的生产级 Agent 架构:
.. mermaid::
:name: recommended-architecture
:caption: 推荐的生产级 Agent 完整架构
flowchart TB
subgraph "用户界面层"
CLI["CLI / TUI"]
WEB["Web UI"]
API["API Gateway"]
end
subgraph "Agent 核心"
LOOP["Agent Loop
observe-think-act"]
BUDGET["Budget Manager
迭代 / Token / 结果"]
ERR["Error Classifier
错误分类与恢复"]
end
subgraph "工具系统"
REG["Tool Registry
自注册 + AST 发现"]
DISPATCH["Tool Dispatcher
并行 / 串行策略"]
MCP["MCP Client
动态工具发现"]
end
subgraph "上下文管理"
COMPRESS["Context Compressor
预压缩 + LLM 摘要"]
CACHE["Prompt Cache
system_and_3"]
MEMORY["Memory Manager
长期记忆"]
end
subgraph "Provider 适配层"
OA["OpenAI Adapter"]
AN["Anthropic Adapter"]
BR["Bedrock Adapter"]
CRED["Credential Pool
轮换 + 冷却"]
end
subgraph "持久化层"
DB["SQLite / PostgreSQL
WAL 模式"]
FS["文件系统
工具结果持久化"]
end
CLI --> LOOP
WEB --> LOOP
API --> LOOP
LOOP --> BUDGET
LOOP --> ERR
LOOP --> REG
REG --> DISPATCH
REG --> MCP
LOOP --> COMPRESS
COMPRESS --> CACHE
LOOP --> MEMORY
LOOP --> OA
LOOP --> AN
LOOP --> BR
OA --> CRED
AN --> CRED
BR --> CRED
LOOP --> DB
DISPATCH --> FS
classDef start fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a
classDef info fill:#f1f5f9,stroke:#64748b,color:#334155
classDef warn fill:#fef9c3,stroke:#ca8a04,color:#854d0e
classDef success fill:#dcfce7,stroke:#16a34a,color:#166534
class CLI,WEB,API start
class LOOP,BUDGET,ERR warn
class REG,DISPATCH,MCP info
class COMPRESS,CACHE,MEMORY info
class OA,AN,BR,CRED info
class DB,FS success
数据流:从用户到 LLM 再回来
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mermaid::
:name: full-data-flow-sequence
:caption: 数据流:从用户到 LLM 再回来
sequenceDiagram
autonumber
participant U as 用户
participant A as Agent Loop
participant P as Provider Adapter
participant L as LLM API
participant T as Tool Registry
participant D as 数据库
U->>A: 用户消息
A->>D: 加载会话历史
A->>A: 构建系统提示词
A->>A: 预压缩检查
A->>P: API 调用 (带工具 schema)
P->>L: HTTP 请求
L-->>P: 流式响应
P-->>A: 归一化响应
alt 有工具调用
A->>T: dispatch(tool_name, args)
T-->>A: 工具结果
A->>A: 追加消息到历史
A->>P: API 调用 (带工具结果)
P->>L: HTTP 请求
L-->>P: 响应
P-->>A: 归一化响应
end
A->>D: 持久化消息
A-->>U: 最终响应
推荐技术栈
~~~~~~~~~~~~
基于 Hermes 的经验和教训,以下是我们推荐的技术栈:
.. list-table::
:header-rows: 1
:widths: 25 35 40
* - 组件
- 推荐选择
- 理由
* - 语言
- Python 3.11+
- LLM SDK 生态最成熟
* - LLM SDK
- OpenAI SDK + provider adapters
- 统一接口 + 差异封装
* - 数据库
- SQLite (单机) / PostgreSQL (分布式)
- WAL 模式解决并发
* - 异步框架
- 同步主循环 + 按需桥接
- 调试友好,避免 asyncio 陷阱
* - 工具注册
- 自注册 + AST 发现
- 零配置扩展
* - 错误处理
- 分类器管线 + 抖动退避
- 精确恢复策略
* - 上下文管理
- 预压缩 + LLM 摘要 + Prompt 缓存
- 75%+ 输入 token 节省
* - 部署
- 单进程 (CLI) / JSON-RPC (网关)
- 简单可靠
从原型到生产的路线图
----------------------
以下是将 MVA 发展为生产级 Agent 的建议路线图:
**阶段一:核心功能** (1-2 周)
- 实现基本的工具注册和调度。
- 添加错误分类和重试逻辑。
- 实现基本的会话持久化。
**阶段二:稳定性** (2-4 周)
- 添加上下文压缩。
- 实现预压缩检查。
- 添加多提供商支持(至少两个提供商)。
- 实现凭证池和轮换。
**阶段三:性能** (1-2 周)
- 实现并行工具执行。
- 添加 Prompt 缓存。
- 优化工具结果大小管理(三层预算)。
**阶段四:可扩展性** (2-4 周)
- 添加插件/钩子系统。
- 实现 MCP 工具集成。
- 添加皮肤/主题系统。
**阶段五:运营** (持续)
- 监控和告警。
- 日志分析和调试工具。
- 性能基准测试和回归检测。
每个阶段都建立在前一个阶段的基础上,且每个阶段结束时
都有一个可运行的、比之前更健壮的系统。这种增量式的方法
降低了"大爆炸失败"的风险,也让你在开发过程中持续获得反馈。
最后的话
----------
构建一个生产级 Agent 的核心挑战不是算法或架构,
而是工程——处理边界情况、做设计权衡、在混乱中保持可靠。
Hermes Agent 的 12,000 行代码是一个宝贵的工程学习素材。
它不是完美的——它有上帝类、分散的提供商 Hack、缓存失效的复杂性。
但正是这些不完美让它更有教育意义:它展示了真实系统如何在
时间压力、需求变更和资源约束下演进。
如果你从这本书中只带走一个教训,那就是:
**简单的、可靠的、可调试的方案,永远优于优雅的、复杂的、
难以理解的方案。** Hermes 的同步主循环、"简单粗暴"的错误消息匹配、
基于 AST 的工具发现——这些都不是最"优雅"的设计,
但它们在实际运行中证明了有效性。
在 Agent 的世界里,工程智慧就是知道什么时候选择简单。