Graph RAG
This content is not available in your language yet.
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu vì sao câu hỏi cần nối nhiều sự kiện làm khó RAG vector, và đồ thị tri thức (knowledge graph) giúp ở chỗ nào.
- Biểu diễn được tri thức bằng bộ ba (triple) chủ thể – quan hệ – đối tượng, và viết được prompt nhờ LLM trích bộ ba ra JSON có kiểm tra.
- Lưu được bộ ba vào Neo4j và viết được truy vấn Cypher một bước, hai bước, và độ dài thay đổi
{1,3}. - Nhận ra ý tưởng của GraphRAG (Microsoft): tóm tắt theo cộng đồng cho câu hỏi về cả kho, và biết khi nào RAG vector vẫn là lựa chọn tốt hơn.
Cần biết trước
Phần tiêu đề “Cần biết trước”- RAG and Modern Chatbots, in Large Language Models(Not translated yet) (Learned)
Trực giác
Phần tiêu đề “Trực giác”Bạn làm chatbot cho một trang tin khởi nghiệp. Kho có vài nghìn bài báo ngắn. Ba đoạn trong đó:
- “Chị Lan, cựu sinh viên ĐH Bách khoa, sáng lập Mây Xanh năm 2021.”
- “Quỹ Sao Mai vừa rót vốn vào Mây Xanh, công ty có trụ sở tại Đà Nẵng.”
- “Lúa Vàng ở Cần Thơ, do anh Minh sáng lập, công bố nhận vốn từ Quỹ Sao Mai.”
Câu hỏi “Ai sáng lập Mây Xanh?” thì dễ. Đoạn thứ nhất giống câu hỏi, RAG vector lấy nó ở top-1, xong.
Còn câu “Những công ty nào có chung nhà đầu tư với Mây Xanh?” thì khác. Câu trả lời là Lúa Vàng, nằm ở đoạn thứ ba. Nhưng đoạn thứ ba không nhắc tới Mây Xanh chữ nào. Muốn trả lời, phải đi hai bước: từ Mây Xanh tới Quỹ Sao Mai (đoạn hai), rồi từ Quỹ Sao Mai tới Lúa Vàng (đoạn ba). RAG vector chấm từng đoạn riêng lẻ theo độ giống câu hỏi, nên đoạn ba dễ bị xếp thấp, và mô hình chỉ thấy một nửa câu chuyện.
Hãy nghĩ tới bảng điều tra trong phim trinh thám. Thám tử ghim ảnh người, công ty, địa điểm lên bảng, rồi căng dây đỏ giữa chúng, mỗi sợi có một nhãn: “sáng lập”, “rót vốn”, “trụ sở ở”. Câu hỏi khó lúc này thành việc lần theo dây: từ ảnh Mây Xanh, theo dây “rót vốn” ngược về Quỹ Sao Mai, rồi theo một sợi “rót vốn” khác sang Lúa Vàng. Không cần đoạn văn nào nhắc cả hai công ty cùng lúc.
Graph RAG dựng chính tấm bảng đó từ văn bản, với LLM làm người đọc hồ sơ và căng dây giúp bạn. Bài toán của bài: biến kho văn bản thành một đồ thị gồm thực thể và quan hệ, rồi trả lời câu hỏi bằng cách đi theo các cạnh của đồ thị, nhiều bước khi cần, thay vì chỉ tìm đoạn văn giống câu hỏi.
Lý thuyết
Phần tiêu đề “Lý thuyết”Đồ thị tri thức: thực thể, quan hệ, bộ ba
Phần tiêu đề “Đồ thị tri thức: thực thể, quan hệ, bộ ba”Chữ “đồ thị” ở đây là graph: một mạng gồm nút (node) và cạnh (edge), không phải biểu đồ (chart) vẽ số liệu.
- Mỗi nút là một thực thể (entity): người, công ty, quỹ, thành phố.
- Mỗi cạnh là một quan hệ (relation) giữa hai thực thể. Cạnh có hướng và có nhãn: “Lan sáng lập Mây Xanh” khác “Mây Xanh sáng lập Lan”.
Một sự kiện được ghi thành một bộ ba : là chủ thể (subject), là quan hệ (predicate), là đối tượng (object). Ví dụ (Lan, FOUNDED, Mây Xanh) đọc là “Lan sáng lập Mây Xanh”, cạnh đi từ Lan tới Mây Xanh. Cả đồ thị tri thức chỉ là một tập bộ ba. Từ ba đoạn văn ở phần Trực giác, cộng thêm vài câu về anh Minh, ta được 8 bộ ba dùng suốt bài:
| Chủ thể | Quan hệ | Đối tượng | Đọc là |
|---|---|---|---|
| Lan | FOUNDED | Mây Xanh | Lan sáng lập Mây Xanh |
| Minh | FOUNDED | Lúa Vàng | Minh sáng lập Lúa Vàng |
| Quỹ Sao Mai | INVESTED_IN | Mây Xanh | Quỹ Sao Mai đầu tư vào Mây Xanh |
| Quỹ Sao Mai | INVESTED_IN | Lúa Vàng | Quỹ Sao Mai đầu tư vào Lúa Vàng |
| Lan | STUDIED_AT | ĐH Bách khoa | Lan học ở ĐH Bách khoa |
| Minh | STUDIED_AT | ĐH Bách khoa | Minh học ở ĐH Bách khoa |
| Mây Xanh | BASED_IN | Đà Nẵng | Mây Xanh có trụ sở ở Đà Nẵng |
| Lúa Vàng | BASED_IN | Cần Thơ | Lúa Vàng có trụ sở ở Cần Thơ |
Điều quan trọng nhất: hai bộ ba chung một thực thể thì tự nối với nhau. Bộ ba thứ ba và thứ tư lấy từ hai bài báo khác nhau, nhưng cùng có nút Quỹ Sao Mai, nên trong đồ thị chúng thành một con đường Mây Xanh – Quỹ Sao Mai – Lúa Vàng. Tri thức rải rác ở nhiều đoạn được ghép lại qua nút chung.
Tên thực thể giữ tiếng Việt. Tên quan hệ viết tiếng Anh, chữ hoa, nối bằng gạch dưới, theo thói quen của Cypher (ngôn ngữ truy vấn ở phần sau). Tên chỉ gồm chữ, số và gạch dưới (không có khoảng trắng) thì không phải bọc trong dấu nháy ngược; bài dùng tiếng Anh chữ hoa vì đó là quy ước quen thuộc của Cypher. Danh sách các loại quan hệ (và loại thực thể) được phép gọi là lược đồ (schema). Có lược đồ thì cùng một ý luôn được ghi bằng cùng một nhãn, thay vì lúc FOUNDED, lúc CO_FOUNDER_OF.
Từ văn bản tới bộ ba: nhờ LLM trích
Phần tiêu đề “Từ văn bản tới bộ ba: nhờ LLM trích”Ai viết ra 8 bộ ba kia? Với vài nghìn bài báo, ta nhờ LLM. Quy trình giống bước lập chỉ mục của RAG: chia văn bản thành chunk, rồi với mỗi chunk gọi mô hình chat một lần. Prompt dùng lại ba ý của bài Prompt Engineering:
- Chỉ dẫn rõ, kèm lược đồ: chỉ được dùng các quan hệ
FOUNDED,INVESTED_IN,STUDIED_AT,BASED_IN; chỉ ghi điều văn bản nói rõ, không suy đoán. - Một ví dụ mẫu (few-shot): một câu ngắn và đúng JSON mong muốn cho câu đó.
- Định dạng đầu ra: chỉ một đối tượng JSON, có thể bật thêm JSON mode như bài Sử dụng API văn bản.
Với đoạn “Quỹ Sao Mai vừa rót vốn vào Mây Xanh, công ty có trụ sở tại Đà Nẵng.”, đầu ra mong muốn là:
{ "triples": [ {"subject": "Quỹ Sao Mai", "relation": "INVESTED_IN", "object": "Mây Xanh"}, {"subject": "Mây Xanh", "relation": "BASED_IN", "object": "Đà Nẵng"} ]}Đầu ra của mô hình vẫn chỉ là chữ, nên chương trình phải kiểm trước khi lưu: JSON có đọc được không, quan hệ có nằm trong lược đồ không, tên có rỗng không. Bộ ba sai lược đồ thì bỏ, hoặc gửi lại kèm lời nhắc. Nên lưu kèm mỗi bộ ba mã chunk nguồn của nó: khi nghi ngờ, bạn mở lại đoạn gốc, và chatbot trích được nguồn như ở bài RAG.
Gộp thực thể (entity resolution) là bước khó nhất khi làm thật. “Mây Xanh”, “công ty Mây Xanh”, “Cty Mây Xanh” là một công ty, nhưng nếu lưu thành ba nút thì đồ thị đứt: đi từ Lan không bao giờ tới Đà Nẵng. Cách làm thường gặp, từ rẻ tới đắt: chuẩn hóa chuỗi (bỏ khoảng trắng thừa, bỏ chức danh như “chị”, “ông”), một bảng tên gọi khác do người viết, nhờ LLM hay embedding gợi ý các cặp tên có thể trùng rồi người duyệt lại.
Cái giá của bước này: RAG vector lập chỉ mục chỉ bằng các lần gọi embedding, còn Graph RAG gọi mô hình chat cho từng chunk. Kho lớn thì lập chỉ mục chậm và tốn hơn hẳn, và tài liệu đổi thì phải trích lại phần đổi.
Lưu và truy vấn bằng Neo4j
Phần tiêu đề “Lưu và truy vấn bằng Neo4j”Bạn lưu bộ ba vào đâu cũng được: một dict Python, một bảng SQL. Nhưng khi đồ thị lớn và cần hỏi nhiều kiểu, người ta dùng cơ sở dữ liệu đồ thị (graph database). Neo4j là một hệ phổ biến, có bản Community miễn phí chạy được trong Docker. Ngôn ngữ truy vấn của nó là Cypher, viết mẫu cần tìm như vẽ bằng ký tự:
- Nút:
(c:Entity {name: 'Mây Xanh'}).clà tên biến,Entitylà nhãn của nút, phần trong ngoặc nhọn là thuộc tính. - Cạnh:
-[:FOUNDED]->, mũi tên chỉ hướng. Bỏ mũi tên (-[:FOUNDED]-) là chấp nhận cả hai chiều.
Tạo bộ ba bằng MERGE: tìm mẫu, chưa có thì tạo. Chú ý: MERGE xét cả mẫu một lượt, không khớp cả mẫu thì tạo cả mẫu, kể cả những nút đã có, vì thế ô code MERGE từng nút trước rồi mới MERGE cạnh. CREATE thì luôn tạo mới, chạy hai lần là có hai nút “Lan”.
MERGE (lan:Entity {name: 'Lan'})MERGE (mx:Entity {name: 'Mây Xanh'})MERGE (lan)-[:FOUNDED]->(mx)Một bước: ai sáng lập Mây Xanh? Mẫu đi từ một nút bất kỳ p theo cạnh FOUNDED tới Mây Xanh.
MATCH (p)-[:FOUNDED]->(c:Entity {name: 'Mây Xanh'})RETURN p.nameKết quả: "Lan".
Hai bước: công ty nào có chung nhà đầu tư với Mây Xanh? Mẫu vẽ đúng con đường ở phần Trực giác: từ Mây Xanh, theo cạnh INVESTED_IN ngược về quỹ f, rồi theo một cạnh INVESTED_IN khác đi xuôi tới công ty other.
MATCH (c:Entity {name: 'Mây Xanh'})<-[:INVESTED_IN]-(f)-[:INVESTED_IN]->(other)RETURN f.name AS quy, other.name AS cong_tyKết quả: "Quỹ Sao Mai", "Lúa Vàng". Mây Xanh không tự xuất hiện ở cột cong_ty, vì trong một mẫu, Cypher không cho đi lại cùng một cạnh hai lần.
Độ dài thay đổi: mọi thực thể cách Mây Xanh từ 1 tới 3 bước, theo cạnh loại nào và chiều nào cũng được.
MATCH (c:Entity {name: 'Mây Xanh'})-[]-{1,3}(n)RETURN DISTINCT n.name AS ten{1,3} đặt sau một cạnh nghĩa là “lặp cạnh này từ 1 tới 3 lần”. Đây là cú pháp mới, theo chuẩn GQL. Tài liệu và ví dụ cũ trên mạng viết -[*1..3]-. Neo4j Community 2026.10 vẫn chạy cả hai và cho cùng kết quả (7 thực thể), nhưng phiên bản Neo4j cũ hơn có thể không hiểu {1,3}: ví dụ nào báo lỗi cú pháp thì kiểm phiên bản trước.
Giá trị từ người dùng đi qua tham số. Trong code, viết {name: $name} và truyền name="Mây Xanh" riêng, đừng nối chuỗi. Nối chuỗi thì một cái tên chứa dấu nháy và vài lệnh Cypher có thể biến thành lệnh xóa dữ liệu, giống prompt injection nhưng ở tầng cơ sở dữ liệu. Có một chỗ không viết $r được như tên: kiểu quan hệ trong mẫu -[:FOUNDED]->. Ô code Neo4j của bài ghép kiểu quan hệ vào chuỗi, nhưng chỉ sau khi kiểm nó nằm trong lược đồ. (Neo4j bản mới còn có cú pháp kiểu động $(...) cho chỗ này, được giới thiệu chính để giảm rủi ro tiêm Cypher, nhưng tài liệu Neo4j lưu ý truy vấn dùng giá trị động có thể chạy chậm hơn truy vấn tĩnh.)
Truy vấn nhiều bước là duyệt đồ thị
Phần tiêu đề “Truy vấn nhiều bước là duyệt đồ thị”Bên dưới {1,3} là một phép duyệt đồ thị. Cách tự nhiên nhất là BFS (breadth-first search, tìm kiếm theo chiều rộng): đi từ nút xuất phát ra mọi láng giềng (bước 1), từ các nút đó ra láng giềng chưa gặp (bước 2), cứ thế từng lớp một, và dừng ở bước. Vì đi theo lớp, lần đầu gặp một nút cũng là lúc có đường đi ngắn nhất tới nó. Ô code chạy trên trang cài đúng thuật toán này bằng một dict và một hàng đợi.
Số nút tìm được tăng rất nhanh theo . Giả sử mỗi nút có khoảng láng giềng. Bước 1 gặp tối đa nút; mỗi nút đó lại mở ra nút nữa, nên bước 2 gặp tối đa ; cứ thế, sau bước có tối đa
nút. Với và , đó là nút: dán hết vào prompt thì không vừa, và phần lớn chẳng liên quan. Vì vậy Graph RAG nên giới hạn số bước (thường 1–2), giới hạn số bộ ba đưa vào prompt, và lọc theo loại quan hệ như truy vấn hai bước ở trên, thay vì đi theo mọi cạnh.
Graph RAG từ đầu tới cuối
Phần tiêu đề “Graph RAG từ đầu tới cuối”Ghép lại, một hệ Graph RAG ở cấp ứng dụng thường chạy như sau:
- Lập chỉ mục (một lần): chia chunk, nhờ LLM trích bộ ba, kiểm, gộp thực thể, lưu vào đồ thị kèm chunk nguồn.
- Nhận ra thực thể trong câu hỏi: nhờ LLM liệt kê tên riêng trong câu hỏi, rồi khớp với tên nút (so chuỗi, hoặc embedding tên nút).
- Duyệt đồ thị quanh các thực thể đó, 1–2 bước, lọc theo loại quan hệ khi biết.
- Biến bộ ba thành chữ: mỗi bộ ba một dòng, ví dụ
Quỹ Sao Mai INVESTED_IN Lúa Vàng, kèm chunk nguồn nếu cần. - Ghép vào prompt và gọi mô hình chat, đúng như RAG thường: chỉ dẫn, ngữ cảnh đánh số, câu hỏi.
So với pipeline ở bài RAG và chatbot hiện đại, chỉ bước truy xuất đổi: thay “tìm top- theo cosine” bằng “duyệt đồ thị”. Hai cách không loại trừ nhau. Nhiều hệ dùng cả hai: vector tìm đoạn văn liên quan, đồ thị bổ sung các mối nối mà đoạn văn không nói cùng một chỗ.
Hình dưới nhắc lại pipeline RAG vector của bài trước. Ở Graph RAG, hãy đọc khối “Kho vectơ” thành đồ thị, và khối “Tìm top-k (cosine)” thành bước duyệt đồ thị quanh các thực thể của câu hỏi; phần ghép prompt và gọi mô hình chat giữ nguyên.
Có một cách khác cho bước 3: nhờ LLM viết luôn truy vấn Cypher từ câu hỏi. Cách này linh hoạt, nhưng chạy thẳng một truy vấn do mô hình viết là mạo hiểm. Nếu làm, hãy chạy truy vấn ở chế độ chỉ đọc (driver Python: routing_=RoutingControl.READ, máy chủ sẽ từ chối lệnh ghi), đặt giới hạn thời gian chạy, và trên Neo4j Enterprise thì dùng tài khoản chỉ có quyền đọc.
GraphRAG của Microsoft: câu hỏi về cả kho
Phần tiêu đề “GraphRAG của Microsoft: câu hỏi về cả kho”Có một loại câu hỏi mà cả RAG vector lẫn duyệt quanh một thực thể đều bó tay: “Các chủ đề chính trong kho tài liệu này là gì?”. Không đoạn nào chứa sẵn câu trả lời, và câu hỏi chẳng nêu thực thể nào để bắt đầu. Lấy top- đoạn thì giống hỏi ý kiến cả lớp mà chỉ nghe ba bạn bàn đầu.
Edge và cộng sự (2024) đề xuất GraphRAG cho loại câu hỏi toàn cục (global) này. Tên giống “Graph RAG”, nhưng đây là tên một cách làm cụ thể và một dự án mã nguồn mở của Microsoft. Ý chính:
- Lúc lập chỉ mục: LLM trích thực thể và quan hệ từ văn bản, dựng đồ thị. Đồ thị được chia thành các cộng đồng (community): những cụm thực thể nối với nhau chặt hơn với phần còn lại. Tài liệu của dự án ghi là dùng thuật toán Leiden, chia theo nhiều cấp. LLM viết tóm tắt cho từng cộng đồng, làm sẵn một lần.
- Lúc hỏi: mỗi bản tóm tắt cộng đồng cho ra một câu trả lời từng phần, rồi các câu trả lời từng phần được gộp thành câu trả lời cuối. Trở lại ví dụ lớp học: mỗi nhóm thảo luận riêng rồi cử người phát biểu, sau đó mới tổng hợp.
Paper báo cáo GraphRAG trả lời đầy đủ hơn và đa dạng hơn so với một RAG vector nền, trên các câu hỏi toàn cục với kho cỡ một triệu token. Đó là kết quả cho đúng loại câu hỏi ấy, không phải “GraphRAG tốt hơn RAG mọi lúc”. Tài liệu của dự án tách các chế độ hỏi: global search dùng tóm tắt cộng đồng cho câu hỏi về cả kho; local search đi từ một thực thể ra láng giềng của nó, gần với cách làm ở mục trước; ngoài ra còn DRIFT search (local search kèm thông tin cộng đồng) và chế độ tìm vector cơ bản cho câu hỏi một chỗ.
Giới hạn
Phần tiêu đề “Giới hạn”- Chất lượng đồ thị bằng chất lượng bước trích. LLM có thể bịa quan hệ, đặt ngược hướng, hoặc tách một thực thể thành nhiều nút. Hộp thứ hai ở mục Lỗi hay gặp nói cách kiểm.
- Lập chỉ mục tốn kém, như đã nói: mỗi chunk một lần gọi mô hình chat, và với GraphRAG còn thêm các lần viết tóm tắt cộng đồng.
- Lược đồ phải bảo trì. Thêm loại câu hỏi mới có thể cần loại quan hệ mới, tức là trích lại.
- Học trên đồ thị là một hướng khác. Mạng tích chập đồ thị (Graph Convolutional Network, GCN) của Kipf và Welling (2017) cập nhật vectơ của mỗi nút từ vectơ của các láng giềng, tạo embedding cho nút. GCN cần huấn luyện; Graph RAG trong bài thì không. Bài chỉ nhắc tên để bạn khỏi nhầm hai thứ.
Minh họa
Phần tiêu đề “Minh họa”Bảng dưới đặt bốn câu hỏi cạnh con đường trả lời chúng trong đồ thị 8 bộ ba. Hãy nhìn cột “Số bước”: càng nhiều bước, các bộ ba trên đường đi càng dễ nằm rải ở nhiều đoạn văn (hàng 3 cần hai bài báo khác nhau), và tìm đoạn theo độ giống câu hỏi càng dễ hụt.
| Câu hỏi | Đường đi trong đồ thị | Số bước |
|---|---|---|
| Ai sáng lập Mây Xanh? | Mây Xanh <-[FOUNDED]- Lan | 1 |
| Người sáng lập Mây Xanh học trường nào? | Mây Xanh <-[FOUNDED]- Lan -[STUDIED_AT]-> ĐH Bách khoa | 2 |
| Công ty nào có chung nhà đầu tư với Mây Xanh? | Mây Xanh <-[INVESTED_IN]- Quỹ Sao Mai -[INVESTED_IN]-> Lúa Vàng | 2 |
| Ai học cùng trường với người sáng lập Mây Xanh? | Mây Xanh <-[FOUNDED]- Lan -[STUDIED_AT]-> ĐH Bách khoa <-[STUDIED_AT]- Minh | 3 |
Bảng thứ hai so sánh ba cách làm. Đọc theo hàng “Mạnh ở” trước: mỗi cách hợp với một loại câu hỏi.
| Tiêu chí | RAG vector | Graph RAG (duyệt đồ thị) | GraphRAG (tóm tắt cộng đồng) |
|---|---|---|---|
| Lập chỉ mục | Chia chunk, gọi embedding | Thêm: LLM trích bộ ba cho từng chunk, gộp thực thể | Thêm nữa: chia cộng đồng, LLM viết tóm tắt cho từng cộng đồng |
| Truy xuất | Top- chunk theo cosine | Duyệt 1–2 bước quanh thực thể trong câu hỏi | Mỗi tóm tắt cho câu trả lời từng phần, rồi gộp |
| Mạnh ở | Câu hỏi một chỗ: “X quy định gì?” | Quan hệ, nhiều bước: “Ai có chung nhà đầu tư với X?” | Câu hỏi về cả kho: “Chủ đề chính là gì?” |
| Yếu ở | Câu trả lời rải ở nhiều đoạn | Phụ thuộc bước trích và lược đồ | Lập chỉ mục tốn nhiều lần gọi LLM nhất |
| Khi tài liệu đổi | Embed lại chunk đổi | Trích lại, gộp lại thực thể | Trích lại, có thể phải tóm tắt lại cộng đồng |
Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 10/2026, với Python 3.14, openai 3.37 và driver neo4j 6.4 trên Neo4j Community 2026.10.1; ô chạy trên trang chỉ dùng Python thuần. Ô thứ nhất chạy ngay trên trang. Ô thứ hai gọi API, ô thứ ba cần Neo4j, nên hai ô này chạy trên máy bạn.
Duyệt nhiều bước bằng BFS
Phần tiêu đề “Duyệt nhiều bước bằng BFS”Ô này dựng đồ thị từ 8 bộ ba của bài, không cần thư viện nào. Mỗi bộ ba được ghi vào danh sách kề ở cả hai đầu: ở chủ thể là cạnh đi ra -[R]->, ở đối tượng là cạnh đi vào <-[R]-. Nhờ vậy BFS đi được cả hai chiều, mà vẫn nhớ chiều thật của từng cạnh. Hàm bfs trả về đường đi ngắn nhất tới mọi thực thể trong tối đa max_hops bước.
Duyệt đồ thị tri thức nhiều bước bằng dict và BFS
# Duyệt đồ thị tri thức nhiều bước bằng dict và BFS, không cần thư viện.from collections import deque
TRIPLES = [ # (chủ thể, quan hệ, đối tượng) ("Lan", "FOUNDED", "Mây Xanh"), ("Minh", "FOUNDED", "Lúa Vàng"), ("Quỹ Sao Mai", "INVESTED_IN", "Mây Xanh"), ("Quỹ Sao Mai", "INVESTED_IN", "Lúa Vàng"), ("Lan", "STUDIED_AT", "ĐH Bách khoa"), ("Minh", "STUDIED_AT", "ĐH Bách khoa"), ("Mây Xanh", "BASED_IN", "Đà Nẵng"), ("Lúa Vàng", "BASED_IN", "Cần Thơ"),]
# Danh sách kề: mỗi cạnh được ghi ở cả hai đầu, kèm chiều của nógraph = {}for s, r, o in TRIPLES: graph.setdefault(s, []).append((f"-[{r}]->", o)) graph.setdefault(o, []).append((f"<-[{r}]-", s))
def bfs(start, max_hops): """Trả về {thực thể: đường đi ngắn nhất từ start}, tối đa max_hops.""" paths = {start: start} hops = {start: 0} queue = deque([start]) while queue: node = queue.popleft() if hops[node] == max_hops: continue # đủ số bước, không đi tiếp từ nút này for edge, nxt in graph[node]: if nxt not in hops: # nút chưa gặp: lần đầu gặp là ngắn nhất hops[nxt] = hops[node] + 1 paths[nxt] = f"{paths[node]} {edge} {nxt}" queue.append(nxt) return paths
for h in (1, 2, 3): print(f"hops <= {h}: {len(bfs('Mây Xanh', h)) - 1} entities")for path in list(bfs("Mây Xanh", 2).values())[1:]: print(path)The first run downloads Python, about 10 MB.
- Ba dòng đầu đếm số thực thể tới được từ Mây Xanh: 3 thực thể trong 1 bước, 5 trong 2 bước, 7 trong 3 bước. Đồ thị chỉ có 8 nút, nên 3 bước đã chạm mọi nút khác. Ô Neo4j ở dưới cũng đếm được 7 với
-[]-{1,3}. - Ba đường đi một bước (Lan, Quỹ Sao Mai, Đà Nẵng) trả lời được câu hỏi một bước như “Ai sáng lập Mây Xanh?”.
- Hai đường đi hai bước chính là hàng 2 và hàng 3 của bảng ở mục Minh họa: Lan
-[STUDIED_AT]->ĐH Bách khoa, và Quỹ Sao Mai-[INVESTED_IN]->Lúa Vàng.
Thử đổi bfs("Mây Xanh", 2) ở vòng for cuối thành bfs("Mây Xanh", 3): hai đường mới tới Minh và Cần Thơ hiện ra. Đường tới Minh là hàng cuối của bảng Minh họa, vì BFS gặp ĐH Bách khoa trước Lúa Vàng.
Trích bộ ba bằng LLM
Phần tiêu đề “Trích bộ ba bằng LLM”Ô này gửi cho mô hình prompt ở mục Lý thuyết: chỉ dẫn kèm lược đồ, một ví dụ mẫu (lượt user và assistant viết sẵn), rồi đoạn văn cần trích. Nó cần ba biến môi trường như ở bài Sử dụng API văn bản. Nhà cung cấp báo lỗi 400 với response_format thì xóa dòng đó: prompt vẫn đòi JSON, và json.loads vẫn kiểm.
Run on your machine
Trích bộ ba từ một đoạn văn, có kiểm lược đồ
- Install
pip install openai- Environment variables
OPENAI_BASE_URL(optional),OPENAI_API_KEY,LLM_MODEL
# Nhờ LLM đọc một đoạn văn và trả về các bộ ba dưới dạng JSON.import jsonimport os
from openai import OpenAI
client = OpenAI( base_url=os.environ.get("OPENAI_BASE_URL"), # bỏ trống: dùng OpenAI api_key=os.environ["OPENAI_API_KEY"],)MODEL = os.environ["LLM_MODEL"]
RELATIONS = ["FOUNDED", "INVESTED_IN", "STUDIED_AT", "BASED_IN"]SYSTEM = ( "Bạn trích bộ ba tri thức từ văn bản. Chỉ trả về một đối tượng JSON " 'dạng {"triples": [{"subject": "...", "relation": "...", ' '"object": "..."}]}. relation chỉ được là một trong: ' + ", ".join(RELATIONS) + ". Chỉ ghi điều văn bản nói rõ, không suy đoán. Tên công ty, " "tổ chức ghi đúng như văn bản, bỏ chức danh như ông, bà, chị.")EXAMPLE_IN = "Ông Tùng sáng lập Gió Nam, một công ty ở Huế."EXAMPLE_OUT = json.dumps({"triples": [ {"subject": "Tùng", "relation": "FOUNDED", "object": "Gió Nam"}, {"subject": "Gió Nam", "relation": "BASED_IN", "object": "Huế"},]}, ensure_ascii=False)TEXT = ( "Chị Lan tốt nghiệp ĐH Bách khoa rồi sáng lập Mây Xanh năm 2021. " "Công ty đặt trụ sở ở Đà Nẵng và vừa nhận vốn từ Quỹ Sao Mai.")
response = client.chat.completions.create( model=MODEL, messages=[ {"role": "system", "content": SYSTEM}, {"role": "user", "content": EXAMPLE_IN}, # một ví dụ mẫu {"role": "assistant", "content": EXAMPLE_OUT}, {"role": "user", "content": TEXT}, ], temperature=0, # trích xuất: cần ổn định response_format={"type": "json_object"}, # JSON mode; lỗi 400 thì xóa)text = response.choices[0].message.contenttry: items = json.loads(text)["triples"]except (json.JSONDecodeError, KeyError, TypeError): items = Noneif not isinstance(items, list): print("Không đọc được JSON:", text) items = []for item in items: # không tin đầu ra: kiểm từng bộ ba if not isinstance(item, dict) or item.get("relation") not in RELATIONS: print("Bỏ qua (sai lược đồ):", item) continue s = " ".join(str(item.get("subject", "")).split()) # bỏ khoảng trắng thừa o = " ".join(str(item.get("object", "")).split()) if s and o: print((s, item["relation"], o)) else: print("Bỏ qua (tên rỗng):", item)Chương trình in mỗi bộ ba hợp lệ thành một tuple, in “Bỏ qua (sai lược đồ)” cho bộ ba có quan hệ ngoài danh sách, và “Bỏ qua (tên rỗng)” cho bộ ba thiếu chủ thể hoặc đối tượng. Nếu mô hình không trả JSON, hoặc triples không phải một danh sách, nó in “Không đọc được JSON” kèm chuỗi nhận được. Đoạn văn được chọn để có hai chỗ đáng xem:
- “tốt nghiệp ĐH Bách khoa” không có chữ “học”, nên mô hình phải tự ánh xạ sang
STUDIED_AT, hoặc bỏ qua. Cả hai đều có thể xảy ra. - “nhận vốn từ Quỹ Sao Mai” viết theo góc nhìn của Mây Xanh. Bộ ba đúng hướng là (Quỹ Sao Mai,
INVESTED_IN, Mây Xanh). Nếu bạn thấy (Mây Xanh,INVESTED_IN, Quỹ Sao Mai), mô hình đã đặt ngược hướng, và bước kiểm lược đồ không bắt được lỗi này.
Lưu vào Neo4j và hỏi bằng Cypher
Phần tiêu đề “Lưu vào Neo4j và hỏi bằng Cypher”Chạy Neo4j Community trong Docker (xem bài Docker và Cloud nếu bạn mới dùng Docker). Mật khẩu đặt bằng biến môi trường, dùng chung cho lệnh docker run và cho code Python; Neo4j đòi mật khẩu ít nhất 8 ký tự. 127.0.0.1: trước mỗi cổng giữ Neo4j chỉ mở cho máy bạn.
export NEO4J_PASSWORD='<mật khẩu, ít nhất 8 ký tự>'docker run --rm --name neo4j-atlas \ -p 127.0.0.1:7474:7474 -p 127.0.0.1:7687:7687 \ -e NEO4J_AUTH="neo4j/$NEO4J_PASSWORD" \ neo4j:community$env:NEO4J_PASSWORD = '<mật khẩu, ít nhất 8 ký tự>'docker run --rm --name neo4j-atlas ` -p 127.0.0.1:7474:7474 -p 127.0.0.1:7687:7687 ` -e NEO4J_AUTH="neo4j/$env:NEO4J_PASSWORD" ` neo4j:communityKhi log hiện dòng Started., mở cửa sổ dòng lệnh thứ hai, đặt lại NEO4J_PASSWORD ở đó, rồi chạy ô dưới. Cổng 7687 dành cho driver; cổng 7474 mở Neo4j Browser tại http://localhost:7474, nơi bạn dán được các khối Cypher ở mục Lý thuyết và xem đồ thị dưới dạng hình. Có --rm và không gắn ổ đĩa (volume), nên dừng container là mất dữ liệu: đủ cho việc học, không dùng cho dữ liệu thật.
Run on your machine
Ghi 8 bộ ba vào Neo4j, hỏi hai bước và nhiều bước
- Install
pip install neo4j- Environment variables
NEO4J_URI(optional),NEO4J_PASSWORD
# Ghi bộ ba vào Neo4j, rồi hỏi hai bước và nhiều bước bằng Cypher.import os
from neo4j import GraphDatabase
URI = os.environ.get("NEO4J_URI", "neo4j://127.0.0.1:7687")AUTH = ("neo4j", os.environ["NEO4J_PASSWORD"])
TRIPLES = [ # cùng 8 bộ ba với ô chạy trên trang ("Lan", "FOUNDED", "Mây Xanh"), ("Minh", "FOUNDED", "Lúa Vàng"), ("Quỹ Sao Mai", "INVESTED_IN", "Mây Xanh"), ("Quỹ Sao Mai", "INVESTED_IN", "Lúa Vàng"), ("Lan", "STUDIED_AT", "ĐH Bách khoa"), ("Minh", "STUDIED_AT", "ĐH Bách khoa"), ("Mây Xanh", "BASED_IN", "Đà Nẵng"), ("Lúa Vàng", "BASED_IN", "Cần Thơ"),]RELATIONS = {"FOUNDED", "INVESTED_IN", "STUDIED_AT", "BASED_IN"}
SHARED_INVESTOR = """MATCH (c:Entity {name: $name})<-[:INVESTED_IN]-(f)-[:INVESTED_IN]->(o)RETURN f.name AS fund, o.name AS company"""WITHIN_3 = """MATCH (c:Entity {name: $name})-[]-{1,3}(n)RETURN count(DISTINCT n) AS n"""
with GraphDatabase.driver(URI, auth=AUTH) as driver: driver.verify_connectivity() # Xóa đồ thị cũ của bài để chạy lại nhiều lần vẫn ra cùng kết quả driver.execute_query("MATCH (n:Entity) DETACH DELETE n") for s, r, o in TRIPLES: # Tên đi qua tham số $s, $o. Kiểu quan hệ được ghép vào chuỗi, # nên phải kiểm trước rằng nó nằm trong lược đồ. if r not in RELATIONS: raise ValueError(f"Quan hệ ngoài lược đồ: {r}") driver.execute_query( "MERGE (a:Entity {name: $s}) MERGE (b:Entity {name: $o}) " f"MERGE (a)-[:{r}]->(b)", s=s, o=o, ) for query in (SHARED_INVESTOR, WITHIN_3): records, _, _ = driver.execute_query(query, name="Mây Xanh") for record in records: print(record.data())Sample output
From one real run.
{'fund': 'Quỹ Sao Mai', 'company': 'Lúa Vàng'}{'n': 7}Ô này xóa mọi nút Entity trong cơ sở dữ liệu, ghi lại 8 bộ ba bằng MERGE, rồi chạy hai truy vấn với tham số name="Mây Xanh". Dòng thứ nhất là truy vấn hai bước “chung nhà đầu tư”: đúng hàng 3 của bảng Minh họa. Dòng thứ hai đếm thực thể cách Mây Xanh 1 tới 3 bước, được 7, khớp với ô BFS ở trên: trên đồ thị này, {1,3} của Cypher và BFS của bạn tìm ra cùng một tập thực thể. (Hai bên không làm y hệt nhau: Cypher liệt kê mọi đường đi không lặp cạnh, còn BFS chỉ giữ đường ngắn nhất tới mỗi nút.) Kết quả giống nhau mọi lần chạy, vì không có mô hình nào trong ô này. Biến NEO4J_URI là tùy chọn, chỉ cần khi Neo4j không nằm ở 127.0.0.1:7687; đừng trỏ nó vào cơ sở dữ liệu đang có dữ liệu thật.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Đồ thị tri thức là một tập bộ ba : thực thể là nút, quan hệ là cạnh có hướng, có nhãn. Hai bộ ba chung một thực thể thì tự nối, nên tri thức rải ở nhiều đoạn được ghép lại.
- LLM trích bộ ba từ từng chunk theo một lược đồ, trả JSON. Chương trình phải kiểm JSON và lược đồ, lưu chunk nguồn, và gộp thực thể; bước trích là chỗ tốn kém và dễ sai nhất.
- Neo4j lưu đồ thị; Cypher vẽ mẫu bằng ký tự, như
(c)<-[:INVESTED_IN]-(f)-[:INVESTED_IN]->(o), và-[]-{1,3}cho 1 tới 3 bước (cú pháp cũ*1..3). Bên dưới là phép duyệt kiểu BFS; số nút tăng cỡ theo số bước, nên phải giới hạn bước và lọc loại quan hệ. - Graph RAG chỉ thay bước truy xuất của RAG bằng duyệt đồ thị. GraphRAG của Microsoft thêm tóm tắt theo cộng đồng cho câu hỏi về cả kho. RAG vector vẫn là lựa chọn rẻ và đủ cho câu hỏi một chỗ.
Đọc thêm:
- Edge, D., Trinh, H., Cheng, N. và cộng sự (2024). From Local to Global: A Graph RAG Approach to Query-Focused Summarization. arXiv:2404.16130. Paper GraphRAG: dựng đồ thị bằng LLM, tóm tắt cộng đồng, gộp câu trả lời từng phần cho câu hỏi toàn cục.
- Microsoft. GraphRAG, tài liệu dự án (truy cập 10/2026). Quy trình lập chỉ mục và các chế độ global search, local search; mã nguồn ở microsoft/graphrag.
- Neo4j. Cypher Manual: Variable-length patterns (truy cập 10/2026). Cú pháp
{1,3}theo GQL, so với cú pháp cũ*1..3. - Neo4j. Python Driver Manual, bản 6 (truy cập 10/2026).
GraphDatabase.driver,execute_query, và cách truyền tham số. - Kipf, T. N., Welling, M. (2017). Semi-Supervised Classification with Graph Convolutional Networks. ICLR 2017. Cho ai muốn biết GCN, hướng học embedding cho nút mà bài chỉ nhắc tên.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About