HOME

本地大模型学习 01:用 LM Studio 从模型导入走到推理与上下文实验

文章目录12 节

把模型下载到电脑里,打开聊天窗口,看到它开始回答,确实很容易产生“已经会部署大模型了”的感觉。我以前体验过本地运行模型,这次想再往前走一步:弄清请求如何进入模型,回答为什么会出错,参数改变了什么,以及这些变化值不值得付出额外时间和内存。

于是,我在一台 M5、16 GB 统一内存的 Mac 上,从只有 LM Studio、没有本地模型的状态重新开始。实验设计和排查有 AI 助手协助,本文记录的是我实际执行的命令、返回结果,以及复盘时修正过的认识。助手代跑的批量测试没有作为我已经掌握自动化的证据。

第一阶段最有价值的收获,是逐渐学会把三件事分开判断:请求是否成功,输出是否符合格式,以及内容是否有事实依据。

这次使用的环境

项目实验环境
设备Apple M5 Mac,16 GB 统一内存
应用LM Studio 0.4.23+1
模型unsloth/Qwen3.5-4B-GGUF
本地文件Qwen3.5-4B-Q4_K_S.gguf
文件大小2.59 GB
模型标识qwen3.5-4b
本地接口地址http://127.0.0.1:1234
常用加载配置上下文 8192 token,最大并行数 4
推理后端llama.cpp;上下文重载实验中从 2.32.0 切换到 2.33.0

4B 表示约 40 亿参数;Q4_K_S 是一种低位数量化方案,用更少位数表示模型权重,以降低存储和运行成本。GGUF 是模型文件格式,llama.cpp 是执行推理的后端,两者不是同一类东西。

Token 是模型处理文本的离散单位,可能对应词、子词、标点、字符或字节片段,并不一定具有完整语义。后文的 token 数均以本次接口返回的统计为准。

这些实验主要用于建立概念和观察行为,每组通常只有一两个代表性请求,没有进行足以比较准确率的统计评测。文中的速度和质量判断都只对应这台机器、这些输入和当时的运行状态。

第一处障碍:文件在磁盘上,模型列表却是空的

我手动下载模型后,把文件放到了下面的位置。以下是当时的目录记录,不是需要执行的命令:

~/.lmstudio/models/Qwen/Qwen3.5-4B-Q4_K_S.gguf

LM Studio 显示没有本地模型。排查索引后发现,这个文件被列在 unclassifiedFiles,也就是“未归类文件”中。

我把它调整为“发布者/模型目录/文件”的层级:

~/.lmstudio/models/unsloth/Qwen3.5-4B-GGUF/Qwen3.5-4B-Q4_K_S.gguf

刷新后,模型出现在列表中,随后成功加载并完成了测试对话。这个过程验证了本次问题与目录组织有关;仅有正确扩展名并不等于应用一定能识别。

下载页面显示的总大小也曾与本地文件大小不同。最初排查时怀疑过下载不完整,但仅凭两个大小不能下结论:下载项可能包含额外组件,也可能来自不同文件组合。这次最终能够加载并生成回答,解决了使用问题;文件来源与完整性若要严格核验,仍应对照具体文件的大小和校验值。

从聊天窗口走到第一条接口请求

API(Application Programming Interface,应用程序编程接口)让其他程序也能使用本地模型。我在 LM Studio 的本地服务页面启动服务后,使用终端发送请求。

这里需要认识三个字段:system_prompt 规定助手的行为,input 提供本次问题,model 指定使用哪个模型。模型标识以实际加载结果为准,并不总是模型文件名。

风险等级:INFO(本机功能验证)

下面是本次验证过的请求形式,在 macOS 终端运行,要求 LM Studio 已启动本地服务并能加载 qwen3.5-4b。端口或模型标识不同的读者需要替换对应值。请求会使用本机算力,内容可能进入本地服务日志,不应放入凭据。-sS 隐藏进度但显示错误,-H 声明正文格式,-d 发送正文;每行结尾的反斜杠表示命令继续到下一行。成功时返回包含 outputstats 的结果。

curl -sS 'http://127.0.0.1:1234/api/v1/chat' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "qwen3.5-4b",
    "system_prompt": "You answer only in rhymes.",
    "input": "What is your favorite color?",
    "reasoning": "off",
    "max_output_tokens": 128,
    "stream": false
  }'

JSON(JavaScript Object Notation,一种结构化数据格式)用字段和值组织这段请求。字符串内的双引号需要转义,字段之间要有逗号。刚开始我曾发送空对象,得到“缺少 model”的错误;也曾因为 JSON 正文中的反斜杠和缺失逗号而得到 invalid_json

这些错误发生在模型生成之前。它们提醒我,先区分请求构造错误与模型回答错误,排查会轻松很多。

成功的请求返回了押韵的英文短诗。一次结果记录为输入 30 token、输出 56 token、思考 token 为 0,生成速度约 36.07 token/s。

stream: false 表示完整结果准备好后再返回,所以终端不会逐字显示。即使服务器统计的首 token 时间很短,用户也要等整段回答结束才能看到结果。

提示词能改变回答方式,却不能保证定义准确

我沿用押韵要求,问模型“token 是什么”。它回答:

Token 就是数字世界的碎片,用来换取价值与权利。

回答有了修辞,却没有解释我正在学习的大语言模型概念。换成“严谨的中文技术助手”后,模型又把 token 解释成通信系统里的标记单位。

这时我才意识到,问题本身没有给出领域。明确问“在计算机网络中”和“在大语言模型中”后,模型能够区分两种语境;不过后者仍用了“最小语义单位”这个不够准确的说法。

随后,我要求它遇到歧义时分别解释不同含义。它开始长篇介绍区块链等领域,128 token 的上限很快耗尽;把上限改成 512 后,答案更长,却仍然没有完整结束。最后明确指定三个领域、每个领域一句话,它才在 104 token 内完成。

这组经历体现了 Prompt Engineering(提示词工程)的作用:可以控制范围、风格和格式,也可以帮助模型处理歧义。但要求“严谨”不意味着定义自然正确,增加输出额度也不意味着模型会自动抓住重点。

结构化输出:合法 JSON 只是第一关

我接着让模型从服务记录中提取 serviceportstatus。连续三种提示下,观察到的结果如下:

提示方式当次结果需要注意的问题
只描述提取任务字段和值正确,但带代码围栏整段正文不能直接作为 JSON 解析
明确只输出 JSON去掉围栏,端口写成 "8080"语法正确,类型未满足应用期望
加类型要求和一个示例端口输出为整数 8080当前样本符合预期,仍需校验

第三次实际返回的正文是:

{"service":"local-llm","port":8080,"status":"运行中"}

这里需要修正一个最初的判断:第二次请求没有明确告诉模型端口必须是整数,所以字符串端口不能简单判为违反指令。第三次既增加了示例,又明确了字段类型,并修改了输入措辞,因此也不能把改善全部归因于示例。

提供一个示例更准确地称为 One-shot Prompting(单样本提示);提供少量示例则通常称为 Few-shot Prompting(少样本提示)。这些手段帮助模型遵循约定,但不会把语言模型变成严格的数据校验器。

我随后使用 jq(命令行 JSON 处理工具)解析返回值。LM Studio 的外层响应是 JSON,里面的 content 又是一段包含 JSON 的字符串,因此要处理两层。

风险等级:INFO(本地数据解析)

对上述成功的纯 JSON 抽取请求,将下面的管道追加在完整 curl 命令之后。需要本机已安装 jq。第一个 jq 读取第一个输出项的正文,-r 去掉外层字符串引号;第二个 jq 再解析正文。只有当前响应的第一个输出项是最终消息时才适用;开启 reasoning 时必须先确认输出类型。预期显示一个格式化对象,解析报错时应查看完整原始响应。

| jq -r '.output[0].content' | jq .

在此基础上,我检查字段存在性,以及 servicestatus 是否为字符串、port 是否为数字,得到 true。这只是最小的 Schema(数据结构约束)检查:它没有检查端口范围、整数性、状态是否真实,也没有证明内容与原始记录一致。

我由此形成了一个更实用的判断顺序:服务返回 HTTP(Hypertext Transfer Protocol,超文本传输协议)成功状态,只能说明请求在协议层成功;解析通过只能说明数据满足已写下的规则;事实正确还要与原始资料或当前系统证据核对。

打开思考之后,我先遇到了两个 null

我想比较关闭和低强度推理,于是把 reasoningoff 改成 low,终端却只显示:

{"output":null,"stats":null}

真正的问题在完整错误响应里:当前模型在 LM Studio 中只支持 offon。用于显示结果的 jq '{output, stats}' 隐藏了 error 字段,不存在的字段则显示为 null

这个失误来自实验设计:接口可能定义多种选项,不代表具体模型都支持。以后我会先读取模型能力,诊断时先查看完整响应。原生接口与兼容接口的参数也需要区分:

本次测试的接口输入字段关闭推理输出上限
/api/v1/chatinputsystem_promptreasoning: "off"max_output_tokens
/v1/chat/completionsmessagesreasoning_effort: "none"max_tokens

这张表描述本次版本和模型的实测用法,不能代替其他版本的能力检查。

思考花了三倍成本,答案却没有明显更可靠

用于对照的是一段模拟事件:模型校验和加载成功;并发 1 时请求在 18 秒内完成;并发 4 时先出现内存压力警告,随后在 60 秒超时;没有模型读取或校验错误。要求回答分成直接证据、最可能推断、未证明事项和两个只读验证步骤。

推理设置输出上限总输出 token其中 reasoning token等待时间最终正文
off5124770约 12.6 秒完整
on512512512约 13.6 秒
on102410241024约 27 秒
on1024015471268约 42 秒完整

前两次开启推理时,额度全部用于 reasoning,尚未生成最终 message。上限提高到 10240 后,模型实际使用 1547 token 就结束了,正文约 279 token。这说明在本次运行中,思考与正文共享输出额度;上限也不意味着必须全部用完。

中间 reasoning 是英文,最终回答是中文。因此只看到英文 reasoning,不能立即判定最终答案会用英文;这些文字也不能当作模型内部计算的完整、可靠解释。

完整的 on 结果比 off 多用了约 3.24 倍输出 token,等待约为 3.33 倍。不过它仍把“并发 1 正常”写成“排除基础逻辑错误”,把内存压力进一步推断成资源耗尽或分配失败。前者不能排除并发条件下的逻辑问题,后者也需要额外指标支持。

它还建议查询垃圾回收等运行时指标。把这些作为待查方向可以讨论,但资料没有证明应用存在相应机制。另一个需要公平对待的问题是:提示里没有说明操作系统,所以模型提出 Linux 命令,不能直接记为“没有遵守 macOS 要求”;真实应用中应先把平台信息交代清楚。

这次对照不足以得出推理模式整体优劣,只让我决定:当前短任务继续关闭推理;复杂任务是否开启,要看最终答案的收益,而不以中间分析的长度判断。由于输出上限、随机采样和缓存条件没有全部固定,这也不是严格性能基准。

温度影响选择,不承诺正确率

Temperature(温度)改变下一个 token 的概率分布。低温更集中于高概率候选,高温让候选之间的概率差距更平缓。它改变生成选择,不给模型增加知识。

我用一段固定事故记录对比 0.1 与 1.0:14:00 因 8080 端口冲突启动失败,14:05 改为 8081,14:06 健康检查通过,没有数据完整性验证结果。两次都要求三句话概括现象、处理和未确认事项。

温度输出 token当次观察
0.180省略明确的 14:06,并写成“服务当前运行正常”
1.093保留三个时间点,未声称数据完整性已验证

两次都写了三句话,却都没有严格做到每句话只承担一个指定角色。高温版本这次反而保留了更多信息,低温版本也产生了时间范围外推。

每档只有一个样本,任务也比较简单,不能据此比较准确率或稳定性。即使能固定随机种子,单个样本仍然不够。我的使用原则是把低温作为事实型任务的起点,并继续核对证据;不能把低温当作防止幻觉的开关。

输入成本:同样的事实,token 数并不相同

Tokenizer(分词器)把文本转换成 token 编号,这个过程称为 Tokenization(标记化或分词)。字符数、字节数和 token 数不能直接互换。

我把服务名、端口和状态分别写成中文自然语言与紧凑 JSON,固定系统提示词,并让两次都只回答“收到”。结果如下:

表达方式完整输入 token输出 token
中文自然语言482
JSON452

JSON 在这组内容中少了 3 token,占完整输入的 6.25%。但完整输入还包括系统提示词和对话模板,这个百分比不能当作正文的通用压缩率。换一种数据结构、换模型或换分词器,结果可能不同;这里也没有直接查看切分列表,不能只凭总数确定是哪几个词贡献了差异。

格式选择仍要考虑阅读、解析和信息完整性,不值得为了这 3 个 token 牺牲任务表达。

输入变长,等待第一个 token 的时间也变长

Context Window(上下文窗口)是模型可容纳的上下文容量;实际输入、历史消息及生成内容要在相应运行限制内安排。把容量设置成 8192,不意味着每次都处理 8192 token。

在开始生成之前,模型先对输入进行 Prefill(预填充)。TTFT(Time to First Token,首 token 延迟)用于观察这一阶段及相关开销。本文表中的 TTFT 是服务端返回的指标,不是客户端精确测得的端到端延迟。

我选取学习项目中的短说明和长计划,各运行一次,都只要求回答“收到”:

输入input token服务端 TTFToutput token
短说明6561.136 秒2
长计划36485.034 秒2

输入约增至 5.56 倍,TTFT 约增至 4.43 倍。结果符合“更多输入通常需要更多预填充工作”的预期,但不能建立固定比例:两个文件内容不同,每档只测一次,缓存与系统负载也没有完全隔离。

接口还返回了 33.23 和 14.77 token/s 的生成速度。因为输出仅有两个 token,计时波动影响很大,我没有用它判断稳定解码性能,也没有测得内存随输入增长的精确曲线。

这个观察对后续 RAG(Retrieval-Augmented Generation,检索增强生成)很直接:多放入文档片段会占用上下文,并可能增加响应前的等待。检索结果应该与问题相关,而不是把可用容量填满。

容量翻倍,并不等于同一请求的工作量翻倍

接着,我保持短输入不变,比较 4096 与 8192 的加载配置。先做资源估算,两档都显示 2.41 GiB,且 Confidence: LOW。GiB 是按 1024 进制计量的容量单位,与十进制 GB 不同;低置信度估算不能替代实际测量。

实际重载时出现了一个意外:旧进程使用 llama.cpp 2.32.0,新进程已经切换到 2.33.0。我没有拿旧版本 8192 的内存直接比较,而是在 2.33.0 下恢复 8192,并补跑相同请求。

上下文配置后端版本input tokenTTFT请求后 RSS
40962.33.06561.101 秒3128.4 MiB
81922.33.06561.128 秒3036.2 MiB

RSS(Resident Set Size,常驻内存集)反映进程当时驻留在物理内存中的页面;MiB 同样是二进制容量单位。RSS 不等于该模型涉及的全部统一内存,也不能单独代表 KV Cache(Key-Value Cache,注意力键值缓存)。

KV Cache 保存注意力计算中可复用的键和值,帮助生成后续 token。其内存增长方式取决于模型架构、缓存精度和后端分配策略。当前 Qwen3.5 属于混合架构,不能直接套用所有层都使用普通全注意力缓存的简单公式。

这次两档 input token 完全一致,TTFT 相差约 2.48%;8192 的 RSS 反而低 92.2 MiB。它没有证明大上下文更省内存,也没有证明缓存一定按精简置备方式分配。更准确的结论是:这两个短请求没有呈现明显的延迟增加,单次 RSS 测量也没有分离出可归因于上下文容量的缓存增量。

实验结束时,我把模型恢复为 8192、并行数 4。这是本次学习环境的保留配置,并不表示已经验证它能承受四个长请求同时运行。

第一阶段,我真正学会了什么

回头看,这一轮最需要克服的是把“有返回”“格式对”“听起来合理”混成一个成功判断。

现在我会先确认请求和运行参数是否生效,再检查输出结构,最后拿原始资料核对事实。问当前配置时,我会以当前系统状态为依据;问历史设计时,就查对应的设计文档。模型自己说得肯定,并不会增加结论的证据强度。

对于格式固定的日志提取,我也不再默认调用大模型。普通文本工具能稳定解决的任务,可以直接使用工具;需要整合含糊信息或多段证据时,再考虑模型。即便开启推理,也仍要检查它有没有把“可能”写成“已经证实”。

这一篇到上下文实验结束。下一阶段再比较同一个模型的不同量化版本,并逐步建立评测规则。当前已经足够明确的一点是:模型部署的进步,不只是能加载更大的模型,也包括能解释每次结果凭什么可信、成本花在了哪里,以及哪些结论还没有测出来。

LM-Studio 本地大模型 学习记录