LangChain 系列 3:用 OpenAI 兼容接口连接本地 Qwen3 LLM


上一篇已经使用 MLX-LM 直接加载本地 Qwen3-14B-AWQ-4bit-MLX,完成中文生成测试,并把模型启动成只监听 127.0.0.1:18080 的本地 HTTP 服务。

直接加载模型适合验证模型文件和推理环境,但业务代码会和 MLX-LM 绑定在一起。上一篇已经完成从直接加载到 HTTP 服务的部署过程,本文从客户端角度说明怎样调用这个服务。

本文只介绍两种客户端调用方式:

  1. 使用 OpenAI Python SDK 调用本地模型。
  2. 使用 LangChain 的 ChatOpenAI 调用同一个本地模型。

两种方式都访问上一篇部署的 MLX-LM Server,不再在客户端代码中直接执行 mlx_lm.load() 和 generate()。

两种客户端与本地模型服务的关系如下:

OpenAI SDK 与 LangChain 调用本地接口

图中的 OpenAI 兼容端点属于 MLX-LM Server,并不是另一层独立服务。两个客户端发送的 HTTP 请求结构相近,但向上提供的调用方法和返回对象不同。

1. 什么是 OpenAI 兼容接口

1.1 从直接加载变成服务调用

上一篇先验证了直接调用过程:

Python 脚本 -> MLX-LM -> 本地模型文件

这种方式下,每个脚本都要自己加载模型。如果多个程序需要调用模型,就会出现重复加载、代码绑定和资源管理困难等问题。

随后将模型启动为 MLX-LM Server,调用过程变成:

OpenAI SDK / LangChain
        ↓ HTTP 请求:http://127.0.0.1:18080/v1/chat/completions
MLX-LM Server(提供 OpenAI 兼容接口)
        ↓ 加载模型并完成推理
本地 Qwen3 模型

模型只由服务端加载,客户端只负责发送消息和接收回答。

1.2 “兼容”不等于使用 OpenAI 在线模型

OpenAI 兼容接口是指服务端提供与 OpenAI API 相近的路径、请求字段和响应结构。本地服务地址为:

POST /v1/chat/completions

请求中仍然有 model、messages、temperature、max_tokens 等字段,但真正完成推理的是本机 Qwen3,不会把问题发送到 OpenAI 在线服务。

这种接口的价值在于:只要客户端允许修改 base_url,同一套调用方式就可以连接不同的兼容服务。

2. 准备客户端项目

2.1 当前代码目录

项目位于:

/path/to/llm-learning

本阶段代码结构如下:

llm-learning/
├── .venv_langchain/
├── Qwen3-14B-AWQ-4bit-MLX/
└── langchain/
    └── p03_openai_compatible_llm/
        ├── 01_test_openai_client.py
        ├── 02_test_langchain_chatopenai.py
        ├── README.md
        └── requirements.txt

Qwen3-14B-AWQ-4bit-MLX 是上一篇创建的项目共用模型目录,不需要重新下载或复制 7.8GB 权重。作者实测时这个目录是指向已有权重目录的符号链接;首次按上一篇下载时,它就是保存模型文件的普通目录,两种情况都使用相同的描述性名称。

2.2 激活 Python 3.12 虚拟环境

进入项目并激活虚拟环境:

cd /path/to/llm-learning
source .venv_langchain/bin/activate

检查解释器:

python -V
python -c 'import sys; print(sys.executable)'

本机输出:

Python 3.12.11
/Users/bianhn/Documents/git/llm-learning/.venv_langchain/bin/python

这里必须确认解释器位于项目的 .venv_langchain 中。终端可能同时显示 Conda 的 (base) 和 venv 名称,不能只根据提示符判断环境是否正确。

2.3 确认上一篇的服务仍在运行

本文不再重复服务部署。运行客户端之前,应先按照上一篇的步骤启动 MLX-LM Server,然后在当前终端执行:

curl http://127.0.0.1:18080/v1/models | \
  python -c 'import json,sys; data=json.load(sys.stdin); print({"object": data["object"], "data": data["data"]})'

服务返回:

{'object': 'list', 'data': []}

这说明客户端当前可以访问 HTTP 路由。上一篇已经通过 /v1/chat/completions 完成真实生成验证;如果这里连接失败,应先回到上一篇检查服务进程、端口和服务端日志。

3. 使用 OpenAI Python SDK 调用本地模型

3.1 安装 OpenAI 组件

在第二个终端进入相同虚拟环境:

cd /path/to/llm-learning
source .venv_langchain/bin/activate
python -m pip install 'openai==2.7.1'

检查安装结果:

python -c 'import importlib.metadata as m; print(m.version("openai"))'

确认命令能够打印版本号后继续。OpenAI SDK 会使用 httpx 发送 HTTP 请求。

3.2 编写 OpenAI SDK 代码

代码文件为 langchain/p03_openai_compatible_llm/01_test_openai_client.py:

# 这个文件只使用 OpenAI Python SDK 调用已经启动的本地 Qwen3 服务。
# 本地服务通过命令 mlx_lm.server 启动,本文件不负责启动服务。

from openai import OpenAI

# 创建 OpenAI 客户端,并把请求地址改成本地 OpenAI 兼容接口。
client = OpenAI(
    base_url="http://127.0.0.1:18080/v1",
    api_key="not-needed",  # 本地服务不校验 key,但 SDK 要求提供这个参数。
)

# 调用本地服务的 Chat Completions 接口。
response = client.chat.completions.create(
    model="Qwen3-14B-AWQ-4bit-MLX",
    messages=[{"role": "user", "content": "请介绍一下李白"}],
    temperature=0.8,
    max_tokens=128,
    presence_penalty=1.5,
    # 启用思维链模式
    extra_body={"chat_template_kwargs": {"entable_thinking": True}},
)

# 只打印模型返回的文本,便于初学者观察调用结果。
print(response.choices[0].message.content)

这段代码可以分成三步:

  1. 创建 OpenAI 客户端,并直接写出本地服务地址。
  2. 调用 chat.completions.create(),直接写出本次使用的模型名称。
  3. 从响应对象中读取模型回答。

3.3 关键参数

参数 作用
base_url 指向本地 OpenAI 兼容接口,而不是 OpenAI 在线地址
api_key OpenAI SDK 要求提供;当前本地服务不校验该值
model 直接指定本次部署的 Qwen3-14B-AWQ-4bit-MLX
messages 按角色保存系统要求和用户问题
temperature=0.8 减少随机性,便于重复测试
max_tokens=128 最多生成 128 个 token

3.4 运行代码

执行:

python langchain/p03_openai_compatible_llm/01_test_openai_client.py

本次复测关键输出:

当然可以!李白(701年-762年),字太白,号青莲居士,是唐代最著名的诗人之一,被后人尊称为“诗仙”。他与杜甫并称“李杜”,是中国文学史上最伟大的诗人之一。

### 一、生平简介

李白出生于碎叶(今中亚地区),祖籍陇西成纪(今甘肃天水)。他自幼聪慧,博览群书,尤其喜爱诗歌和剑术。他性格豪放不羁,喜欢饮酒,常与文人墨客交往,游历四方,足迹

最后一句没有生成完整,是因为示例将 max_tokens 限制为 128。这不影响接口连通性验证;需要完整回答时,可以适当增大该值。

到这里已经证明 OpenAI Python SDK 可以通过本地兼容接口调用 Qwen3,并不依赖 OpenAI 在线模型。

3.5 response 对象结构

ChatCompletion 响应:重要字段速查

字段 字段 含义 你的示例
response.choices[0].message.content 正文 模型生成的回答 李白生平介绍…
response.choices[0].finish_reason 停止原因 为何结束生成 length = 达到 max_tokens 被截断
response.model 模型名 服务端实际用的模型 Qwen3-14B-AWQ-4bit-MLX
字段 字段 含义
response.usage.completion_tokens 输出 token 数 生成了多少 token(示例:128)
response.usage.prompt_tokens 输入 token 数 提示占多少 token
response.id 请求 ID 日志排查用
response.choices[0].message.role 角色 固定为 assistant
层级 对象 你关心什么
1 ChatCompletion modelusage、元信息
2 choices[0] finish_reason
3 message content(正文)
print(response.choices[0].message.content)   # 正文
print(response.choices[0].finish_reason)     # length / stop
print(response.model)                        # Qwen3-14B-AWQ-4bit-MLX

4. 使用 LangChain 调用本地模型

4.1 安装 LangChain 组件

安装 LangChain 组件:

python -m pip install \
  'langchain==1.0.3' \
  'langgraph==1.0.2' \
  'langchain-core==1.0.2' \
  'langchain-openai==1.0.1' \
  'langchain-community==0.4.1'

也可以直接安装本阶段依赖文件:

python -m pip install \
  -r langchain/p03_openai_compatible_llm/requirements.txt

当前代码直接使用的是 langchain-openai 中的 ChatOpenAI。langgraph 和 langchain-community 在本篇还没有用到,先按照当前环境统一安装,后续文章再分别介绍。

4.2 编写 LangChain 代码

代码文件为 langchain/p03_openai_compatible_llm/02_test_langchain_chatopenai.py:

# 这个文件使用 LangChain 的 ChatOpenAI 调用已经启动的本地 Qwen3 服务。
# 本地服务通过命令 mlx_lm.server 启动,本文件只负责发送问题并打印回答。

from langchain_openai import ChatOpenAI


# 创建 LangChain 聊天模型,并把请求地址改成本地 OpenAI 兼容接口。
llm = ChatOpenAI(
    base_url="http://127.0.0.1:18080/v1",
    api_key="not-needed",  # 本地服务不校验 key,但 ChatOpenAI 要求提供这个参数。
    model="Qwen3-14B-AWQ-4bit-MLX",
    temperature=0,
    max_tokens=128,
)

messages = [
    {"role": "system", "content": "你是一个中文老师"},
    {"role": "user", "content": "请介绍一下李白"}]

# invoke 是 LangChain 聊天模型的统一调用方法,返回 AIMessage。
response = llm.invoke(messages)

# AIMessage.content 保存模型生成的文本。
print("这里打印结果:")
print(response.content)

虽然导入语句是:

from langchain_openai import ChatOpenAI

这仍然是在使用 LangChain。LangChain 1.x 把不同模型厂商的集成拆分成独立包,OpenAI 及其兼容接口由 langchain-openai 提供。

4.3 ChatOpenAI 做了什么

ChatOpenAI 在 OpenAI 兼容接口上增加了 LangChain 的聊天模型抽象。

本文最需要理解的是两个地方:

llm = ChatOpenAI(...)
response = llm.invoke("请介绍一下李白")

invoke() 是 LangChain 模型组件的统一调用方法。它返回的不是 OpenAI SDK 原始响应,而是 LangChain 的 AIMessage 对象,因此使用:

response.content

读取模型回答。下一篇介绍消息类型时,会继续说明 AIMessage、HumanMessage 和 SystemMessage。

4.4 运行代码

执行:

python langchain/p03_openai_compatible_llm/02_test_langchain_chatopenai.py

本次复测关键输出:

这里打印结果:
李白(701年-762年),字太白,号青莲居士,唐代著名诗人,被后人尊称为“诗仙”。他是中国文学史上最杰出的浪漫主义诗人之一,与杜甫并称“李杜”,在中国诗歌史上占有极其重要的地位。

### 一、生平简介

李白出生于碎叶(今吉尔吉斯斯坦境内),祖籍陇西成纪(今甘肃天水)。他自幼聪慧,博览群书,尤其喜爱道家思想,向往自由与自然。他一生游历四方,足迹遍布大江南北

Qwen3 推理发生在独立的 MLX-LM Server 中,LangChain 客户端只通过 HTTP 请求服务,不会在客户端重新加载这份模型权重。

5. OpenAI SDK 与 LangChain 的区别

两段代码访问的是同一个地址、同一个模型,区别主要在客户端抽象层。

对比项 OpenAI Python SDK LangChain ChatOpenAI
主要定位 直接调用 OpenAI 或兼容接口 把接口包装为 LangChain 聊天模型
核心调用 client.chat.completions.create() llm.invoke()
消息格式 role/content 字典 字符串或 LangChain Message
返回结果 OpenAI SDK 响应对象 LangChain AIMessage
适合场景 接口连通性测试、简单聊天 Prompt、Message、Chain、Tool、Agent 等应用开发
代码依赖 openai langchain-openai、langchain-core

如果只是判断接口能不能调用,OpenAI SDK 更接近底层请求,排查问题也更直接。

如果后续准备使用 LangChain 的消息、提示词模板、结构化输出和 Agent,就使用 ChatOpenAI。它的价值不在于这一次调用少写几行代码,而在于后续组件可以使用统一模型接口继续组合。

6. 本次遇到的问题

客户端调用失败时,可以先按下面的顺序区分网络和参数问题:

本地 OpenAI 兼容接口调用失败排查

服务端环境错用、模型生成失败和 502 的排查已经放在上一篇。本篇先确认服务正在运行,再检查客户端参数。

6.1 浏览器返回 Not Found

正确地址是:

http://127.0.0.1:18080/v1/models

如果写成 /v1/modles,服务端会返回 404。浏览器 JSON 插件可能默认折叠 data 数组,需要展开数组才能看到模型信息,也可以直接使用 curl 查看完整响应。

6.2 客户端模型名称必须与服务启动参数一致

两个客户端都直接写出模型名称:

model="Qwen3-14B-AWQ-4bit-MLX"

这个字符串与上一篇启动服务时的 --model Qwen3-14B-AWQ-4bit-MLX 完全一致,也就是项目根目录中的模型目录名称。读者不需要再跳转到函数或配置文件查询实际模型。

不能缩写成 model="Qwen3-14B",因为这个字符串不是本文模型目录的有效路径。MLX-LM Server 会尝试把它当成另一份模型加载,因而可能报模型路径或仓库不存在。

当前 MLX-LM 服务没有在 /v1/models 中列出这份本地模型。是否真正成功不能只看模型列表,仍要以聊天请求返回内容为准。

6.3 回答在最后一句被截断

OpenAI SDK 示例的回答停在“足迹”,原因是:

max_tokens=128

max_tokens 限制的是最多生成多少 token,不是字符数。连通性测试使用较小数值可以缩短等待时间;需要完整长回答时可以改为 256 或更大,同时观察内存和响应时间。

7. 小结

上一篇完成了从直接加载模型到启动 HTTP 服务的转换;这一篇则验证了两种客户端都能通过统一地址调用该服务。

OpenAI Python SDK 更适合验证兼容接口本身,LangChain ChatOpenAI 则把同一个接口包装成 LangChain 聊天模型。两者不是互相替代:ChatOpenAI 的底层仍然按照 OpenAI 兼容格式发送请求,但向上提供了 LangChain 的 invoke() 和 AIMessage。

下一篇将围绕这个 ChatOpenAI 对象介绍系统消息、用户消息、AI 消息和多轮对话,不需要在每个示例中重新加载 7.8GB 模型权重。

8. 最终验证结果

验证项 实测结果
本地服务地址 http://127.0.0.1:18080/v1
/v1/models HTTP 200;返回空 data
OpenAI SDK 成功返回真实 Qwen3 回答
LangChain ChatOpenAI 成功返回 AIMessage
LangChain 返回类型 AIMessage,通过 response.content 读取文本
模型服务环境 独立运行的 MLX-LM Server
最终状态 OpenAI SDK 与 LangChain 均成功调用本地 Qwen3

文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录