Средняя модель — как стажер: без контекста — беда
Вы купили карту за полмиллиона, поставили Qwen3.6 с 14B параметров, скормили ей RAG из 50 документов, а она на вопрос "Какая дата основания компании?" выдала "Я не могу этого знать, обратитесь к документации". Знакомо? Я такое видел десятки раз. Проблема не в модели — Qwen3.6 отлично справляется с задачами, если контекст разложен по полочкам. Беда в том, как этот контекст собран.
Маленькие и средние локальные модели (7-14B) не умеют просеивать мусор. Они верят каждому документу, который вы запихали в промпт. Если среди пяти релевантных статей затесалась одна с устаревшими данными — модель выдаст старые цифры. Если контекст перегружен дубликатами — модель запутается. Если порядок секций неправильный — она проигнорирует инструкцию. В этой статье я разберу три ключевых приема, которые превратят вашу среднюю модель в надежного исполнителя.
Как я писал в статье "Почему плохой ответ модели — это не проблема модели", корень 90% плохих ответов — не в модели, а в том, как с ней разговаривают. Контекстная инженерия — это и есть правильный разговор.
Если вы думаете: "Моя модель слабая, надо купить больше GPU" — остановитесь. Сначала попробуйте техники из этой статьи. Часто они дают прирост качества, сравнимый с переходом на модель вдвое большего размера.
Как не надо: контекст одной кучей
Самый частый грех, который я встречаю в реальных проектах: разработчик собирает все документы из RAG, склеивает их в один текстовый блок и отправляет модели. Выглядит это так:
Системное сообщение: Ты — ассистент. Ответь на вопрос, используя контекст.
Контекст:
[документ 1]
[документ 2]
...
[документ 20]
Вопрос: ...
Результат: модель видит 20 килобайт текста, выделяет случайные фрагменты, находит противоречия и выдает усредненную кашу. Особенно это критично для моделей вроде Qwen3.6 — они чувствительны к порядку и шуму.
Однажды я разбирал такой случай: база знаний содержала три версии политики безопасности — 2022, 2023 и 2024 года. Модель выбрала среднюю, потому что не поняла, какая актуальна. После того как я добавил даты в начало каждого документа и отсортировал их по релевантности, ответ стал точным на 100%. Об этом — следующий шаг.
Шаг 1: Порядок секций — кто кого перебивает
LLM, особенно small и medium, имеют неравномерное внимание к разным частям контекста. Исследования показывают: модели лучше запоминают информацию из начала и конца промпта (primacy/recency effect). Середина — зона забывания. Поэтому порядок критичен.
Правильная структура:
- Инструкция (системный промпт) — что делать, как отвечать.
- Релевантные документы — от наиболее важного к наименее важному (по score релевантности).
- Вопрос пользователя — конец промпта, чтобы модель запомнила его и сфокусировалась.
Не делайте так: документы, потом инструкция, потом вопрос. Инструкция в середине — модель её забудет. Пример правильного промпта:
Системное сообщение: Отвечай только на основе предоставленных документов. Если ответа нет в документах — скажи "не знаю".
Документ 1 (релевантность 0.95): ...
Документ 2 (релевантность 0.85): ...
Документ 3 (релевантность 0.72): ...
Вопрос пользователя: Какова текущая версия протокола?
В статье "Как заставить LLM работать с корпоративными данными" описан похожий метод контекстуализации — там добавляют метаданные в каждый документ, что тоже улучшает порядок.
Шаг 2: Порог релевантности — отсекаем шум
Embedding-модели (например, intfloat/multilingual-e5-large) выдают оценки сходства от -1 до 1. Если тащить в контекст все документы с порогом >0.5, вы получите кучу мусора. Особенно в задачах, где есть похожие темы с разными ответами.
На практике для Qwen3.6 оптимальный порог — 0.7. Ниже — модель начинает опираться на слабо связанные документы и галлюцинировать. Выше — теряет полезную информацию. Но порог нужно калибровать под свою предметную область.
Пример фильтрации на Python:
import numpy as np
from typing import List
def filter_by_relevance(
docs: List[dict],
query_embedding: np.ndarray,
threshold: float = 0.7
) -> List[dict]:
relevant = []
for doc in docs:
sim = cosine_similarity(doc["embedding"], query_embedding)
if sim >= threshold:
doc["_score"] = sim
relevant.append(doc)
# сортировка по убыванию релевантности
relevant.sort(key=lambda x: x["_score"], reverse=True)
return relevant
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
Этот код — база. В продакшене я рекомендую добавить нормализацию скоров и адаптивный порог (топ-K или динамический процентиль). Но начните с фиксированного 0.7.
Важно: порог зависит от embedding-модели и домена. Для технической документации с четкими терминами можно ставить 0.8, для новостей с размытыми темами — 0.6. Проверяйте на валидационном наборе.
Шаг 3: Фильтрация — убираем дубли и противоречия
Даже после порога релевантности остаются проблемы:
- Дубликаты — один и тот же документ мог сохраниться несколько раз с разными эмбеддингами (например, из-за перегенерации чанков).
- Почти дубликаты — два документа, которые отличаются одним абзацем. Модель может запутаться или потерять ключевую разницу.
- Противоречия — документ А говорит "версия 2.0", документ Б — "версия 3.0". Модель выберет неверную.
Как с этим бороться? Используйте тот же эмбеддинг для детекции дубликатов по порогу косинусного сходства между документами (обычно >0.95). Для противоречий сложнее: можно отправить пару документов на проверку LLM-судье или довериться правилу: при конфликте оставляем документ с более высоким score релевантности.
Пример удаления дубликатов:
import numpy as np
def deduplicate(docs: List[dict], threshold: float = 0.95) -> List[dict]:
unique = []
for doc in sorted(docs, key=lambda x: x["_score"], reverse=True):
is_duplicate = False
for existing in unique:
sim = cosine_similarity(doc["embedding"], existing["embedding"])
if sim >= threshold:
is_duplicate = True
break
if not is_duplicate:
unique.append(doc)
return unique
В статье "Ваша LLM-аналитика — это подтасовка фактов" я подробно разбирал, как дубли и шум в контексте искажают ответы. Принципы те же.
Собираем все вместе: Agent Harness для Qwen3.6
Теперь объединим все три шага в компактный класс, который можно использовать с любым фреймворком для локального запуска. В качестве бэкенда возьмем llama.cpp, но подойдет и vLLM. Подробнее о выборе фреймворка читайте в обзоре фреймворков.
import numpy as np
from typing import List, Dict, Any
from llama_cpp import Llama # актуальная версия на 2026
class ContextualAgent:
def __init__(self, model_path: str, embed_model):
self.llm = Llama(model_path=model_path, n_ctx=8192)
self.embed_model = embed_model # например, sentence-transformers
def retrieve_and_filter(self, query: str, documents: List[Dict]) -> List[Dict]:
query_emb = self.embed_model.encode(query)
# Шаг 2: фильтр по релевантности
relevant = self._filter_by_relevance(documents, query_emb, threshold=0.7)
# Шаг 3: дедупликация
unique = self._deduplicate(relevant)
return unique
def build_prompt(self, instructions: str, documents: List[Dict], query: str) -> str:
# Шаг 1: правильный порядок
parts = [instructions]
parts.extend([doc["text"] for doc in documents])
parts.append(f"Вопрос: {query}")
return "\n\n".join(parts)
def answer(self, instructions: str, documents: List[Dict], query: str) -> str:
filtered_docs = self.retrieve_and_filter(query, documents)
prompt = self.build_prompt(instructions, filtered_docs, query)
response = self.llm(prompt, max_tokens=512)
return response["choices"][0]["text"]
Это базовая обвязка. В реальном проекте добавьте логирование, мониторинг длин контекста и fallback-стратегию при пустом контексте (например, ответ "нет информации").
Ошибки, которые я видел в 2026
Даже после всех шагов можно наломать дров. Вот самые частые грабли:
- Слишком высокий порог релевантности (0.9+) — контекст пуст, модель отвечает из своего training data, а не из документов. Симптом: ответы выглядят общими, без деталей из базы.
- Игнорирование длины контекста — Qwen3.6 поддерживает 32k токенов, но если вы забили контекст до отказа, модель начинает “забывать” середину. Используйте трюк: обрезайте наименее релевантные документы, пока суммарная длина не станет меньше 80% лимита.
- Несортированные документы — даже с порогом 0.7, если документы идут в случайном порядке, модель может упустить главное. Обязательно сортируйте по score убывания.
- Отсутствие инструкции “не знаю” — если модель не находит ответа, она начнет выдумывать. Всегда добавляйте: “Если в документах нет точного ответа, скажи ‘не знаю’.”
Ошибка номер один, которую я встречаю при аудитах: разработчик поднимает порог до 0.9, чтобы “исключить шум”, а потом удивляется, что модель отвечает из воздуха. Помните: лучше пустой контекст с явным “не знаю”, чем галлюцинация.
Бонус: используйте контекстный профилировщик
Ручная настройка порогов и фильтров — это хорошо, но есть более элегантный метод. В статье "Оптимизация LLM-запросов с помощью контекстного профилировщика" я описываю инструмент, который автоматически анализирует, какие части контекста действительно влияют на ответ, и сжимает его без потери качества. Для средних моделей это может дать прирост точности на 15-20% просто за счет удаления лишних токенов.
Как это работает: профилировщик прогоняет несколько вариаций контекста через модель, измеряет изменение ответа и на основе этого определяет, какие фрагменты ключевые. Те, что не влияют — вырезаются. Удобно, когда у вас нет времени на ручной подбор порога.
Средняя модель становится надежной, когда перестаешь быть ленивым. Выстроил контекст — получил точный ответ. Забил — получил галлюцинации. Выбор за тобой. А если будут вопросы — пиши в комменты, я обычно отвечаю в течение дня.