Перейти к содержимому
ПереводыAnthropic

Туториал Anthropic по промптам, часть 3: продвинутый уровень (главы 8–9 и приложение)

Перевод глав 8–9 и приложения Prompt Engineering Interactive Tutorial: как не дать модели выдумывать, как собрать сложный промпт из десяти элементов, цепочки промптов, вызов инструментов «вручную» и поиск. С упражнениями под локальную модель.

Третья, заключительная часть перевода туториала Anthropic. Глава 8 — о галлюцинациях: как разрешить модели сказать «не знаю» и как заставить её сначала найти доказательства в документе. Глава 9 собирает все приёмы курса в один шаблон из десяти элементов и показывает его на чат-боте, юридической и финансовой задачах и помощнике программиста. Приложение выходит за рамки одного промпта: цепочки промптов, вызов инструментов, собранный из подстановки и цепочек, и поиск по документам.

Все примеры используют помощник pet.py из первой части: положите его рядом с ноутбуком и запустите локальную модель (по умолчанию — qwen3:8b в Ollama). Что изменено по сравнению с оригиналом и почему перевод распространяется по CC BY-NC 4.0, сказано там же. Главы 4–7 — во второй части.

  • Когда. После первых двух частей и урока 8.1.2. Глава 8 — практика к правилу «Только источники + „Не знаю“» из урока 8.2.1. Глава 9 — к разделу «Анатомия хорошего промпта» урока 8.1.2 и к уроку 8.4.1, где промпт превращается в шаблон с переменными под версионным контролем. Приложение 10.1 — пример простейшего workflow из урока 8.3.1, 10.2 — то, что происходит «под капотом» tool calling из того же урока, 10.3 — вход в RAG (урок 8.2.1).
  • Сколько времени. Два-три часа вместе с упражнениями.
  • Что можно пропустить новичку. Упражнение 9.1 строится на тексте налогового кодекса США — можно ограничиться чтением решения. В приложении 10.2 формат вызова инструментов устарел: важна идея, а современный способ описан в уроке 8.3.1 и на странице «Курс Anthropic: Tool use».
  • Длинный контекст. Промпты главы 8 занимают около 8 тыс. токенов, упражнения 9.1 — около 4 тыс. В старых версиях Ollama окно по умолчанию — 4096 токенов, и лишнее молча отрезается. Запустите сервер с запасом: OLLAMA_CONTEXT_LENGTH=16384 ollama serve (подробнее — в уроке 7.2.2).

Плохая новость: модель иногда «галлюцинирует» — делает неверные или ничем не подкреплённые утверждения. Хорошая новость: есть приёмы, которые помогают свести галлюцинации к минимуму.

Ниже разберём два из них:

  • дать модели возможность сказать, что она не знает ответа;
  • попросить модель найти доказательства, прежде чем отвечать.

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

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

from pet import get_completion
# Промпт
PROMPT = "Какой бегемот был самым тяжёлым за всю историю?"
# Ответ модели
print(get_completion(PROMPT))

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

# Промпт
PROMPT = "Какой бегемот был самым тяжёлым за всю историю? Отвечай, только если знаешь ответ наверняка."
# Ответ модели
print(get_completion(PROMPT))

В следующем промпте мы даём модели длинный документ с «отвлекающей информацией», которая почти, но не совсем относится к вопросу пользователя. Без подсказки модель попадается на отвлекающую информацию и даёт неверный, «галлюцинированный» ответ о числе подписчиков компании Matterport на 31 мая 2020 года.

Документ — раздел «Бизнес» годового отчёта Matterport (форма 10-K, поданная в Комиссию по ценным бумагам США) — в оригинале вставлен в ноутбук целиком, это почти 5 тысяч английских слов. Чтобы не перепечатывать его, функция ниже скачивает ноутбук главы 8 с GitHub и достаёт текст между тегами <document>. Вопрос и инструкции мы пишем по-русски, документ остаётся английским: современные модели свободно работают с текстом на одном языке и вопросом на другом.

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

import json
import re
import urllib.request
from pet import get_completion
# Папка с ноутбуками туториала в репозитории anthropics/courses
BASE = ("https://raw.githubusercontent.com/anthropics/courses/master/"
"prompt_engineering_interactive_tutorial/Anthropic%201P/")
def load_from_notebook(notebook, pattern):
"""Скачивает ноутбук и возвращает первый фрагмент текста, совпавший с регулярным выражением."""
with urllib.request.urlopen(BASE + notebook) as f:
nb = json.load(f)
for cell in nb["cells"]:
m = re.search(pattern, "".join(cell["source"]), re.DOTALL)
if m:
return m.group(1).strip()
raise ValueError("Текст не найден")
# Раздел «Бизнес» отчёта Matterport 10-K за 2023 год (около 32 тыс. символов, по-английски)
MATTERPORT_10K = load_from_notebook("08_Avoiding_Hallucinations.ipynb", r"<document>(.*?)</document>")
# Промпт
PROMPT = f"""<question>Каким было число подписчиков Matterport ровно на 31 мая 2020 года?</question>
Прочитай документ ниже. Затем напиши краткий числовой ответ в тегах <answer>.
<document>
{MATTERPORT_10K}
</document>"""
# Ответ модели
print(get_completion(PROMPT))

Как это исправить? Отличный способ уменьшить галлюцинации на длинных документах — заставить модель сначала собрать доказательства.

В этом случае мы просим модель сначала выписать подходящие цитаты, а потом строить ответ на них. С такой инструкцией модель правильно замечает, что цитата на вопрос не отвечает.

# Промпт
PROMPT = f"""<question>Каким было число подписчиков Matterport ровно на 31 мая 2020 года?</question>
Прочитай документ ниже. Затем в тегах <scratchpad> выпиши из документа самую подходящую цитату и подумай, отвечает ли она на вопрос пользователя или в ней недостаточно подробностей. После этого напиши краткий числовой ответ в тегах <answer>.
<document>
{MATTERPORT_10K}
</document>"""
# Ответ модели
print(get_completion(PROMPT))

Иногда галлюцинации удаётся убрать, понизив температуру (temperature) ответов модели. Температура — мера «творческости» ответа от 0 до 1: при 1 ответы менее предсказуемы и менее стандартны, при 0 — самые стабильные.

Если спрашивать модель о чём-то при температуре 0, повторные запуски обычно дают почти одинаковые ответы (хотя полный детерминизм не гарантируется). При температуре 1 (или промежуточных значениях) ответы будут разнообразнее. Подробнее о температуре и других параметрах — в документации API.

Измените PROMPT, чтобы исправить галлюцинацию: дайте модели выход.

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

import re
# Промпт
PROMPT = "В каком году звезда эстрады Бейонсе выпустила свой двенадцатый студийный альбом?"
# Ответ модели
response = get_completion(PROMPT)
# Проверка (адаптирована): модель признаёт, что не знает или что такого альбома нет, и не называет год
def grade_exercise(text):
admits = bool(re.search(
r"к сожалению|не знаю|не могу|нет (точной |достоверной )?информаци|нет (точных |достоверных )?(данных|сведений)"
r"|не выпускала|не существует|такого альбома нет",
text, re.IGNORECASE))
no_year = not re.search(r"\b20[2-3]\d\b", text) # никаких «2027 года» для несуществующего альбома
return admits and no_year
print(response)
print("\n------------------------------------------ ПРОВЕРКА ------------------------------------------")
print("Упражнение решено:", grade_exercise(response))
Подсказка

Проверка ищет в ответе фразы вроде «к сожалению», «не знаю» или «нет информации» (в оригинале — “Unfortunately”, “I do not”, “I don’t”) и отсутствие года. Что должна сделать модель, если она не знает ответа?

Измените PROMPT, чтобы исправить галлюцинацию: попросите модель привести цитаты. Правильный ответ — число подписчиков выросло в 49 раз.

# MATTERPORT_10K и load_from_notebook — из примеров выше
# Промпт
PROMPT = f"""С декабря 2018 по декабрь 2022 года во сколько раз выросло число подписчиков Matterport?
<document>
{MATTERPORT_10K}
</document>"""
# Ответ модели
response = get_completion(PROMPT)
# Проверка (адаптирована): в ответе есть «49 раз», «49-кратн…» или "49-fold"
def grade_exercise(text):
return bool(re.search(r"49\s*(раз|-?кратн|-fold)", text))
print(response)
print("\n------------------------------------------ ПРОВЕРКА ------------------------------------------")
print("Упражнение решено:", grade_exercise(response))
Подсказка

Проверка ищет в ответе «49 раз» (в оригинале — фразу “49-fold”). Заставьте модель сначала показать ход мысли: выписать подходящие цитаты и решить, достаточно ли в них доказательств. Если нужно освежить приём, вернитесь к уроку главы 8.

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

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

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

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

Пример — чат-бот «карьерный консультант»

Заголовок раздела «Пример — чат-бот «карьерный консультант»»

Следующая структура объединяет несколько элементов промпт-инжиниринга и хорошо подходит как отправная точка для сложных промптов. Для одних элементов порядок важен, для других нет. Мы отметим, где лучшие практики требуют определённого порядка, но в целом, если придерживаться этого порядка, получится хорошее начало для отличного промпта.

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

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

from pet import get_completion
######################################## ВХОДНЫЕ ПЕРЕМЕННЫЕ ########################################
# Первая входная переменная — история диалога (её можно передать и предыдущими сообщениями user и assistant в вызове API)
HISTORY = """Клиент: Назови две возможные профессии для выпускников-социологов.
Джо: Вот две возможные профессии для выпускников-социологов:
- Социальный работник. Социология даёт прочную основу для понимания человеческого поведения и социальных систем. С дополнительным обучением или сертификацией диплом социолога позволяет работать социальным работником, куратором случаев, консультантом и организатором местных сообществ, помогая людям и группам.
- Специалист по персоналу (HR). Понимание групповой динамики и организационного поведения из социологии применимо в работе с персоналом. Выпускники могут найти себя в подборе персонала, работе с сотрудниками, обучении и развитии, программах разнообразия и инклюзивности и других HR-функциях. Внимание к социальным структурам и институтам также помогает в смежных профессиях: государственная политика, управление некоммерческими организациями, образование."""
# Вторая входная переменная — вопрос пользователя
QUESTION = "Для какой из двух профессий нужно больше, чем степень бакалавра?"
######################################## ЭЛЕМЕНТЫ ПРОМПТА ########################################
##### Элемент 1: роль `user`
# Следите, чтобы массив messages в вызове API всегда начинался с роли `user`.
# Функция get_completion() делает это автоматически.
##### Элемент 2: контекст задачи
# Расскажите модели, какую роль она играет и каких целей и общих задач вы от неё ждёте.
# Контекст лучше ставить в начало промпта.
TASK_CONTEXT = "Ты будешь играть роль ИИ-консультанта по карьере по имени Джо, созданного компанией AdAstra Careers. Твоя цель — давать пользователям советы о карьере. Ты будешь отвечать пользователям сайта AdAstra, и они растеряются, если ты ответишь не от лица Джо."
##### Элемент 3: тон
# Если для общения это важно, скажите модели, каким тоном говорить.
# В зависимости от задачи этот элемент может быть не нужен.
TONE_CONTEXT = "Сохраняй дружелюбный тон службы поддержки."
##### Элемент 4: подробное описание задачи и правила
# Распишите конкретные задачи модели и правила, которым она должна следовать.
# Здесь же можно дать модели «выход», если у неё нет ответа или она не знает.
# В идеале покажите описание и правила другу: всё ли логично, однозначны ли все слова.
TASK_DESCRIPTION = """Вот важные правила общения:
- Всегда оставайся в образе Джо — ИИ от AdAstra Careers
- Если не знаешь, как ответить, скажи: «Извините, я не понял. Не могли бы вы переформулировать вопрос?»
- Если спрашивают о чём-то постороннем, скажи: «Извините, я Джо и даю советы о карьере. Есть ли у вас сегодня вопрос о карьере, с которым я могу помочь?»"""
##### Элемент 5: примеры
# Дайте модели хотя бы один пример идеального ответа, которому она может подражать. Оберните его в теги <example></example>. Примеров может быть несколько.
# Если примеров несколько, объясните модели, примером чего они служат, и оберните каждый в отдельную пару XML-тегов.
# Примеры — пожалуй, самый эффективный инструмент в интеллектуальной работе, чтобы модель вела себя как нужно.
# Обязательно дайте примеры типичных пограничных случаев. Если промпт использует черновик (scratchpad), полезно показать, как он должен выглядеть.
# Как правило, чем больше примеров, тем лучше.
EXAMPLES = """Вот пример того, как отвечать в обычной ситуации:
<example>
Клиент: Привет, кто тебя создал и чем ты занимаешься?
Джо: Здравствуйте! Меня зовут Джо, меня создала компания AdAstra Careers, чтобы давать советы о карьере. Чем могу помочь сегодня?
</example>"""
##### Элемент 6: входные данные для обработки
# Если модели нужно обработать данные, вставьте их сюда в подходящих XML-тегах.
# Данных может быть несколько, но каждый фрагмент — в своей паре XML-тегов.
# В зависимости от задачи этот элемент может быть не нужен. Порядок тоже гибкий.
INPUT_DATA = f"""Вот история диалога (между пользователем и тобой) перед вопросом. Если истории нет, она может быть пустой:
<history>
{HISTORY}
</history>
Вот вопрос пользователя:
<question>
{QUESTION}
</question>"""
##### Элемент 7: непосредственная задача или запрос
# «Напомните» модели или прямо скажите, что именно она должна сделать сейчас, чтобы выполнить задачу промпта.
# Сюда же ставятся дополнительные переменные, например вопрос пользователя.
# Повторить модели её непосредственную задачу обычно не вредно. Лучше делать это ближе к концу длинного промпта —
# так результаты получаются лучше, чем если поставить это в начало.
# Вопрос пользователя тоже, как правило, лучше ставить ближе к концу промпта.
IMMEDIATE_TASK = "Как ты ответишь на вопрос пользователя?"
##### Элемент 8: предварительное обдумывание (думаем по шагам)
# Для многошаговых задач полезно попросить модель подумать по шагам, прежде чем отвечать.
# Иногда даже приходится сказать «Прежде чем ответить…», чтобы модель точно сделала это сначала.
# Нужно не во всех промптах; если используете, ставьте ближе к концу длинного промпта, сразу после непосредственной задачи.
PRECOGNITION = "Сначала обдумай ответ, а потом отвечай."
##### Элемент 9: формат ответа
# Если ответ нужен в определённом формате, ясно опишите этот формат.
# В зависимости от задачи этот элемент может быть не нужен.
# Если он есть, лучше ставить его ближе к концу промпта, чем к началу.
OUTPUT_FORMATTING = "Помести свой ответ в теги <response></response>."
##### Элемент 10: начало ответа модели (prefill), если нужно
# Место, где можно начать ответ модели заранее написанными словами, чтобы направить её поведение.
# Prefill передаётся в роли `assistant` в вызове API.
# В зависимости от задачи этот элемент может быть не нужен.
PREFILL = "[Джо] <response>"
######################################## СБОРКА ЭЛЕМЕНТОВ ########################################
PROMPT = ""
if TASK_CONTEXT:
PROMPT += f"""{TASK_CONTEXT}"""
if TONE_CONTEXT:
PROMPT += f"""\n\n{TONE_CONTEXT}"""
if TASK_DESCRIPTION:
PROMPT += f"""\n\n{TASK_DESCRIPTION}"""
if EXAMPLES:
PROMPT += f"""\n\n{EXAMPLES}"""
if INPUT_DATA:
PROMPT += f"""\n\n{INPUT_DATA}"""
if IMMEDIATE_TASK:
PROMPT += f"""\n\n{IMMEDIATE_TASK}"""
if PRECOGNITION:
PROMPT += f"""\n\n{PRECOGNITION}"""
if OUTPUT_FORMATTING:
PROMPT += f"""\n\n{OUTPUT_FORMATTING}"""
# Печатаем промпт целиком
print("---------------------------- Полный промпт после подстановки переменных ----------------------------")
print("ХОД ПОЛЬЗОВАТЕЛЯ")
print(PROMPT)
print("\nХОД АССИСТЕНТА")
print(PREFILL)

Теперь запустим промпт! Выполните ячейку ниже, чтобы увидеть ответ модели.

print("\n------------------------------------- Ответ модели -------------------------------------")
print(get_completion(PROMPT, prefill=PREFILL))

Промпты в юридической сфере бывают довольно сложными, потому что приходится:

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

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

Мы поменяли местами несколько элементов, чтобы показать: структура промпта гибкая!

Промпт-инжиниринг — это научный метод проб и ошибок. Смешивайте и комбинируйте, переставляйте элементы (те, для которых порядок не важен) и смотрите, что лучше подходит вам и вашим задачам.

######################################## ВХОДНЫЕ ПЕРЕМЕННЫЕ ########################################
# Первая входная переменная — юридические материалы
LEGAL_RESEARCH = """<search_results>
<search_result id=1>
За прошедший год ветеринарная отрасль оказалась втянута в ряд патентных споров и споров о товарных знаках. В 1994 году Барклай Слокам получил патенты на операцию выравнивающей остеотомии плато большеберцовой кости (TPLO), которую применяют для лечения собак с разрывом передней крестообразной связки, и на инструменты для неё. В 2006 году компания Slocum Enterprises подала иск о нарушении патента против New Generation Devices, утверждая, что пластина Unity Cruciate Plate производства New Generation нарушает патент на пластину Slocum TPLO. Однако до решения по существу суд так и не дошёл: он постановил, что дело ему неподсудно, — из-за малого числа пластин, проданных в штате, где был подан иск, и сведений на сайте Slocum Enterprises. Другие патентные споры 2006 года касались применения лазеров для онихэктомии у кошек, идентификационных чипов для животных, вакцин для свиней и инструментов для вычёсывания шерсти.
</search_result>
<search_result id=2>
В Канаде Ветеринарная медицинская ассоциация Британской Колумбии подала иск против человека без ветеринарного образования, утверждая, что он спиливал «крючки» на зубах лошадей и выравнивал им зубы механическими и ручными инструментами, давал советы и ставил диагнозы за плату и выдавал себя за специалиста, готового проводить такое лечение. Суд счёл, что, принимая Закон о ветеринарной профессии, законодатель стремился защитить людей и животных, и что монопольные законы служат защите общества. Кроме того, суд заключил, что стоматология по своей сути связана со здоровьем зубов и дёсен, отличается от косметического и иного ухода за животными и потому подпадает под определение ветеринарной практики. Ответчику запретили оказывать такие услуги без надзора ветеринара.
</search_result>
<search_result id=3>
Последствия урагана «Катрина», обрушившегося на побережье Мексиканского залива в США в 2005 году, изменили отношение к животным во время стихийных бедствий. В 2006 году Гавайи, Луизиана и Нью-Гэмпшир приняли законы об уходе за животными при бедствиях: например, об убежищах для домашних животных и о праве держать служебных животных рядом с людьми, которым они помогают. Кроме того, Конгресс принял, а президент подписал в 2006 году Закон о стандартах эвакуации и перевозки домашних животных (Pet Evacuation and Transportation Standards Act): он обязывает органы штатов и местные органы, отвечающие за готовность к чрезвычайным ситуациям, включать в планы эвакуации сведения о том, как они разместят домашних и служебных животных в случае бедствия. Калифорния приняла закон, по которому её Управление по чрезвычайным ситуациям, Министерство сельского хозяйства и другие ведомства, готовящиеся к ликвидации бедствий, должны разработать план с учётом нужд служебных животных, скота, лошадей и домашних питомцев при бедствии или крупной чрезвычайной ситуации.
</search_result>
</search_results>"""
# Вторая входная переменная — вопрос пользователя
QUESTION = "Есть ли законы о том, что делать с домашними животными во время урагана?"
######################################## ЭЛЕМЕНТЫ ПРОМПТА ########################################
##### Элемент 1: роль `user` — как в прошлом примере
##### Элемент 2: контекст задачи
TASK_CONTEXT = "Ты — опытный юрист."
##### Элемент 3: тон — здесь не нужен
TONE_CONTEXT = ""
##### Элемент 4: входные данные для обработки
INPUT_DATA = f"""Вот собранные материалы. Используй их, чтобы ответить на юридический вопрос пользователя.
<legal_research>
{LEGAL_RESEARCH}
</legal_research>"""
##### Элемент 5: примеры
EXAMPLES = """Ссылаясь в ответе на материалы, указывай в квадратных скобках номер результата поиска и ставь после скобок точку. Ставь ссылку в конце предложения, которое на неё опирается. Примеры правильного оформления ссылок:
<examples>
<example>
Срок давности по таким преступлениям истекает через 10 лет. [3].
</example>
<example>
Однако защита не действует, если обе стороны явно от неё отказались. [5].
</example>
</examples>"""
##### Элемент 6: подробное описание задачи и правила
TASK_DESCRIPTION = f"""Напиши ясный, краткий ответ на этот вопрос:
<question>
{QUESTION}
</question>
Ответ должен занимать не больше пары абзацев. Если возможно, заверши его одним предложением, которое прямо отвечает на вопрос пользователя. Но если в собранных материалах недостаточно информации для такого ответа, можешь отказаться и написать: «Извините, у меня недостаточно информации, чтобы ответить на этот вопрос»."""
##### Элемент 7: непосредственная задача — здесь её роль играет описание задачи выше
IMMEDIATE_TASK = ""
##### Элемент 8: предварительное обдумывание
PRECOGNITION = "Прежде чем ответить, выпиши самые важные цитаты из материалов в тегах <relevant_quotes>."
##### Элемент 9: формат ответа
OUTPUT_FORMATTING = "Помести ответ из двух абзацев в теги <answer>."
##### Элемент 10: prefill
PREFILL = "<relevant_quotes>"
######################################## СБОРКА ЭЛЕМЕНТОВ ########################################
# Обратите внимание: порядок отличается от прошлого примера — данные и примеры идут раньше правил
PROMPT = ""
if TASK_CONTEXT:
PROMPT += f"""{TASK_CONTEXT}"""
if TONE_CONTEXT:
PROMPT += f"""\n\n{TONE_CONTEXT}"""
if INPUT_DATA:
PROMPT += f"""\n\n{INPUT_DATA}"""
if EXAMPLES:
PROMPT += f"""\n\n{EXAMPLES}"""
if TASK_DESCRIPTION:
PROMPT += f"""\n\n{TASK_DESCRIPTION}"""
if IMMEDIATE_TASK:
PROMPT += f"""\n\n{IMMEDIATE_TASK}"""
if PRECOGNITION:
PROMPT += f"""\n\n{PRECOGNITION}"""
if OUTPUT_FORMATTING:
PROMPT += f"""\n\n{OUTPUT_FORMATTING}"""
# Печатаем промпт целиком
print("---------------------------- Полный промпт после подстановки переменных ----------------------------")
print("ХОД ПОЛЬЗОВАТЕЛЯ")
print(PROMPT)
print("\nХОД АССИСТЕНТА")
print(PREFILL)

В оригинале у каждого элемента стоят те же пояснения, что и в первом примере; здесь мы оставили только заголовки. Теперь запустим промпт:

print("\n------------------------------------- Ответ модели -------------------------------------")
print(get_completion(PROMPT, prefill=PREFILL))

Если хотите поэкспериментировать с промптами урока, не трогая основной текст, в оригинале для этого есть «песочница» в конце ноутбука; здесь просто копируйте ячейки и меняйте их.

Упражнение 9.1 — Чат-бот для финансовых услуг

Заголовок раздела «Упражнение 9.1 — Чат-бот для финансовых услуг»

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

Советуем прочитать содержимое переменных (здесь это QUESTION и TAX_CODE), чтобы понимать, с чем предстоит работать модели. Обязательно подставьте QUESTION и TAX_CODE прямо в промпт (через f-строку, как в других примерах), чтобы туда попало настоящее содержимое переменных.

Заполните элементы промпта содержимым, которое соответствует описаниям и примерам сложных промптов выше. Когда заполните всё, что считаете нужным, запустите ячейку: вы увидите собранный промпт и ответ модели.

Помните, что промпт-инжиниринг редко бывает чистой формулой, особенно для больших и сложных промптов! Важно составлять тестовые примеры и пробовать разные промпты и структуры, чтобы понять, что лучше работает в каждой ситуации. Если вы меняете порядок элементов, не забудьте поменять и порядок сборки в разделе СБОРКА ЭЛЕМЕНТОВ.

# load_from_notebook — из главы 8
######################################## ВХОДНЫЕ ПЕРЕМЕННЫЕ ########################################
# Первая входная переменная — вопрос пользователя
QUESTION = "Сколько у меня времени, чтобы подать заявление по пункту 83(b)?"
# Вторая входная переменная — налоговый кодекс, по которому модель будет отвечать на вопрос
TAX_CODE = load_from_notebook("09_Complex_Prompts_from_Scratch.ipynb", r'TAX_CODE = """(.*?)"""')
######################################## ЭЛЕМЕНТЫ ПРОМПТА ########################################
# Пояснения к каждому элементу — в примере с карьерным консультантом
##### Элемент 1: роль `user` — get_completion() добавит её сама
##### Элемент 2: контекст задачи
TASK_CONTEXT = ""
##### Элемент 3: тон
TONE_CONTEXT = ""
##### Элемент 4: входные данные для обработки
INPUT_DATA = ""
##### Элемент 5: примеры
EXAMPLES = ""
##### Элемент 6: подробное описание задачи и правила
TASK_DESCRIPTION = ""
##### Элемент 7: непосредственная задача или запрос
IMMEDIATE_TASK = ""
##### Элемент 8: предварительное обдумывание (думаем по шагам)
PRECOGNITION = ""
##### Элемент 9: формат ответа
OUTPUT_FORMATTING = ""
##### Элемент 10: prefill
PREFILL = ""
######################################## СБОРКА ЭЛЕМЕНТОВ ########################################
PROMPT = ""
if TASK_CONTEXT:
PROMPT += f"""{TASK_CONTEXT}"""
if TONE_CONTEXT:
PROMPT += f"""\n\n{TONE_CONTEXT}"""
if INPUT_DATA:
PROMPT += f"""\n\n{INPUT_DATA}"""
if EXAMPLES:
PROMPT += f"""\n\n{EXAMPLES}"""
if TASK_DESCRIPTION:
PROMPT += f"""\n\n{TASK_DESCRIPTION}"""
if IMMEDIATE_TASK:
PROMPT += f"""\n\n{IMMEDIATE_TASK}"""
if PRECOGNITION:
PROMPT += f"""\n\n{PRECOGNITION}"""
if OUTPUT_FORMATTING:
PROMPT += f"""\n\n{OUTPUT_FORMATTING}"""
# Печатаем промпт целиком и ответ модели
print("---------------------------- Полный промпт после подстановки переменных ----------------------------")
print("ХОД ПОЛЬЗОВАТЕЛЯ")
print(PROMPT)
print("\nХОД АССИСТЕНТА")
print(PREFILL)
print("\n------------------------------------- Ответ модели -------------------------------------")
print(get_completion(PROMPT, prefill=PREFILL))
Возможное решение

Решение из оригинала — один промпт целиком (его можно разложить по элементам). Цитата в примере оставлена по-английски, потому что и сам кодекс английский.

Ты — опытный налоговый бухгалтер. Твоя задача — отвечать на вопросы пользователей по предоставленной справочной документации.
Вот материал, по которому нужно ответить на вопрос пользователя:
<docs>
{TAX_CODE}
</docs>
Вот пример того, как отвечать:
<example>
<question>
Кого считают «квалифицированным» сотрудником?
</question>
<answer>
<quotes>For purposes of this subsection—
(A)In general
The term "qualified employee" means any individual who—
(i)is not an excluded employee, and
(ii)agrees in the election made under this subsection to meet such requirements as are determined by the Secretary to be necessary to ensure that the withholding requirements of the corporation under chapter 24 with respect to the qualified stock are met.</quotes>
<answer>Согласно предоставленной документации, «квалифицированный сотрудник» — это человек, который:
1. Не является «исключённым сотрудником» в смысле документации.
2. Соглашается выполнять требования, которые установит Министр (Secretary), чтобы корпорация могла выполнить обязанности по удержанию налога согласно главе 24 в отношении квалифицированных акций.</answer>
</example>
Сначала собери в теги <quotes></quotes> цитаты, которые помогают ответить на вопрос пользователя. Если подходящих цитат нет, напиши «подходящих цитат не найдено».
Затем вставь два пустых абзаца и ответь на вопрос пользователя в тегах <answer></answer>. Отвечай, только если уверен, что цитаты в тегах <quotes></quotes> подтверждают твой ответ. Если нет, скажи пользователю, что, к сожалению, у тебя недостаточно информации для ответа.
Вот вопрос пользователя: {QUESTION}

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

Советуем прочитать содержимое переменной (здесь это CODE), чтобы понимать, с чем предстоит работать модели. Обязательно подставьте CODE прямо в промпт через f-строку.

######################################## ВХОДНЫЕ ПЕРЕМЕННЫЕ ########################################
# Входная переменная — код, который модели нужно прочитать и помочь пользователю исправить
CODE = """
# Функция печатает обратные числа (мультипликативные обратные)
def print_multiplicative_inverses(x, n):
for i in range(n):
print(x / i)
"""
######################################## ЭЛЕМЕНТЫ ПРОМПТА ########################################
# Пояснения к каждому элементу — в примере с карьерным консультантом
TASK_CONTEXT = "" # Элемент 2: контекст задачи
TONE_CONTEXT = "" # Элемент 3: тон
TASK_DESCRIPTION = "" # Элемент 4: подробное описание задачи и правила
EXAMPLES = "" # Элемент 5: примеры
INPUT_DATA = "" # Элемент 6: входные данные для обработки
IMMEDIATE_TASK = "" # Элемент 7: непосредственная задача
PRECOGNITION = "" # Элемент 8: предварительное обдумывание
OUTPUT_FORMATTING = "" # Элемент 9: формат ответа
PREFILL = "" # Элемент 10: prefill
######################################## СБОРКА ЭЛЕМЕНТОВ ########################################
# Порядок как в примере с карьерным консультантом
PROMPT = ""
for part in [TASK_CONTEXT, TONE_CONTEXT, TASK_DESCRIPTION, EXAMPLES, INPUT_DATA,
IMMEDIATE_TASK, PRECOGNITION, OUTPUT_FORMATTING]:
if part: # пустые элементы пропускаем
PROMPT += ("\n\n" if PROMPT else "") + part
print("---------------------------- Полный промпт после подстановки переменных ----------------------------")
print("ХОД ПОЛЬЗОВАТЕЛЯ")
print(PROMPT)
print("\nХОД АССИСТЕНТА")
print(PREFILL)
print("\n------------------------------------- Ответ модели -------------------------------------")
print(get_completion(PROMPT, prefill=PREFILL))
Возможное решение
Ты — Codebot, услужливый ИИ-помощник, который находит проблемы в коде и предлагает возможные улучшения.
Веди себя как наставник, работающий по методу Сократа: помогай пользователю учиться.
Тебе дадут код пользователя. Сделай следующее:
1. Найди все проблемы в коде. Помести каждую проблему в отдельные теги <issues>.
2. Предложи пользователю самому написать исправленную версию кода.
Вот пример:
<example>
<code>
def calculate_circle_area(radius):
return (3.14 * radius) ** 2
</code>
<issues>
<issue>
В квадрат возводится и 3.14, хотя возводить нужно только радиус
</issue>
<response>
Почти правильно, но есть проблема с порядком действий. Запишите формулу площади круга и внимательно посмотрите на скобки в своём коде.
</response>
</example>
Вот код, который нужно разобрать:
<code>
{CODE}
</code>
Найди проблемы и напиши ответ в стиле наставника-сократика. Не помогай пользователю слишком сильно! Вместо этого лишь направь его, чтобы он сам нашёл правильное решение.
Помести каждую проблему в теги <issue>, а итоговый ответ — в теги <response>.

Если вы прошли все упражнения, вы теперь в верхних 0,1 % заклинателей LLM. Элита!

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

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

Если хотите увидеть больше примеров хороших промптов для вдохновения:

  • изучите примеры промптов, готовых к продакшену, в кукбуке Anthropic;
  • прочитайте руководство по промптам (в оригинале ссылка ведёт на старый адрес docs.anthropic.com);
  • загляните в библиотеку промптов за идеями;
  • попробуйте экспериментальный метапромпт: модель сама пишет для вас шаблоны промптов;
  • задавайте вопросы на Discord-сервере;
  • разберитесь в параметрах API — temperature, max_tokens и других (на нашем сайте — урок 8.1.1);
  • если тянет к науке, почитайте статьи о промпт-инжиниринге;
  • тренируйтесь: стройте промпты, чтобы модель делала то, что интересно именно вам.

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

# Промпт
PROMPT = "Напиши оду в форме сонета прекрасному студенту, который только что закончил курс по промпт-инжинирингу."
# Ответ модели
print(get_completion(PROMPT))

Говорят: «Писать — значит переписывать». Оказывается, модель часто может сделать ответ точнее, если её об этом попросить!

Попросить модель «подумать ещё раз» можно многими способами. Те формулировки, которыми естественно попросить человека перепроверить работу, обычно работают и с моделью. (Больше примеров того, когда и как строить цепочки промптов, — в документации Anthropic о цепочках промптов.)

В приложении функция вызова модели переписана так, чтобы принимать список сообщений произвольной длины. В pet.py это уже есть — функция chat(messages, system_prompt="", stop_sequences=None).

В этом примере мы просим модель придумать десять слов… но одно или несколько из них — ненастоящие.

from pet import chat
# Первый промпт
first_user = "Назови десять слов, которые оканчиваются ровно на буквы «аб»."
# Массив сообщений для API
messages = [
{"role": "user", "content": first_user},
]
# Сохраняем и печатаем ответ модели
first_response = chat(messages)
print(first_response)

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

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

second_user = "Пожалуйста, замени все «слова», которых на самом деле не существует, настоящими словами."
# Массив сообщений для API
messages = [
{"role": "user", "content": first_user},
{"role": "assistant", "content": first_response},
{"role": "user", "content": second_user},
]
# Печатаем ответ модели
print("------------------------ Полный массив сообщений после подстановки переменных ------------------------")
print(messages)
print("\n------------------------------------- Ответ модели -------------------------------------")
print(chat(messages))

Но исправляет ли модель ответ только потому, что мы так сказали? Что, если ответ с самого начала был верным? Не потеряет ли модель уверенность? Здесь мы подставили на место first_response правильный ответ и снова попросили всё перепроверить.

first_user = "Назови десять слов, которые оканчиваются ровно на буквы «аб»."
first_response = """Вот 10 слов, которые оканчиваются на «аб»:
1. Краб
2. Раб
3. Араб
4. Штаб
5. Масштаб
6. Баобаб
7. Ухаб
8. Прораб
9. Кебаб
10. Генштаб"""
second_user = "Пожалуйста, замени все «слова», которых на самом деле не существует, настоящими словами."
# Массив сообщений для API
messages = [
{"role": "user", "content": first_user},
{"role": "assistant", "content": first_response},
{"role": "user", "content": second_user},
]
# Печатаем ответ модели
print("------------------------ Полный массив сообщений после подстановки переменных ------------------------")
print(messages)
print("\n------------------------------------- Ответ модели -------------------------------------")
print(chat(messages))

Если запустить ячейку выше несколько раз, вы заметите: чаще всего модель оставляет слова как есть, но иногда всё же меняет их, хотя все они правильные. Как с этим бороться? Вспомним главу 8: дадим модели выход! Попробуем ещё раз.

second_user = ("Пожалуйста, замени все «слова», которых на самом деле не существует, настоящими словами. "
"Если все слова настоящие, верни исходный список.")
# Массив сообщений для API (first_user и first_response — из прошлой ячейки)
messages = [
{"role": "user", "content": first_user},
{"role": "assistant", "content": first_response},
{"role": "user", "content": second_user},
]
# Печатаем ответ модели
print("------------------------ Полный массив сообщений после подстановки переменных ------------------------")
print(messages)
print("\n------------------------------------- Ответ модели -------------------------------------")
print(chat(messages))

Запустите этот код несколько раз: модель теперь гораздо твёрже стоит на своём.

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

Сначала получим первую версию рассказа.

# Первый промпт
first_user = "Напиши рассказ из трёх предложений о девочке, которая любит бегать."
# Массив сообщений для API
messages = [
{"role": "user", "content": first_user},
]
# Сохраняем и печатаем ответ модели
first_response = chat(messages)
print(first_response)

Теперь попросим модель улучшить черновик.

second_user = "Сделай рассказ лучше."
# Массив сообщений для API
messages = [
{"role": "user", "content": first_user},
{"role": "assistant", "content": first_response},
{"role": "user", "content": second_user},
]
# Печатаем ответ модели
print("------------------------ Полный массив сообщений после подстановки переменных ------------------------")
print(messages)
print("\n------------------------------------- Ответ модели -------------------------------------")
print(chat(messages))

Такая подстановка — очень мощный инструмент. Мы уже подставляли в промпты списки, слова, прошлые ответы модели и многое другое. С помощью подстановки можно делать и то, что называют «вызовом функций» (function calling): попросить модель вызвать некоторую функцию, взять результат этой функции и попросить модель сделать с ним что-то ещё. Это работает как любая другая подстановка. Подробнее — в следующем разделе приложения.

Ниже ещё один пример: берём результат одного вызова модели и вставляем его в другой, более длинный вызов. Начнём с первого промпта (на этот раз с prefill).

first_user = """Найди все имена в тексте ниже:
«Привет, Женя. Это я, Ира. Звоню насчёт вечеринки, которую Дима устраивает завтра. Катя сказала, что придёт, и, думаю, Мила тоже будет»."""
prefill = "<names>"
# Массив сообщений для API: последний ход — начало ответа модели
messages = [
{"role": "user", "content": first_user},
{"role": "assistant", "content": prefill},
]
# Сохраняем и печатаем ответ модели
first_response = chat(messages)
print("------------------------ Полный массив сообщений после подстановки переменных ------------------------")
print(messages)
print("\n------------------------------------- Ответ модели -------------------------------------")
print(first_response)

Передадим этот список имён в следующий промпт.

second_user = "Расставь имена из списка по алфавиту."
# Массив сообщений для API: prefill и продолжение склеиваем в один ответ ассистента
messages = [
{"role": "user", "content": first_user},
{"role": "assistant", "content": prefill + "\n" + first_response},
{"role": "user", "content": second_user},
]
# Печатаем ответ модели
print("------------------------ Полный массив сообщений после подстановки переменных ------------------------")
print(messages)
print("\n------------------------------------- Ответ модели -------------------------------------")
print(chat(messages))

Теперь, когда вы познакомились с цепочками промптов, переходите к разделу 10.2 — там вызов функций собирается из цепочки промптов.

Поначалу это может показаться сложным, но вызов инструментов (tool use), он же вызов функций (function calling), на самом деле довольно прост! Вы уже владеете всеми навыками, которые нужны для его реализации: это всего лишь сочетание подстановки и цепочек промптов.

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

  1. модель выводит имя инструмента и аргументы, с которыми хочет его вызвать;
  2. генерация останавливается, пока инструмент выполняется;
  3. мы снова вызываем модель, добавив к диалогу результаты инструмента.

Вызов функций полезен тем, что расширяет возможности модели и позволяет ей справляться с гораздо более сложными многошаговыми задачами. Примеры функций, которые можно дать модели:

  • калькулятор;
  • счётчик слов;
  • запросы к базе данных SQL и получение данных;
  • API погоды.

Чтобы модель вызывала инструменты, нужно сочетание двух элементов:

  1. системного промпта, в котором мы объясняем модели саму идею вызова инструментов и подробно описываем доступные ей инструменты;
  2. управляющей логики, которая организует и выполняет запросы модели на вызов инструментов.

В оригинале функция вызова модели здесь переключена на Claude 3 Sonnet, «которая в целом лучше справляется с вызовом инструментов», и умеет принимать стоп-последовательности. С локальной моделью берите ту же chat() из pet.py, а если qwen3:8b путается в формате — попробуйте модель покрупнее, например qwen3:14b.

Этот урок учит формату вызова инструментов, который актуален сейчас. Однако в ближайшем будущем мы обновим и улучшим эту функциональность, в том числе:

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

Чтобы включить вызов инструментов, начнём с системного промпта. В этом специальном системном промпте мы рассказываем модели:

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

Вот первая часть системного промпта: она объясняет модели, что такое вызов инструментов. Эта часть одинакова для всех случаев, когда вы просите модель вызывать инструменты. Структура вызова (<function_calls> [...] </function_calls>) — та, которой Claude специально обучали, поэтому для Claude авторы советуют её не менять.

from pet import chat
system_prompt_tools_general_explanation = """У тебя есть набор функций, которыми можно пользоваться, чтобы ответить на вопрос пользователя. В том числе есть доступ к изолированной вычислительной среде (песочнице). Сейчас ты НЕ можешь просматривать файлы или обращаться к внешним ресурсам иначе как через вызов функций ниже.
Чтобы вызвать одну или несколько функций, напиши в ответе пользователю блок "<function_calls>" такого вида:
<function_calls>
<invoke name="$FUNCTION_NAME">
<parameter name="$PARAMETER_NAME">$PARAMETER_VALUE</parameter>
...
</invoke>
<invoke name="$FUNCTION_NAME2">
...
</invoke>
</function_calls>
Строковые и скалярные параметры указывай как есть, а списки и объекты — в формате JSON. Учти, что пробелы в строковых значениях не обрезаются. Вывод не обязан быть корректным XML: он разбирается регулярными выражениями.
Результаты и/или ошибки появятся в следующем блоке "<function_results>" и останутся частью твоего ответа пользователю.
После этого можешь продолжить ответ, отреагировать на ошибки или, если нужно, снова вызвать функции.
Если после вызова функций блок "<function_results>" НЕ появился, вероятно, вызов оформлен неправильно и не распознан."""

Вот вторая часть системного промпта: она задаёт инструменты, доступные модели в этой конкретной ситуации. В примере мы дадим модели калькулятор с тремя параметрами: двумя операндами и оператором.

Затем склеим две части системного промпта.

system_prompt_tools_specific_tools = """Вот доступные тебе функции:
<tools>
<tool_description>
<tool_name>calculator</tool_name>
<description>
Калькулятор для простой арифметики.
Поддерживает сложение, вычитание, умножение
</description>
<parameters>
<parameter>
<name>first_operand</name>
<type>int</type>
<description>Первый операнд (до оператора)</description>
</parameter>
<parameter>
<name>second_operand</name>
<type>int</type>
<description>Второй операнд (после оператора)</description>
</parameter>
<parameter>
<name>operator</name>
<type>str</type>
<description>Выполняемая операция. Должна быть одной из: +, -, *, /</description>
</parameter>
</parameters>
</tool_description>
</tools>
"""
system_prompt = system_prompt_tools_general_explanation + system_prompt_tools_specific_tools

Теперь зададим модели вопрос, для которого нужен инструмент calculator. Добавим </function_calls> в стоп-последовательности (stop_sequences), чтобы понять, вызвала ли модель функцию и когда.

multiplication_message = {
"role": "user",
"content": "Умножь 1984135 на 9343116",
}
stop_sequences = ["</function_calls>"]
# Ответ модели: генерация остановится на закрывающем теге вызова
function_calling_response = chat([multiplication_message], system_prompt=system_prompt,
stop_sequences=stop_sequences)
print(function_calling_response)

Теперь можно извлечь параметры из вызова функции и действительно выполнить функцию за модель.

Сначала напишем код самой функции.

def do_pairwise_arithmetic(num1, num2, operation):
if operation == "+":
return num1 + num2
elif operation == "-":
return num1 - num2
elif operation == "*":
return num1 * num2
elif operation == "/":
return num1 / num2
else:
return "Ошибка: операция не поддерживается."

Затем извлечём параметры из ответа модели. Если все параметры на месте, запустим калькулятор.

def find_parameter(message, parameter_name):
"""Достаёт значение параметра из блока <invoke>; если параметра нет — возвращает None."""
parameter_start_string = f'name="{parameter_name}">'
start = message.find(parameter_start_string)
if start == -1:
return None
start += len(parameter_start_string)
end = message.find("<", start) # значение тянется до следующего тега
return message[start:end].strip()
first_operand = find_parameter(function_calling_response, "first_operand")
second_operand = find_parameter(function_calling_response, "second_operand")
operator = find_parameter(function_calling_response, "operator")
if first_operand and second_operand and operator:
result = do_pairwise_arithmetic(int(first_operand), int(second_operand), operator)
print("---------------- РЕЗУЛЬТАТ ----------------")
print(f"{result:,}") # 18,538,003,464,660

Результат есть. Теперь его нужно правильно оформить, чтобы, когда мы передадим его обратно, модель поняла, к какому инструменту он относится. Для этого есть заданный формат, который Claude обучали распознавать:

<function_results>
<result>
<tool_name>{TOOL_NAME}</tool_name>
<stdout>
{TOOL_RESULT}
</stdout>
</result>
</function_results>

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

def construct_successful_function_run_injection_prompt(invoke_results):
"""Оформляет результаты вызовов в блок <function_results>, который понимает модель."""
constructed_prompt = (
"<function_results>\n"
+ "\n".join(
f"<result>\n<tool_name>{res['tool_name']}</tool_name>\n<stdout>\n{res['tool_result']}\n</stdout>\n</result>"
for res in invoke_results
) + "\n</function_results>"
)
return constructed_prompt
formatted_results = [{
"tool_name": "do_pairwise_arithmetic",
"tool_result": result,
}]
function_results = construct_successful_function_run_injection_prompt(formatted_results)
print(function_results)

Осталось отправить результат модели: добавить его в ту же цепочку сообщений, что и раньше, — и готово!

# Стоп-последовательность не попадает в ответ — вернём закрывающий тег сами
full_first_response = function_calling_response + "</function_calls>"
# Собираем диалог целиком
messages = [
multiplication_message,
{"role": "assistant", "content": full_first_response},
{"role": "user", "content": function_results},
]
# Ответ модели
final_response = chat(messages, system_prompt=system_prompt, stop_sequences=stop_sequences)
print("------------- ИТОГОВЫЙ ОТВЕТ -------------")
print(final_response)

Поздравляем: вы прошли полную цепочку вызова инструмента от начала до конца!

А что, если задать модели вопрос, для которого инструмент вообще не нужен?

non_multiplication_message = {
"role": "user",
"content": "Назови столицу Франции.",
}
stop_sequences = ["</function_calls>"]
# Ответ модели
function_calling_response = chat([non_multiplication_message], system_prompt=system_prompt,
stop_sequences=stop_sequences)
print(function_calling_response)

Получилось! Как видите, модель поняла, что функцию вызывать не нужно.

В этом упражнении вы напишете промпт для вызова инструментов, чтобы читать и записывать данные в самую маленькую в мире «базу данных». Вот инициализированная база — на самом деле это просто словарь.

db = {
"users": [
{"id": 1, "name": "Алиса", "email": "alice@example.com"},
{"id": 2, "name": "Борис", "email": "boris@example.com"},
{"id": 3, "name": "Вика", "email": "vika@example.com"},
],
"products": [
{"id": 1, "name": "Виджет", "price": 9.99},
{"id": 2, "name": "Гаджет", "price": 14.99},
{"id": 3, "name": "Штуковина", "price": 19.99},
],
}

А вот код функций, которые читают из базы и пишут в неё.

def get_user(user_id):
"""Возвращает пользователя по id или None."""
for user in db["users"]:
if user["id"] == user_id:
return user
return None
def get_product(product_id):
"""Возвращает товар по id или None."""
for product in db["products"]:
if product["id"] == product_id:
return product
return None
def add_user(name, email):
"""Добавляет пользователя; id — следующий по порядку."""
user_id = len(db["users"]) + 1
user = {"id": user_id, "name": name, "email": email}
db["users"].append(user)
return user
def add_product(name, price):
"""Добавляет товар; id — следующий по порядку."""
product_id = len(db["products"]) + 1
product = {"id": product_id, "name": name, "price": price}
db["products"].append(product)
return product

Чтобы решить упражнение, начните с системного промпта по образцу system_prompt_tools_specific_tools выше. Обязательно укажите имя и описание каждого инструмента, а для каждого параметра каждой функции — имя, тип и описание. Заготовка ниже.

system_prompt_tools_specific_tools_sql = """
"""
system_prompt = system_prompt_tools_general_explanation + system_prompt_tools_specific_tools_sql

Когда будете готовы, проверьте свой системный промпт на примерах ниже. Просто запустите ячейку!

examples = [
"Добавь в базу пользователя по имени Дебора.",
"Добавь в базу товар под названием «Фиговина».",
"Скажи, как зовут пользователя 2.",
"Скажи, как называется товар 3.",
]
for example in examples:
message = {"role": "user", "content": example}
# Получаем и печатаем ответ модели
function_calling_response = chat([message], system_prompt=system_prompt, stop_sequences=stop_sequences)
print(example, "\n----------\n\n", function_calling_response, "\n*********\n*********\n*********\n\n")

Если всё сделано правильно, в ответах модель корректно вызовет функции add_user, add_product, get_user и get_product.

Для дополнительных баллов добавьте ячейки с кодом, который разбирает параметры. Затем вызовите функции с параметрами, которые дала модель, и посмотрите, как изменилась «база данных» после вызова.

Возможное решение
system_prompt = system_prompt_tools_general_explanation + """Вот доступные тебе функции:
<tools>
<tool_description>
<tool_name>get_user</tool_name>
<description>
Получает пользователя из базы данных по его ID.
</description>
<parameters>
<parameter>
<name>user_id</name>
<type>int</type>
<description>ID пользователя, которого нужно получить.</description>
</parameter>
</parameters>
</tool_description>
<tool_description>
<tool_name>get_product</tool_name>
<description>
Получает товар из базы данных по его ID.
</description>
<parameters>
<parameter>
<name>product_id</name>
<type>int</type>
<description>ID товара, который нужно получить.</description>
</parameter>
</parameters>
</tool_description>
<tool_description>
<tool_name>add_user</tool_name>
<description>
Добавляет в базу данных нового пользователя.
</description>
<parameters>
<parameter>
<name>name</name>
<type>str</type>
<description>Имя пользователя.</description>
</parameter>
<parameter>
<name>email</name>
<type>str</type>
<description>Адрес электронной почты пользователя.</description>
</parameter>
</parameters>
</tool_description>
<tool_description>
<tool_name>add_product</tool_name>
<description>
Добавляет в базу данных новый товар.
</description>
<parameters>
<parameter>
<name>name</name>
<type>str</type>
<description>Название товара.</description>
</parameter>
<parameter>
<name>price</name>
<type>float</type>
<description>Цена товара.</description>
</parameter>
</parameters>
</tool_description>
</tools>
"""

Поздравляем: вы освоили вызов инструментов и функций! Если хотите узнать больше о поиске и RAG, переходите к последнему разделу приложения.

Знаете ли вы, что модель может искать для вас в Википедии? Она умеет находить и извлекать статьи, а потом их можно резюмировать и сводить вместе, писать на их основе новый текст и многое другое. И не только Википедию! Искать можно и по вашим собственным документам — хранятся ли они как обычный текст или как эмбеддинги в векторной базе данных.

Посмотрите примеры RAG в кукбуке: там показано, как дополнять знания модели и повышать точность и релевантность ответов данными из векторных баз, Википедии, интернета и других источников. Там же можно узнать, как пользоваться эмбеддингами и инструментами векторных баз данных.

Если вам интересны продвинутые архитектуры RAG с Claude, загляните в слайды технической презентации Claude 3 об архитектурах RAG.

  1. Решите упражнения глав 8–9. Для 9.1 и 9.2 сохраните свой промпт в файл и запишите, какие из десяти элементов оказались лишними. Это и есть «сначала добиться работы, потом сократить».
  2. Измерьте галлюцинации. Составьте 10 вопросов о несуществующем (альбомы, книги, законы) и 10 о реальном. Прогоните их с промптом без «выхода» и с ним. Посчитайте две ошибки: сколько выдумок на несуществующем и сколько ложных «не знаю» на реальном. «Выход» обычно снижает первое и немного повышает второе — это и есть цена приёма.
  3. Цитаты против длинного контекста. Повторите пример с Matterport при OLLAMA_CONTEXT_LENGTH=4096 и 16384. В первом случае часть документа отрежется молча — посмотрите, как это меняет ответ. Сколько токенов занял промпт, видно в поле usage.prompt_tokens ответа API.
  4. Тот же калькулятор на настоящем tool calling. Перепишите пример 10.2 через параметр tools OpenAI-совместимого API (урок 8.3.1). Сравните объём кода: что делает за вас сервер?
EnglishРусский
hallucinationгаллюцинация (выдуманный или неподкреплённый ответ)
give Claude an outдать модели выход (разрешить сказать «не знаю»)
distractor informationотвлекающая информация
evidence / relevant quotesдоказательства / подходящие цитаты
scratchpadчерновик (место для рассуждений в ответе)
task context / tone contextконтекст задачи / тон
immediate taskнепосредственная задача
prompt chainingцепочка промптов
tool use / function callingвызов инструментов / вызов функций
stop sequenceстоп-последовательность
function resultsрезультаты функций
search & retrievalпоиск и извлечение
embeddings / vector databaseэмбеддинги / векторная база данных
RAG (retrieval-augmented generation)генерация с дополнением извлечёнными данными