Lesson 18 LangChain & LlamaIndex
Theme:

Lab Guide

Format: Jupyter notebook (lab.ipynb)  ·  Time estimate: 45–60 minutes  ·  Prerequisites: Python 3.10+, langchain, llama-index, anthropic installed

Setup

Before starting, install the required packages:

# Core frameworks
pip install langchain langchain-anthropic langchain-core
pip install llama-index llama-index-llms-anthropic llama-index-embeddings-openai

# Direct API SDK for comparison
pip install anthropic python-dotenv

Create a .env file in your project root with your API keys:

ANTHROPIC_API_KEY=your_anthropic_key_here
OPENAI_API_KEY=your_openai_key_here  # needed for embeddings in Exercise 2

Verify setup by running the first notebook cell — it should import all packages without errors.

API Key Note

Exercise 2 (LlamaIndex RAG) uses OpenAI embeddings by default. If you do not have an OpenAI API key, you can use llama-index-embeddings-huggingface with a local embedding model instead. The lab notebook includes instructions for both options.

Exercise 1: Build a Chain with LangChain

Goal

Build a multi-step chain using LCEL that processes text through two stages: first summarise, then extract key terms from the summary.

Steps

  1. Import the required LangChain components:
    from langchain_anthropic import ChatAnthropic
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_core.runnables import RunnablePassthrough
  2. Create a prompt template for summarisation:
    summarise_prompt = ChatPromptTemplate.from_messages([
        ("system", "You are a concise summariser. Summarise the following text in 2-3 sentences."),
        ("human", "{text}")
    ])
  3. Create a second prompt template for keyword extraction:
    keywords_prompt = ChatPromptTemplate.from_messages([
        ("system", "Extract 5 key technical terms from the following summary. Return them as a comma-separated list."),
        ("human", "{summary}")
    ])
  4. Initialise the model and build both chains:
    model = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0)
    parser = StrOutputParser()
    
    # Chain 1: text -> summary
    summary_chain = summarise_prompt | model | parser
    
    # Chain 2: summary -> keywords
    keyword_chain = keywords_prompt | model | parser
  5. Compose them into a single pipeline:
    # Pipe the output of chain 1 into chain 2
    full_chain = summary_chain | (lambda summary: {"summary": summary}) | keyword_chain
  6. Test with a sample text (use a paragraph about a technical topic, ~200 words).

Expected Output

=== Summary ===
LangChain is an open-source framework that simplifies building applications
with large language models by providing composable abstractions for chains,
agents, and retrieval. It supports multiple LLM providers through a unified
interface and uses LCEL for declarative pipeline composition.

=== Keywords ===
LangChain, LCEL, orchestration framework, composable abstractions, retrieval

Extension: Parallel Chains

Try using RunnableParallel to run both the summary and keyword chains simultaneously on the same input, instead of sequentially:

from langchain_core.runnables import RunnableParallel

parallel_chain = RunnableParallel(
    summary=summary_chain,
    keywords=summarise_prompt | model | parser | (lambda s: {"summary": s}) | keyword_chain
)

Exercise 2: Build a RAG Pipeline with LlamaIndex

Goal

Load a set of text documents, build a vector index, and query it with natural language questions.

Steps

  1. Create a data/ directory with 3–5 short text files (.txt) on a topic of your choice. Each file should be 100–300 words. For example, create files about different programming languages, or different sections of a product FAQ.
  2. Load the documents:
    from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
    from llama_index.llms.anthropic import Anthropic
    from llama_index.embeddings.openai import OpenAIEmbedding
    
    # Configure global settings
    Settings.llm = Anthropic(model="claude-sonnet-4-20250514")
    Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
    
    # Load documents
    documents = SimpleDirectoryReader("./data").load_data()
    print(f"Loaded {len(documents)} documents")
  3. Build the vector index:
    index = VectorStoreIndex.from_documents(documents)
    print("Index built successfully")
  4. Create a query engine and ask questions:
    query_engine = index.as_query_engine(similarity_top_k=2)
    
    response = query_engine.query("What are the main advantages of Python?")
    print(f"Answer: {response}")
    print(f"\nSources used: {len(response.source_nodes)}")
    for node in response.source_nodes:
        print(f"  - {node.node.metadata.get('file_name', 'unknown')} (score: {node.score:.3f})")
  5. Experiment with different similarity_top_k values (1, 2, 5) and observe how the answers change.

Expected Output

Loaded 4 documents
Index built successfully

Answer: Python's main advantages include its readable syntax, extensive
standard library, and large ecosystem of third-party packages. It is widely
used in data science, web development, and automation.

Sources used: 2
  - python.txt (score: 0.847)
  - languages_overview.txt (score: 0.721)

Without OpenAI API Key

If you do not have an OpenAI key for embeddings, use a local model:

pip install llama-index-embeddings-huggingface

from llama_index.embeddings.huggingface import HuggingFaceEmbedding
Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")

Exercise 3: Direct API Comparison

Goal

Implement the same two-step chain from Exercise 1 using only the anthropic SDK. Compare the code complexity, readability, and flexibility of both approaches.

Steps

  1. Implement the summarise-then-extract-keywords pipeline with the Anthropic SDK:
    import anthropic
    from dotenv import load_dotenv
    load_dotenv()
    
    client = anthropic.Anthropic()
    
    def summarise_and_extract(text: str) -> dict:
        # Step 1: Summarise
        summary_response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=300,
            temperature=0,
            system="You are a concise summariser. Summarise the following text in 2-3 sentences.",
            messages=[{"role": "user", "content": text}]
        )
        summary = summary_response.content[0].text
    
        # Step 2: Extract keywords
        keywords_response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=100,
            temperature=0,
            system="Extract 5 key technical terms from the following summary. Return them as a comma-separated list.",
            messages=[{"role": "user", "content": summary}]
        )
        keywords = keywords_response.content[0].text
    
        return {"summary": summary, "keywords": keywords}
  2. Run both implementations on the same input text.
  3. Compare the results side by side.
  4. Fill out the comparison table in the notebook.

Comparison Table

Complete this in your notebook:

CriteriaLangChain (Exercise 1)Direct API (Exercise 3)
Lines of code??
Dependencies??
Readability??
Ease of adding a third step??
Ease of switching to OpenAI??
Debugging (seeing exact prompt)??

Discussion Questions

Finishing Up

Once you have completed all three exercises:

  1. Run all cells from top to bottom to verify everything executes without errors (Kernel → Restart & Run All).
  2. Ensure your comparison table in Exercise 3 is filled out with your observations.
  3. Save the notebook.
  4. Compare your implementation with lab_answer.ipynb.

Self-Check