Hướng dẫn tích hợp chat bot cho ezychat
Back to ezychatEzyChat tách phần quản lý hội thoại khỏi phần kết nối tới mô hình AI. Vì vậy, bạn có thể tích hợp OpenAI, Gemini, Claude, mô hình self-hosted hoặc bất kỳ chatbot nào bằng cách cài đặt một lớp mở rộng theo chuẩn
ChatBotService.Nếu không muốn tự viết các lớp tích hợp, bạn có thể cài đặt EzyAI. EzyAI đã cung cấp sẵn các implementation cho nhiều nhà cung cấp AI; sau khi cài đặt, bạn chủ yếu cấu hình API key, endpoint, model, profile và system prompt trên giao diện quản trị.
Kiến trúc tích hợp chatbot
Trong luồng chatbot, EzyChat chịu trách nhiệm:
- Nhận và lưu tin nhắn của người dùng.
- Kiểm tra chatbot có được bật trong kênh hay không.
- Kiểm tra hạn mức trả lời của người dùng.
- Lấy các tin nhắn gần nhất làm ngữ cảnh.
- Chọn dịch vụ chatbot theo cấu hình.
- Chuyển dữ liệu từ provider về giao diện theo dạng streaming.
- Lưu câu trả lời hoàn chỉnh vào lịch sử hội thoại.
Lớp tích hợp của bạn chỉ cần chuyển
ChatBotRequestModel thành request của provider và chuyển response của provider thành các đoạn nội dung mà EzyChat hiểu được.
flowchart LR
U[Người dùng gửi tin nhắn] --> C[EzyChat]
C --> V{Chatbot có trong kênh?}
V -- Không --> S[Lưu và hiển thị tin nhắn]
V -- Có --> Q{Còn hạn mức trả lời?}
Q -- Không --> L[Gửi thông báo hết hạn mức]
Q -- Có --> H[Lấy lịch sử hội thoại]
H --> P[Chọn ChatBotService]
P --> A[Nhà cung cấp AI]
A -->|Các đoạn nội dung| P
P -->|onDelta| T[Hiển thị trạng thái đang trả lời]
P -->|onCompleted| M[Lưu câu trả lời]
M --> U
EzyChat không phụ thuộc trực tiếp vào một API AI cụ thể. Ranh giới giữa EzyChat và provider được xác định bởi bốn thành phần chính:
-
ChatBotService: điểm vào của một dịch vụ chatbot. -
ChatBotRequestModel: dữ liệu đầu vào của một lượt hỏi. -
ChatBotOutputStream: bộ phân tích response của provider. -
ChatBotAnswerModel: kết quả hoàn chỉnh và metadata của câu trả lời.
Thêm SDK vào dự án
Module chứa implementation của bạn cần phụ thuộc vào
ezychat-sdk:<dependency> <groupId>org.youngmonkeys</groupId> <artifactId>ezychat-sdk</artifactId> <version></version> </dependency>
Hãy dùng phiên bản SDK tương ứng với phiên bản EzyChat đang được cài trên EzyPlatform.
Cài đặt ChatBotService
Mỗi provider phải cài đặt interface sau:
public interface ChatBotService { InputStream createInputStream( ChatBotRequestModel request ) throws Exception; ChatBotOutputStream createOutputStream( ChatBotStreamHandler handler ) throws Exception; default boolean isAvailable() { return true; } String getServiceName(); }
Ý nghĩa của các phương thức:
-
getServiceName()trả về tên duy nhất của dịch vụ. Tên này xuất hiện trong danh sách dịch vụ chatbot trên trang quản trị. -
isAvailable()cho biết dịch vụ đã sẵn sàng hay chưa, chẳng hạn API key và endpoint đã được cấu hình. -
createInputStream()gửi request tới provider và trả về stream chứa response. -
createOutputStream()tạo bộ tiếp nhận và phân tích dữ liệu được đọc từ response stream.
EzyChat nối hai stream theo mô hình sau:
flowchart LR
R[ChatBotRequestModel] --> I[createInputStream]
I --> HTTP[Response stream của provider]
HTTP --> D[Bộ điều phối stream]
D --> O[ChatBotOutputStream]
O -->|onDelta| UI[Giao diện chat]
O -->|getAnswer| F[ChatBotAnswerModel]
F --> DB[(Lịch sử tin nhắn)]
Thiết kế này cho phép tích hợp cả hai loại API:
- API trả toàn bộ nội dung trong một response.
- API streaming qua SSE hoặc một giao thức truyền từng phần tương tự.
Dữ liệu EzyChat chuyển cho provider
ChatBotRequestModel chứa dữ liệu cần thiết để tạo request gửi tới AI:public class ChatBotRequestModel { private ChatProfileModel profile; private String systemMessage; private List<ChatBotMessageModel> contextMessages; private long channelId; private long askerId; private String askerType; }
Trong đó:
-
profilechứa cấu hình của chat profile, ví dụ model, endpoint, timeout và giới hạn token. -
systemMessagelà chỉ dẫn hệ thống được cấu hình cho profile hoặc toàn hệ thống. -
contextMessageslà các tin nhắn gần nhất trong kênh. -
channelIdđịnh danh cuộc hội thoại. -
askerIdvàaskerTypexác định người gửi câu hỏi.
Mỗi tin nhắn ngữ cảnh gồm
role và content:
for (ChatBotMessageModel message : request.getContextMessages()) {
String role = message.getRole();
String content = message.getContent();
// Chuyển sang định dạng message của provider.
}
EzyChat chuyển tin nhắn do chatbot gửi thành vai trò
assistant; các tin nhắn còn lại được chuyển thành vai trò user.Ví dụ implementation tối giản
Ví dụ sau minh họa một provider trả lời dạng streaming. Phần gọi HTTP được trừu tượng hóa để bạn có thể thay bằng client đang sử dụng trong dự án.
import com.tvd12.ezyfox.bean.annotation.EzySingleton; import org.youngmonkeys.ezychat.sdk.handler.ChatBotStreamHandler; import org.youngmonkeys.ezychat.sdk.io.ChatBotOutputStream; import org.youngmonkeys.ezychat.sdk.model.ChatBotAnswerModel; import org.youngmonkeys.ezychat.sdk.model.ChatBotRequestModel; import org.youngmonkeys.ezychat.sdk.service.ChatBotService; import java.io.InputStream; @EzySingleton public class CustomChatBotService implements ChatBotService { private final CustomAIClient aiClient; private final ThreadLocal<ChatBotRequestModel> pendingRequest = new ThreadLocal<>(); public CustomChatBotService(CustomAIClient aiClient) { this.aiClient = aiClient; } @Override public String getServiceName() { return "custom-ai"; } @Override public boolean isAvailable() { return aiClient.isConfigured(); } @Override public InputStream createInputStream( ChatBotRequestModel request ) throws Exception { pendingRequest.set(request); return aiClient.openChatStream( request.getSystemMessage(), request.getContextMessages(), request.getProfile() ); } @Override public ChatBotOutputStream createOutputStream( ChatBotStreamHandler handler ) { ChatBotRequestModel request = pendingRequest.get(); pendingRequest.remove(); return new CustomChatBotOutputStream( request, handler ); } }
createInputStream() và createOutputStream() được gọi liên tiếp trên cùng luồng xử lý. Trong ví dụ, ThreadLocal được dùng để chuyển request sang output stream mà không chia sẻ trạng thái giữa các cuộc hội thoại đồng thời.Cần gọi
remove() ngay sau khi lấy dữ liệu để tránh giữ request lâu hơn cần thiết trong thread pool.Cài đặt output stream
ChatBotOutputStream nhận các byte được đọc từ response của provider. Implementation phải:- Ghép và phân tích các chunk.
- Lấy phần nội dung mới của câu trả lời.
- Gọi
handler.onDelta()khi có nội dung mới. - Giữ lại câu trả lời hoàn chỉnh.
- Trả về
ChatBotAnswerModeltronggetAnswer().
Một implementation đơn giản có thể viết như sau:
import org.youngmonkeys.ezychat.sdk.handler.ChatBotStreamHandler; import org.youngmonkeys.ezychat.sdk.io.ChatBotOutputStream; import org.youngmonkeys.ezychat.sdk.model.ChatBotAnswerModel; import org.youngmonkeys.ezychat.sdk.model.ChatBotRequestModel; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; public class CustomChatBotOutputStream extends ChatBotOutputStream { private final ChatBotRequestModel request; private final ChatBotStreamHandler handler; private final StringBuilder content = new StringBuilder(); public CustomChatBotOutputStream( ChatBotRequestModel request, ChatBotStreamHandler handler ) { this.request = request; this.handler = handler; } @Override public void write(int value) throws IOException { write(new byte[]{(byte) value}, 0, 1); } @Override public void write( byte[] bytes, int offset, int length ) throws IOException { String responseChunk = new String( bytes, offset, length, StandardCharsets.UTF_8 ); // Với SSE hoặc JSON streaming, hãy thay đoạn này // bằng bộ parser có khả năng giữ dữ liệu dở dang giữa các lần write. String delta = parseDelta(responseChunk); if (delta == null || delta.isEmpty()) { return; } content.append(delta); try { handler.onDelta(delta); } catch (Exception e) { throw new IOException(e); } } @Override public ChatBotAnswerModel getAnswer() { Map<String, Object> metadata = new LinkedHashMap<>(); metadata.put("channelId", request.getChannelId()); return ChatBotAnswerModel.builder() .serviceName("custom-ai") .profileName(request.getProfile().getName()) .model(request.getProfile().getAiModel()) .content(content.toString()) .contentType("MARKDOWN") .stopReason("end_turn") .metadata(metadata) .build(); } private String parseDelta(String chunk) { // Phân tích response thực tế của provider tại đây. return chunk; } }
Đoạn
parseDelta() phía trên chỉ nhằm minh họa. Trong production, không nên giả định mỗi lần gọi write() sẽ chứa đúng một JSON object hoặc một SSE event hoàn chỉnh. Một event có thể bị chia qua nhiều chunk, hoặc nhiều event có thể nằm chung trong một chunk.Bộ parser nên giữ buffer riêng:
flowchart TD
B[Nhận một chunk byte] --> A[Ghép vào buffer]
A --> E{Đã đủ một event?}
E -- Chưa --> W[Chờ chunk tiếp theo]
E -- Rồi --> J[Phân tích event]
J --> X{Có delta nội dung?}
X -- Có --> H[handler.onDelta]
X -- Không --> N[Bỏ qua hoặc xử lý metadata]
H --> E
N --> E
Xử lý callback đúng cách
ChatBotStreamHandler có ba callback:public interface ChatBotStreamHandler { void onDelta(String deltaContent) throws Exception; void onCompleted(ChatBotAnswerModel answer); void onError(Exception exception); }
Thông thường provider chỉ cần chủ động gọi
onDelta(). EzyChat sẽ gọi:-
onCompleted()sau khi stream được đọc hết vàgetAnswer()trả về kết quả. -
onError()nếu mở kết nối, đọc stream hoặc phân tích dữ liệu thất bại.
Nếu
onDelta() ném exception, EzyChat sẽ dừng luồng xử lý và đóng kết nối. Cơ chế này hữu ích khi kênh đã bị đóng hoặc phía nhận không còn chấp nhận câu trả lời.
EzyChat giới hạn tần suất cập nhật trạng thái đang trả lời trước khi đẩy xuống client, vì vậy provider không cần tự tạo timer chỉ để cập nhật giao diện.
Tạo ChatBotAnswerModel
Kết quả hoàn chỉnh nên cung cấp tối đa các trường mà provider trả về:
return ChatBotAnswerModel.builder() .serviceName("custom-ai") .profileName(profileName) .model(modelName) .content(answerContent) .contentType("MARKDOWN") .stopReason(stopReason) .inputTokens(inputTokens) .outputTokens(outputTokens) .metadata(metadata) .build();
Một số lưu ý:
- Nếu không khai báo
contentType, EzyChat mặc định coi nội dung là Markdown. -
contentlà câu trả lời hoàn chỉnh. Nếu để trống, EzyChat sẽ ghép nội dung đã nhận qua các lầnonDelta(). -
metadatađược lưu cùng tin nhắn chatbot và có thể dùng để lưu nguồn tham khảo hoặc dữ liệu truy vết. - Không nên đưa API key, access token hoặc dữ liệu bí mật vào metadata.
-
inputTokensvàoutputTokensnên lấy từ usage do provider trả về, nếu có.
Đăng ký dịch vụ với EzyChat
Cách đơn giản nhất là đánh dấu implementation bằng
@EzySingleton:@EzySingleton public class CustomChatBotService implements ChatBotService { // ... }
Khi khởi động, EzyChat quét các singleton cài đặt
ChatBotService, lập chỉ mục theo giá trị của getServiceName() và đưa tên dịch vụ vào danh sách cấu hình.EzyPlatform có thể sử dụng các DI context riêng cho admin, web và socket. Nếu provider cần xuất hiện trong nhiều runtime, hãy đăng ký một bean cho từng runtime cần sử dụng.
Một cách tổ chức phổ biến là đặt logic dùng chung trong lớp cơ sở:
public class CustomChatBotService implements ChatBotService { // Toàn bộ logic dùng chung. }
Sau đó tạo lớp bean mỏng trong từng plugin:
@EzySingleton public class WebCustomChatBotService extends CustomChatBotService { }
@EzySingleton public class AdminCustomChatBotService extends CustomChatBotService { }
Bean phía admin giúp dịch vụ xuất hiện và được kiểm tra trên giao diện quản trị. Bean phía runtime web hoặc socket mới là thành phần thực sự xử lý câu hỏi của người dùng.
Nếu plugin của bạn khởi tạo dịch vụ theo cách động, cũng có thể đăng ký trực tiếp qua
ChatBotServiceManager.addChatBotService() hoặc addChatBotServices().Tên trả về từ
getServiceName() phải:- Ổn định giữa các lần triển khai.
- Không trùng với provider khác.
- Giống nhau trong admin và runtime xử lý chat.
Cấu hình trên giao diện quản trị
Sau khi dịch vụ được đăng ký, mở phần cài đặt EzyChat trên giao diện admin và cấu hình:
- Dịch vụ chatbot mặc định.
- System message mặc định.
- Số tin nhắn gần nhất dùng làm ngữ cảnh.
- Số câu trả lời tối đa cho mỗi người dùng.
- Thời gian đặt lại hạn mức.
- Nội dung thông báo khi người dùng hết hạn mức.
Giá trị hạn mức bằng
0 có nghĩa là không áp dụng giới hạn. Hạn mức được tính khi câu hỏi được chuyển tới chatbot, không chờ đến khi provider trả lời xong.
Bạn cũng có thể tạo chat profile riêng cho từng mục đích sử dụng. Mỗi profile có thể chọn:
- Chatbot service.
- System message.
- AI endpoint.
- Model.
- Giới hạn token.
- Timeout.
- Nguồn dữ liệu hoặc thông tin mở rộng.
Khi xử lý một kênh, EzyChat ưu tiên profile đang hoạt động được gắn với kênh. Nếu không có, hệ thống sử dụng profile hoặc dịch vụ mặc định.
Cuối cùng, chatbot phải được thêm vào kênh hoặc được bật cho kênh cần sử dụng. Việc chỉ chọn provider trong phần cài đặt chưa đồng nghĩa bot sẽ trả lời trong mọi cuộc hội thoại.
Thứ tự chọn dịch vụ
Khi có tin nhắn mới, EzyChat chọn provider theo thứ tự:
flowchart TD
A[Nhận tin nhắn] --> B{Kênh có chat profile?}
B -- Có --> C[Dùng service của profile]
B -- Không --> D[Dùng profile mặc định]
D --> E{Profile mặc định tồn tại?}
E -- Có --> F[Dùng service của profile]
E -- Không --> G[Dùng service mặc định]
C --> H{Service tồn tại và khả dụng?}
F --> H
G --> H
H -- Có --> I[Gửi request tới provider]
H -- Không --> J[Không tạo câu trả lời]
Nếu tên service được cấu hình không tồn tại, EzyChat thử sử dụng dịch vụ mặc định. Nếu không có dịch vụ khả dụng, lượt chat đó không tạo câu trả lời từ bot.
Vì vậy,
isAvailable() nên trả về false khi thiếu API key hoặc cấu hình bắt buộc. Điều này tránh trường hợp chatbot đã được bật trong kênh nhưng không thể phản hồi.Sử dụng EzyAI để không phải tự cài đặt provider
Với hầu hết dự án, cách nhanh nhất là cài đặt EzyAI cùng EzyChat.
EzyAI đã cung cấp sẵn các lớp tích hợp cho những dịch vụ phổ biến như:
- OpenAI/ChatGPT.
- Gemini.
- Claude.
- DeepSeek.
- Grok.
- Qwen.
Các lớp có sẵn xử lý những phần phức tạp như:
- Xây dựng request theo giao thức của từng provider.
- Streaming và SSE.
- Retry đối với lỗi mạng tạm thời.
- Timeout.
- Đọc danh sách model.
- Tool calling.
- AI skill và system prompt mở rộng.
- Knowledge data.
- Giới hạn chủ đề trả lời.
Sau khi cài EzyAI, quy trình thông thường chỉ còn:
- Mở phần quản trị AI.
- Chọn nhà cung cấp.
- Khai báo API key và endpoint.
- Chọn model, token limit và timeout.
- Tạo hoặc kích hoạt AI profile.
- Chọn dịch vụ/profile đó cho EzyChat.
- Bật chatbot cho kênh cần sử dụng.
flowchart LR
I[Cài EzyChat và EzyAI] --> P[Chọn provider]
P --> K[Nhập API key và endpoint]
K --> M[Chọn model]
M --> R[Tạo AI profile]
R --> C[Chọn profile cho EzyChat]
C --> E[Bật chatbot trong kênh]
Bạn chỉ nên tự cài đặt
ChatBotService khi:- Cần kết nối một provider chưa được EzyAI hỗ trợ.
- Có AI gateway riêng trong doanh nghiệp.
- Chạy mô hình nội bộ.
- Provider sử dụng giao thức đặc thù.
- Cần kiểm soát hoàn toàn việc tạo request và phân tích response.
Kiểm tra tích hợp
Sau khi triển khai, nên kiểm tra lần lượt:
- Tên service xuất hiện trên giao diện admin.
-
isAvailable()trả vềfalsekhi thiếu cấu hình. - Bot chỉ trả lời trong các kênh đã bật chatbot.
- System message được chuyển đúng tới provider.
- Số lượng tin nhắn ngữ cảnh đúng với cấu hình.
- Nội dung streaming không bị lặp hoặc mất ký tự.
- Markdown được hiển thị đúng.
- Câu trả lời cuối cùng được lưu vào lịch sử.
- Lỗi provider không làm gián đoạn việc gửi tin nhắn thông thường.
- Timeout và đóng kết nối hoạt động đúng.
- Hạn mức theo người dùng được áp dụng đúng.
- Metadata không chứa thông tin bí mật.
Kết luận
Điểm mở rộng quan trọng nhất của EzyChat là
ChatBotService. Bạn không cần thay đổi luồng gửi tin nhắn, WebSocket, lưu lịch sử hay giao diện chat. Chỉ cần cung cấp input stream từ provider, phân tích response qua ChatBotOutputStream, phát nội dung bằng onDelta() và trả kết quả cuối cùng bằng ChatBotAnswerModel.Nếu muốn tích hợp nhanh các mô hình phổ biến, EzyAI là lựa chọn phù hợp hơn: các lớp provider đã được cài đặt sẵn và phần lớn công việc còn lại có thể thực hiện trực tiếp trên giao diện quản trị.