Создание чат-бота с помощью LLM и LangChain

В этой статье я расскажу об опыте создания чат-ботов в компании Dash. Наша цель  —  всесторонне изучить LangChain, охватив широкий спектр общих тем. Кроме того, я поделюсь личным опытом создания чат-бота, легко интегрируемого с пользовательскими API. Такой чат-бот может стать помощником и предоставлять полезные рекомендации на основе данных пользователя интернет-магазина.

Контекст

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

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

Чат-бот считывает swagger-файл API и исходя из вопроса пользователя решает, какую конечную точку использовать для получения данных из бэкенда приложения, чтобы проанализировать эти данные и предоставить идеальный ответ.

Структура бэкенда чат-бота

Содержание

  • Что такое LLM?
  • Что такое Langchain?
  • Зачем нужен Langchain?
  • Агенты и цепочки Langchain.
  • Память Langchain.
  • Инструменты Langchain.
  • Построение чат-бота.
  • Планировщик и чат-агенты.
  • Агент как инструмент.

Что такое LLM?

LLM (Large Language Models, большие языковые модели)  —  это усовершенствованные модели искусственного интеллекта, предназначенные для обработки и генерации человекоподобных текстов путем анализа и изучения закономерностей из огромных массивов текстовых данных. Эти модели характеризуются способностью понимать, генерировать и манипулировать языком в связном и контекстуально релевантном виде.

Одной из возможностей LLM является способность решать различные задачи, связанные с языком.

  • Генерация текста. Создание связного и контекстуально релевантного текста на основе заданного промпта.
  • Перевод. Перевод текста с одного языка на другой. 
  • Резюмирование текста. Сокращение больших отрывков текста до кратких резюме.
  • Ответы на вопрос. Понимание вопросов и предоставление релевантных ответов на основе обучающих данных.
  • Завершение текста. Прогнозирование следующего слова или фразы в предложении.
  • Понимание языка. Осмысление сути текста и его контекста, даже если речь идет о сложных предложениях.

LLM бывают бесплатными и платными. Вот самые популярные.

  1. GPT-3 (Generative Pre-trained Transformer, генеративный предварительно обученный трансформер). GPT-3, разработанный компанией OpenAI, является одной из наиболее известных и мощных LLM. Он имеет 175 млрд параметров, поэтому является весьма универсальным вариантом для решения широкого круга задач обработки естественного языка. GPT-3 может генерировать связный и контекстуально релевантный текст, отвечать на вопросы, выполнять перевод с одного языка на другой, моделировать диалоги и т. д.
  2. Falcon LLM. Модель разработана Институтом технологических инноваций Абу-Даби и занимает первое место в мире среди ИИ-моделей с открытым исходным кодом. Успех модели Falcon, обученной на 1 трлн токенов с 40 млрд параметров, объясняется высоким качеством извлечения данных с помощью уникального конвейера. Результирующий корпус данных RefinedWeb предлагает уточненный контент, а тонко настроенная архитектура Falcon достигает впечатляющей эффективности и превосходит GPT-3, потребляя меньше вычислительных ресурсов как в процессе обучения, так и при работе над выводами.
  3. LLaMA (Large Language Model Meta AI, большая языковая модель от ИИ-разработчиков Meta). Представляет собой коллекцию современных фундаментальных языковых моделей с количеством параметров от 7 млрд до 65 млрд. Эти модели имеют меньший размер при исключительной производительности, что позволяет значительно сократить вычислительные мощности и ресурсы, необходимые для экспериментов с новыми методологиями, проверки работ других специалистов и изучения инновационных вариантов использования.

В своем чат-боте мы решили использовать GPT LLM.

Что такое LanghCain?

LangChain  —  это мощный инструмент, который можно использовать для работы с большими языковыми моделями. LLM способны эффективно выполнять многие задачи и являются довольно абстрактными по своей природе. Это означает, что иногда они не в состоянии дать конкретные ответы на вопросы или решить задачи, требующие глубоких знаний/опыта в определенной области.

Допустим, вы хотите, чтобы LLM отвечала на вопросы, связанные с медициной или юриспруденцией. LLM ответит на общие вопросы по данным тематикам, но, вероятно, не сможет дать более подробные или уточненные ответы, требующие специальных знаний/опыта.

Рост популярности LangChain был довольно быстрым и, несомненно, впечатляющим!

Почему Langchain?

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

LangChain  —  это фреймворк, который позволяет разработчикам создавать агентов, способных осмысливать проблемы, разбивая их на более мелкие подзадачи. С помощью LangChain можно вводить контекст и память для выполнения задания, создавая промежуточные шаги и объединяя команды в цепочки.

LLM имеют свои ограничения. Чтобы обойти их, LangChain предлагает полезный подход, при котором корпус текстовых данных предварительно обрабатывается путем разбиения на фрагменты или резюме, встраивания их в векторное пространство и поиска похожих фрагментов при задании вопроса. Такая схема предварительной обработки, сбора информации в реальном времени и взаимодействия с LLM является общей и может быть использована в других сценариях, например в написании кода и семантическом поиске.

Таким образом, при использовании API OpenAI напрямую, нам пришлось бы создавать все промпты с нуля, разрабатывать собственное решение для обхода ограничений, самостоятельно создавать средства обобщения и запоминания. Зачем это делать, если все эти средства для работы с промптами и ограничениями предлагает LangChain?

Цепочки и агенты Langchain

Цепочки и агенты LangChain

Цепочки являются жизненно важным ядром LangChain. Эти логические связи с одной или несколькими LLM являются основой функциональности LangChain. Различные виды цепочек  —  от простых до сложных  —  используются в зависимости от необходимости и задействованных LLM.

Чтобы у вас сложилось общее представление о цепочках LangChain, создадим простую цепочку.

from langchain.prompts import PromptTemplate
from langchain.llms import HuggingFace
from langchain.chains import LLMChain

prompt = PromptTemplate(
input_variables=["city"],
template="Describe a perfect day in {city}?",
)

llm = HuggingFace(
model_name="gpt-neo-2.7B",
temperature=0.9)

llmchain = LLMChain(llm=llm, prompt=prompt)
llmchain.run("Paris")

Сначала создаем шаблон промптов и добавляем переменную chain. Взяв ее из вопроса человека, передаем в шаблон, а затем отправляем это сообщение в LLM.

Агенты в LangChain представляют собой инновационный способ динамического вызова LLM на основе пользовательского ввода. Они имеют доступ не только к LLM, но и к набору инструментов (таких как Google Search, Python REPL, математический калькулятор, погодные API и т. д.), которые могут взаимодействовать с внешним миром.

from langchain.agents import initialize_agent, AgentType, load_tools
from langchain.llms import OpenAI

llm = OpenAI(temperature=0)
tools = load_tools(["pal-math"], llm=llm)

agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

response = agent.run("If my age is half of my dad's age and he is going to be 60 next year, what is my current age?")
print(response) # Вывод: "Сейчас мне 29,5 лет".

В данном случае агент использует инструмент PAL-MATH и OpenAI LLM для решения математической задачи, встроенной в промпт на естественном языке. Здесь демонстрируется практический случай, когда агент приносит дополнительную пользу, интерпретируя промпт, выбирая правильный инструмент для решения задачи и возвращая в итоге осмысленный ответ.

Память Langchain

Память может пригодиться в тех случаях, когда необходимо запомнить элементы из предыдущих вводов. Например, если сначала спросить “Кто такой Альберт Эйнштейн?”, а затем задать вопрос “Кто был его наставником?”, то диалоговая память поможет агенту вспомнить, что “его” относится к “Альберту Эйнштейну”.

Реализация шагов памяти

  1. Добавление памяти в промпт:
prefix = """
Your an market specialities to help user to analyze their data and assist them
"""

suffix = """Begin!"

{chat_history}
Question: {input}
{agent_scratchpad}"""

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

2. Краткое содержание чата:

messages=[]
chat_history = ChatMessageHistory(messages=messages)
memory = ConversationSummaryBufferMemory(llm=llm, chat_memory=chat_history, input_key="input", memory_key="chat_history")

3. Передача краткого содержания чата агенту:

llm_chain = LLMChain(llm=llm, prompt=prompt, memory=memory)

agent = ZeroShotAgent(
llm_chain=llm_chain,
tools=tools,
verbose=True,
handle_parsing_errors=self._handle_error,
prompt=prompt
)

Инструменты LanghCain и пользовательские инструменты

Инструменты  —  это функции, используемые агентами для взаимодействия с миром. Этими инструментами могут быть общие утилиты (например, поиск), другие цепочки или даже другие агенты.

Примеры инструментов:

  • gmail;
  • google_places;
  • google_search;
  • google_serper;
  • graphql;
  • взаимодействие с человеком;
  • jira;
  • json.

Как создать пользовательский инструмент

GPT-3 был обучен только на данных, собранных до 2021 года, и не знает фактической (текущей) даты. Поэтому мы создали инструмент, с помощью которого агент сможет узнать фактическую дату.

  1. Создание пользовательского инструмента:
from langchain.callbacks.manager import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun,
)
from typing import Any, Optional
from langchain.tools.base import BaseTool
from datetime import datetime

class GetTodayDate(BaseTool):
name = "get_today_date"
description = "you can use this tool to get today date so u can use it to calc dates before or after"

def _run(
self, query, run_manager: Optional[CallbackManagerForToolRun] = None
) -> str:
"""Use the tool."""
return datetime.today().strftime('%Y-%m-%d')
async def _arun(
self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
) -> str:
"""Use the tool asynchronously."""
raise NotImplementedError("custom_search does not support async")

Инструмент позволяет агенту использовать его (инструмент) для получения фактической даты.

2. Передача агенту:

from langchain.agents import initialize_agent

tools = [GetTodayDate()]

# инициализация агента с помощью инструментов
agent = initialize_agent(
agent='chat-conversational-react-description',
tools=tools,
llm=llm,
verbose=True,
max_iterations=3,
early_stopping_method='generate',
memory=conversational_memory
)

Теперь, если спросить агента о фактической дате или задать вопрос, связанный с фактической сегодняшней датой, он сможет назвать ее.

Другой пример из реальной практики. Если нужно внедрить агента в API приложения, необходимо отправить временные метки с вашим фильтром. Когда пользователь скажет что-то вроде “в прошлом месяце”, агент должен получить сегодняшнюю дату, чтобы понять: дата, о которой говорит пользователь, была в прошлом месяце. Без этого инструмента будет выдана информация о прошлом месяце 2021 года.

Создание чат-бота

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

Первое, что пришло нам в голову,  —  использовать агента-планировщика.

Это агент, который пошагово выполняет следующие операции:

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

Ограничения данного подхода:

  1. Для ответа на вопрос требуется много времени, поскольку необходимо вызвать 3–4 конечные точки.
  2. Не позволяет вести дружескую беседу с пользователем, когда он задает обычные вопросы.
  3. Невозможно использовать пользовательские инструменты.
  4. Невозможно создать для агента память.

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

def create_custom_planner(
api_spec: ReducedOpenAPISpec,
requests_wrapper: RequestsWrapper,
llm: BaseLanguageModel,
shared_memory: Optional[ReadOnlySharedMemory] = None,
memory: Optional[Any] = None,
callback_manager: Optional[BaseCallbackManager] = None,
verbose: bool = True,
agent_executor_kwargs: Optional[Dict[str, Any]] = None,
tools: Optional[List] = [],
**kwargs: Dict[str, Any],
) -> AgentExecutor:

tools = [
_create_api_planner_tool(api_spec, llm),
_create_api_controller_tool(api_spec, requests_wrapper, llm),
*tools,
]

prompt = PromptTemplate(
template=API_ORCHESTRATOR_PROMPT,
input_variables=["input", "chat_history", "agent_scratchpad"],
partial_variables={
"tool_names": ", ".join([tool.name for tool in tools]),
"tool_descriptions": "\n".join(
[f"{tool.name}: {tool.description}" for tool in tools]
),
},
)

agent = ZeroShotAgent(
llm_chain=LLMChain(llm=llm, prompt=prompt, memory=memory),
allowed_tools=[tool.name for tool in tools],
**kwargs,
)

return AgentExecutor.from_agent_and_tools(
agent=agent,
tools=tools,
callback_manager=callback_manager,
verbose=verbose,
memory=memory,
**(agent_executor_kwargs or {}),
)

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

Вот два подхода к решению этой задачи:

  • Агенты-планировщики, использующие чат в качестве инструмента.
  • Чат-агенты, использующие планировщика в качестве инструмента.

Лучший подход, основанный на нашем анализе этих двух вариантов, заключается в выборе чат-агента и использовании планировщика в качестве инструмента. Однако как уже отмечалось, планировщику требуется время, чтобы решить, какие API можно использовать, а затем вызвать 3–4 конечные точки.

Таким образом, мы оказались перед еще одной дилеммой:

  1. Отредактировать промпт для пользовательского планировщика, чтобы ограничить количество запросов одним или двумя вызовами.
  2. Создать все конечные точки в виде инструментов и заставить чат-агента решать, какой из этих инструментов применить.

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

Итоговый код чат-бота:

import aiohttp
from dotenv import dotenv_values
import langchain
from langchain.agents.agent_toolkits.openapi.spec import reduce_openapi_spec
from langchain.requests import RequestsWrapper
from langchain.chat_models import ChatOpenAI
from langchain.agents import initialize_agent, Tool, AgentType
from langchain.agents.agent_toolkits.openapi import planner
# import json
from pathlib import Path
from pprint import pprint
import yaml
from langchain import LLMMathChain
from pydantic import BaseModel, Field
from langchain.output_parsers import PydanticOutputParser
from langchain.memory import (
ConversationBufferMemory,
ReadOnlySharedMemory,
ConversationSummaryMemory,
ConversationBufferWindowMemory,
ConversationSummaryBufferMemory,
ConversationEntityMemory,
ReadOnlySharedMemory
)
from langchain.chains import ConversationChain
# from langchain.chains.conversation.memory import ConversationBufferWindowMemory
from langchain.llms import OpenAI
from langchain.chains.conversation.prompt import ENTITY_MEMORY_CONVERSATION_TEMPLATE
from langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage
)
from langchain.memory.chat_message_histories.in_memory import ChatMessageHistory
from langchain.schema.language_model import BaseLanguageModel
from typing import Any, Callable, Dict, List, Optional
from langchain.agents.conversational_chat.base import ConversationalChatAgent
from langchain.agents.agent import AgentExecutor
from langchain.prompts.prompt import PromptTemplate
from langchain.agents import ZeroShotAgent
from langchain.chains.llm import LLMChain
from langchain.agents.agent_toolkits.openapi import planner
from custom_planner import create_openapi_agent

config = dotenv_values(".env")
print(langchain.__version__)

class CalculatorInput(BaseModel):
question: str = Field()
class ChatBot:
api_url = ""
login_access_token = ""
with open("part_doc.yml") as f:
api_data = yaml.load(f, Loader=yaml.Loader)
def __init__(self, email, password):
self.email = email
self.password = password
async def login(self):
login_data = {
"email": self.email,
"password": self.password
}
async with aiohttp.ClientSession() as session:
async with session.post(self.api_url+"/auth/token", data=login_data) as response:
response_data = await response.json()
self.login_access_token = f'Bearer {response_data["access"]}'

def _handle_error(error) -> str:
return str(error)[:50]

def ask_api_questions(self, question):
llm = ChatOpenAI(openai_api_key=config.get('OPENAI_API_KEY'), temperature=0.0, model="gpt-4")
openai_api_spec = reduce_openapi_spec(self.api_data)
headers = {
"Authorization": self.login_access_token,
"Content-Type": "application/json"
}
requests_wrapper = RequestsWrapper(headers=headers)


messages = [
HumanMessage(content="Hey I am mohammed"),
AIMessage(content="Hey mohammed, how can I help u?"),
]
tools=[]
llm_math_chain = LLMMathChain(llm=llm, verbose=True)

tools.append(
Tool.from_function(
func=llm_math_chain.run,
name="Calculator",
description="useful for when you need to answer questions about math",
args_schema=CalculatorInput
# coroutine= ... <- you can specify an async method if desired as well
)
)

def _create_planner_tool(llm, shared_memory):

def _create_planner_agent(question: str):
agent = create_openapi_agent(
openai_api_spec,
requests_wrapper,
llm,
handle_parsing_errors=self._handle_error,
shared_memory=shared_memory,
)
return agent.run(input=question)


return Tool(
name="api_planner_controller",
func=_create_planner_agent,
description="Can be used to execute a plan of API calls and adjust the API call to retrieve the correct data for Kickbite",
)

prefix = """
You are an AI assistant developed by xxx.

"""

suffix = """Begin!"
{chat_history}
Question: {input}
{agent_scratchpad}"""


prompt = ZeroShotAgent.create_prompt(
tools,
prefix=prefix,
suffix=suffix,
input_variables=["input", "chat_history", "agent_scratchpad"]
)
chat_history = ChatMessageHistory(messages=messages)
window_memory = ConversationSummaryBufferMemory(llm=llm, chat_memory=chat_history, input_key="input", memory_key="chat_history")
shared_memory = ReadOnlySharedMemory(memory=window_memory)
tools.append(_create_planner_tool(llm, shared_memory))

llm_chain = LLMChain(llm=llm, prompt=prompt, memory=window_memory)
agent = ZeroShotAgent(
llm_chain=llm_chain,
tools=tools,
verbose=True,
handle_parsing_errors="Check your output and make sure it conforms!",
prompt=prompt
)

agent_executor = AgentExecutor.from_agent_and_tools(
agent=agent,
tools=tools,
memory=window_memory
)

agent_executor.verbose = True
output = agent_executor.run(input=question)
print("LOL! 🦜🔗")
pprint(output)

Заключение

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Dash ICT: Build Chatbot with LLMs and LangChain 🦜🔗

Предыдущая статьяКак создать первый проект по инженерии данных: инкрементный подход. Часть 2
Следующая статьяСоздание приложения для отслеживания фильмов с помощью HTML, CSS и JavaScript