Tạo AI Agent bằng NestJS – Phần 2: Memory, Prompt Templates & Tool Calling

0 0 0

Người đăng: Nguyễn Văn Huy

Theo Viblo Asia

Ở Phần 1, chúng ta đã tạo một agent đơn giản bằng NestJS kết hợp LangChain và Gemini. Agent đó đã có thể nhận câu hỏi và phản hồi nhờ sức mạnh của LLM. Tuy nhiên, nó vẫn còn một số hạn chế:

  • Không nhớ gì về các cuộc trò chuyện trước đó.
  • Prompt chưa được tổ chức tốt, khó quản lý khi phức tạp hơn.
  • Không thể thực hiện hành động nào ngoài việc sinh văn bản.

Trong Phần 2 này, chúng ta sẽ nâng cấp Agent để giải quyết các vấn đề trên, giúp nó trở nên thông minh và hữu dụng hơn.

Mục tiêu phần 2

Tính năng Mô tả
Memory Giúp Agent "nhớ" lại các thông tin, câu hỏi đã được trao đổi trong cuộc hội thoại.
Prompt Template Tổ chức các prompt một cách chuyên nghiệp, dễ dàng quản lý, tái sử dụng và mở rộng khi cần thiết.
Tool Calling Cho phép Agent gọi đến các "công cụ" (functions, API, DB) để thực hiện hành động, không chỉ là nói chuyện.

1. Tích hợp Memory – Để Agent không "não cá vàng"

Một trong những yếu tố quan trọng nhất để cuộc trò chuyện với AI trở nên tự nhiên là khả năng ghi nhớ. LangChain cung cấp nhiều loại "Memory" khác nhau. Trong ví dụ này, chúng ta sẽ sử dụng BufferMemory, một loại memory đơn giản nhưng hiệu quả, lưu trữ toàn bộ lịch sử hội thoại. Nếu bạn theo dõi từ Phần 1 và đã cài langchain, BufferMemory thường có sẵn.

Thêm Memory vào AgentService

Chúng ta sẽ tạo một service mới hoặc nâng cấp service hiện tại để tích hợp memory. Hãy tưởng tượng chúng ta tạo một InteractiveAgentService mới. Service này sẽ quản lý một Agent Executor có gắn kèm Memory.

src/agents/interactive-agent.service.ts (Phần khởi tạo với Memory):

import { Injectable, Inject } from '@nestjs/common';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from '@langchain/core/prompts';
import { BufferMemory } from 'langchain/memory';
import { AgentExecutor, createStructuredChatAgent } from 'langchain/agents'; @Injectable()
export class InteractiveAgentService { private agentExecutor: AgentExecutor; private memory: BufferMemory; constructor( @Inject('GEMINI_CHAT_MODEL') private readonly llm: BaseChatModel, ) { this.memory = new BufferMemory({ returnMessages: true, memoryKey: 'chat_history', inputKey: 'input', outputKey: 'output', }); } async onModuleInit() { const prompt = ChatPromptTemplate.fromPromptMessages([ SystemMessagePromptTemplate.fromTemplate( `Bạn là một trợ lý AI thông minh.
Bạn có thể sử dụng các công cụ sau để hỗ trợ người dùng: {tool_names} Chi tiết từng công cụ:
{tools}` ), new MessagesPlaceholder('chat_history'), ['human', '{input} \n\n {agent_scratchpad}'], ]); const tools = []; const agent = await createStructuredChatAgent({ llm: this.llm, tools, prompt, }); this.agentExecutor = new AgentExecutor({ agent, tools, memory: this.memory, verbose: true, handleParsingErrors: true, }); } async interact(userInput: string): Promise<string> { console.log('User input:', userInput); const response = await this.agentExecutor.invoke({ input: userInput, }); return response.output; }
}

Cập nhật Module và Controller

src/app.module.ts:

// ... các import khác
import { InteractiveAgentService } from './agents/interactive-agent.service'; @Module({ imports: [ // ... ], controllers: [AppController], providers: [SimpleAgentService, ToolAgentService, InteractiveAgentService], // Thêm InteractiveAgentService
})
export class AppModule {}

src/app.controller.ts:

// ... các import khác
import { InteractiveAgentService } from './agents/interactive-agent.service'; @Controller()
export class AppController { constructor( // ... các service khác private readonly interactiveAgentService: InteractiveAgentService, ) {} @Get('interact') async interactWithAgent(@Query('question') question: string) { if (!question?.trim()) { throw new BadRequestException('Query parameter "question" cannot be empty.'); } try { const answer = await this.interactiveAgentService.interact(question); return { question, answer, timestamp: new Date().toISOString() }; } catch (error) { throw new InternalServerErrorException(`Lỗi xử lý yêu cầu: ${error.message}`); } }
}

Kiểm tra Memory

Sử dụng cURL hoặc Postman để gửi liên tiếp các request:

  • GET http://localhost:3000/interact?question=Chào bạn, tôi tên là An
    • Agent có thể trả lời: "Chào An, rất vui được gặp bạn! Tôi có thể giúp gì cho bạn?"
  • GET http://localhost:3000/interact?question=Bạn có nhớ tên tôi không?
    • Nhờ có BufferMemory, Agent sẽ có thể trả lời: "Tất nhiên rồi, bạn tên là An."

Nếu Agent trả lời đúng, BufferMemory đã hoạt động!

2. Dùng Prompt Template – Giúp prompt rõ ràng, dễ mở rộng

Khi các yêu cầu cho Agent trở nên phức tạp, việc viết prompt trực tiếp trong code (hardcode) sẽ rất khó quản lý và bảo trì. LangChain cung cấp PromptTemplateChatPromptTemplate để giải quyết vấn đề này.

Tại sao cần Prompt Template?

  • Tách biệt logic và nội dung prompt: Giữ code xử lý Agent và nội dung "hướng dẫn" Agent riêng biệt.
  • Dễ quản lý và phiên bản hóa: Bạn có thể lưu các prompt trong file riêng, dễ dàng chỉnh sửa, thử nghiệm nhiều phiên bản khác nhau mà không cần sửa code.
  • Tái sử dụng: Một template có thể được dùng ở nhiều nơi.
  • Hỗ trợ biến đầu vào (input variables) và định dạng phức tạp: Dễ dàng chèn các giá trị động vào prompt.

Tạo file Prompt Template riêng

Tạo thư mục src/prompts và file src/prompts/interactive-agent.prompt.ts:

import { ChatPromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate,
} from '@langchain/core/prompts'; export const interactiveAgentPromptTemplate = ChatPromptTemplate.fromMessages([ SystemMessagePromptTemplate.fromTemplate( `Bạn là một trợ lý AI siêu thông minh và rất chuyên nghiệp. Nhiệm vụ của bạn là hỗ trợ người dùng một cách tốt nhất có thể. Hãy luôn trả lời một cách rõ ràng, ngắn gọn và súc tích. Nếu bạn cần dùng công cụ, hãy suy nghĩ cẩn thận..
Bạn có thể sử dụng các công cụ sau để hỗ trợ người dùng: {tool_names} Chi tiết từng công cụ:
{tools}` ), new MessagesPlaceholder('chat_history'), ['human', 'Câu hỏi của người dùng: {input} \n\n {agent_scratchpad}'],
]);

Giải thích các MessagesPlaceholder:

  • 'chat_history': LangChain sẽ tự động điền lịch sử trò chuyện (từ BufferMemorymemoryKey='chat_history') vào vị trí này.
  • 'agent_scratchpad': Đây là không gian làm việc nội bộ của Agent. Khi Agent quyết định sử dụng một Tool, các bước suy nghĩ, tên Tool được gọi, và kết quả từ Tool sẽ được đưa vào đây để LLM tiếp tục xử lý.

Sử dụng Prompt Template trong Service

Cập nhật src/agents/interactive-agent.service.ts:

// ... các import khác
import { interactiveAgentPromptTemplate } from '../prompts/interactive-agent.prompt'; // Import template // ... bên trong constructor của InteractiveAgentService // ... // const prompt = ChatPromptTemplate.fromMessages([...]); // Dòng này được thay thế const prompt = interactiveAgentPromptTemplate; // Sử dụng template đã import const agent = createStructuredChatAgent({ llm: this.llm, tools, // Sẽ thêm tools ở phần sau prompt, // Truyền template vào agent }); // ...

Bằng cách này, nếu bạn muốn thay đổi cách Agent hành xử, bạn chỉ cần chỉnh sửa file interactive-agent.prompt.ts mà không cần chạm vào logic của InteractiveAgentService.

3. Tool Calling – Khi Agent không chỉ trả lời, mà còn thực thi

Đây là lúc Agent của chúng ta thực sự "ra tay". Tool Calling cho phép Agent sử dụng các "công cụ" (functions, API calls, database queries) để thực hiện các hành động cụ thể, thu thập thông tin mà LLM không có sẵn, hoặc tương tác với các hệ thống khác.

Tool là gì?

Tool trong LangChain là các đối tượng có:

name: Tên định danh duy nhất. description: Mô tả chi tiết về chức năng của tool và khi nào LLM nên sử dụng nó. Đây là phần cực kỳ quan trọng. schema (thường dùng Zod): Định nghĩa cấu trúc dữ liệu đầu vào mà tool mong đợi. func (hoặc _call): Hàm thực thi logic của tool.

Ví dụ: Tạo Tool getTime trả về giờ hiện tại

Chúng ta sẽ sử dụng DynamicStructuredTool để dễ dàng tạo một tool. Cần cài zod nếu chưa có:

yarn add zod

Tạo file src/tools/get-time.tool.ts:

import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod'; export const GetTimeTool = new DynamicStructuredTool({ name: 'get_current_time', description: 'Rất hữu ích khi bạn cần biết thời gian hiện tại. Chỉ dùng khi người dùng hỏi cụ thể về "mấy giờ rồi", "giờ hiện tại", "thời gian bây giờ".', schema: z.object({ // Tool này không cần input, nhưng schema vẫn cần được định nghĩa (có thể là object rỗng) // timezone: z.string().optional().describe("Múi giờ, ví dụ: Asia/Ho_Chi_Minh. Mặc định là giờ server nếu không cung cấp."), }), func: async ({ /*timezone*/ }) => { // Tham số timezone nếu có try { const now = new Date(); return `Bây giờ là ${now.toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}`; } catch (error) { return "Xin lỗi, tôi không thể lấy được thời gian hiện tại."; } },
});

Thêm Tool vào Agent

Cập nhật src/agents/interactive-agent.service.ts:

// ... các import khác
import { GetTimeTool } from '../tools/get-time.tool'; // Import tool vừa tạo @Injectable()
export class InteractiveAgentService { constructor( @Inject('GEMINI_CHAT_MODEL') private readonly llm: BaseChatModel, ) { this.memory = new BufferMemory({ /* ... */ }); const prompt = interactiveAgentPromptTemplate; const tools = [GetTimeTool]; // Thêm GetTimeTool vào danh sách const agent = createStructuredChatAgent({ llm: this.llm, tools, // Truyền tools vào agent prompt, }); this.agentExecutor = new AgentExecutor({ agent, tools, // AgentExecutor cũng cần biết về tools để thực thi chúng memory: this.memory, verbose: true, handleParsingErrors: true, }); } // ... interact method
}

Kiểm tra Tool Calling

Gọi API: GET http://localhost:3000/interact?question=Mấy giờ rồi bạn ơi?

Quan sát log console bạn sẽ thấy:

  1. LLM nhận câu hỏi.
  2. LLM phân tích và quyết định rằng cần dùng tool get_current_time.
  3. AgentExecutor gọi hàm func của GetTimeTool.
  4. Kết quả từ tool (Bây giờ là ...) được trả về cho LLM.
  5. LLM sử dụng thông tin này để tạo câu trả lời cuối cùng cho bạn.

Ví dụ, Agent có thể trả lời: "Dạ, bây giờ là 10:30:45 sáng ạ." (thời gian thực tế lúc gọi).

Gợi ý các Use-Case với Tool

Khả năng của Agent sẽ được mở rộng đáng kể với các Tool sáng tạo:

Tên Tool (Gợi ý) Mục đích Thư viện/Kỹ thuật có thể dùng
search_web Tìm kiếm thông tin trên Google, Bing, DuckDuckGo,... TavilySearchResults (LangChain Tool), google-it, axios
get_weather_forecast Lấy thông tin thời tiết chi tiết từ API thời tiết axios để gọi OpenWeatherMap API, WeatherAPI,...
query_database Truy vấn dữ liệu từ PostgreSQL, MongoDB, MySQL,... pg, mongodb, typeorm, prisma
send_notification Gửi email, tin nhắn Slack, Telegram,... nodemailer, Slack SDK, Telegram Bot API
run_code_interpreter Thực thi một đoạn code Python, JavaScript (cần sandbox cẩn thận!) vm2 (Node.js), Docker
calculate_math_expression Tính toán các biểu thức toán học phức tạp, chuyển đổi đơn vị mathjs
get_stock_price Lấy giá cổ phiếu từ các API tài chính axios để gọi Alpha Vantage, IEX Cloud API
summarize_url_content Tóm tắt nội dung từ một đường link URL axios, cheerio (để scrape), sau đó dùng LLM khác để tóm tắt

Lưu ý khi tạo Tool:

  • Mô tả (description) thật rõ ràng: Đây là yếu tố then chốt để LLM hiểu và sử dụng Tool đúng cách.
  • Schema (Zod) chi tiết: Giúp LLM biết cần cung cấp những tham số nào cho Tool.
  • Xử lý lỗi (Error Handling) trong func: Đảm bảo Tool của bạn không làm crash Agent nếu có sự cố.
  • Bảo mật: Cẩn trọng với các Tool có khả năng thực thi code hoặc tương tác với hệ thống nhạy cảm.

Tổng kết Phần 2

Qua Phần 2, Agent của bạn đã thực sự "trưởng thành" và mạnh mẽ hơn rất nhiều:

  • Có trí nhớ (Memory): Agent không còn "não cá vàng", có thể nhớ và sử dụng thông tin từ các lượt trò chuyện trước đó nhờ BufferMemory.
  • Có ngôn ngữ linh hoạt (Prompt Template): Việc quản lý và tinh chỉnh cách Agent giao tiếp trở nên dễ dàng, chuyên nghiệp hơn với ChatPromptTemplate được tách riêng.
  • Có thể hành động (Tool Calling): Agent không chỉ nói chuyện mà còn có thể thực thi các hành động cụ thể, tương tác với thế giới bên ngoài thông qua các Tools tùy chỉnh như GetTimeTool.

Với những nâng cấp này, bạn đã có một nền tảng vững chắc để xây dựng các ứng dụng AI phức tạp và thông minh hơn. Hãy tiếp tục khám phá và sáng tạo!

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tìm hiểu về NestJS (Phần 2)

Trong bài viết trước, mình đã giới thiệu về NestJS và các thành phần cơ bản của framework này cũng như xây dựng demo một api bằng NestJS. Như mình đã giới thiệu, NestJS có một hệ sinh thái hỗ trợ cho chúng ta trong quá trình phát triển mà các framework khác như Express, Fastify,... phải tự build hoặ

0 0 175

- vừa được xem lúc

NestJS - tìm hiểu và sử dụng Pipes

I. Giới thiệu.

0 0 45

- vừa được xem lúc

Authentication Với NestJS và Passport (Phần 1)

Authentication, hay xác thực thông tin người dùng, là một trong những tính năng cơ bản nhất của phần lớn ứng dụng Web. Trong bài viết này, mình xin chia sẻ phương pháp sử dụng passportjs để xây dựng t

0 0 99

- vừa được xem lúc

Authentication Với NestJS và Passport (Phần 2)

I. Giới thiệu.

0 0 181

- vừa được xem lúc

Middleware, Interceptor and Pipes in Nestjs

Middleware, Interceptor và Pipes củng không quá xa lạ với những anh em code Nestjs.Nhưng ai trong.

0 0 178

- vừa được xem lúc

NestJS - framework thần thánh cho Nodejs

Đọc thì có vẻ giật tít nhưng khoan, mọi chuyện không như bạn nghĩ, hãy nghe mình giải thích . 1.

0 0 65