INTEGRITY Документация

Создание ИИ на основе Retrieval Augmented Generation (RAG)

Это руководство проведёт вас через настройку и развёртывание первого приложения с Cloudflare AI. Вы создадите полнофункциональное приложение на основе ИИ, используя такие инструменты, как Workers AI, Vectorize, D1 и Cloudflare Workers.

К концу этого руководства вы создадите инструмент на основе ИИ, который позволяет сохранять информацию и делать по ней запросы с помощью большой языковой модели. Этот подход, известный как Retrieval Augmented Generation (RAG), удобно реализовать, объединив несколько компонентов набора инструментов Cloudflare для ИИ. Опыт работы с инструментами ИИ для создания этого приложения не требуется.

  1. Зарегистрируйтесь для получения Аккаунт Cloudflare.
  2. Установка Node.js.

менеджер версий Node.js

Используйте менеджер версий Node, например Volta или nvm чтобы избежать проблем с правами доступа и переключать версии Node.js. Wrangler, о котором пойдёт речь далее в этом руководстве, требует версию Node 16.17.0 или более поздней версии.

Вам также потребуется доступ к Vectorize. В этом руководстве мы покажем, как можно при необходимости интегрироваться с Anthropic Claude тоже. Вам потребуется Ключ API Anthropic чтобы сделать это.

1. Создайте новый проект Worker

C3 (create-cloudflare-cli) представляет собой инструмент командной строки, который помогает как можно быстрее настроить и развернуть Workers в Cloudflare.

Откройте окно терминала и запустите C3, чтобы создать проект Worker:

npm create cloudflare@latest -- rag-ai-tutorial

Для настройки выберите следующие параметры:

В каталоге вашего проекта C3 создал несколько файлов.

Какие файлы создал C3?

  1. wrangler.jsonc: Ваш Wrangler файл конфигурации.
  2. index.js/src): минимальный 'Hello World!' Worker, написанный на ES module синтаксис.
  3. package.json: Минимальный файл конфигурации зависимостей Node.
  4. package-lock.json: См. npm документацию о package-lock.json.
  5. node_modules: См. npm документация node_modules.

Теперь перейдите в только что созданный каталог:

cd rag-ai-tutorial

2. Разработка с помощью Wrangler CLI

Интерфейс командной строки Workers, Wrangler, позволяет создать, тест, а также deploy ваших проектов Workers. C3 по умолчанию устанавливает Wrangler в проекты.

После создания первого Worker выполните wrangler dev команду в каталоге проекта, чтобы запустить локальный сервер для разработки Worker. Это позволит тестировать Worker локально во время разработки.

npx wrangler dev

Теперь вы сможете перейти к http://localhost:8787 чтобы увидеть, как работает ваш Worker. Любые изменения в коде запускают пересборку, и после обновления страницы вы увидите актуальный результат работы Worker.

3. Добавьте привязку AI

Чтобы начать использовать продукты Cloudflare для ИИ, вы можете добавить ai блок в конфигурационный файл Wrangler как удалённая привязка. Это создаст привязку к AI-моделям Cloudflare в вашем коде, которую можно использовать для взаимодействия с доступными на платформе AI-моделями.

В этом примере используется @cf/meta/llama-3-8b-instruct модель, которая генерирует текст.

{
	"ai": {
		"binding": "AI",
		"remote": true
	}
}
[ai]
binding = "AI"
remote = true

Теперь найдите src/index.js файл. Внутри fetch обработчика можно выполнять запросы к AI в качестве привязки:

export default {
	async fetch(request, env, ctx) {
		const answer = await env.AI.run("@cf/meta/llama-3-8b-instruct", {
			messages: [{ role: "user", content: `What is the square root of 9?` }],
		});

		return new Response(JSON.stringify(answer));
	},
};

Отправляя запросы к LLM через AI привязку, мы можем напрямую взаимодействовать с большими языковыми моделями Cloudflare AI прямо в коде. В этом примере используется @cf/meta/llama-3-8b-instruct модель, которая генерирует текст.

Разверните ваш Worker с помощью wrangler:

npx wrangler deploy

Теперь запрос к вашему Worker будет генерировать текстовый ответ от LLM и возвращать его в виде объекта JSON.

curl https://example.username.workers.dev
{"response":"Answer: The square root of 9 is 3."}

4. Добавление эмбеддингов с помощью Cloudflare D1 и Vectorize

Эмбеддинги позволяют добавить дополнительные возможности языковым моделям, которые вы используете в своих проектах Cloudflare AI. Это делается через Vectorize, векторную базу данных Cloudflare.

Чтобы начать использовать Vectorize, создайте новый индекс эмбеддингов с помощью wrangler. Этот индекс будет хранить векторы с 768 измерениями и использовать косинусное сходство, чтобы определять, какие векторы наиболее похожи друг на друга:

npx wrangler vectorize create vector-index --dimensions=768 --metric=cosine

Затем добавьте параметры конфигурации нового индекса Vectorize в конфигурационный файл Wrangler:

{
	// ... existing wrangler configuration
	"vectorize": [
		{
			"binding": "VECTOR_INDEX",
			"index_name": "vector-index"
		}
	]
}
[[vectorize]]
binding = "VECTOR_INDEX"
index_name = "vector-index"

Векторный индекс позволяет хранить набор измерений: чисел с плавающей точкой, которые представляют ваши данные. Чтобы выполнить запрос к векторной базе данных, свой запрос тоже можно преобразовать в измерения. Vectorize предназначен для эффективного определения того, какие сохранённые векторы наиболее похожи на ваш запрос.

Чтобы реализовать функцию поиска, необходимо настроить базу данных D1 от Cloudflare. В D1 можно хранить данные приложения. Затем эти данные преобразуются в векторный формат. Когда пользователь выполняет поиск и находится совпадение с вектором, ему можно показать соответствующие данные.

Создайте новую базу данных D1 с помощью wrangler:

npx wrangler d1 create database

Затем вставьте параметры конфигурации, полученные в выводе предыдущей команды, в конфигурационный файл Wrangler:

{
	// ... existing wrangler configuration
	"d1_databases": [
		{
			"binding": "DB", // available in your Worker on env.DB
			"database_name": "database",
			"database_id": "abc-def-geh" // replace this with a real database_id (UUID)
		}
	]
}
[[d1_databases]]
binding = "DB"
database_name = "database"
database_id = "abc-def-geh"

В этом приложении мы создадим notes таблицу в D1, которая позволит нам хранить заметки, а затем получать их в Vectorize. Чтобы создать эту таблицу, выполните команду SQL с помощью wrangler d1 execute:

npx wrangler d1 execute database --remote --command "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT NOT NULL)"

Теперь можно добавить новую заметку в базу данных с помощью wrangler d1 execute:

npx wrangler d1 execute database --remote --command "INSERT INTO notes (text) VALUES ('The best pizza topping is pepperoni')"

5. Создание workflow

Прежде чем начать создавать заметки, познакомимся с Cloudflare Workflow. Это позволит нам определить durable workflow, который сможет надёжно и стабильно выполнить все шаги процесса RAG.

Для начала добавьте новый [[workflows]] блок в ваш конфигурационный файл Wrangler:

{
	// ... existing wrangler configuration
	"workflows": [
		{
			"name": "rag",
			"binding": "RAG_WORKFLOW",
			"class_name": "RAGWorkflow"
		}
	]
}
[[workflows]]
name = "rag"
binding = "RAG_WORKFLOW"
class_name = "RAGWorkflow"

В src/index.js, добавьте новый класс с именем RAGWorkflow который расширяет WorkflowEntrypoint:

import { WorkflowEntrypoint } from "cloudflare:workers";

export class RAGWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		await step.do("example step", async () => {
			console.log("Hello World!");
		});
	}
}

Этот класс определяет один шаг workflow, который выводит «Hello World!» в консоль. К своему workflow можно добавить сколько угодно шагов.

Сам по себе этот workflow ничего не делает. Чтобы его запустить, мы вызовем RAG_WORKFLOW привязку, передав любые параметры, необходимые процессу для успешного завершения. Пример вызова процесса:

env.RAG_WORKFLOW.create({ params: { text } });

6. Создание заметок и их добавление в Vectorize

Чтобы расширить функцию Workers для обработки нескольких маршрутов, добавим hono, библиотеку маршрутизации для Workers. Она позволит нам создать новый маршрут для добавления заметок в базу данных. Установите hono с помощью npm:

npm i hono

Затем импортируйте hono в ваш src/index.js файл. Также необходимо обновить fetch обработчик для использования hono:

import { Hono } from "hono";
const app = new Hono();

app.get("/", async (c) => {
	const answer = await c.env.AI.run("@cf/meta/llama-3-8b-instruct", {
		messages: [{ role: "user", content: `What is the square root of 9?` }],
	});

	return c.json(answer);
});

export default app;

Это установит маршрут по корневому пути / который функционально эквивалентен предыдущей версии вашего приложения.

Теперь можно обновить workflow, чтобы он начал добавлять заметки в базу данных и генерировать для них соответствующие эмбеддинги.

В этом примере используется @cf/baai/bge-base-en-v1.5 модель, который можно использовать для создания эмбеддинга. Эмбеддинги хранятся и извлекаются внутри Vectorize, векторную базу данных Cloudflare. Запрос пользователя также преобразуется в эмбеддинг, чтобы его можно было использовать для поиска в Vectorize.

import { WorkflowEntrypoint } from "cloudflare:workers";

export class RAGWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		const env = this.env;
		const { text } = event.payload;

		const record = await step.do(`create database record`, async () => {
			const query = "INSERT INTO notes (text) VALUES (?) RETURNING *";

			const { results } = await env.DB.prepare(query).bind(text).run();

			const record = results[0];
			if (!record) throw new Error("Failed to create note");
			return record;
		});

		const embedding = await step.do(`generate embedding`, async () => {
			const embeddings = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
				text: text,
			});
			const values = embeddings.data[0];
			if (!values) throw new Error("Failed to generate vector embedding");
			return values;
		});

		await step.do(`insert vector`, async () => {
			return env.VECTOR_INDEX.upsert([
				{
					id: record.id.toString(),
					values: embedding,
				},
			]);
		});
	}
}

Этот workflow выполняет следующие действия:

  1. Принимает text параметр.
  2. Вставьте новую строку в notes таблицу в D1 и получить id новой строки.
  3. Преобразуйте text в вектор с помощью embeddings модель привязки LLM.
  4. Выполните upsert для id и vectors в vector-index индекс в Vectorize.

Так вы создадите новое векторное представление заметки, которое затем можно будет использовать для её поиска.

Чтобы завершить код, добавим маршрут, который позволит пользователям отправлять заметки в базу данных. Этот маршрут распарсит тело JSON-запроса, получит note и создать новый экземпляр workflow, передав этот параметр:

app.post("/notes", async (c) => {
	const { text } = await c.req.json();
	if (!text) return c.text("Missing text", 400);
	await c.env.RAG_WORKFLOW.create({ params: { text } });
	return c.text("Created note", 201);
});

7. Запрос к Vectorize для получения заметок

Чтобы завершить код, можно обновить корневой путь (/) для запросов к Vectorize. Вы преобразуете запрос в вектор, а затем используете vector-index индекс для поиска наиболее похожих векторов.

topK ограничивает количество векторов, возвращаемых функцией. Например, если задать значение topK равное 1, вернёт только наиболее похожие вектор на основе запроса. Если задать topK равным 5 вернет 5 наиболее похожих векторов.

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

Текст этих заметок можно вставить в промпт для привязки LLM в качестве контекста. Именно на этом строится Retrieval-Augmented Generation (RAG): предоставление LLM дополнительного контекста из внешних данных для улучшения генерируемого текста.

Обновим промпт, добавив в него контекст и попросив LLM использовать этот контекст при формировании ответа:

import { Hono } from "hono";
const app = new Hono();

// Existing post route...
// app.post('/notes', async (c) => { ... })

app.get("/", async (c) => {
	const question = c.req.query("text") || "What is the square root of 9?";

	const embeddings = await c.env.AI.run("@cf/baai/bge-base-en-v1.5", {
		text: question,
	});
	const vectors = embeddings.data[0];

	const vectorQuery = await c.env.VECTOR_INDEX.query(vectors, { topK: 1 });
	let vecId;
	if (
		vectorQuery.matches &&
		vectorQuery.matches.length > 0 &&
		vectorQuery.matches[0]
	) {
		vecId = vectorQuery.matches[0].id;
	} else {
		console.log("No matching vector found or vectorQuery.matches is empty");
	}

	let notes = [];
	if (vecId) {
		const query = `SELECT * FROM notes WHERE id = ?`;
		const { results } = await c.env.DB.prepare(query).bind(vecId).run();
		if (results) notes = results.map((vec) => vec.text);
	}

	const contextMessage = notes.length
		? `Context:\n${notes.map((note) => `- ${note}`).join("\n")}`
		: "";

	const systemPrompt = `When answering the question or responding, use the context provided, if it is provided and relevant.`;

	const { response: answer } = await c.env.AI.run(
		"@cf/meta/llama-3-8b-instruct",
		{
			messages: [
				...(notes.length ? [{ role: "system", content: contextMessage }] : []),
				{ role: "system", content: systemPrompt },
				{ role: "user", content: question },
			],
		},
	);

	return c.text(answer);
});

app.onError((err, c) => {
	return c.text(err);
});

export default app;

8. Добавление модели Anthropic Claude (необязательно)

Если вы работаете с более крупными документами, у вас есть возможность использовать разработанную Anthropic Модели Claude, у которых большие контекстные окна и которые хорошо подходят для процессов RAG.

Для начала установите @anthropic-ai/sdk пакет:

npm i @anthropic-ai/sdk

В src/index.js, вы можете обновить GET / маршрут для проверки ANTHROPIC_API_KEY переменную окружения. Если она задана, текст генерируется с помощью Anthropic SDK. Если нет, используется существующий код Workers AI:

import Anthropic from '@anthropic-ai/sdk';

app.get('/', async (c) => {
  // ... Existing code
	const systemPrompt = `When answering the question or responding, use the context provided, if it is provided and relevant.`

	let modelUsed = ""
	let response = null

	if (c.env.ANTHROPIC_API_KEY) {
		const anthropic = new Anthropic({
			apiKey: c.env.ANTHROPIC_API_KEY
		})

		const model = "claude-3-5-sonnet-latest"
		modelUsed = model

		const message = await anthropic.messages.create({
			max_tokens: 1024,
			model,
			messages: [
				{ role: 'user', content: question }
			],
			system: [systemPrompt, notes ? contextMessage : ''].join(" ")
		})

		response = {
			response: message.content.map(content => content.text).join("\n")
		}
	} else {
		const model = "@cf/meta/llama-3.1-8b-instruct"
		modelUsed = model

		response = await c.env.AI.run(
			model,
			{
				messages: [
					...(notes.length ? [{ role: 'system', content: contextMessage }] : []),
					{ role: 'system', content: systemPrompt },
					{ role: 'user', content: question }
				]
			}
		)
	}

	if (response) {
		c.header('x-model-used', modelUsed)
		return c.text(response.response)
	} else {
		return c.text("We were unable to generate output", 500)
	}
})

Наконец, вам нужно задать ANTHROPIC_API_KEY переменную окружения в вашем приложении Workers. Это можно сделать с помощью wrangler secret put:

$ npx wrangler secret put ANTHROPIC_API_KEY

9. Удаление заметок и векторов

Если заметка больше не нужна, вы можете удалить её из базы данных. При каждом удалении заметки необходимо также удалить соответствующий вектор из Vectorize. Реализовать это можно, создав DELETE /notes/:id маршрут в вашем src/index.js файле:

app.delete("/notes/:id", async (c) => {
	const { id } = c.req.param();

	const query = `DELETE FROM notes WHERE id = ?`;
	await c.env.DB.prepare(query).bind(id).run();

	await c.env.VECTOR_INDEX.deleteByIds([id]);

	return c.status(204);
});

10. Разбивка текста (необязательно)

Для больших фрагментов текста рекомендуется разбивать текст на более мелкие части. Это позволяет LLM эффективнее собирать релевантный контекст, не извлекая большие фрагменты текста целиком.

Чтобы реализовать это, добавим в проект новый пакет NPM, `@langchain/textsplitters':

npm i @langchain/textsplitters

RecursiveCharacterTextSplitter класс из этого пакета разбивает текст на более мелкие фрагменты. Его можно настроить под свои нужды, но конфигурация по умолчанию подходит в большинстве случаев:

import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";

const text = "Some long piece of text...";

const splitter = new RecursiveCharacterTextSplitter({
	// These can be customized to change the chunking size
	// chunkSize: 1000,
	// chunkOverlap: 200,
});

const output = await splitter.createDocuments([text]);
console.log(output); // [{ pageContent: 'Some long piece of text...' }]

Чтобы использовать этот разделитель, обновим workflow, чтобы разбивать текст на более мелкие фрагменты. Затем переберём фрагменты и выполним остальную часть workflow для каждого фрагмента текста:

export class RAGWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		const env = this.env;
		const { text } = event.payload;
		let texts = await step.do("split text", async () => {
			const splitter = new RecursiveCharacterTextSplitter();
			const output = await splitter.createDocuments([text]);
			return output.map((doc) => doc.pageContent);
		});

		console.log(
			"RecursiveCharacterTextSplitter generated ${texts.length} chunks",
		);

		for (const index in texts) {
			const text = texts[index];
			const record = await step.do(
				`create database record: ${index}/${texts.length}`,
				async () => {
					const query = "INSERT INTO notes (text) VALUES (?) RETURNING *";

					const { results } = await env.DB.prepare(query).bind(text).run();

					const record = results[0];
					if (!record) throw new Error("Failed to create note");
					return record;
				},
			);

			const embedding = await step.do(
				`generate embedding: ${index}/${texts.length}`,
				async () => {
					const embeddings = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
						text: text,
					});
					const values = embeddings.data[0];
					if (!values) throw new Error("Failed to generate vector embedding");
					return values;
				},
			);

			await step.do(`insert vector: ${index}/${texts.length}`, async () => {
				return env.VECTOR_INDEX.upsert([
					{
						id: record.id.toString(),
						values: embedding,
					},
				]);
			});
		}
	}
}

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

11. Разверните свой проект

Если вы не развернули Worker во время шаг 1, разверните ваш Worker через Wrangler на *.workers.dev поддомен, или Custom Domain, если вы его настроили. Если у вас не настроен ни поддомен, ни домен, Wrangler предложит вам настроить его в процессе публикации.

npx wrangler deploy

Откройте предпросмотр Worker по адресу <YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev.

Полная версия этого кода доступна на GitHub. Она включает пользовательский интерфейс для запросов, добавления и удаления заметок, а также серверный API для работы с базой данных и векторным индексом. Найти его можно здесь: github.com/kristianfreeman/cloudflare-retrieval-augmented-generation-example.

Что ещё можно сделать: