Статья Open WebUI и vLLM: как исправить ошибку `maximum context length` у Qwen и других моделей

1785689872931.png


При работе с локальными языковыми моделями через Open WebUI, vLLM, Qwen Coder, Llama или Mistral можно столкнуться с такой ошибкой:

Код:
This model's maximum context length is 100000 tokens.
However, you requested 20000 output tokens and your prompt contains
at least 80001 input tokens, for a total of at least 100001 tokens.

Please reduce the length of the input prompt or the number of
requested output tokens.

(parameter=input_tokens, value=80001)

На первый взгляд может показаться, что проблема связана с самой моделью или неправильной настройкой vLLM. На самом деле сообщение означает, что сумма входного контекста и максимально разрешённого размера ответа превысила контекстное окно модели.

В этой статье разберём:
  • что означает ошибка maximum context length;
  • откуда Open WebUI берёт десятки тысяч входных токенов;
  • как влияет параметр max_tokens;
  • почему контекст переполняется из-за истории чата;
  • как на размер контекста влияют загруженные файлы;
  • чем отличаются Full Context и Focused Retrieval;
  • почему простое увеличение контекста не решает проблему;
  • как автоматически обрезать историю через Filter Function;
  • какие параметры использовать для модели с контекстом 100 000 токенов;
  • как проверить защиту через Docker-журналы.

---

## Что означает ошибка maximum context length

Рассмотрим сообщение:

Код:
This model's maximum context length is 100000 tokens.
However, you requested 20000 output tokens and your prompt contains
at least 80001 input tokens.

Здесь указаны три важных значения:

Код:
Максимальное окно модели: 100000 токенов
Запрошенный размер ответа: 20000 токенов
Входной контекст:          80001 токен

vLLM проверяет условие:

Код:
input_tokens + max_tokens <= max_model_len

В нашем случае:

Код:
80001 + 20000 = 100001

Но модель настроена только на:

Код:
100000 токенов

Поэтому запрос превышает лимит всего на один токен:

Код:
100001 > 100000

Этого достаточно, чтобы vLLM отклонил запрос ещё до начала генерации ответа.

---

## Почему максимальный вход равен 80 000 токенов

Если модель запущена с контекстом:

Код:
max_model_len = 100000

а в Open WebUI установлено:

Код:
max_tokens = 20000

то максимальный теоретический размер входного контекста равен:

Код:
100000 - 20000 = 80000 токенов

Получается:

Код:
До 80000 входных токенов + до 20000 токенов ответа = 100000

Первое недопустимое значение входа:

Код:
80001

Именно поэтому в ошибке появляется:

Код:
parameter=input_tokens, value=80001

Это не случайное число. vLLM сообщает минимальный размер входа, при котором запрос уже не помещается в заданное окно.

---

# Что входит во входной контекст

Пользователь может видеть в поле ввода всего одну короткую фразу, но Open WebUI отправляет модели значительно больше данных.

Во входной контекст могут входить:
  • системный промпт;
  • текущий вопрос пользователя;
  • предыдущие вопросы;
  • предыдущие ответы модели;
  • исходный код;
  • журналы работы программ;
  • содержимое загруженных файлов;
  • результаты поиска по базе знаний;
  • RAG-фрагменты;
  • результаты веб-поиска;
  • схемы инструментов;
  • вызовы функций;
  • ответы инструментов;
  • сохранённая память пользователя;
  • дополнительные инструкции модели;
  • служебные маркеры ролей;
  • шаблон чата Qwen;
  • данные, добавленные расширениями и фильтрами.
Поэтому реальный запрос может выглядеть так:

Код:
system
user-1
assistant-1
user-2
assistant-2
user-3
assistant-3
user-4

Даже если последний вопрос занимает всего 50 токенов, предыдущие ответы могут содержать десятки тысяч токенов кода, журналов и документации.

---

# Почему история чата быстро переполняет контекст

Особенно быстро контекст растёт при работе с программированием.

Например:

1. Пользователь просит создать REST API.
2. Модель генерирует 5 000 токенов кода.
3. Пользователь просит добавить PostgreSQL.
4. Модель генерирует ещё 7 000 токенов.
5. Пользователь просит добавить Redis.
6. Модель создаёт ещё 6 000 токенов.
7. Пользователь просит добавить Docker Compose.
8. Модель генерирует ещё несколько тысяч токенов.

Через несколько сообщений история может выглядеть так:

Код:
Системный промпт:        3 000 токенов
Первый вопрос и ответ:  10 000 токенов
Второй вопрос и ответ:  14 000 токенов
Третий вопрос и ответ:  12 000 токенов
Четвёртый вопрос:        2 000 токенов
---------------------------------------
Всего:                  41 000 токенов

Если продолжить работу, контекст постепенно приблизится к 80 000 токенов и vLLM вернёт ошибку.

---

# Почему увеличение max_model_len не решает проблему окончательно

Можно увеличить:

Код:
--max-model-len 100000

например до:

Код:
--max-model-len 120000

Но это только отложит ошибку.

Если Open WebUI продолжит передавать всю историю, через некоторое время будет переполнено и новое окно.

Кроме того, большой контекст:
  • увеличивает использование KV-кэша;
  • повышает потребление видеопамяти;
  • снижает возможное количество параллельных запросов;
  • может уменьшать скорость обработки;
  • иногда ухудшает качество внимания модели к важным фрагментам;
  • создаёт дополнительную нагрузку на сервер.
Правильное решение состоит не только в увеличении окна, но и в автоматическом контроле истории.

---

# Быстрые способы исправить ошибку

## 1. Создать новый чат

Самый простой вариант — начать новый диалог.

В новом чате отсутствует накопленная история, поэтому тот же вопрос может успешно пройти.

Это хороший диагностический тест:

1. Создайте новый чат.
2. Отправьте тот же запрос.
3. Посмотрите, появилась ли ошибка.

Если в новом чате ошибка исчезла, значит проблема находилась именно в истории.

Недостаток метода: модель перестаёт видеть предыдущую работу.

---

## 2. Уменьшить max_tokens

При:

Код:
max_model_len = 100000
max_tokens = 20000

под вход остаётся:

Код:
80000 токенов

Если уменьшить размер максимального ответа:

Код:
max_tokens = 10000

для входа останется:

Код:
100000 - 10000 = 90000 токенов

Запрос с 80 001 входным токеном тогда пройдёт.

Однако это временное решение. История продолжит расти и позднее снова переполнит окно.

Для большинства ответов по программированию часто достаточно:

Код:
max_tokens = 8000–12000

Значение:

Код:
max_tokens = 20000

имеет смысл для:
  • длинных статей;
  • генерации больших файлов;
  • подробного анализа журналов;
  • создания нескольких модулей проекта;
  • генерации больших объёмов исходного кода.

---

## 3. Удалить старые сообщения

Можно вручную удалить старые сообщения или начать новую ветку.

Но для публичного сервиса это неудобно. Пользователь не должен самостоятельно считать токены и контролировать размер контекста.

Лучше настроить автоматическую обрезку.

---

# Как загруженные файлы переполняют контекст

Файлы могут занимать больше токенов, чем вся остальная история чата.

Например, текстовый файл содержит:

Код:
85000 символов

При приблизительном соотношении:

Код:
2.5 символа ≈ 1 токен

получаем:

Код:
85000 / 2.5 ≈ 34000 токенов

Если такой файл три раза попадёт в запрос:

Код:
34000 × 3 = 102000 токенов

Контекст переполнится ещё до учёта:
  • системного промпта;
  • вопросов пользователя;
  • ответов модели;
  • максимального ответа.

---

## Full Context и Focused Retrieval

В Open WebUI документы могут обрабатываться по-разному.

### Full Context

В режиме Full Context содержимое файла вставляется в контекст практически целиком.

Преимущества:
  • модель видит весь документ;
  • хорошо подходит для небольших файлов;
  • можно анализировать структуру целиком.
Недостатки:
  • быстро расходуются токены;
  • несколько файлов могут переполнить контекст;
  • один большой PDF или TXT может занимать десятки тысяч токенов.

### Focused Retrieval

В режиме Focused Retrieval документ разбивается на фрагменты, после чего Open WebUI выбирает только наиболее релевантные части.

Например:

Код:
Chunk Size:     1000 токенов
Chunk Overlap:   100 токенов
Top K:             4

В запрос попадёт приблизительно:

Код:
4 × 1000 = 4000 токенов

а не весь документ.

Для больших книг, PDF и технической документации лучше использовать:

Код:
Focused Retrieval

Рекомендуемые начальные параметры:

Код:
Chunk Size:     700–1200
Chunk Overlap:   70–150
Top K:            3–5

---

# Надёжное решение: Filter Function в Open WebUI

Open WebUI позволяет создать фильтр, который изменяет запрос до отправки модели.

Для этого используется метод:

Python:
async def inlet(...)

Фильтр может:
  • сохранить системный промпт;
  • оставить только последние сообщения;
  • приблизительно подсчитать токены;
  • удалить старые диалоговые ходы;
  • сохранить текущий вопрос;
  • отклонить слишком большой запрос;
  • вывести информацию об обрезке в Docker-журнал.

---

# Как должна работать защита

Рекомендуемый алгоритм:

Код:
1. Сохранить начальные system/developer-сообщения.
2. Оставить максимум несколько последних сообщений.
3. Удалить осиротевшие assistant/tool-сообщения.
4. Оценить количество входных токенов.
5. Если бюджет превышен — удалить самый старый полный ход.
6. Снова пересчитать токены.
7. Повторять, пока контекст не станет допустимым.
8. Если текущий вопрос сам слишком большой — отклонить запрос.

Важно удалять не случайное отдельное сообщение, а полный диалоговый ход:

Код:
user
assistant

При использовании инструментов ход может выглядеть так:

Код:
user
assistant с tool_call
tool
assistant

Если удалить только часть такой последовательности, модель может получить результат инструмента без соответствующего вызова.

---

# Готовая функция ограничения истории

Ниже приведён пример Filter Function для Open WebUI.

Функция:
  • сохраняет начальный системный промпт;
  • оставляет не более шести последних сообщений;
  • ограничивает оценочный размер входа;
  • удаляет самые старые полные ходы;
  • сохраняет текущий запрос;
  • отклоняет запрос, который невозможно уместить в бюджет;
  • выводит диагностику в журнал контейнера.

Python:
"""
title: Zer0Kernel Context Window
author: zer0kernel
version: 1.2.0
description: Ограничивает историю чата и оценочный размер входного контекста.
"""

import hashlib
import json
import math
import re
from typing import Any, Optional

from pydantic import BaseModel, Field


class Filter:
    CONTROL_ROLES = {"system", "developer"}
    IMAGE_TYPES = {"image", "image_url", "input_image"}
    TEXT_TYPES = {"text", "input_text", "output_text"}

    MARKER_RE = re.compile(
        r"\b(?:(?:CTX|TEST|TOKEN)[-_][A-Z0-9][A-Z0-9_-]{0,63}"
        r"|M[0-9]{1,3}(?:[-_][A-Z0-9_-]{1,64})?)\b",
        re.IGNORECASE,
    )

    class Valves(BaseModel):
        priority: int = Field(
            default=1000,
            description=(
                "Приоритет выполнения фильтра. "
                "Меньшие значения выполняются раньше."
            ),
        )

        max_messages: int = Field(
            default=6,
            ge=1,
            description=(
                "Максимальное количество последних сообщений диалога. "
                "Начальный системный промпт в этот лимит не входит."
            ),
        )

        max_input_tokens: int = Field(
            default=30000,
            ge=1,
            description=(
                "Максимальный оценочный размер входного контекста."
            ),
        )

        chars_per_token: float = Field(
            default=2.5,
            gt=0.0,
            description=(
                "Приблизительное количество символов на один токен."
            ),
        )

        message_overhead_tokens: int = Field(
            default=16,
            ge=0,
            description=(
                "Служебный расход токенов на каждое сообщение."
            ),
        )

        image_token_reserve: int = Field(
            default=1500,
            ge=0,
            description=(
                "Резерв токенов на одно изображение."
            ),
        )

        keep_current_user_message: bool = Field(
            default=True,
            description=(
                "Сохранять текущий вопрос пользователя."
            ),
        )

        reject_oversized_request: bool = Field(
            default=True,
            description=(
                "Отклонить запрос, если даже текущий ход "
                "не помещается в заданный бюджет."
            ),
        )

        debug: bool = Field(
            default=True,
            description=(
                "Выводить диагностику в журнал Open WebUI."
            ),
        )

        debug_preview_chars: int = Field(
            default=0,
            ge=0,
            le=300,
            description=(
                "Количество символов сообщения для вывода в журнал. "
                "Значение 0 скрывает содержимое сообщений."
            ),
        )

    def __init__(self):
        self.valves = self.Valves()

    def _json_text(self, value: Any) -> str:
        try:
            return json.dumps(
                value,
                ensure_ascii=False,
                separators=(",", ":"),
                sort_keys=True,
                default=str,
            )
        except Exception:
            return str(value)

    def _content_to_text(self, content: Any) -> str:
        if content is None:
            return ""

        if isinstance(content, str):
            return content

        if isinstance(content, list):
            parts: list[str] = []

            for item in content:
                if isinstance(item, str):
                    parts.append(item)
                    continue

                if isinstance(item, dict):
                    item_type = str(item.get("type", ""))

                    if item_type in self.TEXT_TYPES:
                        parts.append(
                            str(
                                item.get(
                                    "text",
                                    item.get(
                                        "content",
                                        item.get("value", ""),
                                    ),
                                )
                            )
                        )
                    elif item_type in self.IMAGE_TYPES:
                        parts.append("[IMAGE]")
                    else:
                        parts.append(self._json_text(item))

                    continue

                parts.append(str(item))

            return "\n".join(parts)

        if isinstance(content, dict):
            return self._json_text(content)

        return str(content)

    def _estimate_text_tokens(self, text: str) -> int:
        if not text:
            return 0

        chars_per_token = max(
            float(self.valves.chars_per_token),
            0.1,
        )

        return math.ceil(
            len(text) / chars_per_token
        )

    def _estimate_content_tokens(self, content: Any) -> int:
        if content is None:
            return 0

        if isinstance(content, str):
            return self._estimate_text_tokens(content)

        if isinstance(content, list):
            total = 0

            for item in content:
                if isinstance(item, dict):
                    item_type = str(item.get("type", ""))

                    if item_type in self.IMAGE_TYPES:
                        total += int(
                            self.valves.image_token_reserve
                        )
                        continue

                    if item_type in self.TEXT_TYPES:
                        text_value = item.get(
                            "text",
                            item.get(
                                "content",
                                item.get("value", ""),
                            ),
                        )

                        total += self._estimate_content_tokens(
                            text_value
                        )
                        continue

                total += self._estimate_text_tokens(
                    self._json_text(item)
                )

            return total

        if isinstance(content, dict):
            return self._estimate_text_tokens(
                self._json_text(content)
            )

        return self._estimate_text_tokens(
            str(content)
        )

    def _estimate_message_tokens(
        self,
        message: dict[str, Any],
    ) -> int:
        estimated = int(
            self.valves.message_overhead_tokens
        )

        estimated += self._estimate_content_tokens(
            message.get("content")
        )

        for field_name in (
            "name",
            "tool_call_id",
            "tool_calls",
            "function_call",
            "reasoning_content",
            "images",
            "audio",
            "files",
            "sources",
        ):
            value = message.get(field_name)

            if value not in (
                None,
                "",
                [],
                {},
            ):
                estimated += self._estimate_text_tokens(
                    self._json_text(value)
                )

        return estimated

    def _estimate_messages_tokens(
        self,
        messages: list[dict[str, Any]],
    ) -> int:
        return sum(
            self._estimate_message_tokens(message)
            for message in messages
        )

    def _estimate_fixed_body_tokens(
        self,
        body: dict[str, Any],
    ) -> int:
        total = 0

        for field_name in (
            "tools",
            "functions",
        ):
            value = body.get(field_name)

            if value not in (
                None,
                "",
                [],
                {},
            ):
                total += self._estimate_text_tokens(
                    self._json_text(value)
                )

        return total

    def _split_leading_control_messages(
        self,
        messages: list[dict[str, Any]],
    ) -> tuple[
        list[dict[str, Any]],
        list[dict[str, Any]],
    ]:
        control: list[dict[str, Any]] = []
        conversation: list[dict[str, Any]] = []

        in_control_prefix = True

        for message in messages:
            role = message.get("role")

            if (
                in_control_prefix
                and role in self.CONTROL_ROLES
            ):
                control.append(message)
                continue

            in_control_prefix = False
            conversation.append(message)

        return control, conversation

    def _normalize_window_start(
        self,
        messages: list[dict[str, Any]],
    ) -> list[dict[str, Any]]:
        result = list(messages)

        if not result:
            return result

        first_user_index = next(
            (
                index
                for index, message in enumerate(result)
                if message.get("role") == "user"
            ),
            None,
        )

        if first_user_index is not None:
            return result[first_user_index:]

        while (
            result
            and result[0].get("role")
            in {"tool", "function"}
        ):
            result.pop(0)

        return result

    def _drop_oldest_complete_turn(
        self,
        messages: list[dict[str, Any]],
    ) -> tuple[list[dict[str, Any]], bool]:
        if not messages:
            return [], False

        next_user_index = next(
            (
                index
                for index in range(
                    1,
                    len(messages),
                )
                if messages[index].get("role")
                == "user"
            ),
            None,
        )

        if next_user_index is None:
            return list(messages), False

        return messages[next_user_index:], True

    def _debug_descriptor(
        self,
        message: dict[str, Any],
        index: int,
    ) -> str:
        role = str(message.get("role", "?"))

        text = self._content_to_text(
            message.get("content")
        )

        digest = hashlib.sha256(
            text.encode(
                "utf-8",
                errors="replace",
            )
        ).hexdigest()[:10]

        markers = sorted(
            set(
                self.MARKER_RE.findall(text)
            )
        )

        marker_text = (
            "|".join(markers[:12])
            if markers
            else "-"
        )

        token_estimate = (
            self._estimate_message_tokens(
                message
            )
        )

        descriptor = (
            f"{index}:{role}:"
            f"chars={len(text)}:"
            f"tokens~={token_estimate}:"
            f"sha={digest}:"
            f"markers={marker_text}"
        )

        preview_chars = int(
            self.valves.debug_preview_chars
        )

        if preview_chars > 0:
            preview = " ".join(
                text[:preview_chars].split()
            )

            descriptor += (
                f":preview={preview!r}"
            )

        return descriptor

    async def inlet(
        self,
        body: dict,
        __user__: Optional[dict] = None,
    ) -> dict:
        raw_messages = body.get("messages")

        if (
            not isinstance(raw_messages, list)
            or not raw_messages
        ):
            return body

        messages = [
            message
            for message in raw_messages
            if isinstance(message, dict)
        ]

        if not messages:
            return body

        (
            control_messages,
            conversation,
        ) = self._split_leading_control_messages(
            messages
        )

        original_conversation = list(
            conversation
        )

        original_token_estimate = (
            self._estimate_messages_tokens(
                messages
            )
        )

        fixed_body_tokens = (
            self._estimate_fixed_body_tokens(
                body
            )
        )

        max_messages = max(
            1,
            int(self.valves.max_messages),
        )

        message_limited = conversation[
            -max_messages:
        ]

        conversation = (
            self._normalize_window_start(
                message_limited
            )
        )

        dropped_by_message_limit = (
            len(original_conversation)
            - len(conversation)
        )

        dropped_turns_by_token_limit = 0

        token_budget = max(
            1,
            int(
                self.valves.max_input_tokens
            ),
        )

        while conversation:
            estimated_total = (
                self._estimate_messages_tokens(
                    control_messages
                    + conversation
                )
                + fixed_body_tokens
            )

            if estimated_total <= token_budget:
                break

            reduced, changed = (
                self._drop_oldest_complete_turn(
                    conversation
                )
            )

            if changed:
                conversation = (
                    self._normalize_window_start(
                        reduced
                    )
                )

                dropped_turns_by_token_limit += 1
                continue

            if (
                self.valves
                .keep_current_user_message
            ):
                break

            conversation = []
            break

        final_messages = (
            control_messages
            + conversation
        )

        final_token_estimate = (
            self._estimate_messages_tokens(
                final_messages
            )
            + fixed_body_tokens
        )

        over_budget = (
            final_token_estimate
            > token_budget
        )

        if (
            over_budget
            and self.valves
            .reject_oversized_request
        ):
            raise ValueError(
                "Zer0Kernel Context Window: "
                "the request is estimated at "
                f"{final_token_estimate} "
                "input tokens, exceeding "
                f"max_input_tokens="
                f"{token_budget}. "
                "Reduce the current message "
                "or file context."
            )

        body["messages"] = final_messages

        if self.valves.debug:
            kept_details = ";".join(
                self._debug_descriptor(
                    message,
                    index,
                )
                for index, message
                in enumerate(final_messages)
            )

            print(
                "[ZK_CONTEXT] "
                f"raw_messages="
                f"{len(raw_messages)}, "
                f"valid_messages="
                f"{len(messages)}, "
                f"leading_control="
                f"{len(control_messages)}, "
                f"conversation="
                f"{len(original_conversation)}"
                f"->{len(conversation)}, "
                f"dropped_by_message_limit="
                f"{dropped_by_message_limit}, "
                f"dropped_turns_by_token_limit="
                f"{dropped_turns_by_token_limit}, "
                f"tokens~="
                f"{original_token_estimate + fixed_body_tokens}"
                f"->{final_token_estimate}, "
                f"budget={token_budget}, "
                f"over_budget="
                f"{str(over_budget).lower()}, "
                f"kept=[{kept_details}]",
                flush=True,
            )

        return body

---

# Как установить Filter Function

Откройте Open WebUI:

Код:
Рабочая область → Функции

Создайте новую функцию и вставьте код.

После сохранения обязательно:

1. Включите функцию.
2. Включите её глобально либо назначьте конкретной модели.
3. Откройте параметры Valves.
4. Установите необходимые значения.

Сам факт появления в журнале строки:

Код:
Loaded module: function_zer0kernel_context_window

означает только успешную загрузку Python-модуля.

Это ещё не означает, что фильтр применяется к запросам.

Если функция должна работать со всеми моделями, включите:

Код:
Active: true
Global: true

Обратите внимание на Global: true

---

# Рекомендуемые настройки

Для модели:

Код:
max_model_len = 100000
max_tokens = 20000

можно начать с таких параметров:

Код:
Priority:                    1000
Max Messages:                6
Max Input Tokens:            30000
Chars Per Token:             2.5
Message Overhead Tokens:     16
Image Token Reserve:         1500
Keep Current User Message:   true
Reject Oversized Request:    true
Debug:                       true
Debug Preview Chars:         0

---

# Как работает Max Messages = 6

Системный промпт в этот лимит не входит.

Допустим, история выглядит так:

Код:
system
user-1
assistant-1
user-2
assistant-2
user-3
assistant-3
user-4

Фильтр берёт последние шесть обычных сообщений:

Код:
assistant-1
user-2
assistant-2
user-3
assistant-3
user-4

Но первое сообщение является ответом без соответствующего вопроса. Поэтому оно удаляется.

Модель получает:

Код:
system
user-2
assistant-2
user-3
assistant-3
user-4

То есть:
  • системный промпт;
  • две предыдущие пары;
  • текущий вопрос.
Значение 6 — это максимальное количество сообщений, а не обязательное.

Если токеновый бюджет превышен, сообщений может остаться меньше.

---

# Как работает Max Input Tokens = 30000

После ограничения количества сообщений функция оценивает размер контекста.

Например:

Код:
system                   3000 токенов
user-2                   7000 токенов
assistant-2              5000 токенов
user-3                   8000 токенов
assistant-3              4000 токенов
текущий user             6000 токенов
------------------------------------
всего                   33000 токенов

Установлен лимит:

Код:
Max Input Tokens = 30000

Контекст превышает бюджет:

Код:
33000 > 30000

Фильтр удаляет самый старый полный ход:

Код:
user-2
assistant-2

После удаления остаётся:

Код:
system                   3000 токенов
user-3                   8000 токенов
assistant-3              4000 токенов
текущий user             6000 токенов
------------------------------------
всего                   21000 токенов

Теперь контекст помещается в установленный бюджет.

---

# Почему нужны два ограничения

Одного Max Messages недостаточно.

Шесть коротких сообщений могут занимать:

Код:
1000 токенов

Но шесть сообщений с исходным кодом и журналами могут занимать:

Код:
50000 токенов

Поэтому используются одновременно:

Код:
Max Messages = 6
Max Input Tokens = 30000

Первый параметр ограничивает количество сообщений.

Второй параметр ограничивает их приблизительный общий размер.

---

# Что происходит с одним огромным запросом

Допустим:

Код:
system prompt       3000 токенов
текущий вопрос     32000 токенов
--------------------------------
всего              35000 токенов

Старой истории нет. Удалять больше нечего.

При настройках:

Код:
Keep Current User Message = true
Reject Oversized Request = true

фильтр отклонит запрос.

Пользователь получит ошибку о превышении внутреннего бюджета, а запрос не будет передан в vLLM.

Если отключить:

Код:
Reject Oversized Request = false

фильтр сохранит текущий запрос и передаст его дальше, несмотря на превышение.

В таком случае vLLM снова может вернуть:

Код:
This model's maximum context length is...

Для строгой защиты рекомендуется:

Код:
Reject Oversized Request = true

---

# Как приблизительно считаются токены

Функция не использует точный токенизатор Qwen.

Она применяет формулу:

Код:
tokens ≈ characters / chars_per_token

При:

Код:
Chars Per Token = 2.5

получаем:

Код:
25000 символов ≈ 10000 токенов
50000 символов ≈ 20000 токенов
75000 символов ≈ 30000 токенов

Чем меньше значение Chars Per Token, тем раньше срабатывает ограничение.

Более строгая настройка:

Код:
Chars Per Token = 2.2

Менее строгая:

Код:
Chars Per Token = 3.0

Для смеси русского текста, исходного кода и JSON разумный диапазон:

Код:
2.3–2.5

---

# Учитывается ли системный промпт

Системный промпт не входит в:

Код:
Max Messages

Но входит в:

Код:
Max Input Tokens

Например:

Код:
Max Input Tokens = 30000
Системный промпт = 5000

На историю и текущий вопрос остаётся примерно:

Код:
30000 - 5000 = 25000 токенов

Системный промпт фильтр не удаляет.

Если сам системный промпт превышает бюджет, запрос будет отклонён даже при пустой истории.

---

# Работа с файлами

Важно понимать, что содержимое файлов может обрабатываться Open WebUI отдельно от обычной истории сообщений.

Если файл используется в режиме Full Context, он может вставляться в запрос целиком.

Поэтому для больших файлов рекомендуется:

Код:
Focused Retrieval

При повторной загрузке нескольких файлов следует убедиться, что Open WebUI не передаёт все предыдущие источники в каждом новом запросе.

Практичная стратегия:

Код:
1. В текущем запросе использовать все специально прикреплённые файлы.
2. В следующем запросе без вложений не передавать прошлые файлы повторно.
3. Для больших документов использовать Focused Retrieval.
4. Ограничить Top K.
5. Не использовать Full Context для книг и крупных PDF без необходимости.

Если модель уже пересказала содержимое файла в предыдущем ответе, этот пересказ становится обычной частью истории и может сохраняться до срабатывания Max Messages или Max Input Tokens.

---

# Почему Max Input Tokens = 30000 безопасен

При:

Код:
max_model_len = 100000
max_tokens = 20000

максимальный вход vLLM равен:

Код:
80000 токенов

Фильтр ограничивает контролируемую часть входа до:

Код:
30000 токенов

Остаётся запас:

Код:
80000 - 30000 = 50000 токенов

Он может использоваться для:
  • погрешности приблизительного подсчёта;
  • шаблона чата;
  • содержимого файлов;
  • RAG-фрагментов;
  • схем инструментов;
  • внутренних инструкций;
  • данных других расширений;
  • служебных токенов модели.

Значение 30000 является консервативным.

Для обычного чата без больших файлов можно использовать:

Код:
40000–50000

Но сначала лучше провести тестирование.

---

# Проверка через Docker-журнал

Для просмотра работы фильтра выполните:

Bash:
docker logs -f --tail=100 zer0kernel-open-webui 2>&1 |
grep --line-buffered ZK_CONTEXT

Вместо zer0kernel-open-webui вставьте своё имя контейнера.
Либо команду...


Пример строки:

Код:
[ZK_CONTEXT]
raw_messages=10,
valid_messages=10,
leading_control=1,
conversation=9->3,
dropped_by_message_limit=4,
dropped_turns_by_token_limit=1,
tokens~=47000->18200,
budget=30000,
over_budget=false

Расшифровка:

Код:
conversation=9->3

Было девять обычных сообщений, модели отправлено три.

Код:
dropped_by_message_limit=4

Четыре сообщения были удалены из-за Max Messages.

Код:
dropped_turns_by_token_limit=1

После этого удалён ещё один полный диалоговый ход из-за токенового бюджета.

Код:
tokens~=47000->18200

До обработки было приблизительно 47 000 токенов, после обработки осталось около 18 200.

Код:
budget=30000

Установленный бюджет равен 30 000 токенов.

Код:
over_budget=false

Итоговый контекст помещается в заданный лимит.

---

# Тест обрезки истории

Создайте новый чат и последовательно отправьте несколько крупных запросов.

Первый запрос:

Код:
Напиши подробный обзор архитектуры FastAPI-сервиса объёмом около 3000 слов.

Второй:

Код:
Добавь PostgreSQL, Redis, очередь задач и распределённые блокировки.
Сохрани подробность ответа.

Третий:

Код:
Добавь структуру проекта и полные примеры основных файлов.

Четвёртый:

Код:
Добавь Docker Compose, Nginx и систему мониторинга.

После нескольких запросов журнал должен показать обрезку:

Код:
conversation=9->5

или:

Код:
dropped_turns_by_token_limit=1

При этом запросы должны продолжать выполняться без ошибки maximum context length.

---

# Тест с маркерами

Для проверки количества сообщений можно использовать уникальные строки.

Отправьте по очереди:

Код:
Запомни строку CTX_M1_111. Ответь только ACK.

Код:
Запомни строку CTX_M2_222. Ответь только ACK.

Продолжите:

Код:
CTX_M3_333
CTX_M4_444
CTX_M5_555
CTX_M6_666
CTX_M7_777

После этого отправьте:

Код:
Выведи только строки с префиксом CTX_, которые дословно присутствуют
в переданном тебе контексте. Не угадывай удалённые строки.

При Max Messages = 6 модель должна видеть только несколько последних маркеров.

Окончательный результат лучше проверять по журналу ZK_CONTEXT, а не только по ответу модели.

---

# Частые ошибки настройки

## Функция создана, но не запускается

Проверьте:

Код:
Рабочая область → Функции

Функция должна быть:

Код:
Active = true

А также либо:

Код:
Global = true

либо назначена конкретной модели.

---

## В журнале нет ZK_CONTEXT

Если отсутствуют строки:

Код:
[ZK_CONTEXT]

фильтр не применяется к запросу.

Возможные причины:
  • функция выключена;
  • глобальный режим не включён;
  • функция не назначена модели;
  • запрос проходит через другую модель;
  • используется другой Open WebUI-контейнер;
  • код не был сохранён.

---

## Установлен Priority = 0

Фильтры с меньшим приоритетом выполняются раньше.

Если ограничитель должен работать после других пользовательских фильтров, установите:

Код:
Priority = 1000

Это не гарантирует, что он будет последним внутренним обработчиком Open WebUI, но позволяет запускать его после большинства других Filter Functions.

---

## Отключён Reject Oversized Request

При:

Код:
Keep Current User Message = true
Reject Oversized Request = false

один огромный текущий вопрос может пройти дальше, несмотря на превышение внутреннего бюджета.

Для строгой защиты:

Код:
Reject Oversized Request = true

---

## Используется Full Context для крупных файлов

Если файл занимает десятки тысяч токенов, переключите его в:

Код:
Focused Retrieval

И уменьшите:

Код:
Top K

Например:

Код:
Top K = 3

---

## Слишком большой max_tokens

Значение max_tokens=20000 резервирует место под очень длинный ответ, даже если модель фактически ответит несколькими предложениями.

Для обычного программирования можно использовать:

Код:
8000–12000

Для крупных статей и больших файлов кода:

Код:
12000–20000

---

# Оптимальная конфигурация для Qwen Coder

Для модели с окном:

Код:
max_model_len = 100000

и максимальным ответом:

Код:
max_tokens = 20000

безопасная начальная конфигурация:

Код:
Priority:                    1000
Max Messages:                6
Max Input Tokens:            30000
Chars Per Token:             2.5
Message Overhead Tokens:     16
Keep Current User Message:   true
Reject Oversized Request:    true
Debug:                       true

Для менее строгого режима:

Код:
Max Input Tokens = 40000–50000

Но перед увеличением следует проверить:
  • длинные ответы;
  • большие исходные файлы;
  • несколько загруженных документов;
  • RAG;
  • веб-поиск;
  • инструменты;
  • размер системного промпта.

---

# FAQ

## Почему ошибка показывает именно 80 001 входной токен?

Потому что:

Код:
100000 - 20000 = 80000

Вход до 80 000 токенов допустим.

Первое недопустимое значение:

Код:
80001

---

## Удаляет ли фильтр сообщения из интерфейса?

Нет.

Сообщения остаются в чате и базе Open WebUI.

Фильтр изменяет только временный запрос, который отправляется модели.

---

## Будет ли модель помнить старые сообщения?

Только пока они находятся в передаваемом контексте.

Если старый ход был удалён фильтром, модель больше его не видит.

Однако часть информации может сохраниться в более новых сообщениях, если пользователь или модель повторили её.

---

## Что важнее: Max Messages или Max Input Tokens?

Нужны оба параметра.

Max Messages защищает от слишком большого количества сообщений.

Max Input Tokens защищает от нескольких огромных сообщений.

Шесть коротких сообщений и шесть больших ответов с кодом — это совершенно разные объёмы контекста.

---

## Можно ли оставить только последний вопрос?

Можно, но качество длительной работы снизится.

Модель начнёт забывать:
  • архитектуру проекта;
  • имена функций;
  • названия классов;
  • используемые библиотеки;
  • предыдущие исправления;
  • требования пользователя.

Практичный компромисс:

Код:
системный промпт
+
последние 4–6 сообщений
+
токеновый бюджет

---

## Почему не использовать только Context Compaction?

Автоматическое сжатие может зависеть от:
  • версии Open WebUI;
  • режима чата;
  • конкретной модели;
  • настроек;
  • доступности вспомогательной модели;
  • работы приватного чата.
Filter Function работает предсказуемее:

Код:
контекст помещается
или старые ходы удаляются
или слишком большой запрос отклоняется

---

# Заключение

Ошибка:

Код:
This model's maximum context length is...

не означает, что vLLM или Qwen неисправны.

Она означает, что сумма:

Код:
история
+ системный промпт
+ текущий вопрос
+ файлы
+ RAG
+ инструменты
+ максимальный ответ

перестала помещаться в заданное контекстное окно.

Надёжная защита состоит из нескольких уровней:

Код:
1. Разумный max_tokens.
2. Ограничение количества сообщений.
3. Ограничение входного токенового бюджета.
4. Удаление старых полных диалоговых ходов.
5. Focused Retrieval для больших документов.
6. Контроль повторного использования файлов.
7. Отклонение одиночных слишком больших запросов.
8. Проверка работы через Docker-журнал.

Для модели с окном 100 000 токенов и max_tokens=20000 можно начать с:

Код:
Max Messages = 6
Max Input Tokens = 30000
Chars Per Token = 2.5
Priority = 1000
Reject Oversized Request = true

Такая конфигурация не просто скрывает ошибку, а устраняет основную причину — бесконтрольное накопление контекста в Open WebUI.
 
Последнее редактирование:
Назад
Верх Низ