做本地大模型这件事,我从“抱着试试看”到“真当成日常工具用”,中间大概折腾了小半年。最开始的想法很朴素:写代码时不想把整段业务逻辑贴到网页对话框里,翻译合同和文档时也不想一份份往外传。于是我把目光放到能在个人机器上跑起来的中等参数量模型上,Qwen2.5-7B 就是在这个背景下进入视野的——7B 级别的体量、官方声明支持 32K 上下文、中英双语底子好、对代码有一定训练投入,这几点正好对上我的两个高频场景:编程助手和多语言翻译。运行框架选了 Ollama,理由很简单,一条命令拉模型、自带 REST 接口、Modelfile 能把参数固化下来,不用自己去折腾推理引擎的编译。
这篇内容就是我这一段时间的实测记录。前半段讲环境怎么搭、参数怎么调、显存怎么算,后半段是两组硬碰硬的测评:一组拿编程任务去打,看它能不能当半个结对伙伴;一组拿多语言翻译去打,看它在中英日德之间来回倒腾时会不会掉链子。中间会穿插大量我踩过的坑,比如上下文长度设大了直接爆显存、温度调高了代码里冒出根本不存在的函数名、批量翻译跑到一半断开重来。适合谁看?手上有 8GB 以上显存、想在自己机器上跑一个能干活的中等模型的朋友;也适合已经把模型跑起来、但觉得“效果一般般”想找找参数和提示词原因的朋友。全部内容基于我个人环境实测,数据仅供参考,不同硬件差异会很大。
1. 为什么在本地跑一个 7B 模型:需求拆解与方案选型
1.1 三个真实诉求决定了选型方向
我把自己的需求摊开列了一遍,其实就三条。第一条是响应速度和隐私的平衡:日常写代码时提问频率很高,一天几十次,每次都走公网接口,延迟叠加起来很烦,而且贴进去的代码往往带业务字段。第二条是离线可用:出差在高铁上、在没网的环境里改脚本,我希望工具还在。第三条是可控性:我想自己决定上下文多长、温度多少、系统提示词怎么写,而不是被网页端固定死。
这三条诉求一摆出来,选型其实就没什么悬念了。云端 API 在绝对能力上肯定更强,但它解决不了“离线”和“参数完全自主”这两点;而纯本地跑一个 70B 级别的模型,我的硬件又扛不住。7B 这个档位是甜点区——量化之后权重文件 4 到 5GB,剩下的显存留给 KV 缓存,一张 16GB 显存的消费级显卡能跑得挺舒服,8GB 显卡降一档量化也能用。再往上到 14B、32B,能力是上去了,但要么得双卡,要么得接受每秒个位数的出词速度,写代码时那种“等它一个字一个字蹦”的体验会直接劝退。
提示:不要一上来就追求最大参数。编程和翻译这两类任务对模型的要求不同,编程更吃“指令遵循 + 代码语料”,翻译更吃“双语对齐质量”,参数量不是唯一变量。7B 在这两件事上的性价比,实测下来比很多人预期的要好。
1.2 为什么是 Qwen2.5-7B 而不是同级的其他选择
同档位可选的模型不少,我最终留下 Qwen2.5-7B-Instruct 作为主力,主要是几个细节打动我。一是上下文:官方标注原生 32K,配合 YaRN 还能往上扩,这对“把一整个源文件丢进去让它找 bug”这种用法是刚需,很多同级模型还停在 8K。二是中英双语的实际手感:它的中文不是“翻译腔中文”,长句的组织方式更接近母语表达,这在做中译英和英译中双向任务时体现得很明显。三是结构化输出能力:我经常让它输出 JSON,它的格式遵循度在 7B 里算比较靠谱的,偶尔跑偏但用 few-shot 一压就回来了。
四是代码能力有专门投入:官方在 7B 这个尺寸上做了代码方向的强化,实测下来写中等难度的 Python、Shell、SQL 基本够用,复杂的算法题会露怯,但日常业务代码的补全、改写、加注释、写单测,完成度是可以接受的。另外它还有个 Coder 变体,专门偏代码,我在第 3 章会顺带提一句两者的差异。
需要说清楚的是,这些都是“够用”而不是“惊艳”。你要是拿它跟顶级闭源模型比复杂推理,那肯定比不过。但作为一个不联网、不出机器、随叫随到的助手,它的定位是“把重复劳动吃掉”,这个目标它完成得不错。
1.3 量化版本怎么选:先把显存账算明白
这一步很多人跳过,直接拉默认标签,结果跑起来要么慢要么爆。我先讲算法,再讲结论。
模型推理占用的显存大致分两块:权重和KV 缓存。权重好算,7.6B 参数乘以每参数字节数:FP16 是 2 字节,约 15GB;Q8_0 约 8GB;Q5_K_M 约 5.4GB;Q4_K_M 约 4.7GB。KV 缓存按公式推:
单 token KV 缓存字节数 = 2(K 和 V)× 层数 × KV 头数 × 头维度 × 每元素字节数
Qwen2.5-7B 是 28 层、GQA 结构下 KV 头数为 4、头维度 128,按 FP16 存:
2 × 28 × 4 × 128 × 2 = 57,344 字节 ≈ 56KB/token
也就是说 8K 上下文约 0.45GB,32K 上下文约 1.8GB。这个数字很关键——很多人以为“支持 32K”就是免费的,实际上你把 num_ctx 从 4096 调到 32768,光缓存就要多吃 1.4GB 左右,8GB 卡直接就顶到墙上了。
我的实测结论整理成下面这张表,硬件按“16GB 显存 + 32GB 内存 + 8 核 CPU”的虚构配置描述,tok/s指生成速度:
| 量化版本 | 权重占用 | 建议 num_ctx | 显存总占用(估) | 生成速度(tok/s) | 适用场景 |
|---|---|---|---|---|---|
| q8_0 | 约 8.0GB | 8192 | 约 9GB | 22 - 28 | 显存充裕,追求质量上限 |
| q5_k_m | 约 5.4GB | 16384 | 约 7GB | 38 - 45 | 质量与速度的平衡点 |
| q4_k_m | 约 4.7GB | 32768 | 约 6.8GB | 45 - 55 | 默认首选,长上下文场景 |
| q4_0 | 约 4.1GB | 32768 | 约 6GB | 50 - 60 | 8GB 显卡、极限压榨 |
我个人日常用q4_k_m,原因是它在质量上的损失我几乎感知不到,但换来的是可以把 num_ctx 开到 16K 甚至 32K 而不心疼显存。如果你的任务以短问短答为主、从不塞长文件,那q5_k_m或q8_0换来的那点质量提升是划算的。别迷信“量化越低越差”,Q4_K_M 这种混合量化对关键层保留了更高精度,实际掉点比 Q4_0 小得多,两者速度差也就 10% 上下。
注意:显存账要留余量。Ollama 在加载时会额外占用几百 MB 做图构建和缓冲,加上系统桌面本身占的那部分,卡着极限设 num_ctx 非常容易在长对话第 N 轮突然崩掉。我的习惯是留 1.5GB 缓冲。
2. Ollama 环境落地:安装、模型拉取与关键参数
2.1 安装与存储目录的规划
Ollama 的安装本身没什么门槛,各平台都有安装包,装完服务默认就在后台跑着,监听本地的 11434 端口。真正需要提前想的是模型文件放哪。默认路径在用户主目录下,一个 7B 模型的多个量化标签加起来十几 GB 很常见,如果你主目录在系统盘上,很快就会看到磁盘告警。
我的做法是装之前就设好环境变量,把模型目录指到一块大容量盘上。Linux 和 macOS 下用OLLAMA_MODELS,Windows 则在系统环境变量里加同名项,改完要重启 Ollama 服务才生效。这一步顺序别搞反——先设变量再拉模型,否则模型已经落到默认目录了,改完变量还得手动搬,容易搬出权限问题。
# Linux / macOS:写进 shell 配置文件后重开终端 export OLLAMA_MODELS=/data/ollama-models export OLLAMA_HOST=127.0.0.1:11434 export OLLAMA_KEEP_ALIVE=30m export OLLAMA_NUM_PARALLEL=2 export OLLAMA_MAX_LOADED_MODELS=1 export OLLAMA_FLASH_ATTENTION=1 export OLLAMA_KV_CACHE_TYPE=q8_0上面这几个变量是核心,逐个解释一下我的取值理由。OLLAMA_KEEP_ALIVE=30m让模型在最后一次调用后驻留显存 30 分钟,默认值只有 5 分钟,写代码时你思考一会儿再问,模型已经被卸载了,下一次提问要重新加载,那个等待感很难受。OLLAMA_NUM_PARALLEL=2允许两路请求并发,做批量翻译时能吃到并行红利,但注意并发会成倍吃 KV 缓存,显存紧的话老实设 1。OLLAMA_MAX_LOADED_MODELS=1是防止你不小心拉了两个模型把显存撑爆。OLLAMA_FLASH_ATTENTION=1开启注意力优化,长上下文下显存和速度都有改善。OLLAMA_KV_CACHE_TYPE=q8_0把 KV 缓存量化到 8 位,能把缓存占用砍掉将近一半,质量影响很小,长上下文场景强烈建议开。
2.2 拉取模型与用 Modelfile 固化参数
拉模型就一条命令,但标签要写全,别只写qwen2.5,那是默认标签,你不一定知道你拿到的是哪个量化版本。
ollama pull qwen2.5:7b-instruct-q4_K_M ollama list # 确认标签和体积 ollama run qwen2.5:7b-instruct-q4_K_M "用一句话说明什么是闭包"命令行直接对话适合验证,但真正要长期用,我建议写一个 Modelfile。原因很实际:num_ctx、temperature、系统提示词这些东西,如果每次都在 API 请求里传,很容易漏;固化成自己的模型标签之后,一句ollama run my-coder就是全套配置,团队里其他人也能一键复现。
FROM qwen2.5:7b-instruct-q4_K_M PARAMETER num_ctx 16384 PARAMETER temperature 0.2 PARAMETER top_p 0.8 PARAMETER top_k 20 PARAMETER repeat_penalty 1.05 PARAMETER num_predict 2048 SYSTEM """ 你是一名严谨的软件工程师助手。回答代码问题时遵循: 1. 先给可运行的完整代码,再给不超过三行的关键说明; 2. 不确定的 API 必须显式标注"需核实",禁止编造函数名和参数; 3. 涉及修改现有代码时,只输出改动片段并标注所在位置。 """ollama create my-coder -f ./Modelfile.coder ollama run my-coder这套参数是我反复试出来的。temperature 0.2对代码任务是关键,我一开始用默认的 0.8,它写出来的东西思路天马行空,函数名经常是“看起来很像但根本不存在”的组合。降到 0.2 之后幻觉明显收敛。repeat_penalty 1.05是个小值,压一下重复输出的倾向,设太大反而会让它不敢用重复的语法结构,代码会变得别扭。num_predict 2048限制单次输出长度,防止它陷入循环时把显存和时间都耗光。
翻译场景我会另建一个标签,参数不一样:temperature 0.1(翻译要更确定)、top_p 0.9、SYSTEM 里写死“只输出译文,不要解释、不要加引号、不要保留原文”。
2.3 用兼容接口把它接进自己的脚本
Ollama 自带一个与主流接口规范兼容的/v1/chat/completions,这意味着你手上大部分现成的客户端代码,改个 base_url 就能直接指过来,不用重写。
import requests, json, time BASE = "http://127.0.0.1:11434/v1/chat/completions" def ask(messages, model="my-coder", temperature=0.2, max_tokens=1024, retries=3): payload = { "model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": False, } for i in range(retries): try: r = requests.post(BASE, json=payload, timeout=180) r.raise_for_status() return r.json()["choices"][0]["message"]["content"] except Exception as e: if i == retries - 1: raise time.sleep(2 ** i) # 指数退避,别用固定间隔死磕这里有个细节值得说:超时要设够。7B 模型在消费级显卡上生成 1000 个 token 大概要 20 到 30 秒,如果你按普通 HTTP 接口的习惯设 30 秒超时,长回答必然超时。我一开始就吃了这个亏,批量任务里一半请求报超时,查了半天以为是服务挂了,其实是客户端等不及先断了。设 180 秒是保守值,实际很少用满。
提示:
/api/chat和/v1/chat/completions两套接口都能用,前者是原生格式、能拿到更细的耗时字段(比如 prompt 评估耗时、生成耗时),后者兼容性好。做性能测量我用前者,做业务集成我用后者。
3. 编程助手实测:从补全到重构
3.1 测评任务怎么设计才算公平
随便问几道题然后说“感觉还行”没有意义。我设计了五类任务,覆盖日常写代码时真正会遇到的场景,每类各 5 道题,一共 25 道,全部在同一个 Modelfile 配置下跑(temperature 0.2、num_ctx 16384),提示词尽量写得像真人求助而不是考试题。
第一类是函数级实现:给一段自然语言描述加输入输出约束,让它写一个完整函数,比如“写一个把嵌套字典拍平成点号路径键的函数,要处理列表”。第二类是缺陷定位:贴一段有 bug 的代码,只问“这段为什么输出不对”,不给答案。第三类是单元测试生成:给一个已有函数,让它补 pytest 用例,重点看它会不会只测 happy path。第四类是SQL 改写:把一段可读性很差的子查询改成带 CTE 的写法,同时保持语义等价。第五类是重构与注释:给一段 80 行的老脚本,要求拆分函数并补文档字符串。
评分我用三档:可直接用(跑通、边界考虑到了)、改改能用(思路对,小错)、不能用(逻辑错或幻觉 API)。这种粗粒度评分主观性难免,但比“感觉”靠谱得多,而且我每道题都实际跑了代码。
3.2 实测结果与逐项点评
25 道题的结果汇总如下,这是我这台机器上的实测分布,换模型版本或量化档位会有波动:
| 任务类型 | 可直接用 | 改改能用 | 不能用 | 主要问题 |
|---|---|---|---|---|
| 函数级实现 | 3 | 2 | 0 | 边界条件偶有遗漏 |
| 缺陷定位 | 4 | 1 | 0 | 有时只描述现象不给修法 |
| 单测生成 | 2 | 2 | 1 | 容易只测正常路径 |
| SQL 改写 | 4 | 1 | 0 | 窗口函数偶有误用 |
| 重构与注释 | 3 | 2 | 0 | 拆函数偏保守,不敢动结构 |
函数实现这块表现比我预期好。嵌套字典拍平那道题,它不但写了递归版本,还主动补了一个用栈的迭代版本,理由是“深嵌套时递归可能触顶”。这个主动性是加分项。漏掉的地方通常是类型混合的边界,比如列表里混了元组,它默认按 list 处理。
缺陷定位是最让我惊喜的一项。我贴了一段典型的“闭包在循环里捕获变量”的代码,它没有泛泛而谈,而是直接指出“循环变量在回调执行时已经是最终值,需要用默认参数或工厂函数把当前值绑住”,并且给了两种改法。这种“指到具体那一行”的能力,是它作为结对伙伴最有价值的地方。
单测生成是短板。它写的用例大多围绕正常输入,异常分支和边界值需要我在提示词里明确点名“必须包含空输入、类型错误、超长输入三种情况”,它才会补。解决办法很土但有效:在 SYSTEM 里固定一句“生成测试时必须覆盖:空值、类型错误、边界值、并发”。加上这句之后,可用率明显提升。
SQL 改写质量不错,把三层嵌套子查询改成 CTE 之后可读性提升明显,语义也保住了。唯一一次翻车是它把一个LEFT JOIN顺手改成了INNER JOIN,理由是“这样更快”——这就是典型的“不懂业务语义瞎优化”,凡是涉及连接类型的改动我都会逐行核对。
重构任务它偏保守,80 行的脚本它倾向于只加注释、抽两个小函数,不太敢大刀阔斧地重划模块边界。这个其实不算缺点,改动越小越容易 review,但如果你期待它帮你做架构级重排,会失望。
至于 Coder 变体,我对比跑过一轮。纯代码任务上它的代码风格更紧凑、注释更少,但在“解释这段代码为什么这样写”这类需要自然语言输出的场景里,反而不如通用版顺手。我的选择是:写代码用通用版就够了,只在纯生成、不需要解释的批量场景才切 Coder。
3.3 提示词模板:把要求写死比反复纠正省事
试了十几版之后,我固定下来一个编程场景的模板。核心思路是:把“不要做什么”写在前面,因为 7B 级别的模型对否定式约束的敏感度不如大模型,得反复强调。
角色:你是我的代码审查搭档,不是聊天机器人。 硬性约束: - 只使用标准库和下列已声明的依赖:{deps} - 任何你不确定的 API,标注 [需核实],不要猜 - 输出代码块后,最多三行说明,不要长篇解释 任务: {task} 上下文: {code}几个参数上的经验:top_k 20配temperature 0.2是我试出来的组合,top_k再往上调,代码里出现奇怪标识符的概率会上升;repeat_penalty别超过 1.1,超了之后它连self、return这种必要重复都开始回避,写出来的东西很别扭。
3.4 编程场景踩过的三个坑
坑一:上下文塞太满,回答被截断。我一开始图省事,把整个 600 行的源文件丢进去,结果它回答到一半就断了。原因是输入 token 加上num_predict超过了num_ctx。后来我改成两段式:先让它只输出“相关函数名和行号范围”,我再把那一小段贴进去做第二轮。这个“先定位再处理”的两段式流程,比一次性塞全文的成功率高得多,也省显存。
坑二:幻觉出一个看起来特别合理的库函数。有一次它给我写了个dict.flatten(sep="."),语法漂亮、命名合理,但 Python 根本没有这个方法。这类幻觉最危险的地方在于“太像真的”。我在 SYSTEM 里加了[需核实]标注要求之后好了很多——它不确定的时候会标出来,我扫一眼就知道哪里要查文档。
坑三:中文注释里的全角符号。它写中文注释时偶尔混入全角括号和全角逗号,粘进代码里不报错但很难看。解决办法是在 SYSTEM 里加一句“注释内标点一律使用半角”,成本极低,效果立竿见影。
4. 多语言翻译实测:中英日德与代码注释本地化
4.1 语料选择与评价维度
翻译这块我没用通用测试集,而是自己攒了三组语料,更贴近实际用途。第一组是技术文档,包括接口说明、错误码描述、部署手册片段,中英各 20 段,特点是术语密集、句式偏长。第二组是产品文案,中英各 15 段,特点是短句多、有营销语气、需要本地化而不是直译。第三组是代码注释与提交信息,中英各 30 条,特点是极短、有固定格式、经常带变量名。
评价维度我定了四个:术语一致性(同一个词在全文中是否统一译法)、语义保真(有没有漏译、错译、添加原文没有的信息)、母语自然度(读起来像不像人写的)、格式保持(Markdown 标记、占位符、变量名有没有被翻译掉)。最后一项特别重要——我见过太多模型把{user_id}翻译成{用户ID},一运行就报错。
4.2 实测表现与典型问题
整体结论先给:中英互译达到“改改就能用”的水平,中日、中德属于“能看懂但需要润色”。具体表现:
| 语言方向 | 术语一致性 | 语义保真 | 自然度 | 格式保持 | 综合判断 |
|---|---|---|---|---|---|
| 英译中(技术) | 好 | 好 | 好 | 好 | 基本可直接用 |
| 中译英(技术) | 好 | 好 | 较好 | 好 | 小改可用 |
| 英译中(文案) | 中 | 好 | 较好 | 好 | 需润色语气 |
| 中译英(文案) | 中 | 较好 | 中 | 好 | 建议人工重写 |
| 中译日 | 中 | 好 | 中 | 好 | 需检查敬语层级 |
| 中译德 | 中 | 较好 | 中 | 好 | 名词复合词需人工拆 |
英译中技术文档是它的强项。长句拆分做得自然,不会出现那种“从句套从句、读到最后忘了主语”的翻译腔。术语方面,像idempotent、backoff、race condition这类词,它给出的译法和我平时用的基本一致(幂等、退避、竞态条件),全文也保持了统一,不会这一段叫“幂等”下一段叫“幂等性”。
中译英技术文档也很能打,句式偏简洁,没有那种“中式英语的长定语前置”。唯一需要注意的是它有时候会把中文的“我们”直译成we,而英文技术文档更习惯用被动或者直接省略主语,这个得手工调。
文案翻译是明显短板。你给它一句“轻装上阵,快人一步”,它会老老实实翻成Light equipment, one step faster,语义没错,但完全没有广告语的味道。这不是模型的问题,是这类任务本身就需要人类译者的再创作。我的用法是:让它出三个版本的直译,我自己挑一个方向再改。
中译日这块要重点提敬语。它会默认用比较中性的敬体,但商务文档往往需要更谦让的表达,而技术文档又不需要那么客气。同一批语料里,它给不同段落选的敬语层级不太统一。解决办法是在提示词里明确“读者为外部客户,使用敬体”或“内部技术文档,使用常体”,指定之后一致性就好了。
格式保持这一项它做得相当好,这是我没预料到的。Markdown 的加粗、代码围栏、列表符号全都保住了,{placeholder}和{{var}}这类占位符也没有被翻译,变量名user_id保持原样。这一点对批量处理特别关键,因为格式一乱,后处理脚本就得写一堆补丁。
4.3 翻译场景的参数与提示词
翻译和编程的参数取向不一样。编程要一点发散来找思路,翻译要的是确定性。我的配置是temperature 0.1、top_p 0.9、top_k 10,repeat_penalty保持 1.0 不动(翻译里重复词很正常,压它反而出错)。
提示词方面,我固定用这个结构,核心是术语表前置 + 输出格式锁死:
你是专业翻译。严格遵守: 1. 只输出译文,不要解释、不要加引号、不要重复原文 2. 保留所有 Markdown 标记、代码围栏、占位符(如 {name})、变量名 3. 术语表(必须遵守):idempotent=幂等; backoff=退避; race condition=竞态条件 4. 目标读者:{audience};语体:{style} 待译内容: --- {text}术语表这一条价值最大。7B 模型在没有约束的情况下,同一个术语在不同段落可能给出两三种译法,尤其是“幂等”这种它可能译成“幂等性”“幂等的”“幂等操作”的词。把术语表写进提示词,一致性立刻上来。术语表不用很长,二十条以内效果最好,太长反而会稀释注意力。
注意:一定要在提示词里明确“不要输出原文”。不加这句,它有很大概率把原文和译文一起吐出来,批量处理时你得写正则去剥离,很麻烦。
4.4 批量翻译脚本:断点、重试和限速
单段翻译用命令行就够了,实际工作中往往是几十上百个文件。我写了个批量脚本,核心是三点:断点续跑、指数退避重试、按段落切分。
import json, os, time, hashlib, requests API = "http://127.0.0.1:11434/v1/chat/completions" CACHE = "translate_cache.jsonl" def key_of(text, style): return hashlib.md5((style + "||" + text).encode()).hexdigest() def load_done(): done = set() if os.path.exists(CACHE): with open(CACHE, encoding="utf-8") as f: for line in f: done.add(json.loads(line)["k"]) return done def translate(text, style="技术文档", glossary="", retries=4): prompt = f"""你是专业翻译。严格遵守: 1. 只输出译文,不要解释、不要加引号、不要重复原文 2. 保留所有 Markdown 标记、占位符、变量名 3. 术语表:{glossary} 4. 语体:{style} 待译内容: --- {text} """ payload = {"model": "my-translator", "messages": [{"role": "user", "content": prompt}], "temperature": 0.1, "max_tokens": 2048, "stream": False} for i in range(retries): try: r = requests.post(API, json=payload, timeout=240) r.raise_for_status() return r.json()["choices"][0]["message"]["content"].strip() except Exception: if i == retries - 1: return None time.sleep(2 ** i) def run(segments, glossary="", style="技术文档"): done = load_done() with open(CACHE, "a", encoding="utf-8") as f: for seg in segments: k = key_of(seg, style) if k in done: continue out = translate(seg, style, glossary) if out is None: print("[跳过] 连续失败:", seg[:40]) continue f.write(json.dumps({"k": k, "src": seg, "dst": out}, ensure_ascii=False) + "\n") f.flush() # 关键:立刻落盘,崩溃不丢进度 time.sleep(0.2)几个设计点解释一下。用内容哈希做缓存键,好处是同一段文字重复出现不会重复翻译,改稿之后只有变动段会重跑。每写一行就flush,因为我被“跑到第 87 段程序崩了、缓存没落盘、从头再来”坑过一次。time.sleep(0.2)是给本地服务的喘息时间,看着没必要,但连续高压请求时偶发过连接被拒,加个间隔就好了。按段落切分而不是按文件切分,因为长文档整篇丢进去很容易触发上下文上限,切段之后每段都能独立重试,一段失败不影响整体。
跑完之后我会做一遍机械校验:对比原文和译文的占位符数量是否一致、代码围栏数量是否一致、数字是否一致。这三项能拦掉绝大部分低级错误,比人眼扫快得多。
5. 性能与资源占用:速度、并发与优化手段
5.1 怎么测才算靠谱
测速这件事很容易自欺欺人。第一次跑完看到tok/s挺高,实际用起来却觉得慢,原因是首次加载没算进去。我的测法是:先发一次短请求把模型预热到显存里(keep_alive设长,保证不被卸载),然后连发 5 次同类请求,取后 4 次的平均值。
两个指标要分开看:prefill 速度(处理输入的速度,单位 token/s,通常几百到上千)和decode 速度(生成输出的速度,几十 tok/s)。长文档翻译这类任务的瓶颈在 prefill——输入 3000 token、输出 300 token,前者的耗时可能占到一半。很多人只盯着生成速度,忽略了输入端这一块。
5.2 不同配置下的实测数据
下面是我这台机器(16GB 显存消费级显卡、32GB 内存、8 核 CPU)的实测,num_ctx和量化的组合如下:
| 量化 | num_ctx | prefill (tok/s) | decode (tok/s) | 首 token 延迟 | 显存峰值 |
|---|---|---|---|---|---|
| q4_k_m | 4096 | 约 620 | 约 52 | 约 0.5s | 5.6GB |
| q4_k_m | 16384 | 约 580 | 约 48 | 约 0.8s | 6.6GB |
| q4_k_m | 32768 | 约 510 | 约 44 | 约 1.2s | 8.1GB |
| q5_k_m | 16384 | 约 540 | 约 41 | 约 0.9s | 7.4GB |
| q8_0 | 8192 | 约 470 | 约 25 | 约 1.4s | 9.3GB |
从数据能看出两个规律。一是 num_ctx 拉大,decode 速度会掉,因为注意力计算量随上下文线性增长,从 4K 到 32K,生成速度掉了约 15%。这个损失不算大,但首 token 延迟翻了一倍多,交互式使用时能感觉到。二是量化档次对速度的影响比想象中小,q8_0 慢主要是因为权重体积翻倍导致内存带宽压力大,而不是计算本身。
5.3 几个真正有效的优化手段
第一,把OLLAMA_KEEP_ALIVE调长。这是我感受最明显的改动。默认 5 分钟卸载,你思考一会儿再问,就得重新加载 5GB 权重,那个 3 到 5 秒的等待非常打断思路。设成 30 分钟之后,一天下来几乎感知不到加载过程。代价是显存一直被占着,如果你还要跑别的吃显存的程序,就按需调短。
第二,开OLLAMA_FLASH_ATTENTION和 KV 缓存量化。这两个组合起来,长上下文场景的显存占用能省下接近 40%。我实测 16K 上下文下,不开是 8.2GB,开了之后降到 6.6GB 左右。省下来的显存可以拿去把 num_ctx 开更大,形成正循环。
第三,别贪OLLAMA_NUM_PARALLEL。设成 4 看起来很美好,实际每个并发槽都要独立分配 KV 缓存,显存直接翻几倍。做批量翻译时我用 2,日常交互用 1。这个参数不是越大越好,是“显存除以单路缓存”算出来的上限。
第四,GPU 层数能全放就全放。Ollama 会自动判断,但在混合配置(显存不够全放)下,剩余的层会落到 CPU 上,速度会断崖式下跌——我试过往 CPU 上放 8 层,生成速度从 48 掉到 13。宁可降一档量化把全部层塞进显存,也别接受混合推理。
提示:判断有没有落到 CPU,看
ollama ps输出里 GPU 占用百分比那一列,100% 才是全量在显卡上。这个命令我几乎每次启动都会扫一眼。
6. 常见问题与排查速查表
6.1 我实际遇到过的六类故障
模型加载失败或跑到一半崩掉。九成是显存不够。排查顺序:先看num_ctx是不是开太大,降到 8192 试;再看OLLAMA_NUM_PARALLEL是不是大于 1,改回 1 试;最后看OLLAMA_MAX_LOADED_MODELS,如果之前跑过别的模型没卸载,显存是被占着的,重启服务最干脆。
输出突然变成乱码或者重复刷同一句话。温度设太高会这样,repeat_penalty设太高也会这样(模型为了避开重复而胡乱换词,换到最后语义崩了)。我的处理是把repeat_penalty拉回 1.0 到 1.05,temperature降到 0.2 以下再试。
回答总是被截断。检查输入 token 加num_predict有没有超过num_ctx。长文档场景我的做法是分片,不要指望一次性塞进去。另外num_predict设得太小(比如 256)也会表现为“话说一半”。
中文输出里全是全角标点,粘到代码里很难看。在 SYSTEM 里明确要求半角标点,或者在提示词里加一句“代码块内部标点使用半角”。这是最低成本的一处修复。
批量任务跑着跑着连接被拒。大概率是并发太高把服务压住了。把NUM_PARALLEL降到 1,请求之间加 0.2 到 0.5 秒间隔,基本就不出现了。另外记得客户端的timeout要设到 180 秒以上,短超时会造成“假故障”。
同一个术语在不同段落译法不一致。这是 7B 级别模型的通病。解法是术语表前置,并且把temperature压到 0.1。还不行的话,就把文档按章节切开,每章单独维护术语表。
6.2 速查表
| 现象 | 最可能原因 | 快速处理 |
|---|---|---|
| 加载超时 / 崩溃 | 显存不足 | 降 num_ctx、NUM_PARALLEL 改 1、重启服务 |
| 生成速度骤降 | 部分层落到 CPU | ollama ps看 GPU 占比,降一档量化 |
| 输出循环重复 | repeat_penalty 过高 | 调回 1.0 - 1.05 |
| 代码里出现不存在的 API | 温度过高 / 缺约束 | temperature ≤ 0.2,SYSTEM 加“不确定标注” |
| 回答被截断 | token 预算超限 | 减少输入、提高 num_ctx 或分片 |
| 占位符被翻译 | 提示词未声明 | 明确“保留 {xxx} 占位符与变量名” |
| 术语前后不一致 | 无术语表约束 | 术语表前置 + temperature 0.1 |
| 客户端报超时但模型在跑 | 客户端超时太短 | timeout 提到 180s 以上 |
6.3 几个我踩过才明白的细节
模型标签一定要写全。ollama pull qwen2.5:7b和你以为的qwen2.5:7b-instruct-q4_K_M可能不是一个东西,体积和量化档位都不一样。我见过有人抱怨“效果差”,最后发现拉的是 base 版本而不是 instruct 版本——base 模型不会遵循指令,它只会续写,你问它问题它给你续一段问题。
改完环境变量必须重启服务。尤其是OLLAMA_MODELS和OLLAMA_KEEP_ALIVE,改了不重启等于没改。这个坑我踩了两次,第一次还以为变量名写错了。
Modelfile 里的 SYSTEM 优先级很高,但也别写太长。我一度写了 300 字的系统提示词,结果模型对用户实际问题的注意力被稀释了,回答开始跑题。后来精简到 80 字以内,只保留最硬的三条约束,效果好得多。约束越多不等于越听话,7B 模型的注意力预算有限。
别用同一个模型标签兼顾编程和翻译。我一开始图省事,一个标签打天下,结果编程场景的temperature 0.2配长 SYSTEM,翻译时它老是忍不住加解释。后来分成my-coder和my-translator两个标签,各管各的,问题消失。
定期清理不用的模型标签。ollama list会越积越长,每个都是几 GB。我一般保留两个量化档位(一个常用、一个备用),其余删掉。磁盘满了导致的故障排查起来很绕,因为报错信息往往指向别的地方。
最后分享一个我最近才用顺手的做法:把常问的问题和满意的回答存成一个本地的小样本库,每次新任务先检索两三条同类示例塞进提示词里做 few-shot。7B 模型在零样本下的表现有波动,但给两三个例子之后,输出格式和风格的一致性会明显提升,尤其是 JSON 输出和翻译的语气控制这两块。这个改动不需要任何额外依赖,就是读写本地文件,但对我日常使用的体验提升,比换一个更大的模型来得直接。