2026/10/26 12:14:53

从跑通到落地:Ollama本地大模型与多模态工程实践

从跑通到落地:Ollama本地大模型与多模态工程实践 本地把模型跑起来这件事这两年从一个门槛不低的活变成了一条命令的事。ollama 就是把这个门槛削平的那把刀——它把大语言模型和多模态模型的权重下载、量化格式、推理后端、上下文管理、API 暴露这一整套东西打了个包你在笔记本上敲一行命令一个能对话的模型就活了再敲一行一个能看图的模型也活了。我刚接触它的时候第一反应是“这不就是个本地版的模型启动器吗”用了大半年之后发现它真正值钱的地方不在于“能跑”而在于“跑起来之后怎么接到真实业务里”而这中间的坑官方文档基本不会写。这篇东西写给三类人一是想在自己机器上试大语言模型、又不想折腾编译环境的人二是手里有个图像理解需求、想知道多模态模型能不能顶上的开发者三是已经把 demo 跑通、卡在“怎么让它在项目里持续干活”这一步的工程同学。下面全部是我实际操作过的路径、算过的账和踩过的坑命令和参数你基本可以照着抄模型名字我会用代称你按自己本地模型库里的实际 tag 替换即可。1. 先想清楚本地跑模型到底该由谁来做1.1 三个真实痛点决定了你要不要走这条路我把身边人用 ollama 的动机归了三类每一类对应的技术选型其实完全不一样。第一类是数据不出机器比如你在处理内部文档、客户资料、还没公开的设计稿这些东西往任何外部接口送一遍合规同事就要来找你喝茶。这种情况下本地推理不是“更便宜”而是“唯一可行”你要关注的是模型能力够不够、量化后精度掉多少。第二类是成本高频调用场景下按 token 计费会迅速变成一个难看的数字尤其是做批量打标、批量摘要这种任务一次跑十万条文本本地跑和调用接口的账差得非常远但本地跑你要自己承担硬件折旧和电费。第三类是延迟和可控性接口的响应时间你控制不了模型版本什么时候被换掉你也控制不了而本地模型的行为是固定的你今天测出来的效果三个月后还是那个效果这对做评测和回归的人来说价值很大。我自己的判断标准很土如果一个任务每天要跑超过两千次、或者输入内容不能离开内网、或者你需要固定模型行为做对比实验那就值得上本地推理反过来如果只是偶尔问几句、或者需要的是最顶级的那一档能力本地模型目前还接不住老老实实走外部接口更划算。这个判断做在前面能省掉后面大量的折腾。1.2 ollama、直接加载权重、外部接口三条路的差别在哪很多人纠结“为什么不直接用推理框架加载权重”。我做过对比结论是看你处在哪个阶段。直接加载权重比如用主流的推理框架自己写服务给你的是完全的控制权采样策略、批处理调度、KV cache 管理、自定义算子你想怎么改就怎么改代价是你要自己处理模型格式转换、显存分配、并发排队、服务保活一个环境依赖没对上就能卡你半天。ollama 走的是另一条路它把模型格式、量化、加载、调度、HTTP 服务全部收敛成一套约定你付出的是灵活性换来的是“五分钟能出结果”。维度ollama自行加载权重外部接口上手时间分钟级半天到数天分钟级数据是否离开本机否否是模型可换性高一条命令换 tag中要改加载代码低看对方给什么参数控制粒度中靠 Modelfile 和请求参数高细到采样器实现低并发与批处理有内置队列可调但不够细完全自定义由服务方决定多模态支持内置图片直接进请求体要自己写预处理看接口长期运行成本硬件电费硬件电费人力按量计费这张表里我最想强调“参数控制粒度”那一行。ollama 的 Modelfile 能改 temperature、top_p、num_ctx、num_predict、repeat_penalty、stop 这些常用项够 90% 的场景用但如果你想做自定义的 logit 处理、想在解码层做约束、想搞投机解码的深度定制它就不够了。所以我的实际做法是探索期和中小流量场景用 ollama等某个任务真的成了核心链路、流量和稳定性要求都上来了再把那一部分迁到自建推理服务上模型权重和 prompt 都是现成的迁移成本比想象中低。1.3 动手之前先把显存这笔账算明白这一步被跳过的人最多结果就是下载了 30B 的模型跑起来发现每秒吐两个字然后怀疑是软件问题。其实瓶颈十有八九在显存和内存带宽上。算法不复杂模型加载占用 ≈ 参数量 × 每个参数的字节数。量化到 4 bit每参数约 0.5 字节再算上一些额外开销7B 模型实际占用大约 4.5GB8 bit 约 7.5GB16 bit 约 14GB。30B 在 4 bit 下大约 18 到 20GB这个数字直接决定了你的显卡能不能装下。除了权重还有一块容易被忽略的KV cache。它的粗略公式是 2 × 层数 × KV 头数 × 每头维度 × 上下文长度 × 精度字节数。拿一个典型的 7B 模型举例32 层、8 个 KV 头分组查询注意力、每头维度 128上下文开到 8192用 16 bit 存 KV2 × 32 × 8 × 128 × 8192 × 2 字节算下来大约 1GB。听着不多但如果你把上下文开到 32K这一项就变成 4GB如果模型用的是传统多头注意力32 个 KV 头同样的 8K 上下文直接就是 4GB。这就是为什么很多人把 num_ctx 调大之后突然爆显存——不是模型变大了是缓存变大了。而且这个数字是“每个并发槽位一份”并发数一乘翻倍得非常快。还有一个更隐蔽的天花板内存带宽。token 生成速度在显存装得下的前提下基本由“带宽 ÷ 模型大小”决定。普通双通道 DDR4 的带宽大约 45 到 50GB/s跑一个 4.5GB 的 4 bit 模型理论上限也就 10 token/s 出头实际能到 6 到 8 就不错了。统一内存架构的机器带宽能到 100 到 200GB/s同样的模型能跑到 20 到 40 token/s。独显的带宽更高所以同样是 7B独显和纯 CPU 的体感差得非常明显。你在选硬件之前先把这两个数算出来就不会出现“买回来发现只能当打字机”的尴尬。提示显存刚好卡在边界上是最难受的状态。模型权重 4.5GB、显卡 6GB看起来能装下但加上 KV cache 和图形界面占用就会溢出到内存速度直接掉一个数量级。留出至少 20% 余量或者干脆把上下文从 8K 砍到 4K。2. 从零把第一个模型跑起来2.1 安装与常驻服务的那几个环境变量安装本身没什么好说的Linux 下用官方一行脚本macOS 和 Windows 直接下安装包装完命令行里就有 ollama 这个命令了。真正值得说的是服务化配置因为默认监听地址是 127.0.0.1只允许本机访问你要是想让同一局域网里的另一台机器调它就得改监听地址。我用的是 systemd 覆盖配置的方式不直接改主服务文件这样升级的时候不会被覆盖掉# /etc/systemd/system/ollama.service.d/override.conf [Service] EnvironmentOLLAMA_HOST0.0.0.0:11434 EnvironmentOLLAMA_NUM_PARALLEL2 EnvironmentOLLAMA_MAX_LOADED_MODELS1 EnvironmentOLLAMA_KEEP_ALIVE30m EnvironmentOLLAMA_FLASH_ATTENTION1 EnvironmentOLLAMA_KV_CACHE_TYPEq8_0改完执行两条命令让它生效sudo systemctl daemon-reload sudo systemctl restart ollama这几个变量每一个都有实际作用我按重要性说。OLLAMA_HOST决定谁能连进来改成 0.0.0.0 之后务必确认你的网络环境是可信的这东西没有鉴权谁连上谁就能用。OLLAMA_NUM_PARALLEL是并发槽位数量默认值会随显存自动推算但你手动设了之后每个槽位都要单独占一份 KV cache显存不够就会排队或者失败所以别贪心先设 2 试试。OLLAMA_MAX_LOADED_MODELS控制同时驻留几个模型设成 1 意味着你换模型的时候旧模型会被卸载下次再切回来要重新加载加载 7B 模型大概要几秒到十几秒如果你的应用要在大语言模型和嵌入模型之间来回切这个值设小了会明显拖慢响应。OLLAMA_KEEP_ALIVE是模型空闲多久后被卸载默认 5 分钟做服务的话建议拉长到 30 分钟甚至 -1永不卸载否则用户隔几分钟来一次请求每次都要付加载成本。后面两个是性能相关的开关OLLAMA_FLASH_ATTENTION打开能降一点显存占用并提升长上下文的速度OLLAMA_KV_CACHE_TYPE设成 q8_0 能把 KV cache 压到原来的一半代价是精度有极小的损失实测在对话和摘要任务上看不出差别我基本都开着。2.2 用 Modelfile 固化一套自己的参数临时试玩直接ollama run 模型tag就行但只要你开始做正经事就该用 Modelfile。原因很简单默认参数是给通用对话调的做摘要、做抽取、做代码补全需要的参数完全不一样每次在命令行里手敲参数不现实写进脚本里又散落各处。Modelfile 就是把这些东西固化成一个带名字的本地模型变体。FROM 对话模型tag # 系统提示词角色和输出约束写在这里 SYSTEM 你是一个严谨的文本信息抽取助手。 只输出 JSON不要输出任何解释性文字。 字段缺失时填 null不要编造。 # 采样参数 PARAMETER temperature 0.2 PARAMETER top_p 0.9 PARAMETER repeat_penalty 1.1 PARAMETER num_ctx 8192 PARAMETER num_predict 1024 # 截断符防止模型自问自答 PARAMETER stop |im_end| PARAMETER stop 用户写完之后构建并运行ollama create extractor-v1 -f ./Modelfile ollama run extractor-v1这里有几个点值得展开。temperature对抽取类任务是关键0.7 到 0.8 是聊天手感好的区间但做结构化抽取的时候0.2 甚至 0.1 能显著降低字段乱填的概率代价是输出会变得有点机械如果你用它写文案就别这么设。num_ctx是最容易设错的一个很多人以为设大点总没坏处其实第一它会吃显存第二上下文过长时模型对中间部分的注意力会下降也就是常说的“中间遗忘”你把 20 页文档一股脑塞进去效果往往不如先检索出最相关的三段。我的经验值是单轮问答 4096 够用文档问答 8192 是个平衡点真的要做长文档处理走分块检索而不是硬堆上下文。stop这个参数特别容易被忽略但它在本地跑小模型时几乎是必须的因为小模型经常在回答完之后继续自问自答、模拟下一轮对话加上正确的截断符能省掉大量后处理。注意Modelfile 里的 SYSTEM 是“默认值”调用 API 时如果请求体里又传了 system 字段会覆盖它。这一点在多人共用一个模型变体的时候很容易出事故建议在应用层统一管理 system prompt别两边都写。2.3 三种调用方式按场景挑一个第一种是命令行适合调试和验证 prompt不适合做业务。第二种是原生 REST 接口最直接/api/generate是单轮补全/api/chat是带消息历史的对话两个都支持流式。第三种是兼容接口路径带/v1格式和主流接口规范一致好处是你手上那些现成的 SDK 和代码几乎不用改就能指过来。import json import requests # 流式对话适合做逐字输出的交互界面 def chat_stream(prompt: str, model: str extractor-v1): url http://127.0.0.1:11434/api/chat payload { model: model, messages: [ {role: system, content: 你是一个简洁的技术助手。}, {role: user, content: prompt}, ], stream: True, options: { temperature: 0.3, num_ctx: 8192, num_predict: 512, }, } with requests.post(url, jsonpayload, streamTrue, timeout120) as resp: resp.raise_for_status() for line in resp.iter_lines(): if not line: continue chunk json.loads(line) piece chunk.get(message, {}).get(content, ) if piece: yield piece if chunk.get(done): # 这里的统计数据很有用能看出真实的吞吐 print(eval_count:, chunk.get(eval_count)) print(eval_duration_ns:, chunk.get(eval_duration)) break if __name__ __main__: for text in chat_stream(用三句话解释什么是量化): print(text, end, flushTrue)这段代码里我最想让你注意的是最后的eval_count和eval_duration两个字段。它们分别是本次生成的 token 数和耗时纳秒。拿 eval_count 除以 eval_duration 再乘 10 的 9 次方就是你机器上真实的 token/s。这个数字比任何跑分都靠谱而且它会把“加载模型的时间”和“生成的时间”分开算你能立刻判断出慢是因为模型没常驻、还是因为算力不够。我在排查性能问题的时候第一步永远是看这两个数。兼容接口的用法更简单把地址指过去就行from openai import OpenAI client OpenAI(base_urlhttp://127.0.0.1:11434/v1, api_keynot-needed) resp client.chat.completions.create( modelextractor-v1, messages[{role: user, content: 把这句话改得更口语化本方案具备较高的可行性。}], temperature0.6, ) print(resp.choices[0].message.content)api_key 随便填一个非空字符串就行本地服务不校验。这个方式的好处是生态兼容坏处是有些原生参数比如 num_ctx没法通过这个接口传得在服务端或者 Modelfile 里定好。我一般两种都用调试和压测走原生接口业务代码走兼容接口省得维护两套客户端。3. 多模态模型图像理解怎么落地才不白费3.1 先把能力边界摸清楚别指望它当 OCR 用多模态模型这块误解最多。很多人第一次用是拿一张模糊的发票照片让它提取金额结果数字错得离谱然后得出结论“这玩意没用”。其实问题出在任务选错了。视觉语言模型擅长的是“理解画面语义”这张图里是什么场景、这个界面在做什么操作、这张图表想表达什么趋势、这个商品图的主体是什么风格。它不擅长的是“逐字精确识别”和“小字号细节”那是有专门工具干的活。你让它读一张清晰的、大字的、排版规整的截图它表现不错你让它读一张倾斜的、有反光的、字号 8pt 的票据它就开始编了。我自己的分工原则是这样需要精确字符的先走专门的文字识别把结果作为文本喂给大语言模型做理解和结构化需要判断画面内容、做分类打标、做视觉问答的直接上多模态模型。这两条路都能在 ollama 里跑前者是文本模型后者是视觉模型切换成本就是一次模型加载。还有一个边界是关于分辨率的。图像在进入模型之前会被切块编码块数是有上限的图太大反而会被压缩细节全丢。所以正确做法不是“给最高清的图”而是“给刚好能看清关键信息的图”。我一般会把长边缩到 1024 到 1568 这个区间再送进去效果比原图直送更好速度也快不少。3.2 图片问答的完整实操链路ollama 的多模态调用方式和文本几乎一样区别就是消息里多一个 images 字段值是图片的 base64 编码不带 data URI 前缀。这里有个坑不同版本对是否接受 URL 支持不一致最保险的做法永远是自己读文件转 base64。import base64 import json import requests def encode_image(path: str) - str: with open(path, rb) as f: return base64.b64encode(f.read()).decode(utf-8) def ask_image(image_path: str, question: str, model: str 视觉模型tag): payload { model: model, messages: [ { role: user, content: question, images: [encode_image(image_path)], } ], stream: False, options: {temperature: 0.1, num_ctx: 4096}, } resp requests.post( http://127.0.0.1:11434/api/chat, jsonpayload, timeout180, ) resp.raise_for_status() return resp.json()[message][content] if __name__ __main__: print(ask_image(./sample.png, 这张图里的界面在做什么操作分步骤描述。))跑通之后你会发现两件事第一速度比纯文本慢不少因为图像编码本身要算力一张图几百毫秒到几秒都正常第二问法对结果影响极大。我做了几十组对比发现“开放式提问”几乎必然得到一堆虚的形容词而“限定输出格式的提问”效果好一个档次。比如不要问“描述这张图”而是问“这张图里一共有几个按钮分别写着什么字用 JSON 输出字段为 buttons 数组”。把问题变成一个填空题模型的准确率会明显上去。3.3 批量处理与结构化输出这才是生产用法真正有价值的场景是批量。比如你有一千张商品图要打上“品类、主色调、是否有文字、拍摄角度”四个标签。手工做要两天写规则做不准这时候多模态模型就是刚需。但批量跑有几个必须处理的问题我一个个说。第一个是失败重试。图片可能损坏、可能格式不支持、可能模型输出跑偏一次失败不能中断整个批次。第二个是结果校验。模型返回的 JSON 未必合法可能带 markdown 代码块包裹可能字段名拼错必须有兜底解析。第三个是限速。本地服务虽然不是外部接口但并发太高一样会排队甚至 OOM得控制同时进行的请求数。import json import re from concurrent.futures import ThreadPoolExecutor, as_completed def extract_json(text: str): 从模型输出里尽力抠出 JSON失败返回 None text text.strip() # 去掉 markdown 代码块包裹 text re.sub(r^(?:json)?\s*|\s*$, , text, flagsre.MULTILINE).strip() try: return json.loads(text) except json.JSONDecodeError: # 退一步找第一个 { 到最后一个 } 之间的内容 start, end text.find({), text.rfind(}) if start ! -1 and end start: try: return json.loads(text[start:end 1]) except json.JSONDecodeError: return None return None PROMPT 分析这张商品图严格按以下 JSON 结构输出不要输出其他内容 {category: 品类20字以内, main_color: 主色调, has_text: true或false, angle: 正面/侧面/俯视/其他} 字段无法判断时填 null禁止编造。 def tag_one(path: str): for attempt in range(3): try: raw ask_image(path, PROMPT) data extract_json(raw) if data is not None: return path, data except Exception as e: if attempt 2: return path, {error: str(e)} return path, {error: parse_failed} def batch(paths, workers2): results [] with ThreadPoolExecutor(max_workersworkers) as pool: futures [pool.submit(tag_one, p) for p in paths] for fut in as_completed(futures): results.append(fut.result()) return results这段代码里workers2是我实测下来的保守值。单张显卡跑 7B 级视觉模型并发开到 4 以上基本就是排队吞吐不会提升反而会因为上下文切换变慢。更稳的做法是先跑 20 张测一下单张平均耗时再决定并发数——如果单张 3 秒你希望 1000 张在两小时内跑完那并发 2 就够没必要往上堆。提示批量跑之前一定先用 20 张典型样本做一次人工核对把错误类型归类。我遇到过最常见的是“模型给所有图都填了同一个主色调”一查发现是 prompt 里没禁止默认值加上“无法判断填 null”之后就好了。这种问题不抽样根本发现不了。4. 工程化落地把模型接进真实业务4.1 RAG 链路嵌入、检索、拼上下文本地模型最典型的工程用法就是检索增强。整条链路四步文档切块、向量化、检索、拼上下文生成。ollama 在这条链路上提供两个角色嵌入模型把文本变成向量和生成模型拿着检索结果回答。嵌入模型很小几百 MB跑起来飞快一般常驻内存不卸载。import numpy as np import requests def embed(texts, model嵌入模型tag): 批量取向量注意不同版本入参格式略有差异 url http://127.0.0.1:11434/api/embed resp requests.post(url, json{model: model, input: texts}, timeout60) resp.raise_for_status() return np.array(resp.json()[embeddings], dtypenp.float32) def cosine_topk(query_vec, doc_vecs, k5): # 向量已归一化时点积即余弦相似度未归一化要先除模长 q query_vec / (np.linalg.norm(query_vec) 1e-8) d doc_vecs / (np.linalg.norm(doc_vecs, axis1, keepdimsTrue) 1e-8) scores d q idx np.argsort(-scores)[:k] return idx, scores[idx]切块策略我踩过的坑最多。一开始按固定 500 字切结果经常把一个完整段落从中间劈开检索出来的片段前后不搭。后来改成按结构切先按标题层级切大块块超过 800 字再按句号切块之间保留 100 字左右的重叠这样跨块的信息不会断。另外每块前面加一行所属章节标题这行元数据对检索命中的帮助比想象中大因为它把上下文信息也编进了向量里。检索这块我的经验是top-k 别设太大。k3 到 5 是甜点区k10 的时候噪声明显变多模型容易被不相关的片段带偏而且上下文变长之后速度也慢。如果召回质量不够优先优化切块和加元数据而不是加 k。有条件的话再加一层重排——用一个小模型对召回的 10 条打分取前 3 条效果提升明显ollama 里跑一个小参数量的模型做重排完全够用。4.2 并发、上下文、显存三个互相拉扯的数这三个数是我调试时花时间最多的地方因为它们不是独立的。并发槽位每增加一个KV cache 就多一份上下文长度每翻一倍KV cache 也跟着翻倍显存总共就那么点三方争抢的结果就是排队或者直接失败。我做过一组实测在一个固定显存的机器上跑 7B 的 4 bit 模型num_parallelnum_ctx单请求速度总吞吐现象14096快低串行延迟最低24096略降约 1.6 倍比较均衡的配置44096明显降约 2 倍延迟敏感场景不合适216384中中长文档问答的常用配置416384慢可能失败显存不够就开始换页看懂这张表的规律你就能自己推配置。并发提升的是吞吐不是延迟如果你的接口是同步等结果的并发开大只会让每个用户等更久如果你的场景是后台批处理那并发就是纯赚。上下文长度是最贵的参数因为它同时影响显存和速度能砍就砍。还有一个隐藏项是OLLAMA_MAX_LOADED_MODELS如果你同时要用生成模型和嵌入模型这个值至少得是 2否则每次检索都要重新加载一个请求能拖到十几秒。注意判断是不是显存问题不要靠猜。跑一次看系统日志或者在请求返回的统计字段里看 prompt_eval 和 eval 的耗时比例。如果加载耗时异常长、或者速度在跑的过程中骤降八成是溢出到内存了。4.3 结构化输出让模型吐出能用代码解析的东西这一节是我认为本地模型最实用的能力。以前做结构化抽取要么靠 prompt 求它输出 JSON再写一堆容错解析要么上微调。现在多数推理服务都支持在请求里直接指定输出格式为 JSON模型会在解码阶段做约束输出合法性大幅提升。payload { model: extractor-v1, messages: [ {role: system, content: 你是信息抽取助手只输出 JSON。}, {role: user, content: 从下文中抽取人名、公司、金额\n text}, ], format: json, stream: False, options: {temperature: 0.1}, }format: json是最简单的用法。要求更严的场景可以直接给一个 JSON Schema服务会按 schema 约束解码字段名、类型、枚举值都能锁死。我做过对比不加约束时一千条抽取里有三四十条需要修复加了 schema 约束之后需要修复的降到个位数而且失败的基本都是原文本身信息缺失不是格式问题。不过约束也有代价。第一是速度会慢一点因为每步解码都要做合法性检查第二是过于严格的 schema 会让模型在信息不足时硬凑所以 schema 里该允许 null 的地方一定要允许第三是嵌套层级别太深三层以上开始容易出结构错乱宁可拆成两次调用。至于轻量的函数调用我的建议是如果你的工具不超过五个、参数都是简单类型用结构化输出就够了不必上完整的工具调用协议。让模型输出一个{tool: search, args: {...}}的 JSON你自己在代码里分发简单可控、调试方便。工具多了、需要多轮编排了再考虑更完整的协议也不迟。5. 问题排查速查与踩坑记录5.1 六类高频故障按这个顺序定位我整理了自己遇到过的问题按“先查什么后查什么”排了个序因为最怕的是一上来就怀疑模型能力折腾半天发现是配置问题。第一类模型压根没跑起来。表现是命令卡住或者报连接失败。先确认服务在不在看进程、看端口再确认模型有没有下完列出本地模型看大小最后确认磁盘空间——一个模型动辄几 G磁盘满了下载会静默失败。第二类跑起来了但慢得离谱。先看是不是每次都重新加载模型办法是连续发两次请求看第二次会不会快很多如果第二次快说明是 keep_alive 设太短或者模型没常驻。然后看是不是显存溢出办法是看日志或降低 num_ctx 试一次。最后才怀疑硬件。第三类输出乱码或者循环重复。八成是 stop 符号没设对模型不知道什么时候停。其次是 temperature 太低加 repeat_penalty 太低尤其在长文本生成里重复惩罚设到 1.1 到 1.2 基本能解决大部分复读。还有一种情况是上下文超了模型把已经生成的内容又看了一遍这时候要减小 num_ctx 或者做分块。第四类多模态返回的内容和图片无关。先检查 base64 有没有前缀污染data:image/png;base64,这个前缀有些版本不认再检查图片是不是太大了被压缩到看不清最后检查 prompt 里有没有明确说明“基于这张图回答”。我遇到过最离谱的一次是路径写错模型收到的是另一张图。第五类结构化输出解析失败。先看返回的原始字符串大概率是被 markdown 代码块包了或者前面加了一句“好的以下是结果”。能用 format 约束就用不能用就在 prompt 里把“不要输出任何解释”写两遍再加重试。第六类批量处理越来越慢。检查有没有内存泄漏图片 base64 存着不释放、有没有并发过高导致排队、有没有结果没写盘全堆在内存里。我自己的批量脚本都强制每 50 条写一次盘出问题能续跑。5.2 一张速查表外加几条私房经验现象最可能原因快速验证处理办法连接被拒服务没起或监听地址不对看进程和端口监听检查服务状态改 OLLAMA_HOST首次请求几十秒模型正在加载连发两次看第二次调大 keep_alive加常驻生成速度突然掉一半显存溢出转内存降 num_ctx 再试减上下文或换更小量化输出重复同一句缺 stop 或惩罚过低手动加 stop 测试补 stoprepeat_penalty 1.1图片问答答非所问base64 带前缀或图过大打印编码前 50 字符去前缀长边缩到 1568JSON 解析失败被代码块包裹打印原始输出用 format 约束 兜底解析切换模型特别慢同时只驻留一个模型看加载耗时调大 MAX_LOADED_MODELS批量越跑越慢内存堆积或并发过高看内存占用曲线分批写盘降并发再说几条文档里不会写的经验。第一条动手写业务代码之前先用命令行把 prompt 调到满意为止别一边写代码一边调 prompt两件事混在一起你永远不知道是 prompt 的问题还是代码的问题。第二条给你的模型变体起带版本号的名字比如extractor-v1、extractor-v2改了一版 prompt 就换个名字出问题能立刻切回去这个习惯救过我好几次。第三条本地模型的输出一定要在应用层做长度截断和敏感内容过滤它不会自动帮你做这些直接透给前端是要出事的。第四条模型的能力边界是固定的与其反复调 prompt 逼它做它不擅长的事不如换个更大的模型或者换条技术路线时间成本差得非常多。最后分享一个我觉得很值的小技巧给每个请求都记一条日志包含模型名、参数、输入 token 数、输出 token 数、耗时、返回内容摘要。这件事前期看着多余等你需要回答“为什么上周的效果比这周好”这种问题时它就是唯一的证据。我在实际使用中发现绝大多数所谓的“模型变笨了”查下来都是某一版的参数被悄悄改了或者上下文长度不够导致关键信息被截掉了有日志一翻就知道。这个内容后面还能往两个方向扩一是把抽取任务做成带人工反馈的小闭环把人工修正的结果回流成 few-shot 示例二是把视觉模型和文本模型串起来做“看图 → 出结构化描述 → 进检索库”的自动化流水线这两个方向我都试过跑得通坑也不少值得单独拎出来聊。