第 2 课
本地服务、OpenAI SDK 与有状态对话
用 lms server start 打开 1234 端口的本地服务,用 OpenAI Python SDK 加 JSON Schema 拿到可直接解析的待办清单,用 /api/v1/chat 的 response_id 和 previous_response_id 做多轮对话并对比 store: false,再卸载模型、用带 ttl 的请求触发 JIT 自动加载。
学习位置仅保存在当前浏览器,有效期 180 天。
图文教材
本课对照的官方页面:lms server start、OpenAI 兼容接口、结构化输出、REST 快速开始、有状态对话、TTL 与自动卸载。命令与输出在无显卡的 Linux 云主机上实测(llmster 0.0.27+1,openai 3.42.0),输出为节选。
上一课模型已经加载进内存,名字是 granite。这一课打开 HTTP 服务,先用 OpenAI 官方 Python SDK 调用它并拿到符合 JSON Schema 的输出,再用 LM Studio 自己的 /api/v1/chat 接口做多轮对话,最后试一下「不手动加载、请求来了再加载」的 JIT 模式。
第 1 步:打开本地 HTTP 服务
如果上一课之后关过机,先把 daemon 和模型重新拉起来:
export PATH="$HOME/.lmstudio/bin:$PATH"
lms daemon up
lms load ibm/granite-4-micro --context-length 8192 \
--identifier granite --ttl 3600 -y
然后启动服务并确认:
lms server start
lms server status
对照结果:
Success! Server is now running on port 1234
The server is running on port 1234.
默认端口是 1234,默认只监听本机(127.0.0.1)。常用开关:
| 写法 | 作用 |
|---|---|
lms server start --port 3000 | 换端口 |
lms server start --cors | 允许网页前端跨域访问 |
lms server start --bind 0.0.0.0 | 对局域网开放,务必先读第 3 课的安全提醒 |
服务开着时,列出可用模型:
curl -s http://localhost:1234/v1/models
对照结果(节选):
{
"data": [
{"id": "granite", "object": "model", ...},
{"id": "ibm/granite-4-micro", "object": "model", ...},
{"id": "text-embedding-nomic-embed-text-v1.5", ...}
],
"object": "list"
}
granite 是已加载实例的名字,ibm/granite-4-micro 是磁盘上的模型。两个都能当 model 用,区别在第 5 步。
第 2 步:用 OpenAI SDK 调用,并要求输出 JSON
LM Studio 提供 OpenAI 兼容的 /v1/chat/completions,现有代码只要把 base_url 换成本机地址。没开鉴权时 api_key 随便填,官方示例填的是 "lm-studio"。
先建虚拟环境装 SDK:
python3 -m venv venv && . venv/bin/activate
pip install openai lmstudio
新建 structured.py。response_format 用 JSON Schema 约束输出:必须是 todos 数组,每项有 task 和取值只能是高/中/低的 priority:
import json
from openai import OpenAI
client = OpenAI(base_url="http://localhost:1234/v1",
api_key="lm-studio")
schema = {
"type": "json_schema",
"json_schema": {
"name": "todo_list",
"strict": True,
"schema": {
"type": "object",
"properties": {
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"task": {"type": "string"},
"priority": {
"type": "string",
"enum": ["高", "中", "低"]},
},
"required": ["task", "priority"],
},
"minItems": 1,
}
},
"required": ["todos"],
},
},
}
resp = client.chat.completions.create(
model="granite",
messages=[
{"role": "system",
"content": "你把用户的话整理成待办清单。"},
{"role": "user",
"content": "明早九点前交周报,下午顺路取快递,"
"周末有空再整理书架。"},
],
response_format=schema,
temperature=0,
)
data = json.loads(resp.choices[0].message.content)
print(json.dumps(data, ensure_ascii=False, indent=2))
print("tokens:", resp.usage.prompt_tokens, "->",
resp.usage.completion_tokens)
运行:
python structured.py
对照结果:
{
"todos": [
{"task": "明早九点前交周报", "priority": "高"},
{"task": "下午顺路取快递", "priority": "中"},
{"task": "周末有空再整理书架", "priority": "低"}
]
}
tokens: 62 -> 98
(为节省篇幅,对照结果把每个对象压成了一行。)json.loads 能直接解析,因为 LM Studio 在生成时按 Schema 约束输出,不是生成完再碰运气。
第 3 步:用 /api/v1/chat 做有状态多轮对话
OpenAI 兼容接口每次都要把完整历史发过去。LM Studio 自己的 /api/v1/chat 默认是有状态的:服务端保存对话,每次返回一个 response_id,下一轮带上 previous_response_id 就能接着聊。
第一轮:
curl -s http://localhost:1234/api/v1/chat \
-H "Content-Type: application/json" \
-d '{"model": "granite",
"input": "我最喜欢的颜色是蓝色。用一句话回应我。"}'
对照结果:
{
"model_instance_id": "granite",
"output": [
{"type": "message",
"content": "我理解,蓝色真的很美丽,它象征着信任、
智慧和稳定,让人感到平静和安心。"}
],
"stats": {"input_tokens": 33, "total_output_tokens": 48,
"tokens_per_second": 8.06, ...},
"response_id": "resp_a1fe3c1a8cc150ac0d9a2238a4aad474..."
}
(content 和 response_id 为排版折行或截断。)把你自己拿到的 response_id 填进第二轮:
curl -s http://localhost:1234/api/v1/chat \
-H "Content-Type: application/json" \
-d '{"model": "granite",
"input": "我最喜欢什么颜色?只回答颜色。",
"previous_response_id": "这里换成你的 response_id"}'
对照结果(节选):
"output": [{"type": "message", "content": "蓝色"}],
"stats": {"input_tokens": 111, "total_output_tokens": 5, ...},
"response_id": "resp_4d5ceae760555128df3dd492e858e775..."
第二轮只发了一句话,input_tokens 却是 111,说明服务端自动把上一轮内容拼了进去。
第 4 步:用 store: false 关掉记录
不想让服务端保存对话时,加 "store": false,返回里就没有 response_id:
curl -s http://localhost:1234/api/v1/chat \
-H "Content-Type: application/json" \
-d '{"model": "granite",
"input": "我刚才说我最喜欢什么颜色?",
"store": false}'
对照结果(节选):
"output": [{"type": "message",
"content": "很抱歉,我不记得你之前的回答。请告诉我你最喜欢的颜色吧!"}],
"stats": {"input_tokens": 27, "total_output_tokens": 38, ...}
这次没带 previous_response_id,模型拿不到上文,input_tokens 只有 27。注意,小模型拿不到上文时不一定会老实说不知道。本课测试机第一次跑这条请求时,模型编了一个「红色」。要靠上文的功能,一定要带 previous_response_id,别指望模型自己承认不知道。
想看服务端视角的模型信息,可以用原生接口列模型。它比 /v1/models 详细,会给出量化、大小、已加载实例的配置和剩余 TTL:
curl -s http://localhost:1234/api/v1/models
对照结果(节选):
"key": "ibm/granite-4-micro",
"quantization": {"name": "Q4_K_M", "bits_per_weight": 4},
"size_bytes": 2099555678,
"params_string": "3B",
"loaded_instances": [{"id": "granite",
"config": {"context_length": 8192, "parallel": 4, ...},
"remaining_ttl_seconds": 3600}],
"max_context_length": 131072,
第 5 步:JIT 按需加载与 TTL
服务开着时,请求里的 model 写磁盘上的模型名(这里是 ibm/granite-4-micro),即使它还没加载,LM Studio 也会在第一次请求时自动加载,这就是 JIT(Just-In-Time)。官方文档说明,JIT 加载的模型默认空闲 60 分钟后自动卸载;也可以像下面这样在请求里写 ttl(单位秒)。
先卸掉上面手动加载的实例,确认内存里没有模型:
lms unload --all
lms ps
对照结果:
Unloaded 1 model.
No models are currently loaded.
发一个带 ttl 的请求,并计时:
time curl -s http://localhost:1234/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "ibm/granite-4-micro",
"ttl": 300,
"messages": [
{"role": "system", "content": "你是简洁的助手。"},
{"role": "user", "content": "只回答:好"}]}'
lms ps
对照结果(节选,JSON 只保留 model 和回答):
"model": "ibm/granite-4-micro" ... "content": "好"
real 0m3.721s
IDENTIFIER MODEL STATUS SIZE CONTEXT
ibm/granite-4-micro ibm/granite-4-micro IDLE 2.10 GB 8192
(后面几列:PARALLEL 4,LOAD CONFIG LM Studio,TTL 5m / 5m)
3.7 秒里包含了加载模型的时间,之后的请求就不用再等。TTL 5m / 5m 表示空闲 5 分钟后自动卸载,每来一次请求计时重置。用 lms load 手动加载的模型默认没有 TTL,会一直占着内存,所以第 1 课加载时写了 --ttl 3600。
小结
lms server start开服务,默认http://localhost:1234、只监听本机;--port、--cors、--bind按需加。- OpenAI SDK 把
base_url换成http://localhost:1234/v1就能用,response_format加 JSON Schema 可以拿到能直接解析的结构化输出。 /api/v1/chat默认有状态:用返回的response_id当下一轮的previous_response_id;"store": false则不保存、不返回response_id。- 请求里写磁盘上的模型名会触发 JIT 自动加载;
ttl控制空闲多久卸载,JIT 默认 60 分钟,lms load默认不卸载。
下一课让模型调用你写的函数,看清模型到底收到了什么,并把服务设成开机自启。
