一个基于大语言模型的智能旅游助手,能够根据用户查询自动获取城市天气信息,并推荐合适的旅游景点。

📋 项目简介

该智能体采用 ReAct(Reasoning + Acting)模式,结合大语言模型的推理能力和外部工具的执行能力,实现智能化的旅游推荐服务。

核心功能

  • 🌤️ 实时天气查询: 通过 wttr.in API 获取指定城市的实时天气信息

  • 🗺️ 智能景点推荐: 基于天气条件和城市信息,使用 Tavily Search API 搜索并推荐合适的旅游景点

  • 🤖 自主决策: 智能体能够自主分析用户需求,按步骤执行任务,直到完成用户请求

🏗️ 架构设计

系统架构

┌─────────────────┐
│   用户输入       │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   Agent 主循环   │ ◄────┐
│  (app.py)       │      │
└────────┬────────┘      │
         │               │
         ▼               │
┌─────────────────┐      │
│  LLM 推理引擎   │      │
│ (client.py)     │      │
└────────┬────────┘      │
         │               │
         ▼               │
┌─────────────────┐      │
│  工具调用层     │      │
│  (tools/)       │      │
└────────┬────────┘      │
         │               │
    ┌────┴────┐          │
    │         │          │
    ▼         ▼          │
┌────────┐ ┌──────────┐ │
│天气查询│ │景点搜索  │ │
│工具    │ │工具      │ │
└────────┘ └──────────┘ │
         │               │
         ▼               │
┌─────────────────┐      │
│  观察结果反馈   │──────┘
└─────────────────┘

工作流程

智能体采用 Thought-Action-Observation 循环:

  1. Thought(思考): LLM 分析当前状态和用户需求,规划下一步行动

  2. Action(行动): 选择并调用合适的工具(天气查询或景点搜索)

  3. Observation(观察): 获取工具执行结果,更新对话历史

  4. 循环: 重复上述步骤,直到任务完成或达到最大循环次数

📁 目录结构

agent/travel/
├── __init__.py                 # 包初始化文件
├── app.py                      # 主程序入口,包含 Agent 主循环
├── README.md                   # 本文档
├── core/                       # 核心模块
│   ├── __init__.py
│   └── client.py               # LLM 客户端封装
└── tools/                      # 工具模块
    ├── __init__.py
    ├── search_weather.py       # 天气查询工具
    └── search_attraction.py    # 景点搜索工具

🔧 核心组件

1. Agent 主程序 (app.py)

主要职责:

  • 初始化 LLM 客户端和工具

  • 管理对话历史(Prompt History)

  • 执行 ReAct 循环

  • 解析 LLM 输出,提取 Thought 和 Action

  • 调用工具并处理观察结果

关键配置:

AGENT_SYSTEM_PROMPT  # 系统提示词,定义智能体的角色和行为规范
available_tools      # 可用工具字典
max_iterations = 5   # 最大循环次数

工作循环:

for i in range(5):
    # 1. 构建完整 Prompt(包含历史对话)
    # 2. 调用 LLM 获取 Thought-Action
    # 3. 解析 Action,提取工具调用信息
    # 4. 执行工具,获取 Observation
    # 5. 更新对话历史
    # 6. 判断是否完成(finish 命令)

2. LLM 客户端 (core/client.py)

OpenAICompatibleClient 类:

  • 功能: 封装兼容 OpenAI API 格式的 LLM 服务调用

  • 特点: 支持任意兼容 OpenAI 接口的服务(OpenAI、DeepSeek、本地部署等)

  • 方法:

    • __init__(model, api_key, base_url): 初始化客户端

    • generate(prompt, system_prompt): 调用 LLM 生成回复

使用示例:

llm = OpenAICompatibleClient(
    model="deepseek-chat",
    api_key="your-api-key",
    base_url="https://api.deepseek.com"
)
response = llm.generate(user_prompt, system_prompt=AGENT_SYSTEM_PROMPT)

3. 天气查询工具 (tools/search_weather.py)

get_weather(city: str) -> str:

  • 功能: 查询指定城市的实时天气信息

  • 数据源: wttr.in API(免费天气服务)

  • 返回格式: 自然语言描述(天气状况 + 温度)

  • 错误处理: 网络错误、数据解析错误的异常捕获

使用示例:

result = get_weather("北京")
# 输出: "北京当前天气:晴朗,气温15摄氏度"

4. 景点搜索工具 (tools/search_attraction.py)

get_attraction(city: str, weather: str) -> str:

  • 功能: 根据城市和天气条件搜索推荐旅游景点

  • 数据源: Tavily Search API(AI 搜索引擎)

  • 特点:

    • 智能理解查询意图

    • 返回综合性的推荐回答

    • 自动总结多个搜索结果

  • 配置: 需要设置 TAVILY_API_KEY 环境变量

使用示例:

result = get_attraction("北京", "晴朗,气温15摄氏度")
# 输出: 基于天气条件的景点推荐列表

🚀 使用指南

前置要求

  • Python 3.10+

  • uv 包管理器

  • LLM API 密钥(OpenAI、DeepSeek 等)

  • Tavily API 密钥(用于景点搜索)

安装依赖

# 项目根目录执行
uv sync

配置 API 密钥

app.pymain() 函数中配置:

# LLM 服务配置
API_KEY = "your-llm-api-key"
BASE_URL = "https://api.deepseek.com"  # 或 https://api.openai.com/v1
MODEL_ID = "deepseek-chat"  # 或 "gpt-4o-mini"

# Tavily API 配置(自动设置到环境变量)
TAVILY_API_KEY = "your-tavily-api-key"

运行方式

方式 1: 作为模块运行(推荐)

uv run python -m agent.travel.app

方式 2: 直接运行文件

uv run python agent/travel/app.py

自定义查询

修改 app.py 中的 user_prompt 变量:

user_prompt = "你好,请帮我查询一下今天南京的天气,然后根据天气推荐一些合适的旅游景点。"

⚙️ 配置说明

LLM 服务配置

支持任何兼容 OpenAI API 格式的服务:

服务商

BASE_URL

MODEL_ID 示例

OpenAI

https://api.openai.com/v1

gpt-4o-mini, gpt-4o

DeepSeek

https://api.deepseek.com

deepseek-chat

本地部署

http://localhost:8000/v1

your-model-name

环境变量

  • TAVILY_API_KEY: Tavily Search API 密钥(用于景点搜索)

📊 依赖说明

项目依赖(见 pyproject.toml):

dependencies = [
    "openai>=2.15.0",        # OpenAI SDK,用于调用 LLM API
    "requests>=2.32.5",       # HTTP 请求库,用于天气 API
    "tavily-python>=0.7.17",  # Tavily 搜索 API 客户端
]

🔄 ReAct 模式详解

输出格式

LLM 必须严格按照以下格式输出:

Thought: [思考过程和计划]
Action: [工具调用,格式为 function_name(arg_name="arg_value")]

示例:

Thought: 用户想要查询南京的天气,我需要先调用天气查询工具获取天气信息。
Action: get_weather(city="南京")

任务完成

当收集到足够信息时,LLM 应输出:

Action: finish(answer="根据查询,南京今天天气晴朗,气温20度,推荐游览中山陵、夫子庙等户外景点...")

对话历史格式

用户请求: [用户输入]
Thought: [思考]
Action: [行动]
Observation: [观察结果]
Thought: [下一步思考]
Action: [下一步行动]
...

📄 源代码

1. Agent 主程序 (app.py)

import os
import re
from agent.travel.core.client import OpenAICompatibleClient
from agent.travel.tools.search_weather import get_weather
from agent.travel.tools.search_attraction import get_attraction

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="...")` 来输出最终答案。

请开始吧!
"""

# 将所有工具函数放入一个字典,方便后续调用
available_tools = {
    "get_weather": get_weather,
    "get_attraction": get_attraction,
}


def main():
    # --- 1. 配置LLM客户端 ---
    # 请根据您使用的服务,将这里替换成对应的凭证和地址
    API_KEY = "sk-8e8ca3c7db564ababce35a47c407783a"
    BASE_URL = "https://api.deepseek.com"
    MODEL_ID = "deepseek-chat"
    TAVILY_API_KEY="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"
    os.environ['TAVILY_API_KEY'] = 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)



if __name__ == "__main__":
    # uv run python -m agent.travel.app
    main()

2. LLM 客户端 (core/client.py)

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 "错误:调用语言模型服务时出错。"

3. 天气查询工具 (tools/search_weather.py)

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}"

4. 景点搜索工具 (tools/search_attraction.py)

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}"