一个多 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,
    },
}

参数校验层只关心:

  1. max_tokens 是否在模型目录中声明
  2. 值是不是 int
  3. 是否处在 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 mappingYAML 模板加载使用存在的模板,并显式校验未知模板
max_tokens 没有作用Provider 运行适配转换成 num_predict
128K 声明没有生效Ollama 运行配置核对或设置 num_ctx
模型对象正确但调用失败本地服务或模型运行执行真实 Ollama smoke test

把错误定位到具体层级后,就不需要用“模型有没有下载”解释配置加载错误,也不会用“YAML 已经改对”替代运行参数验证。