Embedding là gì? Cho chatbot quán Mộc tìm chính sách theo nghĩa

Embedding là gì và vì sao nó giúp chatbot hiểu 'đền tiền' cũng là 'hoàn tiền': chạy mô hình trên máy, đo tìm theo nghĩa so với tìm theo từ khóa.

Embedding biến một đoạn chữ thành dãy số (vector) sao cho hai đoạn gần nghĩa thì dãy số gần nhau. Nhờ vậy code tìm được "hoàn tiền" khi khách viết "đền tiền", dù hai cụm không chung chữ nào. Bạn có muốn chatbot hiểu khách nói gì thay vì chỉ đếm chữ không? Cùng mình làm nhé!

Vì sao tìm theo từ khóa chưa đủ?

Ở bài trước, chatbot của quán Mộc (một quán cà phê nhỏ bán online, quán và dữ liệu trong series là minh họa) học cách đọc chính sách theo kiểu RAG. Nó tách chính sách thành 14 đoạn theo từng mục, tìm vài đoạn khớp với câu hỏi bằng cách đếm từ trùng, rồi chỉ gửi các đoạn đó cho mô hình. Cách này chạy tốt khi khách dùng đúng chữ của tài liệu. Nó hỏng khi khách nói theo cách của họ: "đền tiền" thay vì "hoàn tiền", "tiền về" thay vì "hoàn lại".

Mình không muốn kết luận chỉ bằng vài câu hỏi đã biết trước. Nên trước khi sửa gì, mình viết ra 12 câu hỏi giống kiểu khách hay nhắn, và với mỗi câu ghi rõ những mục chính sách phải có mặt thì mới đủ thông tin để trả lời. Ví dụ "Em hủy đơn chuyển khoản rồi, bao lâu thì tiền về?" cần cả mục Hủy đơn lẫn mục Hoàn tiền, vì con số "ba ngày làm việc" nằm ở mục sau. Bộ câu hỏi này là thứ mình sẽ chạy lại sau mỗi lần đổi cách tìm.

Mình phải nói trước một điều. 12 câu là rất ít, và chính mình viết chúng khi đã đọc chính sách, nên con số bên dưới chỉ là một gợi ý, không phải một phép đo chặt chẽ. Dù vậy, so với việc đoán bằng cảm giác thì có một bộ câu hỏi vẫn tốt hơn nhiều.

Embedding là gì và đo độ giống bằng cách nào?

Một mô hình embedding nhận vào một đoạn chữ và trả về một vector, ở đây là 384 con số. Mô hình được huấn luyện để hai đoạn cùng ý thì vector gần nhau, hai đoạn khác ý thì xa nhau. Bạn không cần hiểu 384 con số đó nghĩa là gì, và thật ra không ai hiểu từng con số. Bạn chỉ cần so được hai vector xem chúng gần nhau đến đâu.

Cách so phổ biến nhất là cosine similarity, đo góc giữa hai vector. Giá trị càng gần 1 thì hai vector càng cùng hướng, tức hai đoạn chữ càng giống nghĩa. Nếu các vector đã được chuẩn hóa độ dài về 1, như mình sẽ làm bên dưới, thì cosine chính là tích vô hướng của hai vector, nên chỉ cần một vòng nhân và cộng.

Trước hết mình muốn bạn thấy tận mắt một vector trông thế nào. File dưới đây nhúng ba câu rồi in độ giống nhau giữa từng cặp. Hai câu đầu cùng ý (khách đòi tiền khi pha sai, và chính sách hoàn tiền), câu thứ ba nói chuyện khác hẳn (mã giảm giá):

src/thu-nghia.ts
// Ba câu ví dụ để thấy embedding đo "giống nghĩa" thế nào. Không cần chính sách, không gọi mô hình ngôn ngữ.
import { doGiong, nhungCauHoi } from "./nhung.ts";

const CAU = [
  "Ly cà phê pha sai, em muốn quán đền tiền",
  "Quán hoàn tiền trong ba ngày làm việc",
  "Mã giảm giá không cộng dồn với tích điểm",
];

const vector = await Promise.all(CAU.map((cau) => nhungCauHoi(cau)));
console.log(`Mỗi câu thành một vector ${vector[0].length} số. Ba số đầu của câu 1: ${vector[0].slice(0, 3).map((x) => x.toFixed(4)).join(", ")}\n`);
for (const [a, b] of [[0, 1], [0, 2], [1, 2]]) {
  console.log(`${doGiong(vector[a], vector[b]).toFixed(3)}  câu ${a + 1} so với câu ${b + 1}`);
}

Hai hàm nhungCauHoi và doGiong nằm trong file nhung.ts, mình sẽ viết ngay sau đây. Đây là kết quả khi chạy:

npx tsx src/thu-nghia.ts
Mỗi câu thành một vector 384 số. Ba số đầu của câu 1: 0.0576, -0.0015, -0.0033

0.863  câu 1 so với câu 2
0.831  câu 1 so với câu 3
0.841  câu 2 so với câu 3

Hãy nhìn các con số. Câu 1 so với câu 2 được 0,863, cao hơn 0,831 và 0,841 của hai cặp còn lại, nên cặp cùng ý đứng đầu. Đúng như mong đợi. Nhưng mức chênh nhỏ đến bất ngờ: cặp "không liên quan" vẫn được 0,83. Đây là đặc điểm của họ mô hình e5 mà mình dùng, điểm của chúng thường dồn vào khoảng 0,7 đến 0,9. Vì vậy bạn đừng đọc 0,86 như "giống 86%". Con số tuyệt đối gần như vô nghĩa, chỉ có thứ hạng là đáng tin: đoạn nào đứng trên đoạn nào.

Chạy mô hình embedding ngay trên máy

Anthropic không có mô hình embedding riêng. Trang Embeddings trong tài liệu Claude nói thẳng điều này và gợi ý dùng Voyage AI nếu bạn muốn một dịch vụ qua API. Với bài này mình chọn cách khác, chạy một mô hình nhỏ ngay trên máy: nó miễn phí, không cần key, và bạn chạy lại được bao nhiêu lần cũng không tốn đồng nào. Mô hình mình dùng là multilingual-e5-small, hỗ trợ tiếng Việt, đã được chuyển sang định dạng mà thư viện @xenova/transformers chạy được bằng JavaScript.

Bạn tạo thư mục mới, lấy chinh-sach.md cùng ba file tach-doan.ts, tim-doan.ts và tra-loi.ts từ bài trước đặt vào src/ (các file này không đổi), rồi cài hai thư viện:

Cài thư viện
npm init -y
npm install --save-exact @anthropic-ai/sdk@1.5.0 @xenova/transformers@2.17.2

Mình ghim đúng phiên bản vì mọi kết quả trong bài là từ các phiên bản này. Thư viện @anthropic-ai/sdk chỉ cần vì tra-loi.ts import nó. File mới đầu tiên là src/nhung.ts:

src/nhung.ts
import { pipeline } from "@xenova/transformers";

import type { Doan } from "./tach-doan.ts";

// Mô hình embedding nhỏ (384 chiều, khoảng 120 MB), chạy ngay trên máy, hiểu tiếng Việt.
// Lần chạy đầu nó tải về một lần rồi lưu vào thư mục cache của thư viện.
const TEN_MO_HINH = "Xenova/multilingual-e5-small";

let boNhung: Awaited<ReturnType<typeof pipeline>> | undefined;

async function nhung(cacChu: string[]): Promise<number[][]> {
  boNhung ??= await pipeline("feature-extraction", TEN_MO_HINH);
  const ketQua = await boNhung(cacChu, { pooling: "mean", normalize: true });
  return ketQua.tolist() as number[][];
}

// Họ mô hình e5 được huấn luyện với tiền tố: "query: " cho câu hỏi và "passage: " cho đoạn tài liệu.
export async function nhungCauHoi(cauHoi: string): Promise<number[]> {
  return (await nhung([`query: ${cauHoi}`]))[0];
}

// Mặc định chỉ nhúng nội dung đoạn. Đặt KEM_TIEU_DE=1 để nhúng cả tiêu đề mục (xem so sánh trong bài).
export async function nhungCacDoan(cacDoan: Doan[]): Promise<number[][]> {
  const kemTieuDe = process.env.KEM_TIEU_DE === "1";
  return nhung(cacDoan.map((d) => `passage: ${kemTieuDe ? `${d.tieuDe}. ` : ""}${d.noiDung}`));
}

// Vector đã chuẩn hóa độ dài về 1, nên tích vô hướng chính là độ giống nhau (cosine similarity).
export function doGiong(a: number[], b: number[]): number {
  return a.reduce((tong, x, i) => tong + x * b[i], 0);
}

Có ba chi tiết bạn nên biết. Thứ nhất, pooling: "mean" gộp vector của từng từ thành một vector cho cả câu, còn normalize: true chuẩn hóa độ dài về 1, đó là lý do doGiong chỉ cần tích vô hướng. Thứ hai, tiền tố query: và passage: không phải trang trí. Theo trang mô hình của intfloat/multilingual-e5-small, mô hình được huấn luyện với các tiền tố này, và bỏ chúng đi thì chất lượng giảm. Mình chưa tự đo mức giảm là bao nhiêu, nên không dám nói con số. Thứ ba, trong file thu-nghia.ts ở trên mình dùng query: cho cả ba câu, vì ở đó mình so câu với câu chứ không phải câu hỏi với tài liệu.

Lần chạy đầu, thư viện sẽ tải mô hình khoảng 120 MB về máy, nên bạn cần có mạng đúng một lần đó. Những lần sau nó đọc từ cache.

Tìm đoạn chính sách theo nghĩa

Có hàm nhúng rồi, tìm theo nghĩa chỉ còn là ba việc. Nhúng từng đoạn chính sách một lần lúc khởi động và giữ lại, đó là "chỉ mục". Khi có câu hỏi, nhúng câu hỏi. Rồi xếp các đoạn theo độ giống với câu hỏi và lấy vài đoạn đầu:

src/tim-nghia.ts
import { doGiong, nhungCacDoan, nhungCauHoi } from "./nhung.ts";
import type { Doan } from "./tach-doan.ts";

export type KetQuaNghia = { doan: Doan; diem: number };

// Chỉ mục: vector của từng đoạn, tính một lần khi khởi động.
export async function lapChiMuc(cacDoan: Doan[]): Promise<number[][]> {
  return nhungCacDoan(cacDoan);
}

// Tìm theo nghĩa: đoạn nào có vector gần vector câu hỏi nhất thì đứng đầu.
export async function timTheoNghia(cauHoi: string, cacDoan: Doan[], chiMuc: number[][], soDoan = 2): Promise<KetQuaNghia[]> {
  const vectorHoi = await nhungCauHoi(cauHoi);
  return cacDoan
    .map((doan, i) => ({ doan, diem: doGiong(vectorHoi, chiMuc[i]) }))
    .sort((a, b) => b.diem - a.diem) // xếp theo điểm chưa làm tròn: các đoạn sát nhau, làm tròn trước sẽ gây hòa điểm
    .slice(0, soDoan);
}

Chú thích ở dòng sort là một lỗi mình gặp thật khi làm bài này. Lúc đầu mình làm tròn điểm tới ba chữ số trước khi xếp, và hai đoạn có điểm làm tròn cùng là 0,848 bị coi là hòa, nên thứ tự phụ thuộc vào cách sort xếp các phần tử bằng nhau. Một câu hỏi trong bộ mẫu vì thế lúc đạt lúc không. Vì các đoạn có điểm sát nhau đến vậy, bạn chỉ làm tròn khi in ra, đừng làm tròn khi xếp hạng.

Tìm theo nghĩa có thật sự tốt hơn không?

Đây là lúc dùng 12 câu hỏi mẫu. Mỗi câu có danh sách can các mục bắt buộc phải có mặt, và đây là toàn bộ bộ câu, bạn chép được nguyên:

src/cau-hoi-mau.ts
// Bộ câu hỏi để chấm bộ tìm kiếm. `can` là những mục chính sách phải có mặt thì mới đủ thông tin trả lời.
export type CauHoiMau = { cau: string; can: string[] };

export const CAU_HOI_MAU: CauHoiMau[] = [
  { cau: "Đơn từ bao nhiêu tiền thì được miễn phí giao hàng?", can: ["Phí giao hàng"] },
  { cau: "Quán giao sai món thì được hoàn tiền bao lâu?", can: ["Đổi món khi giao sai hoặc pha sai", "Hoàn tiền"] },
  { cau: "Em hủy đơn chuyển khoản rồi, bao lâu thì tiền về?", can: ["Hủy đơn", "Hoàn tiền"] },
  { cau: "Ly cà phê pha sai, em muốn quán đền tiền", can: ["Đổi món khi giao sai hoặc pha sai"] },
  { cau: "Mình bị dị ứng sữa thì uống gì được?", can: ["Dị ứng và thành phần"] },
  { cau: "Tết quán có bán không?", can: ["Giờ mở cửa và ngày lễ"] },
  { cau: "Nhà em cách quán 8 cây số, quán có ship không?", can: ["Phí giao hàng"] },
  { cau: "Em mang bình giữ nhiệt tới thì có được bớt không?", can: ["Ly tự mang"] },
  { cau: "Điểm thưởng dùng được trong bao lâu?", can: ["Tích điểm"] },
  { cau: "Em muốn đặt 30 ly cho công ty thì làm sao?", can: ["Đặt số lượng lớn"] },
  { cau: "Quán có đưa số điện thoại của em cho ai khác không?", can: ["Bảo mật thông tin khách"] },
  { cau: "Mã giảm giá dùng chung với điểm tích lũy được không?", can: ["Khuyến mãi và mã giảm giá"] },
];

Mỗi câu hỏi thuộc một loại. Năm câu đầu là những câu mình đã dùng ở bài trước. Các câu "Tết quán có bán không?", "cách quán 8 cây số" và "bình giữ nhiệt" cố ý không dùng đúng chữ của tài liệu (tài liệu viết "Tết Nguyên đán", "bán kính 5 km" và "ly tự mang"). Mấy câu còn lại dùng đúng từ, để chắc rằng cách mới không làm hỏng những gì cách cũ đang làm tốt.

File chấm điểm chạy hai kiểu tìm trên bộ câu hỏi này: từ khóa (timDoan của bài trước) và theo nghĩa (timTheoNghia). Nó nhận số đoạn được chọn làm đối số:

src/so-sanh.ts
// Chấm hai bộ tìm kiếm trên cùng bộ câu hỏi mẫu: từ khóa (tập 6) và theo nghĩa (embedding).
// Một câu "đủ" khi mọi mục trong `can` đều nằm trong số đoạn được chọn. Đối số đầu: số đoạn được chọn (mặc định 2).
import { CAU_HOI_MAU } from "./cau-hoi-mau.ts";
import { docChinhSach } from "./tach-doan.ts";
import { timDoan } from "./tim-doan.ts";
import { lapChiMuc, timTheoNghia } from "./tim-nghia.ts";

const SO_DOAN = Number(process.argv[2] ?? 2);
const CAC_DOAN = docChinhSach();
const chiMuc = await lapChiMuc(CAC_DOAN);

const dau = (du: boolean) => (du ? "đủ   " : "THIẾU");
let duTuKhoa = 0;
let duNghia = 0;

console.log(`Chọn ${SO_DOAN} đoạn cho mỗi câu hỏi.   từ khóa | theo nghĩa`);
for (const { cau, can } of CAU_HOI_MAU) {
  const tuKhoa = timDoan(cau, CAC_DOAN, SO_DOAN).map((kq) => kq.doan.tieuDe);
  const nghia = (await timTheoNghia(cau, CAC_DOAN, chiMuc, SO_DOAN)).map((kq) => kq.doan.tieuDe);
  const du = (chon: string[]) => can.every((muc) => chon.includes(muc));
  duTuKhoa += Number(du(tuKhoa));
  duNghia += Number(du(nghia));
  console.log(`${dau(du(tuKhoa))} | ${dau(du(nghia))}   ${cau}`);
}
console.log(`
Đủ thông tin: từ khóa ${duTuKhoa}/${CAU_HOI_MAU.length}, theo nghĩa ${duNghia}/${CAU_HOI_MAU.length}`);

Chạy với hai đoạn được chọn cho mỗi câu, giống bài trước:

npx tsx src/so-sanh.ts 2
Chọn 2 đoạn cho mỗi câu hỏi.   từ khóa | theo nghĩa
đủ    | đủ      Đơn từ bao nhiêu tiền thì được miễn phí giao hàng?
đủ    | đủ      Quán giao sai món thì được hoàn tiền bao lâu?
THIẾU | đủ      Em hủy đơn chuyển khoản rồi, bao lâu thì tiền về?
đủ    | đủ      Ly cà phê pha sai, em muốn quán đền tiền
đủ    | đủ      Mình bị dị ứng sữa thì uống gì được?
đủ    | đủ      Tết quán có bán không?
THIẾU | đủ      Nhà em cách quán 8 cây số, quán có ship không?
đủ    | đủ      Em mang bình giữ nhiệt tới thì có được bớt không?
THIẾU | đủ      Điểm thưởng dùng được trong bao lâu?
THIẾU | đủ      Em muốn đặt 30 ly cho công ty thì làm sao?
đủ    | đủ      Quán có đưa số điện thoại của em cho ai khác không?
đủ    | đủ      Mã giảm giá dùng chung với điểm tích lũy được không?

Đủ thông tin: từ khóa 8/12, theo nghĩa 12/12

Cách tìm theo từ khóa chỉ đủ thông tin ở 8 trên 12 câu, theo nghĩa được cả 12. Chú ý câu thứ ba, câu "tiền về" mà bài trước bị hụt mất con số ba ngày: tìm theo nghĩa lấy được đủ cả Hủy đơn và Hoàn tiền. Hai câu "8 cây số" và "đặt 30 ly" là kiểu khách không dùng từ nào của tài liệu, và đó đúng là chỗ cách đếm từ bó tay.

Mình thử thêm với ba đoạn thay vì hai (npx tsx src/so-sanh.ts 3). Tìm theo từ khóa khá lên, từ 8 lên 11 trên 12, vì có thêm cơ hội trúng. Tìm theo nghĩa vẫn 12 trên 12. Chỉ còn câu "8 cây số" là cách đếm từ không bao giờ bắt được, dù lấy ba đoạn. Cái giá của việc lấy thêm đoạn là prompt dài hơn và có thêm đoạn nhiễu, nên mình không lạm dụng nó để vá một bộ tìm kiếm yếu.

Nên nhúng cả tiêu đề mục không?

Khi viết nhungCacDoan, mình nghĩ đã có tiêu đề mục thì nên đưa vào luôn, vì tiêu đề như "Hoàn tiền" cho mô hình biết đoạn nói về gì. Mình đã đo thử bằng biến môi trường KEM_TIEU_DE:

KEM_TIEU_DE=1 npx tsx src/so-sanh.ts 2 (rồi chạy lại với đối số 3)
Đủ thông tin: từ khóa 8/12, theo nghĩa 10/12
Đủ thông tin: từ khóa 11/12, theo nghĩa 12/12

Nhúng cả tiêu đề thì khi chọn hai đoạn, tìm theo nghĩa còn 10 trên 12 thay vì 12 trên 12. Khi chọn ba đoạn thì hai cách bằng nhau. Mình không biết chắc vì sao. Phỏng đoán của mình là câu hỏi của khách thường nói về một tình huống, còn tiêu đề là một nhãn ngắn, và nhãn kéo vector của đoạn về phía từ chung chung. Mình chưa kiểm chứng phỏng đoán này. Điều chắc chắn là bộ 12 câu cho thấy giả định ban đầu của mình sai, hoặc ít nhất là không đúng với tài liệu này. Nếu mình không có bộ câu hỏi, mình đã nhúng tiêu đề và không bao giờ biết. Đó là lý do mình nghĩ bạn nên có bộ câu hỏi mẫu trước khi tinh chỉnh bất cứ thứ gì trong RAG.

Với 12 câu hỏi, chênh nhau hai câu vẫn có thể là tình cờ. Hãy coi đây là lý do để thử trên tài liệu của bạn, đừng coi đó là một quy tắc.

Nối vào chatbot và nhìn xem nó gửi gì

Cuối cùng, mình thay bước tìm trong chương trình của bài trước bằng tìm theo nghĩa, lấy ba đoạn:

src/chay.ts
// RAG đầy đủ, bản tìm theo nghĩa: nhúng câu hỏi, lấy 3 đoạn gần nhất, ghép vào prompt, hỏi mô hình.
import { docChinhSach } from "./tach-doan.ts";
import { lapChiMuc, timTheoNghia } from "./tim-nghia.ts";
import { ghepCauHoi, traLoi } from "./tra-loi.ts";

const CAC_DOAN = docChinhSach();
const CAU_HOI = process.argv.slice(2).length ? [process.argv.slice(2).join(" ")] : [
  "Em hủy đơn chuyển khoản rồi, bao lâu thì tiền về?",
  "Ly cà phê pha sai, em muốn quán đền tiền",
];

const chiMuc = await lapChiMuc(CAC_DOAN);
for (const cau of CAU_HOI) {
  const ketQua = await timTheoNghia(cau, CAC_DOAN, chiMuc, 3);
  const duocChon = ketQua.map((kq) => kq.doan);
  console.log(`Khách: ${cau}`);
  console.log(`  gửi kèm: ${ketQua.map((kq) => `${kq.doan.tieuDe} (${kq.diem.toFixed(3)})`).join(" + ")}`);
  console.log(`  ${ghepCauHoi(cau, duocChon).length} ký tự`);
  console.log(`Mộc: ${await traLoi(cau, duocChon)}\n`);
}
npx tsx src/chay.ts
Khách: Em hủy đơn chuyển khoản rồi, bao lâu thì tiền về?
  gửi kèm: Hủy đơn (0.864) + Hoàn tiền (0.848) + Đặt hàng và thanh toán (0.848)
  852 ký tự
Mộc: Dạ, Đơn chuyển khoản hủy hợp lệ sẽ được hoàn lại đủ số tiền, theo mục Hoàn tiền. Đơn chuyển khoản được pha sau khi quán thấy tiền về tài khoản, thường mất dưới hai phút. [Hủy đơn; Đặt hàng và thanh toán]

Khách: Ly cà phê pha sai, em muốn quán đền tiền
  gửi kèm: Ly tự mang (0.856) + Đổi món khi giao sai hoặc pha sai (0.848) + Dị ứng và thành phần (0.846)
  819 ký tự
Mộc: Dạ, Nếu quán giao sai món hoặc pha sai so với đơn, khách nhắn trong vòng 30 phút kể từ lúc nhận, kèm ảnh món đã giao. Quán sẽ pha lại món đúng và giao lại miễn phí, hoặc hoàn tiền nếu khách không muốn đợi. [Đổi món khi giao sai hoặc pha sai]

Hãy nhìn câu trả lời đầu. Đoạn Hoàn tiền, chứa con số "ba ngày làm việc", đã có trong ba đoạn được gửi đi. Nhưng câu trả lời vẫn không có con số đó. Lý do không nằm ở bước tìm. Mô hình giả lập mà mình dùng từ bài trước, vốn chỉ chọn câu có nhiều từ trùng nhất rồi đọc lại, nên không biết "tiền về" tức là hỏi chuyện hoàn tiền. Mình chưa chạy chế độ gọi Claude thật cho bài này, vì vậy mình không khẳng định một mô hình thật sẽ trả lời đúng, mình chỉ biết rằng nó sẽ có đủ thông tin trong tay. Bạn có ANTHROPIC_API_KEY thì chạy lại lệnh trên và so sánh nhé.

Câu thứ hai cho thấy một chuyện khác. Mục Đổi món khi giao sai hoặc pha sai, đoạn đúng cần tìm, chỉ đứng thứ hai, sau Ly tự mang. Điểm của ba đoạn là 0,856, 0,848 và 0,846. Chênh nhau chưa tới 0,01. Với khoảng cách như vậy, mình không tin chỗ nào bạn lấy đúng một đoạn: lấy ba đoạn thì chắc chắn hơn nhiều. Cái giá là đoạn Ly tự mang và Dị ứng và thành phần đi theo như hai đoạn nhiễu, và đó là lý do prompt này dài 819 ký tự thay vì khoảng 600.

Khi nào nên dùng embedding?

Theo mình, đáng dùng khi khách hàng của bạn không viết theo văn phong của tài liệu, tức là hầu hết các trường hợp có người thật hỏi. Còn nếu tài liệu ngắn, bạn vẫn có thể gửi cả tài liệu vào prompt như mình đã nói ở bài trước, và khỏi gánh thêm một bộ phận.

Một vài điều thực tế nếu bạn làm với tài liệu thật:

  • 14 đoạn thì nhúng lúc khởi động là xong nhanh. Với hàng nghìn đoạn thì bạn lưu vector vào database (Postgres có phần mở rộng pgvector chẳng hạn) thay vì tính lại mỗi lần chạy.
  • Mô hình nhỏ chạy trên máy dễ thử và miễn phí, nhưng chưa chắc tốt nhất cho tiếng Việt. Nếu muốn chất lượng cao hơn, bạn đổi sang mô hình lớn hơn hoặc sang dịch vụ qua API như Voyage AI mà tài liệu của Anthropic nhắc tới. Phần còn lại của code không đổi, vì chỉ có nhung.ts biết mô hình nào đang chạy.
  • Mô hình e5 cắt đầu vào ở khoảng 512 token. Đoạn chính sách của Mộc ngắn nên không sao. Một mục dài vài trang thì bạn cần tách nhỏ hơn trước khi nhúng.

Công cụ của agent vẫn nằm trong một ứng dụng

Chatbot của Mộc giờ tìm đúng chính sách theo nghĩa, và nó đã có công cụ tra đơn, thêm món, cùng bước tìm chính sách. Nhưng cả ba công cụ đó đang nằm thẳng trong code của chatbot. Nếu quán muốn đưa đúng những công cụ này vào một ứng dụng khác, ví dụ một trợ lý cho nhân viên tại quầy, thì phải chép code sang và giữ hai bản đồng bộ. Cách đóng gói công cụ để nhiều ứng dụng dùng chung là việc của các bài viết sau.

Bạn theo dõi cả hành trình ở Xây AI agent đầu tiên. Nếu đang ôn phỏng vấn, các câu hỏi về AI agent có trong ngân hàng câu hỏi phỏng vấn.

Nhận bài tiếp theo qua email.

Một email mỗi tuần, chỉ khi có điều đáng đọc. Không spam.