微信扫码
添加专属顾问
我要投稿
PORT=3001GROQ_API_KEY=<GROQ API KEY>GROQ_MODEL=gemma2-9b-itGEMINI_API_KEY=<GEMINI API KEY>GEMINI_TEXT_EMBEDDING_MODEL=text-embedding-004SWAGGER_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.tsexport type DuckDuckGoConfig = {maxResults: number;};
// groq-config.type.tsexport type GroqConfig = {model: string;apiKey: string;};
创建 Angular Doc 模块
nest g mo angularDoc
添加嵌入模型
// application/types/embedding-model-config.type.tsexport type EmbeddingModelConfig = {apiKey: string;embeddingModel: string;};
// application/embeddings/create-embedding-model.tsimport { 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.tsimport { 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();}
从检索器创建检索工具
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 markdownIf 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()]);}
创建 Agent 模块
nest g mo agentnest g s agent/application/agentExecutor --flatnest g s agent/application/dragonBall --flatnest g s agent/presenters/http/agent --flat
创建常量
// agent.constant.tsexport const AGENT_EXECUTOR = 'AGENT_EXECUTOR';
// groq-chat-model.constant.tsexport const GROQ_CHAT_MODEL = 'GROQ_CHAT_MODEL';
// tools.constant.tsexport const TOOLS = 'TOOLS';
Providers
// groq-chat-model.provider.tsimport { 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],};
// tool.provider.tsimport { 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],};
在 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.'),});()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';()export class AgentExecutorService implements ToolExecutor {private chatHistory = [];constructor(() 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 Messagesthis.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 {()()query: string;}
@Post() async ask(@Body() dto: AskDto): Promise<string> { const contents = await this.service.execute(dto.query); return toDivRows(contents); }修改应用控制器以渲染 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>
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2025-12-08
让AI智能体拥有像人类的持久记忆:基于LangGraph的长短期记忆管理实践指南
2025-12-04
Agentic RAG这样用LangChain解决复杂问题
2025-12-01
Deep Agent 进化论:基于文件系统的 Context Engineering 深度解析
2025-11-27
langgraph 1.0.4 最新发布:功能优化与修复详解
2025-11-25
LangChain 最新agent框架deepagents测评:长任务友好,高可控
2025-11-25
被 LangChain 全家桶搞晕了?LangGraph、LangSmith、LangFlow 一文读懂
2025-11-21
如何用 LangGraph 构建高效的 Agentic 系统
2025-11-19
LangChain v1.0 模型选型:静态还是动态?一文看懂 Agent 的正确打开方式
2025-09-13
2025-09-21
2025-11-03
2025-10-23
2025-10-19
2025-10-31
2025-11-06
2025-11-05
2025-09-19
2025-10-23
2025-11-03
2025-10-29
2025-07-14
2025-07-13
2025-07-05
2025-06-26
2025-06-13
2025-05-21