A Plain-English Walkthrough of RAG, From In-Memory Demos to Milvus
1. What is RAG?
RAG = Retrieval + Augmented + Generation
RAG Technical Implementation Flow
2. The Role of RAG
3. Benefits of RAG
4. The Complete RAG Workflow (Core Knowledge Points)
This diagram splits the previous plain-text workflow into two clear zones:
- Upper Zone (Blue · Offline Indexing): Document preparation → Chunking → Vectorization → Store in vector database. This is work done once in advance offline, turning your materials into searchable vectors.
- Lower Zone (Green · Online Query): User asks a question → Retrieve relevant chunks from the vector database → Assemble a Prompt → Large model generates an answer. This is the path taken in real-time for each user query.
- Dashed arrows indicate that the "Retrieval" step needs to query the vector database built in the upper zone.
- Bottom summarizes the five core values of RAG: Reduces hallucinations, accesses private documents, real-time knowledge updates, traceable citations, and low cost.
Under each step, there are specific APIs as follows:
| Step | Technology Choices (Annotated in Diagram) |
|---|---|
| Document Preparation | PyPDFLoader / WebBaseLoader / UnstructuredLoader |
| Chunking | RecursiveCharacterTextSplitter (splits by semantic boundaries, 500 chars/chunk + 50 char overlap) |
| Vectorization | OpenAIEmbeddings + text-embedding-v3 (or bge-large-zh) |
| Storage | FAISS (local) / Chroma / Milvus / Pinecone / Qdrant |
| Retrieval | similarity_search + MMR / Rerank |
| Prompt Assembly | ChatPromptTemplate (System + Context + Question) |
| Generation | ChatOpenAI (can connect to qwen-plus / gpt-4o) |
| Advanced Capability | One-Line Description |
|---|---|
| Rerank | Coarse vector retrieval of 20 items → Fine-rank model picks the most accurate 3, huge improvement |
| HyDE | Let the LLM hypothesize an answer first, use the "hypothetical answer" to retrieve, more accurate than the original question |
| Query Rewriting | Rewrite colloquial questions into retrieval-friendly queries, can decompose multi-turn questions |
| Hybrid Search | Combine vector search + BM25 keyword search for more comprehensive recall |
| GraphRAG | Build a knowledge graph, suitable for complex relational reasoning (open-sourced by Microsoft) |
| Agentic RAG | Agent autonomously decides when to retrieve and what to retrieve, can follow up with multiple queries to complete information |
API Summary:
5. Usage Examples
1. Basic Example
Prepare the embedding model
In the code below, the documents are pre-defined by us. Let's run through the basic RAG flow.
The code is as follows
import 'dotenv/config';
import {ChatOpenAI, OpenAIEmbeddings} from '@langchain/openai';
import {Document} from '@langchain/core/documents';
import {MemoryVectorStore} from '@langchain/classic/vectorstores/memory';
//OpenAIEmbeddings is the embedding model
const model = new ChatOpenAI({
model: process.env.MODEL_NAME,
temperature: 0,
apiKey: process.env.OPENAI_API_KEY,
confuguration:{
baseURL: process.env.OPENAI_BASE_UR,
}
});
//Create an embedding model to convert documents into vectors
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDINGS_MODEL_NAME,
confuguration:{
baseURL: process.env.OPENAI_BASE_UR,
}
});
const docments =[
new Document({
pageContent: 'Mingming is a lively and cute child who likes playing soccer and singing. He is always full of energy and curiosity, enjoying exploring new things and making new friends. He is 9 years old this year, studies at Shuangta Elementary School, and has good grades.',
metadata: {
chapter:1,
character:'Mingming',
type:"Character Introduction",
mood:'Lively and Cute'
}
}),
new Document({
pageContent: `In the distant "Cyber Valley", there lived a young mage named Little Chain (LangChain). Little Chain had a worry: although the wise old man (Large Model LLM) in the valley was very knowledgeable, whenever a question slightly involved new things outside the valley (private knowledge/PDF documents), the old man would make things up (hallucination) because his "knowledge wasn't updated".
To solve this problem, Little Chain decided to embark on the cultivation journey of "RAG (Retrieval-Augmented Generation)".
One day, Little Chain discovered an ancient secret manual (PDF document) in a cave. But this manual was too thick for the old man to read all at once. So, Little Chain invited a tailor master (Text Splitter) to cut the manual into small slips of paper (Documents), making them easier to process later.`,
metadata: {
chapter:1,
character:'Huahua and Mingming',
type:"Epilogue",
mood:'Warm'
}
}),
new Document({
pageContent: `Then, Little Chain found three clever "translators"—these are Embeddings (vectorization models, like text-embedding-v3). Little Chain handed each slip of paper to the translators, who swiftly transformed the text into strings of magical numerical codes (Vectors).
Little Chain stored these numerical codes and their corresponding original text slips into a magical "Memory Treasure Box" (MemoryVectorStore). This treasure box lived in Little Chain's backpack, and although it would be emptied if the process restarted (volatile), it was very convenient and fast for daily cultivation.`,
metadata: {
chapter:1,
character:'Huahua and Mingming',
type:"Epilogue",
mood:'Warm'
}
}),
new Document({
pageContent: 'Huahua is a quiet and smart child who likes reading and drawing. He always sits quietly in a corner reading and drawing, as if the outside world has nothing to do with him. He is 10 years old this year, studies at Shuangta Elementary School, and has good grades.',
metadata: {
chapter:1,
character:'Huahua',
type:"Character Introduction",
mood:'Quiet and Smart'
}
}),
new Document({
pageContent: 'This autumn, the school sports meet was held as scheduled, and both Mingming and Huahua participated. In the relay race, they cooperated very well and ultimately won first place. From then on, they started becoming friends. Whenever Mingming encountered difficulties, Huahua would be the first to step forward and help him solve them. Whenever Huahua was unhappy, Mingming would be the first to step forward and encourage him. This is how the two of them became best friends.',
metadata: {
chapter:1,
character:'Huahua and Mingming',
type:"Friendship Introduction",
mood:'Encouraging'
}
}),
new Document({
pageContent: 'Years later, Mingming became a soccer player, and Huahua became a painter. They often went out together to play and reminisce about the good times of their childhood.',
metadata: {
chapter:1,
character:'Huahua and Mingming',
type:"Epilogue",
mood:'Warm'
}
}),
]
//Create an in-memory vector store and vectorize the documents into the database.
const vectorStore = await MemoryVectorStore.fromDocuments(docments,embeddings);
//Create a retriever to fetch document chunks from the vector store.
const retriever = vectorStore.asRetriever({k:3});
//Create an array of questions to test the retriever.
const questions = ['How did the friendship between Huahua and Mingming start?'];
for(const question of questions){
//Retrieve document chunks
const retrivedDocs = await retriever.invoke(question);
//Query document similarity
// const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);
// retrivedDocs.forEach((doc, i)=>{
// const scordedResult = scoredResults.find(([sciredDoc])=>{
// sciredDoc.pageContent === doc.pageContent
// })
// const score = scordedResult?.[1] || null;
// const similarity = score !== null ? (1-score).toFixed() : 'N/A';
// console.log(`Document ${i+1}. ${doc.pageContent} (Similarity: ${similarity})`);
// })
//Create a story context to generate the story answer.
const context = retrivedDocs.map((doc)=> doc.pageContent).join('\n');
//Create a prompt to generate the story answer.
const prompt=`You are a storytelling teacher. Based on the story excerpts above, please retell this story in warm language. If the story doesn't mention a detail, say the story doesn't mention that detail.
Story Excerpts: ${context}
Question: ${question}
Teacher's Answer:
`;
const response = await model.invoke(prompt);
console.log(response.content, 89999);
}
This example perfectly runs through the diagram below
- Document preparation is: documents, which contain many document fragments.
- Chunking: We didn't chunk, we directly provided document fragments.
- Vectorization and Vector Storage:
const vectorStore = await -MemoryVectorStore.fromDocuments(docments,embeddings);
- User Question:
const questions = ['How did the friendship between Huahua and Mingming start?'];
- Check retrieval:
const retriever = vectorStore.asRetriever({k:3});
const retrivedDocs = await retriever.invoke(question);
Assemble prompt:
const context = retrivedDocs.map((doc)=> doc.pageContent).join('\n');
//Create a prompt to generate the story answer.
const prompt=`You are a storytelling teacher. Based on the story excerpts above, please retell this story in warm language. If the story doesn't mention a detail, say the story doesn't mention that detail.
Story Excerpts: ${context}
Question: ${question}
Teacher's Answer:
`;
Large model processes the prompt:
const response = await model.invoke(prompt);
2. Advanced Example
In real life, our documents exist in forms like Word, PDF, YouTube, URLs, etc., so we must use the corresponding loaders for these file types to load the files, and then add them to the embedding model.
Simply put, these loaders will cut the corresponding files into document fragments and then pass them to the embedding model for processing. Official loader address: https://docs.langchain.com/oss/python/integrations/document_loaders
If the file is too large at this point, you need Splitter and Chunk cutting. The commonly used cutting API is:
import {RecursiveCharacterTextSplitter} from '@langchain/textsplitters';
Here's the code: If I want to use DocxLoader to cut a local Word file, then store the cut files into the in-memory vector database MemoryVectorStore, and then wrap your question into a prompt for the large model to use:
import 'dotenv/config';
import 'cheerio';//jQuery running on the backend, allowing you to easily traverse, manipulate, and render HTML on the backend. Common use cases: web scraping, HTML processing, SEO optimization testing
import { DocxLoader } from "@langchain/community/document_loaders/fs/docx";
//Need to install pnpm add @langchain/community @langchain/core mammoth, mammoth is a library for processing Word documents
import {RecursiveCharacterTextSplitter} from '@langchain/textsplitters';
import {ChatOpenAI, OpenAIEmbeddings} from '@langchain/openai';
import {MemoryVectorStore} from '@langchain/classic/vectorstores/memory';//Local in-memory vector database
const model = new ChatOpenAI({
temperature: 0,
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_API_BASE_URL,
}
});
const embeddings = new OpenAIEmbeddings({
model: process.env.EMBEDDINGS_MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_API_BASE_URL,
}
});
const loader = new DocxLoader("src/day-006/word-test.docx");
const docs = await loader.load();
console.log(docs)
//Universal splitter
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400, //Each chunk is about 400 characters, with 100 characters overlapping.
chunkOverlap: 100,//Chunks overlap by 100 characters.
separators:["。","!","?",]//Chunk separators, default is newline character.
});
const splitDocs = await textSplitter.splitDocuments(docs);
console.log("Split documents",splitDocs);
const vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, embeddings);
const retriever = vectorStore.asRetriever({k:2});
const question =['What impact did the father\'s death have on the author\'s attitude towards life?']
for(const q of question){
const retrievedDoc = await retriever.invoke(q);
const context = retrievedDoc.map((doc, i)=>`[Fragment ${i}]\n${doc.pageContent}\n`).join("\n\n----\n\n");
const prompt = `You are a document assistant, you need to answer questions based on the provided document content.
Document Content:
${context}
Question:
${q}
Your Answer:
`;
const response = await model.invoke(prompt);
console.log(response.content);
console.log('End')
}
Write your file content inside the src/day-006/word-test.docx document. Whether you are reading Word, PDF, or web pages, just use the corresponding loader. The rest of the code is shared. For example, to read webpage data, use the following
3. Vector Database
In the examples above, our data was stored in an in-memory vector database. The vector database to introduce now is: Milvus
1. Difference between MySQL and Milvus
This means that within an agent project, MySQL and Milvus coexist and are in a collaborative relationship, not a replacement one.
2. Installing Milvus
- Install Docker, http://www.docker.com After installation, execute the docker command. No errors indicate success.
- Open the Docker client, create a folder named milvus, and configure the folder into Docker—this tells Docker where to store the vector data next.
- Download Milvus, https://github.com/milvus-io/milvus/releases Find the ./milvus-standalone-docker-compose.yml file.
- Run the command: docker compose -f ./milvus-standalone-docker-compose.yml up -d This command will automatically download Milvus.
- Once running, Docker looks like this:
- Install the data visualization tool (GUI) for Milvus: attu Download address: https://github.com/zilliztech/attu?tab=readme-ov-file#quick-start
- Connect to it the same way you connect to MySQL.
3. Using Milvus in Node.js Code
import "dotenv/config";
import {MilvusClient, MetricType } from '@zilliz/milvus-sdk-node';
import { OpenAIEmbeddings } from "@langchain/openai";
const COLLECTION_NAME = "langchain_collection";
const VECTOR_DIM = 1024;
const embeddeings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
mode: process.env.EMBEDDING_MODE,
configuration: {
baseURL: process.env.EMBEDDING_BASE_URL,
},
dimension: VECTOR_DIM,
});
const client = new MilvusClient({
address: 'localhost:19530',
});
async function main(){
await client.connect();
const query="I want to see diary entries about outdoor activities"
const queryVector = await embeddeings.embedQuery(query);
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
vector: queryVector,
limit: 5,
output_fields: ['content'],
});
searchResult.result.forEach(item => {
console.log(`${item.score}: ${item.content}`);
});
}
main();
4. Using with a Large Model
import "dotenv/config";
import {MilvusClient, MetricType } from '@zilliz/milvus-sdk-node';
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
const COLLECTION_NAME = "langchain_collection";
const VECTOR_DIM = 1024;
const model = new ChatOpenAI({
temperature: 0.7,
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.CHAT_BASE_URL,
},
});
temperature: 0,
});
const embeddeings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
mode: process.env.EMBEDDING_MODE,
configuration: {
baseURL: process.env.EMBEDDING_BASE_URL,
},
dimension: VECTOR_DIM,
});
const client = new MilvusClient({
address: 'localhost:19530',
});
//Retrieve vector data from Milvus
async function retrieveRelevantDoc(query, k=2){
const queryVector = await embeddeings.embedQuery(query);
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
vector: queryVector,
limit: k,
metric_type: MetricType.COSINE,
output_fields: ['id','content'],
});
return searchResult.result
}
async function answerQuestion(query, k=2){
const relevantDocs = await retrieveRelevantDoc(query, k);
const context = relevantDocs.map(doc => doc.content).join('\n');
const prompt = `You are a caring diary assistant. Based on the user's diary questions, answer the following question in a friendly tone.
Please base your answer on the following diary content:
${context}
User Question: ${query}
Answer Requirements:
1. Answer should be friendly, using more colloquial expressions.
2. Avoid using technical jargon.
AI Assistant's Answer:
`
const response = await model.invoke(prompt)
return response.content;
}
function main(){
await client.connectPromise;
await answerQuestion('What can make me happier?',2)
}
main()
The Milvus vector database has methods for query, insert, update, and delete. When storing, you can first use Split to cut the document into chunks and then store them in Milvus. Then, when querying, you don't need to do the cutting process again.
6. Summary
The main problem RAG solves is large model hallucination, which is when it clearly doesn't know something but makes up an answer with a straight face. The solution is: RAG searches the knowledge base for documents based on the prompt, adds the parts with extremely high similarity to the large model's knowledge background, and the large model answers your question based on this new knowledge base.