【实战】 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实现对知识库的上传维护等功能的完善,敬请期待~

相关推荐
我爱加班、、5 分钟前
Chrome安装最新vue-devtool插件
javascript·vue.js·chrome·vue-devtool
weixin_4738947717 分钟前
前端服务器部署分类总结
前端·网络·性能优化
LuckyLay35 分钟前
React百日学习计划-Grok3
前端·学习·react.js
澄江静如练_39 分钟前
小程序 存存上下滑动的页面
前端·javascript·vue.js
源码方舟1 小时前
基于SpringBoot+Vue的房屋租赁管理系统源码包(完整版)开发实战
vue.js·spring boot·后端
互联网搬砖老肖1 小时前
Web 架构之会话保持深度解析
前端·架构
m0_513962531 小时前
vue-ganttastic甘特图label标签横向滚动固定方法
javascript·vue.js·甘特图
菜鸟una1 小时前
【taro3 + vue3 + webpack4】在微信小程序中的请求封装及使用
前端·vue.js·微信小程序·小程序·typescript·taro
Java&Develop1 小时前
怎么查看当前vue项目,要求的node.js版本
vue.js
hao_04131 小时前
elpis-core: 基于 Koa 实现 web 服务引擎架构设计解析
前端