本文为笔者学习LangChain时对官方文档以及一系列资料进行一些总结~覆盖对Langchain的核心六大模块的理解与核心使用方法,全文篇幅较长,共计50000+字,可先码住辅助用于学习Langchain。
一、Langchain是什么?
如今各类AI模型层出不穷,百花齐放,大佬们开发的速度永远遥遥领先于学习者的学习速度。。为了解放生产力,不让应用层开发人员受限于各语言模型的生产部署中..LangChain横空出世界。
Langchain可以说是现阶段十分值得学习的一个AI架构,那么究竟它有什么魔法才会配享如此高的地位呢?会不会学习成本很高?不要担心!Langchain虽然功能强大,但其实它就是一个为了提升构建LLM相关应用效率的一个工具,我们也可以将它理解成一个“说明书",是的,只是一个“说明书”!它标准的定义了我们在构建一个LLM应用开发时可能会用到的东西。比如说在之前写过的AI文章中介绍的prompt,就可以通过Langchain中的PromptTemplate进行格式化:
prompt = """Translate the text \
that is delimited by triple backticks \
into a style that is {style}. \
text: ```{text}```
"""
当我们调用ChatPromptTemplate进行标准化时
from langchain.prompts import ChatPromptTemplate
prompt_template=ChatPromptTemplate.from_template(prompt)
print(prompt_template,'ChatPromptTemplate')
该prompt就会被格式化成:
从上述例子,可以直观的看到ChatPromptTemplate可以将prompt中声明的输入变量style和text准确提取出来,使prompt更清晰。当然,Langchain对于prompt的优化不止这一种方式,它还提供了各类其他接口将prompt进一步优化,这里只是举例一个较为基础且直观的方法,让大家感受一下。
Langchain其实就是在定义多个通用类的规范,去优化开发AI应用过程中可能用到的各类技术,将它们抽象成多个小元素,当我们构建应用时,直接将这些元素堆积起来,而无需在重复的去研究各"元素"实现的细枝末节。
二、官方文档Langchain这么长,我怎么看?
毋庸置疑,想要学习Langchain最简单直接的方法就是阅读官方文档,先贴一个链接Langchain官方文档
通过文档目录我们可以看到,Langchain由6个module组成,分别是Model IO、Retrieval、Chains、Memory、Agents和Callbacks。
Model IO:AI应用的核心部分,其中包括输入、Model和输出。
Retrieval:“检索“——该功能与向量数据密切库相关,是在向量数据库中搜索与问题相关的文档内容。
Memory:为对话形式的模型存储历史对话记录,在长对话过程中随时将这些历史对话记录重新加载,以保证对话的准确度。
Chains:虽然通过Model IO、Retrieval和Memory这三大模块可以初步完成应用搭建,但是若想实现一个强大且复杂的应用,还是需要将各模块组合起来,这时就可以利用Chains将其连接起来,从而丰富功能。
Agents:它可以通过用户的输入,理解用户的意图,返回一个特定的动作类型和参数,从而自主调用相关的工具来满足用户的需求,将应用更加智能化。
Callbacks: 回调机制可以调用链路追踪,记录日志,帮助开发者更好的调试LLM模型。
六个module具体的关系如下图所示(图片来源于网络):
好了,说到这我们只要一个一个module去攻破,最后将他们融会贯通,也就成为一名及格的Langchain学习者了。
三、Model IO
这一部分可以说是Langchain的核心部分,引用一下之前介绍AI时用过的图,介绍了Model IO内部的一些具体实现原理)
由上图可以看出:我们在利用Model IO的时候主要关注的就是输入、处理、输出这三个步骤。Langchain也是根据这一点去实现Model IO这一模块的,在这一模块中,Langchain针对此模块主要的实现手段为:Prompt(输入)、Language model(处理)、Output Pasers(输出),Langchain通过一系列的技术手法优化这三步,使得其更加的标准化,我们也无需再关注每一步骤中的具体实现,可以直接通过Langchain提供的API,堆积木式的完善我们应用构建(贴张官方文档的图,可以更清晰的了解)。
既然我们无需再关注每一步骤的具体实现,所以使用Langchain的Model IO应用时,主要关注的就是prompt的构建了。下文将主要介绍Langchain中常用的一些prompt构建方法。
3.1prompt
Langchain对于prompt的优化:主要是致力于将其优化成为可移植性高的Prompt,以便更好的支持各类LLM,无需在切换Model时修改Prompt。 通过官方文档可以看到,Prompt在Langchain被分成了两大类,一类是Prompt template,另一类则是Selectors。
Propmpt template:这个其实很好理解就是利用Langchain接口将prompt按照template进行一定格式化,针对Prompt进行变量处理以及提示词的组合。
Selectors: 则是指可以根据不同的条件去选择不同的提示词,或者在不同的情况下通过Selector,选择不同的example去进一步提高Prompt支持能力。
3.1.1模版格式:
在prompt中有两种类型的模版格式,一是f-string,这是十分常见的一类prompt,二是jinja2。
f-string 是 Python 3.6 以后版本中引入的一种特性,用于在字符串中插入表达式的值。语法简洁,直接利用{}花括号包裹变量或者表达式,即可执行简单的运算,性能较好,但是只限用在py中。
#使用 Python f 字符串模板:
from langchain.prompts import PromptTemplate
fstring_template = """Tell me a {adjective} joke about {content}"""
prompt = PromptTemplate.from_template(fstring_template)
print(prompt.format(adjective="funny", content="chickens"))
# Output: Tell me a funny joke about chickens.
jinja2常被应用于网页开发,与 Flask 和 Django 等框架结合使用。它不仅支持变量替换,还支持其他的控制结构(例如循环和条件语句)以及自定义过滤器和宏等高级功能。此外,它的可用性范围更广,可在多种语境下使用。但与 f-string 不同,使用 jinja2 需要安装相应的库。
#使用 jinja2 模板:
from langchain.prompts import PromptTemplate
jinja2_template = "Tell me a {{ adjective }} joke about {{ content }}"
prompt = PromptTemplate.from_template(jinja2_template, template_format="jinja2")
print(prompt.format(adjective="funny", content="chickens"))
# Output: Tell me a funny joke about chickens.
总结一下:如果只需要基本的字符串插值和格式化,首选f-string ,因为它的语法简洁且无需额外依赖。但如果需要更复杂的模板功能(例如循环、条件、自定义过滤器等),jinja2 更合适。
3.1.1.2Propmpt Template:
在prompt template这一部分中需要掌握的几个概念:
1️⃣基本提示模版:
大多是字符串或者是由对话组成的数组对象。 对于创建字符串类型的prompt要了解两个概念,一是input_variables 属性,它表示的是prompt所需要输入的变量。二是format,即通过input_variables将prompt格式化。比如利用PromptTemplate进行格式化。
from langchain.prompts import PromptTemplate #用于 PromptTemplate 为字符串提示创建模板。
#默认情况下, PromptTemplate 使用 Python 的 str.format 语法进行模板化;但是可以使用其他模板语法(例如, jinja2 )
prompt_template = PromptTemplate.from_template("Tell me a {adjective} joke about {content}.")
print(prompt_template.format(adjective="funny", content="chickens"))
Output如下(该例子就是将两个input_variables分别设置为funny和chickens,然后利用format分别进行赋值。若在template中声明了input_variables,利用format进行格式化时就一定要赋值,否则会报错,当在template中未设置input_variables,则会自动忽略。)
Tell me a funny joke about chickens.
当对对话类型的prompt进行格式化的时候,可以利用ChatPromptTemplate进行:
#ChatPromptTemplate.from_messages 接受各种消息表示形式。
template = ChatPromptTemplate.from_messages([
("system", "You are a helpful AI bot. Your name is {name}."),
("human", "Hello, how are you doing?"),
("ai", "I'm doing well, thanks!"),
("human", "{user_input}"),
])
messages = template.format_messages(
name="Bob",
user_input="What is your name?"
)
print(messages)
Output如下(可以看到,ChatPromptTemplate会根据role,对每一句进行标准格式化。除了此类方法,也可以直接指定身份模块如SystemMessage, HumanMessagePromptTemplate进行格式化,这里不再赘述。)
[('system', 'You are a helpful AI bot. Your name is Bob.'),
('human', 'Hello, how are you doing?'),
('ai', "I'm doing well, thanks!"),
('human', 'What is your name?')]
2️⃣部分提示词模版:
在生成prompt前就已经提前初始化部分的提示词,实际进一步导入模版的时候只导入除已初始化的变量即可。通常部分提示词模版会被用在全局设置上,如下示例,在正式format前设定foo值为foo,这样在生成最终prompt的时候只需要指定bar的值即可。有两种方法去指定部分提示词:
from langchain.prompts import PromptTemplate
prompt = PromptTemplate(template="{foo}{bar}", input_variables=["foo", "bar"])
# 可以使用 PromptTemplate.partial() 方法创建部分提示模板。
partial_prompt = prompt.partial(foo="foo")
print(partial_prompt.format(bar="baz"))
#也可以只使用分部变量初始化提示。
prompt = PromptTemplate(template="{foo}{bar}", input_variables=["bar"], partial_variables={"foo": "foo"})
print(prompt.format(bar="baz"))
Output如下:
foobaz
foobaz
此外,我们也可以将函数的最终值作为prompt的一部分进行返回,如下例子,如果想在prompt中实时展示当下时间,我们可以直接声明一个函数用来返回当下时间,并最终将该函数拼接到prompt中去:
from datetime import datetime
def _get_datetime():
now = datetime.now()
return now.strftime("%m/%d/%Y, %H:%M:%S")
prompt = PromptTemplate(
template="Tell me a {adjective} joke about the day {date}",
input_variables=["adjective", "date"]
)
partial_prompt = prompt.partial(date=_get_datetime)
print(partial_prompt.format(adjective="funny"))
# 除上述方法,部分函数声明和普通的prompt一样,也可以直接用partial_variables去声明
prompt = PromptTemplate(
template="Tell me a {adjective} joke about the day {date}",
input_variables=["adjective"],
partial_variables={"date": _get_datetime})
Output如下:
Tell me a funny joke about the day 12/08/2022, 16:25:30
3️⃣组成提示词模版:
可以通过PromptTemplate.compose()方法将多个提示词组合到一起。如下示例,生成了full_prompt和introduction_prompt进行近一步组合。
from langchain.prompts.pipeline import PipelinePromptTemplate
from langchain.prompts.prompt import PromptTemplate
full_template = """{introduction}
{example}
"""
full_prompt = PromptTemplate.from_template(full_template)
introduction_template = """You are impersonating Elon Musk."""
introduction_prompt = PromptTemplate.from_template(introduction_template)
example_template = """Here's an example of an interaction """
example_prompt = PromptTemplate.from_template(example_template)
input_prompts = [("introduction", introduction_prompt),
("example", example_prompt),]
pipeline_prompt = PipelinePromptTemplate(final_prompt=full_prompt, pipeline_prompts=input_prompts)
4️⃣自定义提示模版:
在创建prompt时,我们也可以按照自己的需求去创建自定义的提示模版。官方文档举了一个生成给定名称的函数的英语解释例子,在这个例子中函数名称作为输入,并设置提示格式以提供函数的源代码:
import inspect
# 该函数将返回给定其名称的函数的源代码。 inspect作用就是获取源代码
def get_source_code(function_name):
# Get the source code of the function
return inspect.getsource(function_name)
# 测试函数
def test():
return 1 + 1
from langchain.prompts import StringPromptTemplate
from pydantic import BaseModel, validator
# 初始化字符串prompt
PROMPT = """\
提供一个函数名和源代码并给出函数的相应解释
函数名: {function_name}
源代码:
{source_code}
解释:
"""
class FunctionExplainerPromptTemplate(StringPromptTemplate, BaseModel):
"""一个自定义提示模板,以函数名作为输入,并格式化提示模板以提供函数的源代码。 """
@validator("input_variables")
def validate_input_variables(cls, v):
"""验证输入变量是否正确。"""
if len(v) != 1 or "function_name" not in v:
raise ValueError("函数名必须是唯一的输入变量。")
return v
def format(self, **kwargs) -> str:
# 获取源代码
source_code = get_source_code(kwargs["function_name"])
# 源代码+名字提供给prompt
prompt = PROMPT.format(
function_name=kwargs["function_name"].__name__, source_code=source_code)
return prompt
def _prompt_type(self):
return "function-explainer"
FunctionExplainerPromptTemplate接收两个变量一个是prompt,另一个则是传入需要用到的model,该class下面的validate_input_variables用来验证输入量,format函数用来输出格式化后的prompt.
#初始化prompt实例
fn_explainer = FunctionExplainerPromptTemplate(input_variables=["function_name"])
# 定义函数 test_add
def test_add():
return 1 + 1
# Generate a prompt for the function "test_add"
prompt_1 = fn_explainer.format(function_name=test_add)
print(prompt_1)
Output如下:
5️⃣少量提示模版:
在构建prompt时,可以通过构建一个少量示例列表去进一步格式化prompt,每一个示例表都的结构都为字典,其中键是输入变量,值是输入变量的值。该过程通常先利用PromptTemplate将示例格式化成为字符串,然后创建一个FewShotPromptTemplate对象,用来接收few-shot的示例。官方文档中举例:
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
from langchain.prompts import FewShotPromptTemplate, PromptTemplate
examples = [
{"question": "Who lived longer, Muhammad Ali or Alan Turing?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: How old was Muhammad Ali when he died?
Intermediate answer: Muhammad Ali was 74 years old when he died.
Follow up: How old was Alan Turing when he died?
Intermediate answer: Alan Turing was 41 years old when he died.
So the final answer is: Muhammad Ali
"""},
{"question": "When was the founder of craigslist born?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: Who was the founder of craigslist?
Intermediate answer: Craigslist was founded by Craig Newmark.
Follow up: When was Craig Newmark born?
Intermediate answer: Craig Newmark was born on December 6, 1952.
So the final answer is: December 6, 1952
"""},
{"question": "Who was the maternal grandfather of George Washington?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: Who was the mother of George Washington?
Intermediate answer: The mother of George Washington was Mary Ball Washington.
Follow up: Who was the father of Mary Ball Washington?
Intermediate answer: The father of Mary Ball Washington was Joseph Ball.
So the final answer is: Joseph Ball
"""},
{"question": "Are both the directors of Jaws and Casino Royale from the same country?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: Who is the director of Jaws?
Intermediate Answer: The director of Jaws is Steven Spielberg.
Follow up: Where is Steven Spielberg from?
Intermediate Answer: The United States.
Follow up: Who is the director of Casino Royale?
Intermediate Answer: The director of Casino Royale is Martin Campbell.
Follow up: Where is Martin Campbell from?
Intermediate Answer: New Zealand.
So the final answer is: No
"""}
]
# 配置一个格式化程序,该格式化程序将prompt格式化为字符串。此格式化程序应该是一个 PromptTemplate 对象。
example_prompt = PromptTemplate(input_variables=["question", "answer"], template="Question: {question}\n{answer}")
print(example_prompt.format(**examples[0]))
# 创建一个选择器来选择最相似的例子
example_selector = SemanticSimilarityExampleSelector(
examples=examples,
vector_store=Chroma(),
embeddings_model=OpenAIEmbeddings(),
example_prompt=example_prompt
)
# 最后用FewShotPromptTemplate 来创建一个提示词模板,该模板将输入变量作为输入,并将其格式化为包含示例的提示词。
prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
suffix="Question: {input}",
input_variables=["input"]
)
print(prompt)
除了上述普通的字符串模版,聊天模版中也可以采用此类方式构建一个带例子的聊天提示词模版:
from langchain.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
# 这是一个聊天提示词模板,它将输入变量作为输入,并将其格式化为包含示例的提示词。
examples = [{"input": "2+2", "output": "4"}, {"input": "2+3", "output": "5"},]
# 提示词模板,用于格式化每个单独的示例。
example_prompt = ChatPromptTemplate.from_messages(
[("human", "{input}"),
("ai", "{output}"),])
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples)
print(few_shot_prompt.format())
6️⃣独立化prompt:
为了便于共享、存储和加强对prompt的版本控制,可以将想要设定prompt所支持的格式保存为JSON或者YAML格式文件。也可以直接将待格式化的prompt单独存储于一个文件中,通过格式化文件指定相应路径,以更方便用户加载任何类型的提示信息。
创建json文件:
{
"_type": "prompt",
"input_variables": ["adjective", "content"],
"template": "Tell me a {adjective} joke about {content}."
}
主文件代码:
from langchain.prompts import load_prompt
prompt = load_prompt("./simple_prompt.json")
print(prompt.format(adjective="funny", content="chickens"))
Output如下:
Tell me a funny joke about chickens.
这里是直接在json文件中指定template语句,除此之外也可以将template单独抽离出来,然后在json文件中指定template语句所在的文件路径,以实现更好的区域化,方便管理prompt。
创建json文件:
{
"_type": "prompt",
"input_variables": ["adjective", "content"],
"template_path": "./simple_template.txt"
}
simple_template.txt:
Tell me a {adjective} joke about {content}.
其余部分代码同第一部分介绍,最后的输出结果也是一致的。
3.1.1.3Selector:
在few shot模块,当我们列举一系列示例值,但不进一步指定返回值,就会返回所有的prompt示例,在实际开发中我们可以使用自定义选择器来选择例子。例如,想要返回一个和新输入的内容最为近似的prompt,这时候就可以去选择与输入最为相似的例子。这里的底层逻辑是利用了SemanticSimilarityExampleSelector这个例子选择器和向量相似度的计算(openAIEmbeddings)以及利用chroma进行数据存储,代码如下:
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
example_selector = SemanticSimilarityExampleSelector.from_examples(
# 可选的示例列表。
examples,
# 用于生成嵌入的嵌入类,这些嵌入用于测量语义相似性。
OpenAIEmbeddings(),
# 用于存储嵌入并进行相似性搜索的 VectorStore 类。
Chroma,
# 要生成的示例数。
k=1)
然后我们去输入一条想要构建的prompt,遍历整个示例列表,找到最为合适的example。
# 选择与输入最相似的示例。
question = "Who was the father of Mary Ball Washington?"
selected_examples = example_selector.select_examples({"question": question})
print(f"Examples most similar to the input: {question}")
for example in selected_examples:
print("\n")
for k, v in example.items():
print(f"{k}: {v}")
此时就可以返回一个最相似的例子。接下来我们可以重新重复few shot的步骤,利用FewShotPromptTemplate去创建一个提示词模版。
对于聊天类型的few shot的prompt我们也可以采用例子选择器进行格式化:
examples = [
{"input": "2+2", "output": "4"},
{"input": "2+3", "output": "5"},
{"input": "2+4", "output": "6"},
{"input": "What did the cow say to the moon?", "output": "nothing at all"},
{
"input": "Write me a poem about the moon",
"output": "One for the moon, and one for me, who are we to talk about the moon?",
},
]
# 由于我们使用向量存储来根据语义相似性选择示例,因此我们需要首先填充存储。
to_vectorize = [" ".join(example.values()) for example in examples]
# 这里就单纯理解为将value对应的值提取出来进行格式化即可。
# 创建向量库后,可以创建 example_selector 以表示返回的相似向量的个数
# 注意:您需要先创建一个向量存储库(例如:vectorstore = ...)并填充它,然后将其传递给 SemanticSimilarityExampleSelector。
example_selector = SemanticSimilarityExampleSelector(vectorstore=vectorstore, k=2)
# 提示词模板将通过将输入传递给 `select_examples` 方法来加载示例
example_selector.select_examples({"input": "horse"})
此时就可以返回两个个最相似的例子。接下来我们可以重复few shot的步骤 利用FewShotChatPromptTemplate去创建一个提示词模版。
上文中介绍了在利用Langchain进行应用开发时所常用的构建prompt方式,无论哪种方式其最终目的都是为了更方便的去构建prompt,并尽可能的增加其复用性。Langchain提供的prompt相关工具远不止上文这些,在了解了基础能力后可以进一步查阅官方文档找到最适合项目特点的工具,进行prompt格式化。
3.1.2LLM
上除了上文中的prompt,LLM作为langchain中的核心内容,也是我们需要花心思去了解学习的,不过还是那句话,应用层的开发实际上无需到模型底层原理了解的十分透彻,我们更应该关注的是llm的调用形式,Langchain作为一个“工具”它并没有提供自己的LLM,而是提供了一个接口,用于与许多不同类型的LLM进行交互,比如耳熟能详的openai、huggingface或者是cohere等,都可以通过langchain快速调用。
1.单个调用:直接调用Model对象,传入一串字符串然后直接返回输出值,以openAI为例:
from langchain.llms import OpenAI
llm = OpenAI()
print(llm('你是谁'))
2.批量调用:通过generate可以对字符串列表,进行批量应用Model,使输出更加丰富且完整。
llm_result = llm.generate(["给我背诵一首古诗", "给我讲个100字小故事"]*10)
这时的llm_result会生成一个键为generations的数组,这个数组长度为20项,第一项为古诗、第二项为故事、第三项又为古诗,以此规则排列..
3.异步接口:asyncio库为LLM提供异步支持,目前支持的LLM为OpenAI、PromptLayerOpenAI、ChatOpenAI 、Anthropic 和 Cohere 受支持。 可以使用agenerate 异步调用 OpenAI LLM。 在代码编写中,如果用了科学上网/魔法,以openAI为例,在异步调用之前,则需要预先将openai的proxy设置成为本地代理(这步很重要,若不设置后续会有报错)
import os
import openai
import asyncio
from langchain.llms import OpenAI
# 设置代理
openai.proxy = os.getenv('https_proxy')
# 定义一个同步方式生成文本的函数
def generate_serially():
llm = OpenAI(temperature=0.9) # 创建OpenAI对象,并设置temperature参数为0.9
for _ in range(10): # 循环10次
resp = llm.generate(["Hello, how are you?"]) # 调用generate方法生成文本
print(resp.generations[0][0].text) # 打印生成的文本
# 定义一个异步生成文本的函数
async def async_generate(llm):
resp = await llm.agenerate(["Hello, how are you?"]) # 异步调用agenerate方法生成文本
print(resp.generations[0][0].text) # 打印生成的文本
# 定义一个并发(异步)方式生成文本的函数
async def generate_concurrently():
llm = OpenAI(temperature=0.9) # 创建OpenAI对象,并设置temperature参数为0.9
tasks = [async_generate(llm) for _ in range(10)] # 创建10个异步任务
await asyncio.gather(*tasks) # 使用asyncio.gather等待所有异步任务完成
可以用time库去检查运行时间,利用同步调用耗时大概为12s,异步耗时仅有2s。通过这种方式可以大大提速任务执行。
4.自定义大语言模型:在开发过程中如果遇到需要调用不同的LLM时,可以通过自定义LLM实现效率的提高。自定义LLM时,必须要实现的是\_call方法,通过这个方法接受一个字符串、一些可选的索引字,并最终返回一个字符串。除了该方法之外,还可以选择性生成一些方法用于以字典的模式返回该自定义LLM类的各属性。
from langchain.callbacks.manager import CallbackManagerForLLMRun
from langchain.llms.base import LLM
from typing import Optional, List, Any, Mapping
class CustomLLM(LLM): # 这个类 CustomLLM 继承了 LLM 类,并增加了一个新的类变量 n。
n: int # 类变量,表示一个整数
@property
def _llm_type(self) -> str:
return "custom"
def _call(
self,
prompt: str, # 输入的提示字符串
stop: Optional[List[str]] = None, # 可选的停止字符串列表,默认为 None
run_manager: Optional[CallbackManagerForLLMRun] = None, # 可选的回调管理器,默认为 None
**kwargs: Any,
) -> str:
# 如果 stop 参数不为 None,则抛出 ValueError 异常
if stop is not None:
raise ValueError("stop kwargs are not permitted.")
return prompt[: self.n] # 返回 prompt 字符串的前 n 个字符
@property # 一个属性装饰器,用于获取 _identifying_params 的值
def _identifying_params(self) -> Mapping[str, Any]:
"""Get the identifying parameters.""" # 这个方法的文档字符串,说明这个方法的功能是获取标识参数
return {"n": self.n} # 返回一个字典,包含 n 的值
5.测试大语言模型:为了节省我们的成本,当写好一串代码进行测试的时候,通常情况下我们是不希望去真正调用LLM,因为这会消耗token(打工人表示伤不起),贴心的Langchain则提供给我们一个“假的”大语言模型,以方便我们进行测试。
# 从langchain.llms.fake模块导入FakeListLLM类,此类可能用于模拟或伪造某种行为
from langchain.llms.fake import FakeListLLM
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
# 调用load_tools函数,加载"python_repl"的工具
tools = load_tools(["python_repl"])
# 定义一个响应列表,这些响应可能是模拟LLM的预期响应
responses = ["Action: Python REPL\nAction Input: print(2 + 2)", "Final Answer: 4"]
# 使用上面定义的responses初始化一个FakeListLLM对象
llm = FakeListLLM(responses=responses)
# 调用initialize_agent函数,使用上面的tools和llm,以及指定的代理类型和verbose参数来初始化一个代理
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)
# 调用代理的run方法,传递字符串"whats 2 + 2"作为输入,询问代理2加2的结果
agent.run("whats 2 + 2")
与模拟llm同理,langchain也提供了一个伪类去模拟人类回复,该功能依赖于wikipedia,所以模拟前需要install一下这个库,并且需要设置proxy。这里同fakellm需要依赖agent的三个类,此外它还依赖下面的库:
# 从langchain.llms.human模块导入HumanInputLLM类,此类可能允许人类输入或交互来模拟LLM的行为
from langchain.llms.human import HumanInputLLM
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
# 调用load_tools函数,加载名为"wikipedia"的工具
tools = load_tools(["wikipedia"])
# 初始化一个HumanInputLLM对象,其中prompt_func是一个函数,用于打印提示信息
llm = HumanInputLLM(
prompt_func=lambda prompt: print(f"\n===PROMPT====\n{prompt}\n=====END OF PROMPT======"))
# 调用initialize_agent函数,使用上面的tools和llm,以及指定的代理类型和verbose参数来初始化一个代理
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
# 调用代理的run方法,传递字符串"What is 'Bocchi the Rock!'?"作为输入,询问代理关于'Bocchi the Rock!'的信息
agent.run("What is 'Bocchi the Rock!'?")
6.缓存大语言模型:和测试大语言模型具有一样效果的是缓存大语言模型,通过缓存层可以尽可能的减少API的调用次数,从而节省费用。在Langchain中设置缓存分为两种情况:一是在内存中设置缓存,二是在数据中设置缓存。存储在内存中加载速度较快,但是占用资源并且在关机之后将不再被缓存,在内存中设置缓存示例如下:
from langchain.cache import SQLiteCache
import langchain
from langchain.llms import OpenAI
import time
langchain.llm_cache = SQLiteCache(database_path=".langchain.db")
llm = OpenAI(model_name="text-davinci-002", n=2, best_of=2)
start_time = time.time() # 记录开始时间
print(llm.predict("用中文讲个笑话"))
end_time = time.time() # 记录结束时间
elapsed_time = end_time - start_time # 计算总时间
print(f"Predict method took {elapsed_time:.4f} seconds to execute.")
这里的时间大概花费1s+ ,因为被问题放在了内存里,所以在下次调用时几乎不会再耗费时间。
除了存储在内存中进行缓存,也可以存储在数据库中进行缓存,当开发企业级应用的时候通常都会选择存储在数据库中,不过这种方式的加载速度相较于将缓存存储在内存中更慢一些,不过好处是不占电脑资源,并且存储记录并不会随着关机消失。
from langchain.cache import SQLiteCache
import langchain
from langchain.llms import OpenAI
import time
langchain.llm_cache = SQLiteCache(database_path=".langchain.db")
llm = OpenAI(model_name="text-davinci-002", n=2, best_of=2)
start_time = time.time() # 记录开始时间
print(llm.predict("用中文讲个笑话"))
end_time = time.time() # 记录结束时间
elapsed_time = end_time - start_time # 计算总时间
print(f"Predict method took {elapsed_time:.4f} seconds to execute.")
7.跟踪token使用情况(仅限model为openAI):
from langchain.llms import OpenAI
from langchain.callbacks import get_openai_callback
llm = OpenAI(model_name="text-davinci-002", n=2, best_of=2, cache=None)
with get_openai_callback() as cb:
result = llm("讲个笑话")
print(cb)
上述代码直接利用get_openai_callback即可完成对于单条的提问时token的记录,此外对于有多个步骤的链或者agent,langchain也可以追踪到各步骤所耗费的token。
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.llms import OpenAI
from langchain.callbacks import get_openai_callback
llm = OpenAI(temperature=0)
tools = load_tools(["llm-math"], llm=llm)
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)
with get_openai_callback() as cb:
response = agent.run("王菲现在的年龄是多少?")
print(f"Total Tokens: {cb.total_tokens}")
print(f"Prompt Tokens: {cb.prompt_tokens}")
print(f"Completion Tokens: {cb.completion_tokens}")
print(f"Total Cost (USD): ${cb.total_cost}")
8.序列化配置大语言模型:Langchain也提供一种能力用来保存LLM在训练时使用的各类系数,比如template、 model_name等。这类系数通常会被保存在json或者yaml文件中,以json文件为例,配置如下系数,然后利用load_llm方法即可导入:
from langchain.llms.loading import load_llm
llm = load_llm("llm.json")
{
"model_name": "text-davinci-003",
"temperature": 0.7,
"max_tokens": 256,
"top_p": 1.0,
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
"n": 1,
"best_of": 1,
"request_timeout": None,
"_type": "openai"
}
亦或者在配置好大模型参数之后,直接利用save方法即可直接保存配置到指定文件中。
llm.save("llmsave.json")
9.流式处理大语言模型的响应:流式处理意味着,在接收到第一个数据块后就立即开始处理,而不需要等待整个数据包传输完毕。这种概念应用在LLM中则可达到生成响应时就立刻向用户展示此下的响应,或者在生成响应时处理响应,也就是我们现在看到的和ai对话时逐字输出的效果:可以看到实现还是较为方便的只需要直接调用StreamingStdOutCallbackHandler作为callback即可。
from langchain.llms import OpenAI
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
llm = OpenAI(streaming=True, callbacks=[StreamingStdOutCallbackHandler()], temperature=0)
resp = llm("Write me a song about sparkling water.")
可以看到实现还是较为方便的只需要直接调用StreamingStdOutCallbackHandler作为callback即可。
3.1.3OutputParsers
Model返回的内容通常都是字符串的模式,但在实际开发过程中,往往希望model可以返回更直观的内容,Langchain提供的输出解析器则将派上用场。在实现一个输出解析器的过程中,需要实现两种方法:1️⃣获取格式指令:返回一个字符串的方法,其中包含有关如何格式化语言模型输出的说明。2️⃣Parse:一种接收字符串(假设是来自语言模型的响应)并将其解析为某种结构的方法。
1.列表解析器:利用此解析器可以输出一个用逗号分割的列表。
from langchain.output_parsers import CommaSeparatedListOutputParser
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.llms import OpenAI
from langchain.chat_models import ChatOpenAI
output_parser = CommaSeparatedListOutputParser()
format_instructions = output_parser.get_format_instructions()
prompt = PromptTemplate(
template="List five {subject}.\n{format_instructions}",
input_variables=["subject"],
partial_variables={"format_instructions": format_instructions}
)
model = OpenAI(temperature=0)
_input = prompt.format(subject="冰淇淋口味")
output = model(_input)
output_parser.parse(output)
2.日期解析器:利用此解析器可以直接将LLM输出解析为日期时间格式。
from langchain.prompts import PromptTemplate
from langchain.output_parsers import DatetimeOutputParser
from langchain.chains import LLMChain
from langchain.llms import OpenAI
output_parser = DatetimeOutputParser()
template = """回答用户的问题:
{question}
{format_instructions}"""
prompt = PromptTemplate.from_template(
template,
partial_variables={"format_instructions": output_parser.get_format_instructions()},
)
chain = LLMChain(prompt=prompt, llm=OpenAI())
output = chain.run("bitcoin是什么时候成立的?用英文格式输出时间")
3.枚举解析器
from langchain.output_parsers.enum import EnumOutputParser
from enum import Enum
class Colors(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
parser = EnumOutputParser(enum=Colors)
4.自动修复解析器:这类解析器是一种嵌套的形式,如果第一个输出解析器出现错误,就会直接调用另一个一修复错误
# 导入所需的库和模块
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
# 定义一个表示演员的数据结构,包括他们的名字和他们出演的电影列表
class Actor(BaseModel):
name: str = Field(description="name of an actor") # 演员的名字
film_names: List[str] = Field(description="list of names of films they starred in") # 他们出演的电影列表
# 定义一个查询,用于提示生成随机演员的电影作品列表
actor_query = "Generate the filmography for a random actor."
# 使用`Actor`模型初始化解析器
parser = PydanticOutputParser(pydantic_object=Actor)
# 定义一个格式错误的字符串数据
misformatted = "{'name': 'Tom Hanks', 'film_names': ['Forrest Gump']}"
# 使用解析器尝试解析上述数据
try:
parsed_data = parser.parse(misformatted)
except Exception as e:
print(f"Error: {e}")
parser.parse(misformatted)
格式错误的原因是因为json文件需要双引号进行标记,但是这里用了单引号,此时利用该解析器进行解析就会出现报错,但是此时可以利用RetryWithErrorOutputParser进行修复错误,则会正常输出不报错。
from langchain.output_parsers import RetryWithErrorOutputParser
from langchain.llms import OpenAI
retry_parser = RetryWithErrorOutputParser.from_llm(
parser=parser, llm=OpenAI(temperature=0))
retry_parser.parse_with_prompt(bad_response, prompt_value)
这里的“Parse_with_prompt”:一种方法,它接受一个字符串(假设是来自语言模型的响应)和一个提示(假设是生成此类响应的提示)并将其解析为某种结构。提示主要在 OutputParser 想要以某种方式重试或修复输出时提供,并且需要来自提示的信息才能执行此操作。
四、Retrieval
Retrieval直接汉译过来即”检索“。该功能经常被应用于构建一个“私人的知识库”,构建过程更多的是将外部数据存储到知识库中。细化这一模块的主要职能有四部分,其包括数据的获取、整理、存储和查询。如下图:
首先,在该过程中可以从本地/网站/文件等资源库去获取数据,当数据量较小时,我们可以直接进行存储,但当数据量较大的时候,则需要对其进行一定的切片,切分时可以按照数据类型进行切片处理,比如针对文本类数据,可以直接按照字符、段落进行切片;代码类数据则需要进一步细分以保证代码的功能性;此外,除了按照数据类型进行切片处理,也可以直接根据token进行切片。而后利用Vector Stores进行向量存储,其中Embedding完成的就是数据的向量化,虽然这一能力往往被嵌套至大模型中,但是我们也要清楚并不是所有的模型都能直接支持文本向量化这一能力。除此之外的memory、self-hosted以及baas则是指向量存储的三种载体形式,可以选择直接存储于内存中,也可以选择存储上云。最后则利用这些向量化数据进行检索,检索形式可以是直接按照向量相似度去匹配相似内容,也可以直接网络,或者借用其他服务实现检索以及数据的返回。
4.1向量数据库
4.1.1基本概念
从上文中我们可以发现,对于retrievers来说,向量数据库发挥着很大的作用,它不仅实现向量的存储也可以通过相似度实现向量的检索,但是向量数据库到底是什么呢?它和普通的数据库有着怎样的区别呢?相信还是有很多同学和我一样有一点点疑惑,所以在介绍langchain在此module方面的能力前,先介绍一下向量数据库,以及它在LLM中所发挥的作用。
我们在对一个事物进行描述的时候,通常会根据事物的各方面特征进行表述。设想这样一个场景,假设你是一名摄影师,拍了大量的照片。为了方便管理和查找,你决定将这些照片存储到一个数据库中。传统的关系型数据库(如 MySQL、PostgreSQL 等)可以帮助你存储照片的元数据,比如拍摄时间、地点、相机型号等。但是,当你想要根据照片的内容(如颜色、纹理、物体等)进行搜索时,传统数据库可能无法满足你的需求,因为它们通常以数据表的形式存储数据,并使用查询语句进行精确搜索。但向量包含了大量信息,使用查询语句很难精确地找到唯一的向量。
那么此时,向量数据库就可以派上用场。我们可以构建一个多维的空间使得每张照片特征都存在于这个空间内,并用已有的维度进行表示,比如时间、地点、相机型号、颜色....此照片的信息将作为一个点,存储于其中。以此类推,即可在该空间中构建出无数的点,而后我们将这些点与空间坐标轴的原点相连接,就成为了一条条向量,当这些点变为向量之后,即可利用向量的计算进一步获取更多的信息。当要进行照片的检索时,也会变得更容易更快捷。但在向量数据库中进行检索时,检索并不是唯一的而是查询和目标向量最为相似的一些向量,具有模糊性。
那么我们可以延伸思考一下,只要对图片、视频、商品等素材进行向量化,就可以实现以图搜图、视频相关推荐、相似宝贝推荐等功能,那应用在LLM中,小则可直接实现相关问题提示,大则我们完全可以利用此特性去历史对话记录中找到一些最类似的对话,然后重新喂给大模型,这将极大的提高大模型的输出结果的准确性。 为更好的了解向量数据库,接下来将继续介绍向量的几种检索方式,以对向量数据库有一个更深度的了解。
4.1.2存储方式
因为每一个向量所记录的信息量都是比较多的,所以自然而然其所占内存也是很大的,举个例子,如果我们的一个向量维度是256维的,那么该向量所占用的内存大小就是:256 * 32/8=1024字节,若数据库中共计一千万个向量,则所占内存为10240000000字节,也就是9.54GB,已经是一个很庞大的数目了,而在实际开发中这个规模往往更大,因此解决向量数据库的内存占用问题是重中之重的。我们往往会对每个向量进行压缩,从而缩小其内存占用。常常利用乘积量化方法
乘积量化:该思想将高维向量分解为多个子向量。例如,将一个D维向量分解为m个子向量,每个子向量的维度为D/m。然后对每个子向量进行量化。对于每个子向量空间,使用聚类算法将子向量分为K个簇,并将簇中心作为量化值。然后,用子向量在簇中的索引来表示原始子向量。这样,每个子向量可以用一个整数(量化索引)来表示。最后将量化索引组合起来表示原始高维向量。对于一个D维向量,可以用m个整数来表示,其中每个整数对应一个子向量的量化索引。此外这类方法不仅可以用于优化存储向量也可以用于优化检索。
4.1.3检索方式
通过上段文字的描述,我们不难发现,向量检索过程可以抽象化为“最近邻问题“,对应的算法就是最近邻搜索算法,具体有如下几种:
1.暴力搜索:依次比较向量数据库中所有的的向量与目标向量的相似度,然后找出相似度最高一个或一些向量,这样得到的结果质量是极高的,但这对于数据量庞大的数据库来说无疑是十分耗时的。
2.聚类搜索:这类算法首先初始化K个聚类中心,将数据对象分组成若干个类别或簇(cluster)。其主要目的是根据数据的相似性或距离度量来对数据进行分组,然后根据所选的聚类算法,通过迭代计算来更新聚类结果。例如,在K-means算法中,需要不断更新簇中心并将数据对象分配给最近的簇中心;在DBSCAN算法中,需要根据密度可达性来扩展簇并合并相邻的簇。最后设置一个收敛条件,用于判断聚类过程是否结束。收敛条件可以是迭代次数、簇中心变化幅度等。当满足收敛条件时,聚类过程结束。这样的搜索效率大大提高,但是不可避免会出现遗漏的情况。
3.位置敏感哈希:此算法首先选择一组位置敏感哈希函数,该函数需要满足一个特性:对于相似的数据点,它们的哈希值发生冲突的概率较高;对于不相似的数据点,它们的哈希值发生冲突的概率较低。而后利用该函数对数据集中的每个数据点进行哈希。将具有相同哈希值的数据点存储在相同的哈希桶中。在检索过程中,对于给定的查询点,首先使用LSH函数计算其哈希值,然后在相应的哈希桶中搜索相似的数据点。最后根据需要,可以在搜索到的候选数据点中进一步计算相似度,以找到最近邻。
4.分层级的导航小世界算法:这是一种基于图的近似最近邻搜索方法,适用于大规模高维数据集。其核心思想是将数据点组织成一个分层结构的图,使得在高层次上可以快速地找到距离查询点较近的候选点,然后在低层次逐步细化搜索范围,从而加速最近邻搜索过程。
该算法首先创建一个空的多层图结构。每一层都是一个图,其中节点表示数据点,边表示节点之间的连接关系。最底层包含所有数据点,而上层图只包含部分数据点。每个数据点被分配一个随机的层数,表示该点在哪些层次的图中出现。然后插入数据点:对于每个新插入的数据点,首先确定其层数,然后从最高层开始,将该点插入到相应的图中。插入过程中,需要找到该点在每层的最近邻,并将它们连接起来。同时,还需要更新已有节点的连接关系,以保持图的导航性能。其检索过程是首先在最高层的图中找到一个起始点,然后逐层向下搜索,直到达到底层。在每一层,从当前点出发,沿着边进行搜索,直到找到一个局部最近邻。然后将局部最近邻作为下一层的起始点,继续搜索。最后,在底层找到的结果则为最终结果。
4.2向量数据库与AI
前文中大概介绍了向量数据库是什么以及向量数据库所依赖的一些实现技术,接下来我们来谈论一下向量数据库与大模型之间的关系。为什么说想要用好大模型往往离不开向量数据库呢?对于大模型来讲,处理的数据格式一般都是非结构化数据,如音频、文本、图像..我们以大语言模型为例,在喂一份数据给大模型的时候,数据首先会被转为向量,在上述内容中我们知道如果向量较近那么就表示这两个向量含有的信息更为相似,当大量数据不断被喂到大模型中的时候,语言模型就会逐渐发现词汇间的语义和语法。当用户进行问答的时候,问题输入Model后会基于Transformer架构从每个词出发去找到它与其他词的关系权重,找到权重最重的一组搭配,这一组就为此次问答的答案了。最后再将这组向量返回回来,也就完成了一次问答。当我们把向量数据库接入到AI中,我们就可以通过更新向量数据库的数据,使得大模型能够不断获取并学习到业界最新的知识,而不是将能力局限于预训练的数据中。这种方式要比微调/重新训练大模型的方式节约更多成本。
4.3DataLoaders
为了更好的理解retrieval的功能,在上文中先介绍了一下它所依赖的核心概念——向量数据库,接下来让我们看一下Langhcain中的retrieval是如何发挥作用的。我们已经知道,一般在用户开发(LLM)应用程序,往往会需要使用不在模型训练集中的特定数据去进一步增强大语言模型的能力,这种方法被称为检索增强生成(RAG)。LangChain 提供了一整套工具来实现 RAG 应用程序,首先第一步就是进行文档的相应加载即DocumentLoader:
LangChain提供了多种文档加载器,支持从各种不同的来源加载文档(例如,私有的存储桶或公共网站),支持的文档类型也十分丰富:如 HTML、PDF 、MarkDown文件等...
1.加载 md文件:
from langchain.document_loaders import TextLoader
# 创建一个TextLoader实例,指定要加载的Markdown文件路径
loader = TextLoader("./index.md")
# 使用load方法加载文件内容并打印
print(loader.load())
2.加载csv文件:
# 导入CSVLoader类
from langchain.document_loaders.csv_loader import CSVLoader
# 创建CSVLoader实例,指定要加载的CSV文件路径
loader = CSVLoader(file_path='./index.csv')
# 使用load方法加载数据并将其存储在数据变量中
data = loader.load()
3.自定义 csv 解析和加载 指定csv文件的字段名fieldname即可
from langchain.document_loaders.csv_loader import CSVLoader
# 创建CSVLoader实例,指定要加载的CSV文件路径和CSV参数
loader = CSVLoader(file_path='./index.csv', csv_args={
'delimiter': ',',
'quotechar': '"',
'fieldnames': ['title', 'content']
})
# 使用load方法加载数据并将其存储在数据变量中
data = loader.load()
4.可以使用该 source_column 参数指定文件加载的列。
from langchain.document_loaders.csv_loader import CSVLoader
# 创建CSVLoader实例,指定要加载的CSV文件路径和源列名
loader = CSVLoader(file_path='./index.csv', source_column="context")
# 使用load方法加载数据并将其存储在数据变量中
data = loader.load()
除了上述的单个文件加载,我们也可以批量加载一个文件夹内的所有文件,该加载依赖unstructured,所以开始前需要pip一下。如加载md文件就:pip install "unstructured[md]"
# 导入DirectoryLoader类
from langchain.document_loaders import DirectoryLoader
# 创建DirectoryLoader实例,指定要加载的文件夹路径、要加载的文件类型和是否使用多线程
loader = DirectoryLoader('/Users/kyoku/Desktop/LLM/documentstore', glob='**/*.md', use_multithreading=True)
# 使用load方法加载所有文档并将其存储在docs变量中
docs = loader.load()
# 打印加载的文档数量
print(len(docs))
# 导入UnstructuredHTMLLoader类
from langchain.document_loaders import UnstructuredHTMLLoader
# 创建UnstructuredHTMLLoader实例,指定要加载的HTML文件路径
loader = UnstructuredHTMLLoader("./index.html")
# 使用load方法加载HTML文件内容并将其存储在data变量中
data = loader.load()
# 导入BSHTMLLoader类
from langchain.document_loaders import BSHTMLLoader
# 创建BSHTMLLoader实例,指定要加载的HTML文件路径
loader = BSHTMLLoader("./index.html")
# 使用load方法加载HTML文件内容并将其存储在data变量中
data = loader.load()
4.4文本拆分DataTransformers
当文件内容成功加载之后,通常会对数据集进行一系列处理,以便更好地适应你的应用。比如说,可能想把长文档分成小块,这样就能更好地放入模型。LangChain 提供了很多现成的文档转换器,可以轻松地拆分、组合、过滤文档,还能进行其他操作。
虽然上述步骤听起来较为简单,但实际上有很多潜在的复杂性。最好的情况是,把相关的文本片段放在一起。这种“相关性”可能因文本的类型而有所不同。
Langchain提供了工具RecursiveCharacterTextSplitter用来进行文本的拆分,其运行原理为:首先尝试用第一个字符进行拆分,创建小块。如果有些块太大,它就会尝试下一个字符,以此类推。默认情况下,它会按照 ["\n \n", "\n", " ", ""] 的顺序尝试拆分字符。以下为示例代码:
# 打开一个文本文件并读取内容
with open('./test.txt') as f:
state_of_the_union = f.read()
# 导入RecursiveCharacterTextSplitter类
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 创建RecursiveCharacterTextSplitter实例,设置块大小、块重叠、长度函数和是否添加开始索引
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20,
length_function=len,
add_start_index=True,
)
# 使用create_documents方法创建文档并将其存储在texts变量中
texts = text_splitter.create_documents([state_of_the_union])
从输出结果可以看到其是被拆分成了一个数组的形式。
除了上述的文本拆分,代码拆分也经常被应用于llm应用的构建中:
# 导入所需的类和枚举
from langchain.text_splitter import RecursiveCharacterTextSplitter, Language
# 定义一个包含Python代码的字符串
PYTHON_CODE = """
def hello_world():
print("Hello, World!")
# Call the function
hello_world()
"""
# 使用from_language方法创建一个针对Python语言的RecursiveCharacterTextSplitter实例
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)
# 使用create_documents方法创建文档并将其存储在python_docs变量中
python_docs = python_splitter.create_documents([PYTHON_CODE])
调用特定的拆分器可以保证拆分后的代码逻辑,这里我们只要指定不同的Language就可以对不同的语言进行拆分。
4.5向量检索简单应用
在实际开发中我们可以将数据向量化细分为两步:一是将数据向量化(向量化工具:openai的embeding、huggingface的n3d...),二是将向量化后的数据存储到向量数据库中,常见比较好用的免费向量数据库有Meta的faiss、chrome的chromad以及lance。
1.高性能:利用 CPU 和 GPU 的并行计算能力,实现了高效的向量索引和查询操作。 2.可扩展性:支持大规模数据集,可以处理数十亿个高维向量的相似性搜索和聚类任务。 3.灵活性:提供了多种索引和搜索算法,可以根据具体需求选择合适的算法。 4.开源:是一个开源项目,可以在 GitHub 上找到其源代码和详细文档。
安装相关库: pip install faiss-cpu (显卡好的同学也可以install gpu版本)
准备一个数据集,这个数据集包含一段关于信用卡年费收取和提高信用卡额度的咨询对话。客户向客服提出了关于信用卡年费和额度的问题,客服则详细解答了客户的疑问:
text = """客户:您好,我想咨询一下信用卡的问题。\n客服:您好,欢迎咨询建行信用卡,我是客服小李,请问有什么问题我可以帮您解答吗?\n客户:我想了解一下信用卡的年费如何收取?\n客服:关于信用卡年费的收取,我们会在每年的固定日期为您的信用卡收取年费。当然,如果您在一年内的消费达到一定金额,年费会自动免除。具体的免年费标准,请您查看信用卡合同条款或登录我们的网站查询。\n客户:好的,谢谢。那我还想问一下,如何提高信用卡的额度?\n客服:关于提高信用卡额度,您可以通过以下途径操作:1. 登录建行信用卡官方网站或手机APP,提交在线提额申请;2. 拨打我们的客服热线,按语音提示进行提额申请;3. 您还可以前往附近的建行网点,提交提额申请。在您提交申请后,我们会根据您的信用状况进行审核,审核通过后,您的信用卡额度将会相应提高。\n客户:明白了,非常感谢您的解答。\n客服:您太客气了,很高兴能够帮到您。如果您还有其他问题,请随时联系我们。祝您生活愉快!"""
list_text = text.split('\n')
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
db = FAISS.from_texts(list_text, OpenAIEmbeddings())
query = "信用卡的额度可以提高吗"
docs = db.similarity_search(query)
print(docs[0].page_content)
embedding_vector = OpenAIEmbeddings().embed_query(query)
print(f'embedding_vector:{embedding_vector}')
docs = db.similarity_search_by_vector(embedding_vector)
print(docs[0].page_content)
除了上述直接输出效果最好的结果,也可以按照相似度分数进行输出,不过这里的规则是分数越低,相似度越高。
# 使用带分数的相似性搜索
docs_and_scores = db.similarity_search_with_score(query)
# 打印文档及其相似性分数
for doc, score in docs_and_scores:
print(f"Document: {doc.page_content}\nScore: {score}\n")
如果每次都要调用embedding无疑太浪费,所以最后我们也可以直接将数据库保存起来,避免重复调用。
# 保存
db.save_local("faiss_index")
# 加载
new_db = FAISS.load_local("faiss_index", OpenAIEmbeddings())
在官网中还介绍了另外两种向量数据库的使用方法,这里不再赘述。
五、Memory
Memory——存储历史对话信息。该功能主要会执行两步:1.输入时,从记忆组件中查询相关历史信息,拼接历史信息和用户的输入到提示词中传给LLM。2.自动把LLM返回的内容存储到记忆组件,用于下次查询。
5.1Memory的基本实现原理:
Memory——存储历史对话信息。该功能主要会执行两步:
1.输入时,从记忆组件中查询相关历史信息,拼接历史信息和用户的输入到提示词中传给LLM。
2.自动把LLM返回的内容存储到记忆组件,用于下次查询。
不过,GPT目前就有这个功能了,它已经可以进行多轮对话了,为何我们还要把这个功能拿出来细说呢?在之前介绍prompt的文章中介绍过:在进行多轮对话时,我们会把历史对话内容不断的push到prompt数组中,通俗来讲就是将所有的聊天记录都作为prompt了,以存储的形式实现了大语言模型的“记忆”功能,而大语言模型本身是无状态的,这种方式无疑会较为浪费token,所以开发者不得不将注意力聚焦于如何在保证大语言模型功能的基础上尽可能的减少token的使用,Memory这个组件也就随之诞生。po一张Memory官网的图:
从上图可以看到Memory实现思路还是蛮简单的,就是存储查询,存储的过程我们无需过度思考,无非就是存到内存/数据库,但是读取的过程还是值得我们探讨一番,为什么这么说呢?在上文中已经知道memory的目的其实就是要在保证大语言模型能力的前提下尽可能的减少token消耗,所以我们不能把所有的数据一起丢给大语言模型,这就失去了memory的意义了,不是吗?目前memory常利用以下几种查询策略:
1.将会话直接作为prompt喂回给大模型背景,可以称之为buffer。
2.将所有历史消息丢给模型生成一份摘要,再将摘要作为prompt背景,可以称之为summary。
3.利用之前提及的向量数据库,查询相似历史信息,作为prompt背景,可以称之为vector。
5.2Memory的使用方式:
Memory这一功能的使用方式还是较为简单的,本节将会按照memory的三大分类,依次介绍memory中会被高频使用到的一些工具函数。
5.2.1Buffer
1️⃣ConversationBufferMemory
先举例一个最简单的使用方法——直接将内容存储到buffer,无论是单次或是多次存储,其对话内容都会被存储到一个memory:
memory = ConversationBufferMemory() memory.save_context({"input": "你好,我是人类"}, {"output": "你好,我是AI助手"})memory.save_context({"input": "很开心认识你"}, {"output": "我也是"})
存储后可直接输出存储内容:
print(memory.load_memory_variables({}))
# {'history': 'Human: 你好,我是人类\nAI: 你好,我是AI助手\nHuman: 很开心认识你\nAI: 我也是'}
2️⃣ConversationBufferWindowMemory
ConversationBufferMemory无疑是很简单方便的,但是可以试想一下,当我们与大语言模型进行多次对话时,直接利用buffer存储的话,所占内存量是十分大的,并且消耗的token是十分多的,这时通过ConversationBufferWindowMemory进行窗口缓存的方式就可以解决上述问题。其核心思想:就是保留一个窗口大小的对话,其内容只是最近的N次对话。在这个工具函数中,可以利用k参数来声明保留的对话记忆,比如k=1时,上述对话内容输出结果就会发生相应的改变:
memory = ConversationBufferWindowMemory(k=1)
memory.save_context({"input": "你好,我是人类"}, {"output": "你好,我是AI助手"})
memory.save_context({"input": "很开心认识你"}, {"output": "我也是"})
只保存了最近的k条记录:
print(memory.load_memory_variables({}))
# {'history': 'Human: 很开心认识你\nAI: 我也是'}
通过内置在Langchain中的缓存窗口(BufferWindow)可以将meomory"记忆"下来。
3️⃣ConversationTokenBufferMemory
除了通过设置对话数量控制memory,也可以通过设置token来限制。如果字符数量超出指定数目,它会切掉这个对话的早期部分 以保留与最近的交流相对应的字符数量
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationTokenBufferMemory
llm = ChatOpenAI(temperature=0.0)
memory = ConversationTokenBufferMemory(llm=llm,)
memory.save_context({"input": "春眠不觉晓"}, {"output": "处处闻啼鸟"})
memory.save_context({"input": "夜来风雨声"}, {"output": "花落知多少"})
print(memory.load_memory_variables({}))
#{'history': 'AI: 花落知多少。'}
5.2.2Summary
对于buffer方式我们不难发现,如果全部保存下来太过浪费,截断时无论是按照对话条数还是token都是无法保证即节省内存或token又保证对话质量的,所以我们可以对其进行summary:
ConversationSummaryBufferMemory
在进行总结时最基础的就是ConversationSummaryBufferMemory这个工具函数,利用该函数时通过设置token从而在清除历史对话时生成一份对话记录:
memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=40, return_messages=True)
memory.save_context({"input": "嗨"}, {"output": "你好吗"})
memory.save_context({"input": "没什么特别的,你呢"}, {"output": "我也是"})
messages = memory.chat_memory.messages
previous_summary = ""
print(memory.predict_new_summary(messages, previous_summary))
# 人类和AI都表示没有做什么特别的事
该API通过 predict_new_summary成功的将对话进行了摘要总结。
5.2.3vector
最后来介绍一下vector在memory中的用法,通过VectorStoreRetrieverMemory可以将memory存储到Vector数据库中,每次调用时,就会查找与该记忆关联最高的k个文档,并且不会跟踪交互顺序。不过要注意的是,在利用VectorStoreRetrieverMemory前,我们需要先初始化一个VectorStore,免费向量数据库有Meta的faiss、chrome的chromad以及lance,以faiss为例:
import faiss
from langchain.docstore import InMemoryDocstore
from langchain.vectorstores import FAISS
embedding_size = 1536 # Dimensions of the OpenAIEmbeddings
index = faiss.IndexFlatL2(embedding_size)
embedding_fn = OpenAIEmbeddings().embed_query
vectorstore = FAISS(embedding_fn, index, InMemoryDocstore({}), {})
初始化好一个数据库之后,我们就可以根据该数据库实例化出一个memory:
# 在实际使用中,可以将`k` 设为更高的值,这里使用 k=1 来展示
# 向量查找仍然返回语义相关的信息
retriever = vectorstore.as_retriever(search_kwargs=dict(k=1))
memory = VectorStoreRetrieverMemory(retriever=retriever)
# 当添加到一个代理时,内存对象可以保存来自对话或使用的工具的相关信息
memory.save_context({"input": "我最喜欢的食物是披萨"}, {"output": "好的,我知道了"})
memory.save_context({"input": "我最喜欢的运动是足球"}, {"output": "..."})
memory.save_context({"input": "我不喜欢凯尔特人队"}, {"output": "好的"})
print(memory.load_memory_variables({"prompt": "我应该看什么运动?"})["history"])
这时便会根据向量数据库检索后输出memory结果
{
'history': [
{
'input': '我最喜欢的运动是足球',
'output': '...'
}
]
}
这表示在与用户的对话历史中,语义上与 "我应该看什么运动?" 最相关的是 "我最喜欢的运动是足球" 这个对话。更复杂一点可以通过conversationchain进行多轮对话:
llm = OpenAI(temperature=0) # 可以是任何有效的LLM
_DEFAULT_TEMPLATE = """以下是一个人类与AI之间的友好对话。AI非常健谈,并从其上下文中提供大量具体细节。如果AI不知道问题的答案,它会诚实地说不知道。
之前对话的相关部分:
{history}
(如果不相关,您不需要使用这些信息)
当前对话:
人类:{input}
AI:"""
PROMPT = PromptTemplate(
input_variables=["history", "input"], template=_DEFAULT_TEMPLATE
)
conversation_with_summary = ConversationChain(
llm=llm,
prompt=PROMPT,
# 我们为测试目的设置了一个非常低的max_token_limit。
memory=memory,
verbose=True
)
conversation_with_summary.predict(input="嗨,我叫Perry,你好吗?")
# 输出:"> Entering new ConversationChain chain...
# Prompt after formatting:
# ...
# > Finished chain.
# " 嗨,Perry,我很好。你呢?"
# 这里,与篮球相关的内容被提及
conversation_with_summary.predict(input="我最喜欢的运动是什么?")
# 输出:"> Entering new ConversationChain chain...
# ...
# > Finished chain.
# ' 你之前告诉我你最喜欢的运动是足球。'"
# 尽管语言模型是无状态的,但由于获取到了相关的记忆,它可以“推理”出时间。
# 为记忆和数据加上时间戳通常是有用的,以便让代理确定时间相关性
conversation_with_summary.predict(input="我的最喜欢的食物是什么?")
# 输出:"> Entering new ConversationChain chain...
# ...
# > Finished chain.
# ' 你说你最喜欢的食物是披萨。'"
# 对话中的记忆被自动存储,
# 由于这个查询与上面的介绍聊天最匹配,
# 代理能够“记住”用户的名字。
conversation_with_summary.predict(input="我的名字是什么?")
# 输出:"> Entering new ConversationChain chain...
# ...
# > Finished chain.
# ' 你的名字是Perry。'"
conversation_with_summary这个实例使用了一个内存对象(memory)来存储与用户的对话历史。这使得AI可以在后续的对话中引用先前的上下文,从而提供更准确和相关的回答。
在Langchain中memory属于较为简单的一模块,小型开发中常常使用summary类型,对于大一点的开发来说,最常见的就是利用向量数据库进行数据的存储,并在ai模型给出输出时到该数据库中检索出相似性最高的内容。
六、Chains
如果把用Langchain构建AI应用的过程比作“积木模型”的搭建与拼接,那么Chain可以说是该模型搭建过程中的骨骼部分,通过它将各模块快速组合在一起就可以快速搭建一个应用。Chain的使用方式也是通过接口的直接调用,在本文中将Chain分为三种类型,从简单到复杂依次介绍按照首先以一个简单的示例,来直观的感受Chain的作用:
6.1 LLMChains:
这种类型的Chain应用起来很简单也可以说是后续要介绍的Chain的基础,但其功能是足够强大的。通过LLMChain可以直接将数据、prompt、以及想要应用的Model串到一起,以一个简单的例子来感受LLMChain。
from langchain import PromptTemplate, OpenAI, LLMChain
prompt_template = "What is a good name for a company that makes {product}?"
llm = OpenAI(temperature=0)
chain = LLMChain(
llm=llm,
prompt=PromptTemplate.from_template(prompt_template)
)
print(chain("colorful socks"))
# 输出结果'Socktastic!'
在这个示例中,我们首先初始化了一个prompt的字符串模版,并初始化大语言模型,然后利用Chain将模型运行起来。在「Chain将模型运行起来」这个过程中:Chain将会格式化提示词,然后将它传递给LLM。回忆一下,在之前的ai入门篇中,对于每个model的使用,我们需要针对这个model去进行一系列初始化、实例化等操作。而用了chain之后,我们无需再关注model本身。
6.2 Sequential Chains:
不同于基本的LLMChain,Sequential chain(序列链)是由一系列的链组合而成的,序列链有两种类型,一种是单个输入输出/另一个则是多个输入输出。先来看第一种单个输入输出的示例代码:
1.单个输入输出
在这个示例中,创建了两条chain,并且让第一条chain接收一个虚构剧本的标题,输出该剧本的概要,作为第二条chain的输入,然后生成一个虚构评论。通过sequential chains可以简单的实现这一需求。
第一条chain:
# This is an LLMChain to write a synopsis given a title of a play.
from langchain import PromptTemplate, OpenAI, LLMChain
llm = OpenAI(temperature=.7)
template = """You are a playwright. Given the title of play, it is your job to write a synopsis for that title.
Title: {title}
Playwright: This is a synopsis for the above play:"""
prompt_template = PromptTemplate(input_variables=["title"], template=template)
synopsis_chain = LLMChain(llm=llm, prompt=prompt_template)
第二条chain:
# This is an LLMChain to write a review of a play given a synopsis.
from langchain import PromptTemplate, OpenAI, LLMChain
llm = OpenAI(temperature=.7)
template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play.
Play Synopsis:
{synopsis}
Review from a New York Times play critic of the above play:"""
prompt_template = PromptTemplate(input_variables=["synopsis"], template=template)
review_chain = LLMChain(llm=llm, prompt=prompt_template)
最后利用SimpleSequentialChain即可将两个chain直接串联起来:
from langchain.chains import SimpleSequentialChain
overall_chain = SimpleSequentialChain(chains=[synopsis_chain, review_chain], verbose=True)
print(review = overall_chain.run("Tragedy at sunset on the beach"))
可以看到对于单个输入输出的顺序链,就是将两个chain作为参数传给simplesequentialchain即可,无需复杂的声明。
2.多个输入输出
除了单个输入输出的模式,顺序链还支持更为复杂的多个输入输出,对于多输入输出模式来说,最应该需要关注的就是输入关键字和输出关键字,它们需要十分的精准,才能够保证chain的识别与应用,依旧以一个demo为例:
from langchain import PromptTemplate, OpenAI, LLMChain
llm = OpenAI(temperature=.7)
template = """You are a playwright. Given the title of play and the era it is set in, it is your job to write a synopsis for that title.
Title: {title}
Era: {era}
Playwright: This is a synopsis for the above play:"""
prompt_template = PromptTemplate(input_variables=["title", 'era'], template=template)
synopsis_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="synopsis")
#第一条chain
from langchain import PromptTemplate, OpenAI, LLMChain
llm = OpenAI(temperature=.7)
template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play.
Play Synopsis:
{synopsis}
Review from a New York Times play critic of the above play:"""
prompt_template = PromptTemplate(input_variables=["synopsis"], template=template)
review_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="review")
#第二条chain
from langchain.chains import SequentialChain
overall_chain = SequentialChain(
chains=[synopsis_chain, review_chain],
input_variables=["era", "title"],
# Here we return multiple variables
output_variables=["synopsis", "review"],
verbose=True)
#第三条chain
overall_chain({"title": "Tragedy at sunset on the beach", "era": "Victorian England"})
对于每一个chain在定义的时候,都需要关注其output_key 、和input_variables,按照顺序将其指定清楚。最终在运行chain时我们只需要指定第一个chain中需要声明的变量。
6.3RouterChains:
最后介绍一个经常会用到的场景,比如我们目前有三类chain,分别对应三种学科的问题解答。我们的输入内容也是与这三种学科对应,但是随机的,比如第一次输入数学问题、第二次有可能是历史问题... 这时候期待的效果是:可以根据输入的内容是什么,自动将其应用到对应的子链中。Router Chain就为我们提供了这样一种能力,它会首先决定将要传递下去的子链,然后把输入传递给那个链。并且在设置的时候需要注意为其设置默认chain,以兼容输入内容不满足任意一项时的情况。
physics_template = """You are a very smart physics professor. \
You are great at answering questions about physics in a concise and easy to understand manner. \
When you don't know the answer to a question you admit that you don't know.
Here is a question:
{input}"""
math_template = """You are a very good mathematician. You are great at answering math questions. \
You are so good because you are able to break down hard problems into their component parts, \
answer the component parts, and then put them together to answer the broader question.
Here is a question:
{input}"""
如上有一个物理学和数学的prompt:
prompt_infos = [
{
"name": "physics",
"description": "Good for answering questions about physics",
"prompt_template": physics_template
},
{
"name": "math",
"description": "Good for answering math questions",
"prompt_template": math_template
}
]
然后,需要声明这两个prompt的基本信息。
from langchain import ConversationChain, LLMChain, PromptTemplate, OpenAI
llm = OpenAI()
destination_chains = {}
for p_info in prompt_infos:
name = p_info["name"]
prompt_template = p_info["prompt_template"]
prompt = PromptTemplate(template=prompt_template, input_variables=["input"])
chain = LLMChain(llm=llm, prompt=prompt)
destination_chains[name] = chain
default_chain = ConversationChain(llm=llm, output_key="text")
最后将其运行到routerchain中即可,我们此时在输入的时候chain就会根据input的内容进行相应的选择最为合适的prompt。
from langchain.chains.router.llm_router import LLMRouterChain, RouterOutputParser
from langchain.chains.router.multi_prompt_prompt import MULTI_PROMPT_ROUTER_TEMPLATE
# Create a list of destinations
destinations = [f"{p['name']}: {p['description']}" for p in prompt_infos]
destinations_str = "\n".join(destinations)
# Create a router template
router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(destinations=destinations_str)
router_prompt = PromptTemplate(
template=router_template,
input_variables=["input"],
output_parser=RouterOutputParser(),
)
router_chain = LLMRouterChain.from_llm(llm, router_prompt)
chain = MultiPromptChain(
router_chain=router_chain,
destination_chains=destination_chains,
default_chain=default_chain,
verbose=True,
)
print(chain.run('什么是黑体辐射'))
作者:腾讯程序员
文章来源:腾讯技术工程
推荐阅读
更多腾讯AI相关技术干货,请关注专栏腾讯技术工程 欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。