В этой статье я расскажу об опыте создания чат-ботов в компании Dash. Наша цель — всесторонне изучить LangChain, охватив широкий спектр общих тем. Кроме того, я поделюсь личным опытом создания чат-бота, легко интегрируемого с пользовательскими API. Такой чат-бот может стать помощником и предоставлять полезные рекомендации на основе данных пользователя интернет-магазина.
Контекст
Сначала вы получите общее представление о созданном нами чат-боте, затем перейдем к деталям пошаговой разработки.
Этот ИИ-ассистент помогает пользователям анализировать данные интернет-магазина и дает советы, как его улучшить, куда вложить больше денег, чтобы повысить доход, и т. д.
Чат-бот считывает swagger-файл API и исходя из вопроса пользователя решает, какую конечную точку использовать для получения данных из бэкенда приложения, чтобы проанализировать эти данные и предоставить идеальный ответ.
Содержание
- Что такое LLM?
- Что такое Langchain?
- Зачем нужен Langchain?
- Агенты и цепочки Langchain.
- Память Langchain.
- Инструменты Langchain.
- Построение чат-бота.
- Планировщик и чат-агенты.
- Агент как инструмент.
Что такое LLM?
LLM (Large Language Models, большие языковые модели) — это усовершенствованные модели искусственного интеллекта, предназначенные для обработки и генерации человекоподобных текстов путем анализа и изучения закономерностей из огромных массивов текстовых данных. Эти модели характеризуются способностью понимать, генерировать и манипулировать языком в связном и контекстуально релевантном виде.
Одной из возможностей LLM является способность решать различные задачи, связанные с языком.
- Генерация текста. Создание связного и контекстуально релевантного текста на основе заданного промпта.
- Перевод. Перевод текста с одного языка на другой.
- Резюмирование текста. Сокращение больших отрывков текста до кратких резюме.
- Ответы на вопрос. Понимание вопросов и предоставление релевантных ответов на основе обучающих данных.
- Завершение текста. Прогнозирование следующего слова или фразы в предложении.
- Понимание языка. Осмысление сути текста и его контекста, даже если речь идет о сложных предложениях.
LLM бывают бесплатными и платными. Вот самые популярные.
- GPT-3 (Generative Pre-trained Transformer, генеративный предварительно обученный трансформер). GPT-3, разработанный компанией OpenAI, является одной из наиболее известных и мощных LLM. Он имеет 175 млрд параметров, поэтому является весьма универсальным вариантом для решения широкого круга задач обработки естественного языка. GPT-3 может генерировать связный и контекстуально релевантный текст, отвечать на вопросы, выполнять перевод с одного языка на другой, моделировать диалоги и т. д.
- Falcon LLM. Модель разработана Институтом технологических инноваций Абу-Даби и занимает первое место в мире среди ИИ-моделей с открытым исходным кодом. Успех модели Falcon, обученной на 1 трлн токенов с 40 млрд параметров, объясняется высоким качеством извлечения данных с помощью уникального конвейера. Результирующий корпус данных RefinedWeb предлагает уточненный контент, а тонко настроенная архитектура Falcon достигает впечатляющей эффективности и превосходит GPT-3, потребляя меньше вычислительных ресурсов как в процессе обучения, так и при работе над выводами.
- LLaMA (Large Language Model Meta AI, большая языковая модель от ИИ-разработчиков Meta). Представляет собой коллекцию современных фундаментальных языковых моделей с количеством параметров от 7 млрд до 65 млрд. Эти модели имеют меньший размер при исключительной производительности, что позволяет значительно сократить вычислительные мощности и ресурсы, необходимые для экспериментов с новыми методологиями, проверки работ других специалистов и изучения инновационных вариантов использования.
В своем чат-боте мы решили использовать GPT LLM.
Что такое LanghCain?
LangChain — это мощный инструмент, который можно использовать для работы с большими языковыми моделями. LLM способны эффективно выполнять многие задачи и являются довольно абстрактными по своей природе. Это означает, что иногда они не в состоянии дать конкретные ответы на вопросы или решить задачи, требующие глубоких знаний/опыта в определенной области.
Допустим, вы хотите, чтобы LLM отвечала на вопросы, связанные с медициной или юриспруденцией. LLM ответит на общие вопросы по данным тематикам, но, вероятно, не сможет дать более подробные или уточненные ответы, требующие специальных знаний/опыта.
Почему Langchain?
Мы решили использовать LanghСain, чтобы избежать низкоуровневого подхода и не применять напрямую API OpenAI.
LangChain — это фреймворк, который позволяет разработчикам создавать агентов, способных осмысливать проблемы, разбивая их на более мелкие подзадачи. С помощью LangChain можно вводить контекст и память для выполнения задания, создавая промежуточные шаги и объединяя команды в цепочки.
LLM имеют свои ограничения. Чтобы обойти их, LangChain предлагает полезный подход, при котором корпус текстовых данных предварительно обрабатывается путем разбиения на фрагменты или резюме, встраивания их в векторное пространство и поиска похожих фрагментов при задании вопроса. Такая схема предварительной обработки, сбора информации в реальном времени и взаимодействия с LLM является общей и может быть использована в других сценариях, например в написании кода и семантическом поиске.
Таким образом, при использовании API OpenAI напрямую, нам пришлось бы создавать все промпты с нуля, разрабатывать собственное решение для обхода ограничений, самостоятельно создавать средства обобщения и запоминания. Зачем это делать, если все эти средства для работы с промптами и ограничениями предлагает 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
Память может пригодиться в тех случаях, когда необходимо запомнить элементы из предыдущих вводов. Например, если сначала спросить “Кто такой Альберт Эйнштейн?”, а затем задать вопрос “Кто был его наставником?”, то диалоговая память поможет агенту вспомнить, что “его” относится к “Альберту Эйнштейну”.
Реализация шагов памяти
- Добавление памяти в промпт:
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 года, и не знает фактической (текущей) даты. Поэтому мы создали инструмент, с помощью которого агент сможет узнать фактическую дату.
- Создание пользовательского инструмента:
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 для анализа данных и дает пользователю лучший ответ.
Ограничения данного подхода:
- Для ответа на вопрос требуется много времени, поскольку необходимо вызвать 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 конечные точки.
Таким образом, мы оказались перед еще одной дилеммой:
- Отредактировать промпт для пользовательского планировщика, чтобы ограничить количество запросов одним или двумя вызовами.
- Создать все конечные точки в виде инструментов и заставить чат-агента решать, какой из этих инструментов применить.
Мы решили воспользоваться преимуществом задействования агента-планировщика в качестве инструмента и отредактировать его шаблон промптов, чтобы улучшить способ планирования и анализа пользовательских вводов на основе 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. В этой статье мы рассмотрели, как создать диалогового чат-бота на основе собственных наборов данных. Вы также узнали, как оптимизировать память для таких чат-ботов, чтобы можно было выбирать/обобщать диалоги для отправки в промпт, а не отправлять всю историю предыдущих диалогов в качестве части промпта.
Читайте также:
- LangChain + Streamlit + LlaMA: установка диалогового бота с ИИ на локальный компьютер
- Как автоматизировать создание контента для YouTube и блога с помощью LangChain и OpenAI
- 7 фреймворков для работы с LLM
Читайте нас в Telegram, VK и Дзен
Перевод статьи Dash ICT: Build Chatbot with LLMs and LangChain 🦜🔗