article / aiznoyer
LangChain工程实践
LangChain 实践
前言:
该文档为《LangChain 入门与避坑指北》的项目实践,实际上就是将
ChatGLM+Langchain构建本地知识库,只需6G显存,支持实时上传文档
中的项目进行复现与注释~
1、本地知识库问答(文本)
python
import os
from langchain.chains import RetrievalQA # 导入 RetrievalQA 类用于检索问答
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader
from langchain_community.vectorstores import Chroma
from langchain_chatglm3 import ChatGLM3 # 自定义 ChatGLM3 类用于加载模型
# 加载embeddings
embeddings_model_dict = {
"ernie-tiny": "nghuyong/ernie-3.0-nano-zh",
"ernie-base": "nghuyong/ernie-3.0-base-zh",
"text2vec": "GranymeNil/text2vec-large-chinese",
"text2vec2": "uer/sbert-base-chinese-nil",
"text2vec3": "shibing624/text2vec-base-chinese",
}
# 这里默认是同级目录下的 data 文件夹,将txt文件放入 data 文件夹即可
def load_document(directory="data"):
loader = DirectoryLoader(directory) # 加载文档
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=256, chunk_overlap=0)
split_docs = text_splitter.split_documents(documents) # 切分文档
print(split_docs[:2])
return split_docs
def load_embedding_mode(model_name="ernie-tiny"):
encode_kwargs = {"normalize_embeddings": False}
model_kwargs = {"device": "cuda:0"}
return HuggingFaceEmbeddings(
model_name=embeddings_model_dict[model_name],
encode_kwargs=encode_kwargs,
model_kwargs=model_kwargs
)
def store_chroma(docs, embeddings, persist_directory='VectorStore'):
# 存储 Chroma 向量化的文档
db = Chroma.from_documents(docs, embeddings, persist_directory=persist_directory)
db.persist()
return db
embeddings = load_embedding_mode('text2vec3') # 加载嵌入模型
if not os.path.exists("Vectorstore"):
documents = load_document() # 加载文档
db = store_chroma(documents, embeddings) # 存储向量化的文档
else:
# 加载已存在的向量化文档
db = Chroma(persist_directory='VectorStore', embedding_function=embeddings)
# 创建 ChatGLM3 实例
llm = ChatGLM3(endpoint_url="http://localhost:8000/", max_token=5000)
retriever = db.as_retriever() # 创建文档检索器
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type='stuff',
retriever=retriever
) # 装填至 chain
response = qa.invoke('你的数据库中有几本书?') # 使用 QA 模型对问题进行推理
print(response)
**注意:**这个不能 copy 即用,使用的是 GLM3 模型,可以换成其他本地模型如 GLM,从控制台查看引入包即可。
2、本地知识库问答(PDF)
- langchain_helper.py (实现方法的工具类)
python
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.indexes.vectorstore import VectorstoreIndexCreator
from langchain.vectorstores.chroma import Chroma
from langchain.indexes.vectorstore import VectorStoreIndexWrapper #
os.environ["OPENAI_API_BASE"] = 'https://~' # 设置模型接口位置
os.environ["OPENAI_API_KEY"] = 'sk-~' # 设置模型 KEY
file_path = "*.pdf" # PDF 文件路径
local_persist_path = "./vector_store" # 向量存储的本地持久化路径
# 获取索引路径
def get_index_path(index_name):
return os.path.join(local_persist_path, index_name)
# 加载PDF文档并保存索引
def load_pdf_and_save_index(file_path, index_name):
loader = PyPDFLoader(file_path) # 使用 PyPDFLoader 加载PDF文档
index = VectorstoreIndexCreator(vectorstore_kwargs={"persist_directory": get_index_path(index_name)}). \
from_loaders([loader]) # 创建向量存储索引
index.vectorstore.persist() # 持久化索引
# 加载索引
def load_index(index_name):
index_path = get_index_path(index_name) # 获取索引路径
embedding = OpenAIEmbeddings() # 使用 OpenAIEmbeddings 加载嵌入模型
vectordb = Chroma(
persist_directory=index_path,
embedding_function=embedding
) # 创建 Chroma 向量存储
return VectorStoreIndexWrapper(vectorstore=vectordb) # 返回向量存储索引的包装类
def query_index_lc(index, query):
# 使用 map_reduce 方法查询索引
ans = index.query_with_sources(query, chain_type="map_reduce")
return ans['answer']
- main.py (使用 fastapi 制作的简单接口交互)
python
from fastapi import FastAPI # 导入 FastAPI 框架
from typing import Annotated # 导入 Annotated 用于在类型提示中注释
from fastapi import File, Form, UploadFile
from langchain_helper import * # 导入 langchain_helper 中的函数
from pydantic import BaseModel # 导入 BaseModel 用于定义数据模型
import os
file_upload_path = "./uploads" # 文件上传路径
app = FastAPI() # 创建 FastAPI 应用
# 定义根路径的 GET 请求处理函数,返回一个简单的 JSON 响应
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.post("/upload")
def upload_file(file: Annotated[UploadFile, File()],
index_name: Annotated[str, Form(...)]):
# 指定文件上传目标路径
file_upload_target_path = os.path.join(file_upload_path, file.filename)
with open(file_upload_target_path, "wb") as f:
contents = file.file.read()
f.write(contents) # 将接收到的文件内容写入目标文件路径
# 调用 load_pdf_and_save_index 函数对上传的文件进行处理
load_pdf_and_save_index(file_upload_target_path, index_name)
# 返回上传文件的文件名和索引名
return {"filename": file.filename, "index_name": index_name}
class Query(BaseModel):
index_name: str
query: str
@app.post("/query")
def query_index(request: Query):
index_name = request.index_name
query = request.query
index = load_index(index_name) # 调用 load_index 函数加载索引
ans = query_index_lc(index, query) # 使用索引进行查询
# 返回查询结果、索引名和查询内容
return {"answer": ans, "index_name": index_name, "query": query}
3、网页新闻转相声
- html2news.py (网页新闻转相声工具类)
python
import os # 导入os模块
from langchain_community.document_loaders import UnstructuredURLLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains.summarize import load_summarize_chain
from langchain_openai import OpenAI, PromptTemplate, ChatOpenAI
from langchain.schema import HumanMessage
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
# 设置环境变量 OPENAI_API_BASE 和 OPENAI_API_KEY
os.environ["OPENAI_API_BASE"] = 'https://~'
os.environ["OPENAI_API_KEY"] = 'sk-~'
# 定义新闻的URL(这里是当天随便找的新浪新闻,实测知乎的文章不太好用)
url = "https://news.sina.com.cn/zx/gj/2024-01-27/doc-inaexruh3759336.shtml"
# 将URL转换为新闻文本
def url2News(url):
# 初始化 RecursiveCharacterTextSplitter 实例
# 从 “盛世 开始(包含),到 “看,这幅图 结束(不包含)
# 块大小为 1000,块重叠为 20,默认 len 方法计算块长度
text_splitter = RecursiveCharacterTextSplitter(separators=["“盛世", "“看,这幅图"], chunk_size=1000, chunk_overlap=20, length_function=len)
# 初始化 UnstructuredURLLoader 实例
loader = UnstructuredURLLoader([url])
# 加载并分割文本
data = loader.load_and_split(text_splitter)
return data[1:2]
# 将新闻文本转换为相声剧本
def news2script(news):
# 初始化 OpenAI 实例
llm = OpenAI(max_tokens=1500)
# 定义提示模板
prompt_template = """总结这段新闻的内容:
"{text}"
总结:"""
# 初始化 PromptTemplate 实例
chinese_prompt = PromptTemplate(template=prompt_template, input_variables=["text"])
# 加载相声剧本生成模型
chain = load_summarize_chain(llm, prompt=chinese_prompt)
# 运行模型生成相声剧本
summary = chain.invoke(news)
print(summary['output_text'])
# 初始化 ChatOpenAI 实例
openaichat = ChatOpenAI(model_name="gpt-3.5-turbo")
# 定义相声剧本模板
template = """\
我将给你一段新闻的概括,请按照要求把这段新闻改写成郭德纲和于谦的对口相声剧本。
新闻: "{新闻}"
要求: "{要求}"
{output_instructions}
"""
# 初始化 PydanticOutputParser 实例,设置相声的数据类型
parser = PydanticOutputParser(pydantic_object=XiangSheng)
# 初始化 PromptTemplate 实例
prompt = PromptTemplate(
template=template,
input_variables=["新闻", "要求"],
# 装载输出类型
partial_variables={"output_instructions": parser.get_format_instructions()}
)
# 创建人类消息列表
msg = [HumanMessage(
content=prompt.format(新闻=summary['output_text'],
要求="风趣幽默,十分讽刺,剧本对话角色为郭德纲和于谦,以他们的自我介绍为开头"))]
# 运行对话生成相声剧本
res = openaichat.invoke(msg)
print(res.content)
# 解析相声剧本
xiangsheng = parser.parse(res.content)
return xiangsheng
# 定义台词行数据模型
class Line(BaseModel):
character: str = Field(description="说这句台词的角色名字")
content: str = Field(description="台词的具体内容,其中不再包括角色的名字")
# 定义相声剧本数据模型
class XiangSheng(BaseModel):
script: List[Line] = Field(description="一段相声的台词剧本")
# 将URL转换为相声剧本
def url2xiangsheng(url):
doc = url2News(url)
xiangsheng = news2script(doc)
return xiangsheng
- interface.py (gradio 绘制的简易前端)
python
import gradio as gr # 导入 gradio 库
from html2news import * # 导入 html2news 模块,其中包含之前的函数和类定义
with gr.Blocks() as demo: # 创建 Gradio Blocks 应用
url = gr.Textbox() # 创建一个文本框组件,用于输入新闻的URL
chatbot = gr.Chatbot() # 创建一个聊天机器人组件
submit_btn = gr.Button("生成相声") # 创建一个按钮组件用于提交生成相声的请求
# 定义生成对话的函数
def generate_conversation(url):
# 调用之前定义的函数,将URL转换为相声剧本
xiangsheng: XiangSheng = url2xiangsheng(url)
chat_history = [] # 创建一个列表,用于存储对话内容
# 定义解析台词的函数
def parse_line(line: Line):
if line is None:
return ""
return f'{line.character}: {line.content}'
# 将生成的相声剧本转换为对话历史
for i in range(0, len(xiangsheng.script), 2):
line1 = xiangsheng.script[i]
line2 = xiangsheng.script[i + 1] if (i + 1) < len(xiangsheng.script)
else None
chat_history.append((parse_line(line1), parse_line(line2)))
return chat_history # 返回生成的对话历史
# 点击按钮时触发生成对话函数,将URL作为输入,将聊天机器人组件作为输出
submit_btn.click(fn=generate_conversation, inputs=url, outputs=chatbot)
if __name__ == '__main__':
demo.launch() # 运行 Gradio Blocks 应用
总结
希望上面三个简单的 Demo 能够帮助你更好的理解 LangChain 中各个模块的关系,以及一些基础的工程实践流程。复杂的项目往往只需要拆解成简单的需求,对着官方文档一顿操作即可~