简介你是否曾经与Siri、Alexa等AI助手聊天,或者使用过那些帮助你预订航班或查询天气的智能聊天机器人?你是否好奇这些助手背后的工作原理?今天,我们将通过使用Rust和Rig库构建一个属于自己的航班搜索AI助手,来揭开这些技术的神秘面纱。你可能会想:“等等,Rust?那不是以难学著称的语言吗
你是否曾经与Siri、Alexa等AI助手聊天,或者使用过那些帮助你预订航班或查询天气的智能聊天机器人?你是否好奇这些助手背后的工作原理?今天,我们将通过使用Rust和Rig库构建一个属于自己的航班搜索AI助手,来揭开这些技术的神秘面纱。
你可能会想:“等等,Rust?那不是以难学著称的语言吗?”别担心!我们将一步步带你走过整个过程,并解释每个概念。到最后,你不仅会拥有一个酷炫的AI助手,还会对Rust编程有初步的了解。
以下是我们的计划:
本项目的完整源代码可以在我们的Replit页面和Github上找到。
听起来很激动人心吗?让我们开始吧!
Rust是一种系统编程语言,以其高性能和安全性著称。但除此之外,Rust在Web开发、游戏开发以及现在的AI应用领域也崭露头角。以下是我们选择Rust的原因:
Rig是一个开源的Rust库,简化了使用大型语言模型(LLM,如GPT-4、DeepSeek)构建应用程序的过程。你可以将Rig视为一个工具包,它提供了:
通过结合Rust和Rig,我们能够构建一个强大、高效且智能的助手。
在开始编码之前,我们需要准备好一切。
创建一个新的Rust项目
打开终端并运行:
cargo new flight_search_assistant
cd flight_search_assistant
这将初始化一个名为flight_search_assistant
的新Rust项目。
更新Cargo.toml
打开Cargo.toml
文件并添加必要的依赖:
[package]
name = "flight_search_assistant"
version = "0.1.0"
edition = "2021"
[dependencies]
rig-core = "0.7.0"
tokio = { version = "1.34.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json", "tls"] }
dotenv = "0.15"
thiserror = "1.0"
chrono = { version = "0.4", features = ["serde"] }
以下是这些依赖的简要说明:
rig-core
:Rig的核心库。tokio
:Rust的异步运行时,允许我们并发执行任务。serde
和serde_json
:用于序列化和反序列化数据的库。reqwest
:用于发送HTTP请求的客户端。dotenv
:从.env
文件加载环境变量。thiserror
:用于更好的错误处理。chrono
:用于处理日期和时间。设置环境变量
为了安全起见,我们不希望硬编码API密钥。相反,我们将它们存储在.env
文件中。
创建文件:
touch .env
在.env
中添加你的API密钥:
OPENAI_API_KEY=your_openai_api_key_here
RAPIDAPI_KEY=your_rapidapi_key_here
请将占位符替换为你的实际密钥。
安装依赖
在终端中运行:
cargo build
这将下载并编译所有依赖项。
在开始编码之前,让我们澄清一些关键概念。
在Rig(以及一般的AI应用)的上下文中,代理就像你助手的“大脑”。它负责解释用户输入、决定采取什么操作并生成响应。
你可以将代理视为乐队的指挥,协调不同的乐器(或工具)来创作和谐的音乐(或响应)。
工具是代理用来完成任务的能力或操作。每个工具执行特定的功能。在我们的例子中,航班搜索功能就是代理用来获取航班信息的工具。
继续我们的比喻,工具是乐队中的乐器,每个乐器都扮演着特定的角色。
当用户问:“帮我找从纽约到洛杉矶的航班”时,代理会处理这个请求,并决定需要使用航班搜索工具来获取信息。
现在,让我们构建处理航班搜索的工具。
创建工具文件
在src
目录中创建一个新文件flight_search_tool.rs
:
touch src/flight_search_tool.rs
导入必要的库
打开flight_search_tool.rs
并添加:
use chrono::{DateTime, Duration, Utc};
use rig::completion::ToolDefinition;
use rig::tool::Tool;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::env;
定义数据结构
我们将定义结构来处理输入参数和输出结果。
#[derive(Deserialize)]
pub struct FlightSearchArgs {
source: String,
destination: String,
date: Option<String>,
sort: Option<String>,
service: Option<String>,
itinerary_type: Option<String>,
adults: Option<u8>,
seniors: Option<u8>,
currency: Option<String>,
nearby: Option<String>,
nonstop: Option<String>,
}
#[derive(Serialize)]
pub struct FlightOption {
pub airline: String,
pub flight_number: String,
pub departure: String,
pub arrival: String,
pub duration: String,
pub stops: usize,
pub price: f64,
pub currency: String,
pub booking_url: String,
}
FlightSearchArgs
:表示用户提供的参数。FlightOption
:表示我们将展示给用户的每个航班选项。使用thiserror
进行错误处理
Rust鼓励我们显式处理错误。我们将定义一个自定义错误类型:
#[derive(Debug, thiserror::Error)]
pub enum FlightSearchError {
#[error("HTTP request failed: {0}")]
HttpRequestFailed(String),
#[error("Invalid response structure")]
InvalidResponse,
#[error("API error: {0}")]
ApiError(String),
#[error("Missing API key")]
MissingApiKey,
}
实现工具特性
现在,我们将为FlightSearchTool
实现Tool
特性。
首先,定义工具:
pub struct FlightSearchTool;
然后实现特性:
impl Tool for FlightSearchTool {
const NAME: &'static str = "search_flights";
type Args = FlightSearchArgs;
type Output = String;
type Error = FlightSearchError;
async fn definition(&self, _prompt: String) -> ToolDefinition {
ToolDefinition {
name: Self::NAME.to_string(),
description: "Search for flights between two airports".to_string(),
parameters: json!({
"type": "object",
"properties": {
"source": { "type": "string", "description": "Source airport code (e.g., 'JFK')" },
"destination": { "type": "string", "description": "Destination airport code (e.g., 'LAX')" },
"date": { "type": "string", "description": "Flight date in 'YYYY-MM-DD' format" },
},
"required": ["source", "destination"]
}),
}
}
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
// 我们将在下一步实现调用航班搜索API的逻辑。
Ok("Flight search results".to_string())
}
}
实现call
函数
现在,让我们完善call
函数。
a. 获取API密钥
let api_key = env::var("RAPIDAPI_KEY").map_err(|_| FlightSearchError::MissingApiKey)?;
b. 设置默认值
let date = args.date.unwrap_or_else(|| {
let date = Utc::now() + Duration::days(30);
date.format("%Y-%m-%d").to_string()
});
c. 构建查询参数
let mut query_params = HashMap::new();
query_params.insert("sourceAirportCode", args.source);
query_params.insert("destinationAirportCode", args.destination);
query_params.insert("date", date);
d. 发送API请求
let client = reqwest::Client::new();
let response = client
.get("https://tripadvisor16.p.rapidapi.com/api/v1/flights/searchFlights")
.headers({
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("X-RapidAPI-Host", "tripadvisor16.p.rapidapi.com".parse().unwrap());
headers.insert("X-RapidAPI-Key", api_key.parse().unwrap());
headers
})
.query(&query_params)
.send()
.await
.map_err(|e| FlightSearchError::HttpRequestFailed(e.to_string()))?;
e. 解析和格式化响应
let text = response
.text()
.await
.map_err(|e| FlightSearchError::HttpRequestFailed(e.to_string()))?;
let data: Value = serde_json::from_str(&text)
.map_err(|e| FlightSearchError::HttpRequestFailed(e.to_string()))?;
let mut flight_options = Vec::new();
// 这里我们需要提取航班选项。(为了简洁起见,我们省略了完整代码。)
// 将航班选项格式化为可读的字符串
let mut output = String::new();
output.push_str("Here are some flight options:\n\n");
for (i, option) in flight_options.iter().enumerate() {
output.push_str(&format!("{}. **Airline**: {}\n", i + 1, option.airline));
// 其他格式化...
}
Ok(output)
现在我们的工具已经准备好了,让我们构建使用它的代理。
main.rs
打开src/main.rs
并更新:
mod flight_search_tool;
use crate::flight_search_tool::FlightSearchTool;
use dotenv::dotenv;
use rig::completion::Prompt;
use rig::providers::openai;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
dotenv().ok();
let openai_client = openai::Client::from_env();
let agent = openai_client
.agent("gpt-4")
.preamble("You are a helpful assistant that can find flights for users.")
.tool(FlightSearchTool)
.build();
let response = agent
.prompt("Find me flights from San Antonio (SAT) to Atlanta (ATL) on November 15th 2024.")
.await?;
println!("Agent response:\n{}", response);
Ok(())
}
译者注:此处的 OpenAI 可替换为其他大语言模型(LLM),例如近期备受瞩目的 DeepSeek。在学习过程中,我为 Rig 官方补充完善了关于 DeepSeek 能力的相关内容。
让我们看看我们的助手是如何工作的!
构建项目
在终端中运行:
cargo build
修复可能出现的编译错误。
运行应用程序
cargo run
你应该会看到类似以下的输出:
Agent response:
Here are some flight options:
1. **Airline**: Spirit
- **Flight Number**: NK123
- **Departure**: 2024-11-15T05:00:00-06:00
- **Arrival**: 2024-11-15T10:12:00-05:00
- **Duration**: 4 hours 12 minutes
- **Stops**: 1 stop(s)
- **Price**: 77.97 USD
- **Booking URL**: https://www.tripadvisor.com/CheapFlightsPartnerHandoff...
2. **Airline**: American
- **Flight Number**: AA456
- **Departure**: 2024-11-15T18:40:00-06:00
- **Arrival**: 2024-11-15T23:58:00-05:00
- **Duration**: 4 hours 18 minutes
- **Stops**: 1 stop(s)
- **Price**: 119.97 USD
- **Booking URL**: https://www.tripadvisor.com/CheapFlightsPartnerHandoff...
恭喜!你已经使用Rust和Rig构建了一个功能齐全的航班搜索AI助手。以下是我们的成果:
如果你对Rust、LLM或构建智能助手感兴趣,欢迎随时联系我!我很乐意回答你关于Rig的问题,并看看你用Rig构建了什么酷炫的项目。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!