Rust异步封装库ChatGPT-rs:轻松集成OpenAI API,实现函数调用与对话管理
1. 项目概述:ChatGPT-rs,一个为Rust开发者打造的OpenAI API异步封装库
如果你是一名Rust开发者,同时又对集成OpenAI的ChatGPT API感兴趣,那么你很可能已经厌倦了手动处理HTTP请求、解析JSON响应、管理对话状态和令牌计数这些繁琐的底层工作。今天要聊的这个项目——ChatGPT-rs,正是为了解决这些痛点而生。它是一个纯异步的Rust库,对OpenAI的ChatGPT API进行了深度封装,让你能用更符合Rust习惯的方式,轻松地将强大的对话AI能力集成到你的应用中。无论是构建一个命令行聊天工具、一个智能客服后端,还是一个需要复杂推理的自动化流程,这个库都能提供坚实、优雅的基础。
简单来说,ChatGPT-rs让你用几行Rust代码就能发起对话、管理多轮上下文、处理流式响应,甚至调用你自定义的函数(Function Calling)。它抽象了API的细节,让你专注于业务逻辑。库的设计遵循了Rust的哲学:安全、高效、明确。它提供了强类型的API、灵活的配置选项以及对错误处理的良好支持。接下来,我会带你深入这个库的每一个核心功能,分享我在集成和使用过程中积累的实战经验、遇到的坑以及高效的解决方案。
2. 核心功能与设计思路拆解
2.1 异步优先与类型安全的设计哲学
ChatGPT-rs从根子上就是一个异步库,这完全契合了现代网络应用,尤其是I/O密集型AI调用的场景。它底层基于reqwest和tokio(或async-std,取决于你的选择)这样的异步HTTP客户端,确保了在高并发下也能保持高效。更关键的是,它的API设计充满了Rust的“类型安全”味道。
当你调用client.send_message(“Hello”)时,返回的不是一个模糊的Result<serde_json::Value>,而是一个具体的Result<CompletionResponse>。这个CompletionResponse类型包含了强类型的字段,比如message: ChatMessage。这意味着你在编码阶段就能利用Rust编译器的力量:访问不存在的字段?编译器会报错。错误处理不完整?编译器会提醒你。这种设计极大地减少了运行时错误,让你对数据流转更有信心。
我在实际项目中的体会是,这种类型安全在构建复杂工作流时优势明显。例如,当你需要从响应中提取内容并做后续处理时,直接使用response.message().content就能得到一个&str,无需再进行繁琐的JSON解析和空值检查。库已经帮你处理了API返回的各种边界情况,并将它们映射到了合适的Rust类型和错误变体中。
2.2 对话(Conversation)管理:状态保持的核心
与简单的单次问答不同,ChatGPT的强大之处在于其上下文理解能力。ChatGPT-rs通过Conversation(对话)类型完美封装了这一概念。你可以把它想象成一个有状态的会话线程。
创建一个新对话client.new_conversation(),库内部会自动为该对话生成一个唯一的ID,并维护一个history向量,记录所有已发送和接收的消息。当你后续调用conversation.send_message(“新的问题”)时,库会自动将整个历史记录作为上下文附加到新的API请求中。这样,ChatGPT就能“记住”之前的对话内容,实现连贯的多轮交互。
这里有一个非常重要的细节:令牌(Token)管理。OpenAI API按令牌数收费,并且有上下文长度限制(例如,gpt-3.5-turbo通常是4096个令牌)。Conversation对象内部并不自动进行令牌计数或历史截断。这是一个设计上的取舍,将控制权交给了开发者。我的经验是,对于长对话,你需要自己实现一个逻辑来监控历史消息的令牌总数(可以使用tiktoken-rs这类库进行估算),并在接近模型上限时,选择性地移除最早的一些消息(通常是系统消息和最早的用户/助手对话对),以腾出空间给新的交互。库的conversation.history是公开的Vec<ChatMessage>,你可以直接操作它。
2.3 函数调用(Function Calling):扩展模型能力的桥梁
函数调用是ChatGPT API一个革命性的特性,它允许模型在推理后决定调用开发者预先定义好的函数,并将执行结果返回给模型,从而完成更复杂的任务(如查询数据库、执行计算、调用外部API)。ChatGPT-rs通过#[gpt_function]属性宏,让在Rust中定义和使用这一功能变得异常简单。
其设计思路非常巧妙:利用Rust的过程宏和过程宏。你只需要在一个async fn上标注#[gpt_function],库就会在编译时分析函数的签名和文档注释。文档注释成为了模型的“说明书”:函数整体的文档注释描述了函数的功能,而每个参数的注释(格式为* 参数名 - 描述)则说明了参数的意义。库会将这些信息自动转换为OpenAI API要求的函数定义JSON Schema。
在实际使用中,我发现这个特性极大地提升了应用的“行动力”。例如,你可以定义一个query_weather(city: String)的函数,当用户问“北京天气怎么样?”时,ChatGPT会识别出意图,生成一个包含city: “北京”的JSON来调用你的函数。你的函数执行真实的天气查询后,将结果返回,ChatGPT再组织成自然语言回复给用户。整个过程几乎是声明式的,你只需要关心函数本身的业务逻辑。
注意:函数调用会消耗额外的令牌(用于描述函数),并且模型有时会产生“幻觉”,即尝试调用不存在的函数或生成无效参数。库提供了
FunctionValidationStrategy::Strict模式,可以在一定程度上纠正模型的错误,但这并不能完全避免。因此,在你的函数实现中,必须对输入参数进行严格的校验和防御性编程。
3. 从零开始:环境配置与基础使用详解
3.1 项目初始化与依赖添加
首先,你需要一个Rust开发环境。确保安装了最新稳定的Rust工具链(rustup是管理工具链的推荐方式)。ChatGPT-rs要求的最低Rust版本(MSRV)是1.71.1,通常使用最新稳定版即可避免兼容性问题。
在你的Cargo.toml文件中添加依赖。库默认只包含核心的异步客户端和对话管理功能。
[dependencies] chatgpt = “3.2.0” # 请检查crates.io获取最新版本 tokio = { version = “1.0”, features = [“full”] } # 或 async-std,根据你的运行时选择 dotenv = “1.0” # 可选,用于从.env文件加载API密钥如果你需要流式响应、函数调用或特定的持久化格式,需要启用相应的特性(features):
[dependencies] chatgpt = { version = “3.2.0”, features = [“streams”, “functions”, “json”] } # `streams`: 启用流式响应支持。 # `functions`: 启用函数调用支持。 # `json`: 启用JSON格式的对话历史持久化(默认启用)。 # `postcard`: 启用Postcard(二进制)格式的持久化。 # `functions_extra`: 为函数参数提供对`uuid`, `chrono`等常用库类型的Schema支持。3.2 获取与安全管理OpenAI API密钥
使用任何OpenAI API服务的前提是拥有一个有效的API密钥。你需要在OpenAI平台注册并创建API Key。
绝对不要将API密钥硬编码在源代码中,尤其是如果你计划将代码提交到公开的版本控制系统(如GitHub)。密钥泄露会导致他人滥用你的账户,产生巨额费用。推荐的做法是使用环境变量。
本地开发:在项目根目录创建
.env文件(确保该文件已被添加到.gitignore中):OPENAI_API_KEY=sk-your-actual-api-key-here然后在代码中使用
std::env::var或dotenv库来读取。生产环境:通过你的服务器或云平台(如Docker的
-e参数、Kubernetes的Secret、AWS的Parameter Store等)设置环境变量。
一个安全的客户端初始化示例如下:
use chatgpt::prelude::*; use std::env; #[tokio::main] async fn main() -> Result<()> { // 从环境变量读取API密钥 let key = env::var(“OPENAI_API_KEY”) .expect(“请设置 OPENAI_API_KEY 环境变量”); // 创建客户端实例 let client = ChatGPT::new(key)?; // ... 后续操作 Ok(()) }3.3 发起你的第一次API调用
让我们完成一个最简单的单次问答,以验证环境是否配置正确。
use chatgpt::prelude::*; #[tokio::main] async fn main() -> Result<()> { let key = std::env::var(“OPENAI_API_KEY”).unwrap(); let client = ChatGPT::new(key)?; // 发送一条消息 let response: CompletionResponse = client .send_message(“用五个词描述Rust编程语言。”) .await?; // 注意:这里使用了`?`操作符进行错误传播 // 打印回复内容 println!(“ChatGPT 回复: {}”, response.message().content); Ok(()) }运行这段代码,如果一切正常,你会在终端看到ChatGPT对Rust语言的简短描述,例如“安全、并发、高效、系统级、表达性强”。
关键点解析:
ChatGPT::new(key): 这会创建一个使用默认配置的客户端,指向OpenAI的官方API端点,并使用默认的GPT模型(通常是gpt-3.5-turbo)。send_message: 这是一个异步方法,返回Future<Output = Result<CompletionResponse>>。必须使用.await来获取结果。Result<CompletionResponse>: 库使用了自己的Result类型(通常是chatgpt::err::Result),它封装了网络错误、API错误(如认证失败、额度不足)、解析错误等。response.message().content: 这是获取回复文本的标准方式。ChatMessage结构体还包含role(角色,如assistant)等信息。
4. 深入核心功能:对话、流式响应与函数调用实战
4.1 管理多轮对话(Conversation)
单次问答意义有限,真正的价值在于持续的对话。下面我们创建一个对话,并连续进行多轮交互。
use chatgpt::prelude::*; #[tokio::main] async fn main() -> Result<()> { let client = ChatGPT::new(std::env::var(“OPENAI_API_KEY”).unwrap())?; // 1. 创建新对话 let mut conversation = client.new_conversation(); // 2. 第一轮对话 let response_1 = conversation .send_message(“Rust的所有权系统是什么?”) .await?; println!(“助手: {}”, response_1.message().content); // 3. 第二轮对话,模型会基于之前的上下文回答 let response_2 = conversation .send_message(“它能解决什么问题?”) // 这里的“它”指代所有权系统 .await?; println!(“助手: {}”, response_2.message().content); // 4. 查看完整的历史记录 println!(“\n=== 对话历史 ==="); for (i, msg) in conversation.history.iter().enumerate() { println!(“{}: {:?}”, i, msg); } Ok(()) }自定义对话指令:每个对话在创建时都有一个“系统消息”(system message),它用于设定AI助手的角色和行为准则。默认指令是“你是一个由OpenAI开发的AI助手…”。你可以通过new_conversation_directed方法来定制它,这对于构建专业领域的聊天机器人至关重要。
// 创建一个专注于代码审查的助手 let mut code_review_bot = client.new_conversation_directed( “你是一个资深的Rust代码审查专家。你的回答必须专注于代码安全、性能、符合Rust惯用法。对于非Rust代码或不相关的问题,礼貌地拒绝回答。” );4.2 处理流式响应(Streaming)
对于需要长时间生成文本的场景(如写作助手、代码生成),等待完整响应返回可能会造成不好的用户体验。流式响应允许你像看打字机一样,实时看到文本一个个单词地出现。
启用streams特性后,你可以使用send_message_streaming方法。它返回一个Stream(流),你可以逐块(chunk)处理数据。
use chatgpt::prelude::*; use futures_util::StreamExt; // 需要引入 futures_util 或 tokio_stream 的 StreamExt use std::io::{self, Write}; #[tokio::main] async fn main() -> Result<()> { let client = ChatGPT::new(std::env::var(“OPENAI_API_KEY”).unwrap())?; let mut conversation = client.new_conversation(); println!(“用户: 请写一个简单的Rust函数,计算斐波那契数列的第n项。”); print!(“助手: “); // 获取流式响应 let mut stream = conversation .send_message_streaming(“请写一个简单的Rust函数,计算斐波那契数列的第n项。”) .await?; let mut full_response = String::new(); // 迭代流中的每一个数据块 while let Some(chunk) = stream.next().await { match chunk { ResponseChunk::Content { delta, .. } => { // `delta` 是本次块中新增加的文本内容 print!(“{}”, delta); io::stdout().flush().unwrap(); // 立即刷新标准输出,确保内容显示 full_response.push_str(&delta); } // 其他类型的块,如 ResponseChunk::Done,表示流结束 _ => {} } } println!(); // 打印换行 // !!! 重要:流式响应不会自动保存到对话历史中 !!! // 需要手动构造 ChatMessage 并加入 history if !full_response.is_empty() { let assistant_message = ChatMessage { role: Role::Assistant, content: full_response, // name, function_call 等字段根据情况设置 ..Default::default() }; conversation.history.push(assistant_message); } Ok(()) }实操心得:流式处理虽然提升了体验,但增加了复杂性。你需要处理网络中断、管理缓冲区,并记得手动保存消息到历史中。对于对话型应用,我通常会在流式接收完毕后,将完整的消息加入历史,以保证上下文的完整性。另外,注意控制流的速率,避免过快的打印导致终端输出混乱。
4.3 实现函数调用(Function Calling)
这是ChatGPT-rs库最强大的功能之一。我们通过一个完整的例子来演示:构建一个能查询“虚拟城市信息”的助手。
首先,在Cargo.toml中启用functions特性。
use chatgpt::prelude::*; use serde::{Deserialize, Serialize}; use schemars::JsonSchema; // 1. 定义函数参数的结构体,并派生必要的trait #[derive(Debug, Serialize, Deserialize, JsonSchema)] struct CityQuery { /// 要查询的城市名称 city_name: String, /// 需要的信息类型,如 ‘weather‘, ‘population‘ info_type: String, } // 2. 定义函数本身,使用 #[gpt_function] 属性宏 /// 查询指定城市的某类信息。 /// /// * query - 包含城市名和信息类型的查询参数 #[gpt_function] async fn get_city_info(query: CityQuery) -> String { // 这里应该是真实的数据库或API查询 // 为了示例,我们返回模拟数据 match query.info_type.as_str() { “weather” => format!(“{}的天气是晴朗,25摄氏度。”, query.city_name), “population” => format!(“{}的模拟人口是100万。”, query.city_name), _ => format!(“无法查询{}的{}信息。”, query.city_name, query.info_type), } } #[tokio::main] async fn main() -> Result<()> { let client = ChatGPT::new(std::env::var(“OPENAI_API_KEY”).unwrap())?; let mut conversation = client.new_conversation(); // 3. 将函数“添加”到对话中 // 注意:这里传递的是函数调用 `get_city_info()`,它返回一个实现了 `ChatGPTFunction` trait 的对象。 conversation.add_function(get_city_info()); // 4. 发送消息,并允许模型调用函数 let response = conversation .send_message_functions(“我想知道北京的人口和上海的天气。”) .await?; println!(“最终回复: {}”, response.message().content); // 可能的输出:“北京的人口是100万。上海的天气是晴朗,25摄氏度。” // 在这个过程中,模型可能进行了两次函数调用(或一次组合调用)。 Ok(()) }过程解析:
- 模型收到用户消息“我想知道北京的人口和上海的天气。”
- 模型分析后,发现需要调用
get_city_info函数。它会生成一个或多个结构化的函数调用请求(JSON),包含参数{“city_name”: “北京”, “info_type”: “population”}和{“city_name”: “上海”, “info_type”: “weather”}。 ChatGPT-rs库拦截这些请求,在本地执行你定义的get_city_info函数。- 库将函数的返回结果(字符串)作为新的上下文信息发送回给模型。
- 模型接收到函数执行结果后,组织成一段连贯的自然语言回复。
- 你通过
response.message().content获得最终答案。
高级配置与验证:为了防止模型“幻觉”调用,你可以在创建客户端时使用严格验证策略。
use chatgpt::config::ModelConfigurationBuilder; use chatgpt::prelude::*; let config = ModelConfigurationBuilder::default() .function_validation(chatgpt::config::FunctionValidationStrategy::Strict) // 启用严格验证 .build() .unwrap(); let client = ChatGPT::new_with_config(api_key, config)?;在Strict模式下,如果模型尝试调用未定义的函数或提供了无效参数,库会向模型发送一个系统消息进行纠正,要求其重新生成。这通常会增加一次API往返,但能提高可靠性。
5. 高级配置与生产环境考量
5.1 模型配置详解
ModelConfigurationBuilder提供了丰富的选项来定制API请求行为。
use chatgpt::prelude::*; use chatgpt::config::{ModelConfigurationBuilder, ChatGPTEngine}; let config = ModelConfigurationBuilder::default() .temperature(0.7) // 创造性/随机性 (0.0-2.0)。越高越随机,越低越确定。 .top_p(0.9) // 核采样,与temperature二选一。通常只设置一个。 .max_tokens(1024) // 生成回复的最大令牌数。需预留上下文令牌。 .presence_penalty(0.0) // 存在惩罚 (-2.0 到 2.0)。正值降低重复话题概率。 .frequency_penalty(0.0) // 频率惩罚 (-2.0 到 2.0)。正值降低重复用词概率。 .engine(ChatGPTEngine::Gpt4) // 指定模型,如 Gpt4, Gpt4Turbo, Gpt35Turbo等。 .api_url(“https://api.openai.com/v1/chat/completions”.into()) // 自定义端点(例如使用代理) .timeout(std::time::Duration::from_secs(60)) // 请求超时时间 .build() .unwrap(); let client = ChatGPT::new_with_config(api_key, config)?;参数选择经验:
- 温度(Temperature):对于需要确定性输出的任务(如代码生成、数据提取),设置为较低值(0.1-0.3)。对于创意写作、头脑风暴,可以设置高一些(0.7-1.0)。
- 最大令牌数(max_tokens):务必设置。这既是成本控制,也是防止生成过长无用文本的安全阀。需要根据你预留的上下文长度(历史消息的令牌数)来设定。
- 模型引擎(Engine):
Gpt35Turbo性价比高,响应快;Gpt4或Gpt4Turbo理解能力和复杂任务处理能力更强,但价格更贵、速度可能更慢。根据任务需求选择。
5.2 对话持久化:保存与恢复会话状态
对于需要长期运行的聊天应用,将会话历史保存到磁盘或数据库是必须的。ChatGPT-rs内置了JSON和Postcard两种序列化支持。
使用JSON持久化(默认特性json):
// 保存对话 conversation.save_history_json(“./chat_history/conversation_123.json”).await?; // 在程序下次启动时恢复 let mut restored_conversation = client .restore_conversation_json(“./chat_history/conversation_123.json”) .await?; // 现在可以继续和 restored_conversation 对话了使用Postcard持久化(需启用postcard特性): Postcard是一种高效的二进制序列化格式,生成的文件更小,序列化/反序列化速度更快。
conversation.save_history_postcard(“./chat_history/conversation_123.bin”).await?; let mut restored = client.restore_conversation_postcard(“./chat_history/conversation_123.bin”).await?;生产环境建议:
- 分离存储:不要只依赖本地文件。对于Web服务,应该将会话历史与用户信息关联,存储在数据库(如PostgreSQL的JSONB字段、MongoDB)或分布式缓存(如Redis)中。
- 定期清理:对话历史会增长。实现一个清理策略,例如只保留最近N条消息,或当令牌数超过阈值时丢弃最旧的消息对。
- 自定义序列化:由于
Conversation.history是Vec<ChatMessage>,且ChatMessage实现了Serde的trait,你可以轻松地使用任何Serde兼容的库(如bincode,cbor) 将其存储到你的自定义存储后端。
use serde_json; let history_json = serde_json::to_string(&conversation.history)?; // 将 history_json 存入数据库...5.3 错误处理与重试策略
网络请求和远程API调用充满了不确定性。健壮的生产代码必须有完善的错误处理。
use chatgpt::prelude::*; use chatgpt::err::Error; async fn send_message_with_retry( client: &ChatGPT, conversation: &mut Conversation, text: &str, max_retries: u32, ) -> Result<CompletionResponse> { let mut retries = 0; loop { match conversation.send_message(text).await { Ok(resp) => return Ok(resp), Err(e) => { retries += 1; if retries >= max_retries { return Err(e); } // 根据错误类型决定是否重试 match &e { Error::ApiError(api_err) if api_err.is_rate_limit() => { // 速率限制,等待一段时间 println!(“达到速率限制,等待后重试…”); tokio::time::sleep(tokio::time::Duration::from_secs(5 * retries)).await; } Error::ReqwestError(reqwest_err) if reqwest_err.is_timeout() || reqwest_err.is_connect() => { // 网络超时或连接错误,重试 println!(“网络错误,第{}次重试…”, retries); tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } _ => { // 其他错误(如认证错误、逻辑错误),不重试,直接失败 return Err(e); } } } } } }常见错误类型:
Error::ReqwestError: 底层网络错误(超时、连接失败等)。Error::ApiError: OpenAI API返回的错误,包含status_code和message。常见的有429(速率限制)、401(认证失败)、400(无效请求)、503(服务过载)。Error::JsonError: JSON解析错误。Error::InternalError: 库内部逻辑错误。
6. 常见问题、性能优化与避坑指南
6.1 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
编译错误:cannot find macro ‘gpt_function‘ | 未启用functionsCargo特性。 | 在Cargo.toml中确保features = [“functions”, …]。 |
运行时错误:Invalid API Key | API密钥错误、过期或环境变量未正确设置。 | 检查密钥有效性,确保程序运行时能读取到正确的环境变量。使用echo $OPENAI_API_KEY(Unix) 或echo %OPENAI_API_KEY%(Windows) 验证。 |
错误:429 Too Many Requests | 达到OpenAI的速率限制(RPM-每分钟请求数,TPM-每分钟令牌数)。 | 实现指数退避重试逻辑。检查你的使用量,考虑升级套餐或在代码中增加请求间隔。 |
| 流式响应不显示或显示不全 | 标准输出未及时刷新。 | 在打印每个delta后调用io::stdout().flush().unwrap()。 |
| 对话“忘记”了之前的上下文 | 1. 使用了新的Conversation对象。2. 历史记录被意外清空。 3. 上下文长度超限,模型无法处理。 | 1. 确保复用同一个conversation对象。2. 检查代码逻辑,避免覆盖 history。3. 实现历史消息令牌计数和截断逻辑。 |
| 函数调用未被触发 | 1. 未使用send_message_functions方法。2. 函数描述(文档注释)不够清晰。 3. 模型认为不需要调用函数。 | 1. 确认调用的是send_message_functions。2. 完善函数和参数的文档注释。 3. 在用户提问中更明确地提示需要调用函数。 |
| 程序编译或运行缓慢 | 启用了不必要的特性,或依赖项过多。 | 在Cargo.toml中只启用你需要的特性,例如features = [“json”]。使用cargo build –release进行发布构建。 |
6.2 性能优化与最佳实践
- 客户端复用:
ChatGPT客户端是线程安全的,通常应该作为一个全局单例或通过依赖注入在应用中共享。避免为每个请求都创建新的客户端,以减少连接开销。 - 连接池:底层的
reqwest客户端默认使用了连接池。确保你使用的是同一个reqwest::Client实例(ChatGPT-rs内部管理)。 - 异步任务:在处理大量独立对话请求时,使用
tokio::spawn等机制并发处理,充分利用异步IO的优势。但要注意OpenAI的并发和速率限制。 - 令牌估算与成本控制:使用
tiktoken-rs库在发送请求前估算消息的令牌数。这有助于你做出决策:是截断历史、总结历史,还是拒绝过长的请求。建立监控,关注API使用成本和速率限制。 - 超时与熔断:为API调用设置合理的超时(通过
ModelConfigurationBuilder::timeout),并考虑引入熔断器模式(如使用tower或circuitbreaker库),防止因OpenAI服务不稳定导致自身应用雪崩。 - 结构化输出:对于需要从模型回复中提取结构化数据的场景(如生成JSON),除了使用函数调用,也可以尝试在系统指令中明确要求模型以特定格式(如Markdown代码块包裹的JSON)回复,然后在客户端进行解析。这有时比函数调用更轻量。
6.3 我踩过的坑与心得
- 流式响应与历史记录:这是我最初最容易忽略的一点。流式响应 (
send_message_streaming) 不会自动保存消息到对话历史中。如果你在流式接收后继续对话,模型会丢失刚刚那轮回复的上下文。务必记得像前面的示例一样,手动将完整的回复内容构造为ChatMessage并push到conversation.history中。 - 令牌限制不是错误:当对话历史超过模型上下文窗口时,API不会返回一个明确的错误,而是会静默地丢弃最早的消息直到符合长度限制。这可能导致模型“失忆”。因此,主动管理历史长度是你的责任。
- 函数描述的精确性:函数调用功能严重依赖你写的文档注释。模糊的描述会导致模型错误调用或拒绝调用。务必用清晰、无歧义的语言描述函数的目的和每个参数的确切含义。可以多花时间打磨这些“提示词”。
- 配置的继承:通过
ChatGPT::new_with_config创建的客户端配置是全局的。如果你需要针对不同对话使用不同配置(例如,一个对话用GPT-4做复杂分析,另一个用GPT-3.5做简单问答),目前需要创建不同的客户端实例。Conversation本身不持有模型配置。 - 错误处理要细致:不要简单地将所有错误
unwrap。特别是Error::ApiError,它包含了OpenAI返回的详细信息,对于调试认证、配额、内容策略等问题至关重要。将这些错误信息记录到日志中。
