Hướng dẫn cài đặt lớp mở rộng nguồn dữ liệu cho EzyAI
Back to ezyai
Updated at
1792164927000
Tà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 cho từ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 plugin web cũng vậy. Khai báo bean bằng
@EzySingletonhoặc@Servicetrong plugin 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ần lư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 cung cấp dữ liệu tri thức nhưng không muốn gắn chặt với EzyAI. DefaultKnowledgeDataSource gọ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. Handler phả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ột sự 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ọi tấ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ọi fetcher đề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ển thị 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ừng id 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 platform bằ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ết quả đã 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ồn dữ 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àm cá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ên viết thường (ví dụ
help_center), hãy thêm key này vào file messages của plugin. - 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ến mô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ác provider đượ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ần nhanh; 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