Task 1: Create a Basic Interview Chat
In this lesson, we'll create our first LangChain chain - an interviewer that asks technical questions and responds to answers.
What We're Building
┌─────────────────────────────────────────────────────────┐
│ Interview Chain │
│ │
│ ┌──────────┐ ┌─────────┐ ┌────────────────┐ │
│ │ Prompt │ -> │ LLM │ -> │ String Output │ │
│ │ Template │ │ (GPT-4) │ │ │ │
│ └──────────┘ └─────────┘ └────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Step 1: Create the Interviewer Prompt
The prompt defines the interviewer's behavior and personality.
# chains/interviewer.py
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
INTERVIEWER_SYSTEM_PROMPT = """You are an expert technical interviewer conducting a {interview_type} interview.
Your role:
- Ask one clear, focused question at a time
- Questions should be appropriate for a {level} position
- Be professional but encouraging
- After the candidate answers, provide brief acknowledgment before the next question
Interview focus: {focus_area}
Current question number: {question_number} of {total_questions}
"""
interviewer_prompt = ChatPromptTemplate.from_messages([
("system", INTERVIEWER_SYSTEM_PROMPT),
MessagesPlaceholder(variable_name="history", optional=True),
("human", "{input}")
])
Step 2: Build the Basic Chain
# chains/interviewer.py (continued)
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
def create_interviewer_chain(
model: str = "gpt-4o-mini",
temperature: float = 0.7
):
"""Create a basic interviewer chain."""
llm = ChatOpenAI(model=model, temperature=temperature)
chain = interviewer_prompt | llm | StrOutputParser()
return chain
Step 3: Using the Chain
# main.py
from dotenv import load_dotenv
from chains.interviewer import create_interviewer_chain
load_dotenv()
# Create the chain
interviewer = create_interviewer_chain()
# Start the interview
response = interviewer.invoke({
"interview_type": "technical Python",
"level": "senior",
"focus_area": "Python fundamentals and best practices",
"question_number": 1,
"total_questions": 5,
"input": "Please start the interview with your first question."
})
print(f"Interviewer: {response}")
Output:
Interviewer: Welcome! Let's begin with a fundamental Python concept.
Can you explain the difference between a list and a tuple in Python,
and describe a scenario where you would prefer one over the other?
Step 4: Interactive Interview Loop
# main.py (extended)
def run_basic_interview():
interviewer = create_interviewer_chain()
config = {
"interview_type": "technical Python",
"level": "senior",
"focus_area": "Python fundamentals, OOP, and best practices",
"total_questions": 5,
}
print("=" * 50)
print("AI Interview Coach - Basic Mode")
print("=" * 50)
print("Type 'quit' to exit\n")
# Get first question
response = interviewer.invoke({
**config,
"question_number": 1,
"input": "Start the interview with your first question."
})
print(f"\nInterviewer: {response}\n")
question_num = 1
while question_num < config["total_questions"]:
# Get candidate's answer
answer = input("You: ")
if answer.lower() == 'quit':
break
question_num += 1
# Get next question (acknowledging previous answer)
response = interviewer.invoke({
**config,
"question_number": question_num,
"input": f"The candidate answered: {answer}\n\nAcknowledge briefly and ask question {question_num}."
})
print(f"\nInterviewer: {response}\n")
print("\nInterview complete! Thank you for participating.")
if __name__ == "__main__":
run_basic_interview()
Understanding LCEL (LangChain Expression Language)
The pipe operator | chains components together:
# This:
chain = prompt | llm | output_parser
# Is equivalent to:
def chain(input):
prompt_result = prompt.invoke(input)
llm_result = llm.invoke(prompt_result)
return output_parser.invoke(llm_result)
Chain Components
| Component | Purpose | Example |
|---|---|---|
ChatPromptTemplate | Format input into messages | System + Human messages |
ChatOpenAI | Call the LLM | GPT-4, GPT-3.5 |
StrOutputParser | Extract string from response | Gets .content |
Different Interview Types
Modify the prompt for different interview styles:
INTERVIEW_TYPES = {
"behavioral": """Focus on STAR method questions.
Ask about past experiences, challenges, teamwork.""",
"system_design": """Ask about architecture, scalability,
trade-offs. Start high-level, then dive deep.""",
"coding": """Present coding problems. Ask for approach first,
then implementation details. Probe for edge cases.""",
"technical": """Test domain knowledge. Mix conceptual
questions with practical scenarios."""
}
# Usage
config["interview_type"] = "system_design"
config["focus_area"] = INTERVIEW_TYPES["system_design"]
Exercise: Add Interview Styles
Extend the interviewer to support different personalities:
INTERVIEWER_STYLES = {
"friendly": "Be warm, encouraging, help candidates feel comfortable.",
"challenging": "Push back on answers, ask follow-ups, test depth.",
"neutral": "Professional and straightforward, minimal feedback."
}
# Modify the prompt to include style
What's Missing?
Our current implementation has a problem - it doesn't remember previous exchanges!
Each invocation is independent. Try this:
# First question
response1 = interviewer.invoke({...})
# Second question - interviewer doesn't remember first!
response2 = interviewer.invoke({...})
In the next lesson, we'll add conversation memory to fix this.
Key Takeaways
- Chains combine prompts, LLMs, and output parsers
- LCEL (
|) creates readable, composable pipelines - Prompt templates make chains reusable with different inputs
- Basic chains are stateless - no memory between calls