这段时间一直在倒腾本地大模型推理试了不少方案最后留在手里的还是 Ollama。说它是大模型的推理应用其实不太准确更该叫它一个轻量级的推理运行时一条命令把模型拉下来一条命令起服务再配上一个统一的 HTTP 接口就能把事情串起来。更关键的是它支持自定义模型配置、可以加载微调后的模型也能配合向量库做 RAG底层还是 Go 写的单文件部署很省心。这篇文章不是官方文档的复述是我自己从模型管理、词库定制、微调导入、RAG 落地到用 Go 写上层服务这一整套链路里踩出来的经验适合刚入门本地推理、又不想被复杂工程方案劝退的开发者参考。1. 为什么我最终留下 Ollama 作为本地推理底座1.1 一次部署覆盖三件事模型、词库、RAG 全部打通我第一次接触 Ollama 的时候并没有把它当回事觉得无非是个模型下载器。真正让我改观的是它把模型加载、参数配置、接口服务这几件事揉在了一起。之前自己搭推理环境大概要经历这么几步先去模型平台下载权重再写一段加载代码还得自己处理 tokenizer、上下文窗口、采样参数最后起一个 HTTP 服务给上层调用。中间任何一步不顺手排查起来都非常消耗时间。Ollama 的模型管理逻辑不一样它用Modelfile这种类似 Dockerfile 的描述文件把基础模型、系统提示词、推理参数、模板这些打包成一个运行时配置。想换模型改几行配置就行不用动上层代码。词库这个词在 Ollama 的语境里我更愿意把它理解成两层含义。第一层是模型自带的 tokenizer 词表这部分 Ollama 不直接提供编辑能力除非你重新训练或者调整分词器。第二层才是工程上真正用得上的领域术语表、专业词汇库把这些内容通过系统指令或 RAG 注入模型让它在特定场景下说人话。这两件事很多教程混在一起讲但落地路径完全不同后面我会单独展开。RAG 的部分更有意思。Ollama 本身不提供向量数据库但它提供了两个关键能力一个是直接跑 embedding 模型并输出向量另一个是标准的聊天与生成接口。这两个能力一组合一个最简单的 RAG 链路就立起来了。不需要额外起一个推理框架只需要把检索逻辑放在中间层。1.2 Go 开发的推理服务到底带来了什么实际优势Ollama 是 Go 写的这件事不是技术选型上的偶然而是直接决定了它的部署形态和运维方式。最直观的优势是单二进制部署。Go 编译出来的程序没有额外的运行时依赖拷贝过去就能跑。我在实际操作中通常只需要把可执行文件和模型目录放在一起配上环境变量就能提供服务。相比那些依赖 Python 环境和一大堆 pip 包的工具这省掉了非常多的环境问题。第二个优势是并发模型的处理方式。Go 的 goroutine 在处理多个请求的时候开销很小Ollama 内部的请求调度、模型加载、队列管理都受益于此。如果你在本地起服务同时给多个终端或上层应用提供模型能力用 Go 写的服务在资源占用上会明显克制。第三个优势其实容易被忽略既然 Ollama 本身是 Go 写的那么上层应用也用 Go 写技术栈就是统一的。模型服务的客户端、请求调度、业务逻辑可以全部放进一个二进制里部署的时候只要考虑模型文件和配置不需要再额外拉一个 Python 运行时。1.3 它的真实定位推理运行时不是微调工具这里我想先把一个很多人搞混的概念捋清楚。标题里说Ollama 支持模型微调严谨一点讲Ollama 本身不做微调训练它做的是加载和推理。它能支持微调体现在两个地方一是你可以用外部的训练工具对某个开源模型做微调然后把它导出成 GGUF 格式再通过 Ollama 加载使用。二是 Ollama 的Modelfile可以在不重新训练的情况下通过修改系统提示词、温度参数、上下文长度等方式让同一个基础模型表现出不同的行为风格。这两种方式都被用户笼统地叫做微调但本质差距很大。如果你把 Ollama 当成一个训练框架去用一定会碰壁。正确的思路是把它放在整个链路的最末端训练端负责改变权重Ollama 负责把改变后的模型跑起来并且跑得足够稳。2. 模型管理、量化与自定义词库先把架子搭起来2.1 拉取模型与量化档位选择别只看 GGUF 后缀Ollama 的模型文件是 GGUF 格式这一点大家应该都知道了。但真正上手之后你会发现选哪个量化版本这个问题比选哪个模型更容易踩坑。GGUF 的量化档位很多常见的有 q2_k、q3_k、q4_0、q4_k_m、q5_k_m、q8_0 这些。档位越高体积越大精度越接近原始权重。我的选择经验是量化档位相对体积适合场景注意事项q2_k / q3_k很小纯测试、低显存环境输出质量下降明显复杂任务容易胡说q4_0 / q4_k_m适中日常对话、轻量任务性价比最高多数场景的首选q5_k_m较大对输出质量要求较高显存足够时推荐能明显减少质量损失q8_0很大接近原版效果显存充足再考虑否则容易 OOM选量化不只看显存大小还要看模型的参数量。同样是 4bit 量化7B 模型和 13B 模型的资源占用完全不同。我自己的判断标准是先把模型拉到本地用ollama show查看大小和参数信息再结合自己的显存来决定跑不跑、跑哪个档位。还有一个细节同一模型的 q4 和 q5 版本在 Ollama 里是不同的标签拉取和加载的时候要写全否则系统可能用默认标签。默认标签不一定是最优量化。2.2 Modelfile 定制系统指令、上下文窗口与采样参数如果说拉模型是把一个通用工具搬到本地那么Modelfile就是给这个工具装上手柄。它负责描述这个模型应该以什么身份、什么风格、什么规则来运行。一个实际可用的Modelfile大致长这样FROM qwen2.5:7b-instruct-q4_k_m SYSTEM 你是一个严谨的技术文档助手。回答尽量使用中文代码示例必须给出完整可运行版本。 参数不能虚构涉及具体数值时如果不知道请明确说不知道不要编造。 PARAMETER temperature 0.3 PARAMETER top_p 0.9 PARAMETER num_ctx 8192 PARAMETER stop |im_end|这里有几个参数值得拆开说。temperature控制随机性。做知识问答和代码生成我一般压到 0.2 到 0.4 之间做头脑风暴、创意写作再调到 0.7 以上。如果你发现模型输出开始胡说八道先不要急着换模型看看温度是不是太高了。num_ctx是上下文窗口长度。Ollama 里默认值不一定能覆盖你需要的长度如果你把长文档一次性塞进对话模型记不住前面的内容往往就是窗口不够。这个参数调大会增加显存消耗不是越大越好。stop参数用来指定结束标记。不同模型的模板不同如果你的对话接口总把结尾标记一起输出多半是 stop 没有配好。模型 tag 对应的模板里面通常已经有停止标记但自定义的时候建议显式写一遍。定义好Modelfile之后用以下命令创建模型ollama create my-assistant -f ./Modelfile这条命令做的事情类似打包它把基础模型和配置合并成一个新的本地模型标签。这里不需要重新下载权重所以速度很快。这就是参数级别微调的落地方式适合做行为定制。2.3 领域词库的落地思路术语表进入 Prompt 而非改词表很多做垂直领域应用的开发者会纠结一个问题我想让模型懂我这一行的专业术语应该怎么给 Ollama 配置词库首先要接受一个现实Modelfile里面没有词汇表这个字段。模型的 tokenizer 词表是在训练阶段冻结的Ollama 没有提供直接编辑词表的入口想靠改词表让模型认识新词需要走重新分词和增量训练的路子工程成本很高。实际工程里更通用的做法是把术语表当作一种上下文资源让模型在推理时现学现用。最简单的方案是把术语表写进系统指令FROM qwen2.5:7b-instruct-q4_k_m SYSTEM 你是某行业的知识助手。以下是行业术语对照表回答时必须使用标准术语禁止使用口语化替代词。 - 工单: 用户提交的服务请求记录 - SLA: 服务等级协议指响应和解决时限的承诺 - 故障单: 已进入处理流程且需要跟踪的异常事件 ...这种方式的缺点是术语表一旦很大会占据上下文窗口的空间。我见过有人把几百条的词库直接塞进系统提示词结果模型回答的时候频繁走神。更好的做法是配合 RAG把术语表按条目切分后向量化用户提问时先检索相关术语再把命中的术语片段拼进 Prompt。这样既不会超窗口也能保证每次只注入和当前问题有关的知识。从实践效果看术语表通过 Prompt 注入的效果在绝大多数场景下都足够。真正需要改词表的场景非常少只出现在特殊 token 数量极多、Prompt 注入无法覆盖的极端任务里。3. 模型微调从训练端到 Ollama 导入的完整链路3.1 理清边界Ollama 不训练只加载和推理如果你搜Ollama 微调会看到各种说法有人吹有人贬。我的理解是Ollama 是一个推理运行时它负责把权重跑起来但不负责反向传播和梯度更新。所以Ollama 微调更准确的描述应该是在 Ollama 外部完成微调训练把训练后的模型转换为 GGUF 格式通过 Ollama 加载这个模型在运行时对其做行为调参这样分工之后你会发现每个环节的职责都很清晰。训练是模型侧的事Ollama 只解决跑起来和跑得好用。3.2 轻量微调后如何转换格式并导入一个常见的诉求是我有一套业务数据想让开源模型在回答时更贴合业务表达习惯。这时你会用 LoRA 这类轻量微调方案去训练。LoRA 的好处是只训练一小部分低秩矩阵显存占用低、训练速度快适合在消费级显卡上跑。训练完成后拿到的是适配器权重通常需要先和基础模型合并再转换为 GGUF。这里给你一个可执行的路径参考用自己的数据准备训练集最好覆盖至少几百条与业务强相关的指令。用常见的开源训练框架加载基础模型配置 LoRA 参数训练若干轮。训练完成后合并 LoRA 权重到基础模型得到一个完整的模型文件。用转换工具把模型导出为 GGUF 格式这个环节会按你选择的量化方案压缩权重。把生成的 GGUF 文件放到 Ollama 的模型目录下或者放到你习惯的目录写一个Modelfile指向这个文件。执行ollama create注册完成之后就能用本地标签调用微调后的模型。我特别想强调第 3 步到第 4 步之间的验证。转换格式之前一定要先在原来的训练框架里跑几个测试样本确认合并后的模型输出正常。如果这一步没做等转换完再查问题排查链路会变得很长因为你不能确定问题出在合并还是转换。3.3 注册自定义模型的配置与验证转换完成之后Ollama 这边的注册很简单FROM ./my_finetuned_model.gguf SYSTEM 你是经过业务数据微调后的助手回答风格需要贴合内部知识库。 PARAMETER temperature 0.3执行ollama create my-model -f ./Modelfile ollama run my-model验证的时候不要只用一个问题看效果。我一般会准备三组测试集一组是训练数据里的类似问题确认模型学过的东西没有丢一组是训练数据之外但同领域的冷门问题确认泛化能力还在第三组是明显与业务无关的问题确认模型没有因为微调而出现严重的知识遗忘或复读。这里有个常见误区本地有一个微调后的通用模型就直接覆盖了原有基础模型。实际上微调后的模型在通用能力上通常会有一定衰减最好的做法是保留基础模型的标签把微调模型单独注册按任务需要切换使用。4. RAG 落地把文档库变成模型的长期记忆4.1 技术选型Embedding 模型、向量库和分片策略RAG 的全称是检索增强生成核心思想很朴素模型不懂的知识你提前塞进上下文里。Ollama 在 RAG 链路里的角色是提供生成和 embedding 两个能力而检索链路需要自己搭。我常用的技术组合是用 Ollama 跑一个 embedding 模型来做文本转向量用开源向量库来存向量和执行相似度搜索。这里有一个容易忽略的选型问题embedding 模型的输出维度会直接影响向量库的配置。不同的 embedding 模型输出维度不同有的几百维有的上千维。如果你中途更换 embedding 模型已经建立的索引需要重建否则新旧向量放在同一个集合里会产生维度不匹配或相似度无意义的问题。分片策略同样决定 RAG 上限。我做过一个对比测试用固定 500 字的窗口切片跟用段落和标题边界切片后者的检索命中率明显更好。原因是模型做 embedding 的时候语义完整性很重要。强行截断一个长段落会让片段缺少上下文检索出来的内容可能只有半句话模型看得一头雾水。4.2 离线索引与在线检索链路整个 RAG 链路可以拆成离线索引和在线检索两部分。离线索引阶段把文档按段落或章节分片。对每个分片调用 Ollama 的 embedding 接口得到向量。把向量和原始文本一起写入向量库。建好向量索引留一个查询入口。在线检索阶段接收用户问题。对用户问题做同样的 embedding 操作。在向量库里执行相似度搜索取 top-k 个片段。把问题和检索到的片段拼成 Prompt。调用 Ollama 的聊天接口生成最终答案。这个链路的关键在于用户问题和文档片段的 embedding 必须来自同一个模型。如果查询用的 embedding 模型和索引阶段不一致检索结果会非常不稳定。我建议你把 embedding 模型名和服务地址做成环境变量代码里不要写死。4.3 上下文拼装和 Prompt 设计的几个关键点检索到文档之后Prompt 怎么拼直接决定回答质量。我的常见模板是这样请根据以下参考资料回答用户问题。 如果参考资料中没有相关内容请直接说资料库中未找到相关答案不要编造。 参考资料 [1] 文档标题/来源信息 内容xxxxxxxxxx [2] 文档标题/来源信息 内容yyyyyyyyyy 用户问题zzzzzzzz这里有两个细节。第一一定要注明来源。给每个检索到的片段加上编号和标题模型回答时会倾向于按编号引用你也能在应用层把来源展示给用户。第二要显式告诉模型不知道就承认不知道。RAG 检索到的内容未必覆盖用户问题如果没有这句约束模型会用预训练知识去脑补看起来答得流利其实是幻觉。top-k 值的选取也需要调。我试过从 2 到 8 的不同取值在多数问答场景下取 4 到 5 比较合适。太少了信息不足太多了上下文塞了太多无关内容反而干扰回答。5. 用 Go 开发上层推理应用接口设计与示例5.1 为什么上层也建议用 Go同一套技术栈部署无额外依赖如果你的目标只是自己玩一下 Ollama那上面已经够用了。但如果想把它嵌入到实际系统里比如做一个内部问答机器人、给另一个服务提供推理能力那语言选型就得认真考虑。上层服务用 Go 的好处最直观的是部署。Ollama 本身是 Go 写的模型加载后就是一个常驻的本地 HTTP 服务。上层应用再写一个 Go 的服务编译完就是一个独立的二进制不用考虑 Python 环境、虚拟环境、依赖锁定这些问题。生产环境里模型服务和应用服务分开部署也好管理。另外 Go 标准库自带的net/http足够完成对 Ollama 接口的调用不需要引入重的 SDK。对于有 HTTP 部署经验的人来说这套是零学习成本的。5.2 直接调用 REST API聊天、嵌入与模型管理Ollama 的接口设计得很克制核心就是几个 REST 端点。最常用的有三个聊天的/api/chat、生成补全的/api/generate、嵌入的/api/embed。先看一个简单的聊天请求curl http://localhost:11434/api/chat -d { model: my-assistant, messages: [ {role: user, content: 介绍一下 LoRA 微调的基本思路} ], stream: false }stream字段控制是否流式返回。开发调试的时候建议设置成false一次性拿完整结果但生产环境给用户用的时候最好改成流式体验会好很多。再看 embedding 调用curl http://localhost:11434/api/embed -d { model: nomic-embed-text, input: 工单 SLA 的定义 }返回的是一个向量数组。这个接口就是 RAG 链路里的向量来源。模型管理这块ollama list看本地模型ollama pull拉新模型ollama rm删除。这些命令都有对应的 API但日常开发时直接命令行更快没必要为管理去专门调接口。5.3 把 RAG 接进 Go 服务最小可运行示例下面给一个最小可运行的 Go 示例思路是接收用户问题从向量库检索片段拼装 Prompt再调 Ollama 对话接口。package main import ( bytes encoding/json fmt io net/http ) type ChatMessage struct { Role string json:role Content string json:content } type ChatRequest struct { Model string json:model Messages []ChatMessage json:messages Stream bool json:stream } type ChatResponse struct { Message ChatMessage json:message } func askOllama(systemPrompt string, userQuery string) string { url : http://localhost:11434/api/chat reqBody : ChatRequest{ Model: my-assistant, Messages: []ChatMessage{ {Role: system, Content: systemPrompt}, {Role: user, Content: userQuery}, }, Stream: false, } data, _ : json.Marshal(reqBody) resp, err : http.Post(url, application/json, bytes.NewReader(data)) if err ! nil { return 调用模型服务失败: err.Error() } defer resp.Body.Close() body, _ : io.ReadAll(resp.Body) var result ChatResponse if err : json.Unmarshal(body, result); err ! nil { return 解析模型响应失败: err.Error() } return result.Message.Content } func main() { userQuery : 工单超时未处理应该怎么升级 systemPrompt : buildPromptWithContext(retrieveFromVectorDB(userQuery)) answer : askOllama(systemPrompt, userQuery) fmt.Println(answer) } func retrieveFromVectorDB(query string) string { // 此处对接你的向量库查询逻辑返回按相关度排序的文本片段 return 示例根据查询返回的文档片段内容。 } func buildPromptWithContext(context string) string { return fmt.Sprintf(请根据以下资料回答问题没有相关内容就明确说不知道。\n资料\n%s, context) }这个例子把推理、检索、拼 Prompt 分离成了三个函数。实际项目中你可以在retrieveFromVectorDB里调向量库客户端可以在buildPromptWithContext里做更精细的模板拼装。整个代码只用到了标准库没有引入额外依赖。编译产物很小部署时只要把它和 Ollama 跑到同一台机器或者通过环境变量配置 Ollama 的地址就能完成对接。6. 部署与排障经验本机能跑不代表生产能扛6.1 并发与资源调度OLLAMA_NUM_PARALLEL 和 OLLAMA_MAX_LOADED_MODELS本地跑通和正式提供服务中间还隔着一个并发的坎。Ollama 默认的并发能力其实偏保守多个人同时提问的时候后到的请求会排队响应变慢。两个环境变量能帮你调整OLLAMA_NUM_PARALLEL控制单个模型可以并行处理多少个请求OLLAMA_MAX_LOADED_MODELS控制最多同时加载多少个模型。比如同时跑一个 7B 对话模型和一个 embedding 模型如果不做限制Ollama 可能会在内存里加载多个模型挤占资源。我的配置经验是先单模型压测观察显存和内存的峰值再逐步提高OLLAMA_NUM_PARALLEL。加到并发 4 的时候如果显存已经吃满再往上加只会导致 OOM而不是吞吐的提升。还有几个环境变量值得注意OLLAMA_HOST用来绑定监听地址默认是127.0.0.1:11434如果需要给局域网内其他机器提供服务需要改成0.0.0.0并注意访问控制OLLAMA_KEEP_ALIVE控制模型在内存中的保持时间如果调用频率高建议把它调长避免频繁换入换出。6.2 高频报错与排查路径我遇到过几个高频问题说下排查思路。OOM显存不足通常发生在切换大模型或调大上下文窗口之后。解决办法是换低量化档位或者降低num_ctx。有些场景下多个模型同时驻留也会导致 OOM检查一下当前加载模型数量和显存分配情况。模型加载缓慢首次加载需要把权重读入内存换用大模型后会更明显。如果服务重启频繁加载时间会被放大很多次。用OLLAMA_KEEP_ALIVE延长模型驻留时间能减少重复加载。上下文窗口不够导致失忆如果你的对话超过一定轮次模型开始答非所问先查num_ctx。很多模型默认窗口并不大长对话或长文档任务需要显式扩大。Embedding 维度不匹配更换 embedding 模型后向量库里存了新旧两种维度的向量检索时直接报错或结果异常。遇到这个重建索引比兼容方案更省心。6.3 上线前的稳定性建议最后聊几个和代码无关、但直接影响线上体验的点。模型参数要固定。温度、上下文长度、top_p 这些最好在Modelfile或应用配置里统一管理不要让每个调用方自行传参否则同一个模型在不同入口表现的差异会大到让你怀疑模型换了。服务地址要配置化。不要在内网代码里写死localhost:11434用环境变量或配置文件传递。这样模型服务迁移到另一台机器时应用侧不需要改代码重新编译。日志要分开。Ollama 自身有日志你的应用也要打印请求和响应摘要。排障时最怕两头都是黑盒日志能把问题定位到模型乱说还是检索结果不对还是应用拼错了 Prompt。依赖要收敛。我自己写上层 Go 服务的时候尽量只依赖标准库。推理服务和向量库客户端之间用接口抽象方便以后换实现。这样整个工程哪怕是编译期也能稳定复现不会有 Python 环境那种换个机器跑不起来的问题。我自己用这套组合跑了几个月最大的感受是Ollama 不是万能的它不会替你完成训练、检索和应用设计但它把把模型跑起来这件事压缩到了几乎不能再小的程度。真正影响最终效果的反而是模型怎么选、上下文怎么拼、检索怎么做这些外部功夫。希望这篇文章能帮你把 Ollama 放到合适的位置上少走一点我走过的弯路。