iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
IT 管理

Backstage : 打造企業內部開發者整合平台系列 第 27

Day 27:Backstage 專題開發 - 利用 Ollama + AnythingLLM 打造本地 AI 助手

  • 分享至 

  • xImage
  •  

簡介

隨著 Backstage 平台持續整合來自不同部門與領域的大量資料,我們可以透過搜尋引擎插件為使用者提供了快速查找資訊的便利。然而,伴隨資料量的爆炸性增長,如何從龐大的資料庫中篩選出最具價值的內容成為了新挑戰。另外使用者在面對繁雜資訊時,往往需要耗費大量時間整理與理解,嚴重影響工作效率。因此,優化搜尋結果的準確性與內容呈現方式,讓使用者能迅速鎖定核心資訊,成為提升使用者體驗的關鍵。

為解決這些問題,我們可以借助生成式 AI 的力量,進一步提升搜尋與內容摘要的效率與精準度。藉由 AI 的協助,我們能自動分類與篩選資料,並在搜尋過程中提供更具相關性的結果,或自動生成內容摘要,協助使用者快速掌握重點資訊,大幅節省檢閱大量資料的時間。

然而,基於企業資料安全性的考量,我們無法依賴外部第三方的 AI 服務,儘管這些服務在效能上更為可靠。為此,我們選擇在本地部署大型語言模型(LLM),以確保資料的私密性。這不僅保障了資料安全,還能根據企業需求靈活調整模型的運作方式,完美滿足內部的使用情境。
https://ithelp.ithome.com.tw/upload/images/20241006/20128232Tz8eFvqyHz.png
為了解決這個問題,「Local LLM 本地大型語言模型知識庫」的概念應運而生,通過本地部署的大型語言模型,將 Backstage 平台中的各種資料,轉化為利於語言模型分析的資訊,再應用大型模型來幫助使用者,更高效地從大量資料中找到所需內容與萃取精髓。

  • AnythingLLM 的定位

    AnythingLLM 是作為一個介面端口,負責與向量資料庫中的資料進行交互。查詢已經向量化並存儲在資料庫中的資料,並整合和管理不同的語言模型,讓各式模型能夠靈活搭配使用,根據使用者的需求生成對應的內容。並且擁有 Workspace 的概念,能夠切分不同工作區使用不同的模型與參數,並隔離使用者的對話內容或文件。

  • RAG 向量化資料處理與存儲

    Backstage 各類資料會先經過 RAG(Retrieval-Augmented Generation)處理,將資料轉換成向量格式,以便更高效地進行語意檢索提高相關性和精準度。轉換後的向量資料將存儲在像 LancerDB 這樣的向量資料庫中。

  • Ollama 模型運算

    當 AnythingLLM 從向量資料庫查詢到資料後,再透過 API 與大型語言模型運算平台 Ollama 連接, 負責進行進一步的運算,根據查詢結果進行內容的生成和摘要,並將相關資訊回傳給使用者。運算過程中使用顯示卡進行 GPU 加速,以提升效能。

靈感來源 - Mintlify

還記得我們先前提到的 Mintlify 嗎?它巧妙地結合了技術文檔與 API 文檔,讓我們不再需要分別管理兩種不同結構的文件。透過 MDX 檔案格式,我們可以一併撰寫這兩種文檔,並自動生成美觀的文件風格。

Mintlify 的亮點不僅在於文件整合功能,更令人驚艷的是它的 AI 搜尋功能。這也成為了我們為 Backstage 打造 AI 助手的靈感來源。我們計劃開發與 Mintlify 類似的功能,不僅能快速搜尋文件中的資訊,還能統整概念並提供範例程式碼。更重要的是,我們將使用本地運算資源,確保企業資料的安全性。
https://ithelp.ithome.com.tw/upload/images/20241006/20128232uTM5v6Kjxi.png

整合平台 - AnythingLLM

將 AnythingLLM 整合到 Backstage 之中,很適合將 LLM 分眾提供給使用者使用,達到內部使用的 ChatGPT 效果,避免敏感資料的外洩。
https://ithelp.ithome.com.tw/upload/images/20241006/20128232nV4OBrz9NI.png

  • 多用戶管理:AnythingLLM 提供靈活的用戶管理功能,我們能夠分配設定使用者權限與操作,並且隔離工作室的概念,適應不同使用者情境需求。
  • 靈活的 LLM 與向量資料庫選擇:我們可以根據需求搭配不同語言模型使用,還支援多款向量資料庫的搭配,例如使用擅長解讀程式的模型來解讀 API 文件,中文適性強的模型來產生文章等等。
  • 聊天與查詢模式:AnythingLLM 提供直接根據 LLM 的資料內容產生資料的問答模式,或是嚴格的上下文比對查詢模式,嚴謹並降低幻覺,並且還有許多參數可以調整。
  • 自定義聊天工具:可以將聊天功能直接嵌入網站,效果如同前面文章我們提到的 StagerAi 助手。

AnythingLLM API 功能

Anything LLM 內建非常方便的 API 功能,我們可以針對每個不同的 Workspace,搭配不同的語言模型去調用,並且可以透過此 API 上傳檔案進行 RAG 向量化資料,為特定 Workspace 加入避免出現嚴重的幻覺問題,或者可以直接透過 Anything LLM 調整參數,切換為查詢模式。功能豐富並且易於調整與管理權限,是個非常有潛力的項目。
https://ithelp.ithome.com.tw/upload/images/20241006/20128232ml8MkECYcR.png
使用者可以直接在介面上傳文件進行向量化,也可以透過 API 的形式使用,透過 API 非常適合加入到 Backstage 的插件開發中使用。例如我們可以將專案中的相關程式碼都加入到獨立的 Workspace 中向量化,為每個專案打造專屬的 AI 助手,再根據使用者目前檢視的頁面進行判斷、切換 Workspace 來回應。
https://ithelp.ithome.com.tw/upload/images/20241006/20128232b0OVYV8RyG.png

大型語言模型框架 - Ollama + HuggingFace

我們透過 Ollama 這個開源專案來執行 LLM,搭配 RTX 3090 顯示卡來運算開發,以測試用途來說這張顯卡綽綽有餘。啟用 Ollama 非常簡單,優點也是能夠一鍵啟動相關模型,我們利用它來測試多個知名 LLM,試圖找出比較適合我們的模型。另外一點是在 HuggingFace 這個平台上擁有更多 LLM 可供選擇,但是需要經過額外轉檔的設定,我們將不會在本篇討論到。

例如我們嘗試過體驗比較優秀的,國科會TAIDE計畫推出的模型,針對繁體中文與台灣相關的資訊特別精準,或是可以嘗試看看最近討論度很高的模型,LLM 自我檢查的特色,號稱效能超越 ChatGPT 4o。
taide/TAIDE-LX-7B-Chat · Hugging Face
https://ithelp.ithome.com.tw/upload/images/20241006/20128232dr4RCK4TLd.png
圖片來源 - https://tenten.co/learning/reflection-70b/
https://ithelp.ithome.com.tw/upload/images/20241006/20128232Fk6lBRpnlS.png

快速啟動

包含 Ollama 配套使用的 UI 介面、啟用顯示卡運算的選項以及 Anything ,我們可以透過以下 docker-compose.yaml 快速啟動。

anything:
  image: mintplexlabs/anythingllm:latest
  container_name: anything
  restart: always
  ports:
    - "3001:3001"
  volumes:
    - ./anythingllm/data:/app/server/storage
    - ./anythingllm/env.txt:/app/server/.env
  networks:
    - app-network

ollama:
  image: ollama/ollama:latest
  container_name: ollama
  ports:
    - 11434:11434
  volumes:
    - .:/code
    - ./ollama/ollama:/root/.ollama
  #pull_policy: always
  tty: true
  restart: always
  networks:
    - app-network
  deploy:
    resources:
      reservations:
        devices:
          - driver: nvidia
            count: 1
            capabilities: [gpu]

open-webui:
  image: ghcr.io/open-webui/open-webui:main
  container_name: open-webui
  volumes:
    - ./ollama/open-webui:/app/backend/data
  depends_on:
    - ollama
  ports:
    - 8080:8080
  environment:
    - OLLAMA_BASE_URL=http://ollama:11434
  extra_hosts:
    - host.docker.internal:host-gateway
  restart: unless-stopped
  networks:
    - app-network

利用 Ollama API 製作 AI 聊天助手

目前還在試作階段,我們先以直接調用 Ollama API ,並直接給予明確的資料來測試效果,我們可以接續前面 Day 17 : Backstage 插件開發 - 初探後端整合打造 LLM 聊天室 繼續將我們的後端串接上 Ollama API,為 Backstage 賦予最基本的 AI 助手功能。確認模式成熟與適合模型的選定後,可以再引入 AnythingLLM 進行細部客製化的功能整合,預計呈現的效果如下。
https://ithelp.ithome.com.tw/upload/images/20241006/20128232i9w2tWp4YB.png

做一個郵件內容摘要 - 後端

為了測試 LLM 的性能,我們選了一個已經在 Backstage 實現過的功能,並利用它來取得 Outlook 信件中的資料,我們可以先略過這方面的程式碼實現。假設可以透過此方法取得最新 5 封的郵件內文,並且先清除不重要的內容符號與格式等等,最後格式化輸出格式,方便 LLM 的解讀。

格式化信件的內容,使其更為整潔結構。
https://ithelp.ithome.com.tw/upload/images/20241006/20128232DnEbW0qkbS.png
呼叫 Ollama 的 Prompt 詞設定與相關基礎參數呼叫。
https://ithelp.ithome.com.tw/upload/images/20241006/20128232WhnEVwm0Op.png

做一個郵件內容摘要 - 前端

當後端資料成功執行並產生結果後,前端的處理相對就簡單許多,類似之前在 StagerAi 上線狀態時的方式接著寫下去。製作這部分時是為了能快速展示 Demo,因此大量依賴 AI 協助撰寫,後續也沒有特別進行優化,整體程式碼可能稍顯凌亂,且仍在開發中。以下內容僅供參考。

首先定義一個與後端 API 獲取資料的相關處理方法,可以在插件中新增一個 api.tsx 檔案。

import { useApi, configApiRef } from '@backstage/core-plugin-api';

export const useApiService = () => {
  const config = useApi(configApiRef);
  const baseUrl = config.getString('backend.baseUrl');

	// 呼叫後端檢查服務狀態
  const checkApiStatus = async () => {
    try {
      const response = await fetch(`${baseUrl}/api/ollama-api/status`);
      const data = await response.json();

      if (response.ok && data && data.apiStatus === 'ok') {
        return 'ok';
      } else {
        return 'error';
      }
    } catch (error) {
      console.error('Error checking API status:', error);
      return 'error';
    }
  };
	
	// 呼叫後端取得信件摘要
  const fetchEmailSummary = async () => {
    try {
      const response = await fetch(`${baseUrl}/api/ollama-api/email-summary`);
      const data = await response.json();

      if (response.ok && data && data.summary) {
        return data.summary;
      } else {
        return '無法獲取郵件摘要,請稍後再試。';
      }
    } catch (error) {
      console.error('Error fetching email summary:', error);
      return '獲取郵件摘要時發生錯誤,請稍後再試。';
    }
  };
	
	// 傳遞使用者輸入內容
  const sendMessage = async (newMessage: string) => {
    try {
      const response = await fetch(`${baseUrl}/api/ollama-api/chat`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message: newMessage }),
      });
      const data = await response.json();

      if (response.ok && data && data.reply) {
        return data.reply;
      } else {
        return '無法處理您的請求,請再試一次。';
      }
    } catch (error) {
      console.error('Error sending message:', error);
      return '發生錯誤,請稍後再試。';
    }
  };

  return {
    checkApiStatus,
    fetchEmailSummary,
    sendMessage,
  };
};

繼續拓展基於 react-chat-widget 的聊天室互動前端功能。

import React, { useEffect, useState } from 'react';
import {
  Widget,
  addResponseMessage,
  addUserMessage,
  deleteMessages,
  setQuickButtons,
} from 'react-chat-widget';
import { makeStyles } from '@material-ui/core/styles';
import { useApiService } from './api'; // 引入 API 模塊

import 'react-chat-widget/lib/styles.css';

const useStyles = makeStyles(() => ({
  '@global': {
    '.rcw-messages-container': {
      padding: '8px',
    },
    '.rcw-conversation-container .rcw-header': {
      backgroundColor: 'rgba(178, 34, 34)',
      boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.4)',
      backdropFilter: 'blur(10px)',
    },
    '.rcw-message .rcw-response': {
      backgroundColor: '#FFDAB9',
      color: '#000000',
      borderRadius: '18px',
    },
    '.rcw-message .rcw-response .rcw-message-text': {
      maxWidth: '500px',
    },
    '.rcw-message .rcw-client .rcw-message-text': {
      backgroundColor: '#FFDAB9',
      color: '#B22222',
      borderRadius: '18px',
    },
    '.rcw-launcher': {
      backgroundColor: '#B22222',
      '&:hover': {
        backgroundColor: '#CD5C5C',
      },
    },
    '.rcw-close-button': {
      background: 'none',
      border: 'none',
      padding: '8px',
      cursor: 'pointer',
      color: '#ffffff',
    },
    '.rcw-new-message': {
      color: '#999999',
      width: '100%',
      cursor: 'not-allowed',
      pointerEvents: 'none',
      backgroundColor: '#f5f5f5',
    },
    '.rcw-picker-btn': {
      display: 'none',
    },
    '.quick-button': {
      border: '2px solid #FFDAB9',
      borderRadius: '12px',
      padding: '10px 20px',
      background: 'linear-gradient(135deg, #FFDAB9, #FFA07A)',
      color: '#B22222',
      transition: 'background 0.3s, transform 0.3s',
      '&:hover': {
        backgroundColor: '#CD5C5C',
        transform: 'scale(1.05)',
      },
    },
  },
}));

export const StageraiChat = () => {
  const classes = useStyles();
  const [apiStatus, setApiStatus] = useState<'ok' | 'error' | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const { checkApiStatus, sendMessage, fetchEmailSummary } = useApiService();

	// 定時檢查
  useEffect(() => {
    const checkStatus = async () => {
      const status = await checkApiStatus();
      setApiStatus(status);
      if (status === 'ok') {
        addResponseMessage('嗨! 我隨時都在');
        setQuickButtons([
          { label: '查看郵件摘要', value: 'email_summary' },
        //   { label: '其他幫助', value: 'other_help' },
        ]);
      } else {
        addResponseMessage('🔴 StagerAi 目前離線,請稍後再試。');
      }
    };

    checkStatus();

    const intervalId = setInterval(checkStatus, 300000);
    return () => clearInterval(intervalId);
  }, []);
  
  // 模擬一個一個字串流輸出的效果
  const typeMessage = (message, callback, onComplete) => {
    let index = 0;
    const length = message.length;
    const interval = 10;
  
    const type = () => {
      if (index < length) {
        if (index > 0) {
          deleteMessages(1);
        }
        callback(message.substring(0, index + 1));
        index++;
        setTimeout(type, interval);
      } else {
        onComplete();
      }
    };
  
    type();
  };
	
	// 快速按鈕事件處理
  const handleQuickButtonClicked = (buttonValue: string) => {
    if (isLoading) return;
    if (buttonValue === 'email_summary') {
      addUserMessage('嘿! Stager,摘要近5封郵件');
      addResponseMessage('好的,處理中...');
      handleFetchEmailSummary();
    } else if (buttonValue === 'other_help') {
      addResponseMessage('現在還沒辦法幫你。');
    }
  };

  // react-chat-widget 相關功能
  const handleNewUserMessage = async (newMessage: string) => {
    setIsLoading(true);
    const responseMessage = await sendMessage(newMessage);
    typeMessage(
      responseMessage, 
      (msg: string) => addResponseMessage(msg),
      () => setIsLoading(false)
    );
  };
  
  // 呼叫前端取得信件方法
  const handleFetchEmailSummary = async () => {
    setIsLoading(true);
    const summary = await fetchEmailSummary();
    typeMessage(
      summary, 
      (msg: string)  => addResponseMessage(msg),
      () => setIsLoading(false)
    );
  };

	// react-chat-widget 自定義外觀
	const subtitle = apiStatus === 'ok' ? '🟢 在線中' : '🔴 離線';
  return (
    <div>
      <Widget
        handleNewUserMessage={handleNewUserMessage}
        handleQuickButtonClicked={handleQuickButtonClicked}
        title="StagerAI 幫手"
        subtitle={subtitle}
        profileAvatar="https://i.imgur.com/7UVRSVY.png"
        launcherOpenLabel="開啟聊天窗口"
        launcherCloseLabel="關閉聊天窗口"
        senderPlaceHolder="目前尚未支援傳送訊息"
        showCloseButton={true}
        showTimeStamp={false}
        emojis={true}
        resizable={true}
      />
    </div>
  );
};

結論

利用 Ollama 與 AnythingLLM 搭配打造本地化 AI 助手,目前是短期想測試的目標,由於在本地化大型語言模型的極限與適性,對我們來說還有許多未知處。在使用這些語言模型時,我們可以發現性能上其實與 ChatGPT 這類主流線上服務相差甚遠,當然使用的設備上也有一定的差距,我們計畫先行做出可用的應用場景,再慢慢提升效能的部分。本篇文章接續前面的 Backstage 插件開發 - 初探後端整合打造 LLM 聊天室 將後續的邏輯程式實現,在後續的開發方向,我們仍需要針對模型的 RAG、Fine-tuning 開始細部客製化模型的資料,並思考搭配 Anyting LLM 的使用場境。本篇文章提供了開發範例給讀者參考。

參考資料

https://mintlify.com/
https://medium.com/@pang2258/anythingllm-ollama輕鬆架設多人用的客製化-rag-2d05954bf771
https://tenten.co/learning/reflection-70b/


上一篇
Day 26 : 邁向上線的最後一道牆 - Backstage 的三層權限框架概念
下一篇
Day 28:Backstage 專題開發 - Azure DevOps Boards API 整合工作卡片看板
系列文
Backstage : 打造企業內部開發者整合平台30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言