Module 6.2 — Models, Prompts & Chains

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:


    npm install langchain @langchain/core @langchain/openai dotenv

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

  1. ChatOpenAI is LangChain's model wrapper — call it with a string, message array, or chain — it always returns an AIMessage with .content as the response text.
  2. ChatPromptTemplate lets you define reusable prompt structures with {placeholders} — call .formatMessages() or pass it through a chain with .invoke({ variable: value }).
  3. LCEL chains use the pipe operator | to connect components — prompt | llm | parser means 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

Module 6.2 — Models, Prompts & Chains

Writing real LangChain code for the first time Project Setup First — new project for all LangChain work:     mkdir langchain-explorer     cd...