Where we left off
Lesson 20 swapped in PGVector but only proved retrieval. This lesson builds the complete RAG chain, retrieve relevant chunks, stuff them into a prompt, ask the model to answer using only that context, and demonstrates the thing InMemoryVectorStore structurally cannot do: run the program again, later, and the embeddings are already there.
Checking before re-embedding
def already_ingested(dsn: str, collection_name: str) -> bool: with psycopg.connect(dsn) as conn: row = conn.execute( """ SELECT count(*) FROM langchain_pg_embedding e JOIN langchain_pg_collection c ON c.uuid = e.collection_id WHERE c.name = %s """, (collection_name,), ).fetchone() return row[0] > 0PGVector stores its data in two tables it manages itself, langchain_pg_collection (one row per named collection) and langchain_pg_embedding (one row per embedded chunk, linked to its collection). Checking this before calling add_documents again means a second run of this script skips re-embedding entirely, no repeated API calls, no duplicate rows, the whole point of a persistent store.
The RAG chain itself
retriever = vector_store.as_retriever(search_kwargs={"k": 2})
prompt = ChatPromptTemplate.from_template( "Answer the question using only the context below.\n\n" "Context:\n{context}\n\nQuestion: {question}")
chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | model | StrOutputParser())as_retriever() wraps any VectorStore (this one, or InMemoryVectorStore, identically) as a Runnable, so it composes with | exactly like every chain since LangChain Lesson 6. retriever | format_docs retrieves the top k chunks and joins them into one string; RunnablePassthrough() passes the original question through unchanged, both land in the prompt's context and question slots. Everything past this point, the prompt, the model call, the output parser, is unchanged from the LCEL patterns taught throughout the LangChain course.
"Using only the context" is doing real work
Asking this chain about baking bread, the model correctly says it doesn't know, because the retrieved context is about pizza dough, not bread, rather than hallucinating an answer or blending in outside knowledge. That instruction in the prompt is what keeps a RAG system honest about the boundary between "what's in your documents" and "what the model happens to already know."
Run this lesson's script twice. The first run embeds and stores six chunks. The second run prints "already ingested, skipping" and goes straight to answering, proof the data survived between runs.
Checkpoint
PGVectorstores data inlangchain_pg_collectionandlangchain_pg_embedding, checkable directly to avoid redundant re-ingestion.as_retriever(): wraps anyVectorStoreas a composableRunnable, identical forPGVectorandInMemoryVectorStore.- A full RAG chain is just retrieval, formatting, a prompt, a model call, and a parser, composed with
|, nothing new syntactically past what the LangChain course already taught.
If anything here still feels unclear, ask before moving to Lesson 22, where we scale a filtered search past six rows and hit a genuinely surprising failure mode.