第 2 课

本地服务、OpenAI SDK 与有状态对话

用 lms server start 打开 1234 端口的本地服务,用 OpenAI Python SDK 加 JSON Schema 拿到可直接解析的待办清单,用 /api/v1/chat 的 response_id 和 previous_response_id 做多轮对话并对比 store: false,再卸载模型、用带 ttl 的请求触发 JIT 自动加载。

图文25 分钟LM Studio 官方文档 ↗

课程目录第 2 / 3 课

学习位置仅保存在当前浏览器,有效期 180 天。

01 / 图文教材

图文教材

本课对照的官方页面: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 默认不卸载。

下一课让模型调用你写的函数,看清模型到底收到了什么,并把服务设成开机自启。