Читать книгу Создание AI-агента на LangChain и OpenRouter - - Страница 5
Глава 5. Агент с долгосрочной памятью на базе чата
ОглавлениеСоздание агентов, способных поддерживать непрерывный, осмысленный диалог с пользователем, является одной из ключевых задач при разработке современных интерактивных систем. Обычные чат-боты, работающие в режиме «запрос-ответ», не учитывают предыдущий контекст, что делает взаимодействие фрагментарным и неестественным. Для решения этой проблемы в LangChain реализован механизм, объединяющий под управлением агента компоненты памяти, планирования и выполнения действий. Разделение логики позволяет агенту анализировать историю диалога, извлекать из неё релевантные факты и строить стратегию дальнейшего взаимодействия. Агент с долгосрочной памятью на базе чата (Long-Term Memory Chat Agent) – это архитектура, в которой Large Language Model (LLM) выступает в роли ядра мышления, а специальные хранилища данных (векторные базы или базы ключ-значение) обеспечивают доступ к накопленным знаниям. В этой главе мы детально разберем этапы проектирования, реализации и отладки такого агента с использованием openrouter.ai и LangChain.
Предварительные требования и настройка окружения
Перед началом работы необходимо подготовить инфраструктуру. Поскольку агент будет взаимодействовать с внешними API и базами данных, важно корректно настроить переменные окружения и установить необходимые библиотеки. Основной язык программирования – Python (версия 3.9 и выше). Для работы с LLM через openrouter.ai мы будем использовать стандартные HTTP-клиенты или интеграции LangChain, для работы с памятью – специализированные модули, для хранения эмбеддингов – библиотеку Chromadb или FAISS.
Установка зависимостей:
pip install langchain langchain-openai chromadb faiss-cpu openai python-dotenv
Ключевые компоненты архитектуры
Любой агент в LangChain состоит из трех основных блоков:
1. Модель (LLM): «Мозг» агента. Мы будем использовать модели, доступные через openrouter.ai. Это позволяет выбирать самые мощные модели (например, GPT-4, Claude, Llama) без привязки к одному провайдеру.
2. Память (Memory): Хранилище состояния. Она делится на краткосрочную (контекст текущего окна диалога) и долгосрочную (архив знаний и фактов).
3. Инструменты (Tools): Специальные функции, расширяющие возможности агента (поиск информации, выполнение расчетов, доступ к API).
Наша задача – связать эти компоненты так, чтобы агент мог «вспоминать» прошлые взаимодействия, даже если они были давно, и использовать эти воспоминания для текущих ответов.
Подключение модели через OpenRouter
OpenRouter предоставляет единый API для множества моделей. Для работы в LangChain нам требуется создать клиент OpenAI-совместимого формата, указав базовый URL OpenRouter.
Пример конфигурации модели:
import os
from langchain_openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
# Указываем URL API OpenRouter вместо стандартного OpenAI
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
BASE_URL = "https://openrouter.ai/api/v1"
llm = OpenAI(
base_url=BASE_URL,
api_key=OPENROUTER_API_KEY,
model="openai/gpt-3.5-turbo", # Можно выбрать любую доступную модель
temperature=0.7
)
Важно помнить, что при использовании агентов с памятью, особенно с длинной историей, стоимость токенов может расти. Поэтому настройка параметров (temperature, max_tokens) критична для оптимизации.
Краткосрочная память как основа диалога
Прежде чем переходить к «архиву» знаний, агент должен помнить текущую беседу. В LangChain для этого используется класс ConversationBufferMemory. Он хранит всю историю сообщений в виде списка, который передается в LLM как часть промпта. Это базовый слой, необходимый для любого чат-агента.
Однако для полноценного агента нам нужно сохранять не только фразы, но и контекст выполнения. Поэтому мы используем ConversationBufferMemory в связке с версией для агентов.
Пример инициализации памяти:
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
Память должна быть доступна агенту, но передаваться она должна в специальном формате, который агент понимает как историю диалога.
Долгосрочная память: Vector Store и Retrieval
Самая интересная часть – реализация долгосрочной памяти. Хранить весь текст диалога в контексте модели невозможно из-за ограничений длины контекстного окна (context window). Решение – хранить информацию во внешней базе и извлекать её только тогда, когда это необходимо.
Для этого используется механизм Retrieval-Augmented Generation (RAG). Идея следующая:
1. Когда пользователь сообщает важный факт (например, «Я живу в Новосибирске»), агент парсит эту информацию и сохраняет её в векторную базу данных.
2. При последующих вопросах (например, «Какая погода в моем городе?») агент ищет в базе релевантные записи по семантическому сходству (векторный поиск).
3. Найденная информация подставляется в контекст запроса к LLM.
Для реализации нам понадобятся:
Векторная база (Chroma).
Функция вложения (Embeddings). Мы будем использовать встроенные эмбеддинги OpenAI (или доступные через OpenRouter, но обычно для эмбеддингов используют отдельные модели, например, text-embedding-ada-002).
Пример настройки векторного хранилища:
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# Инициализация эмбеддингов
embeddings = OpenAIEmbeddings(
base_url=BASE_URL,
api_key=OPENROUTER_API_KEY,
model="text-embedding-ada-002" # Эта модель обычно доступна через OpenRouter или OpenAI
)
# Создание или загрузка базы
vectorstore = Chroma(
collection_name="agent_memory",
embedding_function=embeddings,
persist_directory="./chroma_db"
)
Чтобы агент мог использовать эту базу как память, нам нужен специальный инструмент (Tool), который будет выполнять операции сохранения и поиска. Но сначала нужно определить, как именно агент решает, когда сохранять информацию.
Стратегия сохранения информации (Memory Management)
Агент не должен сохранять каждую фразу подряд, иначе база заполнится мусором. Нам нужен механизм принятия решения о важности информации. В LangChain это реализуется через концепцию «Планировщика» (Planner) или нативно через промпт агента.
Мы можем реализовать простую стратегию:
1. Использовать LLM для классификации текущего сообщения пользователя.
2. Если сообщение содержит факты (имя, место, предпочтения), LLM форматирует его в структурированную запись (JSON) и отправляет в инструмент сохранения.
3. Если сообщение – просто реплика в диалоге, оно идет только в краткосрочную память (контекст).
Для реализации этой логики создадим кастомный инструмент (Custom Tool).
Кастомный инструмент долгосрочной памяти
Класс инструмента в LangChain наследуется от BaseTool. Нам нужно два инструмента:
1. `SaveMemory`: сохраняет контекст в Chroma.
2. `LoadMemory`: извлекает контекст по запросу.
Реализация инструмента сохранения:
from langchain.tools import BaseTool
from typing import Optional, Type
from pydantic import BaseModel, Field
class MemoryInput(BaseModel):
content: str = Field(description="Содержание для сохранения в долговременную память")
category: Optional[str] = Field(default="general", description="Категория воспоминания")
class SaveMemoryTool(BaseTool):
name = "save_memory"
description = "Используй этот инструмент, чтобы сохранить важную информацию о пользователе или факты для долгосрочного использования. Содержание должно быть кратким и информативным."
args_schema: Type[BaseModel] = MemoryInput
def _run(self, content: str, category: str = "general") -> str:
# Сохранение в векторную базу
try:
vectorstore.add_texts(
texts=[content],
metadatas=[{"category": category}]
)
return "Информация успешно сохранена в памяти."
except Exception as e:
return f"Ошибка при сохранении: {e}"
Нам также понадобится инструмент для поиска. Однако, чтобы не перегружать контекст, мы можем сделать «умный» поиск: агент передает запрос, а инструмент возвращает релевантные фрагменты.
class SearchInput(BaseModel):
query: str = Field(description="Поисковый запрос для извлечения воспоминаний")
class LoadMemoryTool(BaseTool):
name = "retrieve_memory"
description = "Используй этот инструмент, чтобы найти сохраненную информацию о пользователе или прошлых событиях, если это необходимо для ответа. Запрос должен содержать ключевые слова или суть того, что вы ищете."
args_schema: Type[BaseModel] = SearchInput
def _run(self, query: str) -> str:
try:
docs = vectorstore.similarity_search(query, k=3)
if not docs:
return "Нет релевантных воспоминаний."
# Форматируем результаты
result_text = "\n".join([doc.page_content for doc in docs])
return f"Найденные воспоминания:\n{result_text}"
except Exception as e:
return f"Ошибка при поиске: {e}"
Сборка агента: Планирование и Очередь
Теперь, когда у нас есть инструменты памяти, краткосрочная память и модель, мы можем собрать агента. Но есть нюанс. Стандартный агент LangChain (AgentExecutor) выполняет инструменты последовательно, пока не получит финальный ответ. Нам же нужна более сложная логика:
1. Анализ текущего ввода.
2. Решение: нужно ли сохранять информацию прямо сейчас? (Это можно делать как отдельный шаг агента, так и через промпт).
3. Решение: нужно ли извлекать информацию из долгосрочной памяти?
Для упрощения мы можем использовать `AgentExecutor` с системой промптов, которая заставляет агента думать о памяти.
Однако более эффективный подход для "сценария чата" – использование `ConversationalAgent` (или `ToolCallingAgent`) с настроенной памятью.
Мы будем использовать `initialize_agent` из LangChain.
Важно: Агенту нужно передать инструменты, LLM, память и промпт.
Однако стандартная память `ConversationBufferMemory` в `initialize_agent` работает только как история сообщений. Чтобы встроить векторную базу, мы должны немного изменить подход.
Вместо того чтобы заставлять агента постоянно вызывать инструменты поиска, мы можем использовать подход `RetrievalQA` внутри логики агента, или сделать так, чтобы агент сам вызывал инструменты.
В нашем случае мы сделаем так: агент видит инструменты `SaveMemoryTool` и `LoadMemoryTool`. В промпте мы четко пропишем, когда их использовать.
Но для чистоты эксперимента и чтобы избежать бесконечных циклов вызова инструментов, лучше использовать `create_react_agent` (или `create_tool_calling_agent`), где инструменты памяти будут доступны, но агент будет использовать их разумно.
Промпт агента
Ключевой элемент – промпт. Он должен содержать инструкции:
* Ты – полезный ассистент с долгосрочной памятью.
* Ты можешь сохранять факты о пользователе, используя инструмент `save_memory`.
* Если пользователь спрашивает о том, что могло быть сказано ранее, используй `retrieve_memory`.
* История диалога доступна в переменной {chat_history}.
* {agent_scratchpad} – место для мыслей и инструментов.
Создадим промпт:
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import AgentExecutor, create_tool_calling_agent
# Базовый системный промпт
system_prompt = """
Вы – полезный ИИ-ассистент с расширенной долгосрочной памятью.
Ваши возможности:
1. Вы должны поддерживать непрерывный диалог.
2. Если пользователь делится личной информацией (имя, предпочтения, факты о жизни), вы ОБЯЗАНЫ использовать инструмент `save_memory` для сохранения этой информации.
3. Если пользователь задает вопрос, который требует знания фактов из прошлого (которые могли быть сказаны давно или в другом контексте), вы ОБЯЗАНЫ сначала использовать `retrieve_memory` для поиска релевантной информации.
4. Не изобретайте факты, опирайтесь только на историю диалога и результаты из памяти.
5. Всегда отвечай на русском языке.
"""
# Шаблон для агента (Tool Calling Agent)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
MessagesPlaceholder("chat_history"),
("user", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
Инициализация агента
Теперь собираем всё вместе.
from langchain.agents import AgentExecutor
from langchain_openai import ChatOpenAI # Используем ChatOpenAI для поддержки функций/инструментов
# Используем ChatOpenAI для агента, так как он лучше работает с инструментами
# Но можно оставить и OpenAI, если переделать под существующие агенты.
# Для надежности используем ChatOpenAI через OpenRouter
llm_chat = ChatOpenAI(
base_url=BASE_URL,
api_key=OPENROUTER_API_KEY,
model="openai/gpt-3.5-turbo", # Модель с поддержкой function calling
temperature=0.3
)
# Наши инструменты
tools = [SaveMemoryTool(), LoadMemoryTool()]
# Создаем агента
agent = create_tool_calling_agent(llm_chat, tools, prompt)
# Исполнитель агента
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
# Инициализация памяти (для хранения истории чата)
chat_memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
# Тестовый цикл взаимодействия
def run_chat():
print("Агент запущен. Введите 'exit' для выхода.")
while True:
user_input = input("Вы: ")
if user_input.lower() == 'exit':
break
# Получаем историю из памяти
history = chat_memory.load_memory_variables({})
chat_history = history.get("chat_history", [])
# Запуск агента
try:
response = agent_executor.invoke({
"input": user_input,
"chat_history": chat_history
})
answer = response['output']
print(f"Агент: {answer}")
# Сохраняем оба сообщения в память (историю диалога)
chat_memory.save_context({"input": user_input}, {"output": answer})
except Exception as e:
print(f"Ошибка: {e}")
if __name__ == "__main__":
run_chat()
Детальное объяснение работы цикла:
1. Пользователь вводит сообщение.
2. Мы загружаем историю диалога из `ConversationBufferMemory`.
3. Передаем в `agent_executor`: ввод, историю и доступные инструменты.
4. Агент анализирует запрос. Если он решает, что нужно сохранить информацию, он вызывает `SaveMemoryTool`. Мы видим это в `verbose` режиме. Этот вызов сохраняет данные в Chroma.
5. Если он решает, что нужно вспомнить прошлое, он вызывает `LoadMemoryTool`. Данные из Chroma возвращаются в контекст.
6. Агент генерирует финальный ответ на основе текущего ввода, истории и полученных из памяти фактов.
7. Ответ выводится пользователю и сохраняется в `ConversationBufferMemory`.
Продвинутое использование: Инструмент с реверсивной записью
Вышеприведенный код работает, но у него есть недостаток: агент должен явно вызывать инструменты `save_memory` и `retrieve_memory` через шаги мышления (Thought -> Action -> Observation). Это замедляет общение. Мы можем автоматизировать этот процесс, используя концепцию «Трансформеров памяти» (Memory Transformers) или «Скрытых действий» (Hidden Actions).
В LangChain есть возможность создать инструмент, который вызывается автоматически как часть промпта (через скрытый мыслительный процесс), но для простоты и прозрачности мы оставим явный вызов.
Однако мы можем улучшить инструмент `SaveMemoryTool`. Вместо простого сохранения строки, он может принимать JSON-структуру, чтобы обогатить метаданные.
Улучшенный инструмент сохранения:
class EnhancedSaveMemoryTool(BaseTool):
name = "save_memory_enhanced"
description = "Сохраняет информацию в долговременную память. Используй этот инструмент для сохранения фактов, имен, мест, предпочтений пользователя. Сохраняй только суть, убирая лишние слова."
args_schema: Type[BaseModel] = MemoryInput
def _run(self, content: str, category: str = "general") -> str:
# Здесь можно добавить векторизацию и сохранение
try:
# Допустим, мы хотим сохранять исходный контекст
vectorstore.add_texts(
texts=[f"Факт: {content}"],
metadatas=[{"source": "long_term_memory", "category": category}]
)
return "Факт сохранен."
except Exception as e:
return f"Ошибка сохранения: {e}"
Решение проблем и отладка
При работе с агентами памяти часто возникают следующие проблемы:
1. **Забывание контекста инструмента**: Агент вызывает инструмент, получает ответ (например, "Факт сохранен"), но забывает об этом в генерации финального ответа.
* *Решение*: В `agent_scratchpad` (истории мыслей) сохраняются все шаги. Модель видит, что инструмент был вызван, и обычно учитывает это. Если нет – можно добавить инструкцию в промпт: "После выполнения инструмента сделай вывод на основе Observation".