Cách tôi thoát khỏi vũng lầy lập trình nhờ AI Agent
Tôi lao vào xây chatbot với kỹ năng lập trình bằng không và đầy nhiệt huyết — để rồi phát hiện phiên bản v0.1 là thảm họa với database CSV và chunking thô sơ, cho đến khi AI agent kéo tôi ra.
Cập nhật (2026): Chatbot này đã phát triển thành Sydney! Sau nhiều lần lặp lại, Sydney giờ sống tại /ask/ và tập trung vào nội dung blog và sản phẩm.
Hồi đầu tháng 11 năm ngoái, tôi ra mắt chatbot tự làm phiên bản 0.1. Khi đó, tôi viết, "Mặc dù v0.1 là bước đầu tiên lớn với tư cách một người mới học code, nó có những hạn chế đáng kể." À, hóa ra đó là nói giảm nói tránh. Tôi không đánh giá đúng toàn bộ quy trình và bản build của mình thô sơ đến mức nào. Thực tế là, nỗ lực đầu tiên của tôi, dù chân thành, chỉ là một prototype được ghép lại bằng nhiệt huyết nhưng thiếu kiến thức.
Bài viết này không chỉ là phần tiếp theo; nó là một cái nhìn sâu vào hành trình đã diễn ra từ thời điểm đó — một hành trình đầy thử thách, sai lầm, và bài học vô giá. Tôi đang phơi bày mọi chi tiết của cuộc phiêu lưu này, không chỉ vì sự minh bạch mà với hy vọng rằng kinh nghiệm của tôi có thể cộng hưởng hoặc thậm chí giúp ích cho ai đó trên con đường tương tự. (Thêm bối cảnh, tôi là một chuyên gia quảng cáo trung niên với hoàn toàn không có kinh nghiệm lập trình trước đó.)
Như đã đề cập, để ra mắt v0.1 của chatbot, tôi chủ yếu làm theo hướng dẫn từ khóa học ngắn "Building Systems with the ChatGPT API" và hai cookbook từ OpenAI: Question answering using embeddings-based search và How to count tokens with tiktoken.
Đây là lý do chatbot v0.1 của tôi tệ kinh khủng:
- Chunking: Tôi đơn giản chia các bài blog dài thành các chunk nhỏ dựa trên độ dài token aka chunk ký tự tĩnh đơn giản. Cách chia thô sơ nhất có thể :D. Nếu bạn muốn hiểu tại sao đây là ý tưởng tệ, hãy đọc về 5 cấp độ text splitting từ Greg Kamradt ở đây.
- Embedding: Tôi gặp khó khăn với embeddings. Tôi dùng mô hình embedding của OpenAI, nhưng liên tục bị giới hạn API request, khiến quá trình embedding thất bại giữa chừng. Sau đó tôi học cách batch request và thêm timeout giữa các batch để tránh giới hạn. Cuối cùng tôi lưu embeddings đã tạo vào file .csv đơn giản làm "database" tạm.
- Database: Tôi biết CSV không tối ưu cho database, nhưng thiếu kỹ năng cho các giải pháp tốt hơn.
- Metadata: Ban đầu tôi không nhận ra việc bao gồm metadata như ngày xuất bản và URL bài viết quan trọng để chatbot trả lời câu hỏi chính xác. Tôi phải lặp lại embedding và lưu để thêm metadata liên quan.
- Retriever: Tôi không biết về các loại retriever và thuật toán khác nhau. Tôi đơn giản dùng relevance search của OpenAI để lấy số lượng kết quả hardcode.
- Memory: Để có cuộc hội thoại, chatbot cần nhớ những gì người dùng đã nói trước đó. Và đây là nơi với context window hạn chế của gpt-3.5 (thời điểm đó), có một đánh đổi rõ ràng giữa chunk size, số lượng kết quả bạn muốn lấy.
- Ví dụ, nếu chunk size là 800 token và retriever trả về top 8 kết quả, đó là 6.400 token hay hơn 50% giới hạn mô hình cũ.
- Trên đây chỉ là 1 câu hỏi nên bạn có thể tưởng tượng cuộc hội thoại nhiều lượt và memory sẽ đầy nhanh như thế nào.
- Một cách giải quyết là có chunk size nhỏ hơn và retriever trả về ít kết quả hơn nhưng với retriever cơ bản (ở trên), điều này có nghĩa mô hình không có đủ thông tin toàn diện để trả lời câu hỏi.
- Tôi thậm chí không dùng bất kỳ IDE nào. Tất cả code được chỉnh sửa bằng TextEdit trên Mac :D (Tôi đã nói tôi là noob chưa? :P)
- Tôi có thể tiếp tục mãi nhưng chắc bạn đã hiểu bức tranh.
"Thung lũng tử thần" của tôi
Háo hức cải thiện vượt qua những hạn chế của v0.1, tôi thử nhiều khóa học online, hy vọng chúng sẽ cung cấp những mảnh ghép còn thiếu để nâng cấp kỹ năng. Nhưng tiến bộ chỉ dẫn đến ngõ cụt.
Tôi vật lộn qua các bài tập về vector database (Vector Databases: from Embeddings to Applications with Weaviate), phương pháp RAG đánh giá (Building and Evaluating Advanced RAG Applications với Llama-Index và Truera), và kỹ thuật retrieval nâng cao (Advanced Retrieval for AI with Chroma.) Dù cố gắng thế nào, tôi không thể kết nối lý thuyết với ứng dụng thực tế sử dụng dữ liệu blog của mình.
Các khóa học có thiết kế kém không? Không — thiếu sót là ở kiến thức nền tảng của chính tôi. Tuy nhiên, thất bại liên tiếp cực kỳ bực bội, chưa kể rất nản lòng. Tôi thấy mình trong một thung lũng nghĩa bóng, không chắc làm sao tiếp tục.
Những chiến thắng nhỏ theo thời gian
Những giá trị nhỏ vẫn xuất hiện từ các thất bại lặp đi lặp lại:
-
Chuyển sang VS Code thay cho TextEdit
-
Tận dụng các extension GitHub Copilot
-
Đánh giá cao Jupyter Notebook cho môi trường phát triển
Khóa học cuối cùng đề cập đến LangChain, một framework phổ biến mới để xây dựng chatbot. Tôi thực sự đã thử hướng dẫn LangChain cách đây nhiều tháng ("LangChain: Chat with Your Data" và "Functions, Tools and Agents with LangChain") mà không thành công. Nhưng giờ, với kiến thức đã tích lũy được, việc xem lại tài liệu của nó trở nên sáng tỏ. Các khái niệm kết nối với nhau và kiến trúc module của nó trở nên trực giác.
Tôi có thể hình dung việc áp dụng khả năng mạnh mẽ của LangChain vào dự án đam mê của mình. Cuối cùng, một con đường phía trước tự hiện ra! Từng bước một, tôi làm quen với các pipeline cho data ingestion, embedding, storage và retrieval.
Sự tự tin của tôi lớn dần với mỗi phần tôi triển khai được. V2 bắt đầu thành hình...
Xây dựng lại nền tảng
Với Langchain làm hướng dẫn, tôi bắt tay tái xây dựng chatbot từ đầu:
Nhập dữ liệu WordPress Export thành JSON
Sau nhiều thử nghiệm điều chỉnh tham số ingestion, JSONLoader của LangChain đã phân tích đúng các bài viết đã export. Giờ đầu vào đã được validate có thể cung cấp cho các pipeline downstream. (Code cho bước này nằm trong file "DataIngestionAndIndexing.ipynb" trong repo Github công khai này.)
Text Splitting tự động
Chunking thô sơ theo token length của tôi được thay bằng SentenceTransformers của LangChain, sử dụng NLP nâng cao để chia các đơn vị ngữ nghĩa. Không còn câu bị cắt giữa chừng! Cấu hình giữ chunk có kích thước phù hợp cho các mô hình bị giới hạn memory.
from langchain.text_splitter import SentenceTransformersTokenTextSplitter
# Define the token splitter with specific configurations
token_splitter = SentenceTransformersTokenTextSplitter(
chunk_overlap=0, # Overlap between chunks
tokens_per_chunk=256 # Number of tokens per chunk
)
# Split the documents into chunks based on tokens
all_splits = token_splitter.split_documents(documents)
print(f"Total document splits: \{len(all_splits)\}")
Tạo Embeddings và Indexes
Những khó khăn trước đây với giới hạn API OpenAI biến mất khi dùng wrapper của LangChain cho OpenAI embeddings. Gói gọn trong hai dòng code, embeddings trích xuất sạch sẽ các đặc trưng nổi bật từ text đã chia.
Cho vector store, tôi chọn FAISS (Facebook AI Similarity Search) thay vì Weaviate hay Chroma. FAISS đã được kiểm chứng trong ngành và cân bằng đúng giữa khả năng và độ phức tạp cho nhu cầu của tôi. Phiên bản CPU nhanh chóng index các chunk blog, tạo ra database tìm kiếm nhỏ gọn. Tôi không cần lo về batching hay bị giới hạn API request đến OpenAI nữa.
Langchain hỗ trợ nhiều vector store nên bạn có thể xem chúng ở đây.
# Initialize embeddings and FAISS vector store
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(all_splits, embeddings)
# Save the vector store locally
db.save_local("path/to/save/faiss_index") # Placeholder for save path/ index name
Hai dòng code! Chỉ vậy thôi.
Điều tôi cũng nhận ra là nếu nội dung không nhỏ (tôi có 400+ bài blog), thì bạn không nên thử dùng retriever ngay sau embedding vì indexes cần một chút thời gian để hoàn thành/ổn định.
Đánh giá Retrievers
Thiết lập retriever này chỉ cần 1 dòng code :D, sử dụng FAISS làm vector store.
retriever = db.as_retriever(search_type="mmr")
Với index và retriever đã sẵn sàng, tôi có data pipeline sẵn sàng cung cấp cho chatbot thông minh!
Thiết kế Conversational Agents
Tôi quyết định sử dụng agent framework từ langchain để xây chatbot này. Có quá mức cần thiết ở thời điểm này không? Có. Nhưng hy vọng của tôi là theo thời gian, tôi có thể phát triển chatbot này và cho nó thêm "tools" tức các chức năng. Langchain làm cho việc thiết lập agent và cho nó tools cực kỳ dễ dàng.
embeddings = OpenAIEmbeddings()
db = FAISS.load_local("path/to/your/faiss_index_file", embeddings) # Replace the path with your actual FAISS index file path
retriever = db.as_retriever(search_type="mmr")
tool = create_retriever_tool(
retriever,
"search_your_blog", # Replace "search_your_blog" with a descriptive name for your tool
"Your tool description here" # Provide a brief description of what your tool does
)
tools = [tool]
prompt_template = ChatPromptTemplate.from_messages([
# Customize the prompt template according to your chatbot's persona and requirements
])
llm = ChatOpenAI(model_name="gpt-3.5-turbo-1106", temperature=0)
llm_with_tools = llm.bind_tools(tools)
agent = (
\{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(x["intermediate_steps"]),
"chat_history": lambda x: x["chat_history"],
\}
| prompt_template
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
Code Python hoàn chỉnh cho agent
Cuối cùng, nếu bạn muốn thử chatbot v2, đây nó đây.
Có lạ không khi chatbot không biết gì về bạn, Chandler?
P.S: Cảm ơn một số bạn đã liên hệ cho tôi biết chatbot không biết gì về tôi. Và các bạn đúng! Đó là vì tôi quên export trang "Giới thiệu" và chỉ export "các bài đã đăng". Đây là lần thứ hai tôi quên làm điều này nên tôi sẽ đưa các câu hỏi cơ bản về tôi vào danh sách câu hỏi eval. Bài học kinh nghiệm!
Cảm ơn mọi người đã chia sẻ phản hồi mang tính xây dựng. Xin hãy tiếp tục. Và vâng tôi biết chatbot rất chậm khi khởi động nên tôi đang làm việc về vấn đề đó. :| (Tôi đã nói tôi là noob chưa? :P)
Cập nhật nhanh
Vấn đề chatbot không biết gì về tôi đã được sửa. Đây là những gì tôi đã làm và học được:
- Export trang "Giới thiệu" từ Wordpress sang .XML theo ở trên.
- Thực hiện text splitting và tạo embeddings sử dụng FAISS như trên. Lưu vector store với tên khác để test retriever
# save the vector store to local machine
db.save_local("faiss_index_about")
# set up the retriever to test the new vector store about Chandler
from langchain.retrievers.multi_query import MultiQueryRetriever
llm = ChatOpenAI(temperature=0)
retriever_from_llm = MultiQueryRetriever.from_llm(
retriever=db.as_retriever(), llm=llm
)
# test retriever
question = "Who is Chandler Nguyen?"
results = retriever_from_llm.get_relevant_documents(query=question, top_k=8)
for doc in results:
print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")
- Hóa ra quá trình merge hai FAISS vector store đơn giản đến ngạc nhiên, theo tài liệu ở đây.
# Try to merge two FAISS vector stores into 1
# load the vector store from local machine
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
db_1 = FAISS.load_local("faiss_index_about", embeddings)
db_2 = FAISS.load_local("faiss_index", embeddings)
db_2.merge_from(db_1)
# save the vector store to local machine
db_2.save_local("faiss_index_v2")
# test the new vector store to confirm correct retrieved documents
retriever = db_2.as_retriever(search_type="mmr")
results = retriever.get_relevant_documents("Who is Chandler Nguyen?")
for doc in results:
print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")
- Sau đó, về cơ bản giống quy trình như trên, sử dụng vector store mới
Cập nhật 14 tháng 2: Chatbot v2.10 ra mắt
Hai tuần sau khi triển khai chatbot, tôi giới thiệu phiên bản 2.10 nâng cao trải nghiệm người dùng với tốc độ, khả năng mở rộng và sự đơn giản. Bạn có thể đọc thêm ở đây.
Cập nhật 25 tháng 3: Từ nâng cấp Frontend đến vật lộn với Docker và đột phá
Bạn có thể đọc thêm ở đây.





