前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >写点代码,做点视频

写点代码,做点视频

作者头像
tyrchen
发布2023-11-22 17:10:27
1350
发布2023-11-22 17:10:27
举报
文章被收录于专栏:程序人生程序人生

这个周末小宝终于没球赛了,我也不用开车来回奔波两小时,再在寒风中瑟瑟发抖两小时(赛前训练+比赛)看球。本来打算做个应用尝试结合语音和 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 的数据结构:

代码语言:javascript
复制
#[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 function
fn 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 中很简单地引用它:

代码语言:javascript
复制
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::<GetWeatherArgs>(
        "get_weather_forecast",
        "Get the weather forecast for a city.",
    ),
    Tool::new_function::<ExplainMoodArgs>(
        "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 的过程变得非常简单清晰:

代码语言:javascript
复制
#[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<AssistantEvent> for String {
    fn from(event: AssistantEvent) -> Self {
        event.render().unwrap()
    }
}

// 一些 helper function
fn in_transcription() -> String {
    AssistantEvent::Processing(AssistantStep::Transcription).into()
}

...

用于渲染 AssistantEventsignal.html.j2

代码语言:javascript
复制
{% 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>Complete</p>
{% else %}
<p class="text-yellow-700">Unknown event</p>
{% endmatch %}

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

代码语言:javascript
复制
signal_sender.send(in_transcription())?;
let input = transcript(llm, data.to_vec()).await?;
signal_sender.send(in_chat_completion())?;
let output = chat_completion(llm, &input).await?;
signal_sender.send(in_speech())?;
let audio_url = speech(llm, device_id, &output).await?;
signal_sender.send(complete())?;

由于太久不写 javascript,在录制的过程中,当我使用 MediaRecorder 时,按照 copilot 给出的代码(MDN 也是类似),我总遇到获取 audio data 出错的问题,大家可以看看下面的代码,想想为何:

代码语言:javascript
复制
let recorder = {
  mediaRecorder: null,
  recordedChunks: [],
  init: function () {

    // Request access to the microphone
    navigator.mediaDevices.getUserMedia({ audio: true })
      .then(stream => {
        this.mediaRecorder = new MediaRecorder(stream);
        console.log(this.mediaRecorder);

        this.mediaRecorder.ondataavailable = function(e) {
          // 这里 e.data 报错:read property 'push' of undefined
          this.recordedChunks.push(e.data);
        };
        ...
  }
}

一开始我以为是音频设备冲突的原因,于是停止了录制,结果还是出错。后来经过一番 debug,发现是作用域的问题,这句话应该写成:

代码语言:javascript
复制
this.mediaRecorder.ondataavailable = (e) => { ... }

最终,周日并未完成 ava-bot 的全部功能,只写了四百多行 Rust 代码:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序人生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对话机器人
对话机器人(Conversation Robot,ICR),是基于人工智能技术,面向企业场景的 AI 服务,可应用于智能客服、服务咨询、业务办理等场景。本产品旨在帮助企业快速构建,满足自身业务诉求的对话机器人,从而减少企业人力成本或解决服务不及时问题。用户可通过对话机器人用户端引擎,实现高准确率的对话服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档