Bỏ qua để đến nội dung

LangChain và LlamaIndex

Sau bài này, bạn sẽ:

  • Quyết định được khi nào một framework như LangChain hay LlamaIndex đáng dùng, và khi nào vài chục dòng tự viết là đủ.
  • Hiểu các khái niệm chung của hai thư viện: bộ tải tài liệu (document loader), bộ cắt văn bản (text splitter), chỉ mục (index), retriever, chain và query engine.
  • Viết lại được pipeline RAG tối thiểu bằng LangChain với LCEL, rồi bằng LlamaIndex với as_query_engine, cả hai nối với API kiểu OpenAI (OpenAI-compatible).
  • Nhận ra các bẫy thật khi dùng hai thư viện: tên tham số khác nhau, cấu hình mặc định trỏ về OpenAI, import cũ đã bị gỡ, phụ thuộc xung đột.

Chatbot RAG của bạn chạy tốt: khoảng 40 dòng Python, SDK openai, một mảng NumPy làm kho vectơ. Rồi yêu cầu mới đến dồn dập. Tài liệu giờ là 200 file PDF, vài trang web và một thư mục Word. Kho vectơ trong RAM không đủ, cần một cơ sở dữ liệu vectơ thật. Nhóm muốn thử nhà cung cấp khác mà không viết lại code. Và khi chatbot trả lời sai, bạn muốn xem lại nó đã tìm được chunk nào, prompt gửi đi trông ra sao.

Mỗi yêu cầu đều tự làm được. Nhưng làm hết thì bạn đang viết lại một thư viện: bộ đọc PDF, bộ cắt chunk theo câu, lớp bọc cho từng loại kho vectơ, công cụ ghi lại từng bước. LangChain và LlamaIndex là hai thư viện Python làm sẵn những thứ đó. Chúng không làm RAG theo cách khác: vẫn là cắt, embed, tìm, ghép prompt, gọi mô hình. Chúng chỉ đặt tên cho từng khâu và cho bạn các khối lắp sẵn.

Giống chuyện tự đóng một cái bàn. Với bộ đồ nghề, bạn hiểu từng con ốc nhưng mất công. Với bộ bàn đóng gói sẵn, lắp nhanh hơn nhiều; nhưng đến khi nhà sản xuất đổi mẫu ốc, tờ hướng dẫn cũ trên mạng không còn khớp, và bạn phải tìm xem con ốc mới nằm ở túi nào.

Để biết nên chọn cách nào, hãy tự hỏi ba câu:

  1. Quy trình của bạn có cố định và ít bước không?
  2. Bạn có cần nhiều loại nguồn dữ liệu, nhiều kho vectơ hay nhiều nhà cung cấp không?
  3. Bạn có chịu được việc sửa code mỗi khi thư viện đổi API không?

Trả lời “có, không, không” thì tự viết là hợp lý. Càng lệch khỏi ba câu trả lời đó, framework càng đáng xét.

Bài toán: viết lại pipeline RAG tối thiểu bằng LangChain và bằng LlamaIndex, hiểu mỗi khâu ứng với khái niệm nào của từng thư viện, và biết cái giá phải trả khi dùng chúng.

Bài này không có công thức mới. Phần lý thuyết là cách hai thư viện cắt pipeline RAG thành các khái niệm, và cách mỗi thư viện nối các khái niệm đó lại.

Pipeline RAG có năm khâu. Hai thư viện đặt tên khác nhau cho cùng một việc:

  1. Tải tài liệu. Bộ tải tài liệu đọc file hay trang web, trả về các đối tượng Document, mỗi đối tượng gồm chữ và metadata (siêu dữ liệu, ví dụ tên file nguồn). LangChain để phần lớn loader ở gói langchain-community. LlamaIndex gọi chúng là reader, ví dụ SimpleDirectoryReader đọc cả một thư mục.
  2. Cắt chunk. Bộ cắt văn bản chia mỗi Document thành các chunk, giữ metadata cho từng chunk. LangChain có RecursiveCharacterTextSplitter: thử cắt ở chỗ xuống dòng đôi trước, rồi xuống dòng, rồi dấu cách, sao cho mỗi chunk không quá chunk_size ký tự. LlamaIndex gọi khâu này là node parser, và chunk là node; SentenceSplitter cố giữ nguyên câu, đo chunk_size bằng token.
  3. Embedding và lưu. Tính vectơ cho từng chunk rồi cất vào kho vectơ. Ở LangChain đó là OpenAIEmbeddings cộng một kho như InMemoryVectorStore. Ở LlamaIndex, VectorStoreIndex gói cả hai: tạo chỉ mục (index) là cắt, embed và lưu trong một lời gọi.
  4. Truy xuất. Retriever là một đối tượng nhận câu hỏi và trả về các chunk liên quan nhất. Nó che giấu chuyện tìm thế nào: cosine trên kho vectơ, tìm theo từ khóa hay kết hợp cả hai, người gọi đều chỉ thấy “câu hỏi vào, chunk ra”.
  5. Ghép prompt và gọi mô hình. Ở LangChain, bạn tự nối retriever, prompt, mô hình và bộ đọc kết quả thành một chain. Ở LlamaIndex, query engine gói sẵn cả truy xuất, prompt và lời gọi mô hình sau một hàm query.

Hình dưới, dùng lại từ bài RAG và chatbot hiện đại, vẽ lại pipeline đó. Mỗi khối của hình ứng với một khái niệm trong bảng đối chiếu ở mục Minh họa.

Lập chỉ mụclàm một lầnTruy vấnmỗi câu hỏiTài liệuChia chunkEmbeddingKho vectơcùng mộtmô hìnhCâu hỏiEmbeddingTìm top-k(cosine)Ghép promptchỉ dẫn, chunk [1] [2],câu hỏiMô hình chatCâu trả lời+ nguồn
Câu hỏi và chunk được embed bằng cùng một mô hình; chunk thứ ii được chấm điểm s(i)=cos⁡(q,c(i))s^{(i)} = \cos(\mathbf{q}, \mathbf{c}^{(i)}), rồi giữ kk chunk điểm cao nhất.

Mọi khối của LangChain (prompt, mô hình chat, retriever, bộ đọc kết quả) đều là một Runnable: đối tượng có phương thức invoke, nhận một đầu vào và trả một đầu ra. Toán tử | nối hai Runnable thành một: đầu ra của khối trái thành đầu vào của khối phải, giống ống dẫn trong dòng lệnh. Cách viết này gọi là LCEL (LangChain Expression Language). Đây là chain trong code của bài:

chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt | llm | StrOutputParser()
)

Đọc từ trái sang phải, với đầu vào là câu hỏi:

  • Dict ở đầu chạy hai nhánh với cùng một câu hỏi. Nhánh context: retriever trả về danh sách Document, rồi hàm format_docs nối chữ của chúng thành một chuỗi. LangChain tự biến hàm Python thường thành Runnable khi nó đứng cạnh dấu |. Nhánh question: RunnablePassthrough() cho câu hỏi đi qua nguyên vẹn. Kết quả là dict {"context": ..., "question": ...}.
  • prompt là một ChatPromptTemplate có hai chỗ trống {context} và {question}. Nó điền dict vào và tạo danh sách message system, human.
  • llm (ChatOpenAI) gửi các message tới API và trả về một message của mô hình.
  • StrOutputParser() lấy phần chữ của message đó.

Ưu điểm của cách này là đường ống nằm lộ ra: muốn thêm một bước, ví dụ viết lại câu hỏi trước khi tìm, bạn chèn một khối vào đúng chỗ. Nhược điểm là bạn phải tự lắp, và phải hiểu dữ liệu ở mỗi khâu có dạng gì.

LlamaIndex đi theo hướng ngược lại: gói sẵn nhiều hơn. Bạn đặt cấu hình chung một lần trong Settings (mô hình chat, mô hình embedding, bộ cắt chunk), rồi hai dòng làm hết phần còn lại:

index = VectorStoreIndex.from_documents([doc])
query_engine = index.as_query_engine(similarity_top_k=2)

from_documents cắt tài liệu thành node bằng Settings.node_parser, embed từng node bằng Settings.embed_model và lưu vào một kho vectơ trong bộ nhớ. as_query_engine trả về một query engine; gọi query_engine.query(câu_hỏi) thì nó tìm similarity_top_k node gần nhất (mặc định là 2), ghép prompt, gọi Settings.llm, và trả về một đối tượng Response. In Response ra là câu trả lời; còn response.source_nodes cho biết những node nào đã được dùng. Đường ống bị giấu đi, nên đó là chỗ dễ nhất để bạn kiểm tra mô hình đã dựa vào đâu.

Hai điều cần biết về những gì được gói sẵn:

  • Thiếu cấu hình thì mặc định là OpenAI. Nếu không đặt Settings.llm hay Settings.embed_model, LlamaIndex dùng mô hình của OpenAI với địa chỉ của OpenAI, và không đọc OPENAI_BASE_URL. Người dùng Gemini hay Ollama quên một dòng là chương trình gửi key của họ tới máy chủ OpenAI.
  • Prompt nội bộ là tiếng Anh. Lời dặn hệ thống mở bằng “You are an expert Q&A system…”, và phần ngữ cảnh mở bằng “Context information is below.” (xem mục Minh họa). Khi câu hỏi là tiếng Việt, mô hình có thể vẫn trả lời bằng tiếng Việt, nhưng muốn kiểm soát giọng văn hay quy tắc thì bạn đưa prompt của mình qua tham số text_qa_template. Prompt này thay cả lời dặn hệ thống lẫn khung ngữ cảnh mặc định, nên hãy tự ghi đủ quy tắc vào:
from llama_index.core import PromptTemplate
qa_prompt = PromptTemplate(
"Ngữ cảnh:\n{context_str}\n\n"
"Chỉ dựa vào ngữ cảnh, trả lời bằng tiếng Việt.\n"
"Câu hỏi: {query_str}"
)
query_engine = index.as_query_engine(text_qa_template=qa_prompt)

Cả hai thư viện có lớp riêng cho API kiểu OpenAI, nhưng tên tham số không giống nhau. Đây là chỗ dễ sai nhất khi chuyển code từ thư viện này sang thư viện kia:

ViệcLangChainLlamaIndex
Lớp mô hình chatChatOpenAI (gói langchain-openai)OpenAILike (gói llama-index-llms-openai-like)
Địa chỉ APIbase_url=api_base=
Tên mô hình embeddingmodel=model_name=
Điều cần đặt thêmcheck_embedding_ctx_length=False ở OpenAIEmbeddingsis_chat_model=True ở OpenAILike

Vì sao cần ba điểm ở hai dòng cuối:

  • model_name=, không phải model=. OpenAIEmbedding của LlamaIndex kiểm tham số model với danh sách tên mô hình của OpenAI. Truyền một tên khác, ví dụ tên một mô hình embedding của Ollama, là gặp ngay ValueError: '...' is not a valid OpenAIEmbeddingModelType. Tham số model_name bỏ qua bước kiểm đó.
  • check_embedding_ctx_length=False. Mặc định, OpenAIEmbeddings tách chữ thành token bằng tiktoken rồi gửi dãy mã token thay vì chữ. API của OpenAI nhận được kiểu này, nhưng tài liệu của langchain-openai ghi rằng nhiều nhà cung cấp khác thì không, và khuyên đặt False để gửi chữ. Chạy thử với một server giả, bài thấy đúng như vậy: để mặc định thì server nhận các danh sách số, đặt False thì nhận chuỗi chữ.
  • is_chat_model=True. OpenAILike mặc định coi mô hình là loại completion cũ: nó gửi một chuỗi tới endpoint /completions thay vì danh sách message tới /chat/completions. Nhiều nhà cung cấp chỉ có endpoint chat; server giả của bài cũng vậy, và trả lỗi 404 khi thiếu dòng này.

Phần embedding của cả hai ô code đọc biến môi trường giống bài RAG và chatbot hiện đại: EMBEDDING_MODEL là bắt buộc; EMBEDDING_BASE_URL và EMBEDDING_API_KEY là tùy chọn, để trống thì dùng địa chỉ và key của phần chat. Vì vậy, nếu embedding ở chính OpenAI mà phần chat ở nơi khác, EMBEDDING_BASE_URL phải ghi rõ https://api.openai.com/v1: để trống thì yêu cầu embedding đi tới địa chỉ của phần chat.

Hai thư viện đổi API nhanh, nên phần lớn ví dụ trên mạng viết cho bản cũ. Bài đã thử những điều dưới đây với langchain 1.4.7, langchain-classic 1.0.8, llama-index-core 0.14.25 vào tháng 10/2026; bản sau có thể khác.

  • LangChain 1.x tách và gỡ nhiều thứ. from langchain.chains import RetrievalQA và from langchain.text_splitter import ... báo ModuleNotFoundError. Các chain cũ như RetrievalQA nay nằm ở gói riêng langchain-classic (import từ langchain_classic.chains) và đã bị đánh dấu deprecated. Bộ cắt văn bản ở gói langchain-text-splitters; Document, prompt, kho vectơ trong bộ nhớ ở langchain-core.
  • Agent của LangChain đổi tên hàm. create_react_agent của LangGraph đã chuyển thành create_agent trong langchain.agents. Trang hướng dẫn RAG chính thức của LangChain giờ dựng bằng Deep Agents (gói deepagents, hàm create_deep_agent): agent tự quyết khi nào gọi công cụ tìm kiếm. Trang khái niệm Retrieval vẫn giữ “RAG hai bước” (luôn tìm rồi mới gọi mô hình), đúng kiểu chain của bài, và ghi rằng cách này hợp với bot hỏi đáp và bot tài liệu. Chain LCEL như trong bài vẫn chạy và hợp để học từng khâu; agent là chuyện của bài Agents.
  • LlamaIndex cài chọn lọc. Gói gộp llama-index 0.14.25 đòi llama-index-llms-openai dưới 0.8, còn llama-index-llms-openai-like 0.8.1 đòi từ 0.8 trở lên, nên pip báo không cài được. Cài llama-index-core cùng từng gói tích hợp cần dùng thì được, đúng như trang cài đặt của LlamaIndex gợi ý.
  • LlamaIndex chưa nhận openai 3. llama-index-llms-openai đòi openai dưới 3, trong khi các bài khác của phần này dùng openai 3.37. langchain-openai 1.7.2 nhận cả hai.
  • Kho vectơ của cả hai ô code nằm trong bộ nhớ. Thoát chương trình là mất, lần chạy sau phải embed lại từ đầu, tức gọi API lại. Ứng dụng thật lưu chỉ mục xuống đĩa hoặc dùng một cơ sở dữ liệu vectơ.
  • Framework không làm RAG tốt hơn. Chất lượng vẫn do cách cắt chunk, mô hình embedding, kk và prompt quyết định. Cách đánh giá ở bài RAG và chatbot hiện đại vẫn cần như cũ.
  • Bài chỉ chạm phần RAG. Cả hai thư viện đều có agent (LangChain dựng agent trên LangGraph, LlamaIndex có FunctionAgent), là nội dung của bài Agents.

Bảng dưới đối chiếu từng khâu của pipeline ở ba cách làm: tự viết như bài RAG và chatbot hiện đại, LangChain và LlamaIndex. Hãy đọc theo hàng: cùng một việc, ba cái tên.

KhâuTự viết (openai + NumPy)LangChainLlamaIndex
Tài liệuchuỗi hoặc danh sách chuỗiDocument(page_content, metadata)Document(text, metadata), SimpleDirectoryReader
Cắt chunktự cắt, hoặc viết sẵn từng chunkRecursiveCharacterTextSplitter (ký tự)SentenceSplitter (token)
Embeddingclient.embeddings.createOpenAIEmbeddingsOpenAIEmbedding
Kho vectơmảng NumPy đã chuẩn hóaInMemoryVectorStoreVectorStoreIndex (gồm cả cắt và embed)
Truy xuấtindex @ q, rồi argsortstore.as_retriever(search_kwargs={"k": 2})similarity_top_k=2
Promptf-stringChatPromptTemplateprompt có sẵn, hoặc text_qa_template
Gọi mô hình chatclient.chat.completions.createChatOpenAIOpenAILike
Nối các khâuvài dòng Pythonchain LCEL: |query_engine.query(...)
Xem chunk đã dùngcó sẵn trong biến topgọi retriever.invoke(...)response.source_nodes

Để thấy “prompt có sẵn” của LlamaIndex cụ thể ra sao, đây là các message mà ô LlamaIndex của bài gửi tới API với câu hỏi “Câu lạc bộ họp lúc mấy giờ?”. Bài bắt chúng bằng một server giả đứng ở vị trí API, nên đây là prompt thật của llama-index-core 0.14.25, không phải câu trả lời của một mô hình. Hai chunk trong ngữ cảnh do mô hình embedding chọn; bảng thay chúng bằng chỗ giữ chỗ.

Vai tròNội dung
systemYou are an expert Q&A system that is trusted around the world.
Always answer the query using the provided context information, and not prior knowledge.
Some rules to follow:
1. Never directly reference the given context in your answer.
2. Avoid statements like ‘Based on the context, …’ or ‘The context information …’ or anything along those lines.
userContext information is below.
---------------------
source: noi-quy.txt

(chunk thứ nhất)

source: noi-quy.txt

(chunk thứ hai)
---------------------
Given the context information and not prior knowledge, answer the query.
Query: Câu lạc bộ họp lúc mấy giờ?
Answer:

Để ý dòng source: noi-quy.txt: metadata của tài liệu được chép vào trước mỗi chunk. Vì vậy SentenceSplitter trừ số token của metadata khỏi chunk_size. Ô LangChain thì gửi đúng hai message bạn viết trong ChatPromptTemplate, không thêm gì.

Code trong bài kiểm tra lần cuối tháng 10/2026, với Python 3.14. Ô LangChain dùng langchain 1.4.7, langchain-openai 1.7.2 và openai 3.37. Ô LlamaIndex dùng llama-index-core 0.14.25 và openai 2.54, trong một môi trường ảo riêng. Cả hai ô gọi API nên chạy trên máy bạn, không chạy trên trang.

Hai ô làm cùng một việc trên cùng dữ liệu: nội quy bốn đoạn của một câu lạc bộ tưởng tượng, rồi một câu hỏi mà mô hình không thể biết nếu không đọc nội quy. Hãy đặt hai ô cạnh nhau và đối chiếu từng dòng với bảng ở mục Minh họa.

Đặt ba biến môi trường như ở bài Sử dụng API văn bản, cộng thêm tên một mô hình embedding. Nếu nhà cung cấp chat của bạn không có embeddings (bảng ở bài Sử dụng API văn bản ghi rõ nơi nào có), đặt thêm địa chỉ và key của một nhà cung cấp có:

Cửa sổ terminal
export EMBEDDING_MODEL="<tên mô hình embedding>"
# Chỉ khi nhà cung cấp chat không có embeddings:
export EMBEDDING_BASE_URL="<địa chỉ API có embeddings>"
export EMBEDDING_API_KEY="<key của nhà cung cấp đó>"

Cài mỗi ô vào một môi trường ảo riêng. Lý do ở mục “Những gì đã đổi”: bộ LlamaIndex kéo openai xuống bản 2, có thể làm hỏng code của các bài khác nếu cài chung. Phiên bản được ghim đúng bản bài đã thử, vì hai thư viện đổi API thường xuyên.

Chạy trên máy bạn

RAG tối thiểu bằng LangChain: retriever và chain LCEL

Cần cài
pip install 'langchain==1.4.7' 'langchain-openai==1.7.2' 'langchain-text-splitters==1.1.3' numpy
Biến môi trường
OPENAI_BASE_URL (tùy chọn), OPENAI_API_KEY, LLM_MODEL, EMBEDDING_MODEL, EMBEDDING_BASE_URL (tùy chọn), EMBEDDING_API_KEY (tùy chọn)
# Pipeline RAG tối thiểu bằng LangChain: cắt chunk, embedding, truy xuất, LCEL.
import os
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
BASE_URL = os.environ.get("OPENAI_BASE_URL") # bỏ trống: dùng OpenAI
API_KEY = os.environ["OPENAI_API_KEY"]
llm = ChatOpenAI(
model=os.environ["LLM_MODEL"], base_url=BASE_URL, api_key=API_KEY
)
embeddings = OpenAIEmbeddings(
model=os.environ["EMBEDDING_MODEL"],
base_url=os.environ.get("EMBEDDING_BASE_URL") or BASE_URL,
api_key=os.environ.get("EMBEDDING_API_KEY") or API_KEY,
check_embedding_ctx_length=False, # gửi chữ, không gửi mã token
)
# 1. Tài liệu, rồi cắt thành các chunk
text = (
"Câu lạc bộ AI Atlas họp tối thứ Năm, từ 19 giờ, ở phòng 204.\n\n"
"Thành viên mới cần học xong bài hồi quy tuyến tính trước buổi đầu.\n\n"
"Mỗi buổi họp có một bạn trình bày một bài học trong khoảng 20 phút.\n\n"
"Ai vắng ba buổi liền sẽ nhận một email nhắc nhở của ban điều hành."
)
docs = [Document(page_content=text, metadata={"source": "noi-quy.txt"})]
splitter = RecursiveCharacterTextSplitter(chunk_size=80, chunk_overlap=0)
chunks = splitter.split_documents(docs)
# 2. Tính embedding từng chunk, lưu vào kho vectơ trong bộ nhớ
store = InMemoryVectorStore(embeddings)
store.add_documents(chunks)
retriever = store.as_retriever(search_kwargs={"k": 2})
# 3. Prompt có hai chỗ trống: {context} và {question}
prompt = ChatPromptTemplate.from_messages([
("system", "Chỉ trả lời dựa trên ngữ cảnh. Không có thì nói không biết."),
("human", "Ngữ cảnh:\n{context}\n\nCâu hỏi: {question}"),
])
def format_docs(found):
return "\n\n".join(d.page_content for d in found)
# 4. Nối các khâu bằng LCEL: dữ liệu chảy từ trái sang phải
chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt | llm | StrOutputParser()
)
question = "Câu lạc bộ họp lúc mấy giờ?"
print(f"Số chunk: {len(chunks)}")
for d in retriever.invoke(question):
print("-", d.page_content)
print(chain.invoke(question))

Đọc code theo bốn khối đánh số:

  1. RecursiveCharacterTextSplitter với chunk_size=80 cắt nội quy ở các chỗ xuống dòng đôi. Mỗi đoạn dài 60 tới 67 ký tự, còn hai đoạn liền nhau đã quá 80, nên dòng đầu tiên in ra là Số chunk: 4. Phép cắt không gọi API, nên con số này giống nhau ở mọi máy.
  2. InMemoryVectorStore gọi API embedding một lần cho cả bốn chunk, rồi as_retriever tạo retriever trả về 2 chunk gần nhất. Kho này tính cosine bằng NumPy, nên NumPy có trong dòng cài đặt dù code không import nó: thiếu NumPy thì dòng tìm kiếm báo ImportError.
  3. ChatPromptTemplate có lời dặn hệ thống và hai chỗ trống.
  4. Chain LCEL nối tất cả. Hai dòng bắt đầu bằng - là hai chunk retriever tìm được; chunk về giờ họp nên đứng đầu. Dòng cuối là câu trả lời, nên nhắc tới tối thứ Năm, 19 giờ.

Retriever được gọi hai lần: một lần để in, một lần bên trong chain. Mỗi lần gọi embed câu hỏi một lần, nên chương trình gọi API embedding ba lần tất cả.

Chạy trên máy bạn

RAG tối thiểu bằng LlamaIndex: chỉ mục và query engine

Cần cài
pip install 'llama-index-core==0.14.25' 'llama-index-llms-openai-like==0.8.1' 'llama-index-embeddings-openai==0.7.0'
Biến môi trường
OPENAI_BASE_URL (tùy chọn), OPENAI_API_KEY, LLM_MODEL, EMBEDDING_MODEL, EMBEDDING_BASE_URL (tùy chọn), EMBEDDING_API_KEY (tùy chọn)
# Pipeline RAG tối thiểu bằng LlamaIndex: index và query engine.
import os
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai_like import OpenAILike
BASE_URL = os.environ.get("OPENAI_BASE_URL") # bỏ trống: dùng OpenAI
API_KEY = os.environ["OPENAI_API_KEY"]
# Cấu hình chung. Không đặt thì LlamaIndex gọi mô hình mặc định của OpenAI
Settings.llm = OpenAILike(
model=os.environ["LLM_MODEL"],
api_base=BASE_URL, # LangChain gọi là base_url
api_key=API_KEY,
is_chat_model=True, # gọi /chat/completions
)
Settings.embed_model = OpenAIEmbedding(
model_name=os.environ["EMBEDDING_MODEL"], # không dùng model=
api_base=os.environ.get("EMBEDDING_BASE_URL") or BASE_URL,
api_key=os.environ.get("EMBEDDING_API_KEY") or API_KEY,
)
# chunk_size đếm bằng token (tính cả metadata), không phải ký tự
Settings.node_parser = SentenceSplitter(chunk_size=64, chunk_overlap=0)
text = (
"Câu lạc bộ AI Atlas họp tối thứ Năm, từ 19 giờ, ở phòng 204.\n\n"
"Thành viên mới cần học xong bài hồi quy tuyến tính trước buổi đầu.\n\n"
"Mỗi buổi họp có một bạn trình bày một bài học trong khoảng 20 phút.\n\n"
"Ai vắng ba buổi liền sẽ nhận một email nhắc nhở của ban điều hành."
)
doc = Document(text=text, metadata={"source": "noi-quy.txt"})
# Một dòng làm ba việc: cắt chunk, tính embedding, lưu vào chỉ mục
index = VectorStoreIndex.from_documents([doc])
# Query engine gói sẵn: truy xuất, ghép prompt, gọi mô hình
query_engine = index.as_query_engine(similarity_top_k=2)
question = "Câu lạc bộ họp lúc mấy giờ?"
response = query_engine.query(question)
print(f"Số chunk: {len(index.docstore.docs)}")
for node in response.source_nodes:
print("-", node.text)
print(response)

Phần đầu là cấu hình Settings; phần còn lại ngắn hơn hẳn ô LangChain vì from_documents và as_query_engine làm hộ việc cắt, embed, tìm và ghép prompt. Ở đây chunk_size=64 đếm bằng token và đã tính cả metadata: mỗi đoạn nội quy khoảng 30 token, hai đoạn liền nhau vượt chỗ còn lại sau khi trừ metadata, nên LlamaIndex cũng cắt ra Số chunk: 4. Hai dòng - lấy từ response.source_nodes, rồi tới câu trả lời.

Lần đầu chạy, LlamaIndex không cần tải gì thêm: bộ tách token mà SentenceSplitter dùng đã có sẵn trong gói llama-index-core. Nhưng nếu địa chỉ API sai, hãy kiên nhẫn: OpenAIEmbedding mặc định thử lại tới 10 lần, nên khi bài thử với một cổng đóng, lỗi APIConnectionError mất hơn một phút mới hiện ra.

Câu 1/4 Trường hợp nào chuyển từ code tự viết (SDK openai và NumPy) sang một framework như LangChain hay LlamaIndex mang lại ít lợi ích nhất?
Câu 2/4 Bạn chạy code LlamaIndex của bài nhưng xóa dòng Settings.embed_model = .... Biến môi trường trỏ tới Gemini: OPENAI_BASE_URL là địa chỉ của Gemini, OPENAI_API_KEY là key Gemini. Điều gì xảy ra?
Câu 3/4 Trong chain {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser(), bạn gọi chain.invoke("Câu lạc bộ họp lúc mấy giờ?"). Khâu prompt nhận được gì?
Câu 4/4 Bạn chép code từ LangChain sang LlamaIndex và giữ chunk_size=80 cho cả hai. Văn bản tiếng Việt. Chunk của hai bên so với nhau thế nào?

Ý chính:

  • LangChain và LlamaIndex không làm RAG theo cách khác: vẫn là tải tài liệu, cắt chunk, embed và lưu, truy xuất, ghép prompt và gọi mô hình. Chúng đặt tên và làm sẵn từng khâu. Quy trình cố định, ít bước, một nhà cung cấp thì tự viết thường đủ.
  • LangChain lắp từng khối Runnable bằng LCEL: {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser(). Đường ống lộ ra, dễ chèn thêm bước.
  • LlamaIndex gói sẵn: Settings cấu hình một lần, VectorStoreIndex.from_documents lập chỉ mục, as_query_engine trả lời; response.source_nodes cho biết chunk nào được dùng. Prompt mặc định là tiếng Anh, thay được bằng text_qa_template.
  • Hai thư viện đổi nhanh: tên tham số khác nhau (base_url và api_base, model và model_name), cấu hình thiếu thì LlamaIndex về OpenAI, import cũ của LangChain đã bị gỡ. Ghim phiên bản và dùng môi trường ảo riêng.

Đọc thêm:

  • LangChain (2026). LangChain overview. Trang tổng quan chính thức của LangChain 1.x: các khối chính, và vì sao agent được dựng trên LangGraph. Tài liệu không chia theo phiên bản; bài đã đối chiếu với langchain 1.4.7.
  • LangChain (2026). Migrate to LangChain v1. Danh sách những gì đã chuyển sang langchain-classic, đổi tên hay bị gỡ. Nên đọc trước khi dùng một ví dụ cũ.
  • LangChain (2026). Retrieval Augmented Generation (RAG) with Deep Agents. Hướng dẫn RAG chính thức hiện hành, dựng bằng Deep Agents: agent tự quyết khi nào tìm kiếm.
  • LangChain (2026). Retrieval. Trang khái niệm: so sánh RAG hai bước (như chain của bài) với RAG dạng agent, và khi nào dùng cách nào.
  • LlamaIndex (2026). Starter example và Installation. Ví dụ chính thức với SimpleDirectoryReader, VectorStoreIndex, Settings, và cách cài chọn lọc bằng llama-index-core cùng từng gói tích hợp.
  • PyPI. llama-index-llms-openai-like 0.8.1. Trang của gói OpenAILike đúng bản bài đã thử, kèm danh sách phụ thuộc.

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu