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

276 阅读11分钟

本期实现目标

接上期《【实战】 Vue 3、Anything LLM + DeepSeek本地化项目(四)》预期目标本期完成了 知识库内容维护且达到模型的聊天信息可以从知识库中获取的最终效果

知识库内容的维护

知识库添加支持文件、链接和文字3种方式录入(知识库图1)

image.png

image.png 模型在知识库补充前后回答的内容对比

在没有给知识库提供最近很火的《长安的荔枝》电视剧信息前,模型的所有回答基本都是基于2023/06/19的信息进行的作答。 image.png

image.png 将豆瓣长安的荔枝 (2025)影评信息作为知识库内容添加到对应的聊天进程中后得到的结果将存在最新的信息,从思考过程中也可以看到模型有从知识库获取信息,最终模型也成功的把添加的豆瓣信息输出了出来。

image.png

image.png

Anything LLM 知识库

介绍

Anything LLM 知识库是一种基于大语言模型(LLM)构建的本地知识库

特点

  • 数据本地化:知识数据存储在本地,如文档、资料等上传后在本地进行存储、分切、向量嵌入等操作,数据安全可控,不会上传到第三方平台。
  • 模型运行本地化:通过工具如Ollama等可将模型部署在本地设备,依赖本地算力运行,保障了数据的私密性和安全性。
  • 应用程序本地化:本身是可安装在本地电脑上的全栈应用程序,有本地用户界面和交互环境,用户可直接在本地进行知识库的创建、管理、查询等操作,无需联网访问远程界面。
  • 多模态数据支持:能够将各种类型的文档、网页链接、音视频文件以及文字片段等转化为LLM可理解的上下文信息,为知识库提供丰富的内容来源。
  • 灵活的模型与数据库选择:用户可自由选择不同的LLM,如与llama.cpp兼容的开源模型、OpenAI、Google Gemini Pro等,以及多种向量数据库,如LanceDB、Astra DB、Pinecone等,还可根据需求进行替换和组合,打破了技术绑定。
  • 高效的工作区管理:将文档划分为称为“工作区”的对象,工作区类似线程且增加了文档的容器化,不同工作区内容相互独立,互不干扰,可保持每个工作区上下文清晰,同时支持多用户协作与权限分级,保障数据安全,适合企业内部文档管理等多用户场景。
  • 强大的Agent功能加持:内置Agent功能,可联网搜索、执行代码等,能够拓展对话能力边界,使知识库不仅能基于本地知识回答问题,还能结合实时信息提供更全面准确的回答。

构建方式

  • 基于RAG架构:采用Retrieval-Augmented Generation即检索增强型生成架构,工作流程分为预处理(文档清洗、分块)、向量化(嵌入模型处理)、检索(向量数据库查询)和生成(LLM模型回答)四个主要阶段,通过这种方式将文档等知识数据转化为LLM可理解和利用的知识,从而构建知识库。
  • 借助开源框架与工具:利用Anything LLM这一开源框架,结合Docker等工具进行部署,可快速搭建起知识库。其内置了向量数据库和embedding模型,只需接入合适的LLM,按照官方文档进行配置和操作,即可完成知识库的构建。

应用场景

  • 企业知识管理:企业可将内部的各种文档、资料等上传到知识库,员工在需要时通过与知识库的对话快速检索和获取所需信息,提高工作效率,同时保障企业知识的安全性和私密性。
  • 个人学习与研究:个人可将自己在学习、研究过程中积累的资料、笔记等整理到知识库中,方便随时查询和复习,还能利用知识库的智能对话功能进行知识的拓展和深化。
  • 创意开发与应用:开发者可基于Anything LLM知识库开发各种创意应用,如聊天机器人、虚拟助手等,为人们的生活和工作带来更多便利和乐趣。

功能在VUE3中的相关实现

知识库的维护

  1. 在项目中增加src/views/Library/documents.vue组件来作为知识库组件的主入口。
<template>
  <div class="document-main">
    <div class="document-content-group">
      <el-row class="document-content">
        <el-col class="document-content-btn">
          <uploadLibraryPlugin @reload="getDocumentList" />
          <el-button plain @click="deleteDocument">删除</el-button>
        </el-col>
        <el-col class="document-content-table">
          <el-table
            border
            :data="documentTableList"
            height="100%"
            @selection-change="selectionChange"
          >
            <el-table-column type="selection" align="center" width="55"></el-table-column>
            <el-table-column
              align="center"
              type="index"
              label="序号"
              width="80"
            ></el-table-column>
            <el-table-column
              align="center"
              prop="title"
              label="文件名称"
              show-overflow-tooltip
              min-width="200"
            ></el-table-column>
            <el-table-column
              align="center"
              prop="url"
              label="文件路径"
              min-width="200"
              show-overflow-tooltip
            ></el-table-column>
            <el-table-column
              align="center"
              prop="description"
              show-overflow-tooltip
              label="文件描述"
              min-width="200"
            ></el-table-column>
            <el-table-column
              align="center"
              prop="docAuthor"
              show-overflow-tooltip
              label="文件作者"
              min-width="100"
            ></el-table-column>
            <el-table-column
              align="center"
              prop="docSource"
              show-overflow-tooltip
              label="文件来源"
              min-width="120"
            ></el-table-column>
            <el-table-column
              align="center"
              prop="published"
              show-overflow-tooltip
              label="发布时间"
              min-width="120"
            ></el-table-column>
          </el-table>
        </el-col>
      </el-row>
    </div>
  </div>
</template>

<script setup lang="ts">
import uploadLibraryPlugin from "@/views/Library/components/document/uploadLibraryPlugin.vue";
import { getAllDocuments, removeSystemDocuments } from "@/apis/anythionChatAPIs";
import { onMounted, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
let documentTableList = ref<any>([]);
const selectionList = ref([]);
onMounted(() => {
  // 初始化获取所有工作空间信息
  // getWorkspaceList();
  getDocumentList();
});
// 删除文档内容
const deleteDocument = () => {
  if (selectionList.value.length < 1) {
    ElMessage.warning("请至少选择一条数据");
    return;
  }
  ElMessageBox.confirm("确定要删除选中项吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    let _deleteNames = selectionList.value.map((item: any) => {
      return `${item.docName}/${item.name}`;
    });
    removeSystemDocuments({ names: _deleteNames })
      .then(() => {
        ElMessage.success("删除成功");
        getDocumentList();
      })
      .catch(() => {
        ElMessage.error("删除失败");
      });
  });
};
// 列表复选框选中触发
const selectionChange = (selection: any) => {
  selectionList.value = selection;
};
const getDocumentList = async () => {
  const res: any = await getAllDocuments();
  documentTableList.value = [];
  res.localFiles.items.forEach((documentInfo: any) => {
    let _currentDocumentList = documentInfo.items.map((item: any) => {
      item.docName = documentInfo.name;
      item.type = documentInfo.type;
      return item;
    });
    documentTableList.value = documentTableList.value.concat(_currentDocumentList);
  });
};
</script>

<style scoped lang="scss">
.document-main {
  display: flex;
  overflow: hidden;
  height: 100%;

  .document-tab-group {
    width: 300px;
  }

  .document-content-group {
    flex: 1;
    padding: 8px;
    overflow: hidden;

    .document-content {
      display: flex;
      flex-direction: column;
      overflow: hidden;

      .document-content-btn {
        margin-bottom: 4px;
      }

      .document-content-table {
        flex: 1;
        overflow: hidden;
      }
    }
  }
}
</style>

  1. 增加src/views/Library/components/document/uploadLibraryPlugin.vue组件来实现维护知识库的弹窗效果
  <el-button type="primary" @click="opentDialog">新增知识库</el-button>
  <el-dialog append-to-body title="上传知识库" v-model="dialogFormVisible">
    <el-form :model="libFormInfo" label-width="90px">
      <el-radio-group v-model="activeLibInfo" class="upload-library-plugin-dialog-tool">
        <el-radio value="uploadFile">上传文件</el-radio>
        <el-radio value="uploadLink">上传链接</el-radio>
        <el-radio value="rawText">文本信息</el-radio>
      </el-radio-group>
      <template v-if="activeLibInfo == 'uploadFile'">
        <el-form-item prop="fileRaw" label="上传文件">
          <el-upload :auto-upload="false" :on-change="uploadLibFile" class="inline-btn">
            <template #trigger>
              <el-button type="primary">上传文件到知识库</el-button>
            </template>
          </el-upload>
        </el-form-item>
      </template>
      <template v-if="activeLibInfo == 'uploadLink'">
        <el-form-item prop="fileLink" label="上传链接">
          <el-input v-model="libFormInfo.fileLink"></el-input>
        </el-form-item>
      </template>
      <template v-if="activeLibInfo == 'rawText'">
        <el-form-item prop="rawText" label="文本标题">
          <el-input v-model="libFormInfo.title"></el-input>
        </el-form-item>
        <el-form-item prop="keywords" label="关键词">
          <el-input v-model="libFormInfo.keyWords"></el-input>
        </el-form-item>
        <el-form-item prop="rawText" label="文本内容">
          <el-input type="textarea" v-model="libFormInfo.rawText" :rows="4"></el-input>
        </el-form-item>
      </template>
    </el-form>
    <template #footer>
      <el-button @click="cancelDocument">取消</el-button>
      <el-button type="primary" @click="saveDocument">保存</el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { ElMessage } from "element-plus";

import { uploadFile, uploadLink, uploadRawText } from "@/apis/anythionChatAPIs.ts";
import { reactive, ref } from "vue";
import type { LibFromInfo } from "./type/uplaodLibrayPlugin";
const emit = defineEmits(["reload"]);
// 知识库表单信息
const libFormInfo = reactive<LibFromInfo>({
  fileRaw: "",
  fileLink: "",
  rawText: "",
  title: "",
  keyWords: "",
});
// 控制知识库弹窗
const dialogFormVisible = ref(false);
// 当前选中的知识库类型
const activeLibInfo = ref("uploadFile");
const opentDialog = () => {
  dialogFormVisible.value = true;
};
// 取消知识库信息录入
const cancelDocument = () => {
  dialogFormVisible.value = false;
};
// 保存知识库信息录入
const saveDocument = async () => {
  let result: any = {
    success: false,
  };
  switch (activeLibInfo.value) {
    case "uploadFile":
      result = await uploadFileByDocument(libFormInfo.fileRaw);
      break;
    case "uploadLink":
      result = await uploadLinkByDocument();
      break;
    case "rawText":
      result = await rawTextByDocument();
      break;
  }
  if (result && result.success) {
    ElMessage.success("保存成功");
    emit("reload");
    cancelDocument();
  } else {
    ElMessage.error("保存失败");
  }
};
// 上传文件并保存
const uploadFileByDocument = async (file: any) => {
  let formData = new FormData();
  formData.append("file", file.raw);
  return await uploadFile(formData);
};
// 保存知识库链接
const uploadLinkByDocument = async () => {
  return await uploadLink({
    link: libFormInfo.fileLink,
  });
};
// 保存知识库文本
const rawTextByDocument = async () => {
  let _params = {
    textContent: libFormInfo.rawText,
    metadata: {
      title: libFormInfo.title,
      keyOne: "",
      keyTwo: "",
      etc: "",
    },
  };
  if (libFormInfo.keyWords) {
    let _arr = libFormInfo.keyWords.split(",");
    if (_arr.length > 1) {
      _params.metadata.keyOne = _arr[0];
      _params.metadata.keyTwo = _arr[1];
    } else if (_arr.length == 1) {
      _params.metadata.keyOne = _arr[0];
    }
  }
  return await uploadRawText(_params);
};
// 上传文件知识库-文件内容选中触发
const uploadLibFile = (uploadFile: any) => {
  console.log(uploadFile, "uploadFile");
  // 记录上传的文件流信息
  libFormInfo.fileRaw = uploadFile;
};
</script>

<style lang="scss" scoped></style>

至此,完成了知识库的维护效果,注意:知识库内容添加进来之后要配合着工作空间的知识库引入行为才能发挥作用

聊天小助手改造

原知识库聊天小助手是基于默认的聊天接口/api/v1/openai/chat/completions进行的模型对话,因引入了工作空间的概念所以我们需要调用工作空间的聊天接口/anything-server/api/v1/workspace/${slug}/stream-chat来完成最终的对话动作,改造如下:

  1. src/apis/chatAPIStream.ts中的调用增加一种适配,如果存在工作进程的概念则走工作进程的聊天接口
export const callDeepSeekAPI = async (
  messages: any[],
  searchLibrary: boolean,
  slug?: string
): Promise<ReadableStream> => {
  let url = "/deepseek-server/api/chat";
  if (searchLibrary) {
    // 查询知识库
    if (slug) {
      // 进程中查询
      url = `/anything-server/api/v1/workspace/${slug}/stream-chat`;
    } else {
      // 默认查询
      url = "/anything-server/api/v1/openai/chat/completions";
    }
  }
  return searchLibrary
    ? anythingChat(messages, url, slug)
    : deepseekChat(messages, url);
};
  1. src/views/ChatMsgStream/index.vue中兼容改在响应相关函数
const sendMessage = async () => {
  if (!userInput.value.trim()) return;

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

  try {
    let _sendMessage: any = messages.value;
    if (searchLibrary.value && activeSlug.value) {
      // 记录所有用户信息
      let _userMessage = _sendMessage.filter((item: any) => {
        return item.role !== "assistant";
      });
      // 将所有用户信息的最后一个问题点作为进程问答的消息进行发送
      let _message = _userMessage.map((item: any) => item.content)[
        _userMessage.length - 1
      ];
      _sendMessage = {
        message: _message,
        mode: "chat",
        sessionId: "",
        attachments: [],
      };
    }
    // 调用 DeepSeek API
    const stream = await callDeepSeekAPI(
      _sendMessage,
      searchLibrary.value,
      activeSlug.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 && activeSlug.value) {
          item = item.replace(/^(data):\s?/g, "");
          let { textResponse } = JSON.parse(item);
          _content = textResponse || "";
        } else 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 = ""; // 清空输入框
  }
};

完整代码如下: src/views/ChatMsgStream/index.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>
    <template v-if="searchLibrary">
      <el-col :span="24">
        <el-tabs
          v-model="activeSlug"
          type="card"
          class="chat-tabs"
          @tab-click="handleClick"
        >
          <el-tab-pane
            v-for="tabItem in tabList"
            :label="tabItem.name"
            :name="tabItem.slug"
            :key="`tab-${tabItem.slug}`"
          />
        </el-tabs>
      </el-col>
      <el-col :span="24">
        <el-tabs v-model="activeThreadSlug" @tab-click="threadHandleClick">
          <el-tab-pane
            v-for="thread in activeInfo.threads"
            :label="thread.name"
            :name="thread.slug"
            :key="thread.slug"
          />
        </el-tabs>
      </el-col>
    </template>
    <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,
  getWorkspaceThreadChatsBySlug,
} 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 activeSlug = ref("" as string);
let activeInfo = ref({} as any);
let activeThreadSlug = ref("");
let tabList = ref([] as any);
/* 切换tab */
const handleClick = () => {
  activeInfo.value = tabList.value.find((item: any) => item.slug == activeSlug.value);
  if (!activeInfo.value.threads.find((item: any) => item.name === "default")) {
    activeInfo.value.threads.unshift({
      name: "default",
      slug: activeInfo.value.slug,
    });
  }
  activeThreadSlug.value = activeInfo.value.threads[0].slug;
  getChatsMsgBySlug();
};
const threadHandleClick = () => {
  let _currentThread = activeInfo.value.threads.find(
    (item: any) => item.slug == activeThreadSlug.value
  );
  if (_currentThread.name === "default") {
    getChatsMsgBySlug();
  } else {
    getTheadsChartsMsgBySlug();
  }
};
/* 检索知识库 */
const searchLibraryChange = () => {
  if (searchLibrary.value) {
    getAllWrokspaces();
  } else {
    activeSlug.value = "";
  }
};
const _initSetHistoryMessages = (result: any) => {
  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 getChatsMsgBySlug = async () => {
  const result: any = await getWorkspaceChatsBySlug(activeThreadSlug.value);
  _initSetHistoryMessages(result);
};
const getTheadsChartsMsgBySlug = async () => {
  const result: any = await getWorkspaceThreadChatsBySlug(
    activeSlug.value,
    activeThreadSlug.value
  );
  _initSetHistoryMessages(result);
};
/* 获取工作空间列表信息 */
const getAllWrokspaces = async () => {
  const result: any = await getAllWorkspaces();
  tabList.value = result.workspaces;
  // 默认选中第一项
  activeSlug.value = tabList.value[0].slug;
  handleClick();
};
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 {
    let _sendMessage: any = messages.value;
    if (searchLibrary.value && activeSlug.value) {
      // 记录所有用户信息
      let _userMessage = _sendMessage.filter((item: any) => {
        return item.role !== "assistant";
      });
      // 将所有用户信息的最后一个问题点作为进程问答的消息进行发送
      let _message = _userMessage.map((item: any) => item.content)[
        _userMessage.length - 1
      ];
      _sendMessage = {
        message: _message,
        mode: "chat",
        sessionId: "",
        attachments: [],
      };
    }
    // 调用 DeepSeek API
    const stream = await callDeepSeekAPI(
      _sendMessage,
      searchLibrary.value,
      activeSlug.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 && activeSlug.value) {
          item = item.replace(/^(data):\s?/g, "");
          let { textResponse } = JSON.parse(item);
          _content = textResponse || "";
        } else 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: 700px;
  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>

src/apis/chatAPIStream.ts相关完整代码:

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, slug?: string) => {
  const data = slug
    ? messages
    : {
        model: "[模型id]",
        stream: true,
        temperature: 0.7,
        messages,
      };

  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `[令牌信息]`,
    },
    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,
  slug?: string
): Promise<ReadableStream> => {
  let url = "/deepseek-server/api/chat";
  if (searchLibrary) {
    // 查询知识库
    if (slug) {
      // 进程中查询
      url = `/anything-server/api/v1/workspace/${slug}/stream-chat`;
    } else {
      // 默认查询
      url = "/anything-server/api/v1/openai/chat/completions";
    }
  }
  return searchLibrary
    ? anythingChat(messages, url, slug)
    : deepseekChat(messages, url);
};

至此,实现了上一节中预期要实现的功能,后续计划探索更多的模型应用之间的相关知识,敬请期待~