两学一做 网站源码,天猫网站设计,网站开发 男生,自定义功能的网站代码已经开源#xff1a;#x1f680; fgpt 欢迎大家star⭐和fork #x1f44f;
ChatGPT现在免费提供了GPT3.5的Web访问#xff0c;不需要注册就可以直接使用#xff0c;但是#xff0c;它的使用方式是通过Web页面#xff0c;不够方便。
更多技术分享关注 入职啦…代码已经开源 fgpt 欢迎大家star⭐和fork
ChatGPT现在免费提供了GPT3.5的Web访问不需要注册就可以直接使用但是它的使用方式是通过Web页面不够方便。
更多技术分享关注 入职啦https://ruzhila.cn/?fromcsdn
Shell-GPT 是一个流行的OpenAI 命令行工具可以调用ChatGPT的API但是它需要注册并获取API密钥并且需要Python环境对于一些不熟悉Python的用户来说可能不太方便。
无依赖命令行使用GPT还是非常方便的因此我决定用Rust实现一个类似的工具不需要注册就可以直接使用支持CLI和OpenAI API代理的两种模式, 实际的运行效果 文章系列分为三部分发布记录完整的过程
基于ChatGPT的Web API实现基本的调用内置支持代理(这个很重要)完善命令行的功能: 支持代码、文件输入、交互式输入等实现OpenAI API代理兼容OpenAI的OpenAPI接口, 等同于免费使用GPT3.5的API
ChatGPT的Web API工作流程
通过分析ChatGPT的Web API我们可以发现它的工作流程如下
调用backend-anon/sentinel/chat-requirements接口获取一个token调用backend-anon/conversation接口基于SSE获取聊天的结果
所以要做的事情就是根据这个流程用Rust实现完整的流程已达到调用ChatGPT的目的。
我设计了一个简单的使用方式:
fgpt 输出一段python代码实现字符串反转fgpt这个命令行工具会调用ChatGPT的Web API返回一段Python代码并且根据SSE实现打字机的效果和交互式的输入。
需要用到哪些Rust的库(第一个版本)
第一个版本目标是完成基本的调用所以只需要能使用命令行参数、发送HTTP请求、序列化和反序列化、日志输出、生成uuid、正则表达式匹配、实现Stream的功能等。
第一个版本大概需要用到以下几个库
clap 用于解析命令行参数reqwest 用于发送HTTP请求tokio 用于异步编程serde 用于序列化和反序列化log和env_logger 用于日志输出uuid用来生成uuid regex 用于正则表达式匹配features 和Bytes 用于实现Stream的功能
程序结构分析
fgpt的代码结构如下
mpimpis-Mac-mini fgpt % ls src
cli.rs main.rs proxy.rs fgpt.rs主要的代码实现在src/fgpt.rs中src中包含了cli.rs和proxy.rs两个模块分别实现了CLI和OpenAI API代理的功能。
fgpt.rs 的实现
命令行和API代理只是呈现的方式不同但是实现的逻辑是一样的背后调用的都是fgpt.rs的逻辑。
所以我基于Stream的特性设计了一个能够支持CLI和API代理的通用的Stream可以充分利用好Stream的特性
pub(crate) struct CompletionStream {response_stream: PinBoxdyn StreamItem ResultBytes, reqwest::Error Send,
}impl Stream for CompletionStream {type Item reqwest::ResultString;fn poll_next(mut self: Pinmut Self, cx: mut Context_) - PollOptionSelf::Item {match self.response_stream.as_mut().poll_next(cx) {Poll::Ready(Some(Ok(data))) {....},}}}
}/// 调用实现这样的效果这样就可以支持CLI和API代理
let stream CompletionStream::new(reqwest::Client::new(), url, token);
while let Some(result) stream.next().await {println!({}, result.unwrap());
}解析Web API的返回结果
ChatGPT的返回结果是一个SSE的流从测试的情况来看返回有3种情况
data: {message: .... } 表示返回的是聊天的结果data: 2024-03-12 12:12:14.12 表示这个是一个心跳包data: [DONE] 表示当前的聊天结束
根据这个特点实现了一个Enum用来表示这三种情况
enum ChatGPTResponse {Data(CompletionResponse),Done,Heartbeat,Text(String),
}为了考虑后续的兼容性当出现消息不能被CompletionResponse解析当时候还能够返回原始的消息多兼容了一个Text当类型 CompletionResponse是根据ChatGPT返回的消息解析出来的结构体不展开讨论 impl FromBytesMut for CompletionEvent {fn from(line: BytesMut) - CompletionEvent {...serde_json::from_str(line_str).map(CompletionEvent::Data).unwrap_or(CompletionEvent::Text(line_str.to_string()))}}
}如何实现打字机的效果
根据OpenAI的OpenAPI文档我们可以知道ChatGPT的返回结果是一个Delta的结果也就是说每次返回的结果都是上一次的增量。
但是Web API并没有这个Delta的字段每次返回都是完整的结果所以我们需要自己实现这个效果。 这个实现也是比较简单就是保留上一次的结果然后和当前的结果进行比较然后输出差异部分, 实际上用的是strip_prefix这个函数
let mut textbuf String::new();while let Some(message) stream.next().await {match message {Ok(crate::fgpt::CompletionEvent::Data(message)) {let text message.message.content.parts.join(\n);let delta_chars text.strip_prefix(textbuf.as_str()).unwrap_or(text.as_str());textbuf text.clone();print!({}, delta_chars);let _ std::io::stdout().flush();}}....}为了实现打字机的效果print!(..)之后需要flush一下这样才能实现效果否则会等到换行的时候才输出不符合我们的预期。
总结
这个工具是昨天开始构思下午吃完饭的时候开始写晚上就写完第一个可以运行的版本总共写了410行的Rust代码明天会继续完善功能实现更多的功能比如支持文件输入、代码输入、交互式输入等。
可以加群学习