Контекстная инженерия для локальных LLM: гайд 2026 | AiManual
AiManual Logo Ai / Manual.
01 Июл 2026 Гайд

Контекстная инженерия для локальных LLM: как сделать среднюю модель надежной за счет продуманного контекста и фильтрации

Научитесь управлять контекстом, чтобы Qwen3.6 и другие средние модели выдавали точные ответы. Порог релевантности, порядок секций, фильтрация — полное руководст

Реклама
cliv2

Средняя модель — как стажер: без контекста — беда

Вы купили карту за полмиллиона, поставили 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). Середина — зона забывания. Поэтому порядок критичен.

Правильная структура:

  1. Инструкция (системный промпт) — что делать, как отвечать.
  2. Релевантные документы — от наиболее важного к наименее важному (по score релевантности).
  3. Вопрос пользователя — конец промпта, чтобы модель запомнила его и сфокусировалась.

Не делайте так: документы, потом инструкция, потом вопрос. Инструкция в середине — модель её забудет. Пример правильного промпта:

Системное сообщение: Отвечай только на основе предоставленных документов. Если ответа нет в документах — скажи "не знаю".

Документ 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% просто за счет удаления лишних токенов.

Как это работает: профилировщик прогоняет несколько вариаций контекста через модель, измеряет изменение ответа и на основе этого определяет, какие фрагменты ключевые. Те, что не влияют — вырезаются. Удобно, когда у вас нет времени на ручной подбор порога.

Средняя модель становится надежной, когда перестаешь быть ленивым. Выстроил контекст — получил точный ответ. Забил — получил галлюцинации. Выбор за тобой. А если будут вопросы — пиши в комменты, я обычно отвечаю в течение дня.

Подписаться на канал