一个多 Provider 应用通常希望保留统一配置:
{
"provider": "ollama",
"model": "qwen2.5-7b",
"parameters": {
"temperature": 0.7,
"max_tokens": 4096
}
}应用层把“最多生成多少 token”统一叫作 max_tokens。接入本地 Ollama 后,同一个值又遇到两个名字:
max_completion_tokens
num_predict这三个名字没有处在同一层:
max_tokens是应用对外暴露的统一业务参数max_completion_tokens是内部复用的参数校验模板num_predict是 Ollama 原生运行参数
如果只把 YAML 改到不报错,max_tokens=4096 仍可能没有传给 Ollama;如果只在 ChatOllama 前改名,错误的模板引用又会让应用在启动阶段退出。完整适配需要同时打通配置加载和运行时构造两条链。
第一个错误发生在 Ollama 调用之前
加入模型目录后,服务启动时报错:
TypeError: 'NoneType' object is not a mapping对应配置是:
parameters:
- name: max_tokens
use_template: max_tokens看到 Ollama 模型名时,排查方向容易先落在:
- Ollama 服务是否启动
- 模型是否已经下载
- 本地端口是否可访问
- GPU 或内存是否够用
但这次异常发生在读取 YAML、创建 Provider 目录的阶段,程序还没有构造 ChatOllama,也没有发送 HTTP 请求。
错误路径是:
读取模型 YAML
│
▼
use_template = "max_tokens"
│
▼
参数模板表中查不到这个名字
│
▼
DEFAULT_MODEL_PARAMETER_TEMPLATE.get(...) 返回 None
│
▼
代码执行 **None
│
▼
TypeError: 'NoneType' object is not a mapping**value 要求 value 是 mapping。异常只写了 NoneType,没有告诉调用方是哪一个模型、哪一个模板名出错,所以第一眼看不出它来自 YAML。
参数模板到底是什么
多个模型都会声明 temperature、top_p 和输出长度。如果每份 YAML 都重复类型、默认值、最小值和最大值,规则很容易漂移。参数模板保存可复用的校验元数据:
DEFAULT_MODEL_PARAMETER_TEMPLATE = {
"temperature": {
"label": "温度",
"type": "float",
"default": 1,
"min": 0,
"max": 2,
},
"max_completion_tokens": {
"label": "最大输出 Token",
"type": "int",
"default": None,
"min": 1,
"max": 16384,
},
}模型 YAML 可以复用模板,再覆盖与当前模型有关的字段:
parameters:
- name: max_tokens
use_template: max_completion_tokens
default: 4096
max: 8192加载器做的是字典合并:
template = DEFAULT_MODEL_PARAMETER_TEMPLATE[
"max_completion_tokens"
]
parameter = {
**template,
"name": "max_tokens",
"default": 4096,
"max": 8192,
}合并前:
template = {
"type": "int",
"default": None,
"min": 1,
"max": 16384,
}
model_override = {
"name": "max_tokens",
"default": 4096,
"max": 8192,
}合并后:
parameter = {
"name": "max_tokens",
"type": "int",
"default": 4096,
"min": 1,
"max": 8192,
}模板名没有变成运行时参数名。它只复用了“这是整数、最小值为 1”这类规则;最终暴露给业务的名称仍然由 name: max_tokens 决定。
这也是 max_completion_tokens 在这里最容易误解的地方:它是模板索引,不代表 Ollama 收到了同名字段。
未知模板要在加载时给出完整错误
直接展开查找结果:
parameter = {
**DEFAULT_MODEL_PARAMETER_TEMPLATE.get(
item["use_template"]
),
**item,
}会把配置错误变成缺少上下文的 Python 类型错误。显式判断可以在应用启动时指出根因:
def merge_parameter_template(
model_name: str,
item: dict,
) -> dict:
parameter = item.copy()
template_name = parameter.pop("use_template", None)
if not template_name:
return parameter
template = DEFAULT_MODEL_PARAMETER_TEMPLATE.get(
template_name
)
if template is None:
raise ValueError(
f"model={model_name!r} uses unknown "
f"parameter template={template_name!r}"
)
return {
**template,
**parameter,
}两种错误提供的信息不同:
TypeError: 'NoneType' object is not a mapping
ValueError:
model='qwen2.5-7b' uses unknown
parameter template='max_tokens'第二条错误直接连接了模型文件、字段和值,不需要先猜测 Ollama 网络状态。
修好模板后,参数还没有到达 Ollama
YAML 能成功加载,只说明目录里得到了一个合法参数定义:
{
"name": "max_tokens",
"type": "int",
"default": 4096,
"min": 1,
"max": 8192,
}接下来,统一模型工厂会处理业务配置:
model_config = {
"provider": "ollama",
"model": "qwen2.5-7b",
"parameters": {
"max_tokens": 1024,
},
}参数校验层只关心:
max_tokens是否在模型目录中声明- 值是不是 int
- 是否处在 1 到 8192 之间
校验后仍然保留业务名:
validated_parameters = {
"max_tokens": 1024,
}没有显式传值时,目录默认值会补进去:
validated_parameters = {
"temperature": 1,
"top_p": 1,
"max_tokens": 4096,
}模型目录还可以用 attributes 保存 SDK 构造需要的真实模型名:
model: qwen2.5-7b
attributes:
model: "qwen2.5:7b"这里出现两个模型名:
| 名称 | 用途 |
|---|---|
qwen2.5-7b | 应用模型目录中的稳定 ID |
qwen2.5:7b | 真正传给 Ollama 的本地模型标签 |
工厂合并后的构造参数近似是:
init_kwargs = {
"model": "qwen2.5:7b",
"temperature": 1,
"top_p": 1,
"max_tokens": 1024,
}如果直接执行 ChatOllama(**init_kwargs),max_tokens 不一定会被解释成 Ollama 原生的输出长度参数。模板修复和运行参数适配是两个阶段。
max_tokens 怎样变成 num_predict
Ollama 的 Modelfile 参数表 使用 num_predict 表示一次生成最多预测多少 token。原生 ChatOllama 接法也沿用 Ollama 语义。
Provider 适配类可以在调用父类构造函数前完成映射:
from typing import Any
from langchain_ollama import ChatOllama
class OllamaChatModel(ChatOllama):
def __init__(self, **kwargs: Any) -> None:
max_tokens = kwargs.pop("max_tokens", None)
if (
max_tokens is not None
and "num_predict" not in kwargs
):
kwargs["num_predict"] = max_tokens
super().__init__(**kwargs)以 max_tokens=1024 为例,构造过程是:
进入 OllamaChatModel.__init__
kwargs = {
"model": "qwen2.5:7b",
"temperature": 1,
"top_p": 1,
"max_tokens": 1024
}
│
│ pop("max_tokens")
▼
max_tokens = 1024
kwargs = {
"model": "qwen2.5:7b",
"temperature": 1,
"top_p": 1
}
│
│ kwargs["num_predict"] = 1024
▼
kwargs = {
"model": "qwen2.5:7b",
"temperature": 1,
"top_p": 1,
"num_predict": 1024
}
│
▼
ChatOllama(**kwargs)pop() 很关键:如果只增加 num_predict 而保留 max_tokens,父类仍可能收到一个它不认识或语义不同的字段。
判断 "num_predict" not in kwargs 则保留了显式原生配置的优先级:
{
"max_tokens": 1024,
"num_predict": 512,
}这份输入最终使用 num_predict=512,不会被统一参数覆盖。实际项目也可以选择禁止同时传入两个名字;无论采用哪种规则,都要写清优先级。
三个名字怎样走完同一条链
完整路径不是简单的字段重命名:
业务配置
max_tokens = 1024
│
▼
模型目录
name = max_tokens
use_template = max_completion_tokens
│
▼
模板合并结果
name = max_tokens
type = int
min = 1
max = 8192
│
▼
LanguageModelManager
校验 max_tokens 并保留业务名
│
▼
Ollama Provider 适配类
pop max_tokens
写入 num_predict
│
▼
ChatOllama
num_predict = 1024三层各自回答不同问题:
| 层 | 回答的问题 |
|---|---|
| 业务参数 | 应用用什么稳定名称保存配置 |
| 参数模板 | 这个值是什么类型、默认值和范围 |
| Provider 适配 | 当前 SDK 和运行时实际使用哪个字段 |
把模板当适配器,会得到“YAML 能加载但运行参数没有生效”;把适配器当模板,又会让每个模型文件重复校验规则。
上下文窗口与输出上限不是同一个值
Qwen2.5-7B-Instruct 的官方模型卡列出 131,072 tokens 上下文长度和 8,192 tokens 生成上限:Qwen2.5-7B-Instruct。
模型目录可能据此声明:
context_window: 131072
max_output_tokens: 8192
parameters:
- name: max_tokens
default: 4096
max: 8192这三个值仍然不是同一层:
| 字段 | 含义 |
|---|---|
context_window | 模型目录声明的输入与输出总容量 |
max_output_tokens | 目录声明的模型最大输出能力 |
max_tokens | 应用这一次请求选择的输出上限 |
一次请求还要满足近似关系:
输入 token + 实际输出 token <= 当前运行时上下文
实际输出 token <= 本次 num_predict
本次 num_predict <= 模型与应用允许的输出上限目录中写入 context_window: 131072,不会自动把正在运行的 Ollama 实例改成 128K。Ollama 使用 num_ctx 控制运行上下文;官方 OpenAI compatibility 文档说明,OpenAI API 没有设置上下文大小的对应字段,通过兼容端点调整时需要用 Modelfile 创建带 num_ctx 的模型。
num_predict 控制生成长度,num_ctx 控制可用上下文。两者名字相近,修改对象不同:
num_ctx
决定模型这次能看到多少上下文
num_predict
决定最多继续生成多少 token模型卡中的理论能力、目录中的能力声明和本地运行时配置需要分别核对。
原生 ChatOllama 与 OpenAI-compatible 端点
Ollama 同时提供两条接入方式。
原生 LangChain 集成
from langchain_ollama import ChatOllama
model = ChatOllama(
model="qwen2.5:7b",
num_predict=1024,
)这条路径使用 langchain-ollama 和 Ollama 原生参数。LangChain 的 ChatOllama integration 记录了对应的模型类和能力。
OpenAI-compatible 端点
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:11434/v1/",
api_key="ollama",
)
response = client.chat.completions.create(
model="qwen2.5:7b",
messages=[
{
"role": "user",
"content": "解释一下参数映射",
}
],
max_tokens=1024,
)Ollama 的兼容端点当前接受 max_tokens。这不表示原生 ChatOllama 构造函数也应该收到同名字段:
OpenAI-compatible API
max_tokens
│
▼
Ollama 兼容层负责转换
ChatOllama 原生集成
num_predict
│
▼
应用自己的 Provider 适配层负责转换选哪条接入方式,决定了参数映射发生在哪里。不能从“HTTP 兼容端点支持 max_tokens”推导“所有 Ollama SDK 都原生使用 max_tokens”。
离线验证能证明什么
参数目录和对象构造都发生在真正调用模型之前,因此可以离线验证:
def test_ollama_maps_max_tokens_to_num_predict(
manager,
):
model = manager.create_chat_model({
"provider": "ollama",
"model": "qwen2.5-7b",
"parameters": {
"max_tokens": 1024,
},
})
assert model.model == "qwen2.5:7b"
assert model.num_predict == 1024未知模板也可以直接测试:
def test_unknown_template_contains_context():
with pytest.raises(
ValueError,
match=(
"model='qwen2.5-7b'.*"
"template='max_tokens'"
),
):
merge_parameter_template(
"qwen2.5-7b",
{
"name": "max_tokens",
"use_template": "max_tokens",
},
)离线测试可以证明:
- YAML 模板能够找到
- 模板与模型覆盖值按预期合并
- 默认值和范围校验已经生效
- 工厂选择了 Ollama 适配类
max_tokens最终变成num_predict- 本地模型目录 ID 转成正确的 Ollama 标签
它不能证明:
- 本地 Ollama 服务正在运行
- 模型已经下载
- GPU 或内存足够
- 当前 Ollama 版本接受所有构造参数
- 实际生成一定在 1024 token 前以预期原因停止
后一组需要启动 Ollama 后执行真实 invoke() 或 HTTP smoke test。静态配置通过、模型对象构造成功和端到端生成成功是三种不同的验证结果。
最初两个错误对应的边界也就分开了:
| 现象 | 失败层 | 修复 |
|---|---|---|
NoneType is not a mapping | YAML 模板加载 | 使用存在的模板,并显式校验未知模板 |
max_tokens 没有作用 | Provider 运行适配 | 转换成 num_predict |
| 128K 声明没有生效 | Ollama 运行配置 | 核对或设置 num_ctx |
| 模型对象正确但调用失败 | 本地服务或模型运行 | 执行真实 Ollama smoke test |
把错误定位到具体层级后,就不需要用“模型有没有下载”解释配置加载错误,也不会用“YAML 已经改对”替代运行参数验证。