本文原作者: Connie Leung, 谷歌开发者专家 (GDE),原文发布于: DEV Community
https://dev.to/railsstudent/build-agentic-rag-application-using-langchainjs-nestjs-htmx-and-gemma-2-3imd
本文将为您介绍如何使用 LangChain、NestJS 和 Gemma 2 构建 Agentic RAG 应用。然后,HTMX 和 Handlebar 模板引擎将响应呈现为列表。该应用使用 LangChain 创建内置的 DuckDuckGoSearch 工具以在互联网上查找信息。它还构建了一个自定义工具,用于调用 Dragon Ball Z API 来筛选角色,并返回其种族、隶属关系和能力等信息。最后构建了两个检索工具,用于从 angular.dev 检索 Angular Signal 和 Angular Form 网页。
这些工具均绑定到 Gemma 2 模型,然后模型、工具和聊天历史记录将传给 LangChain 智能体。智能体在收到查询请求时进行相应调用,可以智能生成函数调用,并使用正确的工具生成响应。
设置环境变量
将 .env.example 复制到 .env
访问 https://aistudio.google.com/app/apikey,登录帐号以创建一个新的 API 密钥。将 API 密钥替换为 GENINI_API_KEY。
访问 Groq Cloud: https://console.groq.com/,登录帐号并注册一个新的 API 密钥。将 API 密钥替换为 GROQ_API_KEY。
安装依赖项
定义应用的配置
创建 src/configs文件夹并在其中添加 configuration.ts文件。
创建 src/configs/types文件夹,然后添加 duck-config.type.ts和 groq-config.type.ts文件。DuckDuckGoConfig和 GroqConfig是将环境变量存储到自定义对象的配置类型。
// duck-config.type.ts
exporttypeDuckDuckGoConfig = { maxResults: number; };
exporttypeGroqConfig = { model: string; apiKey: string; };
创建 Angular Doc 模块
为从 Angular 的官方文档中生成响应的检索工具创建一个 Angular Doc 模块。
添加嵌入模型
添加 Gemini 文本嵌入模型,从而将文档计算为向量数组。在 application/embeddings文件夹下创建 create-embedding-model.ts文件。
// application/types/embedding-model-config.type.ts
exporttypeEmbeddingModelConfig = { apiKey: string; embeddingModel: string; };
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';
exportfunctioncreateTextEmbeddingModel( configService: ConfigService, title = 'Angular') { const{ apiKey, embeddingModel: model } = configService.get<EmbeddingModelConfig>( 'gemini'); returnnewGoogleGenerativeAIEmbeddings({ apiKey,model,taskType: TaskType.RETRIEVAL_DOCUMENT, title,});}
创建文档
辅助函数将网页列表的内容加载到文档中,并将文档拆分为小块。loadWebPage是一个辅助函数,用于加载来自 angular.dev的网页,并返回拆分后的文档。
// application/loaders/web-page-loader.ts
import{ RecursiveCharacterTextSplitter } from'@langchain/textsplitters'; import{ CheerioWebBaseLoader } from'@langchain/community/document_loaders/web/cheerio';
asyncfunctionloadWebPages( webPages: string[]) { constloaders = webPages.map( ( page) => newCheerioWebBaseLoader(page)); constdocs = awaitPromise.all(loaders.map( ( loader) => loader.load)); constsignalDocs = docs.flat; returnsplitter.splitDocuments(signalDocs); }
loadSignalWebPages函数将 Angular Signal 的页面加载到拆分后的文档中。
exportasyncfunctionloadSignalWebPages( ) { constwebPages = [ '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', ];
returnloadWebPages(webPages); }
loadFormWebPages函数将 Angular Form 的页面加载到拆分后的文档中。
returnloadWebPages(webPages); }
创建检索器
文本嵌入模型通过计算将文档块转换为向量,为了简化操作,向量存储于 MemoryVectorStore中。向量存储调用 asRetriever方法以返回向量存储检索器。
privateasynccreateSignalRetriever { constdocs = awaitloadSignalWebPages; this.logger.log( `number of signal docs -> ${docs.length}` ); constembeddings = createTextEmbeddingModel( this.configService, 'Angular Signal'); constvectorStore = awaitMemoryVectorStore.fromDocuments(docs, embeddings); returnvectorStore.asRetriever; }
privateasynccreateFormRetriever { constdocs = awaitloadFormWebPages; this.logger.log( `number of form docs -> ${docs.length}` ); constembeddings = createTextEmbeddingModel( this.configService, 'Angular Forms'); constvectorStore = awaitMemoryVectorStore.fromDocuments(docs, embeddings); returnvectorStore.asRetriever; }
createSignalRetriever函数返回一个用于 Angular Signal 的检索器,createFormRetriever函数返回一个用于 Angular 模板驱动、响应式和动态表单的检索器。
从检索器创建检索工具
privateasynccreateFormRetrieverTool: Promise<DynamicStructuredTool< any>> { constretriever = awaitthis.createFormRetriever; returncreateRetrieverTool(retriever, { name: 'angular_form_search', deion: `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.`, });}
asynccreateRetrieverTools: Promise<DynamicStructuredTool< any>[]> { returnPromise.all([ this.createSignalRetrieverTool, this.createFormRetrieverTool]); }
createSignalRetrieverTool函数调用 createRetrieverTool方法从 Angular Signal 检索器创建工具。createFormRetrieverTool从 Angular Form 检索器创建工具。最后,createRetrieverTools函数调用 createSignalRetrieverTool和 createFormRetrieverTool来返回检索工具数组。
创建 Agent 模块
智能体模块负责创建一个 LangChain 智能体,该智能体执行各种工具以生成响应。
创建常量
// agent.constant.ts
exportconstAGENT_EXECUTOR = 'AGENT_EXECUTOR';
// groq-chat-model.constant.ts
exportconstGROQ_CHAT_MODEL = 'GROQ_CHAT_MODEL';
// tools.constant.ts
exportconstTOOLS = 'TOOLS';
定义常量是为了在 NestJS 应用中注入自定义资源。
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';
exportfunctionInjectChatModel( ) { returnInject(GROQ_CHAT_MODEL); }
exportconstGroqChatModelProvider: Provider<ChatGroq> = { provide: GROQ_CHAT_MODEL, useFactory: ( configService: ConfigService) => { const{ apiKey, model } = configService.get<GroqConfig>( 'groq'); returnnewChatGroq({ 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';
exportconstToolsProvider: Provider<Tool[]> = { provide: TOOLS, useFactory: async(service: ConfigService, dragonBallService: DragonBallService, docsService: AngularDocsService) => { const{ maxResults } = service.get<DuckDuckGoConfig>( 'duckDuckGo'); constduckTool = newDuckDuckGoSearch({ maxResults }); constcharacterFiltertool = dragonBallService.createCharactersFilterTool; constretrieverTools = awaitdocsService.createRetrieverTools; return[duckTool, characterFiltertool, ...retrieverTools]; },inject: [ConfigService, DragonBallService, AngularDocsService], };
DuckDuckGoSearch是一款用于在互联网上搜索信息的 LangChain 工具。characterFilterTool是一款自定义工具,可调用 Dragon Ball API 根据给定条件筛选角色。retrieverTools是一组工具,用于返回 Angular Signal 和 Angular Form 的知识。ToolsProvider提供程序会返回一个工具列表,智能体可以执行这些工具来获取信息。
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';
constprompt = ChatPromptTemplate.fromMessages([ [ 'system', 'You are a helpful assistant.'], [ 'placeholder', '{chat_history}'], [ 'human', '{input}'], [ 'placeholder', '{agent_scratchpad}'], ]);
exportfunctionInjectAgent( ) { returnInject(AGENT_EXECUTOR); }
exportconstAgentExecutorProvider: Provider<AgentExecutor> = { provide: AGENT_EXECUTOR, useFactory: async(llm: ChatGroq, tools: Tool[]) => { constagent = awaitcreateToolCallingAgent({ llm, tools, prompt, streamRunnable: false}); console.log( 'tools', tools);
returnAgentExecutor.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';
exportconstcharacterFilterSchema = 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.'), });
@InjectableexportclassDragonBallService { constructor( privatereadonly httpService: HttpService ) {}
asyncgetCharacters(characterFilter: CharacterFilter): Promise< string> { constfilter = this.buildFilter(characterFilter);
if(!filter) { returnthis.generateMarkdownList([]); }
constcharacters = awaitthis.httpService.axiosRef .get<Character[]>( `https://dragonball-api.com/api/characters? ${filter}` ) .then( ( { data }) => data);
returnthis.generateMarkdownList(characters); }
createCharactersFilterTool: DynamicStructuredTool< any> { returntool( async(input: CharacterFilter): Promise< string> => this.getCharacters(input), { name: 'dragonBallCharacters', deion: `Call Dragon Ball filter characters API to retrieve characters by name, race, affiliation, or gender.`, schema: characterFilterSchema,});}
getCharacters方法接受姓名、性别、种族和隶属关系等可选条件。然后,它将查询参数附加到 Dragon Ball URL 以检索字符并生成 markdown 文件。createCharactersFilterTool从 LangChain 导入工具,用于创建可供智能体使用的自定义工具。
创建 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';
@InjectableexportclassAgentExecutorService implementsToolExecutor { privatechatHistory = [];
constructor( @InjectAgentprivateagentExecutor: AgentExecutor ) {}
asyncexecute(input: string): Promise<AgentContent[]> { const{ output } = awaitthis.agentExecutor.invoke({ input, chat_history: this.chatHistory });
this.chatHistory = this.chatHistory.concat([ newHumanMessage(input), newAIMessage(output)]); if( this.chatHistory.length > 10) { // remove the oldest Human and AI Messagesthis.chatHistory.splice( 0, 2); }return[ {role: 'Human', content: input,},{role: 'Assistant', content: output,},];}}
AgentExecutorService服务非常简单。它注入 AgentExecutor的实例,调用 invoke方法,将输入提交到链上并输出一个字符串。该方法将聊天历史记录中人类和 AI 之间的聊天消息存储在内存中,并将对话返回给模板引擎进行渲染。
添加 Agent 控制器
exportclassAskDto { @IsString@IsNotEmptyquery: string; }
@Postasyncask( @Bodydto: AskDto): Promise< string> { constcontents = awaitthis.service.execute(dto.query); returntoDivRows(contents); }
Agent 控制器将查询提交到链上,获取结果,并将 HTML 代码发送回模板引擎进行渲染。
修改应用控制器以渲染 Handlebar 模板
应用控制器通知 Handlebar 模板引擎渲染 index.hbs文件。
HTMX 和 Handlebar 模板引擎
这是一个用于显示对话的简单界面。
default.hbs<!DOCTYPE html>< htmllang= "en"> < head> < metacharset= "utf-8"/> < metaname= "deion"content= "Angular tech book RAG powed by gemma 2 LLM."/> < metaname= "author"content= "Connie Leung"/> < metaname= "viewport"content= "width=device-width, initial-scale=1.0"/> < title> {{{ title }}} </ title> < style> *, * ::before, * ::after{ padding: 0; margin: 0; box-sizing: border-box; }</ style> < src= "https://cdn.tailwindcss.com?plugins=forms,typography"> </ > </ head> < bodyclass= "p-4 w-screen h-screen min-h-full"> < src= "https://unpkg.com/htmx.org@2.0.1"integrity= "sha384-QWGpdj554B4ETpJJC9z+ZHJcA/i59TyjxEPXiiUgN2WmTyV5OEZWCD6gQhgkdpB/"crossorigin= "anonymous"> </ > < divclass= "h-full grid grid-rows-[70px_1fr_40px] grid-cols-[1fr]"> {{> header }}{{{ body }}}{{> footer }}</ div> </ body> </ html>
以上是具有页眉、页脚和正文的默认布局。正文最终显示的是 AI 与人类之间的对话。页眉部分则导入 Tailwind,用于设置 HTML 元素的样式,并导入 HTMX 来与服务器交互。
用户可以在文本框中输入问题,然后点击 "发送" 按钮。该按钮向 /agent发出 POST 请求并将对话附加到列表中。
这个 LangChain Agentic RAG 应用到此就创建完成了,创建时用到了 Gemma 2 模型和各种工具,以生成响应。
资源
欢迎您查阅 Github 代码库,以获取更多实用资源:
https://github.com/railsstudent/nestjs-gemma2-wiki-summarizer-app
谷歌开发者特别招募活动 进行中返回搜狐,查看更多
责任编辑: