Writing real LangChain code for the first time
Project Setup
First — new project for all LangChain work:
mkdir langchain-explorer cd langchain-explorer npm init -y
Update package.json:
{ "name": "langchain-explorer", "version": "1.0.0", "type": "module", "scripts": { "start": "node index.js" } }
Install packages:
Create .env:
OPENAI_API_KEY=sk-proj-your-key-here
Part 1 — Models (ChatOpenAI)
What it is
ChatOpenAI is LangChain's wrapper around OpenAI's chat API. Instead of calling openai.chat.completions.create() directly — you use this.
Create src/01_models.js:
import { ChatOpenAI } from "@langchain/openai"; // ChatOpenAI = LangChain's wrapper for OpenAI chat models // Handles API calls, retries, streaming, token counting
import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages"; // Message classes — represent different roles in a conversation // HumanMessage = user's message (role: "user") // SystemMessage = system prompt (role: "system") // AIMessage = model response (role: "assistant")
import * as dotenv from "dotenv"; dotenv.config();
// ───────────────────────────────────────── // SETUP — Create the model // ─────────────────────────────────────────
const llm = new ChatOpenAI({ model: "gpt-4o", // which OpenAI model to use // same as "model" in openai.chat.completions.create()
temperature: 0.7, // randomness — 0 = deterministic, 1 = creative // same concept you learned in Module 1.3
maxTokens: 500, // maximum tokens in response // prevents runaway long responses
// Optional settings you can add: // timeout: 30000, ← 30 second timeout // maxRetries: 3, ← retry failed calls 3 times });
// ───────────────────────────────────────── // EXAMPLE 1 — Simple string invoke // Simplest possible LangChain model call // ─────────────────────────────────────────
async function example1() { console.log("─".repeat(50)); console.log("EXAMPLE 1: Simple string invoke"); console.log("─".repeat(50));
const response = await llm.invoke("What is RAG in AI?"); // invoke() = call the model with input // string input = automatically becomes a HumanMessage // returns an AIMessage object
console.log("Response type:", response.constructor.name); // prints: "AIMessage"
console.log("Content:", response.content); // response.content = the actual text response // example: "RAG stands for Retrieval Augmented Generation..."
console.log("Token usage:", response.usage_metadata); // usage_metadata = how many tokens were used // example: { input_tokens: 10, output_tokens: 87, total_tokens: 97 }
console.log(); }
// ───────────────────────────────────────── // EXAMPLE 2 — Message array invoke // More control — pass specific message roles // ─────────────────────────────────────────
async function example2() { console.log("─".repeat(50)); console.log("EXAMPLE 2: Message array invoke"); console.log("─".repeat(50));
const messages = [ new SystemMessage( "You are a concise assistant. Answer in maximum 2 sentences." ), // SystemMessage = system prompt // sets behavior for the whole conversation
new HumanMessage("What is a vector database?"), // HumanMessage = user's question ];
const response = await llm.invoke(messages); // invoke with array of messages // gives you full control over the conversation structure
console.log("Answer:", response.content); // short 2-sentence answer because of system prompt console.log(); }
// ───────────────────────────────────────── // EXAMPLE 3 — Multi-turn conversation // Simulating a back-and-forth conversation // ─────────────────────────────────────────
async function example3() { console.log("─".repeat(50)); console.log("EXAMPLE 3: Multi-turn conversation"); console.log("─".repeat(50));
// Simulate a conversation history const conversation = [ new SystemMessage("You are a helpful AI tutor teaching about vectors."),
new HumanMessage("What is a vector?"), // first user message
new AIMessage( "A vector is a list of numbers that represents something in space. " + "For example, [3, 4] is a 2D vector." ), // AIMessage = previous model response // we include previous responses to give model conversation context
new HumanMessage("How are vectors used in AI?"), // follow-up question — model has context from above ];
const response = await llm.invoke(conversation); // model sees entire conversation history // can reference "the vector I mentioned earlier"
console.log("Response:", response.content); console.log(); }
// ───────────────────────────────────────── // EXAMPLE 4 — Different model settings // Showing how temperature affects output // ─────────────────────────────────────────
async function example4() { console.log("─".repeat(50)); console.log("EXAMPLE 4: Temperature comparison"); console.log("─".repeat(50));
const question = "Give me a one-word synonym for 'happy'";
// Temperature 0 — always same answer const deterministicModel = new ChatOpenAI({ model: "gpt-4o", temperature: 0, // always picks highest probability token });
// Temperature 1 — varied answers const creativeModel = new ChatOpenAI({ model: "gpt-4o", temperature: 1, // more random token selection });
const r1 = await deterministicModel.invoke(question); const r2 = await deterministicModel.invoke(question); const r3 = await creativeModel.invoke(question); const r4 = await creativeModel.invoke(question);
console.log("Temperature 0 — Run 1:", r1.content); console.log("Temperature 0 — Run 2:", r2.content); // Both should be identical — "Joyful" or "Cheerful"
console.log("Temperature 1 — Run 1:", r3.content); console.log("Temperature 1 — Run 2:", r4.content); // Likely different — "Elated", "Gleeful", "Content", etc. console.log(); }
// ───────────────────────────────────────── // RUN ALL EXAMPLES // ─────────────────────────────────────────
async function main() { console.log("\n🤖 LANGCHAIN MODELS DEMO\n");
await example1(); await example2(); await example3(); await example4();
console.log("✅ All model examples complete!"); }
main().catch(console.error);
Run it:
node src/01_models.js
Part 2 — Prompt Templates
What they are
Prompt templates are reusable prompt structures with placeholders. Instead of building strings manually with template literals — you define a template once and fill it with different values.
Create src/02_prompts.js:
import { ChatPromptTemplate, PromptTemplate } from "@langchain/core/prompts"; // ChatPromptTemplate = for chat models (system + human messages) // PromptTemplate = for simple single string prompts
import { ChatOpenAI } from "@langchain/openai"; import * as dotenv from "dotenv"; dotenv.config();
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0.1 });
// ───────────────────────────────────────── // EXAMPLE 1 — Basic PromptTemplate // Simple string template with one variable // ─────────────────────────────────────────
async function example1() { console.log("─".repeat(50)); console.log("EXAMPLE 1: Basic PromptTemplate"); console.log("─".repeat(50));
const template = PromptTemplate.fromTemplate( "Explain {concept} in simple terms that a 10-year-old can understand." // {concept} = placeholder // gets replaced with actual value when called );
// Format the template with actual values const filledPrompt = await template.format({ concept: "machine learning", // {concept} → "machine learning" });
console.log("Filled prompt:", filledPrompt); // "Explain machine learning in simple terms that a 10-year-old can understand."
const response = await llm.invoke(filledPrompt); console.log("Response:", response.content); console.log(); }
// ───────────────────────────────────────── // EXAMPLE 2 — ChatPromptTemplate // Template with system + human messages // This is what you'll use in real RAG apps // ─────────────────────────────────────────
async function example2() { console.log("─".repeat(50)); console.log("EXAMPLE 2: ChatPromptTemplate"); console.log("─".repeat(50));
const chatPrompt = ChatPromptTemplate.fromMessages([ ["system", `You are an expert in {domain}. Answer questions clearly and concisely. Always give a practical example.`], // {domain} = placeholder filled at runtime // example: "machine learning", "web development", "cooking"
["human", "Explain {topic} in 3 sentences."], // {topic} = another placeholder ]);
// Format with actual values const messages = await chatPrompt.formatMessages({ domain: "artificial intelligence", // fills {domain} in system message
topic: "neural networks", // fills {topic} in human message });
console.log("Formatted messages:"); messages.forEach(msg => { console.log(` ${msg.constructor.name}: ${msg.content.substring(0, 80)}...`); });
const response = await llm.invoke(messages); console.log("\nResponse:", response.content); console.log(); }
// ───────────────────────────────────────── // EXAMPLE 3 — RAG-style prompt template // This is exactly what you'd use in a chatbot // ─────────────────────────────────────────
async function example3() { console.log("─".repeat(50)); console.log("EXAMPLE 3: RAG-style prompt template"); console.log("─".repeat(50));
const ragPrompt = ChatPromptTemplate.fromMessages([ ["system", `You are a helpful assistant that answers questions based ONLY on the provided context.
Rules: 1. Only use information from the context below 2. If not in context say "I don't have that information" 3. Always cite which part of context you used
Context: {context}`], // {context} = the retrieved document chunks // filled with actual document content at runtime
["human", "{question}"], // {question} = user's actual question ]);
// Simulate what RAG does — fill with retrieved context const fakeContext = ` Source 1: Aspirin reduces fever, pain and inflammation. Common side effects include stomach irritation and nausea.
Source 2: The recommended adult dose is 325-650mg every 4-6 hours. Do not exceed 4000mg in 24 hours. `;
const messages = await ragPrompt.formatMessages({ context: fakeContext, question: "What is the maximum daily dose of aspirin?", });
const response = await llm.invoke(messages); console.log("RAG Answer:", response.content); console.log(); }
// ───────────────────────────────────────── // EXAMPLE 4 — Reusing templates // Same template — different inputs // Shows the power of templates // ─────────────────────────────────────────
async function example4() { console.log("─".repeat(50)); console.log("EXAMPLE 4: Reusing templates with different inputs"); console.log("─".repeat(50));
const reviewTemplate = ChatPromptTemplate.fromMessages([ ["system", "You are a code reviewer. Review the code and give feedback."], ["human", `Review this {language} code:
\`\`\`{language} {code} \`\`\`
Focus on: {focus}`], ]);
// Use same template for different code snippets const inputs = [ { language: "JavaScript", code: "for(let i=0; i<=arr.length; i++) { console.log(arr[i]) }", focus: "bugs and errors", }, { language: "JavaScript", code: "const result = arr.filter(x => x > 0).map(x => x * 2)", focus: "readability and best practices", }, ];
for (const input of inputs) { const messages = await reviewTemplate.formatMessages(input); const response = await llm.invoke(messages); console.log(`Review for: ${input.code.substring(0, 40)}...`); console.log(`Feedback: ${response.content}\n`); } }
async function main() { console.log("\n📝 LANGCHAIN PROMPTS DEMO\n"); await example1(); await example2(); await example3(); await example4(); console.log("✅ All prompt examples complete!"); }
main().catch(console.error);
Run it:
node src/02_prompts.js
Part 3 — Chains (LCEL)
What they are
Chains connect components together. The output of one step becomes the input of the next.
LangChain uses the 'pipe()' method to connect components — this is called LCEL (LangChain Expression Language).
Create src/03_chains.js:
import { ChatOpenAI } from "@langchain/openai"; import { ChatPromptTemplate } from "@langchain/core/prompts"; import { StringOutputParser, JsonOutputParser } from "@langchain/core/output_parsers"; // StringOutputParser = converts AIMessage → plain string // JsonOutputParser = converts AIMessage → JavaScript object
import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables"; // RunnableSequence = chain multiple steps together // RunnablePassthrough = pass input through unchanged (useful for branching)
import * as dotenv from "dotenv"; dotenv.config();
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0.1 }); const parser = new StringOutputParser(); // StringOutputParser extracts .content from AIMessage // so you get a plain string instead of an AIMessage object
// ───────────────────────────────────────── // EXAMPLE 1 — Basic pipe chain // prompt | llm | parser // ─────────────────────────────────────────
async function example1() { console.log("─".repeat(50)); console.log("EXAMPLE 1: Basic pipe chain (prompt | llm | parser)"); console.log("─".repeat(50));
const prompt = ChatPromptTemplate.fromMessages([ ["human", "What is {topic}? Answer in one sentence."], ]);
// Chain using pipe operator const chain = prompt.pipe(llm).pipe(parser); // Step 1: prompt → formats the template with input variables // Step 2: llm → calls GPT-4o with formatted messages // Step 3: parser → extracts .content string from AIMessage response
// Invoke the chain const result = await chain.invoke({ topic: "embeddings" }); // { topic: "embeddings" } → fills {topic} in prompt template
console.log("Result type:", typeof result); // "string" — parser converted AIMessage to string
console.log("Result:", result); // "Embeddings are numerical representations of text..." console.log(); }
// ───────────────────────────────────────── // EXAMPLE 2 — Chain with JSON output // Forces LLM to return structured data // ─────────────────────────────────────────
async function example2() { console.log("─".repeat(50)); console.log("EXAMPLE 2: JSON output chain"); console.log("─".repeat(50));
const jsonPrompt = ChatPromptTemplate.fromMessages([ ["system", `You are a data extractor. Extract information and return ONLY valid JSON. No explanation before or after the JSON.`], ["human", `Extract key information about {topic}.
Return this exact JSON structure: {{ "name": "topic name", "category": "what category it belongs to", "keyPoints": ["point 1", "point 2", "point 3"], "difficulty": "beginner/intermediate/advanced" }}`], // Note: {{ }} = escaped curly braces in template literals // Single { } = template variable // Double {{ }} = literal curly brace in the output ]);
const jsonParser = new JsonOutputParser(); // JsonOutputParser = parses LLM string output into JS object
const jsonChain = jsonPrompt.pipe(llm).pipe(jsonParser);
const result = await jsonChain.invoke({ topic: "vector databases" }); // result is now a JavaScript object — not a string
console.log("Result type:", typeof result); // "object"
console.log("Parsed JSON:"); console.log(JSON.stringify(result, null, 2)); // { // "name": "Vector Databases", // "category": "Database Technology", // "keyPoints": [...], // "difficulty": "intermediate" // } console.log(); }
// ───────────────────────────────────────── // EXAMPLE 3 — Sequential chain // Output of first chain becomes input of second // ─────────────────────────────────────────
async function example3() { console.log("─".repeat(50)); console.log("EXAMPLE 3: Sequential chain (chain of chains)"); console.log("─".repeat(50));
// First chain — summarize a topic const summaryPrompt = ChatPromptTemplate.fromMessages([ ["human", "Summarize {topic} in exactly 2 sentences."], ]); const summaryChain = summaryPrompt.pipe(llm).pipe(parser); // summaryChain: topic → 2-sentence summary
// Second chain — translate the summary const translatePrompt = ChatPromptTemplate.fromMessages([ ["human", "Translate this to simple bullet points:\n\n{summary}"], ]); const translateChain = translatePrompt.pipe(llm).pipe(parser); // translateChain: summary → bullet points
// Connect them — output of summaryChain feeds into translateChain const combinedChain = RunnableSequence.from([ summaryChain, // step 1: takes { topic } → returns summary string
(summary) => ({ summary }), // step 2: convert string → object with "summary" key // because translateChain expects { summary: "..." }
translateChain, // step 3: takes { summary } → returns bullet points ]);
const result = await combinedChain.invoke({ topic: "RAG systems" });
console.log("Final bullet points:"); console.log(result); console.log(); }
// ───────────────────────────────────────── // EXAMPLE 4 — Batch processing // Run same chain on multiple inputs at once // ─────────────────────────────────────────
async function example4() { console.log("─".repeat(50)); console.log("EXAMPLE 4: Batch processing"); console.log("─".repeat(50));
const prompt = ChatPromptTemplate.fromMessages([ ["human", "Classify this text as POSITIVE, NEGATIVE, or NEUTRAL:\n\n{text}"], ]);
const chain = prompt.pipe(llm).pipe(parser);
// Process multiple inputs at once const inputs = [ { text: "I love how fast this AI responds!" }, { text: "The system crashed again. Very frustrated." }, { text: "The meeting is scheduled for 3pm." }, { text: "This is the worst experience I've ever had." }, { text: "Temperature today is 22 degrees." }, ];
console.log("Processing", inputs.length, "texts in batch...\n");
const results = await chain.batch(inputs); // batch() = runs chain on all inputs in parallel // much faster than calling invoke() in a loop // returns array of results in same order as inputs
inputs.forEach((input, index) => { console.log(`Text: "${input.text.substring(0, 40)}..."`); console.log(`Classification: ${results[index].trim()}`); console.log(); }); }
// ───────────────────────────────────────── // EXAMPLE 5 — Streaming // Get response word by word as it generates // ─────────────────────────────────────────
async function example5() { console.log("─".repeat(50)); console.log("EXAMPLE 5: Streaming response"); console.log("─".repeat(50));
const prompt = ChatPromptTemplate.fromMessages([ ["human", "Write a short paragraph about {topic}."], ]);
const chain = prompt.pipe(llm).pipe(parser);
console.log("Streaming response (word by word):\n");
// stream() = returns an async iterator // each chunk = a small piece of the response as it generates const stream = await chain.stream({ topic: "the future of AI" });
for await (const chunk of stream) { // chunk = small string piece (usually 1-3 words) process.stdout.write(chunk); // process.stdout.write = print without newline // creates the streaming effect in terminal }
console.log("\n"); // newline after streaming is done }
async function main() { console.log("\n⛓️ LANGCHAIN CHAINS DEMO\n"); await example1(); await example2(); await example3(); await example4(); await example5(); console.log("✅ All chain examples complete!"); }
main().catch(console.error);
Run it:
node src/03_chains.js
The Mental Model — How LCEL Works
Input object
↓
Prompt Template
→ Takes input variables
→ Returns formatted messages array
↓
LLM (ChatOpenAI)
→ Takes messages array
→ Returns AIMessage object
↓
Output Parser
→ Takes AIMessage object
→ Returns string or object
↓
Final result
Written as:
prompt | llm | parser
Each | passes output of left → input of right
What You Can Do Now
// One liner RAG-style call: const answer = await(ragPrompt | llm | parser).invoke({ context: retrievedChunks, question: userQuestion, });
// Batch classify 100 support tickets: const classifications = await(classifyPrompt | llm | parser).batch( tickets.map(t => ({ ticket: t })) );
// Stream a long response: for await (const chunk of (prompt | llm | parser).stream({ topic })) { process.stdout.write(chunk); }
Three lines each. Compare to what you'd write manually.
3-Line Summary
ChatOpenAIis LangChain's model wrapper — call it with a string, message array, or chain — it always returns anAIMessagewith.contentas the response text.ChatPromptTemplatelets you define reusable prompt structures with{placeholders}— call.formatMessages()or pass it through a chain with.invoke({ variable: value }).- LCEL chains use the pipe operator
|to connect components —prompt | llm | parsermeans format input → call LLM → extract string — and the same chain supports.invoke(),.batch(), and.stream().
Module 6.2 — Complete ✅
Coming up — Module 6.3 — Output Parsers, Memory & Tools
Three more core LangChain concepts — structured output parsing so your AI returns proper JSON, conversation memory so your chatbot remembers what was said, and tools that let your LLM call external functions and APIs.