本地大模型LocalAI使用教程指南

 LocalAI是 OpenAI 的开源替代品,它能在本地计算机上运行 LLM。不需要 GPU,消费级硬件就足够了

如果您可以使用与 OpenAI 相同的 Rest API 在本地运行模型,那不是很棒吗?嗯,这正是LocalAI必须为您提供的!

 LocalAI 是 OpenAI 的开源替代方案,具有与 OpenAI API 规范兼容的 Rest API。除此之外,不需要 GPU,您可以在消费级硬件上运行它。不过,建议使用 GPU,因为它的速度大约快 20 倍。

此处描述了 LocalAI for CPU 的安装。

1、克隆 LocalAI git 存储库。
$ git clone https://github.com/go-skynet/LocalAI

2、导航到存储库目录。
$ cd LocalAI

3、存储库包含.env您需要自定义的文件。

  • 取消注释THREADS:并将数字调整为您拥有的物理核心数量(在我的例子中为 12);
  • 取消注释GALLERIES:并将其调整为安装指南中所述的galleries 。

## Set number of threads.
## Note: 优先选择物理内核数量。过多的 CPU 会明显降低性能。
THREADS=12
 
## Specify a different bind address (defaults to ":8080")
# ADDRESS=127.0.0.1:8080
 
## Default models context size
# CONTEXT_SIZE=512
#
## Define galleries.
## 要安装的模型将在 `/models/available` 中显示
GALLERIES=[{"name":"model-gallery", "url":"github:go-skynet/model-gallery/index.yaml"}, {"url": "github:go-skynet/model-gallery/huggingface.yaml","name":"huggingface"}]


4、启动 Docker 容器。
Docker 镜像引用的是最新标签,在撰写本文时,LocalAI 的 v2.0.0 是最新标签。你可以导航到镜像仓库,搜索最新标签,复制清单哈希值,然后搜索复制的清单哈希值。

$ docker compose up -d --pull always

请耐心等待,这需要一些时间。镜像文件约为 70GB。上一版本 v1.40.0 大约为 14GB。

当容器成功启动后,你应该可以检索到可用的模型:

$ curl http://localhost:8080/models/available

安装模型
首先,您需要安装一个模型。您可以通过应用程序接口的模型图库进行安装,但在撰写本文时,这仍处于试验阶段。我更喜欢手动添加模型。说明可以在这里找到,但要知道,随着时间的推移,说明可能会发生变化,所以不要完全依赖本段的内容。

在 models 目录下创建 lunademo.yaml 文件。将线程数改为你机器上的物理内核数。

name: lunademo
parameters:
  model: luna-ai-llama2-uncensored.Q5_K_M.gguf
  top_k: 80
  temperature: 0.2
  top_p: 0.7
context_size: 1024
threads: 12
backend: llama
roles:
  assistant: 'ASSISTANT:'
  system: 'SYSTEM:'
  user: 'USER:'
template:
  chat: lunademo-chat
  completion: lunademo-completion


模型指的是包含模型的文件。从 HuggingFace 将文件下载到模型目录。HuggingFace 包含许多您可以使用的开源模型,但在本示例中,您将使用基于 Llama 2 的模型,Llama 2 是由 Meta 创建的人工智能模型。请注意,在 "模型卡 "中,模型与其用例一起列出。此外,用例中还说明了建议使用的模型。请注意只能使用 GGUF 模型,Llama 2 不再支持 GGML。

还要注意的是,模型的配置文件中定义了两个模板。一个是聊天模板,另一个是完成模板。

在模型目录中创建一个文件 lunademo-chat.tmpl。该模板来自 HuggingFace 的模型卡(搜索 Prompt 模板)。

USER: {{.Input}}
 
ASSISTANT:

在模型目录下创建 lunademo-completion.tmpl 文件。

Complete the following sentence: {{.Input}}

重启 Docker 容器以加载模型。

$ docker compose restart

提问
既然模型已经加载,您就可以开始提问了。您可以查看 OpenAPI 规范,以下是一些示例,以验证本地模型的响应情况和准确性。

你好吗?
作为第一个简单的例子,您可以询问模型感觉如何。在请求中,您可以提及要使用的模型、信息,还可以设置温度。温度越高,模型就越有创造力。模型回答说它做得很好。

$ curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{
     "model": "lunademo",
     "messages": [{"role": "user", "content": "How are you?"}],
     "temperature": 0.9 
   }'
{
   "created":1700993538,
   "object":"chat.completion",
   "id":"2fe33052-f4be-4724-8b53-fdade80b49de",
   "model":"lunademo",
   "choices":[
      {
         "index":0,
         "finish_reason":"stop",
         "message":{
            "role":"assistant",
            "content":"I'm doing well, thank you. How about yourself?"
         }
      }
   ],
   "usage":{
      "prompt_tokens":0,
      "completion_tokens":0,
      "total_tokens":0
   }
}


集成LangChain4j
上面展示了如何通过 LocalAI 运行类似于 OpenAI 的大型语言模型 (LLM)。

使用 OpenAI 的 Rest API 来与 LocalAI 交互。将这些功能集成到 Java 应用程序中可能会很麻烦。

LangChain4j 为您提供了与大模型集成的简化方法。它基于 Python 库LangChain。

LangChain4j 示例存储库中提供了许多示例。特别是,目录中的示例other-examples已被用作此博客的灵感。
本博客中使用的资源可以在GitHub上找到。

为了将 LangChain4j 与 LocalAI 结合使用,您需要将langchain4j-local-ai依赖项添加到 pom 文件中。

<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-local-ai</artifactId>
  <version>0.24.0</version>
</dependency>

为了与 LocalAI 集成,您需要创建一个指定以下项目的 ChatLanguageModel:

  • 可访问 LocalAI 实例的 URL;
  • 您希望在 LocalAI 中使用的模型名称;
  • 温度,温度越高,模型的响应越有创意。

然后,要求模型生成问题的答案并打印出来。

ChatLanguageModel model = LocalAiChatModel.builder()
        .baseUrl("http://localhost:8080")
        .modelName("lunademo")
        .temperature(0.9)
        .build();
 
String answer = model.generate("How are you?");
System.out.println(answer);

启动 LocalAI 并运行上述示例。

响应与预期一致。
I'm doing well, thank you. How about yourself?

LanguageModel 和 ChatLanguageModel 之间的区别
这两个类在 LangChain4j 中都有,那么该选哪个呢?

  • 聊天模型是语言模型的一种变体。如果您需要 "文本输入,文本输出 "功能,您可以选择 LanguageModel。
  • 如果还想使用 "聊天信息 "作为输入和输出,则应使用 ChatLanguageModel。

在上面的示例中,你可以只使用 LanguageModel,它的表现也会类似。

嵌入文档
嵌入文档的最简单方法是阅读文档,将其分割成块,然后嵌入这些块。嵌入意味着将文本转换为向量(数字)。您要提出的问题也需要嵌入。

矢量存储在一个矢量存储库中,该存储库能找到与您的问题最接近的结果,并用这些结果作出回应。源代码source code由以下部分组成:

  • 文本需要嵌入。为此需要一个嵌入模型,为简单起见,您可以使用 AllMiniLmL6V2EmbeddingModel。该模型使用 BERT 模型,这是一种流行的嵌入模型。
  • 嵌入需要存储在一个嵌入存储库中。通常情况下,矢量数据库会被用于此目的,但在这种情况下,你可以使用内存中的嵌入存储。
  • 读取两个文档并将其添加到 DocumentSplitter 中。在这里,您将定义将文档分割成 500 个字符且不重叠的片段。
  • 通过文档分割器,文档被分割成文本段。
  • 嵌入模型用于嵌入文本段。TextSegments 及其嵌入的对应内容都存储在嵌入存储区中。
  • 问题也使用相同的模型嵌入。
  •  
  • 要求嵌入存储查找与嵌入问题相关的嵌入片段。您可以定义存储应检索多少个结果。

在本例中,只要求一个结果:如果找到匹配结果,控制台将打印以下信息:
  • The score得分:表示结果与问题匹配程度的数字;
  • The original text原文:片段的文本;
  • The meta data元数据:将显示片段来自哪个文档。

private static void askQuestion(String question) {
    EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
 
    EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
 
    //读取文件并将其分割成 500 块的片段
    Document springsteenDiscography = loadDocument(toPath("example-files/Bruce_Springsteen_discography.pdf"));
    Document springsteenSongList = loadDocument(toPath("example-files/List_of_songs_recorded_by_Bruce_Springsteen.pdf"));
    ArrayList<Document> documents = new ArrayList<>();
    documents.add(springsteenDiscography);
    documents.add(springsteenSongList);
 
    DocumentSplitter documentSplitter = DocumentSplitters.recursive(500, 0);
    List<TextSegment> documentSegments = documentSplitter.splitAll(documents);
 
    // 嵌入分段
    Response<List<Embedding>> embeddings = embeddingModel.embedAll(documentSegments);
    embeddingStore.addAll(embeddings.content(), documentSegments);
 
    //嵌入问题并查找相关片段
    Embedding queryEmbedding = embeddingModel.embed(question).content();
    List<EmbeddingMatch<TextSegment>> embeddingMatch = embeddingStore.findRelevant(queryEmbedding,1);
    System.out.println(embeddingMatch.get(0).score());
    System.out.println(embeddingMatch.get(0).embedded().text());
    System.out.println(embeddingMatch.get(0).embedded().metadata());
}

问题如下,是文件中可以找到的一些事实:

public static void main(String args) {
    askQuestion("on which album was \"adam raised a cain\" originally released?");
    askQuestion("what is the highest chart position of \"Greetings from Asbury Park, N.J.\" in the US?");
    askQuestion("what is the highest chart position of the album \"tracks\" in canada?");
    askQuestion("in which year was \"Highway Patrolman\" released?");
    askQuestion("who produced \"all or nothin' at all?\"");
}

嵌入 Markdown 文档
将 PDF 文档转换成 Markdown 文件后会有什么变化?在 Markdown 文件中识别表格可能比在 PDF 文档中更好,而且它允许你在行级而不是任意的块大小上分割文档。只有文档中包含问题答案的部分才会被转换。这意味着唱片目录中的录音室专辑和合辑以及录制的歌曲列表。

分割过程如下

  • 逐行分割文档;
  • 在变量 dataOnly 中读取表格数据;
  • 在变量 header 中保存表头;
  • 为 dataOnly 中的每一行创建一个文本段,并将页眉添加到文本段中。

 source code :

List<Document> documents = loadDocuments(toPath("markdown-files"));
 
List<TextSegment> segments = new ArrayList<>();
for (Document document : documents) {
    String splittedDocument = document.text().split("\n");
    String dataOnly = Arrays.copyOfRange(splittedDocument, 2, splittedDocument.length);
    String header = splittedDocument[0] + "\n" + splittedDocument[1] + "\n";
 
    for (String splittedLine : dataOnly) {
        segments.add(TextSegment.from(header + splittedLine, document.metadata()));
    }
}

到目前为止得出的结论是

  • 文件格式以及文件分割和嵌入的方式对结果有很大影响;
  • 如果问题使用的术语与文档中的数据接近,则会取得更好的结果。
  • 读取和嵌入文档的方式似乎对结果影响最大。这种方法的优点是可以显示多个结果。这样,您就可以确定哪个结果是正确的。
  • 更改您的问题,使其使用文本段中使用的术语,有助于获得更好的结果。矢量存储的查询速度非常快。嵌入需要花费一些时间,但只需要做一次。
  • 在不使用 GPU 的情况下,使用 LLM 获取结果需要更多时间。

矢量数据库
Weaviate是一个矢量数据库,还支持RAG

可以使用 Wea​​viate、LangChain4j 和 LocalAI 实现 RAG,结果是相当惊人的。以正确的方式嵌入文档、过滤结果并将其提供给大模型是一个非常强大的组合,可用于许多用例。

步骤:

  1. 文档嵌入并存储在 Weaviate 中;
  2. 嵌入问题并使用 Wea​​viate 执行语义搜索;
  3. Weaviate 返回语义搜索结果;
  4. 结果被添加到提示中并馈送到 LocalAI,后者使用 LangChain4j 运行 LLM;
  5. LLM返回问题的答案。

GitHub上找到实现源码