Создайте RAG с LangChainJS, Milvus и Strapi
Предоставление точных и релевантных ответов на пользовательские запросы имеет решающее значение при создании AI-приложений. Хотя традиционные чат-боты и AI-модели способны вести естественный диалог, им часто не хватает доступа к конкретной, актуальной информации. Их ответы формируются на основе обучающих данных, которые могут быть устаревшими или слишком общими для специализированных задач.
Retrieval Augmented Generation (RAG) решает это ограничение, объединяя генеративные возможности AI-моделей с пользовательской базой знаний на основе векторной базы данных, такой как Milvus. Вместо того чтобы полагаться исключительно на предобученные знания, RAG активно ищет по вашему контенту, чтобы найти релевантную информацию перед генерацией ответов. Такой подход гарантирует, что ответы будут как точными, так и контекстуально уместными.
Понимание RAG и векторного поиска
RAG устраняет разрыв между общими AI-ответами и специализированными знаниями, интегрируя механизм извлечения в процесс генерации. В отличие от традиционных языковых моделей, которые полагаются исключительно на предобученные знания, RAG сначала извлекает релевантную информацию из пользовательской базы знаний, чтобы дополнить ответы AI.
Например, когда AI-ассистента спрашивают о конкретной политике или процедуре, он не полагается только на память; вместо этого он выполняет поиск по релевантным документам, прежде чем предоставить ответ. Этот процесс извлечения работает на основе моделей эмбеддингов и векторных баз данных.
Теперь давайте посмотрим, как работает стандартный RAG.
Текстовые эмбеддинги: Текст преобразуется в числовые представления, или векторы, которые фиксируют его семантическое значение с помощью модели эмбеддингов. Например, фразы How do I return an item? и What's the refund process? могут использовать разные слова, но иметь схожий смысл, что отражается в их векторном сходстве.
Векторный поиск: Когда пользователь задает вопрос, запрос также преобразуется в вектор. Затем этот вектор сравнивается с другими, хранящимися в векторной базе данных, такой как Milvus, чтобы найти наиболее семантически похожие записи.
Генерация ответа: Извлеченная информация вместе с пользовательским запросом передается в большую языковую модель. Модель генерирует ответ, основанный на извлеченном контенте, гарантируя, что ответ будет релевантным и точным.
Эта комбинация извлечения и генерации гарантирует, что ответы AI будут контекстуально уместными и основанными на наиболее релевантных данных из базы знаний.
В следующих разделах мы создадим FAQ-систему на базе RAG, способную отвечать на вопросы о Milvus, используя базу знаний, управляемую в Strapi. Эта система интегрирует Milvus для векторного поиска, LangChain.js для координации рабочего процесса и языковые модели OpenAI для предоставления точных и контекстуальных ответов. Контент будет организован в Strapi, open-source headless CMS на Node.js, и запрашиваться в реальном времени, чтобы основывать ответы AI на ваших пользовательских данных.
Если вы хотите быстро ознакомиться, вот полный код и бэкенд Strapi.
Настройка среды разработки
Прежде чем писать код, нам нужно настроить три ключевых компонента, которые мы обсудили выше: Milvus для векторного хранилища, Strapi для управления контентом и API-ключ OpenAI для генерации эмбеддингов и ответов. Давайте настроим каждый из них.
Установка Milvus
Начнем с настройки Milvus, нашей векторной базы данных. Вы можете скачать предварительно настроенный файл Docker Compose вручную или с помощью wget. В терминале выполните:
wget https://github.com/milvus-io/milvus/releases/download/v2.0.2/milvus-standalone-docker-compose.yml -O docker-compose.yml
Когда файл конфигурации будет готов, запустите Milvus с помощью Docker:
sudo docker-compose up -d
Если вы используете Docker Compose V2, а не V1, используйте docker compose вместо docker-compose. Вы можете проверить свою версию, выполнив docker compose version.
Установка Strapi
Теперь настроим Strapi для управления содержимым нашей базы знаний. Создайте новый проект Strapi с помощью следующей команды:
npx create-strapi-app@latest my-project
Выберите SQLite в качестве базы данных при появлении запроса. После установки Strapi откроется в вашем браузере, где вы создадите свою учетную запись администратора.
В панели администратора Strapi создайте новый тип коллекции с именем MILVUS-KNOWLEDGEBASE. Добавьте два обязательных поля: поле Title с типом Text и поле Content с типом Rich text.
Рисунок — Добавление полей в коллекцию Strapi
Рисунок: Добавление полей в коллекцию Strapi
Эта структура поможет нам эффективно организовать записи нашей базы знаний.
После настройки коллекции перейдите в Content Manager, чтобы добавить пример содержимого.
Рисунок — Коллекция Strapi, заполненная данными
Рисунок: Коллекция Strapi, заполненная данными
Обязательно опубликуйте свои записи после их создания, неопубликованные записи не будут доступны нашему приложению.
После публикации записей нам нужно сделать содержимое доступным для системы RAG, настроив разрешения. Перейдите в Settings, затем Roles и выберите роль Public. Найдите свою коллекцию MILVUS-KNOWLEDGEBASE и включите разрешения find и findOne.
Получение вашего ключа API OpenAI
Последний элемент, который нам нужен, — это ключ API OpenAI для генерации эмбеддингов и ответов. Посетите платформу OpenAI и зарегистрируйтесь или войдите в систему. Перейдите в раздел ключей API и создайте новый секретный ключ. Скопируйте и сохраните ключ в безопасном месте, так как он понадобится нам позже, и вы сможете увидеть его только один раз.
Создание приложения React
Когда наши основные сервисы готовы, настроим приложение React, в котором будет размещена наша система RAG. Мы будем использовать инструмент create-react-app для создания нового проекта React и установки необходимых зависимостей.
Сначала создайте новое приложение React:
npx create-react-app rag-app
cd rag-app
Эта команда настраивает полноценный проект React со всеми необходимыми инструментами сборки и конфигурацией. Затем установите пакеты, необходимые для нашей системы RAG:
npm install @langchain/community @langchain/openai @zilliz/milvus2-sdk-node axios cors express langchain react-markdown
Вот что каждый пакет будет делать в нашем приложении:
@langchain/community: Предоставляет доступ к интеграциям с различными инструментами и сервисами. Мы будем использовать его для интеграции с векторным хранилищем Milvus.@langchain/openai: Обрабатывает взаимодействия с моделями OpenAI. Мы будем использовать его для создания эмбеддингов и генерации ответов с помощью GPT-3.5.@zilliz/milvus2-sdk-node: Официальный SDK Milvus для Node.js. Он позволяет нам напрямую взаимодействовать с нашим экземпляром Milvus для операций с векторами.axios: Это HTTP-клиент. Мы будем использовать его для получения содержимого из нашего Strapi API и выполнения запросов между нашим фронтендом и бэкендом.cors: Middleware для Express, которое включает Cross-Origin Resource Sharing. Это позволяет нашему фронтенду безопасно взаимодействовать с нашим сервером бэкенда.express: веб-фреймворк для Node.js. Мы будем использовать его для создания нашего backend API, который координирует взаимодействие между нашим frontend, Milvus и OpenAI.langchain: основная библиотека LangChain, которая помогает организовать наш рабочий процесс RAG, управляя потоком между извлечением контента и генерацией ответов.react-markdown: отображает Markdown-контент в наших React-компонентах. Мы будем использовать его для отображения форматированных ответов от нашего AI-ассистента.
Создание backend-сервера
Теперь, когда мы завершили настройку нашей среды разработки, давайте начнем создавать сервер, который будет обеспечивать работу нашей RAG-системы. Сервер будет выполнять несколько ключевых задач: получать контент из Strapi, преобразовывать текст в векторные эмбеддинги, хранить их в Milvus и координировать рабочий процесс RAG для генерации ответов.
Создайте файл с именем server.mjs и еще один с именем .env в корне вашего проекта. В файл .env вставьте ваш ключ OpenAI следующим образом:
OPENAI_API_KEY= your-api-key
Это файл, который мы будем использовать для хранения нашей конфиденциальной информации. Теперь откройте файл сервера, и давайте начнем писать код.
1. Настройка сервера и окружения
Начните с импорта необходимых библиотек и настройки переменных окружения, чтобы обеспечить безопасную работу системы.
import express from "express";
import axios from "axios";
import dotenv from "dotenv";
import cors from "cors";
import { ChatOpenAI } from "@langchain/openai";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { OpenAIEmbeddings } from "@langchain/openai";
import { Milvus } from "@langchain/community/vectorstores/milvus";
import { Document } from "langchain/document";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
dotenv.config();
Импорт библиотек, которые мы установили в нашей среде, в наш код гарантирует, что мы сможем вызывать и использовать их функции и методы в нашем коде. Функция dotenv.config() загружает наш API-ключ, обеспечивая его доступность во всем приложении без прямого раскрытия в коде.
2. Настройка приложения
Далее централизуйте настройки вашего приложения с помощью объекта конфигурации. Этот шаг помогает хранить все ключевые параметры в одном месте, упрощая их управление и настройку при необходимости.
const CONFIG = {
PORT: parseInt(process.env.PORT || "30080", 10),
STRAPI: {
URL: "http://localhost:1337/api/milvus-knowledgebases",
TIMEOUT: 5000
},
MILVUS: {
URL: "localhost:19530",
COLLECTION: "rag_collection",
PRIMARY_FIELD: "pk",
VECTOR_FIELD: "vector",
TEXT_FIELD: "text",
TEXT_MAX_LENGTH: 4096,
SEARCH_PARAMS: {
nprobe: 16,
offset: 0
},
BATCH_SIZE: 100
},
CHUNKING: {
SIZE: 2000,
OVERLAP: 200
},
TOP_K: 3
};
Этот объект конфигурации определяет критически важные параметры для нашего приложения. Настройка PORT определяет, какой порт слушает сервер. Раздел STRAPI содержит URL для системы управления контентом и значение тайм-аута, чтобы избежать длительного ожидания при получении данных.
Раздел MILVUS настраивает векторную базу данных, указывая такие детали, как имя коллекции, ограничения длины текста и параметры поиска. nprobe управляет точностью векторного поиска, регулируя, сколько кластеров проверяет Milvus. Наконец, раздел CHUNKING описывает, как документы разбиваются на более мелкие части, включая размер фрагмента и перекрытие, обеспечивая сохранение контекста во время обработки.
3. Инициализация сервера и AI-сервисов
После завершения настройки следующим шагом является инициализация сервера Express и настройка AI-сервисов. К ним относятся большая языковая модель для генерации ответов и сервис эмбеддингов для преобразования текста в векторы.
const app = express();
app.use(express.json({ limit: '1mb' }));
app.use(cors());
const model = new ChatOpenAI({
modelName: "gpt-3.5-turbo",
temperature: 0.7,
openAIApiKey: process.env.OPENAI_API_KEY,
maxRetries: 3,
timeout: 30000,
});
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
maxRetries: 3,
});
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: CONFIG.CHUNKING.SIZE,
chunkOverlap: CONFIG.CHUNKING.OVERLAP,
separators: ["\n\n", "\n", " ", ""]
});
Объект app создается с помощью Express, при этом добавляется промежуточное ПО для разбора входящих JSON-запросов и обработки междоменных запросов с использованием CORS. Это гарантирует, что сервер сможет принимать полезные нагрузки из разных доменов, что крайне важно для взаимодействия с фронтендом.
Далее мы создаем экземпляр ChatOpenAI, который будем использовать для подключения системы к модели OpenAI GPT-3.5 для генерации ответов. Параметр temperature управляет креативностью ответов: более низкие значения дают более предсказуемые результаты, а более высокие значения генерируют более разнообразные и креативные варианты.. Экземпляр OpenAIEmbeddings будет преобразовывать текст в векторы, обеспечивая семантический поиск. Наконец, экземпляр RecursiveCharacterTextSplitter будет делить текст на фрагменты на основе размера и перекрытия, определенных в конфигурации.
4. Подготовка документов к обработке
Прежде чем мы сможем хранить документы или выполнять по ним поиск с помощью Milvus, необходимо очистить и структурировать исходное содержимое в согласованный формат. Это гарантирует, что данные будут готовы для создания эмбеддингов и эффективного извлечения на последующих этапах рабочего процесса.
let milvusStore = null;
let lastDataHash = null;
const processDocument = (content) => {
if (!content?.Content || !content.Title) return null;
const processedContent = content.Content
.map(section => {
if (section.type === 'paragraph' && section.children) {
return section.children
.map(child => child.text)
.join(' ')
.trim();
}
return null;
})
.filter(Boolean)
.join('\n\n');
return processedContent ? new Document({
pageContent: processedContent,
metadata: {
source: 'milvus_content',
id: content.id,
title: content.Title,
documentId: content.documentId
}
}) : null;
};
В приведенном выше коде переменные let milvusStore и let lastDataHash выступают в роли глобальных ссылок. milvusStore хранит инициализированное подключение к коллекции Milvus, а lastDataHash отслеживает, изменились ли базовые данные документов с момента последнего обновления.
Функция processDocument поможет нам взять необработанные данные из Strapi и преобразовать их в структурированный объект Document. Сначала она проверяет, присутствуют ли поля содержимого и заголовка. Если данные валидны, она проходит по разделам содержимого, извлекая текст из абзацев и очищая его путем удаления лишних пробелов по краям. Эти разделы объединяются в цельный блок текста. Затем функция возвращает обработанный текст, обернутый в объект Document, включая метаданные, такие как заголовок документа, ID и источник. Такой формат обеспечивает совместимость с Milvus и LangChain для дальнейшей обработки.
5. Подключение к Milvus или его инициализация
Теперь, когда документы готовы, следующим шагом является их интеграция с Milvus. Мы установим подключение к существующей коллекции Milvus или, если она не существует, создадим ее и заполним подготовленными документами.
async function getMilvusStore() {
if (milvusStore) {
return milvusStore;
}
try {
// Сначала пытаемся подключиться к существующей коллекции
try {
console.log('Попытка подключиться к существующей коллекции');
milvusStore = await Milvus.fromExistingCollection(
embeddings,
{
url: CONFIG.MILVUS.URL,
collectionName: CONFIG.MILVUS.COLLECTION,
primaryField: CONFIG.MILVUS.PRIMARY_FIELD,
vectorField: CONFIG.MILVUS.VECTOR_FIELD,
textField: CONFIG.MILVUS.TEXT_FIELD,
textFieldMaxLength: CONFIG.MILVUS.TEXT_MAX_LENGTH,
}
);
return milvusStore;
} catch (error) {
console.log('Коллекция не существует, будет создана новая');
// Если коллекция не существует, получаем данные и создаём её
const response = await axios.get(CONFIG.STRAPI.URL, {
timeout: CONFIG.STRAPI.TIMEOUT
});
const processPromises = response.data.data.map(processDocument);
const processedDocs = await Promise.all(processPromises);
const uniqueDocs = new Map();
processedDocs
.filter(Boolean)
.forEach(doc => uniqueDocs.set(doc.metadata.documentId, doc));
const docs = Array.from(uniqueDocs.values());
// Разбиваем документы
const splitPromises = docs.map(doc => splitter.splitDocuments([doc]));
const splitDocs = await Promise.all(splitPromises);
const allSplitDocs = splitDocs.flat();
console.log(`Создание новой коллекции с ${allSplitDocs.length} документами`);
milvusStore = await Milvus.fromDocuments(
allSplitDocs,
embeddings,
{
url: CONFIG.MILVUS.URL,
collectionName: CONFIG.MILVUS.COLLECTION,
primaryField: CONFIG.MILVUS.PRIMARY_FIELD,
vectorField: CONFIG.MILVUS.VECTOR_FIELD,
textField: CONFIG.MILVUS.TEXT_FIELD,
textFieldMaxLength: CONFIG.MILVUS.TEXT_MAX_LENGTH,
}
);
lastDataHash = Buffer.from(JSON.stringify(response.data)).toString('base64');
return milvusStore;
}
} catch (error) {
console.error('Не удалось инициализировать Milvus:', error);
throw error;
}
}`
Эта функция управляет подключением к коллекции Milvus. Если переменная milvusStore уже содержит активное подключение, функция немедленно возвращает его, чтобы избежать ненужных операций. В противном случае она пытается подключиться к существующей коллекции в Milvus с помощью метода fromExistingCollection. Если коллекция не существует, функция переходит к созданию новой. Сначала она получает данные из Strapi, обрабатывает документы с помощью функции processDocument и гарантирует уникальность каждого документа, используя documentId как ключ в Map. Затем документы разбиваются на более мелкие фрагменты с помощью splitter, что гарантирует, что документы не будут слишком большими для embedding или индексирования в Milvus. Затем обработанные фрагменты сохраняются в коллекции Milvus с помощью метода fromDocuments. lastDataHash обновляется, чтобы отражать текущее состояние данных Strapi, обеспечивая отслеживание изменений.
6. Поддержание Milvus в актуальном состоянии
Milvus должен оставаться синхронизированным со Strapi, чтобы векторная база данных содержала самые последние данные. Давайте реализуем процесс, который обнаруживает изменения в контенте Strapi, определяет обновлённые или новые документы и соответствующим образом обновляет коллекцию Milvus.
// Обновление данных Milvus при необходимости
async function updateMilvusData() {
try {
const response = await axios.get(CONFIG.STRAPI.URL, {
timeout: CONFIG.STRAPI.TIMEOUT
});
const currentHash = Buffer.from(JSON.stringify(response.data)).toString('base64');
if (currentHash === lastDataHash) {
console.log('Данные не изменились, обновление пропущено');
return false;
}
console.log('Контент изменился, обновление коллекции Milvus');
const processPromises = response.data.data.map(processDocument);
const processedDocs = await Promise.all(processPromises);
const uniqueDocs = new Map();
processedDocs
.filter(Boolean)
.forEach(doc => uniqueDocs.set(doc.metadata.documentId, doc));
const docs = Array.from(uniqueDocs.values());
// Разделение документов
const splitPromises = docs.map(doc => splitter.splitDocuments([doc]));
const splitDocs = await Promise.all(splitPromises);
const allSplitDocs = splitDocs.flat();
// Удаление всех существующих документов перед добавлением новых
await milvusStore.delete({});
// Добавление новых документов пакетами
for (let i = 0; i < allSplitDocs.length; i += CONFIG.MILVUS.BATCH_SIZE) {
const batch = allSplitDocs.slice(i, i + CONFIG.MILVUS.BATCH_SIZE);
await milvusStore.addDocuments(batch);
console.log(`Добавлен пакет ${Math.floor(i / CONFIG.MILVUS.BATCH_SIZE) + 1} из ${Math.ceil(allSplitDocs.length / CONFIG.MILVUS.BATCH_SIZE)}`);
}
lastDataHash = currentHash;
console.log(`Milvus обновлен: ${allSplitDocs.length} документов`);
return true;
} catch (error) {
console.error("Ошибка при обновлении данных Milvus:", error);
throw error;
}
}
Эта функция начинается с получения последних данных из Strapi и вычисления их хеша. Если хеш совпадает с lastDataHash, данные не изменились, и обновление не требуется. В противном случае функция обрабатывает обновленные документы и очищает существующую коллекцию в Milvus с помощью метода delete. Затем новые фрагменты документов добавляются пакетами, что обеспечивает эффективное хранение и при этом сохраняет логирование для отслеживания прогресса. Наконец, она обновляет lastDataHash, чтобы будущие проверки точно определяли изменения в данных.
7. Обработка пользовательских запросов
После заполнения Milvus нашими данными следующий шаг — обработка пользовательских запросов. Система будет извлекать наиболее релевантные документы из Milvus на основе семантического сходства с запросом пользователя. Затем эти документы будут использоваться для генерации ответов с учетом контекста с помощью большой языковой модели OpenAI GPT-3.5.
async function handleQuery(chatHistory, input) {
const store = await getMilvusStore();
try {
await updateMilvusData();
} catch (error) {
console.warn("Не удалось проверить наличие обновлений:", error);
}
const results = await store.similaritySearchWithScore(
input,
CONFIG.TOP_K
);
console.log('\n=== Извлеченные документы из Milvus ===');
results.forEach(([doc, score], index) => {
console.log(`\nДокумент ${index + 1} (оценка: ${score}):`);
console.log('Заголовок:', doc.metadata.title);
console.log('Содержимое:', doc.pageContent.substring(0, 150) + '...');
});
const chain = await createStuffDocumentsChain({
llm: model,
prompt: ChatPromptTemplate.fromMessages([
[
"system",
`You are an AI assistant specializing in Milvus and Zilliz, vector database technologies. Your goal is to provide accurate and helpful answers based on the provided context.
**Guidelines:**
1. **Milvus/Zilliz-Related Queries:**
- If the query is about Milvus or Zilliz and the context includes relevant information, provide a detailed and structured response.
- Use proper Markdown formatting:
- \`\`\` for code blocks
- **Bold** for emphasis
- Bullet points for lists
- ### Headings for sections
- If no relevant information exists in the context, respond with: *"I don't have that information yet."*
2. **Unrelated Queries:**
- If the query is not about Milvus or Zilliz, respond with:
*"This topic is outside my expertise. I specialize in Milvus and Zilliz. Please ask questions related to these technologies."*
**Context:**
{context}
**User Query:**
{input}`
],
...chatHistory,
["user", "{input}"]
]),
documentPrompt: ChatPromptTemplate.fromTemplate("Content: {page_content}\n\n")
});
const response = await chain.invoke({
input,
context: results.map(([doc]) => doc)
});
return {
answer: response,
context: results.map(([doc]) => ({
content: doc.pageContent,
title: doc.metadata.title,
id: doc.metadata.id,
documentId: doc.metadata.documentId
}))
};
}
Эта функция начинается с проверки того, что векторное хранилище Milvus обновлено. Функция updateMilvusData вызывается внутри блока try-catch, чтобы обрабатывать потенциальные ошибки без прерывания процесса запроса. Затем запрос передается в метод Milvus similaritySearchWithScore, который извлекает из базы данных топ k наиболее похожих документов. Полученные документы логируются. Каждый документ содержит метаданные, такие как заголовок и ID, а также усеченный предварительный просмотр его содержимого. Это поможет нам понять, извлекает ли наш RAG-пайплайн правильные документы.
Далее документы передаются в LangChain createStuffDocumentsChain с использованием промпта, который инструктирует модель отвечать исключительно на основе предоставленного контекста. Это гарантирует, что OpenAI’s GPT-3.5 генерирует ответ, основанный на извлеченном содержимом, не полагаясь на внешние знания, сохраняя точность и релевантность. Наконец, функция возвращает как сгенерированный ответ, так и контекст извлеченных документов.
8. Настройка API-эндпоинтов
Теперь, когда backend может обрабатывать пользовательские запросы и взаимодействовать с Milvus, следующий шаг — предоставить эту функциональность через API-эндпоинты. Эти эндпоинты служат мостом между нашим backend и frontend (или другими клиентскими приложениями).
app.post("/chat", async (req, res) => {
try {
const { chatHistory, input } = req.body;
if (!input?.trim()) {
return res.status(400).json({
error: "Invalid input",
message: "No input provided"
});
}
const formattedHistory = Array.isArray(chatHistory)
? chatHistory.map(msg =>
msg.role === "user"
? new HumanMessage(msg.content)
: new AIMessage(msg.content)
)
: [];
const response = await handleQuery(formattedHistory, input);
res.json(response);
} catch (error) {
console.error("Chat request error:", error);
res.status(500).json({
error: "Internal server error",
message: process.env.NODE_ENV === 'production'
? "An unexpected error occurred"
: error.message
});
}
});
app.get("/health", async (req, res) => {
try {
const store = await getMilvusStore();
res.json({
status: "ok",
timestamp: new Date().toISOString(),
milvusInitialized: !!store,
config: CONFIG
});
} catch (error) {
res.status(500).json({
status: "error",
error: error.message
});
}
});
Эндпоинт /chat обрабатывает входящие POST-запросы, содержащие запрос пользователя и историю чата. История чата форматируется в объекты HumanMessage и AIMessage для сохранения контекста. Если запрос некорректен (например, пустой), эндпоинт отвечает статусом 400 Bad Request и соответствующим сообщением об ошибке. Если запрос корректен, вызывается функция handleQuery для обработки ввода, получения релевантных документов из Milvus и генерации ответа. Результат возвращается в виде JSON-объекта. Если происходит ошибка, она логируется, и сервер отвечает 500 Internal Server Error.
С другой стороны, эндпоинт /health предоставляет способ мониторинга состояния сервера. Он проверяет, инициализировано ли хранилище Milvus, и отвечает статусом ok или error, а также конфигурацией сервера и текущей временной меткой.
9. Запуск сервера
Последний шаг — запустить сервер и сделать его доступным для обработки запросов.
app.listen(CONFIG.PORT, () => {
console.log(`Server running on http://localhost:${CONFIG.PORT}`);
console.log('Configuration:', CONFIG);
});
Метод app.listen запускает сервер на порту, указанном в конфигурации, в данном случае на порту 30080. После запуска сервера он выводит сообщение с URL, по которому к нему можно обратиться, а также текущую конфигурацию. Это подтверждает, что бэкенд готов обрабатывать запросы.
Рисунок — сервер бэкенда RAG-системы запущен и выводит текущие конфигурации
Рисунок: сервер бэкенда RAG-системы запущен и выводит текущие конфигурации
Чтобы запустить сервер, перейдите в терминал и выполните эту команду.
node .\server.mjs
Теперь мы завершили создание бэкенда для нашей RAG-системы.
Создание фронтенда вашей RAG-системы
Когда бэкенд RAG-системы полностью функционален, следующий шаг — создать фронтенд, чтобы пользователи могли взаимодействовать с системой. Фронтенд будет представлять собой интерфейс чат-бота, подключенный к бэкенду, что позволит пользователям вводить запросы и получать ответы, сгенерированные ИИ.
Создание пользовательского интерфейса
Начните с создания файла с именем ChatbotUI.js в директории src. Этот файл будет определять компонент чат-бота и управлять потоком пользовательских запросов и ответов бэкенда.
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import axios from 'axios';
import './ChatbotUI.css';
const ChatbotUI = () => {
const [chatHistory, setChatHistory] = useState([]);
const [userInput, setUserInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const chatContainerRef = useRef(null);
useEffect(() => {
// Scroll to the latest message when chat history updates
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}, [chatHistory]);
const handleUserInput = (e) => setUserInput(e.target.value);
const handleSendMessage = async () => {
if (userInput.trim() !== '') {
const newMessage = { role: 'user', content: userInput };
const updatedChatHistory = [...chatHistory, newMessage];
setChatHistory(updatedChatHistory);
setUserInput('');
setIsLoading(true);
try {
const response = await axios.post('http://localhost:30080/chat', {
chatHistory: updatedChatHistory,
input: userInput,
});
const botMessage = {
role: 'assistant',
content: response.data.answer || 'Извините, я не совсем понял.',
};
setTimeout(() => {
setChatHistory((prevMessages) => [...prevMessages, botMessage]);
}, 1000); // Имитация задержки набора текста
} catch (err) {
console.error('Ошибка:', err);
setError('Не удалось подключиться к серверу.');
} finally {
setIsLoading(false);
}
}
};
return (
<div className="chatbot-ui">
{/* Заголовок */}
<div className="chat-header">
<img src="/milvus_logo.png" alt="Ассистент" className="assistant-logo" />
<h2>Чат-ассистент</h2>
</div>
{/* Сообщения чата */}
<div className="chat-body" ref={chatContainerRef}>
{chatHistory.map((message, index) => (
<div
key={index}
className={`message ${message.role === 'user' ? 'user-message' : 'bot-message'}`}
>
<ReactMarkdown>{message.content}</ReactMarkdown>
</div>
))}
{isLoading && (
<div className="bot-message typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
)}
{error && <div className="error-message">{error}</div>}
</div>
{/* Нижняя панель */}
<div className="chat-footer">
<input
type="text"
placeholder="Спросите меня о чём угодно..."
value={userInput}
onChange={handleUserInput}
onKeyPress={(e) => {
if (e.key === 'Enter') handleSendMessage();
}}
disabled={isLoading}
/>
<button onClick={handleSendMessage} disabled={isLoading}>
<span className="send-icon">✈</span>
</button>
</div>
</div>
);
};
export default ChatbotUI;
Этот компонент использует useState из React для управления такими состояниями, как история чата и пользовательский ввод. Хук useEffect обеспечивает автоматическую прокрутку контейнера чата к последнему сообщению. Когда пользователь отправляет запрос, он отправляется на endpoint backend API http://localhost:30080/chat через POST-запрос axios. Затем ответ backend отображается в интерфейсе чата.
Стилизация чат-бота
Далее создайте файл с именем ChatbotUI.css в директории src. Этот файл будет стилизовать интерфейс чат-бота.
/* Полноэкранный градиентный фон */
.chatbot-ui {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #8358ff, #00a6ff); /* Цветовая палитра Milvus */
font-family: 'Roboto', sans-serif;
color: #fff;
}
/* Заголовок чата */
.chat-header {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #1e1e2d, #28293e);
width: 100%;
padding: 15px;
border-radius: 15px 15px 0 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.assistant-logo {
width: 50px;
height: 50px;
margin-right: 15px;
border-radius: 50%;
background: radial-gradient(circle, #8358ff, #00a6ff); /* Динамическое свечение логотипа */
animation: pulse 2s infinite;
}
.chat-header h2 {
font-size: 1.8rem;
font-weight: bold;
color: #ffffff;
}
.chat-header p {
font-size: 0.9rem;
color: #a0a0b1;
margin-top: 5px;
}
/* Тело чата */
.chat-body {
flex: 1;
width: 100%;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
background: #1e1e2d;
color: #ffffff;
border-radius: 0 0 15px 15px;
}
.message {
max-width: 80%;
padding: 12px 15px;
border-radius: 10px;
font-size: 1rem;
line-height: 1.5;
animation: fadeIn 0.3s ease-in-out;
}
.user-message {
align-self: flex-end;
background: linear-gradient(90deg, #8358ff, #00a6ff);
color: #ffffff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.bot-message {
align-self: flex-start;
background: #29293f;
color: #d4d4e5;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
/* Typing Indicator */
.typing-indicator {
display: flex;
justify-content: flex-start;
gap: 5px;
}
.typing-indicator span {
width: 8px;
height: 8px;
background-color: #00a6ff;
border-radius: 50%;
animation: blink 1.2s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
/* Footer */
.chat-footer {
display: flex;
align-items: center;
width: 100%;
padding: 10px 15px;
background: #1e1e2d;
border-top: 1px solid #28293e;
border-radius: 0 0 15px 15px;
box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.2);
}
.chat-footer input {
flex: 1;
padding: 12px 15px;
border-radius: 30px;
border: 1px solid #8358ff;
font-size: 1rem;
outline: none;
background: #29293f;
color: #ffffff;
transition: border-color 0.3s ease;
}
.chat-footer input:focus {
border-color: #00a6ff;
}
.chat-footer button {
margin-left: 10px;
padding: 12px 20px;
border: none;
border-radius: 30px;
background: linear-gradient(90deg, #8358ff, #00a6ff);
color: white;
font-size: 1.2rem;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.chat-footer button:hover {
transform: scale(1.1);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4);
}
.chat-footer button:disabled {
background: #29293f;
color: #a0a0b1;
cursor: not-allowed;
}
@keyframes blink {
0%, 80%, 100% {
opacity: 0;
}
40% {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 10px rgba(131, 88, 255, 0.5);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 20px rgba(0, 166, 255, 0.7);
}
}
CSS-стили создают интерфейс чат-бота, вдохновленный цветовой палитрой Milvus. В фоне используется градиент с глубокими фиолетовыми и голубыми оттенками, отражающий брендинг Milvus. Заголовок чата выполнен в стиле темного градиента, а сообщения пользователя выделены ярким градиентом, соответствующим основным цветам Milvus. Сообщения бота оформлены в более темной, приглушенной теме. Этот дизайн использует цвета Milvus для создания единого и функционального пользовательского интерфейса.
Рисунок — завершенный пользовательский интерфейс системы RAG
Рисунок: завершенный пользовательский интерфейс системы RAG
Вот так выглядит финальный UI.
Интеграция чат-бота в приложение
Наконец, обновите файл App.js в директории src, чтобы отрендерить компонент ChatbotUI.
import React from 'react';
import ChatbotUI from './ChatbotUI';
const App = () => {
return (
<div>
<ChatbotUI />
</div>
);
};
export default App;
Это гарантирует, что чат-бот будет отображаться как основной компонент при запуске приложения. Компонент App служит точкой входа для приложения React.
Тестирование вашей системы RAG
После завершения настройки backend и frontend пора протестировать весь RAG-пайплайн, чтобы убедиться, что он работает должным образом. Вот как действовать:
Запуск backend
Откройте терминал, перейдите в директорию проекта и запустите backend-сервер:
node server.mjs
Убедитесь, что backend работает на http://localhost:30080. Проверьте логи, чтобы убедиться, что Milvus инициализирован и готов к запросам.
Запуск frontend
Откройте отдельный терминал, перейдите в директорию rag-app и запустите frontend на React:
npm start
Это запустит приложение по адресу http://localhost:3000 в вашем браузере по умолчанию.
Тестирование интерфейса чат-бота
В интерфейсе чат-бота введите вопрос, связанный с Milvus или Zilliz. Также попробуйте вводить вопросы вне этих тем, чтобы проверить, ответит ли система соответствующим сообщением. Наконец, попробуйте вводить вопросы, которые могут выходить за пределы базы знаний Strapi. Вот примеры результатов:
Figure- RAG system result of a query related to Milvus
Рисунок: результат системы RAG для запроса, связанного с Milvus
Figure- RAG system testing results
Рисунок: результаты тестирования системы RAG
Скриншоты выше показывают, что наша система RAG работает как ожидается и отображает правильные результаты. Она также основана на использовании нашей базы знаний, поэтому она не знает основателей Milvus, так как этой информации нет в Strapi.
Продолжайте и добавьте больше контента в базу знаний и протестируйте систему.
Заключение
Интеграция LangChain, Milvus и Strapi для создания системы Retrieval-Augmented Generation (RAG) демонстрирует, как ИИ может предоставлять точные, предметно-ориентированные ответы, основанные на реальных, актуальных знаниях. Такая архитектура идеально подходит для приложений вроде клиентской поддержки, управления знаниями и образовательных инструментов. Имея четкое понимание архитектуры и это пошаговое руководство, теперь вы можете создавать системы RAG, адаптированные к вашим конкретным потребностям.
Связанные ресурсы
Читать далее

A Developer's Guide to Exploring Milvus 2.6 Features on Zilliz Cloud
Milvus 2.6 marks a shift from “vector search + glue code” to a more advanced retrieval engine, and it is now Generally Available (GA) on Zilliz Cloud (a managed Milvus service).

Migrating from S3 Vectors to Zilliz Cloud: Unlocking the Power of Tiered Storage
Learn how Zilliz Cloud bridges cost and performance with tiered storage and enterprise-grade features, and how to migrate data from AWS S3 Vectors to Zilliz Cloud.
Milvus/Zilliz + Surveillance: How Vector Databases Transform Multi-Camera Tracking
See how Milvus vector database enhances multi-camera tracking with similarity-based matching for better surveillance in retail, warehouses and transport hubs.


