今天是超級自信之作哈哈哈
🏮 今天完整的程式碼可以拉到最底下 Put it together 區塊或是在 GitHub 找到。
在實作任何程式邏輯之前,我們先來定義一個結構以代表使用者與 LLM 之間的對話。
這在 Rust 中可以透過自訂 Struct
型別來完成,它又稱為結構體 (structure),可以用來把各種相關的數值組合起來成為一個自訂的型別,以物件導向的概念來說,Struct
就像是物件的資料屬性 (attribute)。
將程式碼模組化能把相關的功能組織起來,並依照功能分門別類,如此一來,我們就能清楚地知道實作特定功能的程式碼在哪。
而 Rust 提供一系列管理程式碼組織的功能,其中包含了哪些實作細節能對外公開、哪些細節是私有的,以及程式中每個作用域的名稱為何,這些功能統一稱作模組系統 (module system),其中包含:
use
: 讓你控制組織、作用域與路徑的隱私權這裡簡略說明模組、路徑、use
與 pub
關鍵字在編譯器如何運作:
從 crate 源頭開始:編譯 crate 時,編譯器會先尋找 crate 的源頭檔案來編譯程式碼。
若為函式庫 crate 一般是指 src/lib.rs
,執行檔 crate 則是指 src/main.rs
。
crate 是 Rust 編譯器在當下視為程式碼的最小單位,例如每次初始化專案會給的
main.rs
檔案。
crate 有兩種形式:執行檔 (Binary) crate 或函式庫 (Library) crate。
前者是能編譯成執行檔並執行的程式,通常需要main
函式來定義在執行時該做什麼事。
後者則不會有main
函式也不會被編譯成執行檔,就跟我們熟悉的函式庫功能相同。
宣告模組:在 crate 源頭檔案中,可以使用 mod
關鍵字宣告新的模組。
例如我們宣告了一個「LycoReco」模組 mod LycoReco;
。
編譯器會在下面這幾個地方尋找模組的程式碼:
mod LycoReco {...}
區塊中src/LycoReco.rs
檔案中src/LycoReco/mod.rs
檔案中
這是比較舊的路徑風格,使用這種風格最大的缺點就是專案中有大量名為 mod.rs 的檔案,同時開啟時很容易混淆。
宣告子模組:除了 crate 源頭之外,其他檔案也可以宣告子模組。
例如我們可以在 src/LycoReco.rs
中宣告 mod closet;
。
編譯器會與當前模組同名的目錄底下這幾處尋找子模組的程式碼:
mod closet {...}
區塊中src/LycoReco/closet.rs
檔案中src/LycoReco/closet/mod.rs
檔案中模組的路徑:一旦模組成為 crate 的一部分,只要隱私權規則允許,其程式碼可以在 crate 內任意地方被使用。
例如「LycoReco」模組下「closet」模組中的 Walnut 型別可以用 crate::LycoReco::closet::Walnut
來找到。
私有 vs 公開:模組內的程式碼從上層模組來看預設是私有的。要公開的話,必須將它宣告為 pub mod
,而公開模組內的項目也可以用 pub
來公開。
use
關鍵字:在一個作用域內,use
關鍵字可以建立項目的捷徑,來縮短冗長的路徑名稱。
而上面的範例專案就可以包含以下這些檔案與資料夾:
.
├── Cargo.lock
├── Cargo.toml
└── src
├── LycoReco
│ └── closet.rs
├── closet.rs
└── main.rs
而根據上面的說明,我們可以先在 src
資料夾中建立一個 model
資料夾與 model.rs
,然後在 model 資料夾中加上 conversation.rs
。
然後在 model.rs
的上方加上 pub mod conversation;
與 lib.rs
的 pub mod app;
下面加上 pub mod model;
來將這些模組加入專案的模組樹中。
所以此時 src 資料夾的結構如下:
.
└── src
├── model
│ └── conversation.rs
├── model.rs
├── lib.rs
├── app.rs
└── main.rs
在建立代表對話的結構體之前,我們首先需要另一個結構體來代表訊息本身,這裡將其命名為 Message
:
pub struct Message {
pub user: bool,
pub text: String,
}
其中有兩個欄位 (fields),user
以布林值區分是否為使用者輸入的訊息,而 text
則為訊息本身。
有了代表訊息的結構體後,就可以建立代表對話的結構體了,我們將其命名為 Conversation
,它只有一個欄位,就是以 Message
為元素向量:
pub struct Conversation {
pub messages: Vec<Message>,
}
為了能更輕鬆地產生新的 Conversation
實例,而非每次都要建立一個空向量:我們可以實作一個 關聯函式 作為建構子:
impl Conversation {
pub fn new() -> Conversation {
Conversation {
messages: Vec::new(),
}
}
}
如此一來,之後要建立新對話時,只需要使用 Conversation::new()
即可。
因為這些結構體會在客戶端與伺服器端傳遞,所以必須有將其序列化與反序列化的方法。
而在 Rust 中可以透過 serde crate 來達成。
這時候可以使用 cargo add serde -F derive
或直接到 Cargo.toml 檔 dependencies 區塊加上
`serde = { version = "1.0.188", features = ["derive"] }`
這裡特別註明要使用了 derive
巨集的功能,所以只要在結構體上加上 #[derive(Serialize, Deserialize)]
就能讓該型別自動實作序列化與反序列化。
所以最後 conversation.rs
的程式碼整理如下:
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Conversation {
pub messages: Vec<Message>,
}
impl Conversation {
pub fn new() -> Conversation {
Conversation {
messages: Vec::new(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message {
// distinguish the message from the LLM and the user
pub user: bool,
// the message
pub text: String,
}
有了可以代表對話的資料結構之後,明天就可以開始實作一些程式邏輯了,明天見啦~