0
0
Lập trình
TT

Khởi đầu với phát triển AI Agent bằng LangChain & LangGraph

Đăng vào 3 tháng trước

• 11 phút đọc

Giới thiệu

Trong năm 2023, khi tôi bắt đầu sử dụng ChatGPT, nó chỉ là một chatbot đơn giản mà tôi có thể hỏi những câu hỏi thú vị và nhận phản hồi về mã của mình. Mọi thứ đều bình thường; ứng dụng không có bộ nhớ về trạng thái trước đó hoặc những gì bạn đã nói hôm qua. Tuy nhiên, vào năm 2024, mọi thứ bắt đầu thay đổi. Chúng ta đã chuyển từ một ChatBot không trạng thái sang một AI agent có thể gọi công cụ, tìm kiếm trên internet và tạo liên kết tải xuống.

Sự phát triển này đã khiến tôi tự hỏi: Làm thế nào mà một LLM (Large Language Model) có thể tìm kiếm trên internet? Có phải nó có thể tạo ra công cụ, chương trình của riêng nó hoặc thực thi mã của riêng nó không? Rõ ràng là chúng ta đang tiến tới một cuộc cách mạng Skynet.

Và đó là lúc tôi bắt đầu tìm hiểu và phát hiện ra LangChain, một công cụ hứa hẹn những điều kỳ diệu mà không cần ngân sách hàng tỷ đô la.

LLM Agent là gì?

Theo định nghĩa, một LLM agent là một chương trình phần mềm có khả năng nhận thức môi trường, ra quyết định và thực hiện hành động tự động để đạt được các mục tiêu cụ thể, thường thông qua việc tương tác với các công cụ và hệ thống. Có nhiều quy tắc và quy ước được tạo ra để đạt được điều này, và một trong những quy tắc nổi tiếng và được sử dụng nhiều nhất là khung ReAct (Reason & Act).

Với khung này, LLM nhận một prompt → suy nghĩ → quyết định hành động tiếp theo (có thể là gọi một công cụ cụ thể) → nhận dữ liệu từ công cụ. Khi phản hồi từ công cụ được nhận, mô hình AI quan sát phản hồi, tạo ra phản hồi và lập kế hoạch các hành động tiếp theo dựa trên phản hồi đó.

Bạn có thể đọc thêm về khái niệm này trong tài liệu chính thức.

Lưu ý rằng quy trình làm việc không bị giới hạn ở một lần gọi công cụ duy nhất; nó có thể diễn ra qua nhiều vòng trước khi trở lại với người dùng.

Để LLM agent thực sự giống con người và hành động với kiến thức về quá khứ, nó cần có bộ nhớ, cho phép nó nhớ lại các prompt và phản hồi trước đó, từ đó duy trì sự nhất quán trong chuỗi hội thoại. Không có một nguồn thông tin nào là đúng cho cách tiếp cận này. Hầu hết các agent thực hiện một bộ nhớ ngắn hạn; điều này có nghĩa là agent sẽ bổ sung mỗi cuộc trò chuyện mới vào lịch sử cuộc trò chuyện, và khi một prompt mới được gửi, agent sẽ bổ sung các tin nhắn trước đó vào prompt mới. Phương pháp này rất hiệu quả và cung cấp cho LLM một kiến thức mạnh mẽ về các trạng thái trước đó. Nhưng nó cũng có thể gây ra vấn đề, vì càng nhiều cuộc trò chuyện phát triển, LLM càng phải xem xét tất cả các tin nhắn trước đó để hiểu hành động tiếp theo cần thực hiện.

Bạn không cần phải triển khai điều này từ đầu 😅, nhiều công cụ và khung đã được phát triển để làm cho việc triển khai trở nên dễ dàng nhất có thể. Dù không có gì ngăn cản bạn xây dựng mọi thứ từ đầu, nhưng điều đó sẽ không xảy ra trong bài viết này 😁.

Trong bài viết này, chúng ta sẽ xây dựng một barista Starbucks thu thập thông tin đặt hàng và gọi công cụ create_order một khi đơn hàng đáp ứng đầy đủ tiêu chí. Đây là một công cụ mà chúng ta sẽ tạo ra và cung cấp cho AI.

Khởi tạo dự án

Bắt đầu bằng cách khởi tạo dự án Nest.js của chúng ta. Lưu ý rằng không có gì ở đây gắn liền với Nest.js; đây chỉ là sở thích về khung, và mọi thứ tôi đang làm ở đây đều có thể thực hiện bằng Node.js hoặc Express.js.

Khởi tạo dự án Nest.js và cài đặt tất cả các phụ thuộc cần thiết:

bash Copy
$ npm i -g @nestjs/cli # Nếu bạn chưa cài đặt Nest.js trên máy của mình
$ nest new project-name

"dependencies": {
  "@langchain/community": "^0.3.53",
  "@langchain/core": "^0.3.75",
  "@langchain/google-genai": "^0.2.16",
  "@langchain/langgraph": "^0.4.8",
  "@langchain/langgraph-checkpoint-mongodb": "^0.1.1",
  "@langchain/mongodb": "^0.1.0",
  "@nestjs/mongoose": "^11.0.3",
  "langchain": "^0.3.33",
  "mongodb": "^6.19.0",
  "mongoose": "^8.18.1",
  "zod": "^4.1.8"
}

Lưu ý rằng các phiên bản có thể không giống như khi bạn đọc bài viết này, tôi khuyên bạn nên kiểm tra tài liệu chính thức cho từng gói.

Giờ đây, khi chúng ta đã tạo xong dự án và cài đặt tất cả các gói, hãy xem chúng ta cần gì để biến tầm nhìn của mình thành hiện thực. Hãy nghĩ về những gì bạn cần để tạo ra một barista Starbucks.

  • Đầu tiên, chúng ta cần xác định cấu trúc dữ liệu của mình (tạo các schema).
  • Tạo danh sách menu mà agent của chúng ta sẽ tham chiếu đến.
  • Thêm tương tác LLM.
  • Cuối cùng nhưng không kém phần quan trọng, khả năng lưu trữ các cuộc hội thoại trước đó để tạo ngữ cảnh cho cuộc trò chuyện.

Cấu trúc thư mục

Bạn có thể điều chỉnh cấu trúc thư mục này và thích ứng nó dựa trên khung bạn chọn. Nhưng phần cốt lõi của việc triển khai là giống nhau cho tất cả các khung.

lib/util/schemas/drinks.ts

Tệp này chứa tất cả các định nghĩa schema của chúng ta liên quan đến đồ uống và tất cả các thay đổi mà chúng có thể nhận được.

typescript Copy
import z from 'zod';
import { StructuredOutputParser } from 'langchain/output_parsers';

export const DrinkSchema = z.object({
  name: z.string(),
  description: z.string(),
  supportMilk: z.boolean(),
  supportSweeteners: z.boolean(),
  supportSyrup: z.boolean(),
  supportTopping: z.boolean(),
  supportSize: z.boolean(),
  image: z.string().url().optional(),
});

export const SweetenerSchema = z.object({
  name: z.string(),
  description: z.string(),
  image: z.string().url().optional(),
});

export const SyrupSchema = z.object({
  name: z.string(),
  description: z.string(),
  image: z.string().url().optional(),
});

export const ToppingSchema = z.object({
  name: z.string(),
  description: z.string(),
  image: z.string().url().optional(),
});

export const SizeSchema = z.object({
  name: z.string(),
  description: z.string(),
  image: z.string().url().optional(),
});

export const MilkSchema = z.object({
  name: z.string(),
  description: z.string(),
  image: z.string().url().optional(),
});

export const DrinksSchema = z.array(DrinkSchema);

Các kiểu dữ liệu

Các dòng này sử dụng tiện ích z.infer của Zod để tự động tạo kiểu TypeScript dựa trên các schema đã định nghĩa. Điều này đảm bảo an toàn kiểu trong toàn bộ ứng dụng và giữ các kiểu đồng bộ với các schema xác thực.

typescript Copy
export type Drink = z.infer<typeof DrinkSchema>;
export type Sweetener = z.infer<typeof SweetenerSchema>;
export type Syrup = z.infer<typeof SyrupSchema>;
export type Topping = z.infer<typeof ToppingSchema>;
export type Size = z.infer<typeof SizeSchema>;
export type Milk = z.infer<typeof MilkSchema>;
export type Drinks = z.infer<typeof DrinksSchema>;

Kết nối cơ sở dữ liệu

Bây giờ chúng ta đã có những nền tảng cốt lõi của ứng dụng, hãy thiết lập kết nối cơ sở dữ liệu để chúng ta có thể lưu trữ đơn hàng.

src/app.module.ts

typescript Copy
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ChatsModule } from './chats/chats.module';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [MongooseModule.forRoot(process.env.MONGO_URI), ChatsModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Tạo logic agent

Giờ là lúc viết logic cho agent 🤩.

src/chats/chats.service.ts

typescript Copy
import { Injectable } from '@nestjs/common';
import { MongoClient } from 'mongodb';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { tool } from '@langchain/core/tools';
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { StateGraph } from '@langchain/langgraph';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { Annotation } from '@langchain/langgraph';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { START, END } from '@langchain/langgraph';
import { MongoDBSaver } from '@langchain/langgraph-checkpoint-mongodb';
import { Order } from 'src/data/schema/order.schema';
import { OrderSchema as OrderSchemaData, OrderParser } from 'src/util/schemas/orders/Order.schema';
import { DrinkParser } from 'src/util/schemas/drinks/Drink.schema';
import { DRINKS } from 'src/util/constants/drinks_data';
import { availableToppingsSummary, createDrinkItemSummary, createSweetenersSummary, createAvailableMilksSummary, createSyrupsSummary, createSizesSummary } from 'src/util/summaries/drink';
import z from 'zod';

const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY || '';
const client: MongoClient = new MongoClient(process.env.MONGO_URI || '');
const database_name = 'drinks_db';

@Injectable()
export class ChatService {
  constructor(@InjectModel(Order.name) private orderModel: Model<Order>) {}

  chatWithAgent = async (thread_id: string, query: string) => {
    await client.connect();
    const graphState = Annotation.Root({
      messages: Annotation<BaseMessage[]>({ reducer: (x, y) => [...x, ...y] }),
    });

    const orderTool = tool(
      async ({ order }: { order: OrderType }) => {
        try {
          await this.orderModel.create(order);
          return 'Order created successfully';
        } catch (error) {
          return 'Failed to create the order';
        }
      },
      {
        schema: z.object({
          order: OrderSchemaData.describe('This is the order that will be passed to '),
        }),
        name: 'create_order',
        description: 'Creates a new order in the database',
      },
    );

    const tools = [orderTool];

    const callModal = async (states: typeof graphState.State) => {
      const prompt = ChatPromptTemplate.fromMessages([
        {
          role: 'system',
          content: `
            Your are a helpful assistant that helps people buy drinks from starbucks.
            You take the user request and find missing details based on how a full order looks like.
            A full order looks like this:  ${OrderParser.getFormatInstructions()}.

            *IMPORTANT
            You have access to a create_order tool, this tool is used to create orders in the database and you should call it when
            you want to create an order.

            You should confirm the order once the tool call has been successful, and if it fails you inform the user.

            **VERY IMPORTANT
            Once the order is ready you should ask the user to confirm and if thet do you should call the create_order right away 
            and only come back to the user once the order has been confirmed or failed.
          `,
        },
        new MessagesPlaceholder('messages'),
      ]);

      const formattedPrompt = await prompt.formatMessages({
        time: new Date().toISOString(),
        messages: states.messages,
      });

      const chat = new ChatGoogleGenerativeAI({
        model: 'gemini-2.0-flash',
        temperature: 0,
        apiKey: GOOGLE_API_KEY,
      }).bindTools(tools);

      const result = await chat.invoke(formattedPrompt);
      return { messages: [result] };
    };

    const shouldContinue = (state: typeof graphState.State) => {
      const messages = state.messages;
      const lastMessage = messages[messages.length - 1] as AIMessage;
      return lastMessage.tool_calls?.length ? 'tools' : END;
    };

    const toolsNode = new ToolNode<typeof graphState.State>(tools);

    const graph = new StateGraph(graphState)
      .addNode('agent', callModal)
      .addNode('tools', toolsNode)
      .addEdge(START, 'agent')
      .addConditionalEdges('agent', shouldContinue)
      .addEdge('tools', 'agent');

    const checkpointer = new MongoDBSaver({ client, dbName: database_name });
    const app = graph.compile({ checkpointer });

    const finalState = await app.invoke(
      { messages: [new HumanMessage(query)] },
      { recursionLimit: 15, configurable: { thread_id } },
    );

    function extractJson(response: any) {
      const match = response.match(/```
\s*json\s*([\s\S]*?)\s*```/i);
      if (match && match[1]) {
        return JSON.parse(match[1].trim());
      }
      throw response;
    }

    const lastMessage = finalState.messages.at(-1) as AIMessage;
    return extractJson(lastMessage.content);
  };
}

Kết luận

Với những gì đã được đề cập, bạn đã có một cái nhìn tổng quan về cách xây dựng một AI agent sử dụng LangChain và LangGraph để phát triển một ứng dụng đặt hàng tự động. Hãy nhớ rằng, quá trình này không chỉ đơn thuần là lập trình, mà còn là sự sáng tạo và khám phá. Hãy thử nghiệm với mã nguồn và tạo ra những sản phẩm độc đáo của riêng bạn!

Tài nguyên tham khảo

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào