cover

小伙伴们新年好啊,颓废的 2023 年总算是过去了,过去这一年因为自己的状态不太好,一直也没怎么更新,2024 年是时候重新拾起行囊再出发啦!

前言

去年年底我写过一篇《大模型小助手,Mac 工程师如何拥有自己的人工智能》,在那篇文章里我介绍了如何利用自己手头的计算资源(Mac 电脑)快速拥有一个人工智能助手,然而大多数人手头的算力是很孱弱的,以至于大家千方百计搭桥建梯想要拿到 OpenAI 这艘大船的船票。这无可厚非,但我们知道,在我们这个伟大的国家,科技一定是要讲究自主研发的,不然谈何遥遥领先。因此在去年 8 月,随着《生成式人工智能服务管理暂行办法》的正式实施, 中国自人己的生成式人工智能之路,终于从政策上给出了要求和肯定,让 AIGC 行业发展不再迷茫。

现如今经历了一年多的发展,国产 AI 已经慢慢地走向成熟,其智能体的效果已经具备了产业应用场景落地的基本条件。因此今天我准备从自己的实际需求入手,抛弃 OpenAI,使用我们国内的 AI 平台,展示一下如何使用 LlamaIndex 框架和智谱 AI 结合起来处理常见的应用场景——知识库检索

大炼钢铁——国产大模型间的军备竞赛

ChatGPT 及其背后的 GPT4 大火之后,国内迅速刮起一阵自主研发大模型的风,先不管开源与否,目前市面上叫得上名号的就不止以下这些(排名不分先后):

机构/公司 模型名称
百度 文心大模型
抖音 云雀大模型
智谱 GLM 大模型
中国科学院 紫东太初大模型
百川智能 百川大模型
商汤 日日新大模型
MiniMax ABAB 大模型
上海人工智能实验室 书生通用大模型
科大讯飞 星火认知大模型
腾讯 混元大模型
阿里巴巴 通义千问大模型

吕布之后,人人皆有吕布之勇。 国产大模型亦是如此,GPT4 与大国政策双向奔赴后,这些 AI 厂商都想在国内大模型这场军备竞赛中占得一席之地。

这些 AI 厂商一般有三种提供服务的方式:

  • 模型私有部署:直接部署开源或者闭源的模型(需要显卡);
  • AI 开发平台:很多平台提供了 LLM 服务,可以在线进行模型测试、开发、部署、微调等服务(直接付费即可);
  • API 调用:这个就和 OpenAI 早期的服务模式一样,提供 API 的调用。

在上一篇文章中,我们选择了清华大学与智谱合作开发且开源的 ChatGLM3 作为私有化部署的模型。鉴于对其开源产品的丰富产品线以及较好的使用体验,这次我们仍然选择智谱 AI 作为本文的大模型底座。

2024 年 1 月 16 日,智谱 AI 发布了他们最新的大模型 ChatGLM4,性能全面比肩 GPT-4(乐观计算能达到 GPT-4 九成以上),并且在中文能力上超过了所有竞争对手,长文本能力也一骑绝尘。笔者体验下来效果非常好,初学者可以上手体验。

智谱 AI

智谱 AI 是基于清华大学 ChatGLM 系列大模型衍生出的 AI 产品,今天我们主要通过他的开放平台赋能 👉https://open.bigmodel.cn/

智谱 AI 大模型 MaaS 开放平台

进入开发工作台后,我们可以可以看到最新的 GLM-4 模型已经可供使用了,适用于复杂的对话交互和深度内容创作设计场景,今天我们主要用的也是这个模型。点击右上角,我们需要查看自己的 API key,新用户给了一个月 300 万额度的体验 token,基本够用了。

智谱 AI API KEY

点击「查看 API Key」 可以看到默认的 API Key,直接拷贝使用,或者创建新的 API Key,这个 key 我们后面会用到。

API Keys

API 的基本使用

这块的使用参考开发者后台里的接口文档,基本能了解个七七八八,这里就简单给一个使用的 demo 了。接口文档地址 👉https://open.bigmodel.cn/dev/api

首先安装 zhipuai:

pip install zhipuai

目前 zhipuai 的版本已经到了 2.0.1,和去年发布的 1.0.7 在 api 使用上是有一些差异的,需要关注一下。

写一个测试的例子:

from zhipuai import ZhipuAI
client = ZhipuAI(api_key="") # 填写您自己的APIKey
response = client.chat.completions.create(
    model="glm-4",  # 填写需要调用的模型名称
    messages=[
        {"role": "user", "content": "作为一名营销专家,请为我的产品创作一个吸引人的slogan"},
        {"role": "assistant", "content": "当然,为了创作一个吸引人的slogan,请告诉我一些关于您产品的信息"},
        {"role": "user", "content": "智谱AI开放平台"},
        {"role": "assistant", "content": "智启未来,谱绘无限一智谱AI,让创新触手可及!"},
        {"role": "user", "content": "创造一个更精准、吸引人的slogan"}
    ],
)
print(response.choices[0].message)

上面代码我们注意到,messages 参数是一个数组,其设计是天然针对对话、以及少样本提示的。

比如:

messages = [
    {"role": "user", "content": '提问1'},
    {"role": "assistant", "content": '回答1'},
    {"role": "user", "content": '提问2'},
    {"role": "assistant", "content": '回答2'},
    {"role": "user", "content": prompt},
]

AI 会跟去前面的对话历史,对最后的 messages 进行回答。

🤖:少样本提示(Few-shot prompting)是人工智能领域中的一个概念,特别是在自然语言处理(NLP)和机器学习模型训练中。所谓的“少样本”指的是在模型训练过程中,使用非常有限的数据样本对模型进行训练。这种方法要求模型能够从少量数据中快速学习和泛化,以便在新颖或未见过的数据上进行准确的预测。

知识库检索

大语言模型在专业领域回答缺乏依据、存在幻觉已经是一种共识,因此,为了在私域知识问答方面弥补通用大语言模型的一些短板,通常有两种解决方案:

  1. 微调(fine-tune):利用私有知识库对对 LLM 模型进行附加训练,以增加额外的知识
  2. 上下文学习( in-context learning):在 LLM 查询提示中添加一些额外的知识

据观察,目前由于上下文学习比微调更简单,所以上下文学习比微调更受欢迎,在这篇论文中讲述了这一现象:https://arxiv.org/abs/2305.16938。

对于上下文学习,我们通常采取的方案是将私域知识文档进行切片然后向量化,后续通过向量检索进行召回,再作为上下文输入到大语言模型进行归纳总结

例如,要构建一个可以回答关于某个人的任何问题,甚至扮演一个人的数字化化身的应用程序,我们可以将上下文学习应用于一本自传书籍和 LLM。在实践中,应用程序将使用用户的问题和从书中"搜索"到的一些信息构建提示,然后查询 LLM 来获取答案。

在这种搜索方法中,实现从文档/知识(上述示例中的那本书)中获取与特定任务相关信息的最有效方式之一是嵌入(Embedding)

嵌入(Embedding)通常指的是将现实世界的事物映射到多维空间中的向量的方法。例如,我们可以将图像映射到一个(64 x 64)维度的空间中,如果映射足够好,两个图像之间的距离可以反映它们的相似性。

而 LlamaIndex 就是目前使用较多的工具之一。LlamaIndex 是一个开源工具包,它能帮助我们以最佳实践去做 in-context learning:

  • 它提供了各种数据加载器,以统一格式序列化文档/知识,例如 PDF、维基百科、Notion、Twitter 等等,这样我们可以无需自行处理预处理、将数据分割为片段等操作。
  • 它还可以帮助我们创建嵌入(以及其他形式的索引),并以一行代码的方式在内存中或向量数据库中存储嵌入。
  • 它内置了提示和其他工程实现,因此我们无需从头开始创建和研究,例如,《用 4 行代码在现有数据上创建一个聊天机器人》

感兴趣的朋友可以去查看 LlmaIndex 官网 👉 https://docs.llamaindex.ai/en/stable/ 去了解更多信息。

LlmaIndex 官网

LlamaIndex

先用 Pip 简单安装一下 LlamaIndex:

pip install llama-index

然后我们导入下测试是否安装成功:

import llama_index
print(llama_index.__version__)
# 0.9.10

接下来让 LlamaIndex 使用 智谱 AI 作为大模型底座来实现基于知识库的问答。

LlamaIndex 是一个开源的,并且在海外十分流行的 in-context learning 工具,因此支持众多目前火热的大模型,只是很不凑巧,并没有 「智谱 AI」 以及国内的一众 AI 产品,任重道远啊。

好在,LlamaIndex 提供了非常灵活的接口,支持我们自定义一个可以集成的大模型 👉Customizing LLMs within LlamaIndex Abstractions

集成智谱 LLM

LlamaIndex 提供了一个叫做 CustomLLM 的抽象类来让我们方便的集成自己的大模型产品,只需要负责将文本传递给模型并返回新生成的标记即可。通过这种方式,我们可以结合一些本地模型(例如之前提到的 ChatGLM3),或者是一个 API 的包装器,当然了,如果为了获取完全私人隐私的大模型体验,肯定是本地模型优先。本文为了获得更高效的体验,直接集成智谱 AI 的 API 即可。

参考文档,我们只需要实现 metadatacomplete 以及 stream_complete 三个重要方法即可,熟悉 OpenAI API 的朋友想必一眼就看出来这三个是什么东西了。我们只需要参考智谱 AI 的文档依次实现即可。这里方便起见,就不实现 stream_complete 的方法,智谱 AI 官网有例子,参考即可。下面是一个简单的 demo👇

def invoke_prompt(prompt):
    response = client.chat.completions.create(
        model="glm-4",
        messages=[
            {"role": "user", "content": prompt},
        ],
        top_p=0.7,
        temperature=0.9
    )
    return str(response.choices[0].message.content)

class ZhiPuLLM(CustomLLM):
    model_name: str = "glm-4"

    @property
    def metadata(self) -> LLMMetadata:
        """Get LLM metadata."""
        return LLMMetadata(
            model_name=self.model_name,
        )

    @llm_completion_callback()
    def complete(self, prompt: str, **kwargs: Any) -> CompletionResponse:
        response = invoke_prompt(prompt)
        return CompletionResponse(text=response)


集成智谱 AI 文本嵌入 Embedding

智谱 AI 也提供了文本嵌入接口,那我们当然也是直接拿来用了,这样子就彻底摆脱了了 OpenAI 和自己搭建模型了。

LlamaIndex 的嵌入式基于 BaseEmbedding 实现的,参考官网的文档(Custom Embeddings Implementation),我们也写一个 智谱 AI 的实现,直接看代码:

def invoke_embedding(query):
    response = client.embeddings.create(
        model="embedding-2",  # 填写需要调用的模型名称
        input=query,
    )
    return response.data[0].embedding

class ZhiPuEmbedding(BaseEmbedding):
    _model: str = PrivateAttr()
    _instruction: str = PrivateAttr()

    def __init__(
            self,
            instructor_model_name: str = "text_embedding",
            instruction: str = "Represent a document for semantic search:",
            **kwargs: Any,
    ) -> None:
        # self._model = 'text_embedding'
        # self._instruction = instruction
        super().__init__(**kwargs)

    @classmethod
    def class_name(cls) -> str:
        return "zhipu_embeddings"

    async def _aget_query_embedding(self, query: str) -> List[float]:
        return self._get_query_embedding(query)

    async def _aget_text_embedding(self, text: str) -> List[float]:
        return self._get_text_embedding(text)

    def _get_query_embedding(self, query: str) -> List[float]:
        embeddings = invoke_embedding(query)
        return embeddings

    def _get_text_embedding(self, text: str) -> List[float]:
        embeddings = invoke_embedding(text)
        return embeddings

    def _get_text_embeddings(self, texts: List[str]) -> List[List[float]]:
        return [self._get_text_embedding(text) for text in texts]

知识库检索,启动!

万事俱备,只欠东风。前菜都已准备完毕,剩下的就是将前面的整合在一起,让我们看看能不能正常运行吧!

首先先准备一个你要做 Embedding 的知识库,我这里直接用了我的体检报告。

# define our LLM
llm = ZhiPuLLM()
embed_model = ZhiPuEmbedding()

service_context = ServiceContext.from_defaults(
    llm=llm, embed_model=embed_model
)

# Load the your data
loader = CJKPDFReader()
documents = loader.load_data(file=self.file_path)
index = VectorStoreIndex.from_documents(documents, service_context=service_context)

# Query and print response
query_engine = index.as_query_engine()
response = query_engine.query("在这份体检报告里,我的血常规检测有什么问题吗?")
print(response)

这里我们使用了 LlamaIndex 内置的 PDF 解析器,将文件转换成所需的向量 index,直接调用 query_engine.query() 函数查询即可。至此,一个简易的基于知识库检索的大模型案例便完成了。

通常情况下,我们会不断的对整个文档进行知识检索,因此有必要将我们构建的索引持久化,这样下次再次提问的时候就可以不用重新 Embedding(当然也可节省 token)。持久化也很简单,我们只需要调用 index.storage_context.persist(persist_dir=PERSIST_DIR) 函数即可,等下次再使用的时候,可以先从持久化的索引文件中 load,同样很容易,执行以下函数即可:

storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
index = load_index_from_storage(storage_context)

我们来看看返回的结果,是不是还可以呢?

结语

以上便是本文的全部内容了,本文主要介绍了如何使用 LlamaIndex 结合大模型落地一个知识库索引,主要就是自定义 LLM 和自定义 Embedding 这两块。这两个点对接之后,余下的各种玩法也几乎和使用 OpenAI 无异。如果你对大模型感兴趣的,并且想了解更多可以落地的 AI 玩法的话,不妨给我点个赞关注一下,后面再和大家分享。