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
- 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 - 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}") ]) - 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}") ]) - 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 - 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 - 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
- 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. - 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") - Build the vector index:
index = VectorStoreIndex.from_documents(documents) print("Index built successfully") - 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})") - Experiment with different
similarity_top_kvalues (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
- 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} - Run both implementations on the same input text.
- Compare the results side by side.
- Fill out the comparison table in the notebook.
Comparison Table
Complete this in your notebook:
| Criteria | LangChain (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
- For this specific two-step chain, which approach do you prefer? Why?
- At what level of complexity would you switch from direct API calls to a framework?
- If you needed to add streaming, which implementation would be easier to modify?
- If a junior developer joined your team, which code would be easier for them to understand?
Finishing Up
Once you have completed all three exercises:
- Run all cells from top to bottom to verify everything executes without errors (Kernel → Restart & Run All).
- Ensure your comparison table in Exercise 3 is filled out with your observations.
- Save the notebook.
- Compare your implementation with
lab_answer.ipynb.
Self-Check
- ✓ Exercise 1: Your LCEL chain produces a summary and keywords from a single
invoke()call. - ✓ Exercise 1: You can explain what the pipe operator does at each step.
- ✓ Exercise 2: Your LlamaIndex query engine returns answers with source attribution.
- ✓ Exercise 2: You tested different
similarity_top_kvalues and noted the differences. - ✓ Exercise 3: Both implementations produce equivalent results on the same input.
- ✓ Exercise 3: Your comparison table is complete and reflects your genuine assessment.