逐步解析 | 如何使用 LangChain、NestJS 和 Gemma 2 构建一个 Agentic RAG 应用
本文原作者:Connie Leung, 谷歌开发者专家 (GDE),原文发布于:DEV Community
https://dev.to/railsstudent/build-agentic-rag-application-using-langchainjs-nestjs-htmx-and-gemma-2-3imd
这些工具均绑定到 Gemma 2 模型,然后模型、工具和聊天历史记录将传给 LangChain 智能体。智能体在收到查询请求时进行相应调用,可以智能生成函数调用,并使用正确的工具生成响应。
设置环境变量
将 .env.example 复制到 .env
PORT=3001
GROQ_API_KEY=<GROQ API KEY>
GROQ_MODEL=gemma2-9b-it
GEMINI_API_KEY=<GEMINI API KEY>
GEMINI_TEXT_EMBEDDING_MODEL=text-embedding-004
SWAGGER_TITLE='Langchain Search Agent'
SWAGGER_DESCRIPTION='Use Langchain tools and agent to search information on the Internet.'
SWAGGER_VERSION='1.0'
SWAGGER_TAG='Gemma 2, Langchain.js, Agent Tools'
DUCK_DUCK_GO_MAX_RESULTS=1
安装依赖项
npm i -save-exact @google/generative-ai @langchain/community
@langchain/core @langchain/google-genai @langchain/groq @nestjs/axios @nestjs/config @nestjs/swagger @nestjs/throttler axios cheerio class-transformer class-validator compression duck-duck-scrape hbs langchain zod
定义应用的配置
export default () => ({
port: parseInt(process.env.PORT || '3001', 10),
groq: {
apiKey: process.env.GROQ_API_KEY || '',
model: process.env.GROQ_MODEL || 'gemma2-9b-it',
},
gemini: {
apiKey: process.env.GEMINI_API_KEY || '',
embeddingModel: process.env.GEMINI_TEXT_EMBEDDING_MODEL || 'text-embedding-004',
},
swagger: {
title: process.env.SWAGGER_TITLE || '',
description: process.env.SWAGGER_DESCRIPTION || '',
version: process.env.SWAGGER_VERSION || '',
tag: process.env.SWAGGER_TAG || '',
},
duckDuckGo: {
maxResults: parseInt(process.env.DUCK_DUCK_GO_MAX_RESULTS || '1', 10),
},
});
// duck-config.type.ts
export type DuckDuckGoConfig = {
maxResults: number;
};
// groq-config.type.ts
export type GroqConfig = {
model: string;
apiKey: string;
};
创建 Angular Doc 模块
nest g mo angularDoc
添加嵌入模型
// application/types/embedding-model-config.type.ts
export type EmbeddingModelConfig = {
apiKey: string;
embeddingModel: string;
};
// application/embeddings/create-embedding-model.ts
import { TaskType } from '@google/generative-ai';
import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
import { ConfigService } from '@nestjs/config';
import { EmbeddingModelConfig } from '../types/embedding-model-config.type';
export function createTextEmbeddingModel(configService: ConfigService, title = 'Angular') {
const { apiKey, embeddingModel: model } = configService.get<EmbeddingModelConfig>('gemini');
return new GoogleGenerativeAIEmbeddings({
apiKey,
model,
taskType: TaskType.RETRIEVAL_DOCUMENT,
title,
});
}
创建文档
// application/loaders/web-page-loader.ts
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio';
async function loadWebPages(webPages: string[]) {
const loaders = webPages.map((page) => new CheerioWebBaseLoader(page));
const docs = await Promise.all(loaders.map((loader) => loader.load()));
const signalDocs = docs.flat();
return splitter.splitDocuments(signalDocs);
}
export async function loadSignalWebPages() {
const webPages = [
'https://angular.dev/guide/signals',
'https://angular.dev/guide/signals/rxjs-interop',
'https://angular.dev/guide/signals/inputs',
'https://angular.dev/guide/signals/model',
'https://angular.dev/guide/signals/queries',
'https://angular.dev/guide/components/output-fn',
];
return loadWebPages(webPages);
}
export async function loadFormWebPages() {
const webPages = [
'https://angular.dev/guide/forms',
'https://angular.dev/guide/forms/reactive-forms',
'https://angular.dev/guide/forms/typed-forms',
'https://angular.dev/guide/forms/template-driven-forms',
'https://angular.dev/guide/forms/form-validation',
'https://angular.dev/guide/forms/dynamic-forms',
];
return loadWebPages(webPages);
}
创建检索器
private async createSignalRetriever() {
const docs = await loadSignalWebPages();
this.logger.log(`number of signal docs -> ${docs.length}`);
const embeddings = createTextEmbeddingModel(this.configService, 'Angular Signal');
const vectorStore = await MemoryVectorStore.fromDocuments(docs, embeddings);
return vectorStore.asRetriever();
}
private async createFormRetriever() {
const docs = await loadFormWebPages();
this.logger.log(`number of form docs -> ${docs.length}`);
const embeddings = createTextEmbeddingModel(this.configService, 'Angular Forms');
const vectorStore = await MemoryVectorStore.fromDocuments(docs, embeddings);
return vectorStore.asRetriever();
}
createSignalRetriever 函数返回一个用于 Angular Signal 的检索器,createFormRetriever 函数返回一个用于 Angular 模板驱动、响应式和动态表单的检索器。
从检索器创建检索工具
private async createSignalRetrieverTool(): Promise<DynamicStructuredTool<any>> {
const retriever = await this.createSignalRetriever();
return createRetrieverTool(retriever, {
name: 'angular_signal_search',
description: `Search for information about Angular Signal.
For any questions about Angular Signal API, you must use this tool!
Please Return the answer in markdown
If you do not know the answer, please say you don't know.
`,
});
}
private async createFormRetrieverTool(): Promise<DynamicStructuredTool<any>> {
const retriever = await this.createFormRetriever();
return createRetrieverTool(retriever, {
name: 'angular_form_search',
description: `Search for information about Angular reactive, typed reactive, template-drive, and dynamic forms.
For any questions about Angular Forms, you must use this tool!
Please return the answer in markdown.
If you do not know the answer, please say you don't know.`,
});
}
async createRetrieverTools(): Promise<DynamicStructuredTool<any>[]> {
return Promise.all([this.createSignalRetrieverTool(), this.createFormRetrieverTool()]);
}
createSignalRetrieverTool 函数调用 createRetrieverTool 方法从 Angular Signal 检索器创建工具。createFormRetrieverTool 从 Angular Form 检索器创建工具。最后,createRetrieverTools 函数调用 createSignalRetrieverTool 和 createFormRetrieverTool 来返回检索工具数组。
创建 Agent 模块
nest g mo agent
nest g s agent/application/agentExecutor --flat
nest g s agent/application/dragonBall --flat
nest g s agent/presenters/http/agent --flat
创建常量
// agent.constant.ts
export const AGENT_EXECUTOR = 'AGENT_EXECUTOR';
// groq-chat-model.constant.ts
export const GROQ_CHAT_MODEL = 'GROQ_CHAT_MODEL';
// tools.constant.ts
export const TOOLS = 'TOOLS';
Providers
GROQ_CHAT_MODEL 创建一个应用了 Gemma 2 模型的 Groq 聊天模型。
// groq-chat-model.provider.ts
import { ChatGroq } from '@langchain/groq';
import { Inject, Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GroqConfig } from '~configs/types/groq-config.type';
import { GROQ_CHAT_MODEL } from '../constants/groq-chat-model.constant';
export function InjectChatModel() {
return Inject(GROQ_CHAT_MODEL);
}
export const GroqChatModelProvider: Provider<ChatGroq> = {
provide: GROQ_CHAT_MODEL,
useFactory: (configService: ConfigService) => {
const { apiKey, model } = configService.get<GroqConfig>('groq');
return new ChatGroq({
apiKey,
model,
temperature: 0.3,
maxTokens: 2048,
streaming: false,
});
},
inject: [ConfigService],
};
TOOLS 注入了一组工具,供智能体执行并生成结果。
// tool.provider.ts
import { DuckDuckGoSearch } from '@langchain/community/tools/duckduckgo_search';
import { Tool } from '@langchain/core/tools';
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AngularDocsService } from '~angular-docs/application/angular-docs.service';
import { DuckDuckGoConfig } from '~configs/types/duck-config.type';
import { TOOLS } from '../constants/tools.constant';
import { DragonBallService } from '../dragon-ball.service';
export const ToolsProvider: Provider<Tool[]> = {
provide: TOOLS,
useFactory: async (service: ConfigService, dragonBallService: DragonBallService, docsService: AngularDocsService) => {
const { maxResults } = service.get<DuckDuckGoConfig>('duckDuckGo');
const duckTool = new DuckDuckGoSearch({ maxResults });
const characterFiltertool = dragonBallService.createCharactersFilterTool();
const retrieverTools = await docsService.createRetrieverTools();
return [duckTool, characterFiltertool, ...retrieverTools];
},
inject: [ConfigService, DragonBallService, AngularDocsService],
};
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { Tool } from '@langchain/core/tools';
import { ChatGroq } from '@langchain/groq';
import { Inject, Provider } from '@nestjs/common';
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';
import { AGENT_EXECUTOR } from '../constants/agent.constant';
import { GROQ_CHAT_MODEL } from '../constants/groq-chat-model.constant';
import { TOOLS } from '../constants/tools.constant';
const prompt = ChatPromptTemplate.fromMessages([
['system', 'You are a helpful assistant.'],
['placeholder', '{chat_history}'],
['human', '{input}'],
['placeholder', '{agent_scratchpad}'],
]);
export function InjectAgent() {
return Inject(AGENT_EXECUTOR);
}
export const AgentExecutorProvider: Provider<AgentExecutor> = {
provide: AGENT_EXECUTOR,
useFactory: async (llm: ChatGroq, tools: Tool[]) => {
const agent = await createToolCallingAgent({ llm, tools, prompt, streamRunnable: false });
console.log('tools', tools);
return AgentExecutor.fromAgentAndTools({
agent,
tools,
verbose: true,
});
},
inject: [GROQ_CHAT_MODEL, TOOLS],
};
AgentExecutorProvider 提供程序使用智能体和工具创建智能体执行器。智能体执行器生成函数调用,智能体负责调用工具以生成相关响应。
在 DragonBall Service 中创建自定义工具
import { DynamicStructuredTool, tool } from '@langchain/core/tools';
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { z } from 'zod';
import { CharacterFilter } from './types/character-filter.type';
import { Character } from './types/character.type';
export const characterFilterSchema = z.object({
name: z.string().optional().describe('Name of a Dragon Ball Z character.'),
gender: z.enum(['Male', 'Female', 'Unknown']).optional().describe('Gender of a Dragon Ball Z caracter.'),
race: z.enum(['Human', 'Saiyan'])
.optional()
.describe('Race of a Dragon Ball Z character'),
affiliation: z.enum(['Z Fighter', 'Red Ribbon Army', 'Namekian Warrior'])
.optional()
.describe('Affiliation of a Dragon Ball Z character.'),
});
@Injectable()
export class DragonBallService {
constructor(private readonly httpService: HttpService) {}
async getCharacters(characterFilter: CharacterFilter): Promise<string> {
const filter = this.buildFilter(characterFilter);
if (!filter) {
return this.generateMarkdownList([]);
}
const characters = await this.httpService.axiosRef
.get<Character[]>(`https://dragonball-api.com/api/characters?${filter}`)
.then(({ data }) => data);
return this.generateMarkdownList(characters);
}
createCharactersFilterTool(): DynamicStructuredTool<any> {
return tool(async (input: CharacterFilter): Promise<string> => this.getCharacters(input), {
name: 'dragonBallCharacters',
description: `Call Dragon Ball filter characters API to retrieve characters by name, race, affiliation, or gender.`,
schema: characterFilterSchema,
});
}
创建 Agent 执行器服务
import { AIMessage, HumanMessage } from '@langchain/core/messages';
import { Injectable } from '@nestjs/common';
import { AgentExecutor } from 'langchain/agents';
import { ToolExecutor } from './interfaces/tool.interface';
import { InjectAgent } from './providers/agent-executor.provider';
import { AgentContent } from './types/agent-content.type';
@Injectable()
export class AgentExecutorService implements ToolExecutor {
private chatHistory = [];
constructor(@InjectAgent() private agentExecutor: AgentExecutor) {}
async execute(input: string): Promise<AgentContent[]> {
const { output } = await this.agentExecutor.invoke({ input, chat_history: this.chatHistory });
this.chatHistory = this.chatHistory.concat([new HumanMessage(input), new AIMessage(output)]);
if (this.chatHistory.length > 10) {
// remove the oldest Human and AI Messages
this.chatHistory.splice(0, 2);
}
return [
{
role: 'Human',
content: input,
},
{
role: 'Assistant',
content: output,
},
];
}
}
private chatHistory = [];
if (this.chatHistory.length > 10) {
// remove the oldest Human and AI Messages
this.chatHistory.splice(0, 2);
}
添加 Agent 控制器
import { IsNotEmpty, IsString } from 'class-validator';
export class AskDto {
@IsString()
@IsNotEmpty()
query: string;
}
@Post()
async ask(@Body() dto: AskDto): Promise<string> {
const contents = await this.service.execute(dto.query);
return toDivRows(contents);
}
Agent 控制器将查询提交到链上,获取结果,并将 HTML 代码发送回模板引擎进行渲染。
修改应用控制器以渲染 Handlebar 模板
@Controller()
export class AppController {
@Render('index')
@Get()
async getHello(): Promise<Record<string, string>> {
return {
title: 'Langchain Search Agent',
};
}
}
HTMX 和 Handlebar 模板引擎
这是一个用于显示对话的简单界面。
default.hbs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description" content="Angular tech book RAG powed by gemma 2 LLM." />
<meta name="author" content="Connie Leung" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{{ title }}}</title>
<style>
*, *::before, *::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}
</style>
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
</head>
<body class="p-4 w-screen h-screen min-h-full">
<script src="https://unpkg.com/htmx.org@2.0.1" integrity="sha384-QWGpdj554B4ETpJJC9z+ZHJcA/i59TyjxEPXiiUgN2WmTyV5OEZWCD6gQhgkdpB/" crossorigin="anonymous"></script>
<div class="h-full grid grid-rows-[70px_1fr_40px] grid-cols-[1fr]">
{{> header }}
{{{ body }}}
{{> footer }}
</div>
</body>
</html>
<div>
<div class="mb-2 p-1 border border-solid border-[#464646] rounded-lg">
<p class="text-[1.25rem] mb-2 text-[#464646] underline">Architecture</p>
<ul id="architecture" hx-trigger="load" hx-get="/agent/architecture"
hx-target="#architecture" hx-swap="innerHTML"></ul>
</div>
<div id="results" class="mb-4 h-[300px] overflow-y-auto overflow-x-auto"></div>
<form id="rag-form" hx-post="/agent" hx-target="#results" hx-swap="beforeend swap:1s">
<div>
<label>
<span class="text-[1rem] mr-1 w-1/5 mb-2 text-[#464646]">Question: </span>
<input type="text" name="query" class="mb-4 w-4/5 rounded-md p-2"
placeholder="Ask the agent"
aria-placeholder="Placeholder to ask any question to the agent"></input>
</label>
</div>
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white p-2 text-[1rem] flex justify-center items-center rounded-lg">
<span class="mr-1">Send</span><img class="w-4 h-4 htmx-indicator" src="/images/spinner.gif">
</button>
</form>
</div>
资源
https://github.com/railsstudent/nestjs-gemma2-wiki-summarizer-app
长按右侧二维码
查看更多开发者精彩分享