Chapter 14

动手实践与设计哲学

从零跑通 Hermes、写一个 Plugin、写一个 Skill、写一个 MCP server。 然后总结这本书带走的 10 条工程原则。

本章约 6,000 字 阅读 + 实操 ~60 分钟 关键词:installation · plugin · skill · MCP · philosophy

前 13 章都是。最后一章是动手。读再多代码不动手,知识是漂浮的。 这一章带你跑通四件事:装 Hermes、写一个工具 Plugin、写一个 Skill、写一个 MCP server。 完成这四件你的"工程肌肉记忆"才算建立。然后我们用 10 条原则做收尾。

14.1安装 Hermes

一键脚本(macOS / Linux / WSL2)

curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash
source ~/.bashrc            # 或 ~/.zshrc

脚本会装:uv、Python 3.11、Node.js、ripgrep、ffmpeg、Hermes 本身。安装到 ~/.hermes/。 往 ~/.local/bin/ 加一个 hermes 软链。

从源码(推荐——学习用)

git clone https://github.com/NousResearch/hermes-agent.git
cd hermes-agent
./setup-hermes.sh
# 装 uv、建 .venv、pip install -e .[all]、软链到 ~/.local/bin/hermes
./hermes                    # wrapper 自动激活 venv

设置 Provider

第一次启动 hermes setup 走 wizard。或者手动:

# ~/.hermes/.env
ANTHROPIC_API_KEY=sk-ant-...
# 或 OpenRouter(覆盖 200+ 模型):
OPENROUTER_API_KEY=sk-or-...
# 或 DeepSeek(便宜大碗):
DEEPSEEK_API_KEY=sk-...
# ~/.hermes/config.yaml
model:
  provider: anthropic
  model: claude-opus-4.7
  fallback_provider: openrouter
  fallback_model: anthropic/claude-sonnet-4.6

tools:
  cli:
    enabled: [web, file, browser, terminal, memory, skills, vision]

首次对话

hermes
# 进入交互

> 列出当前目录下 .py 文件,统计总行数

🛠 terminal: ls *.py
📋 todo: ...
🛠 terminal: wc -l *.py
当前目录下有 23 个 .py 文件,总行数 14,892。

常用 slash 命令

/help
命令作用
/new新 session,清空历史
/model切模型
/tools看 / 改启用的 toolset
/insights看 token 用量 + cache 命中率
/skills看已装 skill / 搜 hub
/memory看 / 写 memory
/help所有命令

14.2实战 1:写一个 Plugin 工具

目标:给 Hermes 加一个 fortune_cookie 工具,让它抽一句励志格言。 走 Plugin 路线(不动主仓库)。

步骤 1:建目录

mkdir -p ~/.hermes/plugins/fortune

步骤 2:写 plugin.yaml

~/.hermes/plugins/fortune/plugin.yaml
name: fortune
version: "0.1.0"
description: "Random programming fortune quotes."
author: "your name"
license: MIT

步骤 3:写 __init__.py

~/.hermes/plugins/fortune/__init__.py
import json
import random

FORTUNES = [
    "The best way to predict the future is to invent it. — Alan Kay",
    "Premature optimization is the root of all evil. — Donald Knuth",
    "Make it work, make it right, make it fast. — Kent Beck",
    "Talk is cheap. Show me the code. — Linus Torvalds",
    "Simplicity is prerequisite for reliability. — Edsger Dijkstra",
    "Programming is the art of telling another human what one wants the computer to do. — Donald Knuth",
]


def fortune_handler(args, **kw):
    """Return a random fortune quote."""
    quote = random.choice(FORTUNES)
    return json.dumps({"quote": quote})


FORTUNE_SCHEMA = {
    "name": "fortune_cookie",
    "description": (
        "Return a random programming-themed fortune quote. "
        "Use when the user explicitly asks for inspiration, "
        "a quote, or 'something motivational about coding'. "
        "DO NOT use unprompted — let the user invoke it."
    ),
    "parameters": {
        "type": "object",
        "properties": {},
        "required": [],
    },
}


def register(ctx):
    """Plugin entry point — called by Hermes at load time."""
    ctx.register_tool(
        name="fortune_cookie",
        toolset="fortune",
        schema=FORTUNE_SCHEMA,
        handler=fortune_handler,
        emoji="🥠",
    )

步骤 4:启用 toolset

hermes tools enable fortune
# 或编辑 config.yaml 的 tools.cli.enabled 列表加 "fortune"

步骤 5:试

hermes
> 给我抽一句关于编程的格言

🥠 fortune_cookie() → {"quote": "Talk is cheap. Show me the code. — Linus Torvalds"}

Linus Torvalds 说:"Talk is cheap. Show me the code."
这是他在 LKML 上著名的回应——意思是不要光讨论,把实际代码拿出来。
完成 你刚写了你的第一个 Hermes 工具。没改主仓库一行代码。 下一步可以试做:(1) weather_lookup——调 wttr.in; (2) github_star——给一个 repo 点 star; (3) image_caption——调 Claude vision 给图片打描述。

14.3实战 2:写一个 Skill

目标:让 Agent 知道"按 Conventional Commits 格式写 commit message"。

步骤 1:建目录

mkdir -p ~/.hermes/skills/git/conventional-commit

步骤 2:写 SKILL.md

~/.hermes/skills/git/conventional-commit/SKILL.md
---
name: conventional-commit
description: "Format git commit messages as Conventional Commits."
version: 1.0.0
author: "your name"
license: MIT
metadata:
  hermes:
    tags: [git, commit, convention]
    created_by: user
---

# Conventional Commit Skill

When the user asks for a commit message, output **one** message following
the Conventional Commits format:

\`\`\`
<type>(<scope>): <subject in imperative mood, lowercase, no period>

<optional body explaining WHY, wrapped at 72 cols>

<optional footer: BREAKING CHANGE / refs / issue links>
\`\`\`

## When to Use

- User asks for "a commit message"
- User asks to "write a commit"
- User asks "what should the commit be?"

## When NOT to Use

- User wants a PR title (use a similar but different convention)
- User asks for a changelog entry (different format)
- Project explicitly uses other convention (check CONTRIBUTING.md first)

## Types

| type     | use case                              |
|----------|---------------------------------------|
| feat     | new user-facing functionality         |
| fix      | bug fix                               |
| refactor | code change with no behavior delta    |
| docs     | documentation only                    |
| chore    | tooling, build, deps                  |
| test     | adding or fixing tests                |
| perf     | performance improvement               |
| ci       | CI configuration changes              |
| build    | build system / dep changes            |

## Subject Rules

- **max 50 chars**
- **imperative mood**: "add X" not "adds X" or "added X"
- **no period at end**
- **lowercase**

## Examples

\`\`\`
feat(auth): add OAuth2 PKCE flow

fix(db): handle null user_id in session lookup

refactor(api): extract pagination helper

docs: clarify env var defaults in README
\`\`\`

## With breaking change

\`\`\`
feat(api): use new pagination schema

The /items endpoint now returns {data, cursor} instead of bare array.

BREAKING CHANGE: callers must read response.data instead of treating
the response as an array.
\`\`\`

步骤 3:用

hermes
> /conventional-commit
[Skill loaded: conventional-commit]

> 我刚修了 auth refresh token 过期没处理导致 crash 的 bug,给个 commit message

fix(auth): handle expired refresh token without crash

The refresh endpoint returns 401 when the token has expired. Previously
we re-raised, crashing the session. Now we clear local tokens and
surface a clean re-auth prompt.

Refs: #842

注意 Hermes 输出 立刻符合规范。fix(auth) 不是它瞎猜的—— 是 SKILL.md 告诉它的。

给 Skill 加 Scripts

复杂逻辑别让 LLM inline 写,扔脚本里:

~/.hermes/skills/research/web_scrape/
├── SKILL.md                # 决策手册
├── scripts/
│   └── extract_main.py    # 用 BeautifulSoup 抽主内容
└── references/
    └── selectors.json     # 常见网站的 CSS 选择器

SKILL.md 里只说 "用 scripts/extract_main.py 提取", Agent 通过 execute_codeterminal 工具执行。

让 Agent 自己创建 Skill

跑一个复杂任务,结尾问它:

> 这个流程值得存成一个 skill 吗?如果是,请用 skill_manage 创建。

✓ skill_manage(action="create", name="async-task-monitoring", ...)
   skill saved to ~/.hermes/skills/devops/async-task-monitoring/

Curator 会自动把这个 skill 标记 created_by: agent,未来 90 天不用就归档。 想保留?用 hermes curator pin async-task-monitoring

14.4实战 3:写一个 MCP Server

第 7 章和第 13 章讲了 MCP 的重要性。现在我们写一个。

目标:暴露一个 calculate_distance 工具——两个城市的距离。让 Hermes 自动接入。

步骤 1:建项目

mkdir mcp-distance && cd mcp-distance
uv venv .venv
source .venv/bin/activate
uv pip install "mcp>=1.0"

步骤 2:写 server.py

mcp-distance/server.py
import json
from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types

server = Server("distance-calculator")

# 简化版"城市坐标",实际可换成调 API
CITY_COORDS = {
    "beijing": (39.9042, 116.4074),
    "shanghai": (31.2304, 121.4737),
    "tokyo": (35.6762, 139.6503),
    "san francisco": (37.7749, -122.4194),
    "new york": (40.7128, -74.0060),
}


def haversine(lat1, lon1, lat2, lon2) -> float:
    """Great-circle distance in km."""
    from math import radians, sin, cos, sqrt, atan2
    R = 6371
    dlat = radians(lat2 - lat1)
    dlon = radians(lon2 - lon1)
    a = sin(dlat/2)**2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon/2)**2
    return R * 2 * atan2(sqrt(a), sqrt(1-a))


@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="calculate_distance",
            description="Calculate great-circle distance between two cities in km.",
            inputSchema={
                "type": "object",
                "properties": {
                    "city_a": {"type": "string", "description": "e.g. 'beijing'"},
                    "city_b": {"type": "string", "description": "e.g. 'tokyo'"},
                },
                "required": ["city_a", "city_b"],
            },
        ),
    ]


@server.call_tool()
async def handle_call_tool(name: str, args: dict) -> list[types.TextContent]:
    if name != "calculate_distance":
        raise ValueError(f"Unknown tool: {name}")

    a = args["city_a"].lower().strip()
    b = args["city_b"].lower().strip()

    if a not in CITY_COORDS or b not in CITY_COORDS:
        return [types.TextContent(
            type="text",
            text=json.dumps({"error": "Unknown city. Known: " + str(list(CITY_COORDS))})
        )]

    lat1, lon1 = CITY_COORDS[a]
    lat2, lon2 = CITY_COORDS[b]
    d = haversine(lat1, lon1, lat2, lon2)
    return [types.TextContent(
        type="text",
        text=json.dumps({"distance_km": round(d, 2), "from": a, "to": b})
    )]


async def main():
    async with stdio_server() as (read, write):
        await server.run(read, write, server.create_initialization_options())


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

步骤 3:让 Hermes 连接

~/.hermes/config.yaml
mcp:
  servers:
    distance:
      command: python
      args: [/path/to/mcp-distance/server.py]
      enabled: true

步骤 4:试

hermes
> 北京到东京多远?

🛠 mcp__distance__calculate_distance(city_a="beijing", city_b="tokyo")
  → {"distance_km": 2103.97, "from": "beijing", "to": "tokyo"}

北京到东京约 2104 公里(大圆距离)。
学到了什么 你刚写了一个标准 MCP server。不只 Hermes 能用——Claude Desktop、Cursor、Zed 全部能直接接入。一份代码,多个 host。这就是 MCP 的威力。

14.5实战 4:给项目写 AGENTS.md

最后一个实战是"教 Agent 怎么在你的项目里工作"。 这是 Hermes / Claude Code / Cursor 都支持的通用方式。

AGENTS.md 是什么

项目根目录下的一份 Markdown 文档,告诉 AI Agent:

最简模板

AGENTS.md(项目根目录)
# Project Agent Guide

## What This Is

A Python service that processes incoming webhooks from Stripe and
writes normalized events to PostgreSQL. Built on FastAPI + SQLAlchemy.

## Architecture

\`\`\`
[Stripe] --webhook--> [FastAPI server (app.py)]
                          │
                          │ verify signature
                          │ parse event
                          ▼
                     [Event Handler (handlers/)]
                          │
                          │ normalize → ORM
                          ▼
                     [PostgreSQL via SQLAlchemy]
\`\`\`

## Key Files

| Path | Purpose |
|------|---------|
| `app.py` | FastAPI entry point, route definitions |
| `handlers/` | One file per Stripe event type |
| `db/models.py` | SQLAlchemy models |
| `db/migrations/` | Alembic migrations — never edit by hand |
| `tests/` | pytest tests; use fixtures from `tests/conftest.py` |
| `.env.example` | Required env vars |

## Common Tasks

### Add a new Stripe event handler

1. Create `handlers/<event_name>.py` with a `def handle(event: dict) -> None` function.
2. Register it in `handlers/__init__.py`'s `HANDLERS` dict.
3. Add a test in `tests/handlers/test_<event_name>.py` using `stripe_event_factory`.
4. Run `pytest tests/handlers/`.

### Add a new DB column

1. Add column to the model in `db/models.py`.
2. Generate migration: `alembic revision --autogenerate -m "<desc>"`.
3. Review the generated migration file. Edit if needed.
4. Apply: `alembic upgrade head`.

## Commands

\`\`\`bash
make test         # pytest with coverage
make lint         # ruff + mypy
make migrate      # alembic upgrade head
make dev          # start FastAPI with reload
\`\`\`

## Conventions

- Use `from __future__ import annotations` in every Python file.
- Type hints required for all public functions.
- Test files mirror source structure: `src/foo/bar.py``tests/foo/test_bar.py`.
- Never raise from `handlers/*.py` — log and return.

## Forbidden

- DO NOT edit `db/migrations/*.py` manually after generation (re-generate instead).
- DO NOT use `session.execute()` with raw SQL — use ORM.
- DO NOT add new top-level deps without discussing — we pin everything.

把这份扔到你的项目根目录。Hermes / Cursor / Claude Code 启动时会自动发现并放进 context(第 5 章我们看过——它进 context 层)。Agent 立刻知道你的项目。

实用建议 AGENTS.md 不必长。Hermes 自己的 AGENTS.md 53KB 是极端例子。 普通项目 2–5KB 就够。关键是写清楚"常见任务怎么做"—— Agent 看到清晰步骤会按你的步骤走,否则它会自己摸索(不一定符合你的项目惯例)。

14.6实战 5:让 Skill 跨工具兼容

第 8 章我们看过 Skill 在 2025.12 已经成为开放标准。 这一节带你写一份能在 Claude Code、Cursor、Hermes、ChatGPT 都能用的 SKILL.md

跨工具兼容的 5 个规则

  1. frontmatter 只用标准字段namedescriptionversionlicenseplatforms。 Hermes 私有的 metadata.hermes.* 别用。
  2. 不引用 host-specific 工具名。引用工具时用通用名("file editor"、"shell"), 不写 browser_navigate 这种 Hermes 特定名字。或者在 ## Prerequisites 里写"requires: shell, file read/write",让每个 host 自己 map 到本地工具。
  3. 用 fenced code 块。所有命令例子用 ```bash 包, 不要假设 Markdown 处理器支持 Hermes 特定语法。
  4. 脚本放 scripts/ 子目录,用相对路径引用。 所有 host 都遵守 SKILL.md 同级目录约定。
  5. 不依赖私有环境变量。如果 skill 需要 API key,要求在 ## Prerequisites 里说清楚 env var 名字,让用户自己设。

跨兼容 SKILL.md 模板

~/.hermes/skills/dev/clean-pr-review/SKILL.md
---
name: clean-pr-review
description: "Review a GitHub PR with structured comments grouped by severity."
version: 1.0.0
license: MIT
author: "your name"
---

# Clean PR Review

Generate a structured PR review with comments grouped by severity
(blocker / major / minor / nit).

## When to Use

- User asks "review this PR" or "do a code review"
- User links a GitHub PR URL
- User asks for feedback on a diff

## When NOT to Use

- User asks for a one-line approval comment
- User wants you to merge the PR (use git tooling instead)

## Prerequisites

- Shell access to a tool that can fetch PR diffs (gh CLI, git, or HTTP)
- Read access to the repository

## Procedure

1. Fetch the PR diff. Prefer `gh pr diff <number>` if available;
   fall back to `git diff <base>...<head>`.

2. Identify the changed files. For each file, categorize the change:
   feat / fix / refactor / docs / test.

3. Find issues by severity:
   - **blocker** — bugs, security, broken builds
   - **major** — design issues, missing tests for new behavior
   - **minor** — code style, naming, readability
   - **nit** — purely cosmetic

4. Output in this exact format:

   \`\`\`markdown
   ## Summary

   <1-paragraph high-level summary>

   ## Blockers

   - file:line — <issue> (suggest: <fix>)

   ## Major

   - ...

   ## Minor / Nits

   - ...
   \`\`\`

5. If no issues at a level, omit that section entirely.

## Pitfalls

- Don't pad: omit empty sections, don't say "no blockers found".
- Don't approve / request changes — just output the review.

这份 SKILL.md 没有任何 Hermes 私有内容。把它扔进任意一个兼容 host:

都能直接生效。这是 2025.12 之后 skill 生态的核心红利。

14.710 条设计哲学

读完前 13 章,把这本书的核心智慧提炼成 10 条原则。 把它们贴你写 Agent 代码的桌面上。

① 不要在 Augmented LLM 之上添加抽象,除非你能说清楚它解决什么具体问题

Anthropic、Simon Willison、Hermes 维护者反复说这条。LangChain 风格的 Agent / Chain / Memory 基类层叠让代码难调试。 直接写循环 + 字典是更好的起点。有需要再抽象,不要预先抽象

② 缓存是钱的问题,不是性能问题

Agent 经济 90% 取决于 prompt cache 命中率。一次 system prompt 修改可能让账单 ×10。 所有"动态内容"只 append 到末尾,永不 insert/edit 中间。Skill 走 user message 注入而不是改 system——这是 Hermes 工程最核心的一招。

③ 错误返回值 > 错误异常

工具抛错 → 包成 {"error": "..."} JSON 给模型看。模型大概率自己改正。 抛异常给框架等于浪费 LLM 这层智能。"Tool 'web_seach' doesn't exist. Available: web_search, ..." 让模型下一轮自己改成 web_search

④ 永远给 LLM 留余地

预算耗尽给 grace call、空响应有三层 fallback、幻觉工具名让它重试 3 次—— 永远不要"硬性失败给用户看"。注入合成消息让对话继续比留沉默好。

⑤ 把"自我改进"拆成三件事

Skill(程序性记忆)+ Memory(情景记忆)+ Curator(维护)。没有黑魔法, 只是"系统地把每次新经验存到合适的地方,并定期整理"。三者缺一不可: 没有 Curator 你的 skill 库会腐烂;没有 Memory 你每次从零开始;没有 Skill 你只有事实没有方法。

⑥ 把"提示注入防御"做到工具层

不要试图教 LLM 防御 prompt injection。限制它能做的事。Webhook 来的内容只暴露 safe toolset——就算 LLM 被骗,它根本看不见 terminal 工具。Capability-based security 比 LLM-side 防御靠谱 100 倍。

⑦ Registry-based discovery 优于显式注册

工具、provider、platform adapter、skill——都是"把文件丢进目录自动起效"。 你不维护一份导入列表,添加新东西的摩擦力降到最低。 这也是 plugin 系统能蓬勃发展的基础。

⑧ Hook 化的扩展点优于继承

pre_tool_calltransform_tool_resulton_session_start... Plugin 通过订阅生命周期事件扩展,核心保持稳定。 改不到的"特殊需求"先想"能不能加个 hook",不要往 core 塞特定 plugin 逻辑。

⑨ Provider quirks 隔离在 Profile 子类

不同模型 provider 的怪癖(DeepSeek thinking flag、Gemini tool_choice、reasoning 字段差异) 全部放在ProviderProfile 子类的方法重载里。主循环代码不出现一个 if provider == "..."。 这条规则适用于任何"对接多个 API"的场景。

⑩ 写代码时记住——你写的是给 LLM 看的

Tool 的 schema description、Skill 的 SKILL.md、Provider profile 的注释—— 这些都被 LLM 读到,影响下一轮决策。把它们写清楚比把变量名写好更重要。 Description 模糊 → LLM 用错工具;Skill When to Use 不清 → 该用没用。 "代码不是给人写的,也是给 LLM 写的"——这是 Agent 时代代码风格的新规则。

14.8推荐阅读路径

读完这本书还不够。下面这些资源按优先级排:

必读

  1. Hermes AGENTS.md — 53KB 的项目工程指南,是本书没收进来的"细节大全"
  2. Hermes agent/conversation_loop.py — 把 644-3821 行(主 while)扫一遍。
  3. Anthropic Building Effective Agents
  4. Anthropic Effective Context Engineering
  5. MCP 协议规范 modelcontextprotocol.io

论文(按重要性)

  1. ReAct (Yao 2022) — arXiv:2210.03629
  2. Voyager (Wang 2023) — arXiv:2305.16291
  3. Generative Agents (Park 2023) — arXiv:2304.03442
  4. MemGPT (Packer 2023) — arXiv:2310.08560
  5. Reflexion (Shinn 2023) — arXiv:2303.11366

工业博客

  1. Anthropic Engineering — anthropic.com/engineering
  2. Simon Willison — 持续在 simonwillison.net 综述
  3. Cognition / Devin team blog
  4. Cursor team blog
  5. Hermes "What's New" — hermes-agent.nousresearch.com/docs

动手项目

  1. 给你自己的项目写 AGENTS.md,让 Hermes / Claude Code / Cursor 都能用。
  2. 写一个 MCP server 暴露你常用的 API(GitHub、Linear、自家 CMS……)。
  3. 试 Hermes 的 delegate_task:让 Agent 把一个大任务自己切给 sub-agent。
  4. 跑 Hermes 一周后看 ~/.hermes/skills/——看 Agent 自己创建了什么。
  5. 把 Hermes 部署到 VPS,连 Telegram bot,开始用手机远程让它干活。

14.9结语

你读完了一本 14 章、约 8 万字的 Agent 工程教材。如果你做了所有的练习和实践, 你现在拥有的不只是"知道 Agent 是什么"——你拥有"看到任何 Agent 系统都能拆开 评估"的能力。

Hermes 不是终点。它是 2024–2026 这段时期工程沉淀的样本。明年、后年、五年后, 会有更好的 Agent framework 出现。但底层规律不会变——

把这些原则握在手里,下次你看到任何 Agent 项目都知道该看什么。 这就是本书的价值。

最后 Agent 工程仍是新领域。本书写到 2026 年 5 月,三年后回看可能很多细节过时。 但工程思维不过时。读源码、问 Why、自己动手——这三件永远是最快的学习路径。

祝你 build 出更好的 Agent。

章末练习(也是给你自己的"出师题")

  1. Medium 把本章的 4 个实战全部跑通:装 Hermes、写 Plugin、写 Skill、写 MCP server。
  2. Medium 在你最熟悉的项目里加一份 AGENTS.md,让 AI Agent 能在里面工作。 用一周观察它的表现,然后迭代这份 AGENTS.md。
  3. Medium 把第 14.6 节的 10 条原则印出来贴桌上。下次写 Agent 代码时,逐条对照—— 你违反了几条?
  4. Hard 跑 SWE-Bench Verified 上的一个简单任务,用你自己的小 harness。看你的分数。 想想哪条原则没做到。
  5. Hard 给本书写一个章节"如何不读 Hermes 源码也能学 Agent 工程"——你会推荐什么书 / 论文 / 项目? 把你的版本与本书做对比。
  6. Hard 回头读第 1 章。问自己:Agent 工程这个领域,五年后会变成什么样? 写 500 字预测。一年后回来看你猜中几条。

—— End of Book ——

返回总目录