Hermes Agent 怎么用?从零搭建你的第一个 AI Agent(2026 实战教程)

11 阅读1分钟

上个月我在做一个自动化客服项目,需要让 LLM 自己决定什么时候查数据库、什么时候调外部 API、什么时候直接回复用户。试了 LangChain 的 Agent,配置太重了,光依赖就装了二十多个包。后来同事推荐我看看 Hermes Agent——NousResearch 搞的那套轻量级 Agent 框架,基于 function calling 的思路但更灵活。折腾了两天,确实比我预期的简单不少,这篇把完整流程记录下来。

Hermes Agent 是 NousResearch 推出的轻量级 AI Agent 框架,核心思路是利用 Hermes 系列模型原生的 tool-use 能力,让 LLM 在对话中自主决策调用哪些工具,开发者只需要定义工具函数和系统提示词就能跑起来。

先说结论

维度Hermes AgentLangChain Agent原生 function calling
上手时间30 分钟2-3 小时1 小时
依赖数量3 个包20+ 个包1 个包
多步推理原生支持需要配 chain手动循环
模型兼容OpenAI 兼容接口都行绑定特定 provider看模型支持
可控性高(tool 定义透明)中(抽象层太多)最高

适合场景:中等复杂度的 Agent(3-10 个工具),不想引入重框架,想快速出原型。

环境准备

Python 3.10+,装这几个包就够了:

pip install openai pydantic rich

没错就三个。Hermes Agent 的核心逻辑其实不需要专门的 SDK,它本质上是一套 prompt 工程 + tool calling 协议,任何兼容 OpenAI 接口的服务都能跑。

你需要一个能调 Hermes 系列模型(或者任何支持 function calling 的模型)的 API Key。我这边用的是 hermes-3-llama-3.1-8b 做开发测试,生产环境切 Claude Sonnet 4.6 效果更稳。

方案一:最小化 Hermes Agent(单工具)

先搞一个最简单的——让 Agent 能查天气:

import json
from openai import OpenAI

client = OpenAI(
 api_key="your-key",
 base_url="https://api.ofox.ai/v1"
)

# 定义工具
tools = [
 {
 "type": "function",
 "function": {
 "name": "get_weather",
 "description": "获取指定城市的当前天气信息",
 "parameters": {
 "type": "object",
 "properties": {
 "city": {
 "type": "string",
 "description": "城市名称,如 Beijing, Tokyo"
 }
 },
 "required": ["city"]
 }
 }
 }
]

# 模拟天气 API
def get_weather(city: str) -> str:
 # 实际项目换成真实 API 调用
 fake_data = {"Beijing": "晴 28°C", "Tokyo": "多云 22°C", "Singapore": "雷阵雨 31°C"}
 return fake_data.get(city, f"{city}: 数据暂无")

# Agent 主循环
def run_agent(user_input: str):
 messages = [
 {"role": "system", "content": "你是一个helpful助手。需要时使用工具获取信息,不要编造数据。"},
 {"role": "user", "content": user_input}
 ]
 
 while True:
 response = client.chat.completions.create(
 model="hermes-3-llama-3.1-8b",
 messages=messages,
 tools=tools,
 tool_choice="auto"
 )
 
 msg = response.choices[0].message
 messages.append(msg)
 
 # 没有工具调用,直接返回
 if not msg.tool_calls:
 return msg.content
 
 # 执行工具调用
 for tool_call in msg.tool_calls:
 func_name = tool_call.function.name
 args = json.loads(tool_call.function.arguments)
 
 if func_name == "get_weather":
 result = get_weather(args["city"])
 else:
 result = f"未知工具: {func_name}"
 
 messages.append({
 "role": "tool",
 "tool_call_id": tool_call.id,
 "content": result
 })

# 测试
print(run_agent("东京现在天气怎么样?"))

跑起来大概是这样的输出:

根据查询结果,东京现在的天气是多云,气温22°C。适合出门,建议带一件薄外套。

关键点:那个 while True 循环就是 Agent 的核心——模型决定要不要调工具,调完之后把结果喂回去,模型再决定是继续调还是直接回复。

方案二:多工具 + 多步推理 Agent

实际项目里一个工具肯定不够。我那个客服项目需要:查订单、查库存、发优惠券、转人工。下面是简化版:

import json
from openai import OpenAI
from datetime import datetime

client = OpenAI(
 api_key="your-key",
 base_url="https://api.ofox.ai/v1"
)

tools = [
 {
 "type": "function",
 "function": {
 "name": "query_order",
 "description": "根据订单号查询订单状态和物流信息",
 "parameters": {
 "type": "object",
 "properties": {
 "order_id": {"type": "string", "description": "订单号"}
 },
 "required": ["order_id"]
 }
 }
 },
 {
 "type": "function",
 "function": {
 "name": "check_inventory",
 "description": "查询某商品的库存数量",
 "parameters": {
 "type": "object",
 "properties": {
 "product_name": {"type": "string", "description": "商品名称"}
 },
 "required": ["product_name"]
 }
 }
 },
 {
 "type": "function",
 "function": {
 "name": "issue_coupon",
 "description": "给用户发放优惠券,仅在用户明确不满或投诉时使用",
 "parameters": {
 "type": "object",
 "properties": {
 "user_id": {"type": "string"},
 "amount": {"type": "number", "description": "优惠券金额(元)"}
 },
 "required": ["user_id", "amount"]
 }
 }
 },
 {
 "type": "function",
 "function": {
 "name": "transfer_human",
 "description": "转接人工客服,仅在无法解决问题时使用",
 "parameters": {
 "type": "object",
 "properties": {
 "reason": {"type": "string", "description": "转接原因"}
 },
 "required": ["reason"]
 }
 }
 }
]

# 工具实现(实际接数据库/微服务)
def query_order(order_id):
 return json.dumps({"order_id": order_id, "status": "已发货", "tracking": "SF1234567890", "eta": "2026-04-28"})

def check_inventory(product_name):
 return json.dumps({"product": product_name, "stock": 23, "warehouse": "华东仓"})

def issue_coupon(user_id, amount):
 return json.dumps({"success": True, "coupon_code": "SORRY20", "expires": "2026-05-30"})

def transfer_human(reason):
 return json.dumps({"queued": True, "position": 3, "wait_time": "约2分钟"})

TOOL_MAP = {
 "query_order": lambda args: query_order(args["order_id"]),
 "check_inventory": lambda args: check_inventory(args["product_name"]),
 "issue_coupon": lambda args: issue_coupon(args["user_id"], args["amount"]),
 "transfer_human": lambda args: transfer_human(args["reason"]),
}

def run_customer_agent(user_input: str, user_id: str = "u_10086", max_turns: int = 5):
 system_prompt = f"""你是电商客服 Agent。当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}
用户ID: {user_id}

规则:
1. 先尝试用工具解决问题
2. 发优惠券前必须确认用户确实遇到了问题
3. 超过3轮工具调用仍无法解决,转人工
4. 回复简洁友好,不要复述工具返回的原始JSON"""

 messages = [
 {"role": "system", "content": system_prompt},
 {"role": "user", "content": user_input}
 ]
 
 turn = 0
 while turn < max_turns:
 response = client.chat.completions.create(
 model="claude-sonnet-4.6", # 生产用 Sonnet,推理更稳
 messages=messages,
 tools=tools,
 tool_choice="auto"
 )
 
 msg = response.choices[0].message
 messages.append(msg)
 
 if not msg.tool_calls:
 return msg.content
 
 for tool_call in msg.tool_calls:
 func_name = tool_call.function.name
 args = json.loads(tool_call.function.arguments)
 
 executor = TOOL_MAP.get(func_name)
 if executor:
 result = executor(args)
 else:
 result = json.dumps({"error": f"未知工具 {func_name}"})
 
 messages.append({
 "role": "tool",
 "tool_call_id": tool_call.id,
 "content": result
 })
 
 turn += 1
 
 return "抱歉,我暂时无法处理您的问题,正在为您转接人工客服。"


# 测试多步推理
print(run_customer_agent("我的订单 ORD-2026042201 到哪了?说好3天到的已经5天了"))

实测下来,Claude Sonnet 4.6 在这种多工具场景下的表现比 Hermes 3 8B 好很多——8B 模型偶尔会"幻觉"出不存在的工具参数,Sonnet 基本不会。

sequenceDiagram
 participant U as 用户
 participant A as Agent (LLM)
 participant T1 as query_order
 participant T2 as issue_coupon
 
 U->>A: 订单 ORD-xxx 到哪了?已经5天了
 A->>T1: query_order("ORD-xxx")
 T1-->>A: {status: 已发货, eta: 4月28}
 A->>A: 判断: 用户不满,且确实超时
 A->>T2: issue_coupon("u_10086", 10)
 T2-->>A: {success: true, code: SORRY20}
 A-->>U: 您的包裹预计28号到,已补偿10元券

踩坑记录

坑 1:tool_call_id 不能瞎填

一开始我图省事,tool response 的 tool_call_id 随便写了个 uuid,结果报错:

Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'.", 'type': 'invalid_request_error'}}

必须用模型返回的那个 tool_call.id,一一对应。

坑 2:Hermes 3 8B 的 JSON 输出偶尔不合法

大概 5% 的概率,8B 模型返回的 function.arguments 不是合法 JSON。我加了个 try-except:

try:
 args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError as e:
 # 尝试修复常见问题:尾部多逗号、单引号
 fixed = tool_call.function.arguments.replace("'", '"').rstrip(",}") + "}"
 args = json.loads(fixed)

这个 workaround 挺丑的,换大参数模型(70B 或 Sonnet)基本不会遇到。

坑 3:max_tokens 设太小导致工具调用被截断

默认 max_tokens 如果只给 256,复杂的多工具调用会被截断,模型返回的 tool_calls 数组不完整。我现在统一设 2048,反正 Agent 的中间步骤用户看不到,不心疼 token。

进阶:给 Agent 加记忆

最简单的做法——把 messages 列表持久化。但对话超过 20 轮之后 token 消耗会爆炸。我目前的方案是每 10 轮做一次摘要压缩:

def compress_history(messages: list, client) -> list:
 """把前面的对话压缩成一段摘要"""
 history_text = "\n".join([f"{m['role']}: {m.get('content', '[tool_call]')}" for m in messages[1:-4]])
 
 summary = client.chat.completions.create(
 model="hermes-3-llama-3.1-8b", # 摘要用小模型省钱
 messages=[
 {"role": "system", "content": "用3-5句话概括以下对话历史的关键信息,保留所有具体数据(订单号、金额等)"},
 {"role": "user", "content": history_text}
 ],
 max_tokens=300
 )
 
 compressed = [
 messages[0], # system prompt 保留
 {"role": "system", "content": f"[对话历史摘要] {summary.choices[0].message.content}"},
 *messages[-4:] # 最近4条保留原文
 ]
 return compressed

小结

Hermes Agent 的核心就是 tool calling 的循环调用,没有什么黑魔法。框架的价值在于:约定了工具描述的格式,约定了多步推理的循环逻辑,约定了何时停止。

我也不确定 Hermes 3 8B 是不是做 Agent 的最佳小模型选择——DeepSeek V4 预览版的 function calling 准确率据说也挺高,等正式版出来我再测测。

实际工程里最重要的几件事:工具描述写清楚(模型靠这个决定调不调)、错误处理要兜底(网络超时、JSON 解析失败)、设置最大循环次数(防止 Agent 陷入死循环烧你的钱)。我那个客服项目跑了两周,P95 延迟在 1.2s 左右,大部分时间花在模型推理上,工具执行本身很快。