Урок курса
Шаблоны сообщений и параметризация промптов в LangChain
LangChain 1.0.x: c нуля до продакшн LLM-приложений🔹 PromptTemplate / ChatPromptTemplate
На этом этапе мы познакомимся с инструментами LangChain для создания шаблонов промптов — классами PromptTemplate и ChatPromptTemplate. Разберём их отличия, научимся создавать многоразовые шаблоны запросов и узнаем, в каких случаях целесообразнее использовать каждый из них.
В предыдущем уроке мы писали промпты вручную, вставляя переменные прямо в строку. LangChain упрощает эту задачу, предоставляя специальные классы:
- PromptTemplate – шаблон для однострочного текстового промпта. Используется в ситуациях, когда модель принимает единичное текстовое сообщение (например, старые GPT‑3 модели). Вы задаёте строку-шаблон с плейсхолдерами (например,
{topic},{user_input}), а затем подставляете конкретные значения через методformatилиinvoke. - ChatPromptTemplate – шаблон для моделей чат-формата, которые работают с последовательностью сообщений (с ролями вроде system, human, ai). С его помощью можно определить шаблоны сразу для нескольких сообщений (например: системное сообщение → пользовательское сообщение → модель → пользовательское). Затем метод
format_messagesилиformat_promptвозвращает список сообщений с подставленными значениями переменных, пригодный для вызова чат-модели. Работает именно сMessage objectsи может использовать разные форматы входа.
Использование шаблонов даёт ряд преимуществ: код становится чище (шаблон можно определить один раз и переиспользовать), уменьшается вероятность допустить синтаксическую ошибку в строке, проще управлять версионностью промптов. Вместо конкатенации строк или вставки через f‑строки вы оперируете объектом шаблона.
🔸 Пример (PromptTemplate)
from langchain_core.prompts import PromptTemplate
template_str = "Ты опытный переводчик. Переведи текст с английского на русский:\n\"{input_text}\""
template = PromptTemplate.from_template(template_str)
# Вариант 1 — format (возвращает просто строку)
prompt_text = template.format(input_text="Hello, world!")
print('format result:', prompt_text)
# Вариант 2 — invoke (возвращает объект типа StringPromptValue)
prompt_strobj = template.invoke({"input_text": "Hello, world!"})
print('invoke result:', prompt_strobj)
print(type(prompt_strobj))
Вывод:
format result: Ты опытный переводчик. Переведи текст с английского на русский:
"Hello, world!"
invoke result: text='Ты опытный переводчик. Переведи текст с английского на русский:\n"Hello, world!"'
<class 'langchain_core.prompt_values.StringPromptValue'>
Заметьте: внутри шаблона используется переменная {input_text}.
Для подстановки этой переменной:
-
в метод
format()передаются именованные аргументы по имени этих переменных -
в метод
invoke()передаётся словарь, где ключи совпадают с именами переменных
💡 Если пропустить переменную или указать неверное имя — LangChain выдаст понятное сообщение об ошибке, что упрощает отладку.
в отличии от метода format(), который просто подставляет в шаблон нужные куски текста и возвращает нам строку, метод invoke() возвращает специальный объект типа StringPromptValue
💡 Метод
.from_template()позволяет более кратко создать шаблон с переменными, этот метод автоматически считывает их из фигурных скобок:prompt = PromptTemplate.from_template("Вопрос: {question}")Это аналогично вот такой записи:
prompt = PromptTemplate( template="Вопрос: {question}", input_variables=["question"] )Если это простой шаблон с переменными, то короче записать через
PromptTemplate.from_template(), а если нужно указывать дополнительные аргументы, то нужно использоватьPromptTemplate()
🔸 Пример (ChatPromptTemplate)
from langchain_core.prompts import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate
)
# Создаём шаблон чата с системным и пользовательским сообщением
chat_template = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template("Ты — опытный переводчик."),
HumanMessagePromptTemplate.from_template("Переведи текст с английского на русский:\n\"{input_text}\"")
])
# Вариант 1 — format_messages (возвращает список сообщений/объектов для модели)
messages_list = chat_template.format_messages(input_text="Hello, world!")
print("format_messages result:", messages_list)
# Вариант 2 — invoke (возвращает объект ChatPromptValue со списком внутри него)
prompt_chatobj = chat_template.invoke({"input_text": "Hello, world!"})
print("invoke result:", prompt_chatobj)
print(type(prompt_chatobj))
Вывод:
format_messages result: [SystemMessage(content='Ты — опытный переводчик.', additional_kwargs={}, response_metadata={}), HumanMessage(content='Переведи текст с английского на русский:\n"Hello, world!"', additional_kwargs={}, response_metadata={})]
invoke result: messages=[SystemMessage(content='Ты — опытный переводчик.', additional_kwargs={}, response_metadata={}), HumanMessage(content='Переведи текст с английского на русский:\n"Hello, world!"', additional_kwargs={}, response_metadata={})]
<class 'langchain_core.prompt_values.ChatPromptValue'>
Здесь мы создали шаблон из двух сообщений:
- системного (с фиксированной ролью «Ты – опытный переводчик.»)
- пользовательского (с плейсхолдером для текста)
Разница между format_messages() и invoke() аналогично предыдущему примеру, с той лишь разницей что первый обрабатывает строки и объекты по очереди и выводит их в списке, а второй создаёт специальные объект типа ChatPromptValue
Модель получит структурированный промпт с разделением роли, что предпочтительно при использовании GPT‑5/ GPT‑4o.
Для передачи в модель ChatOpenAI можно использовать любой из описанных выше вариантов промпта
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model_name="gpt-5")
for prompt in [prompt_text, prompt_strobj, messages_list, prompt_chatobj]:
response = llm.invoke(prompt)
print(response.content)
Ответ response.content будет переводом строки "Hello, world!" на русский.
🔸 Технические детали
PromptTemplate и ChatPromptTemplate под капотом похожи – второй по сути содержит набор PromptTemplate для разных ролей. LangChain также предоставляет методы сериализации шаблонов (сохранения в файл JSON/YAML) и загрузки обратно. Это пригодится, если вы захотите вынести промпты в конфиги.
StringPromptValue и ChatPromptValue — это универсальные контейнеры для готового промпта.
Они позволяют передавать промпты в модель или цепочку без забот о формате, обеспечивая совместимость, читаемость и надёжность кода. Если вам понадобится достать из этих объектов строку или список сообщений, то у них есть методы .to_string() и .to_messages()
🔸 Рекомендации
Начинайте любую автоматизацию с создания шаблонов. Даже если задача кажется разовой, оберните промпт в PromptTemplate – это не только удобство, но и дисциплина: вы явно перечисляете, какие переменные ожидаются, что предотвращает многие ошибки. Давайте шаблонам понятные имена – промпты будут расти и важно не запутаться. В дальнейшем вы сможете собирать из шаблонов более сложные цепочки, подключать версии промптов и т.д.
Шаблоны сообщений (ChatPromptTemplate) особенно полезны, когда нужно динамически менять части системных инструкций или добавлять сообщения – они помогут не «захардкодить» лишнего.
🔹 Параметры и плейсхолдеры
Сейчас мы разберёмся, как вводить динамические параметры в промпты с помощью плейсхолдеров, чтобы один шаблон мог обслуживать разные входные данные. Мы также посмотрим на форматирование шаблонов (в том числе через Jinja2) и безопасную подстановку.
Объяснение: Параметризация промпта означает выделение в тексте шаблона мест, куда будут вставляться разные значения при каждом вызове. В примерах ранее мы использовали {input_text} – это и есть плейсхолдер (placeholder) для входного текста.
Главная идея: не писать 10 похожих промптов для 10 случаев, а написать один с параметрами. Например, вы делаете бота-переводчика и хотите менять язык перевода в зависимости от пользователя. Вместо копипасты «Переведи на русский/английский/китайский», пишете шаблон "Переведи текст с {source_lang} на {target_lang}" и подставляете нужные языки при вызове.
LangChain PromptTemplate по умолчанию использует формат f‑строк Python для плейсхолдеров (фигурные скобки {}). Также поддерживается шаблонизацию через Jinja2, что даёт больше возможностей (условия, циклы внутри шаблона и т.д.). Чтобы использовать Jinja2, достаточно при создании PromptTemplate указать параметр template_format="jinja2". Тогда внутри строки можно применять синтаксис Jinja2. Например:
from langchain_core.prompts import PromptTemplate
template_str = "Привет, {% if formal %}уважаемый{% else %}дорогой{% endif %} {{ name }}!"
prompt = PromptTemplate(
template=template_str,
input_variables=["name", "formal"],
template_format="jinja2"
)
print(prompt.format_prompt(name="Иван", formal=True).to_string())
# Вывод: "Привет, уважаемый Иван!"
Здесь мы показали, что условная логика в шаблоне позволяет варьировать обращение (формально/неформально) в зависимости от переменной formal. Jinja2 имеет смысл, когда шаблон очень сложный и содержит повторяющиеся части, циклы или условия. Но в большинстве случаев стандартных f‑строк достаточно – они безопаснее и проще.
⚠️ будьте осторожны: не подставляйте в Jinja‑шаблон данные из непроверенных источников, так как теоретически это может привести к исполнению нежелательных выражений
🔸 prompt.partial()
PromptTemplate так же позволяет задавать частичное заполнение переменных (partial) — значения, которые вы фиксируете заранее.
Например, если в большинстве промптов повторяется одна и та же инструкция, её можно вынести в переменную и один раз подставить, чтобы не дублировать текст.
Или, скажем, вы хотите заранее заполнить часть шаблона и получить новый шаблон:
from langchain_core.prompts import PromptTemplate
# Создаём шаблон с двумя переменными
template = "Привет, {name}! {instruction}"
prompt = PromptTemplate(
template=template,
input_variables=["name", "instruction"]
)
# Частично фиксируем одну из переменных
partial_prompt = prompt.partial(instruction="Пожалуйста, расскажи о себе.")
# Теперь у нового шаблона осталась только одна незаполненная переменная
print(partial_prompt.input_variables) # ['name']
# Подставляем оставшееся значение
result = partial_prompt.format(name="Иван")
print(result) # Привет, Иван! Пожалуйста, расскажи о себе.
💡 Частичные переменные удобно использовать для общих инструкций, которые меняются редко.
🔸 Технические детали
При параметризации промпта важно учитывать экранирование и использование специальных символов.
Если параметр может содержать кавычки, переводы строк или фигурные скобки, это способно нарушить шаблон или затруднить его чтение.
Решение — оборачивать пользовательские данные в кавычки либо использовать специальные маркеры внутри промпта.
Например, в шаблонах перевода текст заключают в кавычки — "{input_text}". Это помогает визуально и синтаксически отделить его от остальной инструкции.
Если вы вставляете в шаблон фрагмент кода или JSON, используйте блок с тройными кавычками или текстовые метки вроде Начало данных / Конец данных. Такие приёмы защищают шаблон от ситуаций, когда пользовательский ввод содержит проблемные последовательности вроде "}{, нарушающие формат.
⚠️ Всегда экранируйте специальные символы в переменных, чтобы избежать ошибок при рендеринге шаблона.
🔸 Пример с цепочками
Покажем, как использовать параметризацию в связке с RunnableSequence. Предположим, у нас есть шаблон промпта для приветствия пользователя по имени и должности:
import os
from dotenv import load_dotenv
load_dotenv()
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnableSequence
MODEL = os.getenv("OPENAI_API_MODEL", "gpt-5")
template = "Отвечай как ассистент: Привет, {name}! Рад тебя видеть. Я могу помочь с вопросами по {topic}."
prompt = PromptTemplate(
template=template,
input_variables=["name", "topic"]
)
chain = RunnableSequence(prompt, ChatOpenAI(model=MODEL))
result = chain.invoke({"name": "Анна", "topic": "программированию"})
print(result)
Здесь мы создали цепочку, передав наш шаблон. Метод chain.invoke() принимает именованные аргументы для всех переменных шаблона. Он подставит name="Анна", topic="программированию" в шаблон, отправит финальный промпт модели и вернёт результат. Таким образом, разделив шаблон и данные, мы легко можем подставлять разные name и topic без переписывания самого промпта.
🔸 Рекомендации
- Когда планируете структуру плейсхолдеров, выбирайте понятные названия переменных – это облегчает сопровождение. Например,
{input}– слишком общее название, лучше уточнить{user_question}или{text_to_translate}. - Следите за тем, чтобы все необходимые переменные передавались в шаблон: LangChain при включённой валидации (
validate_template=True) предупредит, если какая‑то переменная не используется или лишняя – это можно включить при отладке. - Если используете Jinja2‑шаблоны, делайте их содержимое как можно проще и тестируйте отдельно, чтобы убедиться, что логика внутри работает.
- И наконец, не стоит прописывать чувствительные данные в шаблонах – API‑ключи, персональные сведения или длинные тексты лучше передавать как параметры или загружать из внешних источников, чем вписывать в строку. Это и безопаснее, и гибче для обновлений.
🔹 Чейнинг промптов (многошаговость)
В предыдущем уроке, обсуждая антипаттерны, мы упоминали рекомендацию — разбивать сложные задачи на этапы. Теперь разберём, как реализовать это на практике: как объединить несколько вызовов модели в единую последовательность, где результат одного шага становится входом для следующего.
Такой подход позволяет модели рассуждать поэтапно и комбинировать различные функции. Этот принцип называется чейнингом (от англ. chain — «цепь») — техникой, при которой задача делится на несколько шагов, и каждый шаг выполняется моделью с собственным промптом.
🔸 Когда это нужно?
- Разбить сложный вопрос на подзадачи: сначала определить подзадания, затем ответить по каждому.
- Пост‑обработка ответа: создать черновик, потом попросить модель отредактировать его.
- Комбинировать роли: один промпт генерирует идеи, второй оценивает их или выбирает лучшие.
- Часто применяют схему «размышление → ответ»: сначала просят модель обосновать решение, а затем сформулировать финальный ответ.
🔸 Простая реализация в LangChain
Самый простой способ – использовать LCEL, который автоматически передаёт вывод одного шаблона на вход следующему.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
MODEL = os.getenv("OPENAI_API_MODEL", "gpt-5")
llm = ChatOpenAI(model=MODEL)
prompt1 = PromptTemplate.from_template("Назови 5 популярных достопримечательностей в городе {city}.")
prompt2 = PromptTemplate.from_template("Составь маршрут на 3 дня по городу {city}, включив следующие места: {places_list}.")
chain = (
{"city": RunnablePassthrough()}
| RunnablePassthrough.assign(
places_list=lambda x: (prompt1 | llm | StrOutputParser()).invoke(x)
)
| prompt2
| llm
| StrOutputParser()
)
# Запуск
result = chain.invoke({"city": "Париж"})
print(result)
Краткое объяснение цепочки:
{"city": RunnablePassthrough()}— создает словарь с ключом city, передавая входное значение без изменений. Это инициализация контекста.RunnablePassthrough()просто передает входные данные дальше без изменений-
RunnablePassthrough.assign(places_list=...)— ключевой момент! Берет словарь из шага 1, выполняет вложенную цепочку (prompt1 → llm → парсинг), и добавляет результат как новый ключplaces_list. Исходный ключ city при этом сохраняется. На выходе получаем словарь с двумя ключами:cityиplaces_list. -
prompt2— форматирует второй промпт, подставляя оба значения из словаря (city и places_list). Возвращает готовую строку-запрос. -
llm— отправляет отформатированный промпт в модель и получает ответ в видеAIMessage. -
StrOutputParser()— извлекает чистый текст из AIMessage и возвращает финальную строку с маршрутом.
Суть: Первая подцепочка генерирует список мест и добавляет его к контексту через assign(), затем вторая подцепочка использует и город, и список для создания итогового маршрута.
🔸 Практические соображения
Многошаговость означает несколько API‑вызовов, что увеличивает время ответа и стоимость. Поэтому цепочки стоит использовать только тогда, когда один запрос не справляется с задачей.
Чейнинг промптов позволяет строить более гибкие и «умные» сценарии работы с LLM. Главное – держать количество шагов минимальным, следить за смысловой связью между ними и тестировать цепочку на разных входных данных.
💡 Не забывайте логировать промежуточные результаты – это поможет отследить, где цепочка может сбиться.
Задача 1
Что делает ChatPromptTemplate в LangChain?
Задача 2
Когда уместнее взять PromptTemplate вместо ChatPromptTemplate?
Задача 3
Какие преимущества даёт переход на шаблоны вместо f-строк? (выберите все подходящие)
Задача 4
PromptTemplate — это…
Задача 5
Соотнесите роль/цель/формат и расположение в сообщениях:
Сопоставьте элементы
Задача 6
Где и зачем хранить промпты, чтобы удобно отслеживать их версии?
Задача 7
🔹 Практическое задание: шаблон с параметрами (PromptTemplate/Jinja2)
Вам предстоит создать собственный параметризованный шаблон промпта для выбранной задачи и показать, как он работает. В этом задании вы закрепите навыки использования PromptTemplate (или альтернативно Jinja2) для генерации гибких промптов.
🔸 Что предстоит сделать?
Выберите одну из задач, которую планируете автоматизировать (или придумайте учебную задачу), и разработайте для неё шаблон промпта с параметрами. Это может быть что угодно: шаблон ответа техподдержки, генератор описания товара, форматтер данных и т.д. Важно, чтобы в промпте присутствовали переменные, значения которых могут меняться.
🔸 Требования к выполнению задания
- Шаблон промпта: реализуйте его либо с помощью класса
PromptTemplateлибо с использованиемJinja2синтаксиса в отдельном файле. Шаблон должен включать как минимум два параметра. Например, в шаблоне ответа техподдержки это могут быть{user_name}и{issue_description}, или{product_name}и{discount_percentage}в рекламном сообщении. - Логичные плейсхолдеры: переменные ставьте только там, где ожидается изменяемая информация. Статичные части (инструкции модели, формат ответа, вежливые фразы) оставляйте в самом шаблоне; переменные – только для меняющихся частей. Убедитесь, что текст вокруг плейсхолдеров образует цельные предложения.
- Пример использования: подготовьте скрипт
prompt_template_demo.py, или ноутбукprompt_template_demo.ipynbв котором:- Либо создаёте шаблон через
PromptTemplate(...)прямо в коде. Либо загружаете его из файла.💡Если вы сделали Jinja-шаблон в файле, можно использовать
Path.read_text()+Environmentиз Jinja2, но проще воспользоваться LangChain – он умеет читать из файла черезPromptTemplate.from_file(). - подставляете в шаблон несколько разных наборов параметров и выводите сформированные промпты. Достаточно 2–3 примера с разными данными, чтобы показать гибкость.
- (Опционально) вызываете модель (
llm.invoke()илиchain.invoke()) с получившимся промптом, чтобы получить ответ. Это не обязательно, но полезно, чтобы убедиться в корректности шаблона
- Либо создаёте шаблон через
- Документация: в коде или отдельном текстовом файле опишите коротко, для чего ваш шаблон предназначен и какие параметры он принимает. Например: «Шаблон предназначен для генерации приветственного письма новому пользователю. Параметры:
{user_name}– имя пользователя,{welcome_offer}– специальное предложение для него.» Это поможет ревьюерам понять контекст.
🔸 Пример шаблона (концептуально)
Тема: Уведомление об ошибке
Привет, {name}!
Наше приложение столкнулось с непредвиденной ошибкой: {error_details}.
Наша команда уже работает над решением. Спасибо за понимание!
С уважением,
Команда поддержки {app_name}
Здесь параметры – {name}, {error_details}, {app_name}. В коде вы бы подставили, например, имя «Иван», описание ошибки «не удалось подключиться к серверу» и название приложения «SmartApp». Шаблон должен корректно сформироваться.
💡При желании можно отправить этот текст модели, попросив перевести его на английский или проверить грамматику
🔸 Кросс‑рецензирование
Это задание предполагает кросс‑рецензирование: другие студенты будут проверять вашу работу, а вам предстоит анализировать примеры одногруппников.
После подготовки файла добавьте его в репозиторий проекта (на GitHub) и пришлите ссылку на этот фаил.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
