SQL on LLMs應用程式初始化
本節主要從案例代碼的角度切入,探索ChatGPT以及大模型,尤其是從生産環境的視角,來思考具體的最佳實踐。本節主要跟大家談的是,在LangChain這樣一個架構下,我們使用GPT-3.5或者GPT-4大模型,同時使用第三方工具,例如MySQL或者SQLite等等。如果做一些基于記憶體或者輕量級的開發,SQLite是一個非常友善的工具,從整體的角度講,你可以把它當做檔案,然後它内部的資料又是結構化的,可以采用SQL的方式進行操作。許多年前,當我們做安卓應用程式的時候,絕大多數應用程式都使用SQLite。是以,這些工具都非常實用。如果你要開發大型模型應用程式,SQLite仍然是一個很好的工具。當然,如果你要使用MySQL等工具也沒有問題。這些都是基礎操作,在這裡就不再詳細闡述了。
Gavin大咖微信:NLP_Matrix_Space
使用SQL是從工具層面。從模型層面,我們使用GPT-3.5或者GPT-4,另外一個很重要的點是鍊(Chain)層面, 我們回到LangChain的官方文檔,看到的是最新的Python版本,如果你喜歡TypeScript,也是非常棒的,作者去年到今年有很多項目,大多數項目都使用了TypeScript,因為它前端後端都是統一的,同時LangChain本身也支援TypeScript。現在我們主要讨論的是Python。實際上,如果你很有程式設計經驗,程式設計語言對你來說沒有太大差別,隻是面向對象封裝以及解耦合會有所不同,但邏輯都是一樣的。
讓我們再次看一下鍊(Chain),實際上,我們已經多次讨論了鍊的作用。你可以将鍊視為一個容器,類似于Docker或者其他雲計算概念中的容器,它提供了一個封閉的空間,裡面有你需要的一切。如果你需要外部的資源,你是通過容器進行操作。我們可以認為鍊是一個專為某些特定用例設計的容器,你可以自由組合,因為鍊具有嵌套性,也可以自定義開發相關内容。官方文檔中也非常明确地說明了這些内容。單獨地使用大語言模型,對于簡單的應用程式來說是很好的,但更複雜的應用程式需要連結大語言模型,LangChain為“鍊式”應用程式提供了連結口。我們将鍊定義為對元件的一系列調用,這些調用可以包括其他鍊。
對于一些比較複雜的應用程式,LLMChain的處理可能涉及多個語言模型的互相作用,這時就會非常有用。舉個例子,你可以讓GPT-4和GPT-3.5互相互動,進行同行評審(Peer Review),或者與其他模型互相作用。顯然,隻要你管理模型得當,它往往可以提高性能。在我們這裡主要是SQL,還有其他的元件,在這種場景下,鍊的封裝是核心,是以它被稱為鍊的應用程式,是一個比較高層次的接口,可以有不同的實作。
大家看這個官網提供的代碼示例:
- class Chain(BaseModel, ABC):
- """所有鍊應實作的基本接口."""
- memory: BaseMemory
- callbacks: Callbacks
- def __call__(
- self,
- inputs: Any,
- return_only_outputs: bool = False,
- callbacks: Callbacks = None,
- ) -> Dict[str, Any]:
- ...
Chain類繼承至BaseModel類,同時繼承至ABC類,Chain提供一些接口,你必須做具體的實作,例如:__call__方法,call方法的前面和後面都有雙下劃線, __call__是在什麼時候被調用的?Chain類執行個體化之後,執行個體化本身會調它的構造函數,然後你調用這個執行個體的的時候,__call__方法會被調用。
我們在開發大模型應用的時候,經常需要建構對象,并調用這些對象。這是Python非常基礎的内容,大家應該都了解的。之是以強調這一點,是想确認大家對這個概念的了解。舉個例子,以下代碼對SQLDatabaseChain進行了執行個體化:Gavin大咖微信:NLP_Matrix_Space
- db = SQLDatabase.from_uri("sqlite:///./data/ncv.db")
- llm = OpenAI(temperature=0)
- db_chain = SQLDatabaseChain(
- llm=llm,
- database=db,
- verbose=True, # Show its work
- return_direct=False, # Return the results without sending back to the LLM
- )
這裡有一個db_chain對象,它是SQLDatabaseChain類的一個執行個體。那麼,我們如何調用它呢?可以直接給db_chain對象傳遞參數:
- db_chain("How many voters are there?")
這個時候,我們調用的是定義的__call__方法。
- class Chain(BaseModel, ABC):
- ...
- def __call__(
- self,
- inputs: Any,
- return_only_outputs: bool = False,
- callbacks: Callbacks = None,
- ) -> Dict[str, Any]:
- ...
讀者可能會提一個問題:程式設計小白是否适合使用LangChain?我們的回答是,隻要你有程式設計的基礎,就可以學習LangChain。這裡所說的程式設計基礎包括對各種類型的循環的了解,以及對狀态的管理和操縱,此外,掌握基本的面向對象程式設計也是必要的。如果你已經掌握了這些知識,學習LangChain就不會有問題。
我們繼續讨論這個非常重要的__call__方法。從源代碼的角度,來讓大家熟悉它,讓我們再次看看它的重要性。我們看一下base.py。
- class Chain(Serializable, ABC):
- ...
- def __call__(
- self,
- inputs: Union[Dict[str, Any], Any],
- return_only_outputs: bool = False,
- callbacks: Callbacks = None,
- *,
- tags: Optional[List[str]] = None,
- metadata: Optional[Dict[str, Any]] = None,
- include_run_info: bool = False,
- ) -> Dict[str, Any]:
- """ 執行鍊
- ...
- """
- inputs = self.prep_inputs(inputs)
- callback_manager = CallbackManager.configure(
- callbacks,
- self.callbacks,
- self.verbose,
- tags,
- self.tags,
- metadata,
- self.metadata,
- )
- new_arg_supported = inspect.signature(self._call).parameters.get("run_manager")
- run_manager = callback_manager.on_chain_start(
- dumpd(self),
- inputs,
- )
- try:
- outputs = (
- self._call(inputs, run_manager=run_manager)
- if new_arg_supported
- else self._call(inputs)
- )
- except (KeyboardInterrupt, Exception) as e:
- run_manager.on_chain_error(e)
- raise e
- run_manager.on_chain_end(outputs)
- final_outputs: Dict[str, Any] = self.prep_outputs(
- inputs, outputs, return_only_outputs
- )
- if include_run_info:
- final_outputs[RUN_KEY] = RunInfo(run_id=run_manager.run_id)
- return final_outputs
- ...
- @abstractmethod
- def _call(
- self,
- inputs: Dict[str, Any],
- run_manager: Optional[CallbackManagerForChainRun] = None,
- ) -> Dict[str, Any]:
- """ 執行鍊.
以上代碼第3行,是__call__方法,注意,這裡有兩個下劃線。通常情況下,編譯器或其他工具會自動生成該方法。在Chain中,我們也有類似的方法,每當調動對象的時候,都會運作Chain,它的注釋也說得很明白,是執行鍊。當我們運作LangChain時,通常會使用語言模型。例如,可能會使用GPT-4,它會根據輸入提示詞、角色定義、上下文定義、具體的指令、以及使用者的目标來生成輸出。此外,可能還會有一些限制或限制。我們在前面的項目代碼中對這些内容進行了詳細的解釋
以上代碼第28行,執行callback_manager.on_chain_start方法,它負責管理生命周期方法。你也可以自己定義這個方法。由于它是面向對象的,當運作時,它會調用具體子類的實作。這是面向對象的基本概念。Gavin大咖微信:NLP_Matrix_Space
以上代碼第33行至36行,我們看一下output,它調用的是self._call方法。請注意,在__call__方法中,開頭和結尾都有兩個下劃線。在這裡,self._call方法在名稱前加一個下劃線,以實作一些具體的業務邏輯,這是一個内部實作。實際上,在Python程式設計中,這是一種基本的操作。因為它是抽象方法,這邊沒有具體實作,具體實作要到Chain類的子類去檢視。Chain類也提供了async異步實作的方式。
我們再次回到LangChain的官網文檔,雖然文檔中講解了很多内容,但其核心實際上與我們之前談到的内容相同,例如,文檔中講解的PromptTemplate,我們已經多次提到過。
- from langchain.llms import OpenAI
- from langchain.prompts import PromptTemplate
- llm = OpenAI(temperature=0.9)
- prompt = PromptTemplate(
- input_variables=["product"],
- template="What is a good name for a company that makes {product}?",
- )
接下來是LLMChain,傳入一個Prompt參數,它是一個PromptTemplate的執行個體。當LLMChain中運作時,你可以直接調用它的run方法來執行操作。
- from langchain.chains import LLMChain
- chain = LLMChain(llm=llm, prompt=prompt)
- # 隻運作指定輸入變量的鍊
- print(chain.run("colorful socks"))
在LangChain SQL示例中,我們直接建構了SQLDatabaseChain。
Gavin大咖微信:NLP_Matrix_Space
- db = SQLDatabase.from_uri("sqlite:///./data/ncv.db")
- llm = OpenAI(temperature=0)
- db_chain = SQLDatabaseChain(
- llm=llm,
- database=db,
- verbose=True, # 展示
- return_direct=False, # 傳回結果而不發送回LLM
- )
然後,我們将目标對象傳給db_chain,即“How many voters are there?”(“有多少選民?”),這是使用者目标。
- db_chain("How many voters are there?")
這個時候,日志顯示“Entering new SQLDatabaseChain chain”(進入到SQLDatabaseChain鍊”):
> Entering new SQLDatabaseChain chain...
How many voters are there?
SQLQuery:SELECT COUNT(*) FROM ncvoter29;
SQLResult: [(126876,)]
Answer:There are 126876 voters.
> Finished chain.
在這個例子中,我們輸入一些指令,然後産生一個SQLQuery。SQLQuery是由誰産生的呢?當然是由我們的語言模型産生的。當Chain接收到使用者的輸入資訊時,它會調用我們的GPT-4或GPT-3.5來分析這些資訊,生成SQL語句。然後,Chain獲得了SQL語句後,會執行它。
如圖16-1所示,我們來看一下具體的源碼實作。LangChain的chains目錄裡面,有很多鍊的實作,例如:操作bash的鍊,或者進行數學計算的鍊等。
圖16- 1 LangChain的chains目錄
如圖16-2所示,chains目錄裡面有一個sql_database目錄,是資料庫的相關内容。
圖16- 2 chains裡面的sql_database目錄