第五期 LangChain学习
一:LangChain简介
LangChain是一套面向大模型的开发框架。在学习过程中要借鉴其思想,不要注重接口和实现(一直在完善迭代,大模型也在更新,都在往前走),他主要拥有 2 个能力:
- 可以将 LLM 模型与外部数据源进行连接
- 允许与 LLM 模型进行交互
(一)文档汇总
快速入门:https://www.alang.ai/langchain/101/ (文档较新24年的)
参考教程:https://liaokong.gitbook.io/llm-kai-fa-jiao-cheng (学习概念)
发展版本:https://github.com/langchain-ai/langchain/releases(从视频里面0.0.241--->0.2.5,变化不小的)
官方文档:https://python.langchain.com/v0.2/docs/how_to/
中文教程:https://python.langchain.com.cn/docs/
- Use cases(常用案例):https://python.langchain.com.cn/docs/use_cases
- integrations(三方集成):https://python.langchain.com/v0.2/docs/integrations/platforms/包括向量数据库、文档加载...
- api(接口文档):https://api.python.langchain.com/en/latest/langchain_api_reference.html (有点用,但是太乱,迭代太快)
(二)五大核心组件
1.模型I/O封装:包括模型封装和模型的输入/输出封装
- LLMS:大语言模型(生成式语言模型)
- Chat Models:一般基于 LLMS,但按对话结构重新封装(对话式语言模型)
- PromptTemple:提示词模板
- OutputParser:解析输出(文本、代码、json)
2.数据连接封装:调用大模型的时候经常需要和第三方数据交互,包括文档的加载、处理、检索、存储
- Document Loaders:各种格式文件的加载器(pdf、html、word...),当使用loader加载器读取到数据源后,数据源需要转换成 Document 对象后,后续才能进行使用。
- Document transformers: 对文档的常用操作,如:split, filter, translate, extract metadata等等,比如文档分割或者下面的向量化可以在一定程度上解决token长度限制
- Text Embedding Models:文本向量化表示,用于检索等操作
- Verctor stores:(面向检索的)向量的存储
- Retrievers: 向量的检索
补充向量的概念:因为数据相关性搜索其实是向量运算。所以,不管我们是使用 openai api embedding 功能还是直接通过向量数据库直接查询,都需要将我们的加载进来的数据
Document
进行向量化,才能进行向量运算搜索。转换成向量也很简单,只需要我们把数据存储到对应的向量数据库中即可完成向量的转换。embedding用于衡量文本的相关性。这个也是 OpenAI API 能实现构建自己知识库的关键所在。他相比 fine-tuning 最大的优势就是,不用进行训练,并且可以实时添加新的内容,而不用加一次新的内容就训练一次,并且各方面成本要比 fine-tuning 低很多。
3.记忆封装:文本上文、历史记录的管理
- Memory:这里不是物理内存,从文本的角度,可以理解为"上文”、“历史记录“或者说”记忆力”的管理
4. 架构封装:主要包括chain和agent两个概念
- Chain:实现一个功能或者一系列顺序功能组合
- Agent:根据用户输入,自动规划执行步骤,自动选择每步需要的chain/tool,最终完成用户指定的功能。
可以理解为agent可以根据动态的帮我们选择和调用chain或者已有的工具(agent核心就是可以根据需要去做推理选择合适的工具去处理)
其中工具分为:
- Tools:调用外部功能的函数,例如:调 google 搜索、文件 I/O、Linux Shell 等等
- Toolkits:操作某软件的一组工具集,例如:操作 DB、操作 Gmail 等等
5. Callbacks:可以自己做一些回调,包括消息监控、日志处理等
二:LangChain入门
langchain安装:pip install langchain-openai https://python.langchain.com/v0.2/docs/integrations/platforms/openai/
注意:安装后,我们需要在环境变量中配置OPENAI_API_KEY,langchain会自动获取
(一)模型及I/O的封装
1.模型的封装
- 指令生成式模型
from langchain_openai import OpenAI
llm = OpenAI(model_name="gpt-3.5-turbo-instruct")
res = llm.invoke("你好,欢迎")
print(res)
- 对话式生成模型
from langchain_openai import ChatOpenAI
chatModel = ChatOpenAI(model="gpt-3.5-turbo")
res = chatModel.invoke("你好,欢迎")
print(res)
from langchain_openai import ChatOpenAI
from langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage
)
messages = [
SystemMessage(content="你是AIGC课程的助理"),
HumanMessage(content="我来上课了")
]
chatModel = ChatOpenAI(model="gpt-3.5-turbo")
res = chatModel.invoke(messages) #chat_models默认input参数是一个prompt列表
print(res)
可以看到,无论是指令生成还是对话生成,都是统一调用invoke,不同模型是传递字符串还是列表,框架内部进行了处理。相比较教程里面之前的框架,现在使用起来更加简单,尤其在导入各个模块上表现更好。但是迭代太快,文档不太够用
2.I/O封装
- 输入封装:PromptTemplate https://blog.imkasen.com/langchain-prompt-templates/
指令生成prompt模板
from langchain.prompts import PromptTemplate
template = PromptTemplate.from_template("给我讲个关于{title}的笑话")
print(template.input_variables)
print(template.format(title="小米"))
对话生成prompt模板
from langchain.prompts import ChatPromptTemplate
template = ChatPromptTemplate.from_messages([
("system","你是一个{subject}课程助手"),
("human","{user_input}"),
])
prompt_value = template.invoke({
"subject":"AIGC",
"user_input":"我请假一次"
})
print(prompt_value)
- 输出封装:OutputParser,可以按照自定义格式进行解析
import json
from typing import List,Dict #用于泛型
from pydantic import BaseModel,Field,field_validator #Pydantic是一个用于数据建模/解析的Python库,具有高效的错误处理和自定义验证机制。
from langchain_openai import OpenAI
from langchain.prompts import PromptTemplate
from langchain.output_parsers import PydanticOutputParser
def chinses_friendly(string):
lines = string.split("\n")
for i,line in enumerate(lines):
if line.startswith("{") and line.endswith("}"):
try:
lines[i] = json.dumps(json.loads(line),ensure_ascii=False)
except:
pass
return '\n'.join(lines)
#定义输出格式
class Command(BaseModel):
command: str = Field(description="linux shell命令字")
arguments: Dict[str,str] = Field(description="命令字后面的参数(name:value)")
#开始对参数添加自定义校验机制
@field_validator("command")
def no_space(cls,info):
if " " in info or "\t" in info or "\n" in info:
raise ValueError("命令名不能包含特殊字符")
return info
parser = PydanticOutputParser(pydantic_object=Command)
query = "将系统日设置为2024-07-01"
prompt = PromptTemplate(
template="将用户的指令转化为linux命令.\n{format_instructions}\n{query}",
input_variables=["query"],
partial_variables={"format_instructions":parser.get_format_instructions()} #也可以直接赋值
)
model_input = prompt.format_prompt(query=query) #format()返回的是字符串,format_prompt返回的是promptValue
print("---------------prompt----------------")
print(model_input)
print(model_input.to_string())
print(chinses_friendly(model_input.to_string()))
model_name = "gpt-3.5-turbo-instruct"
temperature = 0.0
model = OpenAI(model_name=model_name,temperature=temperature) #可以看到,指令生成式也可以得到答案,根据前文prompt得到
output = model.invoke(model_input)
print("---------------output----------------")
print(output)
print("---------------parser----------------")
cmd = parser.parse(output)
print(cmd)
prompt查看
print("---------------prompt----------------")
print(model_input)
print(model_input.to_string())
print(chinses_friendly(model_input.to_string()))
上面是给大模型看的,压缩后乱,转string如下
但是内部的字符串中文被转换为ascii码,通过json解析,转换成中文
注意上面的输出解析Command类里面的description也是作为prompt一部分传递的。如果写的不好,那么解析就不对
正确输出:description是命令字后面的参数
错误输出:description是命令的参数
(二)数据连接封装:
load加载后成纯文本--->transform文本处理(可选)--->embed向量化处理--->store存储--->retrieve检索;最后将检索的结果放入prompt交由大模型处理
1.文档加载器(Document Loaders):各种格式文件的加载器(pdf、html、word...),当使用loader加载器读取到数据源后,数据源需要转换成 Document 对象后,后续才能进行使用。
pdf加载,pip install pypdf
from langchain.document_loaders import PyPDFLoader
loader = PyPDFLoader("./application.pdf")
pages = loader.load_and_split()
print(pages[0].page_content) #有些特殊的东西解析不出来,比如里面的图片
2.文档处理器(Document transformers): 对文档的常用操作,如:split, filter, translate, extract metadata等等,比如文档分割或者下面的向量化可以在一定程度上解决token长度限制
文档分割,textSplitter,在一定程度上还可以过滤掉一些无用的数据(分割完,找到相关联的部分传递给大模型,而不用全部传递过去)
#pip install langchain_community
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
loader = PyPDFLoader("./application.pdf")
pages = loader.load_and_split()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 200,
chunk_overlap = 50, #重叠字符串(0,200),(150,350),(300,500) ... 保证语义连续,每一部分的信息可以完整保留,不会因为截断丢失信息
length_function=len,
add_start_index = True,
)
paragraphs = text_splitter.create_documents([pages[0].page_content]) #注意传递数组过来,直接传递字符串会被转成多个字符串数组
print(paragraphs)
for para in paragraphs[:5]:
print(para.page_content)
print("------------------------")
3.文档向量化(Text Embedding Models):将目标物体(词、句子、文章)表示成向量的方法。
向量化便于判断两个物体的相似度,比如判断词距(快乐、高兴),词相关性(男人->国王,女人->王后)
- 词向量:基于单个词的向量化操作https://www.cnblogs.com/pinard/p/7160330.html
词向量的原理:用一个词的上下文窗口表示它自身;两种典型方法,CBOW模型通过上下文预测中心词,而Skip-gram模型则通过中心词预测上下文词;Word2Vec模型具有简单、高效、易于理解的特点,而且在大规模文本数据上表现出了良好的性能。它能够将语义相近的词语映射到相近的向量空间中,从而实现词语之间的语义关联。
词向量的不足:同一个词在不同上下文语意不同,我马上下来和我从马上下来;忽略了词语之间的上下文关系,无法捕捉到更复杂的语义信息。其次,Word2Vec模型无法处理词语的多义性,即一个词语可能有多个不同的含义,而Word2Vec只能将其映射到一个固定的向量表示
- 基于语句的向量化操作,根据语句去确定词的向量,同时也表示了整个语句https://www.sohu.com/a/731628300_121719205 BERT能够同时利用上下文信息和双向上下文信息,从而更好地捕捉词语之间的语义关系。https://www.cnblogs.com/wangxuegang/p/16896515.html ,BERT模型的主要输入是文本中各个字/词的原始词向量,该向量既可以随机初始化,也可以利用Word2Vector等算法进行预训练以作为初始值;输出是文本中各个字/词融合了全文语义信息后的向量
bert的输出是词向量,输出还是对应的词的词向量,但是输出的词向量融合了全文语义信息后的向量
只看这些信息bert模型和cbow似乎没有区别,实际上区别如下:https://blog.csdn.net/A496608119/article/details/129379364
通过bert,我们可以通过cls位符号的词向量作为文本的语义表示;也可以在外层再加一层pooler池化层提取关键信息作为文本语义表示。其中文本不再只包含句子,还可以包括篇章段落长文本输入。可以对句子、段落的相关性进行判断
exp1.根据向量判断相似度
import numpy as np
from numpy import dot
from numpy.linalg import norm
from langchain_openai import OpenAIEmbeddings
def cosine_similarity(a, b): # 余弦相似度
return dot(a,b)/(norm(a)*norm(b))
def l2(a,b): #欧式距离
x = np.asarray(a)-np.asarray(b)
return norm(x) #标准差
#也有针对各个语言的模型,只是这个通用性更好;缺点是text-embedding-ada-002的区分度不是很大,需要我们去合理选取阈值
query_embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
doc_embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
query = "国际争端"
documents = [
"联合国就苏丹达尔富尔地区大规模暴力事件发出警告",
"土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判",
"日本岐阜市陆上自卫队射击场内发生枪击事件3人受伤",
"国家游泳中心(水立方):恢复游泳、嬉水乐园等水上项目运营",
"我国首次在空间站开展舱外辐射生物学暴露实验",
]
query_vec = query_embeddings.embed_query(query)
doc_vecs = doc_embeddings.embed_documents(documents)
print("余弦相似度,越接近1月相似:")
for vec in doc_vecs:
print(cosine_similarity(query_vec, vec))
print("欧式距离,越小越相似:")
for vec in doc_vecs:
print(l2(query_vec, vec))
exp2.基于相似度进行聚类
K-means(距离)和DBSCAN聚类(密度)对比https://blog.csdn.net/weixin_47151388/article/details/137950257
DBSCAN的邻域半径取值:https://blog.csdn.net/Cyrus_May/article/details/113504879
#pip install scikit-learn
from langchain_openai import OpenAIEmbeddings
from sklearn.cluster import KMeans,DBSCAN
texts = [
"这个多少钱",
"啥价",
"给我报个价"
"我要红色的",
"不要了",
"算了",
"来红的吧",
"作罢",
"价格介绍一下",
"红的这个给我吧"
]
model = OpenAIEmbeddings(model="text-embedding-ada-002")
X = [] # 存放文本向量
for text in texts:
X.append(model.embed_query(text))
#对文本向量进行聚类
KMcluster = KMeans(n_clusters=3,random_state=41,n_init="auto").fit(X) #kmeans聚类
DBcluster = DBSCAN(eps=0.55,min_samples=2).fit(X) #dbscan聚类,这里没有去计算获取eps值,直接给了
for i,t in enumerate(texts):
print("KMeans: {}\t{}".format(KMcluster.labels_[i],t))
for i,t in enumerate(texts):
print("DBSCAN: {}\t{}".format(DBcluster.labels_[i],t))
4.向量的存储(与索引):Verctor stores(vector database)核心是索引
为什么需要存储?如果只是上面的几句话,简单循环即可,如果数据量达到上万,甚至更多,循环就不行了。这个时候就需要工具进行向量存储(索引)
向量存储做了什么工作?内部对向量之间的关系,类似于聚类的方式,建立了多级索引。比如内部10w个向量,不可能每次都对所有的数据循环计算余弦值(慢、算力不足),所以将向量按类似于结构化的方式建立多级索引,查询的时候按级查询(n->logn复杂度降低),快速检索到和query向量相关的那些向量
vectorDB核心是索引,还做了其他的工作,哪些数据存在内存、磁盘,怎么检索最快.....,这些都进行了封装
常用的向量检索:FAISS(facebook的开源框架,简单,可本地搭建pip,没有极高性能要求的话可以使用,也比较常用)、Pinecone(付费,云服务,易用,国内不行)、ES(支持向量检索、支持APU芯片加速、可优化、老牌生态好)
- FAISS安装及介绍 https://blog.csdn.net/raelum/article/details/135047797(后续查查基于开源faiss,改造自己的向量数据库的思路?)
- 向量存储使用
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
loader = PyPDFLoader("./test.pdf") #内容来自https://mp.weixin.qq.com/s/lKFenYUYoM_zTGvp6RQ4hw
pages = loader.load_and_split()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 200,
chunk_overlap = 50, #重叠字符串(0,200),(150,350),(300,500) ... 保证语义连续,每一部分的信息可以完整保留,不会因为截断丢失信息
length_function=len,
add_start_index = True,
)
paragraphs = text_splitter.create_documents([page.page_content for page in pages]) #注意传递数组过来,直接传递字符串会被转成多个字符串数组
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
db = FAISS.from_documents(paragraphs, embeddings)
query = "地址会变化么?"
docs = db.similarity_search(query)
print(docs[0].page_content)
可以看到检索到了最相似的段落。(单纯检索)
5.向量Retrievers: 向量的检索
前面的similarity_search是查询,向量检索是对search进行封装。和上面的区别不大,只是提取概念,和传统检索进行对比。
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
loader = PyPDFLoader("./test.pdf") #内容来自https://mp.weixin.qq.com/s/lKFenYUYoM_zTGvp6RQ4hw
pages = loader.load_and_split()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 200,
chunk_overlap = 50, #重叠字符串(0,200),(150,350),(300,500) ... 保证语义连续,每一部分的信息可以完整保留,不会因为截断丢失信息
length_function=len,
add_start_index = True,
)
paragraphs = text_splitter.create_documents([page.page_content for page in pages]) #注意传递数组过来,直接传递字符串会被转成多个字符串数组
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
db = FAISS.from_documents(paragraphs, embeddings)
retriever = db.as_retriever() #获取检索器
query = "地址会变化么?"
docs = retriever.get_relevant_documents(query)
print(docs[0].page_content)
传统检索:关键字加权检索(不用向量检索)
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.retrievers import TFIDFRetriever
loader = PyPDFLoader("./test.pdf") #内容来自https://mp.weixin.qq.com/s/lKFenYUYoM_zTGvp6RQ4hw
pages = loader.load_and_split()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 200,
chunk_overlap = 50, #重叠字符串(0,200),(150,350),(300,500) ... 保证语义连续,每一部分的信息可以完整保留,不会因为截断丢失信息
length_function=len,
add_start_index = True,
)
paragraphs = text_splitter.create_documents([page.page_content for page in pages]) #注意传递数组过来,直接传递字符串会被转成多个字符串数组
retriever = TFIDFRetriever.from_documents(paragraphs)
query = "地址会变化么?"
docs = retriever.get_relevant_documents(query)
print(docs[0].page_content)
可以看到不要向量检索的时候可能会错误,无法检索到对的。向量检索包含文本语义效果优于关键字检索
(三)记忆封装:Memory(多轮对话/任务之间需要上文信息)
1.对话上下文:
- ConversationBufferMemory 保留所有对话上下文
from langchain.memory import ConversationBufferMemory
history = ConversationBufferMemory()
history.save_context({"input": "你好啊"}, {"output": "你也好"})
print(history.load_memory_variables({})) #获取上下文 == print({history.memory_key: history.buffer})
history.save_context({"input":"你瞅啥"},{"output":"我瞅你咋地"})
print({history.memory_key: history.buffer})
- ConversationBufferWindowMemory 可以设置只保留指定轮数的对话上下文
from langchain.memory import ConversationBufferWindowMemory
history = ConversationBufferWindowMemory(k=2)
history.save_context({"input": "你好啊"}, {"output": "你也好"})
print(history.load_memory_variables({})) #获取上下文 == print({history.memory_key: history.buffer})
history.save_context({"input":"你瞅啥"},{"output":"我瞅你咋地"})
print({history.memory_key: history.buffer})
history.save_context({"input":"信不信我劈你瓜"},{"output":"信你个头儿,你劈个试试"})
print({history.memory_key: history.buffer})
2.自动对历史信息进行摘要,作为上下文
https://blog.csdn.net/dfBeautifulLive/article/details/133350653
from langchain.memory import ConversationSummaryMemory,ConversationSummaryBufferMemory
from langchain_openai import OpenAI
history = ConversationSummaryMemory(
llm=OpenAI(temperature=0), #default gpt-3.5-turbo-instruct temperature减少随机性,0减少随机但是不代表不随机
# buffer="以英文表示",
buffer="这个对话是基于一个卖瓜商人和暴躁顾客之间的交流",
)
history.save_context({"input": "你好啊"}, {"output": "你也好"})
print(history.load_memory_variables({})) #获取上下文 == print({history.memory_key: history.buffer})
history.save_context({"input":"你瞅啥"},{"output":"我瞅你咋地"})
print({history.memory_key: history.buffer})
history.save_context({"input":"信不信我劈你瓜"},{"output":"信你个头儿,你劈个试试"})
print({history.memory_key: history.buffer})
- 其中ConversationSummaryBufferMemory对比ConversationSummaryMemory,ConversationSummaryBufferMemory的功能就是对token进行限制,当对话的达到max_token_limit长度到多长之后,我们就应该调用 LLM 去把文本内容小结一下
- buffer可以看作chatModel中的system,设置一些背景/任务信息。会在任务中放在前面,提升重要性
(四)架构封装---chain链架构
- chain用于整合各个组件,封装成一个既定的流程(链条)
- chain原理就是设计模式里面的builder模式,用于解藕各种复杂的组件(重写某个组件不影响整体流程)https://www.cnblogs.com/ssyfj/p/9538292.html
1.一个简单的chain:描述使用prompt模板,去调用大语言模型的流程,串成一个chain
from langchain_openai import OpenAI
from langchain.prompts import PromptTemplate
#导入chain,串联上面两个模块
from langchain.chains import LLMChain
llm = OpenAI(model_name="gpt-3.5-turbo-instruct")
prompt = PromptTemplate(
input_variables=["product"],
template = "为生产{product}的公司取一个响亮的中文名字:"
)
chain = LLMChain(llm=llm, prompt=prompt)
print(chain.invoke("眼镜"))
但是现在开始废弃了模块导入,变成了下面方式串联流程成为chain(内部实际就是重写or方法,变成按流程执行串联):
from langchain_openai import OpenAI
from langchain.prompts import PromptTemplate
llm = OpenAI(model_name="gpt-3.5-turbo-instruct")
prompt = PromptTemplate(
input_variables=["product"],
template = "为生产{product}的公司取一个响亮的中文名字:"
)
chain = prompt | llm
print(chain.invoke("眼镜"))
2.在chain中加入memory,描述先调用history进行处理,再使用prompt模板,去调用大语言模型的流程,串成一个chain
https://python.langchain.com/v0.2/docs/how_to/message_history/
- 简单例子使用history
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate,MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
prompt = ChatPromptTemplate.from_messages([
("system","你是聊天机器人小瓜,你可以和人类聊天"),
MessagesPlaceholder(variable_name="memory"),
("human","{human_input}")
])
chatHistory = ChatMessageHistory()
llm = ChatOpenAI(model="gpt-3.5-turbo")
chain = prompt | llm
runnable_with_history = RunnableWithMessageHistory(
chain,
lambda session_id: chatHistory,
input_messages_key="human_input",
history_messages_key="memory",
)
def ChatBot(human_input):
res = runnable_with_history.invoke({
"human_input":human_input
},config={
"configurable":{"session_id":"1"}
})
chatHistory.add_user_message(human_input)
chatHistory.add_ai_message(res)
print("----------------------")
print(chatHistory.messages)
print("----------------------")
ChatBot("我是卢嘉锡,你是谁?")
ChatBot("我是谁?")
主要是使用了RunnableWithMessageHistory方法传递了history,在每次对话中,将对话历史写入history_messages_key占位中
- 使用数据库存储history
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate,MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import SQLChatMessageHistory
def get_session_history(session_id):
return SQLChatMessageHistory(session_id, connection="sqlite:///memory.db") #生成message_store这张表,seesion_id相当于一个普通key,一个seesion_id相当于一个单独对话
prompt = ChatPromptTemplate.from_messages([
("system","你是聊天机器人小瓜,你可以和人类聊天"),
MessagesPlaceholder(variable_name="memory"),
("human","{human_input}")
])
llm = ChatOpenAI(model="gpt-3.5-turbo")
chain = prompt | llm
runnable_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="human_input",
history_messages_key="memory",
)
res = runnable_with_history.invoke({
"human_input":"我是卢嘉锡,你是谁?"
},config={
"configurable":{"session_id":"1"}
})
print(res)
res = runnable_with_history.invoke({
"human_input":"我是谁?"
},config={
"configurable":{"session_id":"1"}
})
print(res)
res = runnable_with_history.invoke({
"human_input":"我是谁?"
},config={
"configurable":{"session_id":"1a"}
})
print(res)
- 如何实现自己的方式存储history(看源码注释)
from typing import List
from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate,MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.pydantic_v1 import BaseModel,Field
from langchain_core.chat_history import BaseChatMessageHistory
class InMemoryHistory(BaseChatMessageHistory, BaseModel):
messages: List[BaseMessage] = Field(default_factory=list)
def add_messages(self, messages: List[BaseMessage]) -> None:
self.messages.extend(messages)
print("-------------------")
print(self.messages)
print("-------------------")
def clear(self) -> None:
self.messages = []
# Here we use a global variable to store the chat message history.
# This will make it easier to inspect it to see the underlying results.
store = {}
def get_by_session_id(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryHistory()
return store[session_id]
prompt = ChatPromptTemplate.from_messages([
("system","你是聊天机器人小瓜,你可以和人类聊天"),
MessagesPlaceholder(variable_name="memory"),
("human","{human_input}")
])
llm = ChatOpenAI(model="gpt-3.5-turbo")
chain = prompt | llm
runnable_with_history = RunnableWithMessageHistory(
chain,
get_by_session_id,
input_messages_key="human_input",
history_messages_key="memory",
)
#对话同上
可以看到,我们可以定义自己的存储方式,只要是实现了特定的方法,返回了BaseChatMessageHistory对象即可。
- 如何使用上文提到的memory,比如ConversationSummaryMemory、ConversationBufferWindowMemory我们可以看到都是包含了BaseChatMessageHistory对象的,但是没有对应的方法和属性,需要我们去适配
重点是学习思想,参考官方文档,实现自己的summary https://python.langchain.com/v0.2/docs/how_to/chatbots_memory/#summary-memory
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate,MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables import RunnablePassthrough
prompt = ChatPromptTemplate.from_messages([
("system","你是聊天机器人小瓜,你可以和人类聊天"),
MessagesPlaceholder(variable_name="memory"),
("human","{human_input}")
])
llm = ChatOpenAI(model="gpt-3.5-turbo")
chain = prompt | llm
chatHistory = ChatMessageHistory()
runnable_with_history = RunnableWithMessageHistory(
chain,
lambda session_id: chatHistory,
input_messages_key="human_input",
history_messages_key="memory",
)
def summarize(chain_input): #进行小结,清空历史,写入小结。也可以使用封装好的ConversationSummaryMemory直接进行总结
messages = chatHistory.messages
if len(messages) == 0:
return False
summoryPrompt = ChatPromptTemplate.from_messages([
("placeholder", "{chat_history}"),
(
"user",
"Distill the above chat messages into a single summary message. Include as many specific details as you can.",
),
])
summaryChain = summoryPrompt | llm
summary_message = summaryChain.invoke({
"chat_history":messages
})
chatHistory.clear()
chatHistory.add_message(summary_message)
return True
runnable_with_summary_history = RunnablePassthrough.assign(messages_summarized=summarize) | runnable_with_history
def ChatBot(human_input):
res = runnable_with_summary_history.invoke({
"human_input":human_input
},config={
"configurable":{"session_id":"1"}
})
chatHistory.add_user_message(human_input)
chatHistory.add_ai_message(res)
print("----------------------")
print(chatHistory.messages)
print("----------------------")
ChatBot("我是卢嘉锡,你是谁?")
ChatBot("我是谁?")
其中RunnablePassthrough用于透传,将用户输入chain_input往后传递到下一个组件中去,并且可以把自己作为组件在其中执行一些操作https://blog.csdn.net/Attitude93/article/details/136531425
3.一个复杂一点的chain(Retrieval QA Chain):存储、检索、调用大模型生成结果。langchain已经封装了
# pip install langchainhub
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings,ChatOpenAI
from langchain.vectorstores import FAISS
from langchain.chains import retrieval
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain import hub #https://blog.csdn.net/qq_41185868/article/details/137847925
loader = PyPDFLoader("./test.pdf") #内容来自https://mp.weixin.qq.com/s/lKFenYUYoM_zTGvp6RQ4hw
pages = loader.load_and_split()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 200,
chunk_overlap = 50, #重叠字符串(0,200),(150,350),(300,500) ... 保证语义连续,每一部分的信息可以完整保留,不会因为截断丢失信息
length_function=len,
add_start_index = True,
)
paragraphs = text_splitter.create_documents([page.page_content for page in pages]) #注意传递数组过来,直接传递字符串会被转成多个字符串数组
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
db = FAISS.from_documents(paragraphs, embeddings)
llm = ChatOpenAI(model="gpt-3.5-turbo")
retrieval_qa_chat_prompt = hub.pull("langchain-ai/retrieval-qa-chat") #qa原语:Answer any use questions based solely on the context below:\n\n<context>\n{context}\n</context>。仅根据以下上下文回答任何使用问题
print(retrieval_qa_chat_prompt)
combine_docs_chain = create_stuff_documents_chain(
llm, retrieval_qa_chat_prompt
)
qaChain = retrieval.create_retrieval_chain(db.as_retriever(),combine_docs_chain)
query = "如何解决for range的时候它的地址发生变化"
res=qaChain.invoke({"input":query}) #注意和直接调用大语言模型不同,这里传递字典
print(res)
可以看到,通过embedding后,检索出来的是相关的paragraph,作为上下文,而不是全文上传,可以节省token
4.chain中的组件按顺序,和前面使用一样,代码中每个 "|" 前后的元素都可看作是一个Runnable,通过|将各个组件按顺序串行
from langchain.prompts import PromptTemplate
from langchain_openai import OpenAI
llm = OpenAI(temperature=0)
namePrompt = PromptTemplate(
input_variables=["product"],
template="为生成{product}的公司取一个好听的中文名字:",
)
nameChain = namePrompt | llm
sloganPrompt = PromptTemplate(
input_variables=["company_name"],
template="请给名字是{company_name}的公司取一个Slogan,输出格式是 name: slogan",
)
sloganChain = sloganPrompt | llm
overallChain = nameChain | sloganChain
print(overallChain.invoke("梳子"))
5.chain中使用预处理、后处理,用于对数据进行处理。这里用transform处理,实际上没必要,和前面用RunnablePassthrough一样即可实现https://python.langchain.com/v0.2/docs/tutorials/chatbot/
import re
from langchain.prompts import PromptTemplate
from langchain_openai import OpenAI
from langchain.chains.transform import TransformChain
def anonymize(inputs: dict) -> dict:
text = inputs["text"]
t = re.compile(r'1(3\d|4[4-9]|5([0-35-9]|6[67]|7[013-8]|8[0-9]|9[0-9]x)\d{8}])')
while True:
s = re.search(t, text)
if s:
text = text.replace(s.group(), "***********")
else:
break
return {"output_text":text}
transform_chain = TransformChain( #用于预处理
input_variables=["text"],output_variables=["output_text"],transform=anonymize
)
prompt = PromptTemplate(
template="根据下述橘子,提取候选人的职业和手机号:\n{output_text}\n输出json,以job、phone为key",input_variables=["output_text"]
)
llm = OpenAI(temperature=0)
overChain = transform_chain | prompt | llm
print(overChain.invoke({"text":"我叫张三,今年20岁,在北京做程序员。手机号是1334252564623"}))
主要是补充一下如何查找文档:
- 在这里查询函数https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.passthrough.RunnablePassthrough.html
- 拉到最下面就有案例,点击即可
6.chain如何进行路由(根据上一个组件结果调用对应的下一个组件)
https://python.langchain.com/v0.2/docs/how_to/routing/
from langchain.prompts import PromptTemplate
from langchain_openai import OpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda,RunnableBranch
chain = (
PromptTemplate.from_template(
"""根据用户的提问,判断他是关于"windows"、"linux"还是"other" 相关的脚本需求
只允许回答上面的分类,只输出分类
<question>
{question}
</question>
CLassfication
"""
)
| OpenAI(temperature=0)
| StrOutputParser()
)
linux_chain = PromptTemplate.from_template("""
你只会写Linux shell脚本。你不会写其他任何语言的程序,也不会写其他系统的脚本。
用户问题:
{question}
"""
) | OpenAI(temperature=0)
windows_chain = PromptTemplate.from_template("""
你只会写windows脚本。你不会写其他任何语言的程序,也不会写其他系统的脚本。
用户问题:
{question}
"""
) | OpenAI(temperature=0)
other_chain = PromptTemplate.from_template("""
根据用户的提问选取合适的脚本语言
用户问题:
{question}
"""
) | OpenAI(temperature=0)
# 方式1,使用自定义函数实现路由(可以自己在中间做一些事情,灵活性好)
def route(info):
print(info)
if "windows" in info["topic"].lower():
return windows_chain
elif "linux" in info["topic"].lower():
return linux_chain
else:
return other_chain
# 将用户的输入传入第一个字典中,里面每一个都相当于回调,都会将输入传递进去
fullChain = {"topic":chain,"question":lambda X:X["question"]} | RunnableLambda(
route
)
res = fullChain.invoke({"question":"帮我在linux上写一个十分钟后关机的脚本"})
print(res)
#方式2:使用RunnableBranch,分支处理(不够灵活)
branchChain = RunnableBranch(
(lambda X: "linux" in X["topic"].lower(),linux_chain),
(lambda X: "windows" in X["topic"].lower(),windows_chain),
other_chain
)
fullChain = {"topic":chain,"question":lambda X:X["question"]} | branchChain
res = fullChain.invoke({"question":"帮我在linux上写一个十分钟后关机的脚本"})
print(res)
7.封装了API调用:通过封装的文档,结合chain实现调用api获取结果的能力
from langchain.chains.api import open_meteo_docs #文档
from langchain_openai import OpenAI
from langchain.chains.api.base import APIChain
chain = APIChain.from_llm_and_api_docs(OpenAI(),open_meteo_docs.OPEN_METEO_DOCS,verbose=True,limit_to_domains=["https://api.open-meteo.com"])
res = chain.invoke("北京今天气温")
print(res)
8.function calling的封装:langchain实现了标准的接口,不再像之前那么复杂
https://python.langchain.com/v0.2/docs/how_to/function_calling/
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage,ToolMessage
from langchain_openai import ChatOpenAI
#方式一:装饰器实现对函数的function calling封装,简单
@tool
def add(a: int, b: int) -> int:
"""Adds a and b."""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiplies a and b."""
return a * b
tools = [add, multiply]
llm = ChatOpenAI(model="gpt-3.5-turbo-0125")
llmTools = llm.bind_tools(tools)
query = "What is 3 * 12? Also, what is 11 + 49?"
messages = [HumanMessage(query)]
tools_msg = llmTools.invoke(messages) #返回要调用的方法
messages.append(tools_msg) #AIMessage
for tool_call in tools_msg.tool_calls:
selected_tool = {"add":add,"multiply":multiply}[tool_call["name"].lower()]
tool_output = selected_tool.invoke(tool_call["args"]) #本地调用
messages.append(ToolMessage(tool_output,tool_call_id=tool_call["id"]))
print(messages)
llmTools.invoke(messages)
当然,我们也可以按这种方式定义function calling,通过注释和description表示输出信息
9.基于Document的chains,在前面的3中已经介绍了一种stuff方式,下面补充一下其他方式
- stuff模式:将检索出来的文档全部拼在一起传递(不加工、纯填充),上下文信息更全,并且只调用一次语言模型
- refine模式,检索出来n个文档,每个文档调用一次语言模型,每次模型返回结果再和下一个文档拼接,然后再得到结果,以此类推得到最后结果。效果不咋地,会弱化前面的答案
- Map reduce模式:并发得到n个答案,然后拼接答案,最后调用大模型进行回答
- Map re-rank,并发,然后rank获取得分最高的答案回复(信息不足,评分也不足弥补)
(五)架构封装---agent智能体架构
1.什么是智能体(agent)
将大语言模型作为一个推理引擎。给定一个任务之后,智能体自动规划生成完成任务所需的步骤,执行相应动作(例如选择并调用工具),直到任务完成。
和chain的区别在于:chain是封装一个固定的流程,chain可以作为智能体里面的一个tool来使用;agent是对tool、任务的调度流程,可以根据需求判断得到分几步、使用什么工具去完成任务
2.先定义一些工具(tools):函数、三方api、chain或者agent的run()作为tool
#三方api
search = SerpAPIWrapper() #搜索引擎
tools = [ #一组工具,供选择
Tool.from_function(
func=search.run,
name="Search",
description="useful for when you need to answer questions about current events" #问一下实时的事情
)
]
#函数自定义
@tool("weekday")
def weekday(date_str:str) -> str:
"""Convert date to weekday name"""
d = parser.parser(date_str)
return calendar.day_name[d.weekday()]
#内部的封装好的
tools = load_tools(["serpapi"])
3.agent智能体类型:ReAct 想-》执行-》看结果
- 提供task任务
- llm自己推理, 先想看如何执行,调用工具执行,结果返回llm,基于这个结果再循环,直到任务完成
# pip install google-search-results
import calendar
import dateutil.parser as parser
from langchain_core.tools import tool
from langchain.agents import load_tools,AgentType,initialize_agent
from langchain_openai import OpenAI
#函数自定义
@tool("weekday")
def weekday(date_str:str) -> str:
"""Convert date to weekday name"""
d = parser.parser(date_str)
return calendar.day_name[d.weekday()]
#三方api
tools = load_tools(["serpapi"]) #https://serpapi.com/dashboard
tools += [weekday]
print(tools)
llm = OpenAI()
agent = initialize_agent(tools,llm,agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,verbose=True)
agent.invoke("周星驰生日那天是星期几")
4.Function Calling实现的agent,agent会帮你调用,不需要你自己去调用
agent=AgentType.OPENAI_FUNCTIONS
5.SelfAskWithSearch智能体。自己拆解信息(每一步先问自己需要什么信息然后获取结果),一步步去搜索结果。适合知识图谱关联搜索
agent=AgentType.SELF_ASK_WITH_SEARCH
6.Plan-and-Excute智能体(类似于autoGpt)
planner:根据输入规划任务,放入任务队列
excuter:每次从任务队列取一个任务执行,会调用agent和tool进行执行交互
(六)Callback回调:用于监测记录调用过程信息
继承基类,实现方法即可,包括各个时机的监控,llm开始,返回单个token、返回全部结果时候...