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目录