快速开始:首个 Hermes 风格 Agent
本指南在 可运行代码 与 决策解释 之间建立桥梁:你会完成环境准备、构建天气 Agent、理解 Hermes 标签解析循环、用 Tool Call Inspector 建立可观测心智,并实现 PDF → 摘要 → 计算器 文件处理链及 Retry/Fallback。全文假设 Python 3.10+;Node 栈读者可对照 LangChain.js API,结构相同。
打开 Tool Call Inspector 与子站 Code Sandbox,把本文每一步的 model_raw 与解析结果对照粘贴,比单读文档快三倍。
一、环境准备
1.1 硬性要求
| 项目 | 要求 | 说明 |
|---|---|---|
| Python | 3.10+ | 需 match 语句、类型联合语法 |
| 包管理 | venv 或 uv | 隔离依赖,避免污染系统 Python |
| 模型 API | OpenAI 兼容 | OpenAI、DeepSeek、硅基流动等;或本地 Ollama |
| 推荐模型 | Hermes-2-Pro、Qwen2.5、GPT-4o mini | 需稳定 function calling 或 Hermes 标签输出 |
| 环境变量 | OPENAI_API_KEY | 若用 Ollama:OPENAI_API_BASE=http://localhost:11434/v1 |
1.2 创建虚拟环境并安装依赖
cd ~/hermes-lab
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -U pip
pip install langchain-openai langchain-core pydantic pypdf
| 包 | 用途 |
|---|---|
| langchain-openai | ChatOpenAI + bind_tools |
| langchain-core | @tool、消息类型 |
| pydantic | 参数校验、Tool Schema |
| pypdf | 文件处理 Agent 读取 PDF(Day 1 实战) |
1.3 验证 API 连通
# verify_api.py — 运行通过再写 Agent
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
print(llm.invoke("回 复 OK 两个字母").content)
若报错 AuthenticationError,检查 API Key;若 Connection error,检查 OPENAI_API_BASE 与网络。
1.4 项目目录建议
hermes-lab/
├── .env # OPENAI_API_KEY=sk-...
├── agents/
│ ├── weather_agent.py
│ ├── hermes_parser_loop.py
│ └── file_pipeline_agent.py
├── tools/
│ └── registry.py
└── tests/
└── test_tool_selection.py
不要把 Key 提交进 Git;.env 加入 .gitignore。
二、首个天气 Agent(LangChain + OpenAI Functions 路径)
本节使用 OpenAI Functions 路径(LangChain 默认),因为 SDK 帮你处理了 tool_calls 解析。第三节再切换到 Hermes 标签 手写循环——两种路径你都要会。
2.1 定义工具:Passive vs Active
# tools/weather.py
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""当用户询问某城市当前天气、温度、是否下雨时调用。
不要用于历史天气统计或地震查询。
参数 city: 中 国城市名,如「北京」「上海」,不要带「市」后缀。"""
# 演示:真实项目替换为 HTTP 天气 API
mock_db = {
"北京": "晴,25°C,湿度 40%,北风 2 级",
"上海": "多云,28°C,湿度 65%,东南风 3 级",
}
key = city.replace("市", "").strip()
if key not in mock_db:
return f"ERROR: 未找到城市 {city},请确认中文名"
return f"{key}:{mock_db[key]}"
@tool
def get_weather_backup(city: str) -> str:
"""仅当 get_weather 返回 ERROR 或超时时调用,作为备用数据源。"""
return f"{city}(备用源):阴,24°C,数据可能有延迟"
为何 Active 描述重要: 用户问「北京今天适合跑步吗」——模型需识别这是 天气意图,不是闲聊。Passive 描述「查询天气」容易被忽略;Active 的「当用户询问当前天气…」提高触发率。
2.2 绑定工具并手动执行一轮
# agents/weather_agent.py
import json
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
from tools.weather import get_weather, get_weather_backup
TOOLS = {t.name: t for t in [get_weather, get_weather_backup]}
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools([get_weather, get_weather_backup])
messages = [HumanMessage(content="北京今天天气怎么样?")]
response = llm_with_tools.invoke(messages)
# 模型为何选 get_weather?见 2.4 节
if response.tool_calls:
messages.append(response)
for tc in response.tool_calls:
fn = TOOLS[tc["name"]]
result = fn.invoke(tc["args"])
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
final = llm_with_tools.invoke(messages)
print(final.content)
else:
print(response.content)
2.3 模型为什么选 get_weather 而不是 get_weather_backup?
逐步拆解 注意力 + 描述 + 用户句 三因素:
| 步骤 | 模型内部逻辑(教学化表述) | 你可验证的信号 |
|---|---|---|
| 1 | 解析「北京」为 city 实体 | tool_calls[0].args.city |
| 2 | 匹配「天气怎么样」→ 当前天气类 | name=get_weather |
| 3 | backup 描述含「仅当…失败」→ 前提不满足 | 未选 backup |
| 4 | 无 tool 时应用 Constraints 禁止编造 | 不应直接胡编温度 |
在 Tool Call Inspector 粘贴 response 的 JSON,确认 tool_calls[0].function.name 与 arguments。
对照实验(建议自做)
- 把
get_weather改成 Passive 描述,同样问句再跑 10 次,记录是否出现 不调用工具直接编造。 - 增加无关工具
search_web,看是否误触发——若误触发,在search_web加 WHEN NOT「不要用于已知城市天气」。
2.4 完整可运行脚本(复制即跑)
#!/usr/bin/env python3
"""weather_agent_demo.py — 单轮工具调用 + 自然语言汇总"""
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def get_weather(city: str) -> str:
"""当用户询问某城市当前天气时调用。参数 city: 中文城市名。"""
data = {"北京": "晴 25°C", "上海": "多云 28°C"}
c = city.replace("市", "")
return data.get(c, f"ERROR: 未知城市 {c}")
@tool
def get_weather_backup(city: str) -> str:
"""仅当 get_weather 返回 ERROR 时调用。"""
return f"{city} 备用: 阴 24°C"
tools = [get_weather, get_weather_backup]
by_name = {t.name: t for t in tools}
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0).bind_tools(tools)
msgs = [HumanMessage("上海今天天气如何?")]
r1 = llm.invoke(msgs)
if not r1.tool_calls:
print(r1.content)
raise SystemExit(0)
msgs.append(r1)
for tc in r1.tool_calls:
out = by_name[tc["name"]].invoke(tc["args"])
msgs.append(ToolMessage(content=out, tool_call_id=tc["id"]))
r2 = llm.invoke(msgs)
print(r2.content)
三、Hermes 标签格式与解析循环
Hermes 模型(及若干开源权重)在 assistant 文本 中输出:
<tool_call>
{"name": "get_weather", "arguments": {"city": "北京"}}
</tool_call>
而非 API 级 tool_calls 字段。你必须实现 解析 → 执行 → 回灌 循环。
3.1 解析器核心
# agents/hermes_parser_loop.py
import json
import re
from typing import Any
TOOL_CALL_RE = re.compile(
r"<tool_call>\s*(\{.*?\})\s*</tool_call>",
re.DOTALL,
)
def parse_tool_calls(text: str) -> list[dict[str, Any]]:
calls = []
for m in TOOL_CALL_RE.finditer(text):
raw = m.group(1)
try:
obj = json.loads(raw)
except json.JSONDecodeError:
obj = json.loads(raw.replace("'", '"')) # 容错
calls.append({
"name": obj["name"],
"arguments": obj.get("arguments") or json.loads(obj.get("arguments", "{}")),
})
return calls
def format_tool_response(name: str, result: str) -> str:
payload = json.dumps({"name": name, "result": result}, ensure_ascii=False)
return f"<tool_response>\n{payload}\n</tool_response>"
3.2 多轮循环直到无 tool_call
MAX_TURNS = 8
def run_hermes_agent(llm, tools: dict, user: str, system: str) -> str:
messages = [
{"role": "system", "content": system},
{"role": "user", "content": user},
]
for _ in range(MAX_TURNS):
assistant_text = llm.invoke(messages) # 你的 LLM 封装,返回 str
messages.append({"role": "assistant", "content": assistant_text})
calls = parse_tool_calls(assistant_text)
if not calls:
return assistant_text # 最终自然语言
for call in calls:
name, args = call["name"], call["arguments"]
if name not in tools:
result = f"ERROR: unknown tool {name}"
else:
result = tools[name](**args)
messages.append({
"role": "user",
"content": format_tool_response(name, result),
})
return "ERROR: max turns exceeded"
3.3 System Prompt 中如何声明 Hermes 工具
OpenAI 路径把 Schema 放在 API tools;Hermes 开源权重常把 工具列表附录 写进 System:
你可以使用以下工具,需要时用 <tool_call> JSON </tool_call> 格式调用:
- get_weather(city: str): 当用户问当前天气时调用
- calculator(expression: str): 当需要精确数学计算时调用
收到工具结果后会以 <tool_response> 形式提供给你。
切勿 在同一对话混用 OpenAI bind_tools 与 Hermes 标签解析——模型输出格式会不稳定。