当前位置:AIGC资讯 > AIGC > 正文

大模型技术实践(三)|用LangChain和Llama 2打造心灵疗愈机器人

上期文章我们实现了Llama 2-chat-7B模型的云端部署和推理,本期文章我们将用“LangChain+Llama 2”的架构打造一个定制化的心灵疗愈机器人。有相关知识背景的读者可以直接阅读「实战」部分。



01 背景


1.1 微调 vs. 知识库


由于大模型在垂直行业领域的问答效果仍有待提升,因此,领域知识的注入成为了最直接的解决方案之一。知识注入方法可以分为领域微调(Fine-tuning)和外挂知识库(Knowledge Base)两种。


1. 领域微调

微调是通过少量特定用例的增量数据对基础模型进行进一步训练,改变其神经网络中的参数权重。微调适用于任务或域定义明确,且有足够的标记数据的场景,比如风格微调。目前常用的微调方法包括Freeze,P-tuning和LoRA,相关细节会在下期文章中详细介绍。


然而,微调方法的不足之处在于:

▪ 高质量训练数据集的构建,微调训练所需的算力以及微调模型定期更新等开销都不容小觑

▪ 试错成本较高,特定领域数据一般难以覆盖模型已学到的参数,且可能会导致模型其他下游任务的表现下降


2. 外挂知识库

外挂知识库的本质在于不修改基座模型参数,通过提示词工程(Prompt Engineering)将特定知识作为prompt中的context,即召回相关性最高的几个文档,让模型分析这些蕴含知识后,并返回答案。知识库适合要求输出明确且精度高的任务。


相对于微调,知识库的优势在于:

▪ 回答精确度更高,基于相关文档中的最相关特定段落进行语义搜索能消除查询歧义以生成更精确的答案

▪ 适应性更强,用户可以通过轻松更新信息源来调整和适配新的领域

但大模型上下文窗口长度的限制和Prompt的构造等因素带来的潜在精度下降也需要纳入知识库构建的考量。


为了打造特定领域(Domain-specific Knowledge)的知识问答系统,我们需要借助提供了外挂知识库的搜索方案LangChain框架。


1.2 LangChain模块


LangChain是一个由语言模型驱动的用于开发应用程序的框架。LangChain主要的两个能力是:

▪ Data-aware:将不同数据源接入到语言模型中

▪ Agentic:允许语言模型和LangChain环境交互


LangChain的核心模块包括Models,Prompts,Chains,Indexes,Agents等 [1]。对于每一个模块,LangChain都提供了标准化的可拓展接口。


图1:LangChain部分模块 [2]


除了用LLM Wrapper可以接入众多的大模型(如 OpenAI、Cohere、Hugging Face),LangChain同时也通过VectorStore Wrapper接口集成了主流的向量数据库(如 Milvus、Pinecone、Chroma等)来优化语义搜索。


LangChain能接入的数据类型涵盖了文本、PPT、图片、HTML、Pdf等非结构化文件。相较于传统数据库的精确搜索,即完全匹配,向量数据库使用最邻近(Approximate Nearest Neighbor,ANN)算法和相似度度量(如余弦相似度,内积等)来找到和查询问题最相似的向量。


基于本地知识库问答的大致流程如下:

加载文档 -> 文本拆分 -> 根据question/query语义检索匹配文本 -> 构建prompt -> LLM生成回答


这里以Milvus数据库和ChatGPT作为示例:

图2:LangChian + Milvus + ChatGPT pipeline [3]

 

02 实战


目前,我们已经拆解完了LangChain+LLM文档问答的大致链路,接下来我们正式进入实战环节。


2.1 环境搭建


a. 安装LangChain

确保Python 版本≥ 3.8.1 且 <4.0。

pip install langchain


b.&nbsp;部署LLama 2

▪&nbsp;关于Llama 2模型的部署,详情可参见上期文章《大模型技术实践(二)|关于Llama 2你需要知道的那些事儿》


▪ UCloud官方的“LLaMA2 模型快速部署”文档:https://docs.ucloud.cn/gpu/practice/LLaMA2?id=llama2-模型快速部署


c.&nbsp;下载Embedding 模型

这里我们选择text2vec-large-chinese [4]这个Embedding模型,下载地址为:

https://huggingface.co/GanymedeNil/text2vec-large-chinese


对于中文的场景,也有其他优秀的开源模型可供选择,如m3e和bge等[5]。


d.&nbsp;下载数据集

心灵鸡汤文本数据集:https://huggingface.co/datasets/soulteary/warm-chicken-soup/


这个数据集是从Google网页上爬取的一些心灵鸡汤引用短文,共包含631条文本

&nbsp;

2.2 文档解析


a.&nbsp;加载数据集

LangChain对于不同格式的数据源内置了不同的解析脚本,最终这些数据都将转换为纯txt文本格式,以实现文本标准化。

from langchain.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

loader = UnstructuredFileLoader("数据集存放地址") &nbsp;
docs = loader.load()


b.&nbsp;文本切分

文本切分中的chunk_size指定了切分后的文本块的字数,chunk_overlap指定了切分文本块之间的重叠字数。由于鸡汤引用文本总长度较短,且文本内部语义关联度高,所以这里的chunk_size设置为50,chunk_overlap设置为20。

text_splitter = RecursiveCharacterTextSplitter(chunk_size=50,chunk_overlap=20)
docs = text_splitter.split_documents(docs)


c.&nbsp;文本嵌入和向量库

文本切分后,我们需要将文本进行向量化表示,将其映射为低维稠密的向量并存储到然向量数据库中。向量数据库选用了无需注册的FAISS。

from langchain.embeddings.huggingface import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS

# 导入向量模型
import os
embeddings = HuggingFaceEmbeddings(
&nbsp;&nbsp;&nbsp;&nbsp;model_name = "{你的地址}/text2vec-large-chinese",
&nbsp;&nbsp;&nbsp;&nbsp;model_kwargs = {'device': 'cuda'})

# 如果没有本地faiss仓库,先读取doc向量库,再将向量库保存到本地
if os.path.exists("{你的地址}/my_faiss_store.faiss") == False:
&nbsp;&nbsp;&nbsp;&nbsp;vector_store = FAISS.from_documents(docs,embeddings)
&nbsp;&nbsp;&nbsp;&nbsp;vector_store.save_local("{你的地址}/my_faiss_store.faiss")
# 如果faiss仓库已存在,则直接读取
else:
&nbsp;&nbsp;&nbsp;&nbsp;vector_store = FAISS.load_local(
&nbsp;&nbsp;&nbsp;&nbsp;"{你的地址}/my_faiss_store.faiss",
&nbsp;&nbsp;&nbsp;&nbsp;embeddings=embeddings)


2.3 加载模型


import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(
&nbsp;&nbsp;&nbsp;&nbsp;'/opt/Llama-2-7b-chat-hf',
&nbsp;&nbsp;&nbsp;&nbsp;trust_remote_code=True)

# 加载模型 Llama 2-chat-7B
base_model = AutoModelForCausalLM.from_pretrained(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"/opt/Llama-2-7b-chat-hf",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;torch_dtype=torch.float16,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;device_map='auto',
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;trust_remote_code=True
&nbsp;&nbsp;&nbsp;&nbsp;)
llm = base_model.eval()


2.4 语义检索


接下来,我就能根据构建好的向量数据库召回对应文本片段。


a.&nbsp;向量化召回

FAISS默认使用L2(欧式距离),召回的文档按照相似度结果从大到小排序。

query = "面对求职屡屡碰壁的大学生,请说一句话来鼓励他?"
docs = vector_store.similarity_search(query) # 计算相似度,并把相似度高的chunk放在前面
context = [doc.page_content for doc in docs] # 提取chunk的文本内容
print(context)


b.&nbsp;设置提示词模板

以下是Llama 2默认的提示词模板

&nbsp;#qa_template = """Use the following pieces of information to answer the user's question.
&nbsp;#If you don't know the answer, just say that you don't know, don't try to make up an answer.
&nbsp;#Context: {context}
&nbsp;#Question: {question}
&nbsp;#Only return the helpful answer below and nothing else.
&nbsp;#Helpful answer: """


我们可以参考上面的模板,根据场景定制化自己的模板来拼接Query和召回结果

context ="\n".join(context)
prompt = f"基于以上内容:\n{context} \n 请回答:{query} \n 字数限制在30字以内"


2.5 推理示例


我们对LLM的参数进行设置,例如最大令牌(max_new_tokens)、最高k值(top_k)、温度(temperature)和重复惩罚(repetition_penalty)等等。最后,将prompt喂给模型。

# 检查显存占用
nvidia-smi

inputs = tokenizer([f"Human:{prompt}\nAssistant:"], return_tensors="pt")
input_ids = inputs["input_ids"].to('cuda')

# llm参数设置
param_config = {
&nbsp;&nbsp;&nbsp;&nbsp;"input_ids":input_ids,
&nbsp;&nbsp;&nbsp;&nbsp;"max_new_tokens":1024,
&nbsp;&nbsp;&nbsp;&nbsp;"do_sample":True,
&nbsp;&nbsp;&nbsp;&nbsp;"top_k":5,
&nbsp;&nbsp;&nbsp;&nbsp;"top_p":0.95,
&nbsp;&nbsp;&nbsp;&nbsp;"temperature":0.1,
&nbsp;&nbsp;&nbsp;&nbsp;"repetition_penalty":1.3
}
result &nbsp;= llm.generate(**param_config)

answer = tokenizer.decode(result[0], skip_special_tokens=True)

print(answer)
# output&nbsp;example

# Q:面对求职屡屡碰壁的大学生,请说一句话来鼓励他?
# A:坚持不懈,机会终将降临


03&nbsp;外挂知识库的问题和优化


3.1 LLM+Embedding-Search的局限


外挂知识库将用户问题和本地知识向量化,比较两者的向量相似度(Vector Similarity)进行召回。然而,这种全量的Embedding-Search在面对多知识点聚合处理的场景下,存在召回精度低的问题。因为知识库的构建是对单个知识点进行索引,而非对不同知识点的排列组合分别索引。


居里夫人的出生年月 -> 单索引
居里夫人、爱因斯坦、奥本海默的出生年月 -> 组合索引

Q: 居里夫人、爱因斯坦和奥本海默三人中谁最早出生?


为了避免召回遗漏,直观的处理方法包括降低相似度阈值(similarity score threshold)和增加召回数量(top_k),但这不免会引入无关的知识点噪声且增加和LLM交互的token开销。


3.2 效果优化方向


意图识别和召回优化

提升问答系统的精度可以从意图识别和召回优化两个角度考虑,且两者都可以用关键词表示,即从直接将用户query和知识点进行embedding转变为对两者提取关键词后再进行匹配。意图识别可以通过关键词提取(Information Extraction, IE)和槽位填充(Slot Filling,SF)实现。


1.&nbsp;关键词提取


a.&nbsp;面向query——槽位填充

利用LLM思维链(Chain-of-Thought,COT)的提示能力来引导用户多轮对话并进行信息总结。针对我们的心灵疗愈机器人的场景,比如用户查询心灵鸡汤的句子,那么就要求用户的提供年龄段,情绪问题和情感需求等信息。语义槽格式如下:

&nbsp; &nbsp;"心灵鸡汤" : {&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"用户年龄段" : ____, # 青年,中年,老年
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"情绪问题" : ____, # 焦虑,失恋
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"情感需求" : ____, # 寻求安慰,寻求激励
&nbsp;&nbsp;&nbsp;&nbsp;}


b.&nbsp;面向知识点——索引入口


对于知识点可以从以下两个方面考虑:

i. 对相同知识点建立多级索引,有助于实现对维度查询。比如对一位奥运冠军的姓名,竞赛项目,年龄,获奖时间等分别建立索引。

ii. 将知识库转化为以关系三元组为核心的知识图谱。三元组的抽取除了传统的命名实体识别(NER)等方法,也可以通过prompt让大模型来进行抽取。


基于关键词的embedding入库和搜索流程如下:


2.&nbsp;多路召回

类似于Bert时代的垂直领域问答系统,我们可以将语义检索和传统的Elasticsearch(ES)关键词搜索并行,对两者进行加权打分投票来获取最终的top_k。



目前类似于以上优化思路已经落地的有“智海-录问”法律大模型 [6],其基座模型为Baichuan-7B。智海-录问知识增强的完整链路如图3。值得注意的是,智海-录问在知识库中对每一个知识点是以 [key, value] pair 形式存储的。key是知识点的内容简介,用于检索;value是知识点的具体内容,用于模型输入。实现细节请参照其Hugging Face仓库。


&nbsp;

图3:“智海-录问”知识增强链路


其他优化方向

除了Embedding部分,“LangChain+LLM”(图2)链路内的其他组件也有进一步优化的空间:


1.&nbsp;知识库细化

当用户手动选择分区后,分区检索可以明显提高召回的精度。


图4:“智海-录问”的交互界面


2.&nbsp;文本切分方式

由于文本重叠(overlap)的大小没有统一标准,如何保证语义完整和连贯都需要不断测试。


3.&nbsp;提示词的质量

在提示词模板的设计上要增加明确约束条件的指令,减少大模型出现幻觉现象的几率。


4.&nbsp;大模型的选型

选择基座模型还是微调后的模型,以及对中文的支持程度的需求都需要结合下游场景进行判别。

&nbsp;

本期文章带你基于“LangChain+LLM”框架快速搭建了知识增强后的问答机器人——心灵疗愈师,并探讨了提升模型的内容理解和执行能力的潜在优化方向。


下期文章我们将深入解读目前主流的大模型微调技术,敬请期待~

更新时间 2024-02-01