Hermes Function Calling 实战:从原理到 XML 工具调用全流程

深入拆解 Hermes 模型的 XML 工具调用机制,从 tools 标签定义到 tool_call 和 tool_response 的完整交互流程,附代码示例和 OpenAI 格式对比。

目录

  1. 为什么要搞工具调用
  2. Hermes 工具调用的核心机制
  3. 工具定义的详细格式
  4. tool_call:模型怎么发起调用
  5. tool_response:把结果喂回去
  6. 用 Python 实现完整的工具调用循环
  7. 和 OpenAI Function Calling 的差异对比
  8. 提升工具调用准确率的技巧
  9. 错误处理和防御性编程
  10. 构建多轮 Agent Loop
  11. 收尾
  12. 延伸阅读:OpenClaw 社区资源

为什么要搞工具调用

大语言模型再聪明,也有搞不定的事——算术不精确、没法查实时数据、不能操作外部系统。解决方案就是给模型配上”工具”,让它在需要的时候调用外部函数来补齐能力。

这就是 Function Calling(函数调用),或者更通用的叫法:Tool Use(工具使用)。

OpenAI 在 GPT-3.5/4 里率先把这个能力产品化了,搞了一套 JSON 格式的函数定义和调用协议。但在开源世界里,不同模型对工具调用的实现方式差异很大。Hermes 走的是 XML 标签 这条路线,如果你之前只接触过 OpenAI 的 JSON 方案,刚上手可能会有点不习惯。

这篇文章会把 Hermes 的工具调用机制从头到尾拆给你看。

Hermes 工具调用的核心机制

Hermes 使用一组特定的 XML 标签来实现工具调用的完整生命周期:

  • <tools> — 定义可用工具的列表
  • <tool_call> — 模型发出的调用请求
  • <tool_response> — 外部系统返回的结果

整个流程就是:你在 system prompt 里用 <tools> 告诉模型有哪些工具可以用,模型在对话中判断什么时候该调用工具,输出 <tool_call> 标签,你的代码解析这个标签、执行对应函数、把结果包在 <tool_response> 里喂回去,模型拿到结果后继续生成回复。

比起 OpenAI 的纯 JSON 方案,这种 XML 方式有一个好处:标签的开闭结构天然适合流式解析。你不用等整个 JSON 对象生成完才能判断模型是否发起了工具调用,看到 <tool_call> 开标签就能立刻准备了。

工具定义的详细格式

工具定义放在 system prompt 的 <tools> 标签内,每个工具用 JSON 格式描述(没错,外层是 XML,里面是 JSON,有点混搭):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<tools>
[
{
"name": "get_weather",
"description": "获取指定城市的当前天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如北京、上海"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认celsius"
}
},
"required": ["city"]
}
},
{
"name": "search_documents",
"description": "在知识库中搜索相关文档",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词"
},
"top_k": {
"type": "integer",
"description": "返回结果数量",
"default": 5
}
},
"required": ["query"]
}
}
]
</tools>

几个关键点:

  1. parameters 结构 基本遵循 JSON Schema 规范,和 OpenAI 的 function calling 参数定义兼容
  2. description 要写好 — 模型就是根据 description 来判断什么场景该用什么工具的,描述越精确,调用越准确
  3. required 字段 明确标出必填参数,减少模型遗漏参数的概率

tool_call:模型怎么发起调用

当模型判断需要调用工具时,会在生成内容中插入 <tool_call> 标签:

1
2
3
<tool_call>
{"name": "get_weather", "arguments": {"city": "北京", "unit": "celsius"}}
</tool_call>

注意几件事:

  • name 对应 tools 里定义的函数名
  • arguments 里的参数要符合之前定义的 schema
  • 模型可能在一次回复里发起多个 tool_call(并行调用)

多工具调用的情况长这样:

1
2
3
4
5
6
7
8
9
我来帮你查一下这两个城市的天气。

<tool_call>
{"name": "get_weather", "arguments": {"city": "北京"}}
</tool_call>

<tool_call>
{"name": "get_weather", "arguments": {"city": "上海"}}
</tool_call>

模型可能在 tool_call 前面加一段自然语言说明,也可能直接输出标签。你的解析代码要能处理这两种情况。

tool_response:把结果喂回去

你的后端拿到 tool_call 后,执行对应的函数,把结果用 <tool_response> 包起来放到下一轮对话里:

1
2
3
<tool_response>
{"city": "北京", "temperature": 22, "unit": "celsius", "condition": "晴", "humidity": 45}
</tool_response>

模型收到 tool_response 后,会基于这些数据生成最终的自然语言回复,比如告诉用户北京现在 22 度、晴天、湿度 45%。

用 Python 实现完整的工具调用循环

下面这段代码展示了一个完整的工具调用循环,基于 llama-cpp-python(如果你用 Ollama 部署 Hermes,思路一样,只是调用接口不同):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import json
import re
from llama_cpp import Llama

# 加载模型
llm = Llama(
model_path="./Hermes-3-Llama-3.1-8B-Q5_K_M.gguf",
n_ctx=8192,
n_gpu_layers=-1,
chat_format="chatml" # Hermes 使用 ChatML 格式
)

# 定义工具
tools_definition = """
<tools>
[
{
"name": "calculate",
"description": "执行数学计算,支持四则运算和常见数学函数",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式,如 2+3*4 或 sqrt(16)"
}
},
"required": ["expression"]
}
},
{
"name": "get_stock_price",
"description": "获取指定股票的当前价格",
"parameters": {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "股票代码,如 AAPL、TSLA"
}
},
"required": ["symbol"]
}
}
]
</tools>
"""


def execute_tool(name: str, arguments: dict) -> str:
"""实际的工具实现"""
if name == "calculate":
try:
import math
allowed = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
result = eval(arguments["expression"], {"__builtins__": {}}, allowed)
return json.dumps({"result": result})
except Exception as e:
return json.dumps({"error": str(e)})
elif name == "get_stock_price":
# 实际项目中这里调用真正的股票 API
mock_prices = {"AAPL": 189.50, "TSLA": 245.30, "GOOGL": 175.20}
symbol = arguments["symbol"]
price = mock_prices.get(symbol)
if price:
return json.dumps({"symbol": symbol, "price": price, "currency": "USD"})
return json.dumps({"error": f"未找到股票 {symbol}"})
return json.dumps({"error": f"未知工具: {name}"})


def parse_tool_calls(text: str) -> list:
"""从模型输出中提取所有 tool_call"""
pattern = r"<tool_call>\s*(.*?)\s*</tool_call>"
matches = re.findall(pattern, text, re.DOTALL)
calls = []
for match in matches:
try:
call = json.loads(match)
calls.append(call)
except json.JSONDecodeError:
continue
return calls


def chat_with_tools(user_message: str):
system_prompt = f"""You are a helpful assistant with access to the following tools:
{tools_definition}

When you need to use a tool, output a <tool_call> tag with the function name and arguments.
After receiving a <tool_response>, use the information to answer the user.
Always respond in Chinese."""

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

# 第一轮:让模型决定是否调用工具
response = llm.create_chat_completion(messages=messages, max_tokens=1024)
assistant_msg = response["choices"][0]["message"]["content"]

# 解析工具调用
tool_calls = parse_tool_calls(assistant_msg)

if not tool_calls:
return assistant_msg

# 执行工具并收集结果
messages.append({"role": "assistant", "content": assistant_msg})

for call in tool_calls:
result = execute_tool(call["name"], call.get("arguments", {}))
tool_response = f"<tool_response>\n{result}\n</tool_response>"
messages.append({"role": "user", "content": tool_response})

# 第二轮:基于工具结果生成最终回复
final_response = llm.create_chat_completion(messages=messages, max_tokens=1024)
return final_response["choices"][0]["message"]["content"]


# 测试
print(chat_with_tools("帮我算一下 (15 * 23) + sqrt(144) 等于多少"))
print(chat_with_tools("AAPL 现在什么价格?"))

核心思路就是一个循环:生成 → 检测工具调用 → 执行 → 喂回结果 → 再生成。实际生产环境中可能需要多轮工具调用(模型拿到一个结果后又发起另一个调用),把这个循环跑多次就行。

和 OpenAI Function Calling 的差异对比

如果你之前用过 OpenAI 的方案,这里做一个直观对比:

维度 OpenAI 方案 Hermes XML 方案
工具定义位置 API 参数中的 tools 字段 System Prompt 的 <tools> 标签
定义格式 JSON XML 包裹 JSON
调用格式 API 响应的 tool_calls 字段 文本中的 <tool_call> 标签
结果回传 消息角色 tool <tool_response> 标签
并行调用 原生支持 多个 <tool_call> 标签
流式解析 需要拼接 JSON delta 检测 XML 开闭标签即可
强制调用 tool_choice: required 在 prompt 里指示

一个比较大的实际差异是:OpenAI 的方案在 API 层做了结构化处理,tool_calls 是独立字段,不会和普通文本混在一起。Hermes 的方案里,tool_call 标签是混在模型输出文本中的,需要自己做解析和分离。

好处是灵活——模型可以在调用工具的同时输出解释性文字(”让我查一下” + tool_call),用户体验更自然。麻烦是你的代码必须处理各种边界情况,比如标签不完整、JSON 格式错误这类问题。

提升工具调用准确率的技巧

实际使用中,Hermes 的工具调用不是百分百稳定的,尤其是小参数量版本(8B)。下面几个方法实测有效:

1. 工具描述要具体,别写模糊表述

1
2
3
4
5
// 差的写法
{"name": "search", "description": "搜索东西"}

// 好的写法
{"name": "search_product", "description": "在商品数据库中按关键词搜索商品,返回商品名称、价格和库存"}

2. 给参数加 example 信息

虽然 JSON Schema 标准里没有 example 字段,但 Hermes 能理解它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"name": "send_email",
"description": "发送电子邮件",
"parameters": {
"type": "object",
"properties": {
"to": {
"type": "string",
"description": "收件人邮箱地址,例如 user@example.com"
},
"subject": {
"type": "string",
"description": "邮件主题"
},
"body": {
"type": "string",
"description": "邮件正文,支持纯文本"
}
},
"required": ["to", "subject", "body"]
}
}

3. 在 System Prompt 里加调用规范

在 tools 定义后面追加一段指令,明确告诉模型怎么输出调用格式:

1
2
3
4
5
6
7
8
9
当你需要使用工具时,请严格按照以下格式输出:
<tool_call>
{"name": "工具名", "arguments": {"参数名": "参数值"}}
</tool_call>

注意:
- arguments 必须是合法的 JSON 对象
- 参数名和类型必须和工具定义一致
- 如果不需要调用工具,直接回复即可,不要输出空的 tool_call

4. 用 few-shot 示例引导

在 system prompt 或前几轮对话中放一个正确调用示例,让模型跟着格式走:

1
2
3
4
5
6
示例对话:
用户:今天上海天气怎么样?
助手:我来查一下上海的天气。
<tool_call>
{"name": "get_weather", "arguments": {"city": "上海"}}
</tool_call>

错误处理和防御性编程

生产环境里,工具调用会遇到各种异常。你需要有一套健壮的错误处理机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def robust_tool_execution(call: dict) -> str:
"""带错误处理的工具执行"""
name = call.get("name", "")
arguments = call.get("arguments", {})

# 检查工具是否存在
available_tools = {"calculate", "get_stock_price", "get_weather"}
if name not in available_tools:
return json.dumps({
"error": f"工具 {name} 不存在",
"available_tools": list(available_tools)
})

# 参数类型校验(模型有时候会把 arguments 输出为字符串)
try:
if not isinstance(arguments, dict):
arguments = json.loads(arguments) if isinstance(arguments, str) else {}
except json.JSONDecodeError:
return json.dumps({"error": "参数格式错误,无法解析为 JSON"})

# 执行并捕获异常
try:
result = execute_tool(name, arguments)
return result
except TimeoutError:
return json.dumps({"error": f"工具 {name} 执行超时"})
except Exception as e:
return json.dumps({"error": f"工具执行异常: {str(e)}"})

让错误信息以结构化方式返回给模型,模型通常能理解并给用户一个合理的回复(比如”查询失败,请稍后重试”),而不是编造数据。

构建多轮 Agent Loop

工具调用最终是为了构建 Agent——一个能自主决策、多步执行的智能体。关于 Hermes 模型的基本介绍在之前的文章里已经讲过了,这里关注怎么基于工具调用搭 Agent 循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def agent_loop(user_message: str, max_iterations: int = 5):
"""支持多轮工具调用的 Agent 循环"""
messages = [
{"role": "system", "content": system_prompt_with_tools},
{"role": "user", "content": user_message}
]

for i in range(max_iterations):
response = llm.create_chat_completion(
messages=messages, max_tokens=2048
)
assistant_msg = response["choices"][0]["message"]["content"]
messages.append({"role": "assistant", "content": assistant_msg})

tool_calls = parse_tool_calls(assistant_msg)

if not tool_calls:
# 没有工具调用,模型认为可以直接回答了
return assistant_msg

# 执行所有工具调用
for call in tool_calls:
result = robust_tool_execution(call)
messages.append({
"role": "user",
"content": f"<tool_response>\n{result}\n</tool_response>"
})

return "达到最大迭代次数,Agent 循环终止。"

max_iterations 是安全阀,防止模型陷入无限调用循环。实际场景中 3-5 次迭代能覆盖大部分需求。

如果你想搭更完善的 Agent 系统,可以去 cocoloop 社区看看其他人分享的生产级实现方案,有不少基于 Hermes 的 Agent 框架值得参考。

收尾

Hermes 的 XML 工具调用方案和 OpenAI 的 JSON 方案表面上差别挺大,但底层逻辑相通——定义工具、检测调用、执行函数、回传结果。掌握了这套机制,就能让 Hermes 从一个”只会说话”的模型变成”能做事”的 Agent。

几个核心要点回顾:

  • <tools> 标签在 system prompt 里定义工具,参数用 JSON Schema 描述
  • 解析 <tool_call> 提取函数名和参数,执行后用 <tool_response> 回传
  • 工具描述写得好坏直接影响调用准确率
  • 做好错误处理,别让一个工具失败拖垮整个对话
  • 多轮调用循环是构建 Agent 的基础

下一步可以看看 Hermes JSON Mode,了解怎么让模型稳定输出结构化数据——这和工具调用是一对好搭档。

延伸阅读:OpenClaw 社区资源

本文由 CocoLoop 中文社区出品。如果你在研究 AI Agent 与主流模型的工程化落地,姊妹站 OpenClaw 中文社区 也许会有帮助:

参与讨论

对这篇文章有疑问或想法?cocoloop 社区有不少开发者在讨论 Hermes 相关话题,欢迎加入交流。

前往 cocoloop 社区 →