专栏名称: 程序人生
十年漫漫程序人生,打过各种杂,也做过让我骄傲的软件;管理过数十人的团队,还带领一班兄弟姐妹创过业,目前在硅谷一家创业公司担任 VP。关注程序人生,了解程序猿,学做程序猿,做好程序猿,让我们的程序人生精彩满满。
目录
相关文章推荐
北美留学生观察  ·  马斯克查出了美国“吸血鬼”,这帮“老不死的” ... ·  10 小时前  
科技美学  ·  小鹏M03如何开启哨兵模式 ·  10 小时前  
北美留学生观察  ·  欠薪停办?深圳一DSE学校开学前网传爆雷,办 ... ·  昨天  
北美留学生观察  ·  事关100万留学家庭,中央发布《教育强国建设 ... ·  2 天前  
51好读  ›  专栏  ›  程序人生

写点代码,做点视频

程序人生  · 公众号  ·  · 2023-11-20 21:05

正文

这个周末小宝终于没球赛了,我也不用开车来回奔波两小时,再在寒风中瑟瑟发抖两小时(赛前训练+比赛)看球。本来打算做个应用尝试结合语音和 chat completion 中的 tools 做个智能客服,结果rust下一个好用的openai sdk都没有,于是干脆心一横,周六边写边录了7个视频(前后大概 6-7 小时),也算是为了一碟醋,包了顿饺子。后来有朋友提醒可以用 async-openai(有 700 多 star),不过木已成舟,也就算了。编辑视频的时候看了看 async-openai 的代码,实现思路跟我类似,但很多处理的选择不那么好,比如 reqwest::Client 其实 Clone 起来非常轻量,但它大量使用带生命周期的 Client,增加没必要的复杂性。此外没有充分利用 reqwest 生态,不管是 retry 还是 multipart 的处理,都写了很多不必要的代码。

不管怎样,自己写一遍 OpenAI API 的 SDK,还是有很多收获的。首先,进一步理解了 OpenAI 的 API,也吐槽了一些 API 参数设计不合理的地方;其次,对 serde,尤其是 serde 对 enum 的各种场景的使用,有了更深刻的了解;最后,就是终于找到了最舒服的使用 chat completion with tools 的方法,比如我只需要为 tools 有关的代码使用特定的使用 JsonSchema 的数据结构:

#[allow(dead_code)]#[derive(Debug, Clone, Deserialize, JsonSchema)]struct GetWeatherArgs {    /// The city to get the weather for.    pub city: String,    /// the unit    pub unit: TemperatureUnit,}
#[allow(dead_code)]#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, JsonSchema)]enum TemperatureUnit { /// Celsius #[default] Celsius, /// Fahrenheit Fahrenheit,}
#[derive(Debug, Clone)]struct GetWeatherResponse { temperature: f32, unit: TemperatureUnit,}
// dummy functionfn get_weather_forecast(args: GetWeatherArgs) -> GetWeatherResponse { match args.unit { TemperatureUnit::Celsius => GetWeatherResponse { temperature: 22.2, unit: TemperatureUnit::Celsius, }, TemperatureUnit::Fahrenheit => GetWeatherResponse { temperature: 72.0, unit: TemperatureUnit::Fahrenheit, }, }}

然后我就可以在 ChatCompletionRequest 中很简单地引用它:

let messages = vec![    ChatCompletionMessage::new_system("I can choose the right function for you.", ""),    ChatCompletionMessage::new_user("What is the weather like in Boston?", "user1"),];let tools = vec![    Tool::new_function::(        "get_weather_forecast",        "Get the weather forecast for a city.",    ),    Tool::new_function::(        "explain_mood",        "Explain the meaning of the given mood.",    ),];let req = ChatCompletionRequest::new_with_tools(messages, tools);let res = SDK.chat_completion(req).await?;assert_eq!(res.model, ChatCompleteModel::Gpt3Turbo);assert_eq!(res.object, "chat.completion");assert_eq!(res.choices.len(), 1);let choice = &res.choices[0];assert_eq!(choice.finish_reason, FinishReason::ToolCalls);assert_eq!(choice.index, 0);assert_eq!(choice.message.content, None);assert_eq!(choice.message.tool_calls.len(), 1);let tool_call = &choice.message.tool_calls[0];assert_eq!(tool_call.function.name, "get_weather_forecast");
get_weather_forecast(serde_json::from_str(&tool_call.function.arguments)?);

使用者不需要自己撰写复杂的关于参数的 json schema。

编写边录了大半天,最终写下了大概 1.2k 行 Rust 代码,录了7个视频:

视频这周每天都发一个,一周就把它发完。

饺子包完了,终于轮到那碟醋 —— 智能客服。周日一大早,我开了个新的项目,叫 ava-bot,也是边录边写。不过周日活动比较多,所以断断续续写了大概4-5小时,录了5个视频。第一个视频探讨了设计思路:

这个思路在实际执行时稍有偏差,比如 mpsc::Channel 最终换成了 broadcast::Channel 。这种通过 Channel 在两个路由间传输数据的方式还是很漂亮的:执行时间很长的 /assistant 路由不断把中间状态发送到 Channel 里,而使用支持 SSE(Server-Side Event)的路由 /chats /signals 不断从 Channel 里拿数据,渲染成 HTML 后,以 SSE 发给客户端,客户端最终通过 HTMX SSE 组件自动进行更新。

这个实现我觉得最优美的地方是使用 enum + template + From trait + helper function 使得冗长的数据结构到渲染 html 的过程变得非常简单清晰:

#[derive(Debug, Clone, Serialize, Deserialize, Template)]#[template(path = "signal.html.j2")]#[serde(tag = "type", content = "data", rename_all = "snake_case")]enum AssistantEvent {    Processing(AssistantStep),    Finish(AssistantStep),    Error(String),    Complete,}
#[derive(Debug, Clone, Serialize, Deserialize, EnumString, Display)]#[serde(rename_all = "snake_case")]#[strum(serialize_all = "snake_case")]enum AssistantStep { UploadAudio, Transcription, ChatCompletion, Speech,}
impl From for String { fn from(event: AssistantEvent) -> Self { event.render().unwrap() }}
// 一些 helper functionfn in_transcription() -> String { AssistantEvent::Processing(AssistantStep::Transcription).into()}
...

用于渲染 AssistantEvent signal.html.j2

{% match self %}{% when AssistantEvent::Processing with (v) %}<p class="text-gray-800"><i class="fa-solid fa-spinner animate-spin">i> Processing {{ v }}p>{% when AssistantEvent::Finish with (v) %}<p class="text-green-800">Finished {{ v }}p>{% when AssistantEvent::Error with (v) %}<p class="text-red-500"><i class="fa-solid fa-circle-exclamation">i>Error: {{ v }}p>{% when AssistantEvent::Complete %}<p class="text-green-800"><i class="fa-solid fa-check">i>Completep>{% else %}<p class="text-yellow-700">Unknown eventp>{% endmatch %}

这使得在 /assitant 路由中发送状态的代码变得非常优美:







请到「今天看啥」查看全文


推荐文章
科技美学  ·  小鹏M03如何开启哨兵模式
10 小时前
金融先生MrFinance  ·  这是一条带了虾爬子味的推送
7 年前
广东配音最前线  ·  香港黑.帮用英文讲数,掂过碌蔗!
7 年前
财经早餐  ·  人间最美是 —— 原谅
7 年前
21世纪药店  ·  上班族不得不看的三条“金规”
7 年前