因为需要离线部署测试模型,但又不知道怎么写Ollama创建模型时用的Modelfile,可以先在外网利用Ollama的pull功能加载模型,然后就会自动在存放模型文件的“blobs”文件夹下,你可以用记事本打开一些小体积的文件,就会发现里面有配置信息,但是这个格式不是我需要的Modelfile的格式。于是可以从Ollama导出标准配置文件。
导出配置文件方法:
1:使用命令查看有哪些模型已经创建,获取标准名称。
        ollama list

2:使用命令导出指定模型配置文件(参考上图)。
ollama show --modelfile 模型名称 > 保存的配置文件名

3:接下来有个坑,你用记事本打开这个导出的模型文件,会发现部分乱码。如下图:

4:为了解决乱码问题,不得已借助AI写了个解析/替换乱码的程序。

目前该程序只能解决常见的几个乱码问题,不能解决中文字符乱码的问题(主要是我也不知道标准Modelfile怎么写,没有标准还原后的格式,也就推不出应该怎么还原),但这个问题不大,因为你可以从“blobs”文件夹下找到对应中文系统设定词(那些小体积的文件用记事本打开,挨个找,总找得到)。
转换程序源码如下:
 

import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox, ttk
import os
import re

class OllamaModelfileConverter:
    def __init__(self, root):
        self.root = root
        self.root.title("Ollama Modelfile 编码修复工具")
        self.root.geometry("900x700")
        self.root.minsize(700, 500)
        
        self.raw_content = None
        self.setup_ui()
        
    def setup_ui(self):
        # 顶部框架 - 文件选择
        top_frame = tk.Frame(self.root, pady=10)
        top_frame.pack(fill=tk.X, padx=10)
        
        tk.Label(top_frame, text="选择Modelfile文件:").pack(side=tk.LEFT, padx=5)
        
        self.file_path_var = tk.StringVar()
        tk.Entry(top_frame, textvariable=self.file_path_var, width=50).pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
        
        tk.Button(top_frame, text="浏览", command=self.browse_file).pack(side=tk.LEFT, padx=5)
        
        # 按钮框架
        button_frame = tk.Frame(self.root, pady=5)
        button_frame.pack(fill=tk.X, padx=10)
        
        tk.Button(button_frame, text="全选", command=self.select_all).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="复制到剪贴板", command=self.copy_text).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="保存为UTF-8文件", command=self.save_as_utf8).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="清除", command=self.clear_text).pack(side=tk.LEFT, padx=5)
        
        # 文本区域 - 使用Notebook创建多标签页
        self.notebook = ttk.Notebook(self.root)
        self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 转换后内容标签页
        converted_frame = tk.Frame(self.notebook)
        self.notebook.add(converted_frame, text="修复后的Modelfile")
        
        self.text_area = scrolledtext.ScrolledText(converted_frame, wrap=tk.WORD, font=("Consolas", 10))
        self.text_area.pack(fill=tk.BOTH, expand=True)
        
        # 原始内容标签页
        original_frame = tk.Frame(self.notebook)
        self.notebook.add(original_frame, text="原始内容")
        
        self.original_text_area = scrolledtext.ScrolledText(original_frame, wrap=tk.WORD, font=("Consolas", 10))
        self.original_text_area.pack(fill=tk.BOTH, expand=True)
        
        # 状态栏
        self.status_var = tk.StringVar(value="准备就绪")
        status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        
        # 绑定快捷键
        self.text_area.bind("<Control-a>", self.select_all)
        self.text_area.bind("<Control-A>", self.select_all)
        self.original_text_area.bind("<Control-a>", self.select_all)
        self.original_text_area.bind("<Control-A>", self.select_all)
        
    def browse_file(self):
        file_path = filedialog.askopenfilename(
            title="选择Modelfile文件",
            filetypes=[("所有文件", "*.*"), ("Modelfile", "Modelfile*")]
        )
        if file_path:
            self.file_path_var.set(file_path)
            self.load_and_fix_modelfile(file_path)
    
    def load_and_fix_modelfile(self, file_path):
        try:
            # 读取文件数据
            with open(file_path, 'rb') as f:
                data = f.read()
            
            # 首先尝试UTF-16LE编码(带BOM标记)
            if data.startswith(b'\xff\xfe'):
                original_content = data.decode('utf-16-le')
                self.status_var.set("检测到UTF-16LE编码")
            elif data.startswith(b'\xfe\xff'):
                original_content = data.decode('utf-16-be')
                self.status_var.set("检测到UTF-16BE编码")
            else:
                # 如果没有BOM,尝试不同的编码
                encodings = ['utf-8', 'utf-16-le', 'utf-16-be', 'gbk', 'gb18030']
                for encoding in encodings:
                    try:
                        original_content = data.decode(encoding)
                        self.status_var.set(f"使用{encoding}编码成功")
                        break
                    except UnicodeDecodeError:
                        continue
                else:
                    # 如果所有编码都失败,使用utf-16-le并替换错误字符
                    original_content = data.decode('utf-16-le', errors='replace')
                    self.status_var.set("使用UTF-16LE编码处理(包含替换字符)")
            
            # 显示原始内容
            self.original_text_area.delete(1.0, tk.END)
            self.original_text_area.insert(tk.END, original_content)
            
            # 修复Ollama特殊标记
            fixed_content = self.fix_ollama_special_tags(original_content)
            
            # 显示修复后的内容
            self.text_area.delete(1.0, tk.END)
            self.text_area.insert(tk.END, fixed_content)
            
            # 保存用于保存的内容
            self.raw_content = fixed_content
            
            self.status_var.set(f"文件已成功加载和修复: {os.path.basename(file_path)}")
            
        except Exception as e:
            messagebox.showerror("错误", f"处理文件时发生错误: {str(e)}")
            self.status_var.set("处理失败")
    
    def fix_ollama_special_tags(self, content):
        """修复Ollama特殊标记"""
        # 常见的特殊标记映射
        special_tags = {
            # 使用正则表达式匹配常见的乱码模式并将其替换为可能的原始标记
            r'<\x1d\x95\xef\x6d\x73\x00\x65\x00\x72\x00\x1d\x95\x3f\x00': '<|User|>',
            r'<\x1d\x95\xce\x6d\x73\x00\x73\x00\x69\x00\x73\x00\x74\x00\x61\x00\x6e\x00\x74\x00\x1d\x95\x3f\x00': '<|Assistant|>',
            r'<\x1d\x95\x06\x6e\x6e\x00\x64\x00\x3b\x92\x77\x4e\x66\x00\x3b\x92\x7b\x4e\x65\x00\x6e\x00\x74\x00\x65\x00\x6e\x00\x63\x00\x65\x00\x1d\x95\x3f\x00': '<|end_of_sentence|>',
            r'<\x1d\x95\x02\x6e\x65\x00\x67\x00\x69\x00\x6e\x00\x3b\x92\x77\x4e\x66\x00\x3b\x92\x7b\x4e\x65\x00\x6e\x00\x74\x00\x65\x00\x6e\x00\x63\x00\x65\x00\x1d\x95\x3f\x00': '<|begin_of_sentence|>',
            
            # 简化版本的模式,可能在不同导出版本中出现
            r'<锝淯ser锝\?': '<|User|>',
            r'<锝淎ssistant锝\?': '<|Assistant|>',
            r'<锝渆nd鈻乷f鈻乻entence锝\?': '<|end_of_sentence|>',
            r'<锝渂egin鈻乷f鈻乻entence锝\?': '<|begin_of_sentence|>'
        }
        
        # 特殊处理用于GBK编码错误的中文
        # 这是很多中文被GBK错误解码后的特征模式
        chinese_pattern = r'浣犳槸閫氫箟鍗冮棶(.*?)鍥炵瓟'
        if re.search(chinese_pattern, content):
            try:
                # 尝试修复中文 - 这种方法可能不完美,但可以尝试
                # 先将内容转为bytes,假设它是GBK编码错误导致的
                gbk_encoded = content.encode('latin1')  # 使用latin1保持字节不变
                # 然后尝试用GBK解码
                fixed_content = gbk_encoded.decode('gbk', errors='replace')
                # 如果解码后看起来更合理,则使用它
                return fixed_content
            except:
                pass  # 如果失败,继续使用原始内容

        # 应用所有特殊标记替换
        fixed_content = content
        for pattern, replacement in special_tags.items():
            fixed_content = re.sub(pattern, replacement, fixed_content)
        
        # 修复PARAMETER行中可能的乱码
        fixed_content = re.sub(r'PARAMETER\s+stop\s+<锝.*?\?', lambda m: m.group(0).replace(m.group(0), 'PARAMETER stop "<|special_tag|>"'), fixed_content)

        return fixed_content
    
    def select_all(self, event=None):
        # 根据当前活动的标签页选择文本区域
        current_tab = self.notebook.index(self.notebook.select())
        if current_tab == 0:  # 修复后内容
            self.text_area.tag_add(tk.SEL, "1.0", tk.END)
            self.text_area.mark_set(tk.INSERT, "1.0")
            self.text_area.see(tk.INSERT)
        else:  # 原始内容
            self.original_text_area.tag_add(tk.SEL, "1.0", tk.END)
            self.original_text_area.mark_set(tk.INSERT, "1.0")
            self.original_text_area.see(tk.INSERT)
        return 'break'  # 防止默认行为
    
    def copy_text(self):
        # 根据当前活动的标签页复制文本
        current_tab = self.notebook.index(self.notebook.select())
        if current_tab == 0:  # 修复后内容
            if self.text_area.tag_ranges(tk.SEL):
                self.root.clipboard_clear()
                self.root.clipboard_append(self.text_area.get(tk.SEL_FIRST, tk.SEL_LAST))
                self.status_var.set("修复后文本已复制到剪贴板")
            else:
                self.status_var.set("未选择文本")
        else:  # 原始内容
            if self.original_text_area.tag_ranges(tk.SEL):
                self.root.clipboard_clear()
                self.root.clipboard_append(self.original_text_area.get(tk.SEL_FIRST, tk.SEL_LAST))
                self.status_var.set("原始文本已复制到剪贴板")
            else:
                self.status_var.set("未选择文本")
    
    def save_as_utf8(self):
        if not self.raw_content:
            messagebox.showinfo("提示", "没有内容可保存")
            return
            
        # 获取当前标签页内容
        current_tab = self.notebook.index(self.notebook.select())
        if current_tab == 0:
            content = self.text_area.get(1.0, tk.END)
        else:
            content = self.original_text_area.get(1.0, tk.END)
            
        # 默认使用原文件名,但添加.utf8后缀
        default_name = os.path.basename(self.file_path_var.get())
        if not default_name:
            default_name = "Modelfile.utf8"
        else:
            default_name = f"{default_name}.utf8"
            
        file_path = filedialog.asksaveasfilename(
            defaultextension=".utf8",
            initialfile=default_name,
            filetypes=[("UTF-8文件", "*.utf8"), ("所有文件", "*.*")]
        )
        if file_path:
            try:
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(content)
                self.status_var.set(f"文件已保存至: {file_path}")
                messagebox.showinfo("成功", "文件已成功保存为UTF-8编码")
            except Exception as e:
                messagebox.showerror("保存错误", f"保存文件时发生错误: {str(e)}")
    
    def clear_text(self):
        self.text_area.delete(1.0, tk.END)
        self.original_text_area.delete(1.0, tk.END)
        self.file_path_var.set("")
        self.raw_content = None
        self.status_var.set("内容已清除")

if __name__ == "__main__":
    root = tk.Tk()
    app = OllamaModelfileConverter(root)
    root.mainloop()

至此,问题总算基本解决,虽然不完美,但已经不影响使用。
有了这个方法,就可以从魔搭社区等地方下载GGUF格式的大模型文件,直接配置到离线服务器了。直接从网页下载是很慢的,但是有个非常好的办法,先把大模型链接导入迅雷,下载到迅雷云盘,然后通过迅雷云盘高速下载到本地。迅雷就是干下载的,能跑满宽带,此方法下载AI大模型文件非常快。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐