智能旅行助手agent:从零构建AI旅游推荐

前言:源码来源于Datawhale社区的hello-agents项目中的第一章:初识智能体,本文仅是对代码的解读,对agent内容感兴趣的推荐去关注他们在github上的项目hello-agents


项目介绍:

在本项目中我们将引导您使用几行简单的 Python 代码,从零开始构建一个可以工作的智能旅行助手。这个过程将遵循我们刚刚学到的理论循环,让您直观地感受到一个智能体是如何"思考"并与外部"工具"互动的。让我们开始吧!

在本案例中,我们的目标是构建一个能处理分步任务的智能旅行助手。需要解决的用户任务定义为:"你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。"要完成这个任务,智能体必须展现出清晰的逻辑规划能力。它需要先调用天气查询工具,并将获得的观察结果作为下一步的依据。在下一轮循环中,它再调用景点推荐工具,从而得出最终建议。

代码总览:

代码一共分为两个部分:工具的定义和agent主体部分代码

工具 1:查询真实天气

python 复制代码
import requests
import json

def get_weather(city: str) -> str:
    """
    通过调用 wttr.in API 查询真实的天气信息。
    """
    # API端点,我们请求JSON格式的数据
    url = f"https://wttr.in/{city}?format=j1"
    
    try:
        # 发起网络请求
        response = requests.get(url)
        # 检查响应状态码是否为200 (成功)
        response.raise_for_status() 
        # 解析返回的JSON数据
        data = response.json()
        
        # 提取当前天气状况
        current_condition = data['current_condition'][0]
        weather_desc = current_condition['weatherDesc'][0]['value']
        temp_c = current_condition['temp_C']
        
        # 格式化成自然语言返回
        return f"{city}当前天气:{weather_desc},气温{temp_c}摄氏度"
        
    except requests.exceptions.RequestException as e:
        # 处理网络错误
        return f"错误:查询天气时遇到网络问题 - {e}"
    except (KeyError, IndexError) as e:
        # 处理数据解析错误
        return f"错误:解析天气数据失败,可能是城市名称无效 - {e}"

工具 2:搜索并推荐旅游景点

python 复制代码
import os
from tavily import TavilyClient

def get_attraction(city: str, weather: str) -> str:
    """
    根据城市和天气,使用Tavily Search API搜索并返回优化后的景点推荐。
    """
    # 1. 从环境变量中读取API密钥
    api_key = os.environ.get("TAVILY_API_KEY")
    if not api_key:
        return "错误:未配置TAVILY_API_KEY环境变量。"

    # 2. 初始化Tavily客户端
    tavily = TavilyClient(api_key=api_key)
    
    # 3. 构造一个精确的查询
    query = f"'{city}' 在'{weather}'天气下最值得去的旅游景点推荐及理由"
    
    try:
        # 4. 调用API,include_answer=True会返回一个综合性的回答
        response = tavily.search(query=query, search_depth="basic", include_answer=True)
        
        # 5. Tavily返回的结果已经非常干净,可以直接使用
        # response['answer'] 是一个基于所有搜索结果的总结性回答
        if response.get("answer"):
            return response["answer"]
        
        # 如果没有综合性回答,则格式化原始结果
        formatted_results = []
        for result in response.get("results", []):
            formatted_results.append(f"- {result['title']}: {result['content']}")
        
        if not formatted_results:
             return "抱歉,没有找到相关的旅游景点推荐。"

        return "根据搜索,为您找到以下信息:\n" + "\n".join(formatted_results)

    except Exception as e:
        return f"错误:执行Tavily搜索时出现问题 - {e}"

agent主体部分代码:

python 复制代码
import os
from tavily import TavilyClient
from openai import OpenAI

class OpenAICompatibleClient:
    """
    一个用于调用任何兼容OpenAI接口的LLM服务的客户端。
    """
    def __init__(self, model: str, api_key: str, base_url: str):
        self.model = model
        self.client = OpenAI(api_key=api_key, base_url=base_url)

    def generate(self, prompt: str, system_prompt: str) -> str:
        """调用LLM API来生成回应。"""
        print("正在调用大语言模型...")
        try:
            messages = [
                {'role': 'system', 'content': system_prompt},
                {'role': 'user', 'content': prompt}
            ]
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
                stream=False
            )
            answer = response.choices[0].message.content
            print("大语言模型响应成功。")
            return answer
        except Exception as e:
            print(f"调用LLM API时发生错误: {e}")
            return "错误:调用语言模型服务时出错。"
AGENT_SYSTEM_PROMPT = """
你是一个智能旅行助手。你的任务是分析用户的请求,并使用可用工具一步步地解决问题。

# 可用工具:
- `get_weather(city: str)`: 查询指定城市的实时天气。
- `get_attraction(city: str, weather: str)`: 根据城市和天气搜索推荐的旅游景点。

# 行动格式:
你的回答必须严格遵循以下格式。首先是你的思考过程,然后是你要执行的具体行动,每次回复只输出一对Thought-Action:
Thought: [这里是你的思考过程和下一步计划]
Action: [这里是你要调用的工具,格式为 function_name(arg_name="arg_value")]

# 任务完成:
当你收集到足够的信息,能够回答用户的最终问题时,你必须在`Action:`字段后使用 `finish(answer="...")` 来输出最终答案。

请开始吧!
"""

import re

# --- 1. 配置LLM客户端 ---
# 请根据您使用的服务,将这里替换成对应的凭证和地址
API_KEY = "YOUR_API_KEY"
BASE_URL = "YOUR_BASE_URL"
MODEL_ID = "YOUR_MODEL_ID"
TAVILY_API_KEY="YOUR_Tavily_KEY"
os.environ['TAVILY_API_KEY'] = "YOUR_TAVILY_API_KEY"

llm = OpenAICompatibleClient(
    model=MODEL_ID,
    api_key=API_KEY,
    base_url=BASE_URL
)

# --- 2. 初始化 ---
user_prompt = "你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。"
prompt_history = [f"用户请求: {user_prompt}"]

print(f"用户输入: {user_prompt}\n" + "="*40)

# --- 3. 运行主循环 ---
for i in range(5): # 设置最大循环次数
    print(f"--- 循环 {i+1} ---\n")
    
    # 3.1. 构建Prompt
    full_prompt = "\n".join(prompt_history)
    
    # 3.2. 调用LLM进行思考
    llm_output = llm.generate(full_prompt, system_prompt=AGENT_SYSTEM_PROMPT)
    # 模型可能会输出多余的Thought-Action,需要截断
    match = re.search(r'(Thought:.*?Action:.*?)(?=\n\s*(?:Thought:|Action:|Observation:)|\Z)', llm_output, re.DOTALL)
    if match:
        truncated = match.group(1).strip()
        if truncated != llm_output.strip():
            llm_output = truncated
            print("已截断多余的 Thought-Action 对")
    print(f"模型输出:\n{llm_output}\n")
    prompt_history.append(llm_output)
    
    # 3.3. 解析并执行行动
    action_match = re.search(r"Action: (.*)", llm_output, re.DOTALL)
    if not action_match:
        print("解析错误:模型输出中未找到 Action。")
        break
    action_str = action_match.group(1).strip()

    if action_str.startswith("finish"):
        final_answer = re.search(r'finish\(answer="(.*)"\)', action_str).group(1)
        print(f"任务完成,最终答案: {final_answer}")
        break
    
    tool_name = re.search(r"(\w+)\(", action_str).group(1)
    args_str = re.search(r"\((.*)\)", action_str).group(1)
    kwargs = dict(re.findall(r'(\w+)="([^"]*)"', args_str))

    if tool_name in available_tools:
        observation = available_tools[tool_name](**kwargs)
    else:
        observation = f"错误:未定义的工具 '{tool_name}'"

    # 3.4. 记录观察结果
    observation_str = f"Observation: {observation}"
    print(f"{observation_str}\n" + "="*40)
    prompt_history.append(observation_str)

调用示例:

用户输入: 你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。

========================================

--- 循环 1 ---

正在调用大语言模型...

大语言模型响应成功。

模型输出:

Thought: 首先需要获取北京今天的天气情况,之后再根据天气情况来推荐旅游景点。

Action: get_weather(city="北京")

Observation: 北京当前天气:Sunny,气温26摄氏度

========================================

--- 循环 2 ---

正在调用大语言模型...

大语言模型响应成功。

模型输出:

Thought: 现在已经知道了北京今天的天气是晴朗且温度适中,接下来可以基于这个信息来推荐一个适合的旅游景点了。

Action: get_attraction(city="北京", weather="Sunny")

Observation: 北京在晴天最值得去的旅游景点是颐和园,因其美丽的湖景和古建筑。另一个推荐是长城,因其壮观的景观和历史意义。

========================================

--- 循环 3 ---

正在调用大语言模型...

大语言模型响应成功。

模型输出:

Thought: 已经获得了两个适合晴天游览的景点建议,现在可以根据这些信息给用户提供满意的答复。

Action: finish(answer="今天北京的天气是晴朗的,气温26摄氏度,非常适合外出游玩。我推荐您去颐和园欣赏美丽的湖景和古建筑,或者前往长城体验其壮观的景观和深厚的历史意义。希望您有一个愉快的旅行!

")

任务完成,最终答案: 今天北京的天气是晴朗的,气温26摄氏度,非常适合外出游玩。我推荐您去颐和园欣赏美丽的湖景和古建筑,或者前往长城体验其壮观的景观和深厚的历史意义。希望您有一个愉快的旅行!

解析部分:

这段代码其实是在手搓一个经典的 ReAct 风格 Agent (Reason + Act):模型按固定格式输出 Thought / Action ,外部程序解析 Action 去调用工具,把结果作为 Observation 再喂回模型,如此循环直到 finish(...)

下面我按"工具调用流程"把它拆开,再解释你问的"为什么会有 Thought/Action,而不是 system/user 字段"。


一、这段代码的工具调用流程(从用户输入到最终 finish)

你可以把它想成一个"对话回放文本 + 解析器 + 工具执行器"的状态机。

0)初始化阶段

  • user_prompt:用户任务(查北京天气 → 推荐景点)

  • prompt_history = ["用户请求: ..."]用纯文本记录对话历史(注意:不是 messages 结构)

1)主循环(最多 5 轮)

每一轮做四件事:构建 Prompt → 调 LLM → 解析 Action → 执行工具 → 记录 Observation

1.1 构建 full_prompt
复制代码
full_prompt = "\n".join(prompt_history)
  • 把历史记录拼成一段大文本:

    复制代码
    用户请求: ...
    Thought: ...
    Action: ...
    Observation: ...
    Thought: ...
    ...
1.2 调用 LLM(让它"思考并决定下一步")
复制代码
llm_output = llm.generate(full_prompt, system_prompt=AGENT_SYSTEM_PROMPT)
  • 关键点:AGENT_SYSTEM_PROMPT 通常会要求模型输出固定格式,比如:

    • Thought: ...

    • Action: tool_name(key="value")

    • Action: finish(answer="...")

为什么要"截断多余 Thought-Action"
复制代码
match = re.search(r'(Thought:.*?Action:.*?)(?=\n\s*(?:Thought:|Action:|Observation:)|\Z)', llm_output, re.DOTALL)
  • 有些模型会一次性输出多轮 Thought/Action(比如它自己连着规划两步)。

  • 但你的外部程序一次只执行一个 Action,所以用正则把输出截成 "第一段 Thought+Action",避免一轮里出现两个工具调用导致解析混乱。

然后把本轮 llm_output 加入历史:

复制代码
prompt_history.append(llm_output)
1.3 解析 Action(决定是 finish 还是工具调用)
复制代码
action_match = re.search(r"Action: (.*)", llm_output, re.DOTALL)
  • 找到 Action 行后面的所有内容(因为 DOTALL,可能跨行)

两种分支:

A) finish 分支:结束循环

复制代码
if action_str.startswith("finish"):
    final_answer = re.search(r'finish\(answer="(.*)"\)', action_str).group(1)
    break
  • 如果 Action: finish(answer="..."),则提取 answer 输出并结束。

B) tool 分支:解析工具名和参数

复制代码
tool_name = re.search(r"(\w+)\(", action_str).group(1)
args_str  = re.search(r"\((.*)\)", action_str).group(1)
kwargs    = dict(re.findall(r'(\w+)="([^"]*)"', args_str))
  • tool_name:抓 ( 前面的单词字符作为工具名(比如 tavily_search

  • args_str:抓括号内全部内容

  • kwargs:从 args_str 里提取所有 key="value" 变成 dict

1.4 执行工具,并把结果写回 Observation
复制代码
if tool_name in available_tools:
    observation = available_tools[tool_name](**kwargs)
else:
    observation = f"错误:未定义的工具 '{tool_name}'"

把结果写成:

复制代码
observation_str = f"Observation: {observation}"
prompt_history.append(observation_str)

这一步是 Agent 的"闭环"关键:模型下一轮会看到 Observation,从而基于工具返回继续推理或 finish。


二、为什么 GPT 返回会带 Thought / Action?它并没有返回 system/user 。

这个问题卡住了很多刚写 Agent 的同学:你把"对话消息的元数据" 和 "模型输出的文本内容"混在一起理解了。

1)system/user/assistant 是"请求结构",不是"模型输出内容"

在标准 Chat API 里,你发送的是类似:

复制代码
[
  {"role": "system", "content": "..."},
  {"role": "user", "content": "..."}
]

模型返回的是:

复制代码
{"role": "assistant", "content": "......"}

也就是说:

  • system/user 是你发给模型时的"消息角色"

  • 模型输出里通常只有 assistant 的 content 文本(不会把 system/user 这几个字打印出来)

你现在这段代码虽然有 system_prompt=AGENT_SYSTEM_PROMPT,但 OpenAICompatibleClient.generate() 看起来返回的是纯字符串 (assistant content),你自己又把历史用 prompt_history 拼成"剧本式文本",所以模型看到的是"Thought/Action/Observation 文字协议",它当然就按协议输出这些字段。

2)Thought/Action/Observation 是一种"文本协议"(prompt 约定)

Thought:Action:Observation: 并不是 OpenAI/ChatGPT API 的固定字段,而是 Agent 圈常用的 ReAct 模式约定

  • Thought:模型"内部推理"(或者说它被要求写出来的推理)

  • Action:下一步要调用的工具与参数

  • Observation:工具返回值(由外部程序写回给模型)

这是一种在没有原生 tool-calling时,最容易做工具调用编排的方法:纯文本 + 正则解析。

3)"为什么要有 Thought?"------历史原因 + 调试便利,但不是必须

早期 ReAct 论文/实践里,会把 Thought 写出来,便于:

  • 调试模型为什么调用这个工具

  • 让模型保持"逐步规划"的结构

但工程上你完全可以(也常常应该)改成:

  • 只让模型输出 Action(机器可读)

  • 或使用"原生函数调用 / tools schema",把工具调用从文本里剥离出去

而且在很多产品场景里,不建议让模型输出详细 Thought

一方面噪声大、易泄露提示词细节,另一方面你也不需要它才能正确调用工具。


三、这段解析方案的几个"很可能会踩坑"的点(顺手帮你排雷)

1)args_str = re.search(r"\((.*)\)", action_str) 是贪婪的

  • 如果参数里出现额外的 ),会吃到最后一个 )

    更稳:r"\((.*?)\)"

2)kwargs 只支持 key="value" 且 value 里不能有双引号

  • top_k=3query='xxx'query="a\"b" 都会解析失败或不完整

3).group(1) 没判空就用,会在匹配失败时直接崩

  • 建议每一步都有 None 保护,错误信息更友好

四、如果你想"更正规"的工具调用:两条更推荐的路线

路线 A:仍用文本,但强制模型只吐"严格 JSON",你用 json.loads 解析

这条路的核心是:模型每一轮只能输出一个 JSON 对象 ,要么请求调用工具,要么直接 finish。然后你用 json.loads 解析它,完全不需要正则。为了更稳,我用 OpenAI 的 Structured Outputs(text.format / json_schema) 强制它遵循你给的 schema。

✅ A 的运行方式(高层流程)
  1. 你把工具列表写在 system prompt 里(告诉模型"只能调用这些工具")。

  2. 模型输出 JSON:{"type":"tool","name":"get_weather","args":{...},"answer":null}{"type":"finish",...}

  3. json.loads -> 如果是 tool 就执行 -> 把 observation 再喂回去 -> 下一轮。

    (Structured Outputs 让 JSON "强制合规",减少重试和脆弱解析。)

可执行代码:agent_json_protocol.py
python 复制代码
import os
import json
import requests
from typing import Any, Dict, Optional
from openai import OpenAI

# -------------------------
# 0) 配置
# -------------------------
MODEL = os.getenv("MODEL", "gpt-5.2")  # 也可以换成 gpt-4o-2024-08-06 等
client = OpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    base_url=os.getenv("OPENAI_BASE_URL")  # 用官方就不需要设置
)

USER_PROMPT = "你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。"
MAX_STEPS = 6
TIMEOUT = 15

# -------------------------
# 1) 你的工具:Open-Meteo 获取实时天气(不需要 API Key)
#    Open-Meteo 的地理编码与 forecast 参数见官方文档
# -------------------------
def _http_get_json(url: str, params: Dict[str, Any], timeout: int = TIMEOUT) -> Dict[str, Any]:
    r = requests.get(url, params=params, timeout=timeout)
    r.raise_for_status()
    return r.json()

def get_weather(location: str) -> Dict[str, Any]:
    """
    使用 Open-Meteo:
      1) geocoding: name -> lat/lon
      2) forecast: current=temperature_2m,precipitation,weather_code,wind_speed_10m
    """
    geo = _http_get_json(
        "https://geocoding-api.open-meteo.com/v1/search",
        {"name": location, "count": 1, "language": "zh", "format": "json"},
    )
    if not geo.get("results"):
        return {"ok": False, "error": f"找不到地点: {location}"}

    g0 = geo["results"][0]
    lat, lon = g0["latitude"], g0["longitude"]
    resolved_name = f'{g0.get("name", location)}, {g0.get("country", "")}'.strip(", ")

    weather = _http_get_json(
        "https://api.open-meteo.com/v1/forecast",
        {
            "latitude": lat,
            "longitude": lon,
            "current": "temperature_2m,precipitation,weather_code,wind_speed_10m",
            "timezone": "auto",
            "forecast_days": 1
        },
    )
    current = weather.get("current", {})
    return {
        "ok": True,
        "location": resolved_name,
        "latitude": lat,
        "longitude": lon,
        "time": current.get("time"),
        "temperature_2m": current.get("temperature_2m"),
        "precipitation": current.get("precipitation"),
        "wind_speed_10m": current.get("wind_speed_10m"),
        "weather_code": current.get("weather_code"),
        "raw": current,
    }

AVAILABLE_TOOLS = {
    "get_weather": get_weather,
}

# -------------------------
# 2) 约束模型输出为"严格 JSON"的 schema(Structured Outputs)
#    这里用固定字段 + null 来避免 if/then(兼容性更好)
# -------------------------
STEP_SCHEMA = {
    "type": "object",
    "properties": {
        "type": {"type": "string", "enum": ["tool", "finish"]},
        "name": {"type": ["string", "null"]},
        "args": {"type": ["object", "null"]},
        "answer": {"type": ["string", "null"]},
    },
    "required": ["type", "name", "args", "answer"],
    "additionalProperties": False,
}

SYSTEM_PROMPT = f"""
你是一个会调用工具的助手,但你每次只能输出一个 JSON 对象,且必须严格符合给定 JSON schema。
规则:
- 如果需要查询天气,调用工具:get_weather
- 工具调用时:type="tool",name="get_weather",args={{"location": "..."}}, answer=null
- 结束时:type="finish",answer="给用户的最终回复",name=null,args=null
- 绝对不要输出除 JSON 以外的任何文字(不要 Markdown,不要解释)。
可用工具:{list(AVAILABLE_TOOLS.keys())}
""".strip()

# -------------------------
# 3) 主循环:json.loads -> 执行工具 -> Observation -> 下一轮
# -------------------------
def main():
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": USER_PROMPT},
    ]

    for step_idx in range(1, MAX_STEPS + 1):
        resp = client.responses.create(
            model=MODEL,
            input=messages,
            text={
                "format": {
                    "type": "json_schema",
                    "name": "agent_step",
                    "schema": STEP_SCHEMA,
                    "strict": True
                }
            },
        )

        if resp.status != "completed":
            raise RuntimeError(f"模型返回非 completed: status={resp.status}, details={getattr(resp, 'incomplete_details', None)}")

        # 模型输出一定是 JSON 字符串(被 schema 强制)
        step_obj = json.loads(resp.output_text)

        # 把模型"做了什么决定"也记入对话(方便下一轮保持一致)
        messages.append({"role": "assistant", "content": resp.output_text})

        if step_obj["type"] == "finish":
            print("\n=== FINAL ANSWER ===\n")
            print(step_obj["answer"])
            return

        if step_obj["type"] == "tool":
            tool_name = step_obj["name"]
            args = step_obj["args"] or {}
            if tool_name not in AVAILABLE_TOOLS:
                observation = {"ok": False, "error": f"未定义工具: {tool_name}"}
            else:
                try:
                    observation = AVAILABLE_TOOLS[tool_name](**args)
                except Exception as e:
                    observation = {"ok": False, "error": f"工具执行异常: {type(e).__name__}: {e}"}

            # 把工具结果作为 Observation 喂回模型(这里用 user role 承载"外部世界反馈")
            messages.append({
                "role": "user",
                "content": f"Observation: {json.dumps(observation, ensure_ascii=False)}"
            })
            continue

        raise RuntimeError(f"未知 type: {step_obj['type']}")

    raise RuntimeError("达到最大步数仍未 finish(可能 prompt 需要更强约束或工具信息不足)。")

if __name__ == "__main__":
    main()

依赖安装:

python 复制代码
pip install openai requests
export OPENAI_API_KEY="你的key"
# 可选:export OPENAI_BASE_URL="你的兼容服务地址"
python agent_json_protocol.py

这套方案的关键点是:text.format: json_schema + strict 让输出结构非常稳定,减少"模型多说一句话就崩"的情况。


路线 B:用原生 tool/function calling(最干净:不用从文本里抠 Action)

这条路的核心是:工具调用是协议层返回的结构化字段 。你不再要求模型打印 Action:,而是让它直接返回 function_call 项;你执行后,再把结果以 function_call_output 回传给模型。官方把它总结成 5 步 flow。

另外一个很容易踩坑的点:对 reasoning 模型(比如 GPT-5 系列),如果响应里有 reasoning items,需要把它们也一并带回下一轮,否则模型可能"断片"或重复推理。

可执行代码:agent_native_tool_calling.py
python 复制代码
import os
import json
import requests
from typing import Any, Dict
from openai import OpenAI

MODEL = os.getenv("MODEL", "gpt-5.2")
client = OpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    base_url=os.getenv("OPENAI_BASE_URL")
)

USER_PROMPT = "你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。"
TIMEOUT = 15
MAX_TURNS = 6

def _http_get_json(url: str, params: Dict[str, Any], timeout: int = TIMEOUT) -> Dict[str, Any]:
    r = requests.get(url, params=params, timeout=timeout)
    r.raise_for_status()
    return r.json()

def get_weather(location: str) -> Dict[str, Any]:
    geo = _http_get_json(
        "https://geocoding-api.open-meteo.com/v1/search",
        {"name": location, "count": 1, "language": "zh", "format": "json"},
    )
    if not geo.get("results"):
        return {"ok": False, "error": f"找不到地点: {location}"}

    g0 = geo["results"][0]
    lat, lon = g0["latitude"], g0["longitude"]
    resolved_name = f'{g0.get("name", location)}, {g0.get("country", "")}'.strip(", ")

    weather = _http_get_json(
        "https://api.open-meteo.com/v1/forecast",
        {
            "latitude": lat,
            "longitude": lon,
            "current": "temperature_2m,precipitation,weather_code,wind_speed_10m",
            "timezone": "auto",
            "forecast_days": 1
        },
    )
    current = weather.get("current", {})
    return {
        "ok": True,
        "location": resolved_name,
        "time": current.get("time"),
        "temperature_2m": current.get("temperature_2m"),
        "precipitation": current.get("precipitation"),
        "wind_speed_10m": current.get("wind_speed_10m"),
        "weather_code": current.get("weather_code"),
    }

# 1) 定义 tools(function schema)
tools = [
    {
        "type": "function",
        "name": "get_weather",
        "description": "Get current weather for a location using Open-Meteo (no API key).",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string", "description": "City name, e.g. 'Beijing, China' or '北京'."}
            },
            "required": ["location"],
        },
    }
]

def call_function(name: str, args: Dict[str, Any]) -> Any:
    if name == "get_weather":
        return get_weather(**args)
    return {"ok": False, "error": f"Unknown function: {name}"}

def main():
    # input 列表(Responses API 用 input 承载多轮上下文)
    input_messages = [{"role": "user", "content": USER_PROMPT}]

    for _ in range(MAX_TURNS):
        # 2) 请求模型(模型可能直接回答,也可能返回 function_call)
        response = client.responses.create(
            model=MODEL,
            input=input_messages,
            tools=tools,
        )

        # 把模型 output(包含 reasoning / function_call 等 item)追加到上下文
        input_messages += response.output

        # 3) 找出工具调用项
        tool_calls = [item for item in response.output if getattr(item, "type", None) == "function_call"]

        # 没有工具调用 -> 这轮就是最终文本(或至少是可输出文本)
        if not tool_calls:
            print("\n=== FINAL ANSWER ===\n")
            print(response.output_text)
            return

        # 4) 执行工具,并把结果以 function_call_output 回传
        for call in tool_calls:
            try:
                args = json.loads(call.arguments) if call.arguments else {}
            except json.JSONDecodeError:
                args = {}

            result = call_function(call.name, args)

            input_messages.append({
                "type": "function_call_output",
                "call_id": call.call_id,
                "output": json.dumps(result, ensure_ascii=False)
            })

    raise RuntimeError("超过最大轮次仍未结束:可能工具信息不足或 prompt 需要更强约束。")

if __name__ == "__main__":
    main()

依赖安装:

python 复制代码
pip install openai requests
export OPENAI_API_KEY="你的key"
python agent_native_tool_calling.py

两条路线怎么选(务实版)

  • A(JSON 协议):优点是"模型/平台无关",任何 LLM 都能玩;缺点是你要自己维护"协议 + 观测回灌 + 防越权"。Structured Outputs 能把可靠性拉满不少。

  • B(原生 tool calling):优点是"最干净、最少 parsing",工具调用是结构化字段,官方推荐的标准 flow;缺点是依赖平台对 tools 协议支持。

顺带一提:之前那种 Thought/Action/Observation 其实就是早期 Agent 训练/提示里常见的"文本协议",B 方案把这套协议下沉到了 API 层,所以你不需要让模型把 Action 以字符串打印出来了。

相关推荐
仙魁XAN8 小时前
如何用豆包、即梦 AI ,快速实现“AI森林治愈系风格视频”的效果
人工智能·ai·视频生成·豆包·即梦·森林治愈系
Cigaretter78 小时前
Day 42 简单CNN
python·深度学习·cnn
春日见8 小时前
控制算法:PID算法
linux·运维·服务器·人工智能·驱动开发·算法·机器人
UI设计兰亭妙微8 小时前
解锁流畅体验:UX 设计中降低认知负荷的核心策略与实践
人工智能·ux·用户体验设计
wen_zhufeng8 小时前
解释Vector Quantize,从简单到原理
人工智能
二哈喇子!8 小时前
PyTorch 生态与昇腾平台适配实践
人工智能·pytorch·python
开发者导航8 小时前
【开发者导航】ChatGPT Atlas 开源平替,一款免费的AI浏览器,让网页自动驾驶!
人工智能·chatgpt
执笔论英雄8 小时前
【RL】 kl loss
人工智能
BitaHub20248 小时前
深度推理力量:用 DeepSeek V3.2 Speciale 打造自动数据分析系统
人工智能·deepseek