11.03 — Creating the Reflector Chain and Tweet Revisor¶
Overview¶
Before building the graph, we need to build the components that run inside it — the chains. This lesson implements two LCEL (LangChain Expression Language) chains:
- Generation Chain — the "writer" that creates and revises tweets
- Reflection Chain — the "critic" that evaluates tweets and provides feedback
These chains are the core intelligence of the reflection agent. The graph (lesson 04) merely orchestrates when they run; the chains define what happens.
The Two-Chain Architecture¶
flowchart LR
subgraph Generation["✍️ Generation Chain"]
G1["System: 'You are a Twitter<br/>techie influencer...'"]
G2["Messages Placeholder"]
G3["→ LLM (GPT-3.5)"]
G1 --> G2 --> G3
end
subgraph Reflection["🔍 Reflection Chain"]
R1["System: 'You are a viral<br/>Twitter influencer...'"]
R2["Messages Placeholder"]
R3["→ LLM (GPT-3.5)"]
R1 --> R2 --> R3
end
Generation -->|"Tweet"| Reflection
Reflection -->|"Critique"| Generation
style Generation fill:#4a9eff,color:#fff
style Reflection fill:#f59e0b,color:#fff
The key insight is that both chains use the same LLM (GPT-3.5 Turbo), but with different system prompts. The system prompt is what gives each chain its specialized behavior — one acts as a writer, the other as a critic.
Implementation¶
Step 1: Imports¶
# chains.py
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
Let's understand each import:
| Import | What It Does |
|---|---|
ChatPromptTemplate |
Creates structured prompts with system messages, human messages, and placeholders. It's LangChain's way of building reusable prompt templates. |
MessagesPlaceholder |
A special placeholder that accepts a list of messages — this is how we inject the conversation history into the prompt at runtime. |
ChatOpenAI |
LangChain's wrapper around OpenAI's chat models. Handles API calls, retries, and response parsing. |
Step 2: The Reflection Prompt¶
reflection_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a viral Twitter influencer grading a tweet. "
"Generate critique and recommendations for the user's tweet. "
"Always provide detailed recommendations, including requests "
"for length, virality, style, and hashtags.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
Anatomy of this prompt:
flowchart TD
P["ChatPromptTemplate"]
P --> S["System Message\n(Fixed instructions)"]
P --> M["MessagesPlaceholder\n(Dynamic conversation history)"]
S --> S1["'You are a viral Twitter<br/>influencer grading a tweet...'"]
M --> M1["Injected at runtime:\n- Original tweet\n- Previous revisions\n- Previous critiques"]
style S fill:#8b5cf6,color:#fff
style M fill:#10b981,color:#fff
The prompt has two parts:
-
System message (fixed): Sets the persona and instructions — "you are a critic, give detailed feedback on length, virality, style, and hashtags." This never changes between iterations.
-
MessagesPlaceholder (dynamic): This is where the conversation history gets injected at runtime. On the first iteration, it might contain just the user's original tweet request and the first generated tweet. On later iterations, it contains the entire history — all previous tweets, all previous critiques — giving the LLM full context of how the content has evolved.
Why use MessagesPlaceholder instead of a simple string variable?
Because we need to inject a list of messages (human messages, AI messages) rather than a single string. The MessagesPlaceholder preserves the message types and ordering, which is important for the LLM to understand the conversation flow — who said what, and in what order.
Step 3: The Generation Prompt¶
generation_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a Twitter techie influencer assistant tasked with "
"writing excellent Twitter posts. Generate the best Twitter "
"post possible for the user's request. If the user provides "
"critique, respond with a revised version of your previous "
"attempts.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
Notice the crucial difference in the instructions:
| Aspect | Reflection Prompt | Generation Prompt |
|---|---|---|
| Persona | "viral Twitter influencer grading a tweet" | "Twitter techie influencer assistant" |
| Task | "Generate critique and recommendations" | "Generate the best Twitter post possible" |
| Key instruction | "provide detailed recommendations" | "If critique is provided, respond with a revised version" |
The generation prompt includes the instruction "If the user provides critique, respond with a revised version of your previous attempts." This is critical — it tells the LLM that when it sees critique in the message history, it should produce a revision, not a completely new tweet. This ensures continuity across iterations.
Step 4: Create the Chains¶
# Initialize the LLM (defaults to GPT-3.5 Turbo)
llm = ChatOpenAI()
# Create the two chains using LCEL (pipe operator)
generate_chain = generation_prompt | llm
reflect_chain = reflection_prompt | llm
What does the pipe (|) operator do?
The pipe operator is LCEL (LangChain Expression Language) syntax for composing components into a chain. When you write prompt | llm, you're saying:
- First, format the prompt with the input variables
- Then, send the formatted prompt to the LLM
- Return the LLM's response
It's equivalent to:
# This is what `generate_chain.invoke({"messages": msgs})` does internally:
formatted_prompt = generation_prompt.format_messages(messages=msgs)
response = llm.invoke(formatted_prompt)
But the LCEL syntax is more concise and allows chaining many steps together.
How the Chains Work Together¶
Let's trace through what happens in a complete reflection cycle:
Iteration 1 — First Draft¶
Input to Generation Chain:
messages = [
HumanMessage("Make this tweet better: [original tweet]")
]
LLM receives:
System: "You are a Twitter techie influencer assistant..."
Human: "Make this tweet better: [original tweet]"
Output: AIMessage("🚀 Exciting news! LangChain's new tool calling feature...")
Reflection on Iteration 1¶
Input to Reflection Chain:
messages = [
HumanMessage("Make this tweet better: [original tweet]"),
AIMessage("🚀 Exciting news! LangChain's new tool calling feature...")
]
LLM receives:
System: "You are a viral Twitter influencer grading a tweet..."
Human: "Make this tweet better: [original tweet]"
AI: "🚀 Exciting news! LangChain's new tool calling feature..."
Output: AIMessage("The tweet is a good start but could be improved:
1. Too long — tweets should be punchy and under 280 characters
2. Missing hashtags — add #AI #LangChain #DevTools
3. The hook isn't strong enough — start with a question or bold statement
4. Add a call to action at the end")
Iteration 2 — Revised Draft¶
Input to Generation Chain:
messages = [
HumanMessage("Make this tweet better: [original tweet]"),
AIMessage("🚀 Exciting news! LangChain's new tool calling feature..."),
HumanMessage("The tweet is a good start but could be improved: 1. Too long...")
]
LLM receives all the history, including the critique, and generates an improved version.
[!IMPORTANT] Notice that the reflection output (originally an AI message) is cast to a HumanMessage before being added back to the history. This is a deliberate prompt engineering technique that we'll explore in detail in the next lesson.
The Complete chains.py File¶
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
# --- Prompts ---
reflection_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a viral Twitter influencer grading a tweet. "
"Generate critique and recommendations for the user's tweet. "
"Always provide detailed recommendations, including requests "
"for length, virality, style, and hashtags.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
generation_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a Twitter techie influencer assistant tasked with "
"writing excellent Twitter posts. Generate the best Twitter "
"post possible for the user's request. If the user provides "
"critique, respond with a revised version of your previous "
"attempts.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
# --- Chains ---
llm = ChatOpenAI()
generate_chain = generation_prompt | llm
reflect_chain = reflection_prompt | llm
[!TIP] The file is intentionally short (~30 lines). Chains should be focused and testable — they contain only prompts and LLM configuration. All the orchestration logic (loops, conditions, state management) goes in the graph, not in the chains.
Summary¶
| Component | Purpose | Key Design Decision |
|---|---|---|
| Reflection Prompt | Critique the tweet with specific recommendations | Persona is a "grading" influencer — focused on evaluation |
| Generation Prompt | Write and revise tweets | Includes the "if critique is provided, revise" instruction |
| MessagesPlaceholder | Inject dynamic conversation history | Preserves message types and full context across iterations |
LCEL pipe (\|) |
Compose prompt → LLM into a chain | Concise, composable, consistent with LangChain patterns |
| Same LLM, different prompts | Specialized behavior from a single model | The system prompt is what gives each chain its expertise |