一个基于大语言模型的智能旅游助手,能够根据用户查询自动获取城市天气信息,并推荐合适的旅游景点。
📋 项目简介
该智能体采用 ReAct(Reasoning + Acting)模式,结合大语言模型的推理能力和外部工具的执行能力,实现智能化的旅游推荐服务。
核心功能
🌤️ 实时天气查询: 通过 wttr.in API 获取指定城市的实时天气信息
🗺️ 智能景点推荐: 基于天气条件和城市信息,使用 Tavily Search API 搜索并推荐合适的旅游景点
🤖 自主决策: 智能体能够自主分析用户需求,按步骤执行任务,直到完成用户请求
🏗️ 架构设计
系统架构
┌─────────────────┐
│ 用户输入 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Agent 主循环 │ ◄────┐
│ (app.py) │ │
└────────┬────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ LLM 推理引擎 │ │
│ (client.py) │ │
└────────┬────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ 工具调用层 │ │
│ (tools/) │ │
└────────┬────────┘ │
│ │
┌────┴────┐ │
│ │ │
▼ ▼ │
┌────────┐ ┌──────────┐ │
│天气查询│ │景点搜索 │ │
│工具 │ │工具 │ │
└────────┘ └──────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ 观察结果反馈 │──────┘
└─────────────────┘
工作流程
智能体采用 Thought-Action-Observation 循环:
Thought(思考): LLM 分析当前状态和用户需求,规划下一步行动
Action(行动): 选择并调用合适的工具(天气查询或景点搜索)
Observation(观察): 获取工具执行结果,更新对话历史
循环: 重复上述步骤,直到任务完成或达到最大循环次数
📁 目录结构
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.py 的 main() 函数中配置:
# 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 格式的服务:
环境变量
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}"