Advanced retrieval patterns and autonomous agents — the final piece of Phase 6
What This Module Covers
Advanced Retrievers → get better results from your vector store
LangChain Agents → LLMs that think, plan, and take actions
These are the two most powerful concepts in LangChain. By the end you'll understand how production RAG systems go beyond simple similarity search — and how agents work differently from chains.
Part 1 — Advanced Retrievers
Why Basic Retrieval Isn't Always Enough
Your current RAG system does this:
User question → embed → find similar chunks → inject into prompt
This works well. But it has limitations:
Problem 1 — One query, one perspective
"What are the benefits of RAG?"
→ Only finds chunks that literally discuss "benefits"
→ Misses chunks that discuss advantages, pros, improvements
Problem 2 — Irrelevant sentences in relevant chunks
A chunk might be 80% relevant and 20% noise
→ That 20% noise confuses the LLM
Problem 3 — User question is vague or badly phrased
"Tell me about the thing we discussed earlier"
→ Terrible embedding → terrible retrieval → bad answer
LangChain has specific retrievers that solve each problem.
Create src/05_retrievers.js:
import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai"; import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory"; import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf"; import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters"; import { MultiQueryRetriever } from "@langchain/classic/retrievers/multi_query"; // MultiQueryRetriever = generates multiple versions of the question // searches with all versions → merges results // solves Problem 1
import { ContextualCompressionRetriever } from "@langchain/classic/retrievers/contextual_compression"; // ContextualCompressionRetriever = compresses retrieved chunks // removes irrelevant sentences from each chunk // solves Problem 2
import { LLMChainExtractor } from "@langchain/classic/retrievers/document_compressors/chain_extract"; // LLMChainExtractor = uses LLM to extract only relevant parts // from each retrieved chunk
import { Document } from "@langchain/core/documents"; // Document = LangChain's document class // { pageContent, metadata }
import * as dotenv from "dotenv"; dotenv.config();
// ───────────────────────────────────────── // SETUP — Build vector store once // Reuse across all examples // ─────────────────────────────────────────
async function buildVectorStore() { console.log("Setting up vector store...");
const loader = new PDFLoader("./sample.pdf"); const docs = await loader.load();
const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 600, chunkOverlap: 100, }); const chunks = await splitter.splitDocuments(docs);
const vectorStore = await MemoryVectorStore.fromDocuments( chunks, new OpenAIEmbeddings({ model: "text-embedding-3-small" }) );
console.log(`✅ Vector store ready — ${chunks.length} chunks\n`); return vectorStore; }
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
// ───────────────────────────────────────── // RETRIEVER 1 — Basic retriever (baseline) // The simple retriever you already know // ─────────────────────────────────────────
async function retriever1(vectorStore) { console.log("─".repeat(55)); console.log("RETRIEVER 1: Basic similarity search (baseline)"); console.log("─".repeat(55));
const retriever = vectorStore.asRetriever({ k: 3 }); // basic retriever — searches with the EXACT question as-is
const question = "What are the main benefits?"; console.log(`Question: "${question}"\n`);
const docs = await retriever.invoke(question); // embeds question once → finds top 3 similar chunks
console.log(`Retrieved: ${docs.length} chunks`); docs.forEach((doc, i) => { console.log(` Chunk ${i + 1}: ${doc.pageContent.substring(0, 80)}...`); });
console.log("\nLimitation: Only searched for this ONE version of the question"); console.log("Might miss relevant content phrased differently.\n"); }
// ───────────────────────────────────────── // RETRIEVER 2 — MultiQueryRetriever // Generates multiple versions of the question // Searches with ALL versions — merges unique results // Solves: "missing relevant content" problem // ─────────────────────────────────────────
async function retriever2(vectorStore) { console.log("─".repeat(55)); console.log("RETRIEVER 2: MultiQueryRetriever"); console.log("─".repeat(55));
const baseRetriever = vectorStore.asRetriever({ k: 3 });
const multiQueryRetriever = MultiQueryRetriever.fromLLM({ llm: llm, // LLM used to generate alternative questions
retriever: baseRetriever, // the underlying retriever to use for each question
queryCount: 3, // generate 3 alternative versions of the question // then search with all 3 — merge unique results });
const question = "What are the main benefits?"; console.log(`Original question: "${question}"\n`); console.log("MultiQueryRetriever will generate ~3 alternative versions:"); console.log(' e.g. "What advantages does this provide?"'); console.log(' e.g. "What are the positive aspects?"'); console.log(' e.g. "What value does this offer?"'); console.log("Then search with ALL versions → merge unique results\n");
const docs = await multiQueryRetriever.invoke(question); // internally generates multiple questions // searches with each → deduplicates results // returns combined unique documents
console.log(`Retrieved: ${docs.length} unique chunks (vs 3 with basic retriever)`); docs.forEach((doc, i) => { console.log(` Chunk ${i + 1}: ${doc.pageContent.substring(0, 80)}...`); });
console.log("\nBenefit: Finds more relevant content by searching"); console.log("from multiple angles.\n"); }
// ───────────────────────────────────────── // RETRIEVER 3 — ContextualCompressionRetriever // Compresses retrieved chunks — removes irrelevant parts // Solves: "noisy chunks" problem // ─────────────────────────────────────────
async function retriever3(vectorStore) { console.log("─".repeat(55)); console.log("RETRIEVER 3: ContextualCompressionRetriever"); console.log("─".repeat(55));
const baseRetriever = vectorStore.asRetriever({ k: 3 });
// Create the compressor — uses LLM to extract relevant parts const compressor = LLMChainExtractor.fromLLM(llm); // LLMChainExtractor looks at each retrieved chunk // and extracts ONLY the sentences relevant to the question // discards irrelevant sentences from the chunk
const compressionRetriever = new ContextualCompressionRetriever({ baseCompressor: compressor, // the LLM-based compressor
baseRetriever: baseRetriever, // retrieve chunks first with base retriever // then compress each chunk with the compressor });
const question = "What are the key risks mentioned?"; console.log(`Question: "${question}"\n`);
console.log("Process:"); console.log("1. Retrieve top 3 chunks (some may be partially relevant)"); console.log("2. For each chunk — LLM extracts only relevant sentences"); console.log("3. Return compressed, focused content\n");
const compressedDocs = await compressionRetriever.invoke(question); // Step 1: basic retriever gets 3 chunks // Step 2: LLMChainExtractor compresses each chunk // Step 3: returns only the relevant parts
console.log(`Compressed results: ${compressedDocs.length} chunks`); compressedDocs.forEach((doc, i) => { console.log(`\n Compressed chunk ${i + 1}:`); console.log(` "${doc.pageContent}"`); // much shorter than original — only relevant sentences kept });
console.log("\nBenefit: Less noise in context → LLM gives more focused answers.\n"); console.log("Tradeoff: Slower (extra LLM call per chunk) + costs more.\n"); }
// ───────────────────────────────────────── // RETRIEVER 4 — Ensemble (combine retrievers) // Use multiple retrievers and merge results // Good for: keyword AND semantic search combined // ─────────────────────────────────────────
async function retriever4(vectorStore) { console.log("─".repeat(55)); console.log("RETRIEVER 4: Using retrievers with different settings"); console.log("─".repeat(55));
// Retriever A — semantic similarity (good for meaning) const semanticRetriever = vectorStore.asRetriever({ k: 3, searchType: "similarity", });
// Retriever B — MMR (good for diversity) const diverseRetriever = vectorStore.asRetriever({ k: 3, searchType: "mmr", searchKwargs: { fetchK: 10, lambda: 0.5 }, });
const question = "What are the main concepts?"; console.log(`Question: "${question}"\n`);
const semanticDocs = await semanticRetriever.invoke(question); const diverseDocs = await diverseRetriever.invoke(question);
// Merge and deduplicate results const allContent = new Set(); const merged = [];
[...semanticDocs, ...diverseDocs].forEach(doc => { const key = doc.pageContent.substring(0, 50); // use first 50 chars as dedup key if (!allContent.has(key)) { allContent.add(key); merged.push(doc); } });
console.log(`Semantic retriever: ${semanticDocs.length} chunks`); console.log(`Diverse retriever: ${diverseDocs.length} chunks`); console.log(`After merge+dedup: ${merged.length} unique chunks`);
merged.forEach((doc, i) => { console.log(` ${i + 1}. ${doc.pageContent.substring(0, 70)}...`); });
console.log("\nBenefit: Combines relevance + diversity for richer context.\n"); }
// ───────────────────────────────────────── // CHOOSING THE RIGHT RETRIEVER // ─────────────────────────────────────────
function retrieverGuide() { console.log("─".repeat(55)); console.log("GUIDE: Which retriever to use?"); console.log("─".repeat(55));
console.log(` Basic Retriever → Use when: questions are clear and specific → Cost: cheapest (no extra LLM calls) → Speed: fastest
MultiQueryRetriever → Use when: questions might be vague or have synonyms → Cost: moderate (generates N alternative questions) → Speed: moderate → Best for: general purpose RAG chatbots
ContextualCompressionRetriever → Use when: chunks are long and might contain noise → Cost: expensive (one LLM call per chunk) → Speed: slow → Best for: high-accuracy requirements (legal, medical)
MMR Retriever → Use when: answers need diverse information → Cost: same as basic (no extra LLM calls) → Speed: slightly slower than basic → Best for: summarization, broad questions
Recommendation for most apps: → Start with MMR retriever → Add MultiQuery if retrieval quality is poor → Add Compression only if answer quality needs boost and you can afford the extra cost `); }
async function main() { console.log("\n🔍 ADVANCED RETRIEVERS DEMO\n");
const vectorStore = await buildVectorStore();
await retriever1(vectorStore); await retriever2(vectorStore); await retriever3(vectorStore); await retriever4(vectorStore); retrieverGuide();
console.log("✅ Retrievers demo complete!"); }
main().catch(console.error);
Run:
node src/05_retrievers.js
Part 2 — LangChain Agents
Chain vs Agent — The Key Difference
CHAIN:
Fixed sequence of steps
Input → Step 1 → Step 2 → Step 3 → Output
You decide the steps at build time
Same steps every time
AGENT:
LLM decides the steps at runtime
Input → LLM thinks → maybe tool → LLM thinks → maybe tool → Output
Steps change based on the question
Can handle questions you didn't plan for
Chain example — always does these steps:
embed question → search → build prompt → call LLM
Agent example — decides at runtime:
"This needs a calculator and a web search"
→ calls calculator
→ calls web search
→ combines results
→ generates answer
Different question → different tools → different steps
Create src/06_agents.js:
import { ChatOpenAI } from "@langchain/openai"; import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"; import { AgentExecutor, createToolCallingAgent } from "@langchain/classic/agents"; import { tool } from "@langchain/core/tools"; import { z } from "zod"; import { RunnableWithMessageHistory } from "@langchain/core/runnables"; import { InMemoryChatMessageHistory } from "@langchain/core/chat_history"; import * as readline from "readline"; import * as dotenv from "dotenv"; dotenv.config();
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
// ───────────────────────────────────────── // DEFINE TOOLS // ─────────────────────────────────────────
// Tool 1 — Calculator const calculator = tool( async ({ expression }) => { try { // Simple math evaluator // In production use a proper math library const sanitized = expression .replace(/[^0-9+\-*/().% ]/g, "") // remove anything that's not math .trim();
const result = Function(`"use strict"; return (${sanitized})`)(); // evaluate the math expression // Function() = safer than eval() for simple math
return `${expression} = ${result}`; } catch { return `Could not calculate: ${expression}`; } }, { name: "calculator", description: `Evaluates math expressions. Use for any calculation — addition, subtraction, multiplication, division, percentages. Input should be a valid math expression. Examples: "245 * 18", "1500 / 12", "100 * 0.15"`, schema: z.object({ expression: z.string() .describe("math expression to evaluate, e.g. '245 * 18'"), }), } );
// Tool 2 — Unit converter const unitConverter = tool( async ({ value, fromUnit, toUnit }) => { const conversions = { // length "km_to_miles": (v) => v * 0.621371, "miles_to_km": (v) => v * 1.60934, "m_to_feet": (v) => v * 3.28084, "feet_to_m": (v) => v * 0.3048, // weight "kg_to_lbs": (v) => v * 2.20462, "lbs_to_kg": (v) => v * 0.453592, // temperature "c_to_f": (v) => (v * 9 / 5) + 32, "f_to_c": (v) => (v - 32) * 5 / 9, // data "gb_to_mb": (v) => v * 1024, "mb_to_gb": (v) => v / 1024, };
const key = `${fromUnit.toLowerCase()}_to_${toUnit.toLowerCase()}`; const fn = conversions[key];
if (!fn) return `Conversion from ${fromUnit} to ${toUnit} not supported`;
const result = fn(value).toFixed(4); return `${value} ${fromUnit} = ${result} ${toUnit}`; }, { name: "unit_converter", description: `Converts between units. Supported: km/miles, m/feet, kg/lbs, c/f (celsius/fahrenheit), gb/mb. Use when asked to convert measurements.`, schema: z.object({ value: z.number().describe("the number to convert"), fromUnit: z.string().describe("source unit e.g. 'km', 'kg', 'c'"), toUnit: z.string().describe("target unit e.g. 'miles', 'lbs', 'f'"), }), } );
// Tool 3 — Dictionary / definition const dictionary = tool( async ({ word }) => { // Simulated dictionary — in production call a real dictionary API const definitions = { "RAG": "Retrieval Augmented Generation — a technique that combines LLMs with document retrieval to ground responses in real information", "embedding": "A numerical representation of text as a vector of floating point numbers, capturing semantic meaning", "hallucination": "When an AI model generates confident but factually incorrect information", "transformer": "A neural network architecture using self-attention mechanisms, the foundation of modern LLMs", "vector": "A list of numbers representing a point in multi-dimensional space", "token": "A unit of text used by LLMs — roughly 4 characters or 0.75 words in English", "fine-tuning": "Additional training of a pre-trained model on specific data to adapt its behavior", "inference": "Running a trained model to generate outputs — the process of actually using an LLM", };
const def = definitions[word.toUpperCase()] || definitions[word.toLowerCase()]; if (def) return `${word}: ${def}`; return `Definition not found for "${word}" in this dictionary`; }, { name: "dictionary", description: `Looks up definitions of AI and tech terms. Use when asked to define or explain a technical term. Covers: RAG, embedding, hallucination, transformer, vector, token, fine-tuning, inference`, schema: z.object({ word: z.string().describe("the word or term to define"), }), } );
// Tool 4 — Text analyzer const textAnalyzer = tool( async ({ text, analysisType }) => { switch (analysisType) { case "word_count": const words = text.trim().split(/\s+/).length; const chars = text.length; const sentences = text.split(/[.!?]+/).filter(s => s.trim()).length; return `Word count: ${words} | Characters: ${chars} | Sentences: ${sentences}`;
case "reading_time": const wordCount = text.trim().split(/\s+/).length; const minutes = Math.ceil(wordCount / 200); // average reading speed = 200 words per minute return `Estimated reading time: ${minutes} minute(s) (${wordCount} words at 200 wpm)`;
case "sentiment": const positiveWords = ["good", "great", "excellent", "amazing", "best", "love", "perfect"]; const negativeWords = ["bad", "terrible", "awful", "worst", "hate", "poor", "horrible"]; const lower = text.toLowerCase(); const posCount = positiveWords.filter(w => lower.includes(w)).length; const negCount = negativeWords.filter(w => lower.includes(w)).length;
if (posCount > negCount) return `Sentiment: POSITIVE (${posCount} positive signals)`; if (negCount > posCount) return `Sentiment: NEGATIVE (${negCount} negative signals)`; return "Sentiment: NEUTRAL";
default: return "Unknown analysis type"; } }, { name: "text_analyzer", description: `Analyzes text for various properties. Use for: counting words, estimating reading time, detecting sentiment. Analysis types: 'word_count', 'reading_time', 'sentiment'`, schema: z.object({ text: z.string().describe("the text to analyze"), analysisType: z.enum(["word_count", "reading_time", "sentiment"]) .describe("type of analysis to perform"), }), } );
// ───────────────────────────────────────── // BUILD AGENT // ─────────────────────────────────────────
const tools = [calculator, unitConverter, dictionary, textAnalyzer];
const agentPrompt = ChatPromptTemplate.fromMessages([ ["system", `You are a helpful AI assistant with access to tools.
Guidelines: - Use tools whenever they would improve accuracy - Always use calculator for any math — never calculate in your head - Use unit_converter for any unit conversions - Use dictionary to define technical terms - Use text_analyzer for text statistics - If multiple tools are needed — use them all - After using tools — give a clear, natural response`],
new MessagesPlaceholder("chat_history"), // conversation history for memory
["human", "{input}"], // current user message
new MessagesPlaceholder("agent_scratchpad"), // LangChain uses this internally for tool call tracking ]);
const agent = createToolCallingAgent({ llm, tools, prompt: agentPrompt });
const agentExecutor = new AgentExecutor({ agent, tools, verbose: false, // false = don't print internal thinking steps // set to true during development to see what agent is doing maxIterations: 5, // maximum tool calls per question // prevents infinite loops if agent gets confused });
// ───────────────────────────────────────── // ADD MEMORY TO AGENT // ─────────────────────────────────────────
const messageHistories = {};
const agentWithMemory = new RunnableWithMessageHistory({ runnable: agentExecutor, getMessageHistory: (sessionId) => { if (!messageHistories[sessionId]) { messageHistories[sessionId] = new InMemoryChatMessageHistory(); } return messageHistories[sessionId]; }, inputMessagesKey: "input", historyMessagesKey: "chat_history", outputMessagesKey: "output", });
// ───────────────────────────────────────── // DEMO 1 — Test agent with preset questions // ─────────────────────────────────────────
async function runDemo() { console.log("\n" + "=".repeat(55)); console.log("🤖 LANGCHAIN AGENT DEMO"); console.log("=".repeat(55) + "\n");
const testQuestions = [ "What is 847 multiplied by 63, then divided by 7?", "Convert 5.5 km to miles and also 72 kg to pounds", "Define 'embedding' and 'hallucination'", "How many words are in this sentence: 'The quick brown fox jumps over the lazy dog'? Also what's the reading time?", "What is 15% of 24000? And is this sentence positive or negative: 'This is a terrible and awful experience'", ];
for (const question of testQuestions) { console.log("─".repeat(55)); console.log(`Q: ${question}`); console.log("─".repeat(55));
try { const result = await agentWithMemory.invoke( { input: question }, { configurable: { sessionId: "demo" } } ); console.log(`A: ${result.output}\n`); } catch (error) { console.log(`Error: ${error.message}\n`); } } }
// ───────────────────────────────────────── // DEMO 2 — Interactive agent chat // ─────────────────────────────────────────
async function interactiveDemo() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, });
function prompt(q) { return new Promise(resolve => rl.question(q, resolve)); }
console.log("\n" + "=".repeat(55)); console.log("💬 INTERACTIVE AGENT CHAT"); console.log(' Type "quit" to exit'); console.log("=".repeat(55) + "\n");
const sessionId = `session_${Date.now()}`; // unique session ID for this conversation
while (true) { const input = await prompt("You: "); const trimmed = input.trim();
if (!trimmed) continue;
if (trimmed.toLowerCase() === "quit") { console.log("\n👋 Goodbye!\n"); rl.close(); break; }
try { const result = await agentWithMemory.invoke( { input: trimmed }, { configurable: { sessionId } } ); console.log(`\nAgent: ${result.output}\n`); } catch (error) { console.log(`\nError: ${error.message}\n`); } } }
// ───────────────────────────────────────── // MAIN // ─────────────────────────────────────────
async function main() { const mode = process.argv[2] || "demo"; // node src/06_agents.js demo → run preset questions // node src/06_agents.js interactive → chat with agent
if (mode === "interactive") { await interactiveDemo(); } else { await runDemo(); } }
main().catch(console.error);
Run demo mode:
node src/06_agents.js demo
Run interactive mode:
node src/06_agents.js interactive
Chain vs Agent — When to Use Which
Use a CHAIN when:
→ You know exactly what steps are needed
→ Same steps every time
→ Need predictable, fast execution
→ Building RAG — question → retrieve → answer
→ Building classifiers, extractors, summarizers
→ Production apps where reliability matters
Use an AGENT when:
→ Steps depend on the question
→ Need to use different tools based on input
→ Tasks require multi-step reasoning
→ Building assistants that handle varied requests
→ Combining multiple data sources dynamically
→ You need "I don't know which tool I'll need" flexibility
Real world examples:
Chain → PDF chatbot, support ticket classifier, summarizer
Agent → Personal assistant, research tool, code helper
The Complete LangChain Picture
LangChain Components You Now Know:
LOADING DATA:
PDFLoader, TextLoader, WebLoader → Document objects
SPLITTING DATA:
RecursiveCharacterTextSplitter → smaller Document chunks
EMBEDDING:
OpenAIEmbeddings → vectors for each chunk
STORING:
MemoryVectorStore → searchable vector storage
RETRIEVING:
Basic retriever → simple similarity search
MultiQueryRetriever → multiple question angles
CompressionRetriever → noise removal from chunks
MMR retriever → diverse results
GENERATING:
ChatOpenAI → call GPT-4o
ChatPromptTemplate → structured prompts
StringOutputParser → clean string output
StructuredOutputParser → typed JSON output
CHAINING:
LCEL pipe operator → connect components
RunnableSequence → multi-step pipelines
RunnablePassthrough → pass data through
MEMORY:
ChatMessageHistory → store conversations
RunnableWithMessageHistory → wrap chain with memory
AGENTS:
tool() → define callable functions
createToolCallingAgent → LLM + tools
AgentExecutor → run the agent loop
3-Line Summary
- Advanced retrievers solve specific problems — MultiQueryRetriever generates alternative phrasings of questions to find more relevant content, ContextualCompressionRetriever removes noise from retrieved chunks, and MMR retriever balances relevance with diversity.
- Agents differ from chains in that they decide at runtime which tools to call and in what order — while chains always follow the same fixed steps, agents reason about each question and dynamically choose their approach.
- Use chains when steps are predictable and reliability matters (RAG, classifiers, extractors), use agents when tasks are varied and require dynamic tool selection (personal assistants, research tools, multi-step reasoning).
Module 6.5 — Complete ✅
Phase 6 is done. 🎉
✅ Module 6.1 — What is LangChain & what problem it solves
✅ Module 6.2 — Models, Prompts & Chains
✅ Module 6.3 — Output Parsers, Memory & Tools
✅ Module 6.4 — Document Loaders, Text Splitters & Vector Stores
✅ Module 6.5 — Retrievers & Agents
Course Progress
✅ Phase 1 — AI Foundations
✅ Phase 2 — LLM Internals
✅ Phase 3 — Embeddings
✅ Phase 4 — Vector Databases
✅ Phase 5 — RAG (+ PDF Chatbot project)
✅ Phase 6 — LangChain
Coming up → Phase 7: Advanced LangChain
Coming up → Phase 8: AI Agents
Coming Up — Phase 7: Advanced LangChain
Module 7.1 — Production RAG System
We build a production-grade RAG system with LangChain — proper error handling, streaming responses, source citations, conversation history, and a clean API that you could actually deploy. This is portfolio-level work.
No comments:
Post a Comment