🔥 0
0
Lesson 4 of 8 25 min +150 XP

Task 3: Generate Structured Feedback

Real interview feedback needs structure:

  • Numerical scores
  • Specific strengths
  • Areas for improvement
  • Actionable suggestions

Let's use LangChain's Output Parsers to get structured JSON instead of free text.

The Problem with Unstructured Output

# Free-form feedback is hard to use programmatically
feedback = "The candidate showed good understanding of Python basics,
            particularly with list comprehensions. However, they struggled
            with explaining decorators. I'd rate them 7/10..."

# How do you extract the score? The strengths? 🤷

Solution: Pydantic Output Parser

Define your output structure with Pydantic, and LangChain ensures the LLM returns valid JSON.

Step 1: Define the Feedback Schema

# chains/evaluator.py
from pydantic import BaseModel, Field
from typing import List, Optional

class AnswerFeedback(BaseModel):
    """Structured feedback for a single answer."""

    score: int = Field(
        description="Score from 1-10 (1=poor, 10=excellent)",
        ge=1,
        le=10
    )
    understanding: str = Field(
        description="Assessment of conceptual understanding"
    )
    communication: str = Field(
        description="How well they explained their answer"
    )
    strengths: List[str] = Field(
        description="Specific things the candidate did well"
    )
    improvements: List[str] = Field(
        description="Specific areas to improve"
    )
    follow_up_question: Optional[str] = Field(
        description="A follow-up question to probe deeper",
        default=None
    )

class InterviewReport(BaseModel):
    """Final interview evaluation report."""

    overall_score: int = Field(ge=1, le=10)
    recommendation: str = Field(
        description="hire / maybe / no_hire"
    )
    summary: str = Field(
        description="2-3 sentence overall assessment"
    )
    technical_skills: int = Field(ge=1, le=10)
    communication_skills: int = Field(ge=1, le=10)
    problem_solving: int = Field(ge=1, le=10)
    strengths: List[str]
    areas_to_improve: List[str]
    suggested_topics_to_study: List[str]

Step 2: Create the Evaluation Chain

# chains/evaluator.py (continued)
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser

def create_evaluator_chain():
    """Create a chain that returns structured feedback."""

    # Create parser from our Pydantic model
    parser = PydanticOutputParser(pydantic_object=AnswerFeedback)

    prompt = ChatPromptTemplate.from_messages([
        ("system", """You are an expert interview evaluator.

Evaluate the candidate's answer to the given question.
Be specific and constructive in your feedback.

Question context: {question}
Position level: {level}

{format_instructions}
"""),
        ("human", "Candidate's answer: {answer}")
    ])

    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

    chain = prompt | llm | parser

    return chain, parser

# Usage
evaluator, parser = create_evaluator_chain()

feedback = evaluator.invoke({
    "question": "Explain the difference between lists and tuples in Python",
    "level": "senior",
    "answer": "Lists are mutable and tuples are immutable. You use lists when you need to modify the collection.",
    "format_instructions": parser.get_format_instructions()
})

print(f"Score: {feedback.score}/10")
print(f"Strengths: {feedback.strengths}")
print(f"Improvements: {feedback.improvements}")
Output:
AnswerFeedback(
    score=6,
    understanding="Shows basic understanding but lacks depth",
    communication="Clear but too brief for a senior position",
    strengths=["Correctly identified mutability difference"],
    improvements=[
        "Explain when immutability matters (hashability, dict keys)",
        "Discuss performance implications",
        "Give concrete use case examples"
    ],
    follow_up_question="Can you explain why tuples can be used as dictionary keys but lists cannot?"
)

Step 3: Using with_structured_output (Simpler!)

LangChain now has a cleaner approach:

# chains/evaluator.py (modern approach)
from langchain_openai import ChatOpenAI

def create_evaluator_simple():
    """Use with_structured_output for cleaner code."""

    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

    # Automatically handles the output parsing
    structured_llm = llm.with_structured_output(AnswerFeedback)

    prompt = ChatPromptTemplate.from_messages([
        ("system", """You are an expert interview evaluator.
Evaluate the candidate's answer to the given question.
Be specific and constructive in your feedback.

Question: {question}
Position level: {level}"""),
        ("human", "{answer}")
    ])

    chain = prompt | structured_llm

    return chain

Step 4: Generate Final Report

# chains/evaluator.py (continued)
def create_report_generator():
    """Generate final interview report."""

    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
    structured_llm = llm.with_structured_output(InterviewReport)

    prompt = ChatPromptTemplate.from_messages([
        ("system", """You are generating a final interview evaluation report.

Based on the interview transcript below, provide a comprehensive assessment.
Be fair, specific, and constructive.

Position: {position}
Level: {level}
Interview type: {interview_type}
"""),
        ("human", """Interview transcript:
{transcript}

Individual question scores: {scores}

Generate the final report.""")
    ])

    return prompt | structured_llm

# Usage
report_generator = create_report_generator()

report = report_generator.invoke({
    "position": "Senior Python Developer",
    "level": "senior",
    "interview_type": "technical",
    "transcript": """
Q1: Explain Python decorators.
A1: Decorators are functions that wrap other functions...

Q2: What are generators?
A2: Generators use yield to produce values lazily...
""",
    "scores": [7, 8, 6, 7]
})

print(f"Overall: {report.overall_score}/10")
print(f"Recommendation: {report.recommendation}")
print(f"Summary: {report.summary}")

Step 5: Real-time Feedback Integration

Combine with our interviewer:

# main.py
from chains.interviewer import create_interviewer_with_history
from chains.evaluator import create_evaluator_simple, create_report_generator

def run_interview_with_feedback():
    interviewer = create_interviewer_with_history()
    evaluator = create_evaluator_simple()

    session_id = "interview_001"
    config = {"configurable": {"session_id": session_id}}

    interview_config = {
        "interview_type": "technical Python",
        "level": "senior",
        "focus_area": "Python internals"
    }

    scores = []
    transcript = []

    # Start interview
    question = interviewer.invoke(
        {**interview_config, "input": "Start the interview"},
        config=config
    )
    print(f"\nInterviewer: {question}\n")
    current_question = question

    for i in range(5):  # 5 questions
        answer = input("You: ")
        if answer.lower() == 'quit':
            break

        # Evaluate the answer
        feedback = evaluator.invoke({
            "question": current_question,
            "level": "senior",
            "answer": answer
        })

        scores.append(feedback.score)
        transcript.append(f"Q: {current_question}\nA: {answer}")

        # Show score
        print(f"\n[Score: {feedback.score}/10 - {feedback.understanding}]")
        if feedback.improvements:
            print(f"[Tip: {feedback.improvements[0]}]")

        # Get next question
        next_input = f"The candidate answered: {answer}"
        if feedback.follow_up_question:
            next_input += f"\n\nConsider asking this follow-up: {feedback.follow_up_question}"

        question = interviewer.invoke(
            {**interview_config, "input": next_input},
            config=config
        )
        print(f"\nInterviewer: {question}\n")
        current_question = question

    # Generate final report
    print("\n" + "=" * 50)
    print("INTERVIEW REPORT")
    print("=" * 50)

    report_gen = create_report_generator()
    report = report_gen.invoke({
        "position": "Senior Python Developer",
        "level": "senior",
        "interview_type": "technical",
        "transcript": "\n\n".join(transcript),
        "scores": scores
    })

    print(f"\nOverall Score: {report.overall_score}/10")
    print(f"Recommendation: {report.recommendation.upper()}")
    print(f"\nSummary: {report.summary}")
    print(f"\nStrengths:")
    for s in report.strengths:
        print(f"  ✓ {s}")
    print(f"\nAreas to Improve:")
    for a in report.areas_to_improve:
        print(f"  • {a}")

Handling Parsing Errors

Sometimes the LLM returns malformed output:

from langchain_core.exceptions import OutputParserException

def safe_evaluate(evaluator, question, answer, level):
    """Evaluate with error handling."""
    try:
        return evaluator.invoke({
            "question": question,
            "level": level,
            "answer": answer
        })
    except OutputParserException as e:
        # Return a default feedback
        return AnswerFeedback(
            score=5,
            understanding="Could not evaluate - please try again",
            communication="N/A",
            strengths=["Answer was received"],
            improvements=["Please provide more detail"]
        )

Key Takeaways

  • Pydantic models define the structure you want
  • PydanticOutputParser instructs the LLM and parses output
  • with_structured_output() is the modern, cleaner approach
  • Structured output enables programmatic use of LLM responses
  • Always handle parsing errors gracefully

Next Up

Task 4: RAG for Job-Specific Questions - Load job descriptions and generate relevant interview questions.
Task 2: Add Conversation Memory