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.
No comments:
Post a Comment