Trong Phần 3: Làm Chủ Qdrant Hybrid Search - Giải Bài Toán Semantic và Hard Filters, chúng ta đã xây dựng thành công một engine tìm kiếm Hybrid mạnh mẽ, kết hợp giữa Dense Semantic và Sparse Lexical Search. Tuy nhiên, một hệ thống tìm kiếm e-commerce thực chiến không chỉ đơn thuần là việc lấy ra các văn bản tĩnh từ cơ sở dữ liệu vector.

Ví dụ, người dùng hỏi: “Tôi muốn mua tủ lạnh Samsung Inverter 400L có sẵn tại chi nhánh Quận 1 và đang được áp dụng khuyến mãi.” Nếu chỉ dựa vào Vector Database, chúng ta sẽ gặp hai lỗi nghiêm trạng:

  1. Dữ liệu tĩnh bị out-of-date: Trạng thái tồn kho tại Quận 1 thay đổi liên tục từng giây qua giao dịch POS, không thể cập nhật liên tục vào Vector Index mà không gây tốn kém tài nguyên.
  2. Thông tin khuyến mãi động: Các chương trình Flash Sale hoặc voucher giảm giá thường được tính toán theo thời gian thực dựa trên tài khoản người dùng, chiến dịch marketing và giỏ hàng hiện tại.

Để giải quyết triệt để bài toán này, hệ thống cần dịch chuyển từ mô hình Passive RAG sang Active RAG (Agentic RAG) sử dụng Strict Tool Calling trong Golang. Bài viết này sẽ hướng dẫn bạn thiết lập cơ chế này thông qua framework Eino (CloudWeGo).


1. Sự Khác Biệt Giữa Passive RAG và Active RAG

Trước khi đi sâu vào code, hãy cùng làm rõ ranh giới kiến trúc giữa hai mô hình:

graph TD
    subgraph Passive["Passive RAG (Tĩnh - Một chiều)"]
        direction LR
        Q1[Query] --> VS[Vector Search]
        VS --> CQ[Context + Query]
        CQ --> LLM1[LLM]
        LLM1 --> R1[Response]
    end

    subgraph Active["Active RAG (Động - Lặp tuần hoàn)"]
        direction LR
        Q2[Query] --> LLM2[LLM Reasoning]
        LLM2 -- Quyết định gọi Tool --> API[Execute Go API]
        API -- Nhận kết quả thực tế --> LLM2
        LLM2 --> R2[Response]
    end
  • Passive RAG (Linear RAG): Người dùng nhập câu hỏi -> Hệ thống thực hiện Vector Search để lấy Context -> Nối Context vào Prompt -> LLM trả ra câu trả lời. Mô hình này hoàn toàn thụ động và tuyến tính. Nếu dữ liệu lấy ra từ Vector DB bị sai hoặc cũ, LLM sẽ tự tin trả về thông tin sai lệch (Hallucination).
  • Active RAG (Agentic RAG): LLM đóng vai trò bộ não điều khiển (Reasoning Engine). Khi nhận câu hỏi, LLM sẽ phân tích ý định và quyết định xem mình cần gọi những công cụ (Tools) nào. Nó có thể gọi Tool tìm kiếm sản phẩm, phát hiện thấy sản phẩm còn hàng nhưng thiếu giá khuyến mãi, nó tiếp tục gọi Tool tính toán voucher, rồi mới tổng hợp câu trả lời cuối cùng cho người dùng.

2. Định Nghĩa Tool Trong Golang Với Eino

Framework Eino sử dụng cơ chế Reflection của Go để tự động phản chiếu các Struct thành JSON Schema gửi cho LLM. Để định nghĩa một Tool, chúng ta cần:

  1. Input Struct: Định nghĩa các tham số đầu vào của API kèm theo các Struct Tag để LLM hiểu kiểu dữ liệu và mô tả của từng trường.
  2. Callback Function: Hàm xử lý logic thực tế (gọi cơ sở dữ liệu, microservice hoặc API bên thứ ba).

Dưới đây là cách triển khai hai Tool thực tế: CheckLiveInventory (kiểm tra tồn kho thực tế) và GetActivePromotions (lấy khuyến mãi hiện hành).

package agent

import (
	"context"
	"fmt"
)

// CheckInventoryInput định nghĩa tham số cho tool kiểm tra tồn kho
type CheckInventoryInput struct {
	SKU      string `json:"sku" jsonschema:"required" jsonschema_description:"Mã SKU định danh duy nhất của sản phẩm"`
	Location string `json:"location" jsonschema:"required" jsonschema_description:"Tên quận hoặc chi nhánh cửa hàng (ví dụ: 'District 1', 'District 3')"`
}

// CheckLiveInventory thực thi việc gọi sang Inventory Microservice qua gRPC/REST
func CheckLiveInventory(ctx context.Context, input *CheckInventoryInput) (string, error) {
	// Giả lập cuộc gọi API thực tế tới DB/Microservice
	// Trong thực tế, bạn sẽ khởi tạo gRPC client ở đây
	if input.SKU == "" || input.Location == "" {
		return "", fmt.Errorf("missing required parameters SKU or Location")
	}

	// Giả lập kết quả trả về từ database tồn kho thời gian thực
	stock := 12
	if input.Location == "District 3" {
		stock = 0
	}

	return fmt.Sprintf("Sản phẩm SKU '%s' tại chi nhánh '%s' hiện còn: %d sản phẩm trong kho.", 
		input.SKU, input.Location, stock), nil
}

// GetPromotionsInput định nghĩa tham số cho tool kiểm tra khuyến mãi
type GetPromotionsInput struct {
	SKU string `json:"sku" jsonschema:"required" jsonschema_description:"Mã SKU sản phẩm cần áp dụng khuyến mãi"`
}

// GetActivePromotions thực thi việc tính toán chương trình khuyến mãi thực tế
func GetActivePromotions(ctx context.Context, input *GetPromotionsInput) (string, error) {
	// Giả lập gọi sang Promotion service
	return fmt.Sprintf("Mã SKU '%s' đang được áp dụng chương trình: 'Mùa Hè Rực Rỡ' - Giảm ngay 10% tối đa 500,000đ khi thanh toán qua thẻ ngân hàng.", input.SKU), nil
}

3. Ràng Buộc Schema Chặt Chẽ Với Strict Tool Calling

Các LLM thương mại như OpenAI GPT hay Gemini hỗ trợ tính năng Strict Function Calling (OpenAI strict: true). Tính năng này bắt buộc JSON Schema định nghĩa tham số phải có thuộc tính "additionalProperties": false, nghĩa là LLM không được phép sinh thêm bất kỳ tham số ảo nào ngoài các trường đã được định nghĩa.

Trong Eino (phiên bản v0.5.4+), chúng ta sử dụng hàm utils.InferTool cùng với cấu hình WithSchemaModifier để tự động gán thuộc tính "additionalProperties": false vào JSON Schema được tạo ra từ Go Struct:

package agent

import (
	"reflect"

	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/components/tool/utils"
	"github.com/invopop/jsonschema"
)

// BuildStrictTools khởi tạo danh sách tools với cấu hình Schema Modifiers nghiêm ngặt
func BuildStrictTools() ([]tool.BaseTool, error) {
	// Cấu hình Schema Modifier để ép buộc "additionalProperties: false"
	strictModifier := utils.WithSchemaModifier(func(name string, t reflect.Type, tag reflect.StructTag, s *jsonschema.Schema) {
		if s.Type == "object" {
			s.AdditionalProperties = jsonschema.AdditionalPropertiesFalse
		}
	})

	// 1. Khởi tạo Inventory Tool
	inventoryTool, err := utils.InferTool(
		"check_inventory",
		"Kiểm tra số lượng sản phẩm tồn kho thực tế tại một chi nhánh cụ thể theo thời gian thực",
		CheckLiveInventory,
		strictModifier,
	)
	if err != nil {
		return nil, err
	}

	// 2. Khởi tạo Promotion Tool
	promotionTool, err := utils.InferTool(
		"get_promotions",
		"Lấy thông tin các chiến dịch khuyến mãi và giảm giá đang áp dụng cho sản phẩm",
		GetActivePromotions,
		strictModifier,
	)
	if err != nil {
		return nil, err
	}

	return []tool.BaseTool{inventoryTool, promotionTool}, nil
}

4. Thiết Lập ReAct Loop Bằng Eino Graph

Để LLM có thể thực hiện suy nghĩ (Reasoning) và hành động (Action) nhiều lần một cách linh hoạt, chúng ta sẽ xây dựng một đồ thị điều hướng Eino Graph tuần hoàn thay vì một Chain tuyến tính thông thường.

Dưới đây là kiến trúc đồ thị điều phối Agentic Search:

package agent

import (
	"context"
	"fmt"

	"github.com/cloudwego/eino/compose"
	"github.com/cloudwego/eino/components/model"
	"github.com/cloudwego/eino/schema"
)

// OrchestrateAgentGraph xây dựng đồ thị ReAct Loop hoàn chỉnh
func OrchestrateAgentGraph(ctx context.Context, chatModel model.ChatModel, tools []tool.BaseTool) (compose.Runnable[[]*schema.Message, *schema.Message], error) {
	// 1. Tạo node xử lý Tools
	toolsNode, err := compose.NewToolsNode(ctx, &compose.ToolsNodeConfig{
		Tools: tools,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create tools node: %w", err)
	}

	// 2. Khởi tạo Graph với đầu vào là mảng các Messages và đầu ra là câu trả lời cuối cùng
	g := compose.NewGraph[[]*schema.Message, *schema.Message]()

	// Thêm các node chức năng vào đồ thị
	g.AddChatModelNode("llm_reasoning", chatModel)
	g.AddToolsNode("tools_execution", toolsNode)

	// Cấu hình luồng tĩnh ban đầu: Graph bắt đầu sẽ chuyển dữ liệu thẳng đến LLM
	g.AddEdge(compose.START, "llm_reasoning")

	// 3. Xây dựng nhánh rẽ hướng động (Conditional Branch)
	// LLM sau khi suy nghĩ sẽ trả ra một Message:
	// - Nếu Message chứa yêu cầu gọi Tool (ToolCalls) -> chuyển tới Node "tools_execution"
	// - Nếu không chứa ToolCalls -> kết thúc đồ thị (END) và trả về câu trả lời cho User
	reactBranch := compose.NewGraphBranch(func(ctx context.Context, msg *schema.Message) (string, error) {
		if len(msg.ToolCalls) > 0 {
			return "tools_execution", nil
		}
		return compose.END, nil
	}, map[string]bool{
		"tools_execution": true,
		compose.END:        true,
	})

	// Gán nhánh rẽ hướng này ngay sau Node LLM
	g.AddBranch("llm_reasoning", reactBranch)

	// Sau khi Tool chạy xong, kết quả phản hồi của Tool (ToolMessages) quay ngược lại LLM
	// tạo thành chu kỳ ReAct Loop tuần hoàn
	g.AddEdge("tools_execution", "llm_reasoning")

	// 4. Biên dịch đồ thị kèm giới hạn an toàn để chống lặp vô hạn (Max Steps)
	runnable, err := g.Compile(ctx, compose.WithMaxRunSteps(20))
	if err != nil {
		return nil, fmt.Errorf("failed to compile graph: %w", err)
	}

	return runnable, nil
}

5. Kịch Bản Vận Hành Thực Tế

Hãy xem chu trình xử lý của Agentic Graph khi người dùng nhập câu hỏi: “Cho tôi thông tin khuyến mãi và kiểm tra tồn kho tại chi nhánh Quận 1 của sản phẩm SKU ‘SAMSUNG-RF400’.”

  1. Lượt 1 (LLM Reasoning):
    • LLM phân tích câu hỏi và nhận diện thấy hai ý định cần thực hiện: lấy khuyến mãi và kiểm tra tồn kho.
    • Nhờ cơ chế Strict Tool Calling, LLM trả về danh sách ToolCalls chuẩn xác với định dạng JSON sạch sẽ:
      • check_inventory(sku: "SAMSUNG-RF400", location: "District 1")
      • get_promotions(sku: "SAMSUNG-RF400")
  2. Lượt 2 (Tools Execution & Loopback):
    • Đồ thị rẽ nhánh tới tools_execution. Eino tự động thực thi song song hai hàm Go CheckLiveInventoryGetActivePromotions.
    • Hai kết quả được trả về dưới dạng ToolMessage và tự động ghép vào lịch sử cuộc gọi.
    • Đồ thị chạy đường biên AddEdge("tools_execution", "llm_reasoning") để trả dữ liệu về cho LLM.
  3. Lượt 3 (Tổng hợp câu trả lời):
    • LLM đọc thông tin phản hồi từ hai Tool: “Còn 12 tủ lạnh”“Giảm 10% tối đa 500,000đ”.
    • LLM nhận thấy không cần gọi thêm Tool nào khác (độ dài ToolCalls bằng 0).
    • Nhánh rẽ hướng chuyển tiếp sang compose.END.
    • Hệ thống trả lại câu trả lời tự nhiên và chính xác 100% theo thời gian thực cho người dùng.

Tóm Tắt & Bài Học Rút Ra từ Phần 4

  1. Dữ liệu động cần Active RAG: Tránh đưa các dữ liệu thay đổi liên tục (tồn kho, giá cả, khuyến mãi) vào Vector Database. Hãy sử dụng Tool Calling để truy vấn thời gian thực qua các API nội bộ.
  2. Bắt buộc dùng Strict Mode: Thêm cấu hình "additionalProperties": false thông qua utils.WithSchemaModifier giúp hệ thống backend của bạn không bao giờ bị crash vì các tham số dị biệt từ LLM.
  3. Eino Graph giúp luồng đi linh hoạt: Sử dụng compose.Graph để thiết lập các ReAct loops an toàn kiểu dữ liệu, cho phép Agent tự động sửa sai hoặc gọi nhiều tool lồng nhau.
  4. Kiểm soát số vòng lặp: Sử dụng compose.WithMaxRunSteps khi Compile để đảm bảo hệ thống tự động ngắt kết nối nếu LLM bị rơi vào trạng thái lặp vô hạn, tiết kiệm chi phí sử dụng API.

Tuy nhiên, nếu LLM vẫn trả lời sai vì lỗi logic nội bộ, hoặc dữ liệu từ Tool trả về có định dạng kỳ lạ làm LLM hiểu lầm thì sao? Làm thế nào để Agent tự phê bình (Self-Reflection) và kiểm tra lại câu trả lời trước khi gửi cho khách hàng?

Trong Phần 5: The Self-Reflection Critique Loop - Kỹ Thuật Ngăn Chặn Hallucination, chúng ta sẽ thiết lập một chu trình “Tự kiểm định và Phê bình” (Self-Reflection Loop) độc lập trong Eino nhằm đảm bảo chất lượng phản hồi đạt mức tuyệt đối trước khi hiển thị cho người dùng cuối.