助力数智化转型:使用检索增强生成【RAG】构建物业行业大模型

​本文作者:蔡冠杰,碧桂园服务后端开发高级工程师,拥有8年开发经验。

1 (RAG)检索增强生成技术介绍
1.1 检索增强生成是什么?
举个例子,比如我作为员工,直接问大模型:

提问:我们公司几点下班?

大模型在没有“接触”过我们公司《员工手册》等企业私有数据的情况下,大模型往往难以给出正确答案。RAG的能力在于能够让大模型结合企业私有数据,完成特定领域的知识问答。RAG把这类问题拆成两个步骤:

基于用户的提问,到《员工手册》里找到最相关的N个内容片段。

把用户提问和N个内容片段一起送到大模型中,获得答案。

上边这个例子“我们公司几点下班?”就变成:

提问:请参考员工手册,回答以下问题:我们公司几点下班?

员工手册内容如下:遵守上班时间不迟到、不早退......工作时间每周一至周五上午8:40到下午6:10......

大模型回复:

回复:下班时间是下午6点10分。

LLM是一种通过大量数据训练的模型,可以回答任何问题或完成任务,并利用其参数化记忆。然而,这些模型有一个知识截止日期,取决于它们上次训练的时间。比如当被问及超出其知识范围或在知识截止日期之后发生的事件时,模型会产生幻觉。

Meta公司的研究人员发现,通过提供与手头任务相关的信息,模型在完成任务时表现得到显著改善。例如,如果询问模型关于截止日期之后发生的事件,则提供该事件作为背景信息并随后提问,可帮助模型正确回答问题。

但由于LLM上下文窗口长度有限,所以在处理当前任务时只能传递最相关的知识,而且添加到上下文中的数据质量也会影响模型生成响应结果的质量。因此,机器学习从业者在RAG流程的不同阶段使用多种技术来改善LLM的性能。

1.2 RAG如何工作?
RAG架构和管道包括三个主要阶段:数据准备、检索和生成。

数据准备阶段:包括确定数据源、从数据源中提取数据、清理数据并将其存储到数据库中。

检索阶段:包括根据手头的任务从数据库中检索相关数据。

生成阶段:包括利用检索到的数据和手头的任务生成输出结果。

输出的质量取决于数据的质量和检索策略。下文将详细介绍每个阶段。

2 大模型结合RAG在企业知识库问答的落地
2.1 基于LangChain+LLM的本地知识库问答

2.1.1 什么是LangChain?

LangChain是一个开源的框架,旨在帮助开发人员使用语言模型构建端到端的应用程序。它提供了一套工具、组件和接口,可简化创建由大型语言模型 (LLM) 和聊天模型提供支持的应用程序的过程。LangChain可以轻松管理与语言模型的交互,将多个组件链接在一起,并集成额外的资源,例如API和数据库。

LangChain旨在为六个主要领域的开发人员提供支持:

LLM和提示:LangChain使管理提示、优化它们以及为所有LLM创建通用界面变得容易。此外,它还包括一些用于处理LLM 的便捷实用程序。
链(Chain):这些是对LLM或其他实用程序的调用序列。LangChain为链提供标准接口,与各种工具集成,为流行应用提供端到端的链。
数据增强生成:LangChain使链能够与外部数据源交互以收集生成步骤的数据。例如,它可以帮助总结长文本或使用特定数据源回答问题。
Agents:Agents 让LLM做出有关行动的决定,采取这些行动,检查结果,并继续前进直到工作完成。LangChain提供了代理的标准接口,多种代理可供选择,以及端到端的代理示例。
内存:LangChain有一个标准的内存接口,有助于维护链或代理调用之间的状态。它还提供了一系列内存实现和使用内存的链或代理的示例。
评估:很难用传统指标评估生成模型。这就是为什么LangChain提供提示和链来帮助开发者自己使用LLM评估他们的模型。

2.1.2 什么是LLM?
LLM(英文:Large Language Mode),也称大型语言模型,是一种人工智能模型,旨在理解和生成人类语言。它们在大量的文本数据上进行训练,可以执行广泛的任务,包括文本总结、翻译、情感分析等等。

LLM的特点是规模庞大,包含数千亿的参数,帮助它们学习语言数据中的复杂模式。这些模型通常基于深度学习架构,如转化器,这有助于它们在各种NLP任务上取得令人印象深刻的表现。

自ChatGPT为代表的大语言模型出现以来,由于其惊人的人工智能(AGI)的能力,自然语言处理领域掀起了新一轮的研究和应用的浪潮。特别是随着ChatGLM、LLaMA等较小规模的LLM开源,使得平民玩家也能够运行这些模型,业界涌现出了许多基于LLM的二次微调或应用的案例。

常见底座模型细节概览:

1、知识库构建

(1)文本加载和读取

加载文件:这是读取存储在本地的知识库文件的步骤。

读取文件:读取加载的文件内容,通常是将其转化为文本格式。

接入非结构化文档:

  • txt, .rtf, .epub, .srt
  • eml, .msg
  • html, .xml, .toml
  • json, .jsonl
  • md, .rst
  • docx, .doc, .pptx, .ppt, .odt
  • pdf
  • jpg, .jpeg, .png, .bmp

结构化数据接入:

  • csv, .tsv
  • xlsx, .xlsd

(2)文本分割

通常情况下,为了更有效地利用语言模型,需要将大型文本文档拆分为较小的块。文本拆分器负责将文档拆分为较小的文档片段。在理想情况下,我们希望将语义相关的文本片段放在一起。然而,“语义相关”的含义可能因文本类型而异。

根据规则:

根据中文文章的常见终止符号,利用规则进行文本分割。如:单字符断句符、中英文省略号、双引号等。

根据语义:

将文本拆分为语义上有意义的小块(通常是句子),然后开始将这些小块组合成一个较大的块,直到达到一定的大小(由某个函数测量)。一旦达到该大小,就将该块设置为自己的文本段,并开始创建一个具有一些重叠的新文本块。

依据中文标点符号设计的 ChineseTextSplitter

点击查看代码
class ChineseTextSplitter(CharacterTextSplitter):
...    
def split_text(self, text: str) -> List[str]:   ##此处需要进一步优化逻辑
    if self.pdf:
        text = re.sub(r"\n{3,}", r"\n", text)
        text = re.sub('\s', " ", text)
        text = re.sub("\n\n", "", text)

    text = re.sub(r'([;;.!?。!?\?])([^”’])', r"\1\n\2", text)  # 单字符断句符
    text = re.sub(r'(\.{6})([^"’”」』])', r"\1\n\2", text)  # 英文省略号
    text = re.sub(r'(\…{2})([^"’”」』])', r"\1\n\2", text)  # 中文省略号
    text = re.sub(r'([;;!?。!?\?]["’”」』]{0,2})([^;;!?,。!?\?])', r'\1\n\2', text)
    # 如果双引号前有终止符,那么双引号才是句子的终点,把分句符\n放到双引号后,注意前面的几句都小心保留了双引号
    text = text.rstrip()  # 段尾如果有多余的\n就去掉它
    # 很多规则中会考虑分号;,但是这里我把它忽略不计,破折号、英文双引号等同样忽略,需要的再做些简单调整即可。
    ls = [i for i in text.split("\n") if i]
    for ele in ls:
        if len(ele) > self.sentence_size:
            ele1 = re.sub(r'([,,.]["’”」』]{0,2})([^,,.])', r'\1\n\2', ele)
            ele1_ls = ele1.split("\n")
            for ele_ele1 in ele1_ls:
                if len(ele_ele1) > self.sentence_size:
                    ele_ele2 = re.sub(r'([\n]{1,}| {2,}["’”」』]{0,2})([^\s])', r'\1\n\2', ele_ele1)
                    ele2_ls = ele_ele2.split("\n")
                    for ele_ele2 in ele2_ls:
                        if len(ele_ele2) > self.sentence_size:
                            ele_ele3 = re.sub('( ["’”」』]{0,2})([^ ])', r'\1\n\2', ele_ele2)
                            ele2_id = ele2_ls.index(ele_ele2)
                            ele2_ls = ele2_ls[:ele2_id] + [i for i in ele_ele3.split("\n") if i] + ele2_ls[
                                                                                                   ele2_id + 1:]
                    ele_id = ele1_ls.index(ele_ele1)
                    ele1_ls = ele1_ls[:ele_id] + [i for i in ele2_ls if i] + ele1_ls[ele_id + 1:]

            id = ls.index(ele)
            ls = ls[:id] + [i for i in ele1_ls if i] + ls[id + 1:]
    return ls

(3)文本向量化(embedding)-存储到向量数据库

文本向量化(embedding):这通常涉及到NLP的特征抽取,可以通过诸如TF-IDF、word2vec、BERT等方法将分割好的文本转化为数值向量。

存储到向量数据库:文本向量化之后存储到数据库vectorstore(milvus)。

点击查看代码

#初始化
def _load_milvus(self, embeddings: Embeddings = None):
    if embeddings is None:
        embeddings = self._load_embeddings()
    self.milvus = Milvus(embedding_function=EmbeddingsFunAdapter(embeddings),
                         collection_name=self.kb_name, connection_args=kbs_config.get("milvus"))
#添加到向量数据库
def do_add_doc(self, docs: List[Document], **kwargs) -> List[Dict]:
    ids = self.milvus.add_documents(docs)
    doc_infos = [{"id": id, "metadata": doc.metadata} for id, doc in zip(ids, docs)]
    return doc_infos

(4)问句向量化

这是将用户的查询或问题转化为向量,应使用与文本向量化相同的方法,以便在相同的空间中进行比较。

2、知识库问答

(1)在文本向量中匹配出与问句向量最相似的top k个

这一步是信息检索的核心,通过计算余弦相似度、欧氏距离等方式,找出与问句向量最接近的文本向量。

点击查看代码
def query(self, q):
        #在向量数据库中查找与问句向量相似的文本向量
        vector_store = self.init_vector_store()
        docs = vector_store.similarity_search_with_score(q, k=self.top_k)
        for doc in docs:
            dc, s = doc
            yield s, dc

下面是similarity_search_with_score函数:

点击查看代码
def similarity_search_with_score(
    self,
    query: str,
    k: int = 4,
    param: Optional[dict] = None,
    expr: Optional[str] = None,
    timeout: Optional[int] = None,
    **kwargs: Any,
) -> List[Tuple[Document, float]]:
    """Perform a search on a query string and return results with score.

    For more information about the search parameters, take a look at the pymilvus
    documentation found here:
    https://milvus.io/api-reference/pymilvus/v2.2.6/Collection/search().md

    Args:
        query (str): The text being searched.
        k (int, optional): The amount of results to return. Defaults to 4.
        param (dict): The search params for the specified index.
            Defaults to None.
        expr (str, optional): Filtering expression. Defaults to None.
        timeout (int, optional): How long to wait before timeout error.
            Defaults to None.
        kwargs: Collection.search() keyword arguments.

    Returns:
        List[float], List[Tuple[Document, any, any]]:
    """
    if self.col is None:
        logger.debug("No existing collection to search.")
        return []

    # Embed the query text.
    embedding = self.embedding_func.embed_query(query)

    res = self.similarity_search_with_score_by_vector(
        embedding=embedding, k=k, param=param, expr=expr, timeout=timeout, **kwargs
    )
    return res

(2)匹配出的文本作为上下文和问题一起添加到prompt中

这是利用匹配出的文本来形成与问题相关的上下文,用于输入给语言模型。

点击查看代码
# 基于本地知识问答的提示词模版
PROMPT_TEMPLATE = """<指令>根据已知信息,简洁和专业的来回答问题。如果无法从中得到答案,请说 “根据已知信息无法回答该问题”,不允许在答案中添加编造成分,答案请使用中文。 </指令>

<已知信息>{{ context }}</已知信息>

<问题>{{ question }}</问题>"""
#组合提示模板和上下文
def knowledge_base_chat(query: str = Body(..., description="用户输入", examples=["你好"])
...
model = ChatOpenAI(
    streaming=True,
    verbose=True,
    callbacks=[callback],
    temperature=0,
    openai_api_key=llm_model_dict[model_name]["api_key"],
    openai_api_base=llm_model_dict[model_name]["api_base_url"],
    model_name=model_name,
    openai_proxy=llm_model_dict[model_name].get("openai_proxy")
)
docs = search_docs(query, knowledge_base_name, top_k, score_threshold)
context = "\n".join([doc.page_content for doc in docs])

input_msg = History(role="user", content=PROMPT_TEMPLATE).to_msg_template(False)
chat_prompt = ChatPromptTemplate.from_messages(
    [i.to_msg_template() for i in history] + [input_msg])
...

(3)提交给LLM生成回答

最后,将这个问题和上下文一起提交给语言模型(例如GPT系列),让它生成回答。

点击查看代码
#最后是调用langchain的LLMChain方法生成回答
model = ChatOpenAI(
    streaming=True,
    verbose=True,
    callbacks=[callback],
    temperature=0,
    openai_api_key=llm_model_dict[model_name]["api_key"],
    openai_api_base=llm_model_dict[model_name]["api_base_url"],
    model_name=model_name,
    openai_proxy=llm_model_dict[model_name].get("openai_proxy")
)
docs = search_docs(query, knowledge_base_name, top_k, score_threshold)
context = "\n".join([doc.page_content for doc in docs])

input_msg = History(role="user", content=PROMPT_TEMPLATE).to_msg_template(False)
chat_prompt = ChatPromptTemplate.from_messages(
    [i.to_msg_template() for i in history] + [input_msg])

chain = LLMChain(prompt=chat_prompt, llm=model)

这种结合langchain和LLM的方式,特别适合于一些垂直领域或大型集团企业搭建内部私有问答系统,以实现通过LLM的智能对话能力。此外,它也适合个人针对英文paper进行问答。

2.2 向量数据库
向量数据库(Vector Database)主要用来存储和处理向量数据。

向量是一种数据表示方式,用于描述对象的特征或属性。每个向量代表一个单独的数据点,例如一个词或一张图片,由描述其多个特性的值的集合组成。这些值有时被称为“特征”或“维度”。例如,一张图片可以表示为像素值的向量,整个句子也可以表示为单词嵌入的向量。

一些常用的数据向量如下:

图像向量:通过深度学习图像识别。

文本向量:通过词嵌入技术如Word2Vec、BERT等生成的文本特征向量,这些向量包含了文本的语义信息,可以用于文本分类、情感分析等任务。

语音向量:通过声学模型从声音信号中提取的特征向量,这些向量捕捉了声音的重要特性,如音调、节奏、音色等,可以用于语音识别。

原生的向量数据库是专门为存储和检索向量而设计的。包括Chroma, LanceDB, Marqo, Milvus/ Zilliz, Pinecone,Qdrant,Vald,Vespa, Weaviate等, 所管理的数据是基于对象或数据点的向量表示进行组织和索引。

Milvus是基于Faiss、Annoy、HNSW等向量搜索库构建的,可以轻松管理数百万个实体。它可以根据不同的数据特点选择最合适的索引算法,核心是解决稠密向量相似度检索的问题。除了向量检索,Milvus还支持数据分区分片、数据持久化、增量数据摄取、标量向量混合查询和time travel等功能。同时,它还大幅优化了向量检索的性能,可满足任何向量检索场景的应用需求。

Milvus还提供了多种客户端SDK,如Python、Java、C++等,使得用户可以方便地使用不同的编程语言来访问和操作 Milvus。

2.3 Embedding在大模型中的价值

2.3.1 Embedding模型的重要性
Embedding模型是一种将词、短语或整个句子转化为向量形式的技术,够捕捉到语义的丰富含义,使计算机可以像处理数字一样来处理文本。在大语言模型时代,Embedding模型可以帮助大模型处理更长的上下文,也可以将大模型与私有数据结合。它几乎可以用来表示任何事情,如文本、音乐、视频等,但在这里我们主要关注文本的Embedding。

Embedding模型的重要性在于它可以表示单词或语句的语义。实值向量的Embedding可以表示单词的语义,这是因为这些Embedding向量是根据单词在语言上下文中的出现模式进行学习的。例如,如果一个单词在某些上下文中经常与另一个单词一起出现,那么这两个单词的嵌入向量在向量空间中就会有相似的位置,这意味着它们具有相似的含义和语义。

2.3.2 主流Embedding模型
榜单地址:https://huggingface.co/spaces/mteb/leaderboard

2.4 Prompt提示工程
提示工程(Prompt Engineering)是自然语言处理领域的重要概念,指的是用于触发和引导人工智能语言模型生成特定输出的文本或语句片段。Prompt可以是一个单词、一个短语、一个问题或一个完整的句子,它们可以用来指导模型生成特定的回答、文本、摘要或其他输出。

例如,在语言模型GPT-3中,可以使用Prompt来引导模型生成特定的文本输出。输入“写一篇介绍自然语言处理的文章”时,可以使用Prompt来引导模型生成与自然语言处理相关的内容。

Prompt的选择和设计对于生成结果的质量和准确性至关重要。好的Prompt应该能够准确地表达模型期望生成的内容,同时也应该避免含糊或模糊的语言,以免导致生成结果不准确或不符合预期。

在一些场景下,Prompt也可以被用于调整模型的偏好或倾向,以生成更加符合用户需求的输出。例如,在情感分析任务中,可以使用Prompt来调整模型的情感倾向,以便获得更加准确的情感分析结果。

实际上,大多数的prompt由「指令」(instruction)和「内容」(content)两部分构成。其中,指令部分表示需要大模型执行的任务,如“判断下列句子的情感”,而内容则是实际的句子,例如“我今天很高兴”。

需要注意,并非所有的prompt都有这样的形式,例如比较简短的prompt:“中国的首都在哪里”、“模仿百年孤独的开头写一段话”等只有指令而没有内容的prompt。在这种情况下,我们可以认为内容是空,但上述构成依然适用。

2.5 基于大模型知识库实现物业行业的智能客服机器人

随着互联网的普及,越来越多的企业开始重视客户服务,以提升客户满意度。然而,传统的客服方式往往依赖于人力,效率低下,难以满足企业快速发展的需求。凤凰会APP是碧桂园服务线上服务平台,主要面向业主提供一站式服务。

目前,凤凰会APP采用人工客服的形式,通过电话或者IM在线回答问题。为了提高效率和响应速度,可以考虑将日常回答的问题整理成结构化Word文档(如下图所示),并导入知识库向量化。然后通过大模型来自动回答常见问题,同时支持客服快速检索。

此外,基于私有知识库的智能客服机器人,可以快速处理大量客户咨询,提高客服效率,节省人力成本,并实时响应客户需求,从而提高客户满意度。

2.5.1 后台知识库管理
后台知识库管理如下图所示:


知识库管理


编辑知识库


支持知识库检索


知识库文档管理


知识库文档内容

milvus向量数据库管理工具:

知识库文档向量化

2.5.2 前端机器人交互

上面的提问对应的知识库原问题答案是:

问题:装修垃圾清运费的收费标准是什么?

答案:业主进行室内装修,需交装修按金、装修工人卡费用等,具体请咨询物业工作人员。

2.6 基于大模型知识库实现物业行业的智能语音机器人

凤凰会APP是碧桂园物业服务的最大C端平台,为千万业主提供智能社区服务。通过该平台,业主可以足不出户,轻松解决生活需求。

凤凰会APP原先的语音助手是基于传统NLP实现的智能问答助手,需要准备大量的数据集,划分分词和配置意图,配置非常复杂,回答效果也不理想。

为了提高效率和准确性,可以考虑基于物业行业知识库实现的大模型语音机器人。这种机器人省去了复杂的配置过程,内置模板,只需导入结构化的知识文档,系统即可自动完成向量化并投入使用。这样,机器人可以更准确地识别用户的意图,提供更好的服务体验更好,持续为用户提供更优质的服务。

2.6.1 后台知识库管理

2.6.2 凤凰会APP端语音助手

3 总结
本文介绍了基于大语言模型的特定领域知识检索,并提出了以下优化方法来提高生成回答的效果:

  • 结构化的QA文档可以快速地检索到精准的答案。
  • 使用Embedding模型提升语义向量化的效果,以提高相似问题的匹配度,使得能够匹配出最满足要求的文本段落作为上下文。
  • 优化分词算法,使匹配出的结果作为上下文时能够提供更合理的推理/回答依据。
  • 优化LLM模型,使得给定提问相同情况下,得到更理想的推理/回答结果。
posted @ 2024-04-25 10:45  智在碧得  阅读(30)  评论(0编辑  收藏  举报