首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Hermes 与 Agent 工程实战:从模型选型到生产级智能体的完整代码实现

Hermes 与 Agent 工程实战:从模型选型到生产级智能体的完整代码实现

原创
作者头像
IT大佬 jzit-top
修改于 2026-09-22 17:41:47
修改于 2026-09-22 17:41:47
1410
举报

摘要:Hermes 是 NousResearch 推出的开源大模型系列,因其原生函数调用(Function Calling)、结构化输出、强指令遵循能力,成为 Agent 工程中极具性价比的基座。本文从专业视角系统讲解 Hermes 模型家族、Agent 工程核心范式,并给出一套可运行的 Python 实现:覆盖本地部署(vLLM / Ollama / llama.cpp)、原生 tool calling、ReAct 循环、多工具编排、状态管理、权限审计、评估与生产化。示例使用 Hermes 的 prompt 格式与函数调用协议,可直接接入本地或远程服务。


目录

  1. 为什么 Agent 工程需要 Hermes
  2. Hermes 模型家族全景
  3. Hermes 的 Agent 关键能力
  4. Hermes Prompt 格式与工具调用协议
  5. Agent 工程核心范式
  6. 代码实战:基于 Hermes 的 Agent 系统
  7. 模块一:Hermes 客户端封装
  8. 模块二:工具注册与 Schema
  9. 模块三:Agent Loop 引擎
  10. 模块四:多工具编排与并行
  11. 模块五:状态与记忆
  12. 模块六:权限、审计与安全
  13. 模块七:评估与回归测试
  14. 模块八:API 与前端接入
  15. 部署:vLLM / Ollama / llama.cpp
  16. 生产化清单
  17. 常见陷阱
  18. 总结

一、为什么 Agent 工程需要 Hermes

在做 Agent 时,模型选型是第一道关。闭源模型(GPT、Claude)能力强,但存在:

  • 成本高;
  • 数据出境风险;
  • 不可私有化;
  • 供应商锁定;
  • 调用频率受限。

开源模型早期的问题是:不会用工具。很多模型能聊天,但一到 function calling 就格式错乱、参数瞎编。

Hermes 系列的核心价值,就是把函数调用和结构化输出做到开源模型的一流水准。

Hermes 对 Agent 工程的意义:

能力

普通开源模型

Hermes

指令遵循

一般

强

JSON 输出

不稳定

稳定

函数调用

弱

原生支持

多轮工具

易迷失

稳定

角色扮演

一般

强

本地部署

支持

支持

私有化

支持

支持

一句话:Hermes 是"能落地的 Agent 基座"。


二、Hermes 模型家族全景

NousResearch 的 Hermes 系列经历了多个版本:

版本

基座

参数

特点

Hermes 2 Pro

Llama 3

8B

首个强 tool calling

Hermes 2 Theta

Llama 3

7B

混合推理

Hermes 3

Llama 3.1

8B / 70B / 405B

全面增强

Hermes 4

Llama 3.2 / 新一代

多规格

更强 Agent 能力

Hermes 的训练重点:

  1. 指令遵循:严格按格式输出;
  2. 函数调用:原生 <tool_call> 标签;
  3. 结构化输出:JSON mode;
  4. 多轮对话:长上下文一致性;
  5. 角色稳定:系统提示不漂移。

选型建议:

  • 本地开发:8B(消费级显卡可跑);
  • 生产中小流量:8B + vLLM;
  • 高质量场景:70B;
  • 极致质量:405B 或多模型路由。

三、Hermes 的 Agent 关键能力

3.1 原生函数调用

Hermes 用特殊 token 表示工具调用:

代码语言:javascript
复制
<tool_call>
{"name": "get_weather", "arguments": {"city": "上海"}}
</tool_call>

工具返回:

代码语言:javascript
复制
<tool_response>
{"temperature": 24, "condition": "多云"}
</tool_response>

这种格式比纯 JSON 更稳定,模型能清楚区分"思考"和"调用"。

3.2 结构化输出

Hermes 支持 JSON mode:

代码语言:javascript
复制
<json>
{"intent": "query_weather", "slots": {"city": "上海"}}
</json>

3.3 系统提示遵循

Hermes 对 system prompt 的遵循度高,适合定义 Agent 的角色、边界、工具使用规范。

3.4 多轮工具链

Hermes 能在多轮中保持工具调用上下文,适合复杂任务。


四、Hermes Prompt 格式与工具调用协议

4.1 ChatML 格式

Hermes 使用 ChatML:

代码语言:javascript
复制
<|im_start|>system
你是一个企业助手。<|im_end|>
<|im_start|>user
帮我查一下上海天气。<|im_end|>
<|im_start|>assistant
<tool_call>
{"name": "get_weather", "arguments": {"city": "上海"}}
</tool_call><|im_end|>
<|im_start|>tool
<tool_response>
{"temperature": 24}
</tool_response><|im_end|>
<|im_start|>assistant
上海当前 24 度,多云。<|im_end|>

4.2 工具定义注入

在 system prompt 中注入工具列表:

代码语言:javascript
复制
你可以使用以下工具:

<tools>
[
  {
    "name": "get_weather",
    "description": "查询城市天气",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {"type": "string", "description": "城市名"}
      },
      "required": ["city"]
    }
  }
]
</tools>

调用工具时,输出:
<tool_call>
{"name": "工具名", "arguments": {...}}
</tool_call>

4.3 解析策略

  • 用正则提取 <tool_call>...</tool_call>;
  • JSON 解析参数;
  • 校验 Schema;
  • 执行工具;
  • 用 <tool_response> 回填。

五、Agent 工程核心范式

代码语言:javascript
复制
用户目标
  -> 意图理解
  -> 上下文检索(记忆 + RAG)
  -> 规划
  -> 选择工具
  -> 执行工具
  -> 观察结果
  -> 反思
  -> 继续或输出

工程上要解决的问题:

问题

方案

工具选择错误

工具描述优化 + few-shot

参数错误

Schema 校验 + 重试

死循环

最大步数 + 重复检测

状态丢失

状态持久化

权限失控

工具级 RBAC

无法审计

全量日志

效果不稳定

评估集 + 回归


六、代码实战:基于 Hermes 的 Agent 系统

6.1 技术栈

  • Python 3.12
  • FastAPI
  • Pydantic
  • SQLModel
  • SQLite(示例)/ PostgreSQL(生产)
  • httpx(调用本地服务)
  • Pytest

6.2 项目结构

代码语言:javascript
复制
hermes-agent/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── db.py
│   ├── models.py
│   ├── hermes_client.py
│   ├── prompt.py
│   ├── tools.py
│   ├── parser.py
│   ├── agent.py
│   ├── memory.py
│   ├── security.py
│   ├── audit.py
│   ├── evaluator.py
│   └── config.py
├── tests/
│   └── test_hermes_agent.py
├── requirements.txt
└── Dockerfile

6.3 依赖

代码语言:javascript
复制
pip install fastapi uvicorn pydantic sqlmodel httpx pytest

requirements.txt:

代码语言:javascript
复制
fastapi
uvicorn[standard]
pydantic
sqlmodel
httpx
pytest

七、模块一:Hermes 客户端封装

app/config.py:

代码语言:javascript
复制
import os

HERMES_BASE_URL = os.getenv("HERMES_BASE_URL", "http://localhost:8000/v1")
HERMES_MODEL = os.getenv("HERMES_MODEL", "NousResearch/Hermes-3-Llama-3.1-8B")
HERMES_API_KEY = os.getenv("HERMES_API_KEY", "not-needed")
HERMES_TIMEOUT = float(os.getenv("HERMES_TIMEOUT", "60"))
HERMES_MAX_TOKENS = int(os.getenv("HERMES_MAX_TOKENS", "1024"))
HERMES_TEMPERATURE = float(os.getenv("HERMES_TEMPERATURE", "0.2"))

app/hermes_client.py:

代码语言:javascript
复制
import httpx
from typing import List, Dict, Any
from app.config import (
    HERMES_BASE_URL,
    HERMES_MODEL,
    HERMES_API_KEY,
    HERMES_TIMEOUT,
    HERMES_MAX_TOKENS,
    HERMES_TEMPERATURE,
)


class HermesClient:
    """
    兼容 OpenAI Chat Completions 协议。
    可对接 vLLM / Ollama / llama.cpp server / TGI。
    """

    def __init__(
        self,
        base_url: str = HERMES_BASE_URL,
        model: str = HERMES_MODEL,
        api_key: str = HERMES_API_KEY,
    ):
        self.base_url = base_url.rstrip("/")
        self.model = model
        self.api_key = api_key
        self.client = httpx.Client(timeout=HERMES_TIMEOUT)

    def chat(
        self,
        messages: List[Dict[str, str]],
        tools: List[Dict[str, Any]] | None = None,
        temperature: float = HERMES_TEMPERATURE,
        max_tokens: int = HERMES_MAX_TOKENS,
        stop: List[str] | None = None,
    ) -> Dict[str, Any]:
        payload = {
            "model": self.model,
            "messages": messages,
            "temperature": temperature,
            "max_tokens": max_tokens,
        }
        if tools:
            payload["tools"] = tools
        if stop:
            payload["stop"] = stop

        resp = self.client.post(
            f"{self.base_url}/chat/completions",
            json=payload,
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
            },
        )
        resp.raise_for_status()
        return resp.json()

    def complete(
        self,
        prompt: str,
        temperature: float = HERMES_TEMPERATURE,
        max_tokens: int = HERMES_MAX_TOKENS,
    ) -> str:
        payload = {
            "model": self.model,
            "prompt": prompt,
            "temperature": temperature,
            "max_tokens": max_tokens,
        }
        resp = self.client.post(
            f"{self.base_url}/completions",
            json=payload,
            headers={"Authorization": f"Bearer {self.api_key}"},
        )
        resp.raise_for_status()
        data = resp.json()
        return data["choices"][0]["text"]

说明:如果使用 Ollama,把 base_url 换成 http://localhost:11434/v1,模型名换成 hermes3 即可。vLLM 默认 http://localhost:8000/v1。


八、模块二:工具注册与 Schema

app/tools.py:

代码语言:javascript
复制
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List


@dataclass
class ToolSpec:
    name: str
    description: str
    parameters: Dict[str, Any]
    handler: Callable[..., Any]
    requires_roles: List[str] = field(default_factory=lambda: ["member"])
    risk_level: str = "low"
    requires_confirmation: bool = False

    def to_openai_schema(self) -> dict:
        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": self.parameters,
            },
        }


class ToolRegistry:
    def __init__(self):
        self._tools: Dict[str, ToolSpec] = {}

    def register(self, spec: ToolSpec):
        self._tools[spec.name] = spec

    def get(self, name: str) -> ToolSpec | None:
        return self._tools.get(name)

    def schemas_for_role(self, role: str) -> List[dict]:
        return [
            t.to_openai_schema()
            for t in self._tools.values()
            if role in t.requires_roles
        ]

    def execute(self, name: str, arguments: dict, context: dict) -> dict:
        spec = self.get(name)
        if not spec:
            return {"ok": False, "error": f"未知工具:{name}"}
        try:
            result = spec.handler(**arguments)
            return {"ok": True, "tool": name, "result": result}
        except TypeError as e:
            return {"ok": False, "tool": name, "error": f"参数错误:{e}"}
        except Exception as e:
            return {"ok": False, "tool": name, "error": f"执行失败:{e}"}


# ---------- 示例工具 ----------

def tool_get_weather(city: str) -> dict:
    return {"city": city, "temperature": 24, "condition": "多云"}


def tool_calculator(expression: str) -> dict:
    allowed = set("0123456789+-*/(). ")
    if not set(expression) <= allowed:
        raise ValueError("表达式含非法字符")
    value = eval(expression, {"__builtins__": {}}, {})
    return {"expression": expression, "value": value}


def tool_search_docs(query: str, top_k: int = 3) -> dict:
    return {
        "query": query,
        "results": [
            {"title": f"文档 {i+1}", "snippet": f"关于 {query} 的说明 {i+1}"}
            for i in range(top_k)
        ],
    }


def tool_create_ticket(title: str, priority: str = "medium") -> dict:
    return {
        "ticket_id": f"TK{abs(hash(title)) % 100000}",
        "title": title,
        "priority": priority,
        "status": "open",
    }


def tool_send_email(to: str, subject: str, body: str) -> dict:
    return {"to": to, "subject": subject, "status": "queued"}


def build_default_registry() -> ToolRegistry:
    reg = ToolRegistry()

    reg.register(ToolSpec(
        name="get_weather",
        description="查询指定城市的当前天气",
        parameters={
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "城市名,例如 上海"},
            },
            "required": ["city"],
        },
        handler=tool_get_weather,
        requires_roles=["member", "manager", "admin"],
        risk_level="low",
    ))

    reg.register(ToolSpec(
        name="calculator",
        description="计算数学表达式",
        parameters={
            "type": "object",
            "properties": {
                "expression": {"type": "string", "description": "如 2+3*4"},
            },
            "required": ["expression"],
        },
        handler=tool_calculator,
        requires_roles=["member", "manager", "admin"],
        risk_level="low",
    ))

    reg.register(ToolSpec(
        name="search_docs",
        description="在企业知识库中检索文档",
        parameters={
            "type": "object",
            "properties": {
                "query": {"type": "string"},
                "top_k": {"type": "integer", "default": 3},
            },
            "required": ["query"],
        },
        handler=tool_search_docs,
        requires_roles=["member", "manager", "admin"],
        risk_level="low",
    ))

    reg.register(ToolSpec(
        name="create_ticket",
        description="创建一个工单",
        parameters={
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "priority": {
                    "type": "string",
                    "enum": ["low", "medium", "high", "urgent"],
                },
            },
            "required": ["title"],
        },
        handler=tool_create_ticket,
        requires_roles=["member", "manager", "admin"],
        risk_level="medium",
    ))

    reg.register(ToolSpec(
        name="send_email",
        description="发送邮件(高风险,需要确认)",
        parameters={
            "type": "object",
            "properties": {
                "to": {"type": "string"},
                "subject": {"type": "string"},
                "body": {"type": "string"},
            },
            "required": ["to", "subject", "body"],
        },
        handler=tool_send_email,
        requires_roles=["manager", "admin"],
        risk_level="high",
        requires_confirmation=True,
    ))

    return reg

九、模块三:Agent Loop 引擎

9.1 Prompt 构建

app/prompt.py:

代码语言:javascript
复制
import json
from typing import List


SYSTEM_TEMPLATE = """你是 Hermes Agent,一个可靠的企业智能体。

## 你的职责
- 理解用户目标;
- 在需要时调用工具;
- 基于工具结果给出准确回答;
- 不确定时明确说明,不要编造。

## 工具调用格式
调用工具时,必须输出:
<tool_call>
{{"name": "工具名", "arguments": {{...}}}}
</tool_call>

## 可用工具
<tools>
{tools_json}
</tools>

## 行为准则
- 每次只调用一个工具;
- 参数必须符合 Schema;
- 工具失败时分析原因,可重试一次;
- 高风险操作需等待人工确认;
- 不要输出密钥、密码等敏感信息。
"""


def build_system_prompt(tool_schemas: List[dict], extra: str = "") -> str:
    tools_json = json.dumps(tool_schemas, ensure_ascii=False, indent=2)
    prompt = SYSTEM_TEMPLATE.format(tools_json=tools_json)
    if extra:
        prompt += f"\n## 补充说明\n{extra}\n"
    return prompt

9.2 解析器

app/parser.py:

代码语言:javascript
复制
import json
import re
from dataclasses import dataclass
from typing import Optional


TOOL_CALL_RE = re.compile(
    r"<tool_call>\s*(\{.*?\})\s*</tool_call>",
    re.DOTALL,
)


@dataclass
class ToolCall:
    name: str
    arguments: dict


@dataclass
class ParsedOutput:
    kind: str                       # "tool_call" / "final"
    content: str = ""
    tool_call: Optional[ToolCall] = None


def parse_hermes_output(text: str) -> ParsedOutput:
    """
    解析 Hermes 输出,支持:
    - <tool_call>{"name":..., "arguments":{...}}</tool_call>
    - 纯文本最终回答
    """
    if not text:
        return ParsedOutput(kind="final", content="")

    match = TOOL_CALL_RE.search(text)
    if match:
        raw = match.group(1)
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            return ParsedOutput(
                kind="final",
                content=f"工具调用解析失败:{raw}",
            )

        name = data.get("name") or data.get("tool")
        arguments = data.get("arguments") or data.get("parameters") or {}
        if not name:
            return ParsedOutput(
                kind="final",
                content=f"工具调用缺少 name:{raw}",
            )

        return ParsedOutput(
            kind="tool_call",
            tool_call=ToolCall(name=name, arguments=arguments),
        )

    # 兼容 OpenAI tool_calls 字段(若 vLLM 已解析)
    return ParsedOutput(kind="final", content=text.strip())


def strip_tool_tags(text: str) -> str:
    return TOOL_CALL_RE.sub("", text).strip()

9.3 Agent Loop

app/agent.py:

代码语言:javascript
复制
import json
from typing import List, Dict, Any

from app.hermes_client import HermesClient
from app.prompt import build_system_prompt
from app.parser import parse_hermes_output
from app.tools import ToolRegistry


MAX_STEPS = 8
MAX_REPEATS = 2


class HermesAgent:
    def __init__(
        self,
        client: HermesClient,
        tools: ToolRegistry,
        role: str = "member",
        extra_system: str = "",
    ):
        self.client = client
        self.tools = tools
        self.role = role
        self.extra_system = extra_system

    def run(self, user_message: str) -> dict:
        tool_schemas = self.tools.schemas_for_role(self.role)
        system_prompt = build_system_prompt(tool_schemas, self.extra_system)

        messages: List[Dict[str, str]] = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_message},
        ]

        steps = []
        action_history = []

        for step in range(MAX_STEPS):
            resp = self.client.chat(messages=messages)
            content = resp["choices"][0]["message"]["content"] or ""

            parsed = parse_hermes_output(content)

            if parsed.kind == "final":
                return {
                    "reply": parsed.content,
                    "steps": steps,
                    "raw_model_output": content,
                }

            tc = parsed.tool_call
            key = f"{tc.name}:{json.dumps(tc.arguments, sort_keys=True)}"
            action_history.append(key)

            if action_history.count(key) > MAX_REPEATS:
                return {
                    "reply": f"检测到重复调用 {tc.name},已停止。",
                    "steps": steps,
                    "error": "repeat_action",
                }

            steps.append({"tool": tc.name, "arguments": tc.arguments})

            result = self.tools.execute(tc.name, tc.arguments, {"role": self.role})

            messages.append({"role": "assistant", "content": content})
            messages.append({
                "role": "tool",
                "content": json.dumps(result, ensure_ascii=False),
            })

        return {
            "reply": "达到最大步数,任务未完成。",
            "steps": steps,
            "error": "max_steps",
        }

十、模块四:多工具编排与并行

当任务需要多个独立工具时,可以并行执行:

app/agent.py(扩展):

代码语言:javascript
复制
import concurrent.futures


def run_parallel_tools(agent: HermesAgent, calls: list) -> list:
    """
    并行执行多个工具调用。
    """
    results = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
        futures = {
            pool.submit(
                agent.tools.execute, c["name"], c["arguments"], {"role": agent.role}
            ): c
            for c in calls
        }
        for fut in concurrent.futures.as_completed(futures):
            results.append(fut.result())
    return results

多智能体编排示例:

代码语言:javascript
复制
class MultiAgentOrchestrator:
    """
    简单编排:研究员 + 写手 + 审校。
    """

    def __init__(self, client: HermesClient, tools: ToolRegistry):
        self.researcher = HermesAgent(client, tools, role="member",
                                      extra_system="你负责检索资料,只输出事实。")
        self.writer = HermesAgent(client, tools, role="member",
                                  extra_system="你负责把资料整理成简洁报告。")
        self.reviewer = HermesAgent(client, tools, role="manager",
                                    extra_system="你负责审校事实与逻辑,指出问题。")

    def run(self, topic: str) -> dict:
        research = self.researcher.run(f"检索并整理主题:{topic}")
        draft = self.writer.run(f"基于以下资料写报告:\n{research['reply']}")
        review = self.reviewer.run(f"审校以下报告:\n{draft['reply']}")
        return {
            "research": research,
            "draft": draft,
            "review": review,
        }

十一、模块五:状态与记忆

app/models.py:

代码语言:javascript
复制
from datetime import datetime
from typing import Optional
from sqlmodel import SQLModel, Field


class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    username: str = Field(index=True, unique=True)
    role: str = "member"
    enabled: bool = True
    created_at: datetime = Field(default_factory=datetime.utcnow)


class Conversation(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    user_id: int = Field(index=True)
    title: str = ""
    created_at: datetime = Field(default_factory=datetime.utcnow)


class MessageRecord(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    conversation_id: int = Field(index=True)
    role: str
    content: str
    created_at: datetime = Field(default_factory=datetime.utcnow)


class Memory(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    user_id: int = Field(index=True)
    key: str
    value: str
    created_at: datetime = Field(default_factory=datetime.utcnow)


class ToolAudit(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    user_id: int
    conversation_id: int
    tool_name: str
    arguments: str
    result: str
    status: str = "ok"
    created_at: datetime = Field(default_factory=datetime.utcnow)

app/memory.py:

代码语言:javascript
复制
from sqlmodel import Session, select
from app.models import Memory


def remember(db: Session, user_id: int, key: str, value: str):
    existing = db.exec(
        select(Memory).where(Memory.user_id == user_id, Memory.key == key)
    ).first()
    if existing:
        existing.value = value
        db.add(existing)
    else:
        db.add(Memory(user_id=user_id, key=key, value=value))
    db.commit()


def recall(db: Session, user_id: int) -> dict:
    rows = db.exec(select(Memory).where(Memory.user_id == user_id)).all()
    return {r.key: r.value for r in rows}


def build_memory_prompt(db: Session, user_id: int) -> str:
    mem = recall(db, user_id)
    if not mem:
        return ""
    lines = ["已知用户信息:"]
    for k, v in mem.items():
        lines.append(f"- {k}: {v}")
    return "\n".join(lines)

十二、模块六:权限、审计与安全

app/security.py:

代码语言:javascript
复制
from fastapi import Header, HTTPException, Depends
from sqlmodel import Session, select
from app.db import get_session
from app.models import User

API_KEYS = {
    "admin-key": "admin",
    "manager-key": "manager",
    "member-key": "member",
}


def get_current_user(
    x_api_key: str = Header(..., alias="X-API-Key"),
    session: Session = Depends(get_session),
) -> User:
    role = API_KEYS.get(x_api_key)
    if not role:
        raise HTTPException(status_code=401, detail="Invalid API Key")

    user = session.exec(select(User).where(User.username == role)).first()
    if not user or not user.enabled:
        raise HTTPException(status_code=401, detail="User not found")
    return user


def require_role(*roles):
    def checker(user: User = Depends(get_current_user)):
        if user.role not in roles:
            raise HTTPException(status_code=403, detail="Forbidden")
        return user
    return checker

app/audit.py:

代码语言:javascript
复制
import json
from sqlmodel import Session, select
from app.models import ToolAudit


def log_tool_call(
    db: Session,
    user_id: int,
    conversation_id: int,
    tool_name: str,
    arguments: dict,
    result: dict,
    status: str = "ok",
):
    db.add(ToolAudit(
        user_id=user_id,
        conversation_id=conversation_id,
        tool_name=tool_name,
        arguments=json.dumps(arguments, ensure_ascii=False),
        result=json.dumps(result, ensure_ascii=False, default=str),
        status=status,
    ))
    db.commit()


def list_audits(db: Session, user_id: int | None = None, limit: int = 200):
    stmt = select(ToolAudit)
    if user_id:
        stmt = stmt.where(ToolAudit.user_id == user_id)
    return db.exec(stmt.order_by(ToolAudit.id.desc()).limit(limit)).all()

安全要点:

  • 工具级 RBAC;
  • 高风险工具二次确认;
  • 输入输出内容过滤;
  • 敏感信息脱敏;
  • 全量审计。

十三、模块七:评估与回归测试

app/evaluator.py:

代码语言:javascript
复制
import json
from typing import List, Dict, Any
from app.parser import parse_hermes_output


def evaluate_tool_selection(
    cases: List[Dict[str, Any]],
    agent,
) -> dict:
    """
    cases: [{"input": "...", "expected_tool": "get_weather"}]
    """
    total = len(cases)
    correct = 0
    details = []

    for c in cases:
        result = agent.run(c["input"])
        steps = result.get("steps", [])
        selected = steps[0]["tool"] if steps else None
        ok = selected == c["expected_tool"]
        correct += int(ok)
        details.append({
            "input": c["input"],
            "expected": c["expected_tool"],
            "selected": selected,
            "ok": ok,
        })

    return {
        "total": total,
        "correct": correct,
        "accuracy": round(correct / total, 4) if total else 0.0,
        "details": details,
    }


def evaluate_parser(cases: List[Dict[str, str]]) -> dict:
    """
    cases: [{"raw": "<tool_call>...</tool_call>", "expect_kind": "tool_call"}]
    """
    correct = 0
    details = []

    for c in cases:
        parsed = parse_hermes_output(c["raw"])
        ok = parsed.kind == c["expect_kind"]
        correct += int(ok)
        details.append({
            "raw": c["raw"][:60],
            "expected": c["expect_kind"],
            "got": parsed.kind,
            "ok": ok,
        })

    total = len(cases)
    return {
        "total": total,
        "correct": correct,
        "accuracy": round(correct / total, 4) if total else 0.0,
        "details": details,
    }

评估集示例:

代码语言:javascript
复制
TOOL_SELECTION_CASES = [
    {"input": "上海天气怎么样", "expected_tool": "get_weather"},
    {"input": "帮我算一下 12*8", "expected_tool": "calculator"},
    {"input": "查一下报销制度", "expected_tool": "search_docs"},
    {"input": "给 IT 提个工单,打印机坏了", "expected_tool": "create_ticket"},
]

PARSER_CASES = [
    {
        "raw": '<tool_call>{"name": "get_weather", "arguments": {"city": "上海"}}</tool_call>',
        "expect_kind": "tool_call",
    },
    {
        "raw": "上海今天 24 度,多云。",
        "expect_kind": "final",
    },
]

十四、模块八:API 与前端接入

app/main.py:

代码语言:javascript
复制
import json
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
from sqlmodel import Session

from app.db import init_db, get_session
from app.models import User, Conversation, MessageRecord
from app.security import get_current_user, require_role
from app.hermes_client import HermesClient
from app.tools import build_default_registry
from app.agent import HermesAgent
from app.audit import list_audits, log_tool_call
from app.memory import build_memory_prompt

app = FastAPI(title="Hermes Agent Engineering")

client = HermesClient()
registry = build_default_registry()


@app.on_event("startup")
def on_startup():
    init_db()


class ChatRequest(BaseModel):
    message: str
    conversation_id: int | None = None


@app.get("/health")
def health():
    return {"status": "ok", "model": client.model, "base_url": client.base_url}


@app.get("/tools")
def list_tools(user: User = Depends(get_current_user)):
    return [
        {
            "name": t.name,
            "description": t.description,
            "risk_level": t.risk_level,
            "requires_confirmation": t.requires_confirmation,
        }
        for t in registry._tools.values()
        if user.role in t.requires_roles
    ]


@app.post("/chat")
def chat(
    payload: ChatRequest,
    user: User = Depends(get_current_user),
    session: Session = Depends(get_session),
):
    if payload.conversation_id:
        conv = session.get(Conversation, payload.conversation_id)
        if not conv or conv.user_id != user.id:
            raise HTTPException(status_code=404, detail="Conversation not found")
    else:
        conv = Conversation(user_id=user.id, title=payload.message[:30])
        session.add(conv)
        session.commit()
        session.refresh(conv)

    session.add(MessageRecord(
        conversation_id=conv.id,
        role="user",
        content=payload.message,
    ))
    session.commit()

    memory_prompt = build_memory_prompt(session, user.id)

    agent = HermesAgent(
        client=client,
        tools=registry,
        role=user.role,
        extra_system=memory_prompt,
    )

    result = agent.run(payload.message)

    session.add(MessageRecord(
        conversation_id=conv.id,
        role="assistant",
        content=result.get("reply", ""),
    ))
    session.commit()

    for step in result.get("steps", []):
        log_tool_call(
            session,
            user_id=user.id,
            conversation_id=conv.id,
            tool_name=step["tool"],
            arguments=step["arguments"],
            result={"step": "executed"},
        )

    return {
        "conversation_id": conv.id,
        "reply": result.get("reply"),
        "steps": result.get("steps", []),
        "error": result.get("error"),
    }


@app.get("/audit/tool-calls")
def audit_tool_calls(
    user: User = Depends(require_role("admin", "manager")),
    session: Session = Depends(get_session),
):
    return list_audits(session)

十五、部署:vLLM / Ollama / llama.cpp

15.1 vLLM(推荐生产)

代码语言:javascript
复制
pip install vllm

python -m vllm.entrypoints.openai.api_server \
  --model NousResearch/Hermes-3-Llama-3.1-8B \
  --served-model-name hermes-3-8b \
  --host 0.0.0.0 \
  --port 8000 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.9

设置环境变量:

代码语言:javascript
复制
export HERMES_BASE_URL=http://localhost:8000/v1
export HERMES_MODEL=hermes-3-8b

15.2 Ollama(推荐本地开发)

代码语言:javascript
复制
ollama pull hermes3
ollama serve

代码语言:javascript
复制
export HERMES_BASE_URL=http://localhost:11434/v1
export HERMES_MODEL=hermes3
export HERMES_API_KEY=ollama

15.3 llama.cpp(CPU / 低显存)

代码语言:javascript
复制
./llama-server \
  -m hermes-3-8b.Q4_K_M.gguf \
  --host 0.0.0.0 \
  --port 8080 \
  -c 8192

代码语言:javascript
复制
export HERMES_BASE_URL=http://localhost:8080/v1
export HERMES_MODEL=hermes-3-8b

15.4 Docker 部署 Agent 服务

Dockerfile:

代码语言:javascript
复制
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app ./app

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

代码语言:javascript
复制
docker build -t hermes-agent .
docker run -p 8000:8000 \
  -e HERMES_BASE_URL=http://host.docker.internal:8000/v1 \
  -e HERMES_MODEL=hermes-3-8b \
  hermes-agent

十六、测试

tests/test_hermes_agent.py:

代码语言:javascript
复制
import os
import pytest
from fastapi.testclient import TestClient

os.environ["DATABASE_URL"] = "sqlite:///./test_hermes.db"

from app.main import app  # noqa: E402
from app.parser import parse_hermes_output  # noqa: E402
from app.tools import build_default_registry  # noqa: E402

client = TestClient(app)


def test_health():
    r = client.get("/health")
    assert r.status_code == 200


def test_parser_tool_call():
    raw = '<tool_call>{"name": "get_weather", "arguments": {"city": "上海"}}</tool_call>'
    parsed = parse_hermes_output(raw)
    assert parsed.kind == "tool_call"
    assert parsed.tool_call.name == "get_weather"
    assert parsed.tool_call.arguments == {"city": "上海"}


def test_parser_final():
    parsed = parse_hermes_output("上海今天 24 度。")
    assert parsed.kind == "final"
    assert "24" in parsed.content


def test_parser_malformed():
    parsed = parse_hermes_output("<tool_call>{bad json}</tool_call>")
    assert parsed.kind == "final"
    assert "解析失败" in parsed.content


def test_registry_schema():
    reg = build_default_registry()
    schemas = reg.schemas_for_role("member")
    names = [s["function"]["name"] for s in schemas]
    assert "get_weather" in names
    assert "calculator" in names


def test_registry_permission():
    reg = build_default_registry()
    schemas = reg.schemas_for_role("member")
    names = [s["function"]["name"] for s in schemas]
    assert "send_email" not in names   # 高风险工具只对 manager/admin 开放

    schemas_mgr = reg.schemas_for_role("manager")
    names_mgr = [s["function"]["name"] for s in schemas_mgr]
    assert "send_email" in names_mgr


def test_tool_execute():
    reg = build_default_registry()
    result = reg.execute("calculator", {"expression": "2+3*4"}, {})
    assert result["ok"] is True
    assert result["result"]["value"] == 14


def test_tool_execute_error():
    reg = build_default_registry()
    result = reg.execute("calculator", {"expression": "__import__('os')"}, {})
    assert result["ok"] is False


def test_tools_endpoint():
    r = client.get("/tools", headers={"X-API-Key": "member-key"})
    assert r.status_code == 200
    names = [t["name"] for t in r.json()]
    assert "get_weather" in names
    assert "send_email" not in names


def test_audit_requires_role():
    r = client.get("/audit/tool-calls", headers={"X-API-Key": "member-key"})
    assert r.status_code == 403

    r = client.get("/audit/tool-calls", headers={"X-API-Key": "admin-key"})
    assert r.status_code == 200

运行:

代码语言:javascript
复制
pytest -v

十七、生产化清单

  • □ 模型服务高可用(vLLM 多副本 + LB)
  • □ 模型版本管理与灰度
  • □ Prompt 版本化
  • □ 工具 Schema 校验
  • □ 工具级 RBAC
  • □ 高风险工具二次确认
  • □ 全量审计(请求、模型输出、工具调用)
  • □ 内容安全过滤(输入 + 输出)
  • □ 敏感信息脱敏
  • □ 会话与记忆持久化
  • □ 最大步数与成本上限
  • □ 重复动作检测
  • □ 超时与重试
  • □ 降级策略(模型不可用时切备用)
  • □ 评估集 + 回归测试
  • □ 可观测性(日志、指标、链路追踪)
  • □ 成本统计(Token、QPS、GPU 利用率)
  • □ 用户反馈闭环
  • □ 数据合规(个人信息、跨境)
  • □ 灾难恢复演练

十八、常见陷阱

  1. Prompt 里工具描述太简:模型选错工具;
  2. Schema 不严格:参数瞎编;
  3. 不做解析容错:模型多输出一个字就崩;
  4. 不做重复检测:陷入死循环烧 GPU;
  5. 工具无权限控制:低权限用户调用高危工具;
  6. 不做审计:出问题无法追责;
  7. 本地模型不调 temperature:输出不稳定;
  8. 上下文无限增长:超出模型窗口;
  9. 不做评估:改 Prompt 后效果回退不知道;
  10. 忽略成本:GPU 成本失控。

十九、总结

Hermes 与 Agent 工程的关系:

代码语言:javascript
复制
Hermes = 能稳定调用工具的开放基座
Agent  = 把模型能力转化为可执行任务的系统

核心公式:

代码语言:javascript
复制
Agent 系统 = Hermes 模型 + 工具系统 + Agent Loop + 状态记忆 + 权限审计 + 评估

三条工程原则:

  1. 模型可替换:Hermes 只是执行器,接口标准化后可换任何模型;
  2. 工具可管控:注册表 + RBAC + 审计;
  3. 效果可度量:评估集 + 回归测试。

Hermes 的价值在于:让企业能够私有化、低成本、可控地构建 Agent,而不必完全依赖闭源 API。

当你能用 Hermes 稳定完成"查天气、算数、检索知识库、建工单、发邮件"这些任务,并且每一步都可审计、可回放、可评估时,你就真正掌握了 Agent 工程的落地方法。


附:完整项目结构

代码语言:javascript
复制
hermes-agent/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── db.py
│   ├── models.py
│   ├── config.py
│   ├── hermes_client.py
│   ├── prompt.py
│   ├── tools.py
│   ├── parser.py
│   ├── agent.py
│   ├── memory.py
│   ├── security.py
│   ├── audit.py
│   └── evaluator.py
├── tests/
│   └── test_hermes_agent.py
├── requirements.txt
└── Dockerfile

运行顺序:

代码语言:javascript
复制
pip install -r requirements.txt
# 启动 Hermes 服务(vLLM 或 Ollama)
pytest -v
uvicorn app.main:app --reload

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 目录
  • 一、为什么 Agent 工程需要 Hermes
  • 二、Hermes 模型家族全景
  • 三、Hermes 的 Agent 关键能力
    • 3.1 原生函数调用
    • 3.2 结构化输出
    • 3.3 系统提示遵循
    • 3.4 多轮工具链
  • 四、Hermes Prompt 格式与工具调用协议
    • 4.1 ChatML 格式
    • 4.2 工具定义注入
    • 4.3 解析策略
  • 五、Agent 工程核心范式
  • 六、代码实战:基于 Hermes 的 Agent 系统
    • 6.1 技术栈
    • 6.2 项目结构
    • 6.3 依赖
  • 七、模块一:Hermes 客户端封装
  • 八、模块二:工具注册与 Schema
  • 九、模块三:Agent Loop 引擎
    • 9.1 Prompt 构建
    • 9.2 解析器
    • 9.3 Agent Loop
  • 十、模块四:多工具编排与并行
  • 十一、模块五:状态与记忆
  • 十二、模块六:权限、审计与安全
  • 十三、模块七:评估与回归测试
  • 十四、模块八:API 与前端接入
  • 十五、部署:vLLM / Ollama / llama.cpp
    • 15.1 vLLM(推荐生产)
    • 15.2 Ollama(推荐本地开发)
    • 15.3 llama.cpp(CPU / 低显存)
    • 15.4 Docker 部署 Agent 服务
  • 十六、测试
  • 十七、生产化清单
  • 十八、常见陷阱
  • 十九、总结
  • 附:完整项目结构
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档