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 来代替。总而言之,技术选择有很多,本文只是提供一个参考。时间还是要花在更重要的事上, 已经有了利器,赶快去善其事吧。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。