Trong Phần 4: Active RAG & Strict Tool Calling - Kết Nối LLM Với Real-time Inventory API, chúng ta đã xây dựng thành công một đồ thị ReAct tuần hoàn để cho phép LLM gọi các API kiểm tra tồn kho và lấy thông tin khuyến mãi theo thời gian thực. Tuy nhiên, trong môi trường sản xuất thực tế, việc LLM có quyền truy cập vào các công cụ (Tools) vẫn chưa đủ để đảm bảo độ chính xác tuyệt đối.

Một hiện tượng rất phổ biến là Hallucination (ảo giác) hoặc bỏ sót ràng buộc: LLM nhận được dữ liệu tồn kho bằng 0 từ Tool nhưng trong phần tổng hợp câu trả lời vẫn đề xuất sản phẩm đó cho khách hàng, hoặc bỏ qua bộ lọc mức giá tối đa mà người dùng đã yêu cầu rõ ràng trong câu truy vấn ban đầu.

Để khắc phục triệt độ vấn đề này, chúng ta cần triển khai mô hình Self-Reflection (Tự phản chiếu / Tự phê bình) thông qua một chu trình Critique Loop (Retrieve - Critique - Regenerate) khép kín. Bài viết này sẽ hướng dẫn bạn thiết lập hệ thống này bằng framework Eino (CloudWeGo) của ByteDance.


Trong tìm kiếm thương mại điện tử, lỗi ảo giác của LLM thường xuất hiện dưới 3 dạng:

  1. Lỗi logic tồn kho: LLM bỏ qua dữ liệu thực tế từ API và tự suy diễn trạng thái hàng hóa (ví dụ: Tool báo “hết hàng tại Quận 1” nhưng LLM trả lời “sản phẩm hiện có sẵn tại Quận 1”).
  2. Lỗi bỏ sót ràng buộc: Bỏ qua các tiêu chí lọc cứng như giá cả, màu sắc, kích thước mà khách hàng yêu cầu.
  3. Lỗi định dạng đầu ra: Trả về dữ liệu không đúng cấu trúc JSON mong muốn của tầng frontend.

Chu trình Retrieve-Critique-Regenerate sinh ra để đóng vai trò như một người kiểm duyệt (Auditor) độc lập:

graph TD
    Q[User Query] --> GEN[LLM Generator]
    GEN --> R[Response]
    R --> CRIT[Critique Node Đánh giá]
    CRIT -- Không Đạt - Loop back --> REGEN[Regenerate]
    REGEN --> GEN
    CRIT -- Đạt --> FINAL[User Response]

Mỗi câu trả lời của bộ sinh (Generator) sẽ không được gửi thẳng tới người dùng mà phải đi qua node phê bình (Critique Node). Nếu không vượt qua bài kiểm tra chất lượng, phản hồi kèm theo lý do lỗi sẽ được đẩy ngược lại để Generator tự sửa lỗi và tái tạo câu trả lời.


2. Quản Lý Trạng Thế Vòng Lặp Với Pointer-Based State

Khác với các framework Python như LangGraph sử dụng cơ chế ghi đè hoặc hợp nhất dictionary, Eino quản lý trạng thái luồng đi qua một Struct Go dạng con trỏ (Pointer-Based State). Trạng thái này được khởi tạo duy nhất một lần bằng compose.WithGenLocalState khi khai báo Graph và được cập nhật an toàn bằng compose.ProcessState[S].

Dưới đây là định nghĩa trạng thái của Agent và cách khởi tạo:

package agent

import (
	"context"

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

// Product đại diện cho thông tin sản phẩm trong e-commerce
type Product struct {
	SKU     string  `json:"sku"`
	Name    string  `json:"name"`
	Price   float64 `json:"price"`
	InStock bool    `json:"in_stock"`
}

// AgentState lưu trữ trạng thái của vòng lặp phê bình
type AgentState struct {
	OriginalQuery string     `json:"original_query"` // Yêu cầu ban đầu từ người dùng
	LatestResponse string    `json:"latest_response"` // Câu trả lời gần nhất của Generator
	Feedback       string    `json:"feedback"`        // Nhận xét chi tiết từ Critique Node
	Iterations     int       `json:"iterations"`      // Số lần lặp lại hiện tại
	ShouldStop     bool      `json:"should_stop"`     // Flag ra hiệu kết thúc
}

// InitializeGraph khởi tạo đồ thị với generator trạng thái cục bộ
func InitializeGraph() {
	g := compose.NewGraph[[]*schema.Message, *schema.Message](
		// Khởi tạo AgentState cho mỗi lượt gọi
		compose.WithGenLocalState(func(ctx context.Context) *AgentState {
			return &AgentState{
				Iterations: 0,
				ShouldStop: false,
			}
		}),
	)
	_ = g
}

3. Định Nghĩa Critique Node Bằng InvokableLambda & MessageJSONParser

Critique Node đóng vai trò phân tích câu trả lời của Generator đối chiếu với yêu cầu của người dùng để trả ra kết quả đánh giá có cấu trúc (Structured Output). Chúng ta sẽ định nghĩa một Go Struct chứa kết quả chấm điểm và lý do, sau đó bọc logic thực thi bằng compose.InvokableLambda và dùng schema.NewMessageJSONParser để trích xuất dữ liệu.

Bước A: Định nghĩa cấu trúc đánh giá phản hồi

package agent

import (
	"context"
	"fmt"

	"github.com/cloudwego/eino/callbacks"
	"github.com/cloudwego/eino/compose"
	"github.com/cloudwego/eino/schema"
)

// CritiqueResult định nghĩa cấu trúc JSON đầu ra của Critique Node
type CritiqueResult struct {
	Score      int    `json:"score" jsonschema:"required" jsonschema_description:"Điểm số đánh giá từ 0 đến 100 dựa trên độ chính xác"`
	Feedback   string `json:"feedback" jsonschema:"required" jsonschema_description:"Nhận xét chi tiết về các lỗi logic hoặc thông tin sai lệch nếu có"`
	ShouldStop bool   `json:"should_stop" jsonschema:"required" jsonschema_description:"Đặt là true nếu câu trả lời đã hoàn hảo hoặc không thể cải thiện thêm"`
}

Bước B: Xây dựng hàm thực thi Critique Node

Hàm xử lý sẽ đọc nội dung tin nhắn, giải mã thành struct CritiqueResult và sử dụng compose.ProcessState để cập nhật trực tiếp vào trạng thái toàn cục của luồng chạy:

// RunCritiqueNode thực hiện kiểm duyệt và cập nhật bộ đếm vòng lặp
func RunCritiqueNode(ctx context.Context, input *schema.Message) (*schema.Message, error) {
	// 1. Khởi tạo JSON Parser để phân tích phản hồi có cấu trúc từ LLM kiểm duyệt
	parser := schema.NewMessageJSONParser[CritiqueResult](&schema.MessageJSONParseConfig{
		ParseFrom: schema.MessageParseFromContent,
	})

	critique, err := parser.Parse(ctx, input)
	if err != nil {
		// Trong thực tế nếu LLM sinh lỗi format, ta trả về feedback yêu cầu chỉnh sửa định dạng
		_ = compose.ProcessState[*AgentState](ctx, func(ctx context.Context, state *AgentState) error {
			state.Iterations++
			state.Feedback = "Lỗi: Không thể phân tích kết quả phê duyệt. Hãy đảm bảo trả về định dạng JSON hợp lệ."
			return nil
		})
		return input, nil
	}

	// 2. Cập nhật state một cách an toàn
	err = compose.ProcessState[*AgentState](ctx, func(ctx context.Context, state *AgentState) error {
		state.Iterations++
		state.Feedback = critique.Feedback
		state.LatestResponse = input.Content

		// Đạt điểm tối thiểu (ví dụ: 85 điểm) hoặc có cờ dừng thì kết thúc
		if critique.ShouldStop || critique.Score >= 85 {
			state.ShouldStop = true
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("failed to process state in critique: %w", err)
	}

	return input, nil
}

4. Định Tuyến Điều Kiện & Thiết Lập Giới Hạn Vòng Lặp An Toàn

Sau khi Critique Node chạy xong và cập nhật trạng thái, đồ thị cần một nhánh điều hướng (compose.NewGraphBranch) để đưa ra quyết định:

  • Đi tiếp đến node Generator để tái sinh câu trả lời (kèm Prompt chứa feedback phê bình cũ).
  • Kết thúc tại compose.END để trả kết quả cho người dùng.

Đồng thời, để tránh rủi ro LLM rơi vào vòng lặp vô hạn (gây cạn kiệt token và treo hệ thống), Eino cung cấp tùy chọn biên dịch đồ thị compose.WithMaxRunSteps(steps) để ngắt kết nối an toàn khi số bước thực thi vượt ngưỡng quy định.

package agent

import (
	"context"
	"fmt"

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

// OrchestrateReflectedGraph lắp ghép đồ thị tự kiểm duyệt hoàn chỉnh
func OrchestrateReflectedGraph(ctx context.Context, generatorModel model.ChatModel, auditorModel model.ChatModel) (compose.Runnable[[]*schema.Message, *schema.Message], error) {
	
	// Khởi tạo Graph với AgentState cục bộ
	g := compose.NewGraph[[]*schema.Message, *schema.Message](
		compose.WithGenLocalState(func(ctx context.Context) *AgentState {
			return &AgentState{
				Iterations: 0,
				ShouldStop: false,
			}
		}),
	)

	// Bọc hàm xử lý phê duyệt thành một Lambda Node của Eino
	critiqueNode := compose.NewInvokableLambda(RunCritiqueNode)

	// Đăng ký các node vào đồ thị
	g.AddChatModelNode("llm_generator", generatorModel)
	g.AddChatModelNode("llm_auditor", auditorModel) // Node LLM đóng vai trò Critique
	g.AddLambdaNode("critique_processor", critiqueNode) // Node xử lý cập nhật state

	// Thiết lập luồng chạy tĩnh ban đầu
	g.AddEdge(compose.START, "llm_generator")
	g.AddEdge("llm_generator", "llm_auditor")
	g.AddEdge("llm_auditor", "critique_processor")

	// Xây dựng nhánh rẽ hướng điều kiện dựa trên trạng thái state cập nhật
	routingBranch := compose.NewGraphBranch(func(ctx context.Context, msg *schema.Message) (string, error) {
		var nextNode string
		err := compose.ProcessState[*AgentState](ctx, func(ctx context.Context, state *AgentState) error {
			// Dừng nếu đạt chất lượng hoặc số vòng lặp vượt ngưỡng an toàn (ví dụ: tối đa 3 vòng)
			if state.ShouldStop || state.Iterations >= 3 {
				nextNode = compose.END
			} else {
				nextNode = "llm_generator"
			}
			return nil
		})
		return nextNode, err
	}, map[string]bool{
		"llm_generator": true,
		compose.END:     true,
	})

	// Kết nối Critique Processor tới nhánh định tuyến điều kiện
	g.AddBranch("critique_processor", routingBranch)

	// Biên dịch đồ thị kèm giới hạn an toàn tối đa 15 bước xử lý trong toàn đồ thị (WithMaxRunSteps)
	runnable, err := g.Compile(ctx, compose.WithMaxRunSteps(15))
	if err != nil {
		return nil, fmt.Errorf("failed to compile reflected graph: %w", err)
	}

	return runnable, nil
}

5. Luồng Dữ Liệu Thực Tế Khi Xử Lý Lỗi Logic

Hãy xem kịch bản vận hành thực tế khi người dùng gửi câu hỏi tìm kiếm: “Tìm cho tôi laptop Asus ROG dưới 30 triệu đồng còn hàng ở chi nhánh Quận 1.”

  1. Lần lặp 1 (Generation):

    • llm_generator trả về: “Dưới đây là Asus ROG Strix G15, giá 28.5 triệu, còn hàng tại chi nhánh Quận 3.”
    • llm_auditor phân tích và phát hiện lỗi logic nghiêm trọng: Người dùng yêu cầu cửa hàng tại Quận 1, nhưng Generator lại đưa ra sản phẩm tại Quận 3.
    • Kết quả phân tích của Auditor: { "score": 40, "feedback": "Sản phẩm đề xuất nằm ở Quận 3, không đúng yêu cầu Quận 1 của khách hàng.", "should_stop": false }.
    • critique_processor cập nhật trạng thái: Iterations = 1, lưu Feedback, cờ ShouldStop = false.
    • Nhánh định tuyến nhận thấy chưa đạt yêu cầu, chuyển luồng ngược về đầu: llm_generator.
  2. Lần lặp 2 (Regeneration):

    • llm_generator nhận được feedback cảnh báo lỗi và tự động sửa sai trong prompt lặp lại.
    • Kết quả phản hồi mới: “Xin lỗi vì sự nhầm lẫn. Cửa hàng Quận 1 hiện còn laptop Asus ROG Flow X13 giá 29.9 triệu, sẵn hàng phục vụ.”
    • llm_auditor kiểm duyệt lại: Sản phẩm khớp toàn bộ tiêu chí (Asus ROG, < 30 triệu, đúng chi nhánh Quận 1).
    • Kết quả phân tích mới: { "score": 95, "feedback": "Kết quả hoàn toàn chính xác.", "should_stop": true }.
    • critique_processor cập nhật trạng thái: Iterations = 2, cờ ShouldStop = true.
    • Nhánh định tuyến chuyển hướng luồng sang compose.END.
    • Câu trả lời chất lượng cao được chuyển tới khách hàng.

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

  1. State Mutation trong Go cực kỳ rõ ràng: Cơ chế compose.ProcessState giúp việc theo dõi trạng thái, tăng số vòng lặp và cập nhật feedback của Eino trở nên trực quan, tường minh về kiểu dữ liệu (strongly-typed).
  2. Luôn sử dụng Message Parser: schema.NewMessageJSONParser giúp đơn giản hóa việc giải mã các chỉ thị kiểm duyệt phức tạp từ JSON String sang Go Struct.
  3. WithMaxRunSteps là chốt chặn cuối: Đừng bao giờ bỏ qua giới hạn an toàn khi biên dịch đồ thị tuần hoàn. Đây là cách bảo vệ tài khoản API LLM của bạn tránh khỏi những hóa đơn nghìn đô ngoài ý muốn do lỗi logic lặp vô hạn.

Với cơ chế tự kiểm duyệt kết quả đã vững vàng, làm thế nào để chúng ta đưa hệ thống RAG Agent này lên môi trường sản xuất quy mô lớn? Làm thế nào để giải quyết vấn đề phản hồi chậm của LLM bằng cơ chế Streaming Token, tối ưu chi phí qua Semantic Caching, và theo dõi toàn bộ đường chạy bằng OpenTelemetry?

Hãy cùng đón đọc Phần 6: Production Operations - Semantic Caching, Model Routing, SSE & Telemetry để hoàn thiện mảnh ghép vận hành thực tế cuối cùng cho hệ thống Agentic Search của bạn!