Hướng dẫn cài đặt lớp mở rộng nguồn dữ liệu cho EzyAI
Back to ezyaiTài liệu này dành cho developer muốn cho AI của EzyAI tìm và đọc thêm dữ
liệu ngoài bài viết và sản phẩm có sẵn, ví dụ dữ liệu sự kiện, khoá học,
câu hỏi thường gặp ở một bảng riêng, hoặc dữ liệu nằm ở một hệ thống bên
ngoài như vector database.
EzyAI cung cấp năm điểm mở rộng, từ đơn giản đến toàn quyền:
| Điểm mở rộng | Dùng khi | Phụ thuộc vào EzyAI |
|---|---|---|
Internal event ezyai_knowledge_data_*_fetcher | Thêm một loại dữ liệu mới, không muốn phụ thuộc vào ezyai-sdk | Không |
EzyAIKnowledgeDataFetcher | Thêm một loại dữ liệu mới, viết bằng Java trong plugin có ezyai-sdk | Có |
KnowledgeSearchStrategy | Thêm một cách tìm kiếm mới bên cạnh data index | Có |
KnowledgeDataSource | Thay toàn bộ cách tìm và đọc dữ liệu cho một profile | Có |
KnowledgeDataSourceProvider | Tạo nguồn dữ liệu động theo tên, ví dụ mỗi khách hàng một nguồn | Có |
Kiến trúc
Luồng tìm kiếm
flowchart TB
AI["AI gọi tool<br/>search_knowledge_data_list"] --> H["SearchKnowledgeDataListToolEventHandler"]
H -->|"dataSource = profile.aiDatasource"| M["KnowledgeDataSourceManager"]
M -->|"bean KnowledgeDataSource<br/>có getName() trùng"| DS["KnowledgeDataSource"]
M -.->|"không có bean trùng tên"| PV["KnowledgeDataSourceProvider<br/>provideKnowledgeDataSource(name)"]
PV -.-> DS
DS -->|"DEFAULT"| DDS["DefaultKnowledgeDataSource"]
DDS --> SS["KnowledgeSearchStrategy<br/>(theo priority tăng dần)"]
SS -->|"searchKnowledgeDataList"| KD["KnowledgeData"]
SS -->|"searchDataTypeIdList"| TI["(dataType, dataId)"]
TI --> F["EzyAIKnowledgeDataFetcher"]
F --> F1["Post"]
F --> F2["Product"]
F --> F3["Event fetcher"]
F3 -->|"ezyai_knowledge_data_list_fetcher"| EXT["Handler của plugin khác"]
F1 & F2 & EXT --> KD
KD --> CH["KnowledgeContentChunker"]
CH --> R["Sắp xếp theo từ khoá,<br/>cắt theo limit"]
Các đối tượng chính
classDiagram
class KnowledgeDataSourceManager {
searchKnowledgeDataList(dataSource, query, parameters, limit)
getKnowledgeDataByTypeAndIdOrCode(dataSource, type, id, code, parameters)
getDataSourceByName(name)
getKnowledgeDataSourceNames()
}
class KnowledgeDataSource {
<<interface>>
searchKnowledgeDataList(query, parameters, limit)
getKnowledgeDataByTypeAndIdOrCode(type, id, code, parameters)
getKnowledgeDataListByTypeAndIds(type, ids, parameters)
getName()
getDisplayName()
}
class KnowledgeDataSourceProvider {
<<interface>>
provideKnowledgeDataSource(name)
getKnowledgeDataSourceNames()
}
class DefaultKnowledgeDataSource
class KnowledgeSearchStrategy {
<<interface>>
searchDataTypeIdList(query, parameters, limit)
searchKnowledgeDataList(query, parameters, limit)
getPriority()
}
class EzyAIKnowledgeDataFetcher {
<<interface>>
getKnowledgeDataByTypeAndIdOrCode(type, id, code, parameters)
getKnowledgeDataListByTypeAndIds(type, ids, parameters)
}
KnowledgeDataSourceManager --> KnowledgeDataSource
KnowledgeDataSourceManager --> KnowledgeDataSourceProvider
KnowledgeDataSource <|.. DefaultKnowledgeDataSource
DefaultKnowledgeDataSource --> KnowledgeSearchStrategy
DefaultKnowledgeDataSource --> EzyAIKnowledgeDataFetcher
KnowledgeData
Mọi điểm mở rộng đều trả về
KnowledgeData:| Trường | Bắt buộc | Ý nghĩa |
|---|---|---|
id | Có | Id của dữ liệu, AI dùng để lấy chi tiết |
type | Có | Loại dữ liệu, ví dụ post, product, event |
title | Có | Tiêu đề, hiển thị kèm link nguồn |
slug | Không | Mã định danh dạng chữ, AI có thể dùng thay id |
content | Không | Nội dung đầy đủ, được chia chunk trước khi gửi cho AI |
excerpt | Không | Đoạn trích ngắn; khi chia chunk, EzyAI tự tạo đoạn trích cho từng chunk |
chunked | Không | true nếu dữ liệu đã được chia sẵn, EzyAI sẽ không chia lại |
dataUrl | Không | Đường dẫn tới dữ liệu, tương đối hoặc tuyệt đối; đường dẫn tương đối được ghép với profileWebUrl |
productCode | Không | Mã sản phẩm, khi dữ liệu là sản phẩm |
price, currencyIsoCode | Không | Giá và đơn vị tiền, chỉ trả trong tool lấy chi tiết |
Tham số truyền vào
Khi AI gọi tool, EzyAI trộn tham số do AI sinh với context của lượt chat:
| Khoá | Nguồn | Ý nghĩa |
|---|---|---|
query, limit | AI | Từ khoá và số kết quả của search_knowledge_data_list |
dataType, dataId, slug, productCode | AI | Định danh của search_knowledge_data_by_type_and_id |
dataSource | Profile | Tên nguồn dữ liệu chọn ở profile |
parameters | Profile | Map metadata của profile: rag_collection_name và các key khai báo ở ezyai_additional_ai_profile_meta_keys |
profileName, profileWebUrl | Profile | Tên và web url của profile |
channelId, askerId, askerType, userId | Lượt chat | Thông tin người hỏi |
Các điểm mở rộng nhận được
parameters, vì vậy có thể cấu hình riêng chotừng profile qua metadata, ví dụ mỗi profile trỏ tới một collection khác
nhau của vector database.
Bean context
Các điểm mở rộng viết bằng Java được EzyAI tìm bằng
singletonFactory.getSingletonsOf(...) trong bean context nơi nó chạy:- Admin, web: các plugin admin dùng chung một bean context, các pluginweb cũng vậy. Khai báo bean bằng
@EzySingletonhoặc@Servicetrongplugin của bạn là EzyAI thấy được. - Socket: mỗi socket plugin có bean context và
EventHandlerManagerriêng. Bean hay event handler khai báo ở socket plugin khác sẽ không đượcezyai-socket-pluginnhìn thấy, chỉ riêng các MCP tool (*_mcp_tool_*)được gom từ toàn zone. Chatbot nhúng qua ezychat chạy trong socket, cầnlưu ý điểm này khi thiết kế.
Danh sách fetcher, strategy, data source được khởi tạo lười ở lần dùng đầu
tiên và giữ nguyên sau đó, nên bean phải có mặt từ lúc khởi động.
Thêm loại dữ liệu qua internal event
Cách này không cần phụ thuộc vào
ezyai-sdk, phù hợp cho plugin muốn cungcấp dữ liệu tri thức nhưng không muốn gắn chặt với EzyAI.
DefaultKnowledgeDataSourcegọi hai internal event qua
EzyAIEventKnowledgeDataFetcher:| Event | Tham số | Kết quả |
|---|---|---|
ezyai_knowledge_data_list_fetcher | dataType, dataIds, parameters | List các Map (hoặc KnowledgeData) |
ezyai_knowledge_data_fetcher | dataType, dataId, dataCode, parameters | Một Map (hoặc KnowledgeData), null nếu không có |
Các khoá của
Map kết quả trùng tên các trường của KnowledgeData. Handlerphải trả về rỗng hoặc
null với dataType không phải của mình.Ví dụ plugin quản lý sự kiện cung cấp dữ liệu loại
event:@EzySingleton @AllArgsConstructor public class AdminEventKnowledgeDataListFetcherEventHandler extends AbstractEventHandler<Map<String, Object>, Object> { private final AdminEventRepository eventRepository; @SuppressWarnings("unchecked") @Override public Object handleEventData(Map<String, Object> data) { if (!"event".equals(data.get("dataType"))) { return Collections.emptyList(); } Collection<Long> ids = (Collection<Long>) data.get("dataIds"); return eventRepository.findListByIds(ids) .stream() .map(it -> EzyMapBuilder.mapBuilder() .put("id", it.getId()) .put("type", "event") .put("title", it.getName()) .put("slug", it.getSlug()) .put("content", it.getDescription()) .put("dataUrl", "/events/" + it.getSlug()) .toMap() ) .collect(Collectors.toList()); } @Override public String getEventName() { return "ezyai_knowledge_data_list_fetcher"; } }
Làm tương tự cho
ezyai_knowledge_data_fetcher để AI lấy được chi tiết mộtsự kiện.
Để tool tìm kiếm tìm ra dữ liệu loại mới bằng data index của platform:
- Dữ liệu phải được đánh index trong
DataIndexServicevớidataTypetrùng vớidataTypemà handler xử lý. - Thêm
dataTypeđó vào settingezyai_knowledge_data_types(mặc địnhezyarticle_posts,ecommerce_products).
Event chỉ được gọi khi đã có
(dataType, dataId) do một search strategy
tìm ra. Nếu dữ liệu không có trong data index, cần thêm một
KnowledgeSearchStrategy như phần dưới.Thêm loại dữ liệu bằng EzyAIKnowledgeDataFetcher
Tương đương với internal event nhưng viết trực tiếp bằng Java, kiểu dữ liệu
rõ ràng và không cần chuyển qua
Map. DefaultKnowledgeDataSource gọitất cả fetcher:
-
getKnowledgeDataListByTypeAndIds: gộp kết quả của mọi fetcher. -
getKnowledgeDataByTypeAndIdOrCode: lấy kết quả khácnullđầu tiên.
@EzySingleton @AllArgsConstructor public class AdminCourseKnowledgeDataFetcher implements EzyAIKnowledgeDataFetcher { private static final String DATA_TYPE = "course"; private final AdminCourseRepository courseRepository; @Override public KnowledgeData getKnowledgeDataByTypeAndIdOrCode( String type, Long id, String code, Map<String, Object> parameters ) { if (!DATA_TYPE.equals(type)) { return null; } Course course = id != null ? courseRepository.findById(id) : courseRepository.findByField("code", code); return course != null ? toKnowledgeData(course) : null; } @Override public List<KnowledgeData> getKnowledgeDataListByTypeAndIds( String type, Collection<Long> ids, Map<String, Object> parameters ) { if (!DATA_TYPE.equals(type)) { return Collections.emptyList(); } return newArrayList( courseRepository.findListByIds(ids), this::toKnowledgeData ); } private KnowledgeData toKnowledgeData(Course course) { return KnowledgeData.builder() .id(course.getId()) .type(DATA_TYPE) .title(course.getName()) .slug(course.getCode()) .content(course.getDescription()) .dataUrl("/courses/" + course.getCode()) .build(); } }
Lưu ý:
- Luôn kiểm tra
typevà trả về rỗng với loại không phải của mình, vì mọifetcher đều được gọi. - Chỉ trả về dữ liệu được phép công khai cho người dùng chatbot (ví dụ đãxuất bản), vì nội dung sẽ được gửi cho nhà cung cấp AI và có thể hiểnthị trong câu trả lời.
- Truy vấn theo danh sách id một lần (
findListByIds), không truy vấn từngid trong vòng lặp.
Thêm cách tìm kiếm bằng KnowledgeSearchStrategy
Search strategy quyết định dữ liệu nào khớp với từ khoá. Mặc định chỉ có
KnowledgeIndexedDataSearchStrategy, tìm trong data index của platformbằng Reciprocal Rank Fusion,
priority = Short.MAX_VALUE (chạy sau cùng).Một strategy có thể trả kết quả theo một trong hai cách:
| Method | Trả về | Khi nào dùng |
|---|---|---|
searchKnowledgeDataList | KnowledgeData đầy đủ | Nguồn tìm kiếm trả luôn nội dung, ví dụ vector database, search engine |
searchDataTypeIdList | (dataType, dataId) | Nguồn tìm kiếm chỉ trả id, nội dung được đọc sau bằng fetcher |
Cách
DefaultKnowledgeDataSource dùng các strategy:
sequenceDiagram
participant DS as DefaultKnowledgeDataSource
participant S as Strategies (priority tăng dần)
participant F as Fetchers
participant C as Chunker
loop từng strategy
DS->>S: searchKnowledgeDataList(query, parameters, limit)
Note over DS: dừng sớm khi đã nhiều hơn limit kết quả
end
loop từng strategy
DS->>S: searchDataTypeIdList(query, parameters, limit)
end
DS->>F: getKnowledgeDataListByTypeAndIds (gom theo dataType)
DS->>C: chunk các dữ liệu chưa chunked
DS->>DS: ưu tiên chunk chứa từ khoá, cắt theo limit
Ví dụ strategy tìm trong vector database, collection lấy từ metadata
rag_collection_name của profile:@EzySingleton @AllArgsConstructor public class AdminVectorKnowledgeSearchStrategy implements KnowledgeSearchStrategy { private final AdminVectorSearchClient vectorSearchClient; @Override public List<KnowledgeData> searchKnowledgeDataList( String query, Map<String, Object> parameters, int limit ) { String collectionName = toStringOrNull( parameters.get(META_KEY_RAG_COLLECTION_NAME) ); if (isBlank(collectionName)) { return Collections.emptyList(); } return newArrayList( vectorSearchClient.search(collectionName, query, limit), it -> KnowledgeData.builder() .id(it.getId()) .type(it.getType()) .title(it.getTitle()) .content(it.getText()) .dataUrl(it.getUrl()) .chunked(true) .build() ); } @Override public int getPriority() { return 0; } }
Lưu ý:
prioritynhỏ chạy trước. Ở vòngsearchKnowledgeDataList, khi tổng kếtquả đã vượtlimitthì các strategy sau bị bỏ qua.- Đặt
chunked(true)nếu kết quả đã là đoạn ngắn, tránh bị chia lại. - Strategy được áp dụng cho nguồn
DEFAULT, tức mọi profile chọn nguồnnày. Hãy dựa vàoparametersđể quyết định có tìm hay không.
Thêm nguồn dữ liệu bằng KnowledgeDataSource
Khi cần thay hoàn toàn cách tìm và đọc dữ liệu cho một nhóm profile, cài
KnowledgeDataSource với tên riêng. Profile chọn nguồn này ở trường **Nguồndữ liệu**, EzyAI gọi thẳng nguồn đó mà không qua strategy, fetcher hay
chunker mặc định.
@EzySingleton @AllArgsConstructor public class AdminHelpCenterKnowledgeDataSource implements KnowledgeDataSource { private final AdminHelpCenterClient helpCenterClient; @Override public List<KnowledgeData> searchKnowledgeDataList( String query, Map<String, Object> parameters, int limit ) { return newArrayList( helpCenterClient.searchArticles(query, limit), this::toKnowledgeData ); } @Override public KnowledgeData getKnowledgeDataByTypeAndIdOrCode( String type, Long id, String code, Map<String, Object> parameters ) { HelpCenterArticle article = helpCenterClient.getArticle(id, code); return article != null ? toKnowledgeData(article) : null; } @Override public List<KnowledgeData> getKnowledgeDataListByTypeAndIds( String type, Collection<Long> ids, Map<String, Object> parameters ) { return newArrayList( helpCenterClient.getArticles(ids), this::toKnowledgeData ); } @Override public String getName() { return "HELP_CENTER"; } @Override public String getDisplayName() { return "Help center"; } private KnowledgeData toKnowledgeData(HelpCenterArticle article) { return KnowledgeData.builder() .id(article.getId()) .type("help_center_article") .title(article.getTitle()) .content(article.getBody()) .dataUrl(article.getUrl()) .build(); } }
Lưu ý:
getName()phải duy nhất, đây là giá trị lưu ở profile(ChatProfile.aiDatasource). Đổi tên sau khi đã có profile dùng sẽ làmcác profile đó mất nguồn dữ liệu.- Trang chi tiết profile hiển thị nguồn dữ liệu bằng message key là tênviết thường (ví dụ
help_center), hãy thêm key này vào file messages củaplugin. - Nguồn tự lo việc giới hạn độ dài nội dung, vì không đi qua chunker mặcđịnh. Có thể inject
KnowledgeContentChunkerđể dùng lại. - Thông tin kết nối hệ thống ngoài (url, api key) đọc từ setting hoặc biếnmôi trường lúc chạy, không hardcode trong mã nguồn.
Tạo nguồn dữ liệu động bằng KnowledgeDataSourceProvider
Provider được hỏi khi không có bean
KnowledgeDataSource nào trùng tên.Dùng khi danh sách nguồn không cố định, ví dụ mỗi workspace của khách hàng
là một nguồn.
@EzySingleton @AllArgsConstructor public class AdminWorkspaceKnowledgeDataSourceProvider implements KnowledgeDataSourceProvider { private static final String PREFIX = "WORKSPACE_"; private final AdminWorkspaceService workspaceService; private final AdminWorkspaceSearchClient searchClient; @Override public KnowledgeDataSource provideKnowledgeDataSource( String datasourceName ) { if (!datasourceName.startsWith(PREFIX)) { return null; } long workspaceId = toLongOrZero( datasourceName.substring(PREFIX.length()) ); return workspaceId > 0 ? new WorkspaceKnowledgeDataSource( datasourceName, workspaceId, searchClient ) : null; } @Override public List<KnowledgeDataSourceNameModel> getKnowledgeDataSourceNames() { return newArrayList( workspaceService.getActiveWorkspaces(), it -> KnowledgeDataSourceNameModel.builder() .name(PREFIX + it.getId()) .displayName(it.getName()) .build() ); } }
provideKnowledgeDataSourcetrảnullvới tên không phải của mình, cácprovider được hỏi lần lượt tới khi có kết quả.getKnowledgeDataSourceNamescung cấp danh sách cho ô Nguồn dữ liệuở form profile.provideKnowledgeDataSourceđược gọi ở mỗi lần AI dùng tool, nên cầnnhanh; cache đối tượng nếu khởi tạo tốn kém.
Kiểm tra
- Khởi động admin, mở form thêm/sửa profile, kiểm tra nguồn mới có trong ôNguồn dữ liệu (với
KnowledgeDataSourcevà provider). - Gắn vào profile một kỹ năng có tool
search_knowledge_data_listvàsearch_knowledge_data_by_type_and_id. - Chat thử ở trang chi tiết dịch vụ với câu hỏi liên quan tới dữ liệu mới.Câu trả lời trả về metadata
knowledgeListliệt kê các dữ liệu AI đãdùng, kiểm tratype,id,dataUrlđúng như mong đợi. - Nếu AI không tìm thấy dữ liệu:
Kiểm tra Cách xử lý Profile đã chọn đúng nguồn dữ liệu Sửa profile dataTypecó trongezyai_knowledge_data_typesThêm vào setting Dữ liệu đã có trong data index Đánh index lại, hoặc thêm search strategy Fetcher/handler có bỏ qua do sai typeSo sánh dataTypetrả về từ strategy vớitypemà fetcher kiểm traBean nằm ở socket plugin khác Chuyển sang admin/web hoặc vào cùng plugin với EzyAI Kiểm tra Cách xử lý Profile đã chọn đúng nguồn dữ liệu Sửa profile dataTypecó trongezyai_knowledge_data_typesThêm vào setting Dữ liệu đã có trong data index Đánh index lại, hoặc thêm search strategy Fetcher/handler có bỏ qua do sai typeSo sánh dataTypetrả về từ strategy vớitypemà fetcher kiểm traBean nằm ở socket plugin khác Chuyển sang admin/web hoặc vào cùng plugin với EzyAI