Hermes Agent开发实战:从API调用到Tool Use
Agent 工程的第一步不是写循环,而是搞清楚怎么调大模型。一篇讲透 OpenAI、Claude、DeepSeek 三大主流厂商的 API 的异同,从基础调用到多轮对话再到工具调用。
Hermes Agent——一个与 Claude Code 类似的开源 Harness 工程实践项目。它适合学习Harness 工程,因为完全开源、架构清晰。但在深入 Hermes Agent 的代码之前,我们需要先掌握一项基础能力:调用大模型 API。这是所有 Agent 和 Harness 工程的起点——无论多复杂的 Agent 循环,最终都要落到一次次的模型调用上。本文将带你从零开始,搞懂 OpenAI、Claude、DeepSeek 三大主流模型 API 的调用方式、多轮对话机制,以及 Tool Use 的完整流程。
大模型 API调用:相似但不相同
调用大模型主要使用两家的SDK:OpenAI、Anthropic(Claude),其中OpenAI因发布的最早,一定程度上成为了大模型调用的标准,像后来的DeepSeek 等模型API一般都是兼容OpenAI的格式。它们的 API 调用流程高度相似——获取 Key、安装 SDK、调用接口——但在细节上各有差异。
OpenAI
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-5.4",
input="Write a one-sentence bedtime story about a unicorn."
)
print(response.output_text)OpenAI 最新推荐使用 Responses API(responses.create),取代了之前的 Chat Completions API。输入用 input 参数(字符串或消息数组),输出直接取 response.output_text。
Claude (Anthropic)
import anthropic
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-opus-4-7",
max_tokens=1000,
messages=[
{
"role": "user",
"content": "What should I search for to find the latest developments in renewable energy?",
}
],
)
print(message.content)OpenAI-compatible(以DeepSeek为例)
import os
from openai import OpenAI
client = OpenAI(
api_key='OPENAI_API_KEY',
base_url='OPENAI_BASE_URL'
)
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "Hello"},
],
stream=False
)
print(response.choices[0].message.content)DeepSeek 没有自己的 SDK,直接用 OpenAI SDK + 自定义 base_url 即可。注意 DeepSeek 使用的是 OpenAI 旧版 Chat Completions API(chat.completions.create),这也是很多国产模型的做法——兼容 OpenAI 格式,降低迁移成本。
一句话总结差异
| 维度 | OpenAI(Responses API) | Claude | DeepSeek |
|---|---|---|---|
| SDK | openai | anthropic | 复用 openai |
| 调用方法 | responses.create | messages.create | chat.completions.create |
| 系统指令 | instructions 参数 | 顶层 system 参数 | messages 中的 system 角色 |
| 输入参数 | input(字符串或消息数组) | messages 列表 | messages 列表 |
| 响应取值 | response.output_text | message.content[0].text | response.choices[0].message.content |
max_tokens | 可选(有默认值) | 必填 | 可选 |
LiteLLM:一套代码调所有模型
每家 API 格式不同,难道要为每个模型写一套代码?
不用。LiteLLM 是一个统一调用层,底层自动转换不同厂商的 API 格式,对外统一暴露 OpenAI 兼容接口。
from openai import OpenAI
# 指向 LiteLLM 代理,一个入口调所有模型
client = OpenAI(
api_key="API_KEY",
base_url="BASE_URL",
)
# 只需切换 model 参数
for model in ["gpt-5", "claude-sonnet-4.6", "deepseek-v3.2"]:
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "用一句话介绍你自己。"}],
)
print(f"[{model}] {response.choices[0].message.content}")三个模型、三个厂商,代码完全一样,只改 model 名。这就是 LiteLLM 的价值——在 Harness 工程中,它让你的工具层和编排层不需要关心底层用的是哪家模型。
核心参数对比
不管是哪家 API,核心就三个参数:model、messages、tools。
model:选哪个模型
直接传模型名称字符串。OpenAI 是 gpt-5.4,Claude 是 claude-opus-4-6,DeepSeek 是 deepseek-chat。通过 LiteLLM 统一调用时,模型名是自己定义的。
messages:对话历史
messages 是一个列表,每条消息有 role 和 content 两个字段。
| 参数 | OpenAI(Responses API) | Claude |
|---|---|---|
| 输入格式 | input 可以是字符串或消息数组 | messages 列表,每条含 role + content |
| 消息角色 | developer、user、assistant | 仅 user 和 assistant(system 用顶层参数) |
| content 格式 | 字符串或内容块数组 | 字符串或内容块数组 |
content 可以是简单字符串,也可以是内容块数组——支持文本、图片、文件等多种类型。这种设计让模型能处理多模态输入,agent 可以根据需要传入不同类型的内容。
tools:赋予模型行动能力
tools 是工具列表,赋予模型调用外部能力(搜索、文件操作、API 调用等)。这是模型从"回答者"变成"执行者"的关键。
| 工具能力 | 说明 | OpenAI | Claude |
|---|---|---|---|
| Tool Use | 调用开发者自定义函数 | ✅ | ✅ |
| Web Search | 搜索互联网获取实时数据 | ✅ | ✅ |
| Computer Use | 控制计算机界面 | ✅ | ✅ |
| Shell / Bash | 执行 shell 命令 | ✅ | ✅ |
| MCP 接入 | 连接第三方服务 | ✅ | ✅ |
| File Search | 搜索上传文件内容 | ✅ | ❌ |
| Image Generation | 生成或编辑图片 | ✅ | ❌ |
| Text Editor | 内置文本编辑工具 | ❌ | ✅ |
正是因为有了 tools,我们才能构建 agent。
多轮对话:Agent 记忆的基础
单轮调用只是 hello world。真正的 agent 需要多轮对话——记住用户说过什么,逐步推进任务。
多轮对话的本质很简单:维护一个 messages 列表,每轮追加 user 和 assistant 消息。
# 第三轮请求时,messages 长这样:
messages = [
{"role": "user", "content": "我想去日本旅行,有什么推荐?"},
{"role": "assistant", "content": "推荐三个方向:1. 东京+镰仓(都市+海岸)2. 京都+大阪(古都+美食)3. 北海道(自然+温泉)"},
{"role": "user", "content": "我比较喜欢第二个,大概需要几天?"},
{"role": "assistant", "content": "京都+大阪建议 5-7 天。京都 3 天看寺庙和岚山,大阪 2 天吃道顿堀逛心斋桥。"},
{"role": "user", "content": "帮我列一个 5 天的行程大纲。"},
]
response = client.messages.create(
model="claude-sonnet-4.6",
max_tokens=1000,
system="你是一个旅行规划助手,帮用户规划旅行行程。回答简洁。",
messages=messages,
)关键观察:每一轮请求都携带完整的对话历史。
第一轮,messages 里只有一条用户消息。第二轮,三条(第一轮的 user + assistant + 新 user)。第三轮,五条。模型本身是无状态的——它不记得上一次请求。所谓"记忆",完全靠客户端维护 messages 列表来实现。
这就是为什么 token 消耗会随对话轮次增长——每次都把完整历史发过去了。这也是后续 context management(上下文管理)要解决的核心问题。
理解多轮对话的实现方式,是理解 Agent Loop、Context Management、Memory 等 agent 核心概念的基础。
Tool Use:模型学会"动手"
多轮对话让模型有了记忆,但它依然只能生成文本。Tool Use 让模型能调用工具执行任务——这是 agent 的雏形。
流程拆解
Tool Use 分三步:
用户提问 → 模型返回 tool_use(我要调这个函数)
→ 开发者执行函数
→ 把结果通过 tool_result 返回给模型
→ 模型生成最终回答用一个真实的天气查询场景来演示完整流程。代码分三部分:定义工具 → 实现工具 → 发起请求、执行工具、返回结果。
import json
import os
import anthropic
import requests
client = anthropic.Anthropic()
AMAP_KEY = os.environ["AMAP_KEY"]
# ---- 第一步:定义工具 ----
# 告诉模型有哪些函数可用,传入函数名、描述和参数 schema
tools = [
{
"name": "get_weather",
"description": "获取指定城市的当前天气信息",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 北京、上海",
},
},
"required": ["city"],
},
}
]
# ---- 第二步:实现工具 ----
# 这里调用高德天气 API 实现
def city_to_adcode(city: str) -> str | None:
resp = requests.get(
"https://restapi.amap.com/v3/geocode/geo",
params={"key": AMAP_KEY, "address": city},
timeout=5,
)
data = resp.json()
if data.get("status") == "1" and data.get("geocodes"):
return data["geocodes"][0]["adcode"]
return None
def get_weather(city: str) -> dict:
adcode = city_to_adcode(city)
if not adcode:
return {"error": f"无法识别城市「{city}」"}
resp = requests.get(
"https://restapi.amap.com/v3/weather/weatherInfo",
params={"key": AMAP_KEY, "city": adcode, "extensions": "base"},
timeout=5,
)
data = resp.json()
live = data["lives"][0]
return {
"city": live["city"],
"weather": live["weather"],
"temperature": f"{live['temperature']}°C",
"humidity": f"{live['humidity']}%",
"wind": f"{live['winddirection']}风 {live['windpower']}级",
}
# 工具注册表:动态分派,新增工具只需加一行映射
tool_registry = {
"get_weather": get_weather,
}
# ---- 第三步:发起请求、执行工具、返回结果 ----
messages = [
{"role": "user", "content": "北京和上海今天天气怎么样?"},
]
# 第一轮:发送用户消息,模型返回 tool_use
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1000,
tools=tools,
messages=messages,
)
# stop_reason 是 "tool_use" 而非 "end_turn"
# 模型在说"我还没完,先帮我执行这些工具"
print(response.stop_reason) # "tool_use"
# 把 assistant 的完整回复追加到对话历史
messages.append({"role": "assistant", "content": response.content})
# 执行工具:遍历 tool_use block,调用真实函数
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
func = tool_registry[block.name]
result = func(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result, ensure_ascii=False),
})
# 工具结果通过 user 角色的 tool_result 块返回
messages.append({"role": "user", "content": tool_results})
# 第二轮:带上完整历史,模型生成最终回答
final_response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1000,
tools=tools,
messages=messages,
)
print(final_response.content[0].text)输出类似:
🏙️ 北京 — 雾,10°C,湿度 88%,东北风 ≤3级
🏙️ 上海 — 阴,26°C,湿度 56%,西风 ≤3级这就是 Tool Use 的完整闭环:模型决定调什么、传什么参数,开发者负责执行,结果返回给模型做最终整合。
为什么用 tool_registry
你可能注意到代码里用了 tool_registry 字典做动态分派,而不是直接 get_weather(**block.input)。
原因很简单:真实的 agent 会有多个工具。模型返回的 block.name 可能是 get_weather,也可能是 search_flights 或 book_hotel。用注册表模式,新增工具只需加一行映射,不需要改分派逻辑。
内置 Web Search:服务端工具
Tool Use 是客户端工具——模型告诉你要调什么,你来执行。
还有一种是服务端工具(Server Tool)——模型自己调、自己拿结果、直接回答,开发者只需要声明一下。
Anthropic 的内置 Web Search 就是典型的服务端工具:
import anthropic
client = anthropic.Anthropic()
tools = [
{
"type": "web_search_20260209",
"name": "web_search",
"max_uses": 3,
"user_location": {
"type": "approximate",
"country": "CN",
"city": "Beijing",
"timezone": "Asia/Shanghai",
},
}
]
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2000,
tools=tools,
messages=[{"role": "user", "content": "Claude Code 最新版本有什么新功能?"}],
)
# 响应中包含 web_search_tool_result 和最终文本
for block in response.content:
if block.type == "text":
print(block.text)一次 API 调用就搞定,不需要实现任何搜索逻辑。响应中会包含 web_search_tool_result 类型的 content block,里面是搜索结果。是否支持服务端工具、支持哪些工具、工具的参数配置,完全由模型服务商决定。开发者只需要在请求里声明一下,就能用。
可选的控制参数:
| 参数 | 说明 |
|---|---|
max_uses | 单次请求最多搜索几次 |
allowed_domains | 限制只搜索指定域名 |
blocked_domains | 屏蔽指定域名 |
user_location | 用户位置,获得本地化搜索结果 |
两种工具模式的对比:
| 维度 | Tool Use(客户端工具) | Server Tool(服务端工具) |
|---|---|---|
| 执行者 | 开发者 | 模型服务商 |
| API 轮次 | 至少两轮(调用 + 结果) | 一轮 |
| 灵活性 | 高(自定义任何逻辑) | 低(只能用服务商提供的) |
| 适用场景 | 自定义业务逻辑 | 通用能力(搜索、代码执行) |
从 API 到 Agent 的距离
这篇文章覆盖了 Harness 工程的地基层:
API 基础调用 → 多轮对话 → Tool Use → 内置工具如果你跟着代码跑了一遍,你已经掌握了:
- 三大模型 API 的调用方式和差异——知道该用哪个 SDK、参数怎么传
- 多轮对话的本质——就是维护 messages 列表,模型本身无状态
- Tool Use 的完整流程——模型决策 → 开发者执行 → 结果返回
- 服务端工具的用法——声明即用,无需实现
但从这里到一个真正的 agent,还差什么?
- Agent Loop:把 Tool Use 的"两轮"变成"循环"——模型可能需要多次调用工具才能完成任务
- Context Management:messages 列表会越来越长,token 会爆,需要压缩和裁剪
- Tool Registry:工具越来越多,需要动态加载和管理
- Error Handling:工具执行失败怎么办?模型幻觉怎么办?
这些就是后续文章要讲的内容。
地基打好了,该盖楼了。