LLM 编程框架-LangChain

1、LangChain介绍

LangChain 就是一个 LLM 编程框架,你想开发一个基于 LLM 应用,需要什么组件它都有,直接使用就行;甚至针对常规的应用流程,它利用链(LangChain中Chain的由来)这个概念已经内置标准化方案了。下面我们从新兴的大语言模型(LLM)技术栈的角度来看看为何它的理念这么受欢迎。

借助 Langchain,可以创建聊天机器人、问答系统和其他人工智能代理。

其官方的定义

LangChain是一个基于语言模型开发应用程序的框架。它可以实现以下应用程序:

  • 数据感知:将语言模型连接到其他数据源
  • 自主性:允许语言模型与其环境进行交互

LangChain的主要价值在于:

  • 组件化:为使用语言模型提供抽象层,以及每个抽象层的一组实现。组件是模块化且易于使用的,无论您是否使用LangChain框架的其余部分。
  • 现成的链:结构化的组件集合,用于完成特定的高级任务

现成的链使得入门变得容易。对于更复杂的应用程序和微妙的用例,组件化使得定制现有链或构建新链变得更容易。

新兴 LLM 技术栈

大语言模型技术栈由四个主要部分组成:

  • 数据预处理流程(data preprocessing pipeline)
  • 嵌入端点(embeddings endpoint )+向量存储(vector store)
  • LLM 终端(LLM endpoints)
  • LLM 编程框架(LLM programming framework)

数据预处理流程

该步骤包括与数据源连接的连接器(例如S3存储桶或CRM)、数据转换层以及下游连接器(例如向矢量数据库)。通常,输入到LLM中的最有价值的信息也是最难处理的(如PDF、PPTX、HTML等),但同时,易于访问文本的文档(例如.DOCX)中也包含用户不希望发送到推理终端的信息(例如广告、法律条款等)。

因为涉及的数据源繁杂(数千个PDF、PPTX、聊天记录、抓取的HTML等),这步也存在大量的 dirty work,使用OCR模型、Python脚本和正则表达式等方式来自动提取、清理和转换关键文档元素(例如标题、正文、页眉/页脚、列表等),最终向外部以API的方式提供JSON数据,以便嵌入终端和存储在向量数据库中。

嵌入端点和向量存储

使用嵌入端点(用于生成和返回诸如词向量、文档向量等嵌入向量的 API 端点)和向量存储(用于存储和检索向量的数据库或数据存储系统)代表了数据存储和访问方式的重大演变。以前,嵌入主要用于诸如文档聚类之类的特定任务,在新的架构中,将文档及其嵌入存储在向量数据库中,可以通过LLM端点实现关键的交互模式。直接存储原始嵌入,意味着数据可以以其自然格式存储,从而实现更快的处理时间和更高效的数据检索。此外,这种方法可以更容易地处理大型数据集,因为它可以减少训练和推理过程中需要处理的数据量。

LLM终端

LLM终端是接收输入数据并生成LLM输出的终端。LLM终端负责管理模型的资源,包括内存和计算资源,并提供可扩展和容错的接口,用于向下游应用程序提供LLM输出。

LLM编程框架

LLM编程框架提供了一套工具和抽象,用于使用语言模型构建应用程序。在现代技术栈中出现了各种类型的组件,包括:LLM提供商、嵌入模型、向量存储、文档加载器、其他外部工具(谷歌搜索等),这些框架的一个重要功能是协调各种组件。

关键组件解释

Prompts

Prompts用来管理 LLM 输入的工具,在从 LLM 获得所需的输出之前需要对提示进行相当多的调整,最终的Promps可以是单个句子或多个句子的组合,它们可以包含变量和条件语句。

Chains

是一种将LLM和其他多个组件连接在一起的工具,以实现复杂的任务。

Agents

是一种使用LLM做出决策的工具,它们可以执行特定的任务并生成文本输出。Agents通常由三个部分组成:Action、Observation和Decision。Action是代理执行的操作,Observation是代理接收到的信息,Decision是代理基于Action和Observation做出的决策。

Memory

是一种用于存储数据的工具,由于LLM 没有任何长期记忆,它有助于在多次调用之间保持状态。

典型应用场景

  • 特定文档的问答:从Notion数据库中提取信息并回答用户的问题。
  • 聊天机器人:使用Chat-LangChain模块创建一个与用户交流的机器人。
  • 代理:使用GPT和WolframAlpha结合,创建一个能够执行数学计算和其他任务的代理。
  • 文本摘要:使用外部数据源来生成特定文档的摘要。

Langchain 竞品

(个人认为)在商业化上,基于大模型业务分为三个层次:

  • 基础设施层:通用的大模型底座
  • 垂直领域层:基于大模型底座+领域场景数据微调形成更强垂直能力
  • 应用层:基于前两者,瘦前端的方式提供多样化应用体验

类似 LangChain 这种工具框架可以做到整合各个层能力,具备加速应用开发和落地验证的优势,因此也出现了很多竞争者。

名称 语言 特点
LangChain Python/JS 优点:提供了标准的内存接口和内存实现,支持自定义大模型的封装。
缺点:评估生成模型的性能比较困难。
Dust.tt Rust/TS 优点:提供了简单易用的API,可以让开发者快速构建自己的LLM应用程序。
缺点:文档不够完善。
Semantic-kernel TypeScript 优点:轻量级SDK,可将AI大型语言模型(LLMs)与传统编程语言集成在一起。
缺点:文档不够完善。
Fixie.ai Python 优点:开放、免费、简单,多模态(images, audio, video…)
缺点:PaaS平台,需要在平台部署
Brancher AI Python/JS 优点:链接所有大模型,无代码快速生成应用, Langchain产品)
缺点:-

六大模块

LangChain 主体分为 6 个模块,分别是对(大语言)模型输入输出的管理、外部数据接入、链(Chains)的概念、(上下文记忆)存储管理、智能代理(Agent)以及回调系统,通过文档的组织结构,你可以清晰了解到 LangChain的侧重点,以及在大语言模型开发生态中对自己的定位。

1、LLM输入输出管理

Models

LangChain 中提供了多种不同的语言模型,按功能划分,主要有两种。

  • 语言模型(LLMs):我们通常说的语言模型,给定输入的一个文本,会返回一个相应的文本。常见的语言模型有 GPT3.5,chatglm,GPT4All 等。
1
2
from langchain.llms import OpenAI
llm = OpenAI(openai_api_key="...")
  • 聊天模型(Chat model):可以看做是封装好的拥有对话能力的 LLM,这些模型允许你使用对话的形式和其进行交互,能够支持将聊天信息作为输入,并返回聊天信息。这些聊天信息都是封装好的结构体,而非一个简单的文本字符串。常见的聊天模型有 GPT4、Llama 和 Llama2,以及微软云 Azure 相关的 GPT 模型。
1
2
from langchain.chat_models import ChatOpenAI
chat = ChatOpenAI(openai_api_key="...")

Prompts

提示词是模型的输入,通过编写提示词可以和模型进行交互。LangChain 中提供了许多模板和函数用于模块化构建提示词,这些模板可以提供更灵活的方法去生成提示词,具有更好的复用性。根据调用的模型方式不同,提示词模板主要分为普通模板以及聊天提示词模板。

提示模板(PromptTemplate

  • 提示模板是一种生成提示的方式,包含一个带有可替换内容的模板,从用户那获取一组参数并生成提示
  • 提示模板用来生成 LLMs 的提示,最简单的使用场景,比如“我希望你扮演一个代码专家的角色,告诉我这个方法的原理 {code}”。
  • 类似于 python 中用字典的方式格式化字符串,但在 langchain 中都被封装成了对象

一个简单的调用样例如下所示:

1
2
3
4
5
6
7
8
9
10
from langchain import PromptTemplate


template = """\
You are a naming consultant for new companies.
What is a good name for a company that makes {product}?
"""

prompt = PromptTemplate.from_template(template)
prompt.format(product="colorful socks")

输出结果:

1
2
3
# 实际输出
You are a naming consultant for new companies.
What is a good name for a company that makes colorful socks?

聊天提示模板(ChatPromptTemplate

  • 聊天模型接收聊天消息作为输入,这些聊天消息通常称为 Message,和原始的提示模板不一样的是,这些消息都会和一个角色进行关联。
  • 在使用聊天模型时,建议使用聊天提示词模板,这样可以充分发挥聊天模型的潜力。

一个简单的使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain.prompts import (
ChatPromptTemplate,
PromptTemplate,
SystemMessagePromptTemplate,
AIMessagePromptTemplate,
HumanMessagePromptTemplate,
)
from langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage
)
template="You are a helpful assistant that translates {input_language} to {output_language}."
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
human_template="{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])

# get a chat completion from the formatted messages
chat_prompt.format_prompt(input_language="English", output_language="French", text="I love programming.").to_messages()

输出结果:

1
2
[SystemMessage(content='You are a helpful assistant that translates English to French.', additional_kwargs={}),
HumanMessage(content='I love programming.', additional_kwargs={})]

Output Parsers

语言模型输出的是普通的字符串,有的时候我们可能想得到结构化的表示,比如 JSON 或者 CSV,一个有效的方法就是使用输出解析器。

输出解析器是帮助构建语言模型输出的类,主要实现了两个功能:

  1. 获取格式指令,是一个文本字符串需要指明语言模型的输出应该如何被格式化
  2. 解析,一种接受字符串并将其解析成固定结构的方法,可以自定义解析字符串的方式

一个简单的使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.llms import OpenAI
from langchain.chat_models import ChatOpenAI

from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field, validator
from typing import List

model_name = 'text-davinci-003'
temperature = 0.0
model = OpenAI(model_name=model_name, temperature=temperature)
# Define your desired data structure.
class Joke(BaseModel):
setup: str = Field(description="question to set up a joke")
punchline: str = Field(description="answer to resolve the joke")

# You can add custom validation logic easily with Pydantic.
@validator('setup')
def question_ends_with_question_mark(cls, field):
if field[-1] != '?':
raise ValueError("Badly formed question!")
return field
# Set up a parser + inject instructions into the prompt template.
parser = PydanticOutputParser(pydantic_object=Joke)
prompt = PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
partial_variables={"format_instructions": parser.get_format_instructions()}
)
# And a query intended to prompt a language model to populate the data structure.
joke_query = "Tell me a joke."
_input = prompt.format_prompt(query=joke_query)
output = model(_input.to_string())
parser.parse(output)

输出结果:

1
2
Joke(setup='Why did the chicken cross the road?', 
    punchline='To get to the other side!')

2、数据接入层

有的时候,我们希望语言模型可以从自己的数据中进行查询,而不是仅依靠自己本身输出一个结果。数据连接器的组件就允许你使用内置的方法去读取、修改,存储和查询自己的数据,主要有下面几个组件组成。

  • 文档加载器(Document loaders):连接不同的数据源,加载文档。
  • 文档转换器(Document transformers):定义了常见的一些对文档加工的操作,比如切分文档,丢弃无用的数据
  • 文本向量模型(Text embedding models):将非结构化的文本数据转换成一个固定维度的浮点数向量
  • 向量数据库(Vector stores):存储和检索你的向量数据
  • 检索器(Retrievers):用于检索你的数据

3、Embedding专题

在机器学习中,向量通常用于表示数据的特征。

而文本嵌入是一种将文本这种离散数据映射到连续向量空间的方法,嵌入技术可以将高维的离散数据降维到低维的连续空间中,并保留数据之间的语义关系,从而方便进行机器学习和深度学习的任务。

文本嵌入算法

文本嵌入算法是指将文本数据转化为向量表示的具体算法,通常包括以下几个步骤:

  • 分词:将文本划分成一个个单词或短语。
  • 构建词汇表:将分词后的单词或短语建立词汇表,并为每个单词或短语赋予一个唯一的编号。
  • 计算词嵌入:使用预训练的模型或自行训练的模型,将每个单词或短语映射到向量空间中。
  • 计算文本嵌入:将文本中每个单词或短语的向量表示取平均或加权平均,得到整个文本的向量表示。

常见的文本嵌入算法包括 Word2Vec、GloVe、FastText 等。这些算法通过预训练或自行训练的方式,将单词或短语映射到低维向量空间中,从而能够在计算机中方便地处理文本数据。

文本嵌入用途

文本嵌入用于测量文本字符串的相关性,通常用于:

  • 搜索(结果按与查询字符串的相关性排序)
  • 聚类(其中文本字符串按相似性分组)
  • 推荐(推荐具有相关文本字符串的项目)
  • 异常检测(识别出相关性很小的异常值)
  • 多样性测量(分析相似性分布)
  • 分类(其中文本字符串按其最相似的标签分类)

使用文本嵌入模型

  • 可以使用 HuggingFace上能够处理文本嵌入的开源模型,例如:uer/sbert-base-chinese-nli

  • 使用之前介绍的 OpenAI 文本嵌入API 可以将文本转换为向量,OpenAI API提供了多个文本嵌入模型,这篇博客对它们的性能进行了比较,这里是性能最好的text-embedding-ada-002说明:

模型名称 价格 分词器 最大输入 token 输出
text-embedding-ada-002 $0.000/1k tokens cl100k_base 8191 1536

支持文本嵌入的其他模型

矢量数据库

  • 为了快速搜索多个矢量,建议使用矢量数据库,下面是一些可选的矢量数据库:

  • Pinecone,一个完全托管的矢量数据库

  • Weaviate,一个开源的矢量搜索引擎

  • Redis作为矢量数据库

  • Qdrant,一个矢量搜索引擎(开源免费)

  • Milvus,一个为可扩展的相似性搜索而构建的矢量数据库

  • Chroma,一个开源嵌入式商店

  • Typesense,快速的开源矢量搜索引擎

  • Zilliz,数据基础设施,由Milvus提供技术支持

  • FAISS 是Meta开源的用于高效搜索大规模矢量数据集的库

5、Chains模块

链定义为对组件的一系列调用

在 LangChain 中有许多实现好的 chain,以最基础的 LLMChain 为例,它主要实现的就是接收一个提示词模板,然后对用户输入进行格式化,然后输入到一个 LLM,最终返回 LLM 的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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}?",
)

from langchain.chains import LLMChain
chain = LLMChain(llm=llm, prompt=prompt)

# Run the chain only specifying the input variable.
print(chain.run("colorful socks"))

LLMChain 不仅支持 llm,同样也支持 chat llm,下面是一个调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
)
human_message_prompt = HumanMessagePromptTemplate(
prompt=PromptTemplate(
template="What is a good name for a company that makes {product}?",
input_variables=["product"],
)
)
chat_prompt_template = ChatPromptTemplate.from_messages([human_message_prompt])
chat = ChatOpenAI(temperature=0.9)
chain = LLMChain(llm=chat, prompt=chat_prompt_template)
print(chain.run("colorful socks"))

4、存储管理Memory

支持LLM 进行多轮的对话

在 LangChain 中,提供这个功能的模块就称为 Memory,用于存储用户和模型交互的历史信息。在 LangChain 中根据功能和返回值的不同,会有多种不同的 Memory 类型,主要可以分为以下几个类别:

  1. 对话缓冲区内存(ConversationBufferMemory),最基础的内存模块,用于存储历史的信息
  2. 对话缓冲器窗口内存(ConversationBufferWindowMemory),只保存最后的 K 轮对话的信息,因此这种内存空间使用会相对较少
  3. 对话摘要内存(ConversationSummaryMemory),这种模式会对历史的所有信息进行抽取,生成摘要信息,然后将摘要信息作为历史信息进行保存。
  4. 对话摘要缓存内存(ConversationSummaryBufferMemory),这个和上面的作用基本一致,但是有最大 token 数的限制,达到这个最大 token 数的时候就会进行合并历史信息生成摘要

值得注意的是,对话摘要内存的设计出发点就是语言模型能支持的上下文长度是有限的(一般是 2048),超过了这个长度的数据天然的就被截断了这个类会根据对话的轮次进行合并,默认值是 2,也就是每 2 轮就开启一次调用 LLM 去合并历史信息。

1
2
3
4
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(memory_key="chat_history")
memory.chat_memory.add_user_message("hi!")
memory.chat_memory.add_ai_message("whats up?")

参考官方的教程,Memory 同时支持 LLM 和 Chat model。

llm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.memory import ConversationBufferMemory

# llm
llm = OpenAI(temperature=0)
# Notice that "chat_history" is present in the prompt template
template = """You are a nice chatbot having a conversation with a human.

Previous conversation:
{chat_history}

New human question: {question}
Response:"""
prompt = PromptTemplate.from_template(template)
# Notice that we need to align the `memory_key`
memory = ConversationBufferMemory(memory_key="chat_history")
conversation = LLMChain(
llm=llm,
prompt=prompt,
verbose=True,
memory=memory
)
conversation({"question": "hi"})

chat-llm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from langchain.chat_models import ChatOpenAI
from langchain.prompts import (
ChatPromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
from langchain.chains import LLMChain
from langchain.memory import ConversationBufferMemory

# chatllm
llm = ChatOpenAI()
prompt = ChatPromptTemplate(
messages=[
SystemMessagePromptTemplate.from_template(
"You are a nice chatbot having a conversation with a human."
),
# The `variable_name` here is what must align with memory
MessagesPlaceholder(variable_name="chat_history"),
HumanMessagePromptTemplate.from_template("{question}")
]
)
# Notice that we `return_messages=True` to fit into the MessagesPlaceholder
# Notice that `"chat_history"` aligns with the MessagesPlaceholder name.
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
conversation = LLMChain(
llm=llm,
prompt=prompt,
verbose=True,
memory=memory
)
conversation({"question": "hi"})

5、代理模块Agent

某些应用程序需要基于用户输入的对LLM和其他工具的灵活调用链。Agents为此类应用程序提供了灵活性。代理可以访问单一工具,并根据用户输入确定要使用的工具。代理可以使用多个工具,并使用一个工具的输出作为下一个工具的输入。

代理的核心思想就是使用 LLM 去选择对用户的输入,应该使用哪个特定的工具去进行操作。这里的工具可以是另外的一个 LLM,也可以是一个函数或者一个 chain。在代理模块中,有三个核心的概念。

1、Agent

依托于强力的语言模型和提示词,代理是用来决定下一步要做什么,其核心也是构建一个优秀的提示词。这个提示词大致有下面几个作用:
  • 角色定义,给代理设定一个符合自己的身份
  • 上下文信息,提供给他更多的信息来要求他可以执行什么任务
  • 丰富的提示策略,增加代理的推理能力

2、Tools

代理会选择不同的工具去执行不同的任务。工具主要给代理提供调用自己的方法,并且会描述自己如何被使用。工具的这两点都十分重要,如果你没有提供可以调用工具的方法,那么代理就永远完不成自己的任务;同时如果没有正确的描述工具,代理就不知道如何去使用工具。

3、Toolkits: LangChain 提供了工具包的使用,在一个工具包里通常包含 3-5 个工具。

Agent 技术是目前大语言模型研究的一个前沿和热点方向,但是目前受限于大模型的实际效果,仅 GPT 4.0 可以有效的开展 Agent 相关的研究。我们相信在未来,随着大模型性能的优化和迭代,Agent 技术应该能有更好的发展和前景。

6、Callback模块

回调模块允许接到LLM应用程序的各个阶段,鉴于LLM的幻觉问题,这对于日志记录、监视、流式处理和其他任务非常有用,现在也有专用的工具Helicone,Arize AI等产品可用,下面我们开始看代码:

参考资料:

小白入门大模型:LangChain