Shanraq.org Shanraq.org
Промпт как код: параметры и воспроизводимость
IT

Python: от данных до своей сводки Урок 49 из 56

Промпт как код: параметры и воспроизводимость

Сорок восьмой урок курса по Python. Перестаём набирать запрос к модели одной случайной строкой: отделяем роль, задачу, данные и ограничения, фиксируем temperature, seed и предел ответа, а затем сохраняем весь запрос рядом с результатом. Одинаковый запрос становится проверяемым опытом — но не обещанием побитно одинакового текста.

Зачем это нужно

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

В разработке конфигурация является частью программы. Для языковой модели конфигурация — это не только текст, но и имя модели, сообщения по ролям, temperature, seed, предел ответа и режим потока.

Цель урока не «заставить модель всегда писать одинаково». Цель скромнее и важнее: сделать запуск повторяемым и объяснимым — сохранить все входы, отделить данные от инструкции и видеть, что именно поменялось.

Сразу целиком

Файл prompt.py. Он строит запрос без обращения к Ollama, поэтому его можно проверять быстро и без модели.

"""Урок 48: промпт и параметры — часть программы."""

import hashlib
import json

MODEL = "gemma4"
SYSTEM = "Ты редактор данных. Не добавляй фактов, которых нет во входных данных."


def build_request(data, *, temperature=0.0, seed=42):
    """Собрать полностью заданный и сохраняемый запрос Ollama."""
    prompt = """ЗАДАЧА
Составь одно предложение по данным.

ДАННЫЕ
<data>
{data}
</data>

ОГРАНИЧЕНИЯ
- сохрани число, год и единицу измерения;
- не объясняй причины;
- не добавляй другие числа.
""".format(data=data)
    return {
        "model": MODEL,
        "messages": [
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": prompt},
        ],
        "stream": False,
        "options": {
            "temperature": temperature,
            "seed": seed,
            "num_predict": 120,
        },
    }


def canonical(payload):
    """Одни и те же поля — одни и те же байты для журнала и сравнения."""
    return json.dumps(
        payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")
    ).encode("utf-8")


request = build_request("Казахстан, инфляция 2025 года: 11.4 %")
again = build_request("Казахстан, инфляция 2025 года: 11.4 %")
fingerprint = hashlib.sha256(canonical(request)).hexdigest()[:12]

print("модель:", request["model"])
print("роли:", [message["role"] for message in request["messages"]])
print("temperature:", request["options"]["temperature"])
print("seed:", request["options"]["seed"])
print("предел:", request["options"]["num_predict"])
print("тот же запрос:", canonical(request) == canonical(again))
print("отпечаток:", fingerprint)

Вывод:

модель: gemma4
роли: ['system', 'user']
temperature: 0.0
seed: 42
предел: 120
тот же запрос: True
отпечаток: 96df65f40c1f

Четыре части вместо заклинания

Роль (system) задаёт постоянное правило работы. Задача говорит, что сделать сейчас. Данные лежат внутри явных границ <data>. Ограничения называют, чего в ответе быть не должно.

Разделители не являются защитной стеной: текст внутри данных всё равно может содержать команды. Поэтому данные не получают доверия только из-за тегов. Здесь они помогают человеку и модели видеть структуру; защита появляется позже, когда результат проверяется кодом.

Промпт стоит писать как функцию: один вход, понятный результат, минимум скрытого состояния. Не склеивайте в нём сегодняшнюю дату, случайный идентификатор или содержимое файла, если это не часть задачи — иначе два запуска уже не одинаковы.

Параметры генерации

temperature управляет разбросом выбора следующих токенов: более высокое значение обычно даёт больше вариантов. Для извлечения и пересказа чисел начинаем с 0.0; для творческого текста может понадобиться больше.

seed фиксирует начальное состояние генератора случайных чисел. Вместе с одинаковыми моделью, запросом и параметрами он уменьшает случайный разброс. Но это не гарантия побитной воспроизводимости между версиями Ollama, файлами модели, драйверами и разным оборудованием.

num_predict ограничивает максимальную длину ответа. Это предохранитель времени и памяти, а не просьба модели обязательно написать ровно 120 токенов.

stream: False остаётся из прошлого урока: сохраняем один законченный объект. Поток подключают, когда нужен интерфейс, а не для изменения смысла ответа.

Каноническая запись и отпечаток

У словаря порядок полей не должен менять смысл. sort_keys=True и компактные разделители превращают одинаковый запрос в одинаковые байты. SHA-256 здесь не прячет данные: короткий отпечаток лишь помогает связать строку журнала с сохранённым запросом.

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

Что сохранять рядом с ответом

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

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

Карта урока

Карта урока: роль, данные, параметры и сохранённый запрос

Закройте текст и восстановите опорный сигнал: роль + задача + данные + ограничения + параметры → сохранённый запрос.

Скажите своими словами

  1. Почему temperature=0 не является абсолютной гарантией одинакового текста?
  2. Чем отпечаток запроса отличается от сохранённого запроса?
  3. Зачем отделять данные от инструкции, если теги не являются защитой?
  4. Что должно измениться в опыте только одно, если сравнивают две температуры?

Разминка

1. Предскажите. Одинаковы ли канонические строки?

import json

a = {"seed": 42, "temperature": 0}
b = {"temperature": 0, "seed": 42}
canon = lambda value: json.dumps(value, sort_keys=True, separators=(",", ":"))
print(canon(a) == canon(b))

2. Заполните пропуск. Закрепите начальное состояние генератора.

options = {"temperature": 0.0, ...: 42, "num_predict": 120}

3. Почините. В этом опыте одновременно поменялись данные и температура, поэтому причина разницы неизвестна.

first = build_request("2024: 8.7 %", temperature=0.0)
second = build_request("2025: 11.4 %", temperature=0.7)

Задание

Обязательное. Напишите validate_request(payload). Она должна проверить точное имя модели, роли system и user, stream is False, temperature, seed, положительный num_predict и наличие границ <data>...</data>. Верните словарь настроек. Затем через JSON сделайте копию запроса, измените только температуру и докажите, что исходный запрос не изменился.

модель: gemma4
роли: system,user
temperature: 0.0
seed: 42
предел: 120
копия независима: True
изменилось одно поле: True

На своих данных. Составьте запрос по одной таблице своей сводки. Выполните его трижды с одинаковыми параметрами и сохраните ответы рядом. Отметьте только фактические различия; не объявляйте один ответ «лучше» без критерия.

По желанию. Сравните temperature=0.0 и 0.7, меняя только это поле. До запуска письменно определите критерий: сохранены ли число, год, единица и запрет на причины.

Куда это встанет в проекте

Как и в уроке 47, в рабочую сводку модель пока не входит. Новый результат урока — сохраняемый запрос. В следующем уроке добавим JSON Schema и проверим ответ до того, как хоть одна его строка попадёт в отчёт.

Ответы

Показать ответы
  1. Реализация, версия модели и вычисления на разном оборудовании могут отличаться; seed управляет случайностью, но не фиксирует всю систему.
  2. Отпечаток позволяет сравнить входы, но не содержит сам вход. Для повторения нужен сохранённый запрос.
  3. Чтобы структура была видна и данные не смешивались с авторской инструкцией; безопасность всё равно требует проверки результата.
  4. Только temperature; иначе нельзя связать различие ответа с одной причиной.
True
  1. Пропущенный ключ — "seed".
options = {"temperature": 0.0, "seed": 42, "num_predict": 120}
print(options["seed"])
42
  1. Оставьте данные одинаковыми и поменяйте только температуру.
def build_request(data, *, temperature):
    return {"data": data, "temperature": temperature}

first = build_request("2025: 11.4 %", temperature=0.0)
second = build_request("2025: 11.4 %", temperature=0.7)
print(first["data"] == second["data"])
True

Источники

  • Ollama /api/chatmessages, stream и параметры temperature, seed, num_predict.
  • Ollama Modelfile — системное сообщение и параметры модели.
  • Python hashlib — SHA-256 и назначение криптографического отпечатка.

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.