Created: 2023-09-03 17:49
使用的模型 我們直接下載下來的檔案,只是模型的權重,我們需要使用套件來讀取並使用這些權重 我們將使用llm這個套件來進行模型的處理
我們還需要leptos,這是一個包含前後端的框架,基於WASM技術之上的,讓我們可以進行Full stack開發
而且leptos的效能很好,贏過很多目前主流的前端架構,包含Vue、React,不過輸給Solid,Solid可以說是目前效能最好的前端框架
接著安裝trunk跟cargo-leptos,trunk負責將我們的內容打包送到前端去,也提供local hosting的功能,這是web development必備的,當我們更改前端的內容時,他會同步更新顯示的畫面(hot-reloading),而cargo-leptos則做到相同的事,在我們更改後端服務時,他會hot-reloading將更新內容更新到服務上
安裝後我們使用cargo-leptos來新增專案 這邊留意cargo-leptos跟leptos不太一樣,cargo-leptos是leptos的build tool(用於自動化建置(build)過程),就像cargo那樣。 並指定一個git專案,指定專案的用意是讓我們可以直接套用模板,並輸入專案名稱rusty_llama,按下enter就可以了 作者的版本是trunk = 0.17.1跟cargo-leptos = 0.1.11,我則是trunk = 0.17.5跟cargo-leptos = 0.1.11
cargo leptos watch
來對該專案的變更進行監控
這邊一個卡關,因為leptos升級至4.10,讓我的cargo-leptos建立不起來了,目前打算去修改cargo.toml看能不能處理 ![[Screenshot 2023-09-03 at 6.33.06 PM.png]]後來發現是site.start().line()多了()導致無法啟動,去除之後就可以使用了 後來leptos回覆,説只要rust更新就可以正常使用了
rust update nightly
然後到瀏覽器輸入localhost:3000,就可以看到一個簡易的Web畫面,那個就是我們套用的模板
接著我們打開app.rs,然後修改HomePage function的內容,將Welcome to Leptos!
改成任意你想要的文字,儲存出去之後,可以看到畫面有即時的變更,這就是hot-reloading的威力!!
接著來看一下專案目錄,這邊包含了我們前後端所需要的東西,app.rs是前端的entry point,而main.rs是後端的entry point,後端的部分我們也可以使用actix、axum來抽換
點開main.rs這邊可以看到兩個cfg屬性,一個是ssr,一個是csr,ssr表示只有在後端build的時候才會一起build(Server-side rendering: Generate an HTML string (typically on the server)),而csr(Client-side rendering: Generate DOM nodes in the browser)則相反,表示只有在前端build的時候才會一起build,這可以幫助我們控管依賴,這相當重要,因為很多crate是沒辦法用WASM的方式build起來(像是actix,你總不會把網頁後端也打包送過去吧),透過ssr, csr,我們可以prune依賴圖,根據我們今天要build前端還是後端,舉例來說,我們今天要build前端,我們不需要actix,透過這個屬性的設置,就可以避免使用所有依賴
我們打開cargo.toml,可以看到dependencies的部分,actix-web的部分是optional的,再看到features部分的csr,可以看到actix沒有列在其中,而是列在ssr的部分,這將前後端所需的crate區分,只有在build前端的時候,前端的crate才會一併build,反之亦然
我們畫面的部分主要區分三大component,分別是訊息區塊、prompt輸入區塊跟畫面區塊,畫面區塊是所有的基底,訊息跟prompt都是在畫面之上去呈現的
在這個專案中,我們不需要HomePage component,也不需要router,因此我們可以直接刪除,並先加上ChatArea跟TypeArea,變成如圖下這樣 接著我們要建立一個module叫做model,並在裡面建立conversation.rs,用來擺放我們定義的資料物件,以及mod.rs,來將conversation include到裡面
再回到conversation.rs,現在要來實作conversation struct,但在此之前,我們先來實作message struct
我們先定義Message,有兩個屬性user跟text,user是bool,用來表示該訊息是user發出的,還是llm模型回應的,然後是text,這是訊息的文字。 接著我們實作Conversation,有一個屬性messages,是一個Vector裡面存放的是Message物件,也就是聊天記錄。 由於這兩個物件都需要能夠序列化、反序列化成JSON,所以我們需要使用serde這個套件,以及其derive feature,安裝指令是
cargo add serde -F derive
安裝後我們就可以對兩個物件進行derive,同時我們也需要能夠複製跟Debug print,所以也加上Clone跟Debug屬性。
最後,為了方便Conversation的建立,我們幫conversation實作fn new
來快速創建這個物件。
我們需要創建signal物件,signal物件會回傳一個tuple物件包含兩個elements,一個是ReadSignal,一個用來WriteSignal,如果我需要讀取資料,就對ReadSignal做事,如果需要更新資料,就對WriteSignal做事。這邊我們將Conversation物件傳入,用意是每當要更新資訊時,我們是更新我們傳入的Conversation物件
建立action,action是指使用者在做特定行為之後,連帶觸發的一系列邏輯。 在這裡,該特定行為是使用者輸入文字後點選submit button,我們將會把訊息建立成一個Message物件,並透過WriteSignal更新到conversation裡面,然後,我們將conversation設在ChatArea之後,代表當conversation更新,畫面顯示也要跟著更新。
在src底下建立api.rs,用來放置所有server端的function,但在開始撰寫邏輯之前,先回到lib.rs,將api module引入
開始撰寫我們的第一隻function,這是一個async function,他會接受上下文物件、Conversation當作參數,然後回傳Result,成功的話是String,錯誤的話是ServerFnError,function裡面會use一些套件。
leptos很棒的一點是,你定義的方法,在前端那邊可以直接使用,leptos會幫你呼叫HTTP來調用,我們不需要自己去實作那部分的邏輯,可以專注在function的使用設計上就好
可以看到我們的function上面有#[server(Converse "api")]
,這段是宣告說下面這個方法是server端的方法,當ssr被開啟時,這段才會被啟用,若是從client端呼叫此方法,即csr或hydrate有開啟時,並不是呼叫此方法,取而代之是發出一個request到server上,Converse是這個方法的名稱,"api"則是這個方法的URL prefix,我們這邊沒有特別指定這個方法的URL是什麼是因為前端那邊知道這個方法的路徑並可以直接呼叫,我們就不需要額外定義,且leptos會幫我們生成一個unique path給這個方法
會到Cargo.toml,將 llm加入依賴,並在ssr那邊標示llm是後端才需要的依賴
如果你熟知actix-web,你可能知道Extractor,簡單來說Extractor可以幫你將HTTP的部分拆解,讓你可以獲得HTTP資料的各個Part,而我們也可以透過Extractor讓server端的邏輯share共通的資料,這點很重要,因為我們是建立在actix這個框架,所以我們沒辦法explicitly的表明要傳遞什麼參數,而是必須透過actix的設計來進行傳遞,而在actix,這資料稱為"App Data" Data裡面物件為Llama,Llama是一個llm物件包含我們讀取的model,呼叫into_inner()讓我們extract該data wrapper,into_inner後的分號是誤加的
提供對話情境,讓llm模型可以inference,每句前面都會加上三個#字號,然後是Assistant表示模型,Human代表我們,在最後要給模型inference的部分,我們記上Assistant,表示後面是模型要進行預測的部分
這邊有使用到rand,要記得到Cargo.toml加上依賴,ssr的部分也要記得添加。 在正式環境,你可能會儲存session以達到更好的效率,但這邊我們每次都重新產生一個session。 這邊在做的事,是讓模型進行inference,我們給予模型、隨機數、InferenceRequest、預設output request、callback作為參數
- 模型是要用來進行推論的模型
- 隨機數是讓結果有隨機性,每次都不一樣
- InferenceRequest會接受
- prompt來告訴模型他要推論的內容
- 初始的參數
- 是否要對之前接受的token呼叫callback,我們的情況,每次session都會刷新,所以設為false
- token的最大產出數量
- OutputRequest不確定是什麼
- callback方法是用來告訴模型什麼時候要停止,不然模型會不斷的往下預測,這邊的停止條件是當模型輸出Human的時候 注意:parameter的Some是錯的,需要去掉,以及最後要回傳的是Ok(res)而不是空String
這邊沒有多做說明,只有說這是用來讓模型用來判斷是否停止的邏輯 這邊留意,我們提供的public api是沒有進行驗證、權限檢查的等等的,如果有要production的話,記得要加上去
回到main.rs加上加載model的部分,這邊使用dotenv這個套件來讀取環境變數,然後得知我們模型下載的位置,當然如果求方便,直接使用寫死的路徑也可以,如果有用dotenv的話,記得到Cargo.toml去添加依賴。
然後呼叫llm提供的關聯函數來讀取模型,如果有錯的話直接panic關閉服務。
這邊我們不能用過去的作法,透過設定feature來表示ssr,而必須使用cfg_if,是因為我們需要函數回傳是Llma,我們必須在fn上面添加use,先將Llma引入,所以才必須寫得像這樣,當然可能會覺得為什麼不把所有use都寫到最上面,當然可以,但這會使作用域變得太臃腫,而且在laptos中有區分前後端,可以分別啟動,如果單一邊啟動就必須加載所有的依賴,那也太浪費資源了。
這邊加上model,是一個上下文物件,用來共享使用的,我們用actix提供的Data struct將model包裝在裡面,這個Data底層是atomic的reference,可以讓我們在多執行緒的情況下也做到安全的共享
添加css函數,並添加到service上,讓外界可以讀取到我們的Tailwind css
回到Cargo.toml,我們添加一個section,這個section講者也不知道在幹嘛,但添加可以加快本機的運算速度
添加converse函數,將所需的上下文、內容作為參數,converse是我們在server實作的函數,不過我們在前端這邊也可以直接使用並呼叫,leptos會自動識別,相當方便
添加create_effect監聽,當使用者輸入文字時,會在畫面上出現“...”來表示正在回應中。
接著來實作ChatArea畫面的部分,這邊建立component這個module,並生成三個檔案mod.rs、chat_area.rs、type_area.rs。 來到chat_area,首先使用到component這個macro,這個macro可以將我們寫的函數轉換成前端物件。
我們提供參數,cx:上下文、conversation:ReadSignal,並回傳IntoView,畫面的細節先不管,回傳的內容是從conversation裏面遍歷出來,並根據訊息是user還是model回覆的進行tailwind css的風格調整。這邊都是使用閉包,我猜想應該是因為畫面的渲染是動態的,所以要給view!一個閉包函數,讓他來做調用
接著使用create node ref,這個函數將建立一個DOM node 的 reference,讓你能夠直接修改那個node(前端我沒很懂),這個ref我們要用來更新畫面,當聊天視窗有新訊息加入時,我們要確保畫面會一直保持在最上方(在討論畫面時,上方指的是下面,而下方指的是上面,這跟座標有關,(0,0)是在左下方)。
最後是TypeArea的部分,我們創建Input的ref,並添加閉包函數,當觸發submit時,我們會將訊息傳遞到後端,並刷新輸入框,這邊可以看到prevent_default,這是用來將submit這個buttom的預設事件關閉,也就是將跳轉取消,這樣訊息依然可以傳遞,但畫面不會跳轉。
然後加上node_ref,這用來標記我們要ref哪個物件,ChatArea那邊也要記得添加。
cargo.toml這邊,將style-file改成"style/ouput.css",等等我們會用tailwindcss將input.css編譯成output.css
最後
cargo leptos watch
此專案為參考大老影片並由我記錄下來的筆記,如有問題絕對是我紀錄錯誤