跪拜 Guibai
← Back to the summary

NestJS Meets LangChain: A Practical Guide to Wrapping vs. Building from Scratch

NestJS + LangChain Integration Guide: From Library Wrapper to Manual Construction

📌 Preface

There are two main ways to integrate LangChain in NestJS:

  1. Library Wrapper Approach: Use the third-party library nestjs-langchain to quickly integrate via LangChainModule and LangChainService.
  2. Manual Construction Approach: Use core packages like @langchain/openai directly in a Service to manually assemble Chains.

Each approach has its pros and cons. This article will start with the simplest "out-of-the-box" solution and gradually transition to a more flexible and controllable manual construction approach, helping you choose the right one based on your project's needs.


🚀 Phase 1: Library Wrapper Approach (Out-of-the-Box)

1.1 Install Dependencies

npm install nestjs-langchain langchain @langchain/openai

1.2 Register the LangChain Module

Configure globally in AppModule using LangChainModule.register():

// app.module.ts
import { Module } from '@nestjs/common';
import { LangChainModule } from 'nestjs-langchain';
import { AiModule } from './ai/ai.module';

@Module({
  imports: [
    LangChainModule.register({
      model: {
        model: 'openai:gpt-3.5-turbo',  // Specify the model
        apiKey: process.env.OPENAI_API_KEY,
      },
      systemPrompt: 'You are a helpful assistant.',
    }),
    AiModule,
  ],
})
export class AppModule {}

1.3 Inject and Use in a Service

Inject LangChainService via the constructor in any Service:

// ai.service.ts
import { Injectable } from '@nestjs/common';
import { LangChainService } from 'nestjs-langchain';

@Injectable()
export class AiService {
  constructor(private readonly langChainService: LangChainService) {}

  async askQuestion(question: string) {
    // Directly call the LangChainService run method
    return await this.langChainService.run(question);
  }
}

1.4 Create AI Tools (Advanced Feature)

One of the most powerful features of nestjs-langchain: expose any Service method as an AI-callable tool using the @Tool() decorator.

// math.service.ts
import { Injectable } from '@nestjs/common';
import { Tool, ToolParam } from 'nestjs-langchain';

@Injectable()
export class MathService {
  @Tool({
    description: 'Add two numbers. Use this tool when the user needs to perform addition.',
  })
  add(
    @ToolParam({ name: 'a', description: 'The first addend', type: 'number' })
    a: number,
    @ToolParam({ name: 'b', description: 'The second addend', type: 'number' })
    b: number,
  ): number {
    return a + b;
  }
}

When registering tools, simply pass MathModule into the tools array:

@Module({
  imports: [
    LangChainModule.register({
      model: { model: 'openai:gpt-3.5-turbo', apiKey: process.env.OPENAI_API_KEY },
      systemPrompt: 'You are an intelligent assistant that can use tools to help users.',
      tools: [MathModule], // Register all @Tool() methods in MathModule
    }),
    MathModule,
  ],
})
export class AppModule {}

1.5 Pros and Cons of the Library Wrapper Approach

Pros Cons
Out-of-the-box, simple configuration, less code ❌ Limited by the library's API, lower flexibility
Convenient tool registration, the @Tool() decorator is very elegant ❌ Support for the latest LangChain features may lag
✅ Complete dependency injection system, deeply integrated with NestJS ❌ Introduces extra dependencies, increasing project size

🔧 Phase 2: Manual Construction Approach (Full Control)

When your business scenario requires high customization (such as custom Prompt templates, multi-model switching, complex Chain orchestration), manual construction is the better choice. This approach uses core packages like @langchain/openai directly and assembles Chains within a Service.

2.1 Install Dependencies

npm install @langchain/openai @langchain/core

2.2 Create the AI Module

Use the NestJS CLI to generate the module, controller, and service:

nest g module ai
nest g controller ai
nest g service ai

2.3 Manually Build the Chain (Core Code)

This is the most critical part—manually initializing the LangChain chain in the AiService constructor.

// ai.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import type { Runnable } from '@langchain/core/runnables';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AiService {
    // Chain invocation object 
    private readonly chain: Runnable;

    constructor(@Inject(ConfigService) configService: ConfigService) {
        // 1. Define the Prompt template
        const prompt = PromptTemplate.fromTemplate(
            `Please answer the following question: \n\n{query}`
        );
        
        // 2. Initialize the ChatOpenAI model (read config from ConfigService)
        const model = new ChatOpenAI({
            temperature: 0.7,
            modelName: configService.get('MODEL_NAME'),
            apiKey: configService.get('OPENAI_API_KEY'),
            configuration: {
                baseURL: configService.get('OPENAI_BASE_URL'),
            },
        });
        
        // 3. Assemble the Chain (chain using the .pipe() method)
        this.chain = prompt.pipe(model).pipe(new StringOutputParser());
    }
    
    // 4. Expose the run method for the Controller to call
    async runChain(query: string): Promise<string> {
        return this.chain.invoke({ query });
    }
}

2.4 Register the Module

// ai.module.ts
import { Module } from '@nestjs/common';
import { AiService } from './ai.service';
import { AiController } from './ai.controller';

@Module({
  controllers: [AiController],
  providers: [AiService],
})
export class AiModule {}

2.5 Write the Controller

// ai.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { AiService } from './ai.service';

@Controller('ai')
export class AiController {
  constructor(private readonly aiService: AiService) {}
  
  @Get('chat')
  async chat(@Query('query') query: string) {
    const answer = await this.aiService.runChain(query);
    return {
      answer,
    };
  }
}

2.6 Configure Environment Variables

Add the necessary configuration to the .env file:

OPENAI_API_KEY=your-api-key-here
OPENAI_BASE_URL=https://api.openai.com/v1
MODEL_NAME=gpt-3.5-turbo

2.7 Line-by-Line Analysis of Key Code

Code Segment Explanation
PromptTemplate.fromTemplate() Defines the Prompt template, using the {query} placeholder
new ChatOpenAI({...}) Initializes the OpenAI model instance, reading config from ConfigService
prompt.pipe(model).pipe(new StringOutputParser()) Chains using the .pipe() method: Template → Model → Output Parser
this.chain.invoke({ query }) Executes the chain call, passing query to replace the placeholder in the template
ConfigService NestJS built-in config service, reads environment variables from the .env file

2.8 Pros and Cons of the Manual Construction Approach

Pros Cons
Fully controllable, freely customize every step ❌ Requires manual management of LangChain dependencies and versions
No extra dependencies, only uses LangChain core packages ❌ Relatively more code, needs self-maintained Chain construction logic
Keeps up with the latest LangChain features, not limited by third-party libraries ❌ Tool registration requires implementing Agent logic yourself
Easy to test, can mock specific methods

📊 Comparison Summary of the Two Approaches

Comparison Dimension Library Wrapper (nestjs-langchain) Manual Construction
Configuration Complexity Simple, centralized in AppModule Medium, requires self-initialization of Model and Chain
Flexibility Limited by library API Fully flexible, allows deep customization
Tool Registration @Tool() decorator, extremely convenient ❌ Requires manual implementation of Agent logic
Learning Curve Gentle, out-of-the-box Steeper, requires understanding of LangChain core concepts
Dependency Management One extra nestjs-langchain dependency Only depends on @langchain/* core packages
Applicable Scenarios Rapid prototyping, standard Q&A, tool invocation Complex Prompt engineering, multi-model switching, custom Chains

🎯 Practical Advice: How to Choose?

Choose the 'Library Wrapper Approach' if:

Choose the 'Manual Construction Approach' if:


🔄 Migrating from Manual Construction to Library Wrapper (or Vice Versa)

If you are currently using the manual construction approach (like the AiService in this article) and want to switch to the library wrapper approach, you just need to:

  1. Install nestjs-langchain.
  2. Register LangChainModule in AppModule.
  3. Delete the @Inject(ConfigService) and LangChain initialization code in AiService.
  4. Change the constructor to inject LangChainService.
  5. Call this.langChainService.run(question) instead of this.chain.invoke({ query }).

Conversely, if you switch from the library wrapper back to manual construction, just reverse the steps above.


🧠 Summary

The library wrapper approach is like "buying a pre-built PC," while manual construction is like "building your own."

A pre-built PC (library wrapper) is out-of-the-box and suitable for most scenarios; building your own (manual construction) takes a bit more effort, but you decide the specs of every component.

As a learning path, I suggest:

  1. Beginner Phase: First, get the full workflow running with the library wrapper approach to build confidence.
  2. Advanced Phase: Switch to manual construction to deeply understand LangChain's core concepts (Prompt, Model, Chain, Parser).
  3. Practical Phase: Based on actual project needs, flexibly choose between the two approaches or even mix them.

Your AiService manual construction code is very standard; you have perfectly mastered the second approach. If you need the AI to call external tools, you can always consider introducing the @Tool() decorator from nestjs-langchain to free your hands! 🚀