GarethNg

Gareth Ng

With a bamboo staff and straw sandals, I feel lighter than riding a horse, In a cloak amidst the misty rain, I live my life as it comes.
github
email
x
telegram

在 Mac 用 LM studio 部署本地大模型(DeepSeek/Qwen) + 翻譯

得益於 Mac 的 CPU 和 GPU 共享內存,以及大的內存帶寬,使得使用 macBook 運行本地大模型成為可能,借著最近 DeepSeek 大火的東風,我也嘗試在本地構建了一套 AI 翻譯的系統。本文將會介紹如何在 Mac 電腦上正確的配置這套系統。設置完成後,你將可以

  • 在 Mac 上免費使用大語言模型進行對話
  • 無需等待伺服器響應,提高效率
  • 快速翻譯任何文檔,截圖,網頁等

本文以 macBook 為例,理論上 windows 電腦也可以獲得同樣的效果,僅供參考

本文下載工具在中國大陸可能會遇到網絡問題,請自行解決

模型管理工具#

所謂模型管理工具,就是可以本地運行管理大模型的工具,並且可以提供伺服器功能,這樣可以省去一些沒有必要的麻煩。此外,模型管理工具還可以提供本地聊天的功能,這樣在網絡不暢的情況下,也可以使用到最近最火的 DeepSeek。

比較流行的模型管理工具有 OllamaLM Studio. 這兩個工具比起來,LM Studio 有一個 GUI 頁面,下載模型也更方便,對新手比較友好。所以本文將使用 LM Studio。

安裝模型#

如何找到合適的模型是一切開始的關鍵,當下比較流行的開源大模型有 deepseek-r1,Qwen,LLama 等,根據需要選擇你喜歡的。作者需要使用到中文 - 英文翻譯,所以選擇了中文更友好的 deepseek 和 Qwen(千問)。

然後是根據自己 Mac 的配置選擇合適的模型大小。LM Studio 會把當前配置無法下載的模型禁止掉,當然即使可以使用,這和使用的舒服也有一定的區別。

作者的配置是 MacBook Pro M3 Max 36G 內存,測試過程中,32B 大小的 DeepSeek R1 是可以正常使用的,但是運行速度會比較慢,簡單對話沒有問題,但是翻譯場景,尤其是比較大型的 PDF 就很讓人著急,再加上 DeepSeek R1 還有大量推理過程,32B 模型的速度就更慢了。當然如果擁有更好配置的機器,尤其是大內存,肯定是越大的模型效果越好,這一點豐儉由人。

關於 DeepSeek R1 還有一點要說,DeepSeek R1 會展示出一個長的思維鏈,這一點固然很棒,但是在翻譯的場景下,思維鏈其實並不是必須的,甚至是累贅,拖慢翻譯速度,相比而言 Qwen 模型在這個場景下就是更好的那個選擇,後文會給出一個解決思維鏈的方案。

總結一下,模型很多,各有利弊。根據自己的需求,配置選擇合適的模型即可。我使用了 qwen2.5-7b-instruct-1m 用來翻譯(14B 應該也沒什麼問題)。

可以參考下圖進行下載安裝。

image

啟動服務#

接下來就是加載模型,啟動服務,按照下圖即可。

image

image

一旦加載成功,就可以使用這個大模型了。左邊欄最上方一個是對話功能,在這裡可以和你加載的大模型對話。

image

同時還可以複製代碼到命令行檢查模型是否正常運行以及服務是否正常啟動。到這裡,大模型相關的配置就結束了,恭喜你,已經獲得了一個可以運行在本地,不受伺服器影響,快速響應的,專屬於你的大模型了。

如果你把這個服務開放在局域網上,乃至公網,你就可以在其他設備上訪問到你的大模型了,這就是另一個故事了。

image

image

翻譯 - Easydict#

本文使用了一個開源的本地翻譯工具 Easydict,當然如果有你發現了其他工具也可以使用。本文僅以此為例。

安裝#

你可以使用下面兩種方式之一安裝。

Easydict 最新版本支持系統 macOS 13.0+,如果系統版本為 macOS 11.0+,請使用 2.7.2

1. 手動下載安裝#

下載 最新版本的 Easydict。

2. Homebrew 安裝#

brew install --cask easydict

配置#

安裝成功後,點擊按鈕,選擇配置,進入配置頁面。
然後點擊服務,配置我們自己的伺服器地址,這裡其實選擇 ollama 翻譯 和 自定義 OpenAI 翻譯理論上都可以。

輸入自己的伺服器地址端口和模型名稱即可,這些在 LM Studio 頁面都能找到。

image

image

使用#

配置會之後,就可以正常使用了。關於 Easydict 的具體使用方法,請參考對應的官方文檔

image

另外要說明的是,EasyDict 也支持其他形式的 API,也內置了翻譯,不需要前面本地大模型那一套,本身也是一個很好用的應用。

翻譯 - 沉浸式翻譯#

沉浸式翻譯幾乎是 OpenAI 橫空出世之後最火的一個瀏覽器翻譯插件了。它也支持使用自定義 API 接口來進行翻譯,且同時支持網頁翻譯和 PDF 翻譯,且翻譯的展示效果非常優秀。

可以支持一個 pro 會員,這樣就無需折騰,開箱即用。如果願意繼續折騰本地大模型,就往後看吧。

這是官網鏈接

image

下載完插件之後,進入配置頁面,還是同樣輸入 API 地址和模型名稱,就可以使用本地大模型進行沉浸式翻譯了。

image

image

伺服器轉發#

走到這一步,幾乎可以使用了,但是你會遇到以下問題

  1. deepseek R1 的翻譯結果會帶上思維鏈,影響翻譯體驗
  2. 沉浸式翻譯的 API 格式和 LM Studio 的 API 格式不能無縫對接,所以即使 API 通了,沉浸式翻譯也無法顯示翻譯結果

所以,不得已,我用 Python web.py 寫了一個最簡單的本地伺服器,用來轉發請求,並在中間做一些微小的工作,解決上述兩個小問題,使得翻譯體驗更好。這裡附上代碼,僅供參考。web.py 的默認端口是 8080,也可以按需修改成你需要的

記得要安裝 Python 並使用 pip install webpy
這裡不再贅述

import web
import json
import requests
import re
import time

# 配置URLs路由
urls = (
    '/v1/chat/completions', 'ChatCompletions',
    '/v1/models', 'Models'
)

def add_cors_headers():
    # 添加CORS相關的響應頭
    web.header('Access-Control-Allow-Origin', '*')
    web.header('Access-Control-Allow-Credentials', 'true')
    web.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
    web.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')

def remove_think_tags(text):
    # 移除<think>標籤及其內容
    return re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)

class ChatCompletions:
    def OPTIONS(self):
        # 處理預檢請求
        add_cors_headers()
        return ''
        
    def POST(self):
        web.header('Content-Type', 'application/json')
        add_cors_headers()
        
        try:
            data = json.loads(web.data())
            lm_studio_url = "http://localhost:1234/v1/chat/completions"
            
            # 檢查是否為流式請求
            is_stream = data.get('stream', False)
            
            # 轉發請求到LM Studio
            response = requests.post(
                lm_studio_url,
                json=data,
                headers={'Content-Type': 'application/json'},
                stream=is_stream  # 設置stream參數
            )
            
            if is_stream:
                # 對於流式請求,先收集完整內容
                full_content = ""
                current_id = None
                
                def generate_stream():
                    nonlocal full_content, current_id
                    
                    for line in response.iter_lines():
                        if line:
                            line = line.decode('utf-8')
                            if line.startswith('data: '):
                                line = line[6:]
                            if line == '[DONE]':
                                # 處理完整內容並發送最後一個塊
                                cleaned_content = remove_think_tags(full_content)
                                # 發送清理後的完整內容
                                final_chunk = {
                                    "id": current_id,
                                    "object": "chat.completion.chunk",
                                    "created": int(time.time()),
                                    "model": "local-model",
                                    "choices": [{
                                        "index": 0,
                                        "delta": {
                                            "content": cleaned_content
                                        },
                                        "finish_reason": "stop"
                                    }]
                                }
                                yield f'data: {json.dumps(final_chunk)}\n\n'
                                yield 'data: [DONE]\n\n'
                                continue
                                
                            try:
                                chunk_data = json.loads(line)
                                current_id = chunk_data.get('id', current_id)
                                
                                if 'choices' in chunk_data:
                                    for choice in chunk_data['choices']:
                                        if 'delta' in choice:
                                            if 'content' in choice['delta']:
                                                # 累積內容而不是直接發送
                                                full_content += choice['delta']['content']
                                
                                # 發送空的進度更新
                                progress_chunk = {
                                    "id": current_id,
                                    "object": "chat.completion.chunk",
                                    "created": int(time.time()),
                                    "model": "local-model",
                                    "choices": [{
                                        "index": 0,
                                        "delta": {},
                                        "finish_reason": None
                                    }]
                                }
                                yield f'data: {json.dumps(progress_chunk)}\n\n'
                                
                            except json.JSONDecodeError:
                                continue
                
                web.header('Content-Type', 'text/event-stream')
                web.header('Cache-Control', 'no-cache')
                web.header('Connection', 'keep-alive')
                return generate_stream()

            else:
                # 非流式請求的處理
                response_data = json.loads(response.text)
            
                if 'choices' in response_data:
                    for choice in response_data['choices']:
                        if 'message' in choice and 'content' in choice['message']:
                            choice['message']['content'] = remove_think_tags(
                                choice['message']['content']
                            )
                return json.dumps(response_data)
            
        except Exception as e:
            print(e)
            return json.dumps({
                "error": {
                    "message": str(e),
                    "type": "proxy_error"
                }
            })

class Models:
    def OPTIONS(self):
        # 處理預檢請求
        add_cors_headers()
        return ''
        
    def GET(self):
        web.header('Content-Type', 'application/json')
        add_cors_headers()
        # 返回一個模擬的模型列表
        return json.dumps({
            "data": [
                {
                    "id": "local-model",
                    "object": "model",
                    "owned_by": "local"
                }
            ]
        })

if __name__ == "__main__":
    app = web.application(urls, globals())
    app.run() 

到這裡,一個在 Mac 上運行本地部署的大模型,再加上網頁、PDF 文檔、文字翻譯的工具鏈就完成了。這其中可以替代的選項還有很多。
模型部署還有 ollama 等,大模型還可以用 Phi、llama 等,轉發伺服器也可以用其他方案,或者可能還有不用轉發的選項可以研究,翻譯工具也可以用 Bob 來代替。總而言之,技術選擇有很多,本文只是提供一個參考。時間還是要花在更重要的事上,已經有了利器,趕快去善其事吧。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。