18.3 base.py源码逐行剖析
现在我们要聚焦于源代码中的大语言模型部分。如图18-3所示,LangChain提供了许多语言模型的选择。 Gavin大咖微信:NLP_Matrix_Space
图18- 3 LangChain的llms目录
如图18-4所示,整个LangChain的模块化设计非常出色,你可以看到大模型(llms)、索引(indexes)、嵌入(embeddings)、评估(evaluation)等功能,这些都是非常好的模块化设计。
图18- 4 LangChain的模块化设计
Gavin大咖微信:NLP_Matrix_Space
另一方面,LangChain的结构设计也非常出色。直觉上,你会想要查看它的源代码,因为它有很多实现。当然,你的核心或出发点应该是查看base.py,只要你有基本的编程经验,就不会在这个地方遇到问题。因为base.py定义了它的协议,并提供了一些通用的操作或基本的工具类,最重要的是它提供了接口。其他模块的源代码要遵循这个接口,因为我们将模型放入LangChain或Agent的整个框架中,当框架调用你的代码时,是根据接口来调用的。
如果是一个语言模型,语言模型涉及到生命周期。当你调用语言模型时,需要进行初始化,LangChain的官网文档中清晰地介绍了这一点,大家可以更好地理解。LangChain提供了一个回调系统,允许挂接语言模型应用程序的各个阶段。这对于日志记录、监视、流式传输和其他任务非常有用。 Gavin大咖微信:NLP_Matrix_Space
- class BaseCallbackHandler:
- """可用于处理langchain回调的基本回调处理程序."""
- def on_llm_start(
- self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
- ) -> Any:
- """LLM开始运行时运行."""
- def on_chat_model_start(
- self, serialized: Dict[str, Any], messages: List[List[BaseMessage]], **kwargs: Any
- ) -> Any:
- """当聊天模型开始运行时运行"""
- def on_llm_new_token(self, token: str, **kwargs: Any) -> Any:
- """使用新的LLM标记运行。仅在启用流时可用."""
- def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
- """LLM结束运行时运行."""
- ...
以上代码定义了一个BaseCallbackHandler的基础回调处理器类。该类用于处理来自LangChain的回调。BaseCallbackHandler类包含了一些生命周期方法,每个方法都代表了一个不同的回调事件。
- on_llm_start方法:当语言模型开始运行时调用。
- on_chat_model_start方法:当聊天模型开始运行时调用。
- on_llm_new_token方法:当语言模型有新的标记可用时调用。这个方法只在启用了流式传输时才可用。它接收一个标记字符串作为参数。
- on_llm_end方法:当语言模型结束运行时调用。
回到语言模型的base.py代码,llms的base.py的代码实现:
- """大型语言模型的基本接口."""
- from __future__ import annotations
- import asyncio
- import inspect
- import json
- import logging
- import warnings
- from abc import ABC, abstractmethod
- from pathlib import Path
- from typing import (
- Any,
- Callable,
- Dict,
- List,
- Mapping,
- Optional,
- Sequence,
- Tuple,
- Type,
- Union,
- )
- import yaml
- from pydantic import Field, root_validator, validator
- from tenacity import (
- before_sleep_log,
- retry,
- retry_base,
- retry_if_exception_type,
- stop_after_attempt,
- wait_exponential,
- )
- import langchain
- from langchain.callbacks.base import BaseCallbackManager
- from langchain.callbacks.manager import (
- AsyncCallbackManager,
- AsyncCallbackManagerForLLMRun,
- CallbackManager,
- CallbackManagerForLLMRun,
- Callbacks,
- )
- from langchain.load.dump import dumpd
- from langchain.schema import (
- Generation,
- LLMResult,
- PromptValue,
- RunInfo,
- )
- from langchain.schema.language_model import BaseLanguageModel
- from langchain.schema.messages import AIMessage, BaseMessage, get_buffer_string
- ...
- def get_prompts(
- params: Dict[str, Any], prompts: List[str]
- ) -> Tuple[Dict[int, List], str, List[int], List[str]]:
- """获取已缓存的提示."""
- llm_string = str(sorted([(k, v) for k, v in params.items()]))
- missing_prompts = []
- missing_prompt_idxs = []
- existing_prompts = {}
- for i, prompt in enumerate(prompts):
- if langchain.llm_cache is not None:
- cache_val = langchain.llm_cache.lookup(prompt, llm_string)
- if isinstance(cache_val, list):
- existing_prompts[i] = cache_val
- else:
- missing_prompts.append(prompt)
- missing_prompt_idxs.append(i)
- return existing_prompts, llm_string, missing_prompt_idxs, missing_prompts
- ...
- def update_cache(
- existing_prompts: Dict[int, List],
- llm_string: str,
- missing_prompt_idxs: List[int],
- new_results: LLMResult,
- prompts: List[str],
- ) -> Optional[dict]:
- """更新缓存并获取LLM输出"""
- for i, result in enumerate(new_results.generations):
- existing_prompts[missing_prompt_idxs[i]] = result
- prompt = prompts[missing_prompt_idxs[i]]
- if langchain.llm_cache is not None:
- langchain.llm_cache.update(prompt, llm_string, result)
- llm_output = new_results.llm_output
- return llm_output
以上代码的第37行至第42行,要特别注意的第一个方面是管理器,它提供了一些方法来管理生命周期,你会看到与回调相关的内容,尤其是CallbackManager,这里还有异步的方式,因为有时候你可能想在后台执行一些任务,或者提高性能,异步操作非常重要。在作者看来,所有的服务器类编写都应该使用异步的方式,除非你处理的是主线程,不执行这一步,其他的任务都无法执行,否则的话,都应该是异步的方式,在使用Python编写代码时,Python的基本设计是单线程,为了实现多任务,Python引入了Coroutine(协程)的概念,同时还有一些线程和进程的方式,这些是Python编程的基础内容。
以上代码的第54行至第70行,第二个要注意的方面是get_prompts方法,这非常重要,因为与语言模型进行交互时,你是通过提示词(prompt)与它进行对话,从语言模型的角度来看,你所有的输入实际上都是提示词。例如:我想让系统教我关于托福阅读的内容,可以输入信息:"Can you teach me about TOEFL reading?"(“你能教我托福阅读吗?”),这部分内容是提示词的一部分,但是它的核心是背后的提示词技术,提示词可以设计一些内部的思考过程,基于用户的输入和系统的设计,这涉及到很多复杂的内容,例如,自我评估(self-criticism)可以使用两个模型,进行同行评审(Peer Review),这意味着有两个模型,比如GPT-4、Llama或者其他一些模型,它们之间相互促进,这不是非此即彼的关系,而是提供了更多的视角,让对方给出评估,基于这种评估进行改进,这是一个不断循环的过程。这就是一个同行评审的流程。当然,作者在“TOEFL on ChatGPT”项目背后,还有很多其他的内容,这也是作者最近一两个月与许多机构和大学合作的一个核心要点。
要强调的一个关键点是,如果你想深入了解系统内部,在模型进行每一步输入之前的提示词,并进行调优,那么get_prompts方法是一个非常重要的方法。get_prompts函数接收两个参数:params和prompts。params是一个字典,存储了一些参数信息,prompts是一个字符串列表,包含了多个提示。首先将params字典中的键值对按照键进行排序,并将其转换为字符串llm_string。然后,遍历prompts列表中的每个提示。如果langchain.llm_cache不为None,则函数尝试通过调用langchain.llm_cache.lookup(prompt, llm_string)来查找缓存中的值。如果返回的cache_val是一个列表,说明该提示已经存在于缓存中,将该提示及其索引添加到existing_prompts字典中。否则,如果返回的cache_val不是列表,说明该提示缺失于缓存中,函数将该提示添加到missing_prompts列表中,并记录其索引到missing_prompt_idxs列表中。然后,函数返回四个值:existing_prompts(已存在于缓存的提示及其索引的字典)、llm_string(经过排序后的params的字符串表示)、missing_prompt_idxs(缺失的提示的索引列表)和missing_prompts(缺失的提示列表),这对于管理提示词信息是很重要的。 Gavin大咖微信:NLP_Matrix_Space
update_cache函数的作用是在使用语言模型进行生成时,更新缓存以提高效率并获取语言模型的输出结果。它通过缓存已生成的提示和结果,以便在以后的调用中可以直接从缓存中获取结果,而无需重新生成。函数接收以下参数:existing_prompts (现有提示的字典)、 llm_string (LLM字符串)、 missing_prompt_idxs (缺失提示的索引列表)、 new_results (LLM结果对象)和 prompts (提示列表),通过遍历 new_results.generations 中的结果,并将其存储到 existing_prompts 字典中,使用缺失提示的索引作为键。然后,根据相应的索引从 prompts 列表中获取提示,并在LLM缓存对象 langchain.llm_cache 不为None时更新缓存,将提示、LLM字符串和结果作为参数传递给缓存的 update 方法。然后,函数返回 new_results.llm_output 作为语言模型的输出。这些都是工具方法。
以上代码第81行,我们将使用enumerate(new_results.generations)进行遍历,大家对于enumerate应该都很熟悉,它会生成一个类似于生成器的对象,可以通过不断获取其中的内容来进行遍历。
以上代码第84行,其中langchain.llm_cache是基于一个BaseCache进行了封装。
langchain的__init__.py的代码实现:
- verbose: bool = False
- debug: bool = False
- llm_cache: Optional[BaseCache] = None
langchain的cache.py的BaseCache代码实现:
- class BaseCache(ABC):
- """缓存的基本接口."""
- @abstractmethod
- def lookup(self, prompt: str, llm_string: str) -> Optional[RETURN_VAL_TYPE]:
- """根据提示和llm_string查找"""
- @abstractmethod
- def update(self, prompt: str, llm_string: str, return_val: RETURN_VAL_TYPE) -> None:
- """基于提示和llm_string更新缓存."""
- @abstractmethod
- def clear(self, **kwargs: Any) -> None:
- """清除可以接受其他关键字参数的缓存"""
BaseCache 类定义了缓存的基本接口,是一个抽象基类(ABC),包含了三个抽象方法: lookup、update和clear,包括查找、更新和清空等操作,为缓存的实现提供了统一的规范。
如图18-5所示,LangChain提供了子类的一些实现,大家可以看一下相关的代码。
图18- 5 LangChain的缓存
例如,InMemoryCache子类可以实现自己的方法。
langchain的cache.py的InMemoryCache代码实现:
- class InMemoryCache(BaseCache):
- """将事物存储在内存中的缓存"""
- def __init__(self) -> None:
- """使用空缓存初始化."""
- self._cache: Dict[Tuple[str, str], RETURN_VAL_TYPE] = {}
- def lookup(self, prompt: str, llm_string: str) -> Optional[RETURN_VAL_TYPE]:
- """根据提示和llm_string查找."""
- return self._cache.get((prompt, llm_string), None)
- def update(self, prompt: str, llm_string: str, return_val: RETURN_VAL_TYPE) -> None:
- ""基于提示和llm_string更新缓存."""
- self._cache[(prompt, llm_string)] = return_val
- def clear(self, **kwargs: Any) -> None:
- """清除缓存."""
- self._cache = {}
在InMemoryCache的场景中,最简单的方式就是使用一个字典(dictionary),可以使用键值对进行操作,这非常重要的原因是你需要进行更新(update)和清空(clear)等相关操作,特别是查找(lookup),可以快速地检索。所以,你可以将它看作是一个字典,使用get方法来获取值,然后使用update方法进行更新。在更新时,你需要输入一个键,其中包括提示(prompt)和我们大型语言模型字符串的内容。至于clear方法,它非常简单粗暴地将缓存清空了。
我们回到base.py代码,BaseLLM类继承至BaseLanguageModel类,同时也继承了ABC抽象基类。在开发阶段,可以把verbose设置为True。设置了回调管理器callbacks和callback_manager。