Продвинутый RAG: переформулировка, гибридный поиск, re-ranking | Урок 9
Цель урока
Вы научитесь улучшать качество RAG и переформулировать запросы перед поиском. Генерировать несколько вариантов запроса, комбинировать векторный и полнотекстовый поиск. Переранжировать результаты и поймёте, какую технику применять под конкретную проблему.
Необходимые знания:
- Урок 8 (базовый RAG: загрузка, чанкинг, VectorStore, RAG цепочка)
Ключевые концепции:
- Проблемы базового RAG
- Query rewriting - переформулировка запроса
- Multi-query retrieval - несколько вариантов запроса
- Contextual compression - фильтрация нерелевантного текста
- Гибридный поиск (BM25 + векторный)
- Re-ranking результатов
- Conversational RAG - RAG с памятью диалога
Проблемы базового RAG
Базовый RAG из урока 8 работает хорошо для простых, конкретных вопросов. Он ломается в нескольких ситуациях:
1. Размытый или неточный вопрос
"Расскажи про это" — что "это"? Retriever ищет буквально.
2. Вопрос зависит от контекста диалога
Пользователь: "Как работает asyncio?"
Пользователь: "А как его тестировать?" — retriever не знает, что "его" = asyncio
3. Один запрос не покрывает все аспекты вопроса
"Сравни FastAPI и Django" — нужны документы по обоим, но один запрос может найти только один
4. Чанки содержат много нерелевантного текста
Нашли нужный документ, но в нём 80% не по теме вопроса
5. Семантический поиск не находит точные термины
"asyncio.gather" — векторный поиск может не найти точное имя функции
Каждая из этих проблем решается отдельной техникой.
Query Rewriting: переформулировка запроса
Перед поиском LLM улучшает формулировку вопроса и делает его точнее, и удобнее для семантического поиска.
Query rewriting эффективен, когда пользователи формулируют вопросы разговорным языком.
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
# Цепочка переформулировки
rewrite_prompt = ChatPromptTemplate.from_messages([
("system", """Ты эксперт по информационному поиску.
Перефразируй вопрос пользователя так, чтобы он лучше подходил для поиска
в технической документации. Сделай его конкретнее и убери лишние слова.
Верни только переформулированный вопрос, без пояснений."""),
("human", "{question}"),
])
rewriter = rewrite_prompt | model | StrOutputParser()
# Пример
original = "Расскажи, как вообще сделать так, чтобы несколько штук работало одновременно"
rewritten = rewriter.invoke({"question": original})
print(f"Оригинал: {original}")
print(f"Переформулировка: {rewritten}")
# "параллельное выполнение задач asyncio Python coroutines"
# Встраиваем в RAG цепочку
rag_with_rewrite = (
{"question": rewriter, "original_question": RunnablePassthrough()}
# | ... далее стандартная цепочка RAG
)
Multi-Query Retrieval: несколько вариантов запроса
Одна формулировка может не найти всё нужное. Генерируем несколько вариантов запроса и объединяем результаты.
Multi-query полезен для сравнительных вопросов и вопросов с несколькими аспектами.
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.documents import Document
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# --- Создаем VectorStore ---
texts = [
"FastAPI поддерживает async/await и работает на Starlette.",
"Django — полнофункциональный фреймворк с ORM, админкой и шаблонами.",
"FastAPI автоматически генерирует документацию Swagger.",
"Django REST Framework добавляет поддержку REST API в Django.",
"FastAPI использует Pydantic для валидации входных данных.",
"Django имеет встроенную систему аутентификации пользователей.",
]
vectorstore = FAISS.from_texts(texts, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
# --- Multi-query: генерируем 3 варианта запроса ---
multi_query_prompt = ChatPromptTemplate.from_messages([
("system", """Сгенерируй 3 разных варианта поискового запроса для нахождения
информации по вопросу пользователя. Варианты должны охватывать разные аспекты вопроса.
Верни только запросы, по одному на строке, без нумерации и пояснений."""),
("human", "{question}"),
])
query_generator = multi_query_prompt | model | StrOutputParser()
def generate_queries(question: str) -> list[str]:
result = query_generator.invoke({"question": question})
queries = [q.strip() for q in result.strip().split("\n") if q.strip()]
queries.append(question) # добавляем оригинальный вопрос
return queries
def multi_query_search(question: str) -> list[Document]:
queries = generate_queries(question)
print(f"Запросы: {queries}")
# Собираем все результаты, дедуплицируем по содержимому
seen = set()
unique_docs = []
for query in queries:
docs = retriever.invoke(query)
for doc in docs:
if doc.page_content not in seen:
seen.add(doc.page_content)
unique_docs.append(doc)
return unique_docs
# Тест
docs = multi_query_search("Чем FastAPI лучше Django для REST API?")
print(f"\nНайдено уникальных документов: {len(docs)}")
for doc in docs:
print(f" - {doc.page_content[:80]}")
Contextual Compression: сжатие контекста
Нашли нужный чанк, но в нём много лишнего текста.
ContextualCompressionRetriever извлекает только релевантные части.
Компрессия добавляет один вызов к LLM на каждый документ дороже, но качество контекста выше.
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_classic.retrievers import ContextualCompressionRetriever
from langchain_classic.retrievers.document_compressors import LLMChainExtractor
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# Большой текст с лишней информацией
texts = [
"""asyncio — это стандартная библиотека Python для написания конкурентного кода.
Она основана на event loop, который управляет выполнением корутин.
История библиотеки началась в Python 3.4 как экспериментальная возможность.
asyncio.gather() позволяет запускать несколько корутин параллельно и дождаться
всех результатов. Для работы с файлами используй aiofiles.
Библиотека была значительно улучшена в Python 3.7 с введением asyncio.run().""",
]
vectorstore = FAISS.from_texts(texts, embeddings)
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 1})
# Компрессор: извлекает только релевантные части чанка
compressor = LLMChainExtractor.from_llm(model)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever,
)
# Без компрессии будет весь чанк
docs = base_retriever.invoke("как запустить несколько корутин параллельно?")
print("БЕЗ компрессии:")
print(docs[0].page_content)
# С компрессией, только нужная часть
compressed_docs = compression_retriever.invoke("как запустить несколько корутин параллельно?")
print("\nС компрессией:")
print(compressed_docs[0].page_content if compressed_docs else "ничего не найдено")
# "asyncio.gather() позволяет запускать несколько корутин параллельно и дождаться всех результатов."
Гибридный поиск: BM25 + векторный
Векторный поиск хорош для семантической близости, но плохо справляется с точными терминами: названиями функций, кодами ошибок и т.д.
BM25 это классический полнотекстовый поиск, который находит точные совпадения, но не понимает смысл. Гибридный поиск берёт лучшее из обоих подходов.
Гибридный поиск объединяет оба:
from dotenv import load_dotenv
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain_classic.retrievers import EnsembleRetriever
from langchain_core.documents import Document
load_dotenv()
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
texts = [
"asyncio.gather() запускает несколько корутин параллельно.",
"asyncio.sleep() приостанавливает выполнение корутины на заданное время.",
"FastAPI использует async def для асинхронных эндпоинтов.",
"Pydantic BaseModel используется для валидации данных в FastAPI.",
"SQLAlchemy AsyncSession нужен для асинхронной работы с базой данных.",
"HTTPException в FastAPI вызывается для возврата HTTP-ошибок.",
]
docs = [Document(page_content=t) for t in texts]
# Векторный retriever
vectorstore = FAISS.from_documents(docs, embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# BM25 retriever (точный поиск по ключевым словам)
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 3
# Ensemble: объединяем с весами
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6], # 40% BM25, 60% векторный
)
# Тест: точный термин — BM25 справится лучше
results = ensemble_retriever.invoke("asyncio.gather")
print("Гибридный поиск по 'asyncio.gather':")
for doc in results:
print(f" - {doc.page_content}")
Ожидаемый вывод:
Гибридный поиск по 'asyncio.gather':
- asyncio.gather() запускает несколько корутин параллельно.
- asyncio.sleep() приостанавливает выполнение корутины на заданное время.
- FastAPI использует async def для асинхронных эндпоинтов.
- HTTPException в FastAPI вызывается для возврата HTTP-ошибок.
- SQLAlchemy AsyncSession нужен для асинхронной работы с базой данных.
- Pydantic BaseModel используется для валидации данных в FastAPI.
EnsembleRetriever возвращает объединение результатов обоих retrievers.
BM25 поднимает наверх документы с точными совпадениями (asyncio.gather, asyncio.sleep),
векторный поиск добавляет семантически близкие.
При базе из 6 документов с k=3 у каждого retriever итог содержит все 6, сначала наиболее релевантные
по точному совпадению, затем по смыслу.
Веса подбираются экспериментально. Для технических запросов с точными терминами больший вес BM25. Для концептуальных вопросов, больший вес векторного.
Re-ranking: переупорядочивание результатов
После первичного поиска результаты можно переранжировать более мощной моделью. Это двухэтапный подход:
- Быстрый retriever находит N кандидатов (например, 20)
- Re-ranker оценивает каждый кандидат относительно вопроса и выбирает top-K
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain_classic.retrievers import EnsembleRetriever
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.documents import Document
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# Пример базы знаний для re-ranking
texts = [
"asyncio.gather() запускает несколько корутин параллельно.",
"asyncio.sleep() приостанавливает выполнение корутины.",
"FastAPI использует async def для асинхронных эндпоинтов.",
"aiohttp — библиотека для асинхронных HTTP запросов.",
"HTTPException в FastAPI вызывается для возврата HTTP ошибок.",
"SQLAlchemy AsyncSession нужен для асинхронной работы с БД.",
]
docs_for_search = [Document(page_content=t) for t in texts]
vectorstore = FAISS.from_documents(docs_for_search, embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
bm25_retriever = BM25Retriever.from_documents(docs_for_search)
bm25_retriever.k = 3
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6],
)
rerank_prompt = ChatPromptTemplate.from_messages([
("system", """Оцени релевантность фрагмента документа для ответа на вопрос.
Верни только число от 0 до 10, где:
0 — фрагмент совершенно не относится к вопросу
10 — фрагмент напрямую отвечает на вопрос
Только число, без пояснений."""),
("human", "Вопрос: {question}\n\nФрагмент: {document}"),
])
scorer = rerank_prompt | model | StrOutputParser()
def rerank_documents(question: str, docs: list[Document], top_k: int = 3) -> list[Document]:
"""Переранжирует документы по релевантности к вопросу."""
scored = []
for doc in docs:
try:
score_str = scorer.invoke({
"question": question,
"document": doc.page_content,
})
score = float(score_str.strip())
except (ValueError, Exception):
score = 0.0
scored.append((score, doc))
# Сортируем по убыванию оценки
scored.sort(key=lambda x: x[0], reverse=True)
print("Оценки re-ranking:")
for score, doc in scored[:top_k]:
print(f" {score:.1f}: {doc.page_content[:70]}...")
return [doc for _, doc in scored[:top_k]]
# Пример использования с retriever
question = "Как сделать асинхронный HTTP запрос?"
candidates = ensemble_retriever.invoke(question) # 6 кандидатов
reranked = rerank_documents(question, candidates, top_k=3)
Re-ranking с помощью LLM добавляет N вызовов API (по одному на документ).
Для больших объёмов используйте специализированные cross-encoder модели, например sentence-transformers.
Conversational RAG: RAG с памятью диалога
Главная проблема RAG + диалог, пользователь говорит "расскажи об этом подробнее", а retriever не понимает контекст.
Решение: перед поиском переформулировать последний вопрос пользователя в самодостаточный запрос, используя историю диалога как контекст.
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# VectorStore
texts = [
"asyncio.gather() запускает корутины параллельно и возвращает список результатов.",
"asyncio.create_task() создаёт задачу из корутины без ожидания её завершения.",
"asyncio.run() — точка входа для запуска асинхронного кода в синхронном контексте.",
"pytest-asyncio позволяет тестировать async def функции с помощью @pytest.mark.asyncio.",
"aiohttp — библиотека для асинхронных HTTP запросов, альтернатива requests.",
]
vectorstore = FAISS.from_texts(texts, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# Шаг 1: переформулируем вопрос с учетом истории
contextualize_prompt = ChatPromptTemplate.from_messages([
("system", """Используя историю диалога и последний вопрос пользователя,
сформулируй самодостаточный поисковый запрос. Если вопрос уже самодостаточен, верни его без изменений.
Верни только запрос, без пояснений."""),
MessagesPlaceholder(variable_name="history"),
("human", "{question}"),
])
contextualize_chain = contextualize_prompt | model | StrOutputParser()
# Шаг 2: Промпт RAG с историей
rag_prompt = ChatPromptTemplate.from_messages([
("system", """Отвечай на вопрос только на основе контекста.
Если ответа нет, скажи об этом.
Контекст:
{context}"""),
MessagesPlaceholder(variable_name="history"),
("human", "{question}"),
])
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# Сборка: contextualize → retriever → generate
def make_rag_chain(retriever):
return (
RunnablePassthrough.assign(
# Переформулируем вопрос с учетом истории, используем для поиска
context=contextualize_chain | retriever | format_docs,
)
| rag_prompt
| model
| StrOutputParser()
)
conversational_rag = make_rag_chain(retriever)
# Оборачиваем с историей
store = {}
def get_session_history(session_id: str) -> ChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
chain_with_history = RunnableWithMessageHistory(
conversational_rag,
get_session_history,
input_messages_key="question",
history_messages_key="history",
)
config = {"configurable": {"session_id": "dev_session"}}
def chat(question: str) -> str:
return chain_with_history.invoke({"question": question}, config=config)
# Тест, контекстный диалог
print(chat("Расскажи про asyncio.gather"))
print()
print(chat("А как его тестировать?"))
# Второй вопрос переформулируется в "как тестировать asyncio.gather?"
print()
print(chat("Есть ли альтернативы для HTTP запросов?"))
# Переформулируется с учетом контекста про asyncio
Выбор техники под задачу
| Проблема | Техника | Стоимость |
|---|---|---|
| Размытые вопросы | Query rewriting | +1 вызов LLM |
| Один аспект не покрывает тему | Multi-query | +1 вызов LLM + больше поисков |
| В чанках много лишнего | Contextual compression | +N вызов LLM |
| Точные термины не находятся | Гибридный поиск (BM25) | Нет вызов LLM |
| Нерелевантные результаты | Re-ranking | +N вызов LLM |
| Диалог теряет контекст | Conversational RAG | +1 вызов LLM |
Начинайте с гибридного поиска, он бесплатный. Добавляйте остальные техники по мере выявления конкретных проблем.
Распространённые ошибки
1. Все техники сразу
Не нужно применять все техники сразу. Начните с базового RAG, найди конкретную проблему и добавь только ту технику, которая её решает.
2. Слишком много кандидатов для re-ranking
# Дорого: 20 вызовов LLM на каждый запрос
candidates = retriever.invoke(question) # k=20
reranked = rerank_documents(question, candidates)
Оптимально: 5-10 кандидатов для re-ranking.
3. Contextual compression убирает нужный текст
Компрессор может ошибиться и выбросить важный контекст. Если после компрессии документ оказался пустым, подставляйте оригинал документа.
4. Query rewriting меняет смысл вопроса
Переформулировка может исказить вопрос. Всегда добавляйте оригинальный вопрос к набору запросов, как в примере с multi-query.
5. Не сохранять индекс при использовании гибридного поиска
BM25Retriever создаётся из документов в памяти и не сохраняется на диск. При перезапуске нужно пересоздавать.
Пример кода
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain_classic.retrievers import EnsembleRetriever
from langchain_core.documents import Document
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# --- База знаний ---
raw_texts = [
"asyncio.gather() принимает корутины и запускает их параллельно. Возвращает список результатов в том же порядке.",
"asyncio.create_task() создаёт Task из корутины. Задача начинает выполняться немедленно в фоне.",
"asyncio.sleep(n) приостанавливает корутину на n секунд без блокировки event loop.",
"asyncio.wait() похож на gather, но возвращает множества done и pending задач.",
"aiohttp.ClientSession используется для асинхронных HTTP-запросов. Сессию нужно закрывать явно.",
"httpx.AsyncClient — современная альтернатива aiohttp с похожим API на requests.",
"pytest-asyncio добавляет поддержку async def тестов. Используй декоратор @pytest.mark.asyncio.",
"asyncio.timeout() (Python 3.11+) устанавливает таймаут на выполнение корутины.",
"FastAPI автоматически запускает async def эндпоинты в event loop без дополнительных настроек.",
"SQLAlchemy AsyncSession требует async with для управления транзакциями.",
]
docs = [Document(page_content=t, metadata={"id": i}) for i, t in enumerate(raw_texts)]
# --- Гибридный retriever ---
vectorstore = FAISS.from_documents(docs, embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 4
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6],
)
# --- Query rewriting ---
rewrite_prompt = ChatPromptTemplate.from_messages([
("system", "Перефразируй вопрос для технического поиска по документации. Только запрос, без пояснений."),
("human", "{question}"),
])
rewriter = rewrite_prompt | model | StrOutputParser()
# --- Re-ranking ---
rerank_prompt = ChatPromptTemplate.from_messages([
("system", "Оцени релевантность фрагмента к вопросу от 0 до 10. Только число."),
("human", "Вопрос: {question}\n\nФрагмент: {document}"),
])
scorer = rerank_prompt | model | StrOutputParser()
def rerank(question: str, docs: list[Document], top_k: int = 3) -> list[Document]:
scored = []
for doc in docs:
try:
s = float(scorer.invoke({"question": question, "document": doc.page_content}).strip())
except Exception:
s = 0.0
scored.append((s, doc))
scored.sort(key=lambda x: x[0], reverse=True)
return [d for _, d in scored[:top_k]]
# --- Промпт RAG ---
rag_prompt = ChatPromptTemplate.from_messages([
("system", """Отвечай только на основе контекста. Если ответа нет, скажи об этом.
Контекст:
{context}"""),
("human", "{question}"),
])
def format_docs(docs):
return "\n\n".join(f"[{i+1}] {d.page_content}" for i, d in enumerate(docs))
# --- Продвинутая RAG цепочка ---
chain = (
RunnablePassthrough.assign(
rewritten=lambda x: rewriter.invoke({"question": x["question"]}),
)
| RunnablePassthrough.assign(
candidates=lambda x: hybrid_retriever.invoke(x["rewritten"]),
)
| RunnablePassthrough.assign(
top_docs=lambda x: rerank(x["rewritten"], x["candidates"], top_k=3),
)
| RunnablePassthrough.assign(
context=lambda x: format_docs(x["top_docs"]),
)
| rag_prompt
| model
| StrOutputParser()
)
# --- Тестирование ---
questions = [
"Как запустить несколько задач одновременно?",
"asyncio.gather vs asyncio.wait, в чём разница?",
"Что такое LangChain?",
"Какие есть ORM вна языке Nim?",
]
for q in questions:
print(f"\nВопрос: {q}")
answer = chain.invoke({"question": q})
print(f"Ответ: {answer}")
print("-" * 60)
Практическое задание
Улучшите RAG систему из урока 8, применив продвинутые техники.
Требования:
1) Добавьте гибридный поиск (BM25 + векторный, веса 0.3 / 0.7)
2) Реализуйте multi-query: генерируйте 3 варианта запроса, объединяйте и дедуплицируйте результаты
3) Добавьте re-ranking: из всех кандидатов выбирайте top-3 по оценке LLM
4) Реализуйте conversational RAG: система помнит историю диалога и переформулирует вопросы с её учётом
5) Добавьте метрику качества: после каждого ответа выводите, сколько документов было найдено до и после re-ranking
Ожидаемый вывод:
Вопрос: Как делать параллельные запросы?
Варианты запроса: ['параллельные HTTP запросы asyncio', 'concurrent requests Python', ...]
Кандидатов до re-ranking: 8
Топ-3 после re-ranking: [7.0, 6.5, 5.0]
Ответ: ...
Вопрос: А с таймаутом?
[Переформулировка с учётом контекста: "параллельные HTTP запросы с таймаутом"]
...
Итоги урока
Мы освоили все ключевые техники RAG. Теперь нужно убедиться, что система работает правильно и научиться это измерять. В следующем уроке разберём оценку и отладку LLM приложений. Научимся использовать tracing с помощью LangSmith, метрики качества RAG (precision, recall, faithfulness) и как находить проблемы в продакшн.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться разрабатывать AI агентов, пишите, обсудим условия
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru