HuanCode Docs

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 APIresponses.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)ClaudeDeepSeek
SDKopenaianthropic复用 openai
调用方法responses.createmessages.createchat.completions.create
系统指令instructions 参数顶层 system 参数messages 中的 system 角色
输入参数input(字符串或消息数组)messages 列表messages 列表
响应取值response.output_textmessage.content[0].textresponse.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,核心就三个参数:modelmessagestools

model:选哪个模型

直接传模型名称字符串。OpenAI 是 gpt-5.4,Claude 是 claude-opus-4-6,DeepSeek 是 deepseek-chat。通过 LiteLLM 统一调用时,模型名是自己定义的。

messages:对话历史

messages 是一个列表,每条消息有 rolecontent 两个字段。

参数OpenAI(Responses API)Claude
输入格式input 可以是字符串或消息数组messages 列表,每条含 role + content
消息角色developeruserassistantuserassistant(system 用顶层参数)
content 格式字符串或内容块数组字符串或内容块数组

content 可以是简单字符串,也可以是内容块数组——支持文本、图片、文件等多种类型。这种设计让模型能处理多模态输入,agent 可以根据需要传入不同类型的内容。

tools:赋予模型行动能力

tools 是工具列表,赋予模型调用外部能力(搜索、文件操作、API 调用等)。这是模型从"回答者"变成"执行者"的关键。

工具能力说明OpenAIClaude
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_flightsbook_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 → 内置工具

如果你跟着代码跑了一遍,你已经掌握了:

  1. 三大模型 API 的调用方式和差异——知道该用哪个 SDK、参数怎么传
  2. 多轮对话的本质——就是维护 messages 列表,模型本身无状态
  3. Tool Use 的完整流程——模型决策 → 开发者执行 → 结果返回
  4. 服务端工具的用法——声明即用,无需实现

但从这里到一个真正的 agent,还差什么?

  • Agent Loop:把 Tool Use 的"两轮"变成"循环"——模型可能需要多次调用工具才能完成任务
  • Context Management:messages 列表会越来越长,token 会爆,需要压缩和裁剪
  • Tool Registry:工具越来越多,需要动态加载和管理
  • Error Handling:工具执行失败怎么办?模型幻觉怎么办?

这些就是后续文章要讲的内容。

地基打好了,该盖楼了。

On this page