EzyAI là bộ plugin cho EzyPlatform, cung cấp một lớp trung gian thống nhất
để gọi tới các nhà cung cấp AI (ChatGPT, Claude, DeepSeek, Gemini, Grok,
Qwen). Ngoài việc gọi chat, EzyAI còn bổ sung:
  • Profile: mỗi dịch vụ có nhiều profile, mỗi profile có api key,
    endpoint, model, giới hạn token, timeout riêng.
  • Skill: đoạn system prompt kèm danh sách tool được phép dùng, gắn vào
    profile.
  • Tool (MCP): AI có thể gọi tool do bất kỳ plugin nào cung cấp, chỉ
    cần đăng ký event handler theo quy ước tên.
  • Knowledge (RAG): tool tìm kiếm dữ liệu tri thức (bài viết, sản phẩm,
    dữ liệu tuỳ biến) để AI trả lời dựa trên nội dung của hệ thống.
  • Topic guard: giới hạn chủ đề AI được phép trả lời.
  • Skill Library: thư viện skill được quản trị ở admin và công khai ra
    trang web.

Kiến trúc module

flowchart TB
    subgraph EzyPlatform
        subgraph SDK["ezyai-sdk"]
            CS["AbstractAIChatService<br/>ChatGpt / Claude / DeepSeek /<br/>Gemini / Grok / Qwen"]
            PS["EzyAIChatProfileService"]
            SS["AISkillService"]
            ST["EzyAISettingService"]
            TP["McpAIChatToolProvider"]
            KN["KnowledgeDataSourceManager<br/>DefaultKnowledgeDataSource"]
        end
        ADMIN["ezyai-admin-plugin<br/>Trang quản trị + Admin API"]
        WEB["ezyai-web-plugin<br/>Trang Skill Library + Web API"]
        SOCKET["ezyai-socket-plugin<br/>Đăng ký bot cho ezychat,<br/>gom MCP tool toàn zone"]
        EZYCHAT["ezychat<br/>ChatBotServiceManager"]
        OTHER["Các plugin khác<br/>(*_mcp_tool_event)"]
    end
    AI[("AI providers<br/>OpenAI, Anthropic, ...")]
    DB[("MySQL<br/>chat_profile, settings,<br/>data_meta, post, ...")]

    ADMIN --> SDK
    WEB --> SDK
    SOCKET --> SDK
    SOCKET -- "addChatBotServices" --> EZYCHAT
    TP -. "gọi tool" .-> OTHER
    CS -- "HTTP / SSE" --> AI
    SDK --> DB
ModuleVai trò
ezyai-sdkToàn bộ logic dùng chung: các chat service theo từng nhà cung cấp, quản lý profile, skill, setting, tool provider, knowledge. Không chứa bean, mỗi plugin tự khai báo lớp con (Admin*, Web*, Socket*) để đưa vào bean context của mình.
ezyai-admin-pluginGiao diện quản trị: dịch vụ AI, profile, skill, Skill Library, cài đặt; chat thử qua SSE; đăng ký các post type riêng của EzyAI.
ezyai-web-pluginTrang công khai /ezyai/skill-library và API đọc Skill Library; có sẵn chat service, tool, knowledge để các plugin web khác dùng.
ezyai-socket-pluginKhi server sẵn sàng, đăng ký các chat service của EzyAI vào ChatBotServiceManager của app ezychat (để bot trong ezychat dùng được AI), và gom các MCP tool của mọi plugin/app trong zone; cung cấp command mcpToolList cho admin.
Các chat service của EzyAI cài đặt interface ChatBotService của
ezychat-sdk, vì vậy cách gọi AI ở mọi nơi đều đi qua
ChatBotServiceManager.chatStreamAsync(...).

Các nhà cung cấp AI

classDiagram
    class ChatBotService {
        <<interface ezychat>>
        createInputStream(request)
        createOutputStream(handler)
        getServiceName()
    }
    class AIChatBotService {
        <<interface>>
        getModelNames()
    }
    class AbstractAIChatService
    class ClaudeChatService
    class ChatGptChatService
    ChatBotService <|.. AbstractAIChatService
    AIChatBotService <|.. AbstractAIChatService
    AbstractAIChatService <|-- ClaudeChatService
    AbstractAIChatService <|-- ChatGptChatService
    ChatGptChatService <|-- DeepSeekChatService
    ChatGptChatService <|-- GeminiChatService
    ChatGptChatService <|-- GrokChatService
    ChatGptChatService <|-- QwenChatService
Dịch vụEndpoint mặc địnhModel mặc địnhAPI
CHATGPThttps://api.openai.comgpt-4o/v1/chat/completions
CLAUDEhttps://api.anthropic.comclaude-opus-5/v1/messages
DEEPSEEKhttps://api.deepseek.comdeepseek-chattương thích OpenAI
GEMINIhttps://generativelanguage.googleapis.com/v1beta/openaigemini-2.5-flashtương thích OpenAI
GROKhttps://api.x.aigrok-4.6tương thích OpenAI
QWENhttps://dashscope-intl.aliyuncs.com/compatible-modeqwen-plustương thích OpenAI
AbstractAIChatService lo phần chung (skill, topic guard, vòng lặp tool,
retry, stream); lớp con chỉ cần mô tả khác biệt của nhà cung cấp: URL,
header, cách dựng message/request body, cách đọc tool call, cách đọc từng
event stream. Thêm nhà cung cấp tương thích OpenAI thường chỉ cần kế thừa
ChatGptChatService và đổi tên dịch vụ, endpoint mặc định.
Lỗi mạng tạm thời (UnknownHostException, ConnectException,
NoRouteToHostException, SocketTimeoutException) được thử lại tối đa 3
lần, cách nhau 500ms. Các lỗi khác (sai key, request sai, lỗi nhà cung
cấp) được ném ra ngay.

Luồng một cuộc chat

sequenceDiagram
    autonumber
    participant C as Caller<br/>(admin API / ezychat bot)
    participant M as ChatBotServiceManager
    participant S as AbstractAIChatService
    participant DB as Profile / Skill / Setting
    participant T as McpAIChatToolProvider
    participant AI as AI provider

    C->>M: chatStreamAsync(serviceName, request, handler)
    M->>S: createInputStream(request)
    S->>DB: api key, skill ids, skills, allowed tools
    S->>S: applySkills + applyTopicGuard (system message)
    alt Có tool được phép
        loop tối đa max_allowed_tool_interactions vòng
            S->>AI: request (không stream) + tools
            AI-->>S: answer / tool calls
            alt Không có tool call
                S-->>M: answer đã sẵn sàng (không cần stream)
            else Có tool call
                S->>T: callTool(context, toolCall)
                T-->>S: tool result (+ metadata)
                S->>S: thêm assistant message + tool result
            end
        end
    end
    S->>AI: request stream=true (tool_choice = none nếu có tools)
    AI-->>S: SSE events
    M->>S: createOutputStream(handler)
    loop mỗi event
        S-->>C: onDelta(deltaContent)
    end
    S-->>C: onCompleted(answer: nội dung, token, metadata)
Một số điểm cần lưu ý:
  • System message được ghép theo thứ tự: system prompt của các skill →
    system message của request → chỉ dẫn topic guard.
  • Vòng tool chạy ở chế độ không stream. Nếu AI trả lời bằng văn bản
    ngay ở một vòng, câu trả lời được dùng luôn, không mở stream nữa. Nếu
    hết số vòng cho phép, vòng cuối được gửi kèm tool_choice = none để
    buộc AI trả lời bằng văn bản.
  • Token của các vòng tool được cộng dồn vào câu trả lời cuối.
  • Metadata mà tool trả về (ví dụ knowledgeList: danh sách nguồn tri
    thức đã dùng) được gộp lại và trả kèm câu trả lời, phía giao diện có thể
    dùng để hiển thị nguồn tham khảo.
  • Context của tool: mỗi lần gọi tool, tham số do AI sinh ra được trộn
    thêm context của request và metadata của profile (ví dụ
    profileWebUrl, rag_collection_name, các meta key bổ sung).
  • Lời gọi chạy trên thread pool riêng của ChatBotServiceManager, kết quả
    trả về qua ChatBotStreamHandler (onDelta, onCompleted, onError).

Ví dụ gọi từ code

chatBotServiceManager.chatStreamAsync(
    "CLAUDE",
    ChatBotRequestModel.builder()
        .profile(profile)
        .systemMessage("You are a helpful assistant.")
        .messages(
            Collections.singletonList(
                ChatBotMessageModel.ofUser("Xin chào")
            )
        )
        .build(),
    new ChatBotStreamHandler() {
        @Override
        public void onDelta(String deltaContent) {}

        @Override
        public void onCompleted(ChatBotAnswerModel answer) {}

        @Override
        public void onError(Exception exception) {}
    }
);

Chat thử ở trang quản trị

POST /api/v1/ai-chat-services/{serviceName}/chat/stream trả về
text/event-stream với 3 loại event:
EventDữ liệu
deltaMột phần câu trả lời
doneToàn bộ câu trả lời kèm số token, metadata
errorThông tin lỗi của nhà cung cấp
Request được giữ mở bằng AsyncContext vì câu trả lời được ghi từ thread
khác sau khi method controller đã return.

Profile và cấu hình

erDiagram
    CHAT_PROFILE ||--o| SETTINGS : "api key (PASSWORD)"
    CHAT_PROFILE ||--o{ DATA_META : "metadata, skill_id"
    DATA_META }o--|| POST_AI_SKILL : "skill_id"
    POST_AI_SKILL ||--o{ POST_META : "ai_skill_allowed_tool"

    CHAT_PROFILE {
        long id
        string chatBotServiceName
        string name
        string endpoint
        string model
        string status
    }
    SETTINGS {
        string name "ezyai_[service]_profile_[id]_api_key"
        string value "đã mã hoá"
    }
    DATA_META {
        string dataType "chat_profile"
        long dataId "profile id"
        string metaKey
        string metaValue
    }
    POST_AI_SKILL {
        long id
        string type "AI_SKILL"
        string content "system prompt"
    }
    POST_META {
        long postId
        string metaKey
        string metaValue "tên tool"
    }
  • Profile dùng entity ChatProfile của ezychat. Profile mặc định của mỗi
    dịch vụ lưu ở setting của ezychat; nếu chưa đặt, hệ thống lấy profile
    đang ACTIVATED đầu tiên của dịch vụ đó.
  • Api key luôn lưu ở setting kiểu PASSWORD để được platform mã hoá, tên
    setting là ezyai_[service]_profile_[profileId]_api_key ([service]
    viết thường). Khi sửa profile mà api key là chuỗi đã che (****), key cũ
    được giữ nguyên.
  • Metadata của profile lưu ở data_meta. Mặc định có rag_collection_name,
    có thể khai báo thêm meta key qua setting
    ezyai_additional_ai_profile_meta_keys. Các meta này được đưa vào context
    khi gọi tool.
  • Cấu hình không được cache, thay đổi ở trang quản trị có hiệu lực ngay ở
    lần chat tiếp theo.

Các setting chung

SettingMặc địnhÝ nghĩa
ezyai_max_allowed_tool_interactions2Số vòng gọi tool tối đa trong một lượt chat
ezyai_topic_guard_allowed_topicstrốngCác chủ đề được phép; trống là tắt topic guard
ezyai_topic_guard_refusal_messagecâu từ chối mặc địnhCâu AI dùng khi từ chối
ezyai_knowledge_data_typespost, productLoại dữ liệu được tìm trong data index
ezyai_knowledge_post_typesWIKI, LIBRARY, FAQ, POST, BLOG, NEWSLoại bài viết được coi là tri thức
ezyai_knowledge_limit_data_search10Số kết quả mặc định của tool tìm kiếm
ezyai_knowledge_limit_indexed_data_search15Số kết quả tối thiểu lấy từ data index
ezyai_knowledge_chunk_max_length600Độ dài tối đa một chunk nội dung
ezyai_knowledge_chunk_excerpt_length160Độ dài đoạn trích
ezyai_knowledge_chunk_paragraph_separatornnDấu tách đoạn
ezyai_knowledge_chunk_sentence_boundary_pattern(?<=[.!?])s+Regex tách câu
ezyai_additional_ai_profile_meta_keystrốngMeta key bổ sung cho profile

Skill

Skill là một bài viết có type = AI_SKILL:
  • Nội dung là system prompt, được ghép vào đầu system message.
  • Allowed tools lưu ở post_meta với key ai_skill_allowed_tool, mỗi
    dòng một tên tool.
Một profile gắn nhiều skill (lưu ở data_meta, key skill_id). Khi chat,
AI chỉ thấy các tool nằm trong hợp các allowed tools của những skill gắn
với profile. Profile không có skill nào có tool thì cuộc chat bỏ qua vòng
tool và stream thẳng.
flowchart LR
    P["Profile"] -->|skill_id| S1["Skill A<br/>prompt + tools"]
    P -->|skill_id| S2["Skill B<br/>prompt + tools"]
    S1 --> SP["System message"]
    S2 --> SP
    S1 --> AT["Allowed tools"]
    S2 --> AT
    ALL["Mọi MCP tool<br/>trong hệ thống"] --> F{"lọc"}
    AT --> F
    F --> TOOLS["Tool gửi cho AI"]

Tool theo chuẩn MCP

McpAIChatToolProvider không có danh sách tool cố định. Một tool là một
cặp event handler đặt tên theo quy ước:
EventVai trò
[tên tool]_mcp_tool_schema_eventTrả về name, description, inputSchema (JSON Schema)
[tên tool]_mcp_tool_eventNhận tham số, trả về kết quả dạng MCP
Kết quả của tool có dạng:
{
  "content": [{ "type": "text", "text": "..." }],
  "structuredContent": { },
  "metadata": { },
  "isError": false
}
Phần text và structuredContent được ghép lại thành nội dung trả cho AI;
metadata không gửi cho AI mà được gộp vào câu trả lời cuối.
Vì vậy, plugin nào muốn cho AI dùng thêm tool chỉ cần khai báo hai event
handler trên, EzyAI không cần biết trước. Danh sách tool được đọc lại ở mỗi
lượt chat nên tool mới có hiệu lực ngay. Ở socket plugin,
SocketEzyAIMcpEventHandlerProvider gom các handler *_mcp_tool_* của mọi
plugin và app trong zone.
Tool có sẵn của EzyAI:
ToolChức năng
search_knowledge_data_listTìm dữ liệu tri thức theo từ khoá, trả về danh sách đoạn trích kèm link nguồn
search_knowledge_data_by_type_and_idLấy chi tiết một mục tri thức theo type và id/code

Knowledge (RAG)

flowchart TB
    Q["search_knowledge_data_list<br/>(query, limit, dataSource)"] --> MGR["KnowledgeDataSourceManager"]
    MGR -->|theo tên| DS["DefaultKnowledgeDataSource"]
    MGR -.->|nguồn khác| PRV["KnowledgeDataSourceProvider"]
    DS --> STR["KnowledgeSearchStrategy<br/>(sắp theo priority)"]
    STR --> IDX["KnowledgeIndexedDataSearchStrategy<br/>DataIndexService<br/>Reciprocal Rank Fusion"]
    IDX -->|"(dataType, dataId)"| FET["EzyAIKnowledgeDataFetcher"]
    FET --> F1["Post fetcher"]
    FET --> F2["Product fetcher"]
    FET --> F3["Event fetcher<br/>ezyai_knowledge_data_list_fetcher"]
    F1 & F2 & F3 --> CH["KnowledgeContentChunker"]
    CH --> RANK["Ưu tiên chunk chứa từ khoá<br/>cắt theo limit"]
    RANK --> R["Kết quả cho AI<br/>+ metadata knowledgeList"]
  • Data source: mặc định là DEFAULT. Có thể thêm nguồn khác bằng cách
    khai báo bean KnowledgeDataSource hoặc KnowledgeDataSourceProvider.
  • Search strategy: chiến lược mặc định tìm trong data index của platform
    (kết hợp nhiều kết quả bằng Reciprocal Rank Fusion) theo các loại dữ liệu
    trong ezyai_knowledge_data_types. Có thể thêm chiến lược khác bằng bean
    KnowledgeSearchStrategy.
  • Fetcher: đổi (dataType, dataId) thành KnowledgeData. Có sẵn
    fetcher cho bài viết và sản phẩm. Loại dữ liệu khác được hỗ trợ qua
    internal event ezyai_knowledge_data_list_fetcher /
    ezyai_knowledge_data_fetcher, nên plugin khác có thể cung cấp dữ liệu
    tri thức mà không phụ thuộc vào EzyAI.
  • Chunker: bỏ thẻ HTML, gọn khoảng trắng rồi chia nội dung dài thành các
    chunk (ưu tiên theo đoạn, sau đó theo câu, cuối cùng cắt cứng) để chỉ gửi
    phần liên quan cho AI, giảm token.
  • Kết quả gửi cho AI gồm id, loại, tiêu đề có link nguồn, mã sản phẩm
    (nếu có) và đoạn trích. Link được dựng từ profileWebUrl của profile, AI
    được nhắc giữ nguyên link trong câu trả lời.

Topic guard

Khi ezyai_topic_guard_allowed_topics có giá trị, một chỉ dẫn được thêm vào
cuối system message: AI chỉ được hỗ trợ các chủ đề đã khai báo, nếu câu hỏi
(xét cùng ngữ cảnh hội thoại) nằm ngoài chủ đề hoặc không phù hợp thì từ
chối bằng câu tương tự ezyai_topic_guard_refusal_message.

Skill Library

Skill Library là thư viện skill để chia sẻ, khác với AI_SKILL dùng cho
profile:
  • Mỗi skill là bài viết type = SKILL_LIBRARY, thuộc một danh mục (term
    SKILL_CATEGORY, tự tạo khi chưa có).
  • Admin có thể publish, publish nội bộ, gỡ publish.
  • Web plugin cung cấp trang /ezyai/skill-library và API
    /api/v1/skill-library/categories, /api/v1/skill-library/skills để xem
    các skill đã publish.

Các post type EzyAI đăng ký

Post typeMục đích
AI_SKILLSkill gắn vào profile
KNOWLEDGE_DATADữ liệu tri thức nhập tay
KNOWLEDGE_FETCHER_SCRIPTScript lấy dữ liệu tri thức
SKILL_LIBRARYMục trong Skill Library
Các loại này không cần short content; AI_SKILL, KNOWLEDGE_DATA,
KNOWLEDGE_FETCHER_SCRIPT không tạo link công khai.

Danh sách API quản trị

Tất cả nằm dưới tiền tố /api/v1 của admin và yêu cầu đăng nhập.
MethodĐường dẫnMô tả
PUT/settingsLưu cài đặt EzyAI
PUT/ai-chat-services/{serviceName}/set-as-defaultĐặt dịch vụ mặc định
POST/ai-chat-services/{serviceName}/chat/streamChat thử (SSE)
GET/ai-chat-services/{serviceName}/modelsLấy danh sách model từ nhà cung cấp
GET/ai-chat-services/{serviceName}/profilesDanh sách profile
POST/ai-chat-services/{serviceName}/profiles/addThêm profile
PUT/ai-chat-services/{serviceName}/profiles/{profileId}Sửa profile
DELETE/ai-chat-services/{serviceName}/profiles/{profileId}Xoá profile (kèm api key, metadata)
PUT/ai-chat-services/{serviceName}/profiles/{profileId}/set-as-defaultĐặt profile mặc định
GET/ai-chat-services/{serviceName}/profiles/{profileId}/skillsSkill của profile
PUT/ai-chat-services/{serviceName}/profiles/{profileId}/skill-post-idsGắn skill cho profile
GET/ai-skillsDanh sách skill
POST/ai-skills/addThêm skill
PUT/ai-skills/{skillId}Sửa skill
DELETE/ai-skills/{skillId}Xoá skill
GET / PUT/ai-skills/{skillId}/allowed-tool-namesXem / lưu tool được phép
GET/skill-library, /skill-library/categories, /skill-library/{skillId}Xem Skill Library
POST/skill-library/addThêm skill vào thư viện
PUT/skill-library/{skillId}Sửa
PUT/skill-library/{skillId}/publish, /internal-publish, /unpublishĐổi trạng thái publish
DELETE/skill-library/{skillId}Xoá

Điểm mở rộng

Muốn làmCách làm
Thêm nhà cung cấp AIKế thừa AbstractAIChatService (hoặc ChatGptChatService nếu tương thích OpenAI), khai báo lớp con là singleton trong plugin
Thêm tool cho AIKhai báo cặp event handler [tool]_mcp_tool_schema_event và [tool]_mcp_tool_event, rồi thêm tên tool vào allowed tools của skill
Thêm loại dữ liệu tri thứcXử lý internal event ezyai_knowledge_data_list_fetcher / ezyai_knowledge_data_fetcher, hoặc khai báo bean EzyAIKnowledgeDataFetcher
Thêm cách tìm kiếm tri thứcKhai báo bean KnowledgeSearchStrategy với priority phù hợp
Thêm nguồn tri thức riêngKhai báo bean KnowledgeDataSource hoặc KnowledgeDataSourceProvider
Đưa thêm thông tin profile vào toolThêm meta key vào setting ezyai_additional_ai_profile_meta_keys