Разработка чат-бота
Раздел содержит пример разработки чат-бота на основе LLM. Этот чат-бот может вести беседу и запоминать предыдущие действия пользователя.
В примере рассмотрен чат-бот, который для ведения беседы использует только языковую модель. Также существуют другие способы разработки чат-ботов, которые могут вас заинтересовать:
- Агенты — чат-боты, которые могут выполнять действия.
Здесь вы найдете базовую информацию о разработке чат-ботов, которая будет полезна при работе с приведенными выше разделами. Но, если нужно, вы можете сразу начать с более сложных чат-ботов.
Подготовка к разработке
Jupyter-блокноты
Это руководство, как и большинство других в документации, использует Jupyter-блокноты. Они отлично подходят для изучения работы с LLM-системами, так как предоставляют интерактивную среду для работы с руководствами и позволяют работать с непредвиденными ситуациями: недоступностью API, нетипичным выводом и другими.
Подробнее об установке jupyter — в официальной документации.
Установка
Для установки пакетов, которые понадобятся для работы с примером, выполните команду:
pip install langchain-gigachat langgraph
Подробнее об установке — в разделе Установка.
Быстрый старт
Сначала ознакомьтесь, как использовать языковую модель отдельно. Хотя GigaChain поддерживает различные языковые модели, основным преимуществом библиотеки является возможность работы с моделями GigaChat.
# | output: false
# | echo: false
from langchain_gigachat.chat_models import GigaChat
model = GigaChat(
credentials="ключ_аптворизации",
scope="GIGACHAT_API_PERS",
model="GigaChat-Max",
verify_ssl_certs=False,
)
Объект GigaChat принимает параметры:
credentials
— ключ авторизации для обмена сообщениями с GigaChat API. О том как получить ключ авторизации — в разделе Быстрый старт.scope
— версия API, к которой будет выполнен запрос. Необязательный параметр. Возможные значения:GIGACHAT_API_PERS
— версия API для физических лиц;GIGACHAT_API_B2B
— версия API для ИП и юрлиц при работе по предоплате.GIGACHAT_API_CORP
— версия API для ИП и юрлиц при работе по постоплате.
По умолчанию запросы передаются в версию для физических лиц.
model
— необязательный параметр, в котором можно явно задать модель GigaChat.По умолчанию запросы передаются в модель GigaChat Lite.
streaming
— необязательный параметр, который включает потоковую передачу токенов.verify_ssl_certs
— необязательный параметр, с помощью которого можно отключить проверку сертификатов НУЦ Минцифры.
Попробуйте обратиться к модели напрямую.
Объекты ChatModel
— это экземпляры Runnable-интерфейса GigaChain.
Все экземпляры Runnable предоставляют стандартный интерфейс для взаимодействия.
Так, чтобы обратиться к модели, достаточно вызвать метод .invoke()
со списком сообщений.
from langchain_core.messages import HumanMessage
model.invoke([HumanMessage(content="Привет! Меня зовут Вася")])
AIMessage(content='Привет, Вася! Рад знакомству. Чем могу быть полезен?', additional_kwargs={}, response_metadata={'token_usage': Usage(prompt_tokens=19, completion_tokens=16, total_tokens=35), 'model_name': 'GigaChat-Max:1.0.26.20', 'finish_reason': 'stop'}, id='run-9a83bbd9-b2ff-42c4-908b-3318448cc9a0-0')
Сама модель не сохраняет информацию о состоянии. В этом можно убедиться, если задать ей дополнительный вопрос:
model.invoke([HumanMessage(content="Как меня зовут?")])
AIMessage(content='К сожалению, я не могу знать вашего имени, если вы его не указали. Если хотите, можете написать свое имя, и я буду обращаться к вам так.', additional_kwargs={}, response_metadata={'token_usage': Usage(prompt_tokens=16, completion_tokens=33, total_tokens=49), 'model_name': 'GigaChat-Max:1.0.26.20', 'finish_reason': 'stop'}, id='run-401243a7-252f-4f38-9401-cb315ea8d58f-0')
Чтобы обойти это ограничение, передайте всю историю разговора в модель:
from langchain_core.messages import AIMessage
model.invoke(
[
HumanMessage(content="Привет! Меня зовут Вася"),
AIMessage(
content="Здравствуйте, Вася! Я генеративная языковая модель от Сбера. Готова ответить на ваши вопросы."
),
HumanMessage(content="Как меня зовут?"),
]
)
AIMessage(content='Ваше имя — Вася.', additional_kwargs={}, response_metadata={'token_usage': Usage(prompt_tokens=60, completion_tokens=8, total_tokens=68), 'model_name': 'GigaChat-Max:1.0.26.20', 'finish_reason': 'stop'}, id='run-a3a735e6-4de2-4b57-99fb-60cf6d622588-0')
Теперь модель может гораздо точнее отвечать на дополнительные вопросы.
Работа с историей сообщений позволяет чат-боту вести разговор. Ниже показано, как ее реализовать.
Персистенция сообщений
В LangGraph есть встроенный уровень персистентности, который помогает создавать чат-приложения с памятью.
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, MessagesState, StateGraph
# Инизиализируйте граф
workflow = StateGraph(state_schema=MessagesState)
# Определите функцию, которая вызывает модель
def call_model(state: MessagesState):
response = model.invoke(state["messages"])
return {"messages": response}
# Задайте вершину графа
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
# Добавьте память
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
Добавьте параметры конфигурации, которые всегда нужно передавать в runnable.
Конфигурация не является частью входных данных, но предоставляет полезную информацию.
В примере используется параметр thread_id
.
config = {"configurable": {"thread_id": "abc123"}}
Параметр позволяет реализовать несколько отдельных разговоров в одном приложении.
query = "Привет! Меня зовут Вася."
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print() # вывод содержит все сообщения в состоянии
================================== Ai Message ==================================
Привет, Вася! Рад знакомству. Чем могу быть полезен?
query = "Как меня зовут?"
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================
Тебя зовут Вася.
Теперь чат-бот запоминает информацию.
Если вы укажете новый thread_id
, то увидите, что чат-бот начал разговор заново.
config = {"configurable": {"thread_id": "abc234"}}
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================
К сожалению, я не могу знать вашего имени, если вы его не указали. Если хотите, можете написать свое имя, и я буду обращаться к вам так.
Но вы всегда можете вернуться к изначальному разговору, так как он сохраняется в базе данных.
config = {"configurable": {"thread_id": "abc123"}}
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================
Твое имя — Вася.
Таким образом чат-бот может вести разговор с несколькими пользователями одновременно.
Чтобы поддержать асинхронность, добавьте в вершину call_model
асинхронную функцию и используйте метод .ainvoke
для запуска приложения:
# Асинхронная функция для использования в вершине графа:
async def call_model(state: MessagesState):
response = await model.ainvoke(state["messages"])
return {"messages": response}
# Граф определяется как прежде:
workflow = StateGraph(state_schema=MessagesState)
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
app = workflow.compile(checkpointer=MemorySaver())
# Асинхронный вызов:
output = await app.ainvoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
Таким образом, ваш чат-бот сможет разговаривать с разными пользователями.
Примеры ниже показывают, как использовать шаблон промпта, чтобы расширить и персонализировать данные, которые сохраняет чат-бот.
Шаблоны промптов
Шаблоны промптов помогают преобразовать необработанные данные пользователя в формат, с которым может работать LLM. В данном случае необработанные данные — это просто сообщение, которое вы передаете в LLM. Попробуйте развить сообщение.
Сначала добавьте системное сообщение со своими инструкциями (но все еще принимая сообщения в качестве входных данных). А затем добавьте больше входных данных, помимо сообщений.
Для добавления системного сообщения создайте экземпляр ChatPromptTemplate
.
Чтобы передать все сообщения, используйте MessagesPlaceholder
.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Ты личный помощник. Старайся как можно лучше помочь пользователю.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
chain = prompt | model
Теперь можно добавить шаблон в приложение.
workflow = StateGraph(state_schema=MessagesState)
def call_model(state: MessagesState):
chain = prompt | model
response = chain.invoke(state)
return {"messages": response}
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
Приложение запускается как прежде.
config = {"configurable": {"thread_id": "abc345"}}
query = "Привет! Меня зовут Кира."
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================
Здравствуйте, Кира! Рад знакомству с вами. Чем могу быть полезен?
query = "Как меня зовут?"
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================
Ваше имя — Кира.
Усложните полученный промпт. Предположим, что шаблон промпта теперь выглядит примерно так:
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Ты — личный ассистент. Старайся как можно лучше помочь пользователю. Отвечай на все вопросы пользователя на следующем языке: {language}. Не называй своего имени.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
Выше в промпт добавлена новая переменная language
.
Приложение теперь принимает два параметра messages
и language
.
Обновите состояние приложения, чтобы отразить это.
from typing import Sequence
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
from typing_extensions import Annotated, TypedDict
class State(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
language: str
workflow = StateGraph(state_schema=State)
def call_model(state: State):
chain = prompt | model
response = chain.invoke(state)
return {"messages": [response]}
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "abc456"}}
query = "Привет! Меня зовут Вася."
language = "Spanish"
input_messages = [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages, "language": language},
config,
)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================
¡Hola, Vasya! ¿Cómo estás hoy?
Обратите внимание, что состояние приложения сохраняется.
Таким образом, если вы не хотите ничего менять, повторно передавать параметр language
не нужно.
query = "Как меня зовут?"
input_messages = [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages},
config,
)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================
Te llamas Vasya.
Управление историей разговоров
При разработке чат-бота рано или поздно вам понадобится управлять историей разговоров. Как правило, это продиктовано размером контекстного окна модели, в котором перестанут помещаться все сообщения разговора. Чтобы избежать этого, нужно добавить этап, на котором будет ограничиваться размер передаваемых сообщений.
При этом, этот этап должен срабатывать до шаблона промпта, но после загрузки предыдущих сообщений из Message History.
Для этого перед промптом вы можете добавить шаг, который изменяет содержимое messages
соответствующим образом, а затем обернуть полученную цепочку в класс Message History.
В LangChain есть несколько встроенных инструментов для управления списком сообщений.
В примере ниже для уменьшения количества сообщений, которые нужно передать в модель, используется функция trim_messages
.
Она позволяет указать, сколько токенов в истории сообщений нужно сохранить, а также задать другие параметры. Например, всегда ли нужно сохранять системное сообщение и разрешать ли частичные сообщения.
from langchain_core.messages import SystemMessage, trim_messages
trimmer = trim_messages(
max_tokens=35,
strategy="last",
token_counter=model,
include_system=True,
allow_partial=False,
start_on="human",
)
messages = [
SystemMessage(content="Ты — личный ассистент"),
HumanMessage(content="Привет! Меня зовут Вася"),
AIMessage(content="Привет!"),
HumanMessage(content="Я люблю шоколадное мороженое"),
AIMessage(content="Здорово"),
HumanMessage(content="Сколько будет 2 + 2"),
AIMessage(content="4"),
HumanMessage(content="Спасибо"),
AIMessage(content="Пожалуйста!"),
HumanMessage(content="Тебе нравится наш разговор?"),
AIMessage(content="Да!"),
]
trimmer.invoke(messages)
[SystemMessage(content='Ты — личный ассистент'),
HumanMessage(content='Сколько будет 2 + 2'),
AIMessage(content='4'),
HumanMessage(content='Спасибо'),
AIMessage(content='Пожалуйста!'),
HumanMessage(content='Тебе нравится наш разговор?'),
AIMessage(content='Да!')]
Потоковая передача
Потоковая передача ответа — важная составляющая хорошего пользовательского опыта при разработке чат-ботов. Языковые модели могут долго генерировать ответ, поэтому для повышения отзывчивости большинство приложений обрабатывает и отображает каждый токен по мере его генерации. Это позволяет пользователю видеть прогресс.
Для работы с потоковой передачей все цепочки предоставляют метод .stream
, в том числе и те, что используют историю сообщений.
Используйте его, чтобы получить потоковый ответ.
config = {"configurable": {"thread_id": "abc789"}}
query = "Привет! Я Вася. Расскажи историю из 100 слов"
language = "English"
input_messages = [HumanMessage(query)]
for chunk, metadata in app.stream(
{"messages": input_messages, "language": language},
config,
stream_mode="messages",
):
if isinstance(chunk, AIMessage): # Filter to just model responses
print(chunk.content, end="|")
Hello, Vasya!| Once upon a time, there was a little boy named Alex who lived in a small village.| One day, he found an old book hidden in the attic.| The book told stories of magical creatures and faraway lands.| Alex decided to go on an adventure to find these places.| He packed his bag and set off into the unknown.| Along the way, he met many interesting people and saw amazing things.| Finally, after months of traveling, he reached his destination - a beautiful castle surrounded by forests and mountains.| There, he discovered that magic truly existed!||
Смотрите также
- Агенты — чат-боты, которые могут выполнять действия.