【实战】 Vue 3、Anything LLM + DeepSeek本地化项目(三)

本章节目标在实现《【实战】 Vue 3、Anything LLM + DeepSeek本地化项目(二)》的基础上借用Anything LLM自带的API接口实现工作空间的加载和聊天记录的保存

预期效果

Anything LLM介绍

Anything LLM 是一个强大的开源全栈 AI 应用程序,集成了 RAG(Retrieval-Augmented Generation,检索增强生成)和 AI Agent 功能,能够将多种格式的文档、网址或内容转化为上下文,以便与大型语言模型(LLM)交流时使用。它支持本地运行或远程部署,适合个人用户、开发者和企业使用。

核心功能

  • 多模态交互:支持文本、图像和音频等多种输入方式,提供更丰富的交互体验。
  • 文档处理与上下文管理:将文档划分为独立的"工作区",支持多种格式(如 PDF、TXT、DOCX 等),保持上下文隔离,确保对话的清晰性。
  • 多用户支持与权限管理:Docker 版本支持多用户实例,管理员能控制用户权限,适合团队协作。
  • AI 代理与工具集成:支持在工作区内运行 AI 代理,执行网页浏览、代码运行等任务,扩展应用的功能。
  • 本地部署与隐私保护:默认情况下,所有数据(包括模型、文档和聊天记录)存储在本地,确保隐私和数据安全。
  • 强大的 API 支持:提供完整的开发者 API,方便用户进行自定义开发和集成。
  • 云部署就绪:支持多种云平台(如 AWS、GCP 等),方便用户根据需求进行远程部署。

技术原理

  • 前端:用 ViteJS 和 React 构建,提供简洁易用的用户界面,支持拖拽上传文档等功能。
  • 后端:基于 NodeJS 和 Express,负责处理用户交互、文档解析、向量数据库管理及与 LLM 的通信。
  • 文档处理:基于 NodeJS 服务器解析和处理上传的文档,将其转化为向量嵌入,存储在向量数据库中。
  • 向量数据库:用 LanceDB 等向量数据库,将文档内容转化为向量嵌入,便于在对话中快速检索相关上下文。
  • LLM 集成:支持多种开源和商业 LLM(如 OpenAI、Hugging Face 等),用户根据需求选择合适的模型。
  • AI 代理:在工作区内运行 AI 代理,代理能执行各种任务(如网页浏览、代码执行等),扩展应用的功能。支持的模型和数据库
  • 大型语言模型(LLMs):支持多种开源和闭源模型,如 OpenAI、Google Gemini Pro、Hugging Face 等。
  • 嵌入模型:支持 AnythingLLM 原生嵌入器、OpenAI 等。
  • 语音转文字和文字转语音:支持多种语音模型,包括 OpenAI 和 ElevenLabs。
  • 向量数据库:支持 LanceDB、Pinecone、Chroma 等。

安装与部署

桌面版

  • 系统要求:支持 Windows、MacOS 和 Linux,建议至少 8GB 内存,推荐 16GB 或更高。
  • 下载和安装:访问 AnythingLLM 官方网站,根据操作系统选择对应的安装包。
  • 安装程序
    • Windows:双击安装程序并按照提示完成安装。
    • MacOS:双击 DMG 文件,将应用程序拖入"应用程序"文件夹。
    • Linux:基于包管理器安装 DEB 或 RPM 文件。
  • 启动应用:安装完成后,打开 AnythingLLM 应用。
  • 初始化设置
    • 选择模型:首次启动时,选择一个语言模型(LLM)。
    • 配置向量数据库:选择默认的向量数据库(如 LanceDB)或配置其他支持的数据库。
  • 创建工作区:点击"新建工作区",为项目或文档创建一个独立的工作区。上传文档(如 PDF、TXT、DOCX 等),应用自动解析并生成向量嵌入,存储在向量数据库中。
  • 开始对话
    • 在工作区内输入问题或指令,应用根据上传的文档内容生成智能回答。
    • 支持多模态交互,上传图片或音频文件,应用根据内容进行处理。

Docker 版

  • 克隆项目
bash 复制代码
git clone https://github.com/Mintplex-Labs/anything-llm.git
cd anything-llm
  • 配置环境变量
  • 在项目根目录下运行以下命令,生成.env文件。
  • 编辑server/.env.development文件,配置LLM和向量数据库的参数。
  • 启动 Docker 容器。
  • 访问应用:打开浏览器,访问http://localhost:3000进入AnythingLLM的 Web 界面。
  • 使用方法
    • 创建工作区:与桌面版类似,创建工作区并上传文档。
    • 多用户管理:Docker 版支持多用户登录和权限管理,管理员在后台设置用户权限。
    • 嵌入式聊天小部件:Docker 版支持生成嵌入式聊天小部件,支持嵌入到网站中。
  • 高级功能
    • 自定义集成:基于 API 和插件扩展应用功能
    • 云平台部署:支持在 AWS、GCP、Digital Ocean 等云平台上部署。
  • 使用场景
  • 个人用户:可以将个人文档、网页链接等转化为上下文,与 LLM 进行智能对话,获取个性化的信息和建议。
  • 开发者:利用其强大的 API 支持,进行自定义开发和集成,构建个性化的 AI 应用。
  • 企业:企业可以将内部文档、资料等转化为知识库,结合 LLM 实现智能客服、知识管理等功能,提高工作效率。

将Anything LLM应用到项目

启动Anything LLM 生成API密钥

了解相关API文档内容

后续会持续使用该文档接口进行研发拓展功能。接口具体的内容暂时先不进行介绍,后续结合实际应用的接口再依次展开介绍

VUE3项目中接入

基于Anything LLM相关的API进行聊天

1、调整配置可以兼容Anything LLM API的聊天接口请求(改造chatAPIStream.ts,支持Ollama APIAnything LLM API)

typescript 复制代码
const deepseekChat=async(messages:any,url:string)=>{
  const data = {
    model: 'deepseek-r1:32b',
    messages,
    stream: true, // 启用流式响应
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.body as ReadableStream; // 返回流式数据
}
const anythingChat=async(messages:any,url:string)=>{
  const data = {
    model: "[模型ID]",
    stream: true,
    temperature: 0.7,
    messages
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer [Anything LLM API密钥]`
    },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.body as ReadableStream; // 返回流式数据
}
export const callDeepSeekAPI = async (messages: any[],searchLibrary:boolean): Promise<ReadableStream> => {
  const url =  searchLibrary?'/anything-server/api/v1/openai/chat/completions':'/deepseek-server/api/chat';
  return searchLibrary?anythingChat(messages,url):deepseekChat(messages,url);
};

2、改造views/ChatMsgStream/index.vue支持检索知识库并加载对应工作空间及聊天信息

vue 复制代码
<template>
  <el-row class="chat-window">
    <el-col :span="2">
      <el-checkbox v-model="searchLibrary" @change="searchLibraryChange"
        >检索知识库</el-checkbox
      >
    </el-col>
    <el-col class="chat-title" :span="20">
      智能小助手<span v-if="isThinking"><i class="el-icon-loading"></i>(思考中......)</span>
    </el-col>
    <el-col :span="24" v-if="searchLibrary">
      <el-tabs
        v-model="activeName"
        type="card"
        class="chat-tabs"
        @tab-click="handleClick"
      >
        <el-tab-pane
          v-for="tabItem in tabList"
          :label="tabItem.name"
          :name="tabItem.slug"
          :key="tabItem.slug"
        />
      </el-tabs>
    </el-col>
    <el-col class="chat-content" :span="24">
      <template v-for="(message, index) in messages" :key="index">
        <el-collapse v-if="message.role === 'assistant'">
          <el-collapse-item
            :title="`思考信息${
              (message.thinkTime && '-' + message.thinkTime + 's') || ''
            }`"
          >
            {{ message.thinkInfo }}
          </el-collapse-item>
        </el-collapse>
        <div class="chat-message">
          <div
            v-if="message.content"
            class="chat-picture"
            :class="{ 'chat-picture-user': message.role === 'user' }"
          >
            {{ message.role === "user" ? "用户" : "助手" }}
          </div>
          <v-md-preview :text="message.content"></v-md-preview>
        </div>
      </template>
    </el-col>
    <el-col class="chat-input" :span="20">
      <el-input
        v-model="userInput"
        type="textarea"
        :rows="4"
        placeholder="请输入消息"
      ></el-input>
    </el-col>
    <el-col class="chat-btn" :span="4">
      <el-button type="primary" plain @click="sendMessage">发送</el-button>
    </el-col>
  </el-row>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { callDeepSeekAPI } from "@/apis/chatAPIStream";
import { getAllWorkspaces, getWorkspaceChatsBySlug } from "@/apis/anythionChatAPIs.ts";
const messages = ref<
  { role: string; content: string; thinkTime?: number; thinkInfo?: string }[]
>([]);
const userInput = ref<string>("");
let thinkStartTime = 0; // 思考开始时间
let thinkEndTime = 0; // 思考结束时间
let isThinking = ref<boolean>(false); // 是否处于思考状态
let searchLibrary = ref<boolean>(false);
let activeName = ref("");
let tabList = ref([] as any);
/* 切换tab */
const handleClick = () => {
  getChatsMsgBySlug();
};
/* 检索知识库 */
const searchLibraryChange = () => {
  getAllWrokspaces();
};
/* 获取工作空间的聊天记录 */
const getChatsMsgBySlug = async () => {
  const result: any = await getWorkspaceChatsBySlug(activeName.value);
  messages.value = result.history.map(
    (chartInfo: {
      role: string;
      content: string;
      chatId?: number;
      sentAt?: string;
      thinkInfo: string;
      attachments?: Array<any>;
    }) => {
      let _exp = new RegExp("<think>.*?</think>", "gs");
      let _thinkInfo = chartInfo.content.match(_exp);
      if (_thinkInfo) {
        // 记录思考过程
        chartInfo.thinkInfo = _thinkInfo[0]
          .replace("<think>", "")
          .replace("</think>", "");
      }
      // 处理 <think> 标签
      chartInfo.content = chartInfo.content.replace(_exp, "");
      return chartInfo;
    }
  );
};
/* 获取工作空间列表信息 */
const getAllWrokspaces = async () => {
  const result: any = await getAllWorkspaces();
  tabList.value = result.workspaces;
  // 默认选中第一项
  activeName.value = tabList.value[0].slug;
  getChatsMsgBySlug();
};
const formatDuring = (millisecond: number): number => {
  let seconds: number = (millisecond % (1000 * 60)) / 1000;
  return seconds;
};
const contentFactory = (assistantContent: string) => {
  // 处理 <think> 标签
  if (/<think>(.*?)/gs.test(assistantContent) && !/<\/think>/gs.test(assistantContent)) {
    let _thinkInfo = assistantContent.replace(/<think>/gs, "");
    if (!thinkStartTime) {
      thinkStartTime = Date.now();
    }
    messages.value[messages.value.length - 1].thinkInfo = _thinkInfo;

    isThinking.value = true;
    return;
  } else if (/<\/think>/gs.test(assistantContent)) {
    assistantContent = assistantContent.replace(/<think>(.*?)<\/think>/gs, "");
    isThinking.value = false;
    if (!thinkEndTime) {
      thinkEndTime = Date.now();
    }
    messages.value[messages.value.length - 1].thinkTime = formatDuring(
      thinkEndTime - thinkStartTime
    );
  }
  // 逐字输出动画
  let currentContent = "";
  const chars = assistantContent.split("");
  chars.forEach((char, i) => {
    currentContent += char;
    messages.value[messages.value.length - 1].content = currentContent;
  });
};
const sendMessage = async () => {
  if (!userInput.value.trim()) return;

  // 添加用户消息
  messages.value.push({ role: "user", content: userInput.value, thinkTime: 0 });

  try {
    // 调用 DeepSeek API
    const stream = await callDeepSeekAPI(messages.value, searchLibrary.value);
    const decoder = new TextDecoder("utf-8");
    let assistantContent = ""; // 初始化助手内容
    const reader = stream.getReader();
    messages.value.push({ role: "assistant", content: "", thinkTime: 0, thinkInfo: "" });
    // 读取流式数据
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      // console.log(value, "value");
      const chunk = decoder.decode(value, { stream: true });
      // console.log(chunk, "chunk");
      let _chunkArr = chunk.split("\n").filter(Boolean);
      _chunkArr.forEach((item: string) => {
        let _content = "";
        if (searchLibrary.value) {
          item = item.replace(/^data:\s?/g, "");
          let { choices } = JSON.parse(item);
          _content = choices[0].delta.content;
        } else {
          let {
            message: { content },
          } = JSON.parse(item);
          _content = content;
        }
        assistantContent += _content; // 拼接流式数据
      });
      // 处理消息
      contentFactory(assistantContent);
    }
    thinkStartTime = 0;
    thinkEndTime = 0;
  } catch (error) {
    console.error("API 调用失败:", error);
  } finally {
    userInput.value = ""; // 清空输入框
  }
};
</script>

<style scoped lang="scss">
.user {
  color: blue;
}

.assistant {
  color: green;
}

.chat-window {
  width: 60%;
  padding: 10px;
  height: 640px;
  margin: 100px auto;
  box-shadow: 0 0 10px #6cb4ffcf;
  overflow: hidden;
  .chat-tabs {
    :deep() {
      .el-tabs__header {
        margin-bottom: 0;
      }
    }
  }
  .chat-title {
    text-align: center;
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 10px;
    height: 30px;
  }

  .chat-content {
    overflow-y: auto;
    border: 1px solid #e4e7ed;
    padding: 10px;
    margin-bottom: 10px;
    width: 100%;
    height: 436px;
    .chat-message {
      position: relative;
    }
    .chat-picture {
      width: 35px;
      height: 35px;
      background: #d44512;
      color: #fff;
      overflow: hidden;
      border-radius: 25px;
      font-size: 20px;
      line-height: 35px;
      text-align: center;
      position: absolute;
      top: 12px;
      left: -6px;
      &.chat-picture-user {
        background: #0079ff;
      }
    }
  }
  .chat-input,
  .chat-btn {
    height: 94px;
  }
  .chat-input {
  }

  .chat-btn {
    text-align: center;
    button {
      width: 100%;
      height: 100%;
    }
  }
}
</style>

3、新增anytingAxios.ts脚本基于Axios封装Anything LLM的API接口请求

typescript 复制代码
import axios from "axios";
declare module "axios" {
  interface AnythingResponse<T = any, D = any> {
    localFiles: {
      items: any;
      name: string;
      type: string;
    };
  }
  export interface AxiosResponse<T = any, D = any>
    extends AnythingResponse<T, D> {}
}
const instance = axios.create({
  baseURL: "/anything-server", // 设置基础 URL
  timeout: 5000, // 设置超时时间
});
// 请求拦截器
instance.interceptors.request.use(
  (config) => {
    // 在发送请求之前做些什么,例如添加 Token
    config.headers["Authorization"] = `Bearer [Anything LLM API密钥]`;
    return config;
  },
  (error) => {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
);

// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    // 对响应数据做点什么
    return response.data;
  },
  (error) => {
    // 对响应错误做点什么
    if (error.response.status === 401) {
      // 处理 401 未授权错误
      console.error("未授权,请重新登录");
    }
    return Promise.reject(error);
  }
);

export default instance;

4、新增anythionChatAPIs.ts脚本配置Anything LLM API的所有接口信息

typescript 复制代码
import axios from "@/anytingAxios.ts"

/**
 * /api/v1/workspaces
 * 获取当前所有工作空间
 * @returns 
 */
export function getAllWorkspaces(){
  return axios.get("/api/v1/workspaces")
}

/**
 * /api/v1/workspace/{slug}/chats
 * 通过工作空间的slug,获得工作区聊天信息
 * @param slug 工作空间的slugID
 * @returns 
 */
export function getWorkspaceChatsBySlug(slug:string){
  return axios.get(`/api/v1/workspace/${slug}/chats`)
}

5、调整vite.config.ts的代理配置,使接口能够正常调用

typescript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': '/src', // 确保这里的路径是正确的
    },
  },
  server: {
    port: 8080,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
      '/deepseek-server': {
        target: 'http://localhost:11434',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/deepseek-server/, ''),
      },
      '/anything-server': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/anything-server/, ''),
      }
    },
  },
})

至此,成功完成了Vue中使用Anything LLM API进行聊天的实现,后续会针对Anything LLM API实现对知识库的上传维护等功能的完善,敬请期待~

相关推荐
Bunury8 分钟前
element-plus添加暗黑模式
开发语言·前端·javascript
心走12 分钟前
八股文中TCP三次握手怎么具象理解?
前端·面试
Aiolimp21 分钟前
React常见Hooks使用(二)
前端·react.js
By北阳21 分钟前
CSS 中实现 div 居中有以下几种常用方法
前端·css
在广东捡破烂的吴彦祖23 分钟前
window配置Flutter开发环境
前端·flutter
辣椒粉丝26 分钟前
记rspack想提issuse,提太慢白嫖不上了
前端·javascript
腰间盘突出的红利28 分钟前
npm组件库搭建
前端
火星思想28 分钟前
前端基础布局写法详解:左右、左中右及弹性布局实践
前端·css
小桥风满袖28 分钟前
Three.js-硬要自学系列10 (创建纹理贴图、自定义顶点UV坐标)
前端·css·three.js
七月丶31 分钟前
🧼 为什么我开始在项目里禁用 CSS 文件?
前端·javascript·后端