Ý tưởng: dùng một model LLM rất nhỏ (dưới 3B tham số), chạy local, ghép với một search engine. Python đóng vai trò điều phối và giới hạn model chỉ được search rồi tổng hợp kết quả, không được tự trả lời từ kiến thức nền hoặc suy luận ngoài nội dung search được.
File này là nhật ký tiến độ dự án (theo commit). Nghiên cứu nền/tham khảo
sâu hơn về từng chủ đề kỹ thuật cụ thể (không phải kết quả thực nghiệm của
dự án) nằm ở notes/ - xem notes/android-thermal-apis.md (2 lớp API
nhiệt Android, ý nghĩa từng loại cảm biến), notes/android-background-app-throttling.md
(Android hạ xung app chạy nền, dễ tưởng nhầm RAM/nhiệt),
notes/llamacpp-hardware-acceleration-android.md (CPU dotprod/i8mm, GPU
OpenCL, NPU Hexagon - cái nào đáng làm), notes/multi-hop-question-decomposition.md
(vì sao chọn regex+POS thay vì LLM để tách câu hỏi),
notes/termux-pgrep-pkill-exact-match-quirk.md (vì sao pkill -x không
dùng được trên máy này), notes/micro-llm-alternatives-to-qwen.md
(khảo sát model nhỏ khác ngoài Qwen2.5, tháng 9/2026), và
notes/qwen3-reasoning-chain-adaptive.md (thí nghiệm chain 3 bước tận dụng
reasoning của Qwen3 - chưa hoàn thiện, xem trạng thái trong note).
Model nhỏ hay bị hallucination khi trả lời trực tiếp từ tri thức nội tại vì dung lượng tham số thấp. Nếu bắt nó chỉ tổng hợp trên nội dung đã search được (grounding), độ chính xác tăng lên đáng kể so với hỏi trực tiếp.
Python không thể ép model "không được sai" ở tầng sinh token — cơ chế sinh text không có khái niệm đúng/sai. Hai việc Python thực sự làm được:
- Giới hạn đầu vào — model chỉ nhìn thấy đoạn text đã search được, không có gì khác trong context.
- Kiểm tra đầu ra sau khi sinh — bắt model trích dẫn nguyên văn từ nguồn thay vì diễn giải tự do, rồi đối chiếu câu trả lời với nguồn gốc để loại bỏ phần không có căn cứ.
Ràng buộc "chỉ được search, không được trả lời/suy luận" chỉ là ràng buộc mềm qua prompt và luồng điều khiển, model vẫn có thể lách nếu search không ra kết quả tốt.
- Model: chạy qua
llama.cpphoặc Ollama, bản quantize (Q4). Vùng an toàn cho chất lượng tổng hợp: 1.5B-3B tham số. Dưới 1B thường quá yếu để tổng hợp mạch lạc. - Search: SearXNG tự host hoặc DuckDuckGo, không cần API key.
- Điều phối: script Python — nhận câu hỏi, gọi search, lọc/rank snippet, đưa vào prompt ép trích dẫn, gọi model, validate output so với nguồn.
Chạy trên Termux/Android (điện thoại). Model từ 3B trở lên trên CPU sẽ chậm (vài chục giây mỗi câu trả lời). Cần cân nhắc giữa chất lượng và thời gian phản hồi khi chọn kích thước model.
Ý tưởng grounding LLM bằng search không mới, đã có nhiều dự án mã nguồn mở:
- Perplexica — clone mã nguồn mở của Perplexity, 20k+ stars, dùng SearXNG + LLM để search và tổng hợp có trích dẫn nguồn. Gần nhất với ý tưởng này về kiến trúc.
- Farfalle — self-host AI search engine, hỗ trợ local LLM (Llama 3, Gemma, Mistral, Phi-3) hoặc cloud.
- GroundedLLM — agent được "ground" bằng search + extract tool để giảm hallucination, gần với mục tiêu "chỉ được search, không được tự suy luận".
- Sova — RAG-based web search engine dùng Ollama + scraping Google, quy mô nhỏ gọn.
- RAGFlow — RAG engine lớn hơn, thiên về document search có agent, không tập trung vào web search.
Điểm khác biệt (gap) trong ý tưởng này: các dự án trên đều nhắm tới model tầm trung/lớn (Llama 3, GPT-4, Mistral) để có chất lượng tổng hợp tốt. Không có dự án nào tập trung cụ thể vào model rất nhỏ (dưới 3B) kết hợp ràng buộc kiến trúc cứng "chỉ search, không suy luận" — hầu hết chỉ ràng buộc qua prompt.
bash scripts/setup.sh # cài llama-cpp, pip deps, tải model (idempotent, có checksum)
llama-bench -m models/qwen2.5-1.5b-instruct-q4_k_m.gguf # baseline tok/s thô, không qua search
python search.py "câu hỏi" # test riêng phần search
python llm.py "câu hỏi" # search + model, in câu trả lời
python benchmark.py "câu hỏi" # như trên, kèm đo thời gian, ghi vào benchmark/results.csv
Model: Qwen/Qwen2.5-1.5B-Instruct-GGUF, bản q4_k_m (~1.1GB). Chạy qua
llama-server (HTTP, OpenAI-compatible, CPU only) — llm.py tự khởi
động server nếu chưa chạy và giữ nó chạy warm giữa các lần gọi tiếp theo.
Search: DuckDuckGo HTML endpoint, không cần API key.
Lý do không gọi llama-cli trực tiếp: ở chat mode, llama-cli echo lại
prompt vào stdout xen với câu trả lời (và cắt ngắn phần echo khi prompt
dài), không có ranh giới rõ ràng để tách câu trả lời bằng regex.
llama-server trả JSON sạch (choices[0].message.content + timings).
llama-bench baseline (không search, không qua HTTP): pp512 ~24 t/s,
tg128 ~6.8 t/s. Sau đó chạy full pipeline (search + server) cho 8 câu hỏi
đa dạng lĩnh vực, server giữ warm giữa các lần:
| câu hỏi | search_s | model_s | gen_tps | câu trả lời |
|---|---|---|---|---|
| Dân số VN 2026 | 1.12 | 28.3 | 8.7 | "102.457.889 người, theo đoạn trích [1]" |
| Tổng thống Mỹ hiện tại | 0.86 | 18.8 | 8.9 | "Donald Trump" |
| Giá vàng hôm nay | 0.86 | 53.2 | 0.5 | lặp lại tiêu đề snippet (hỏng) |
| Chiều cao Everest | 1.80 | 24.4 | 7.0 | "8.848,86 mét" |
| Tỷ giá USD/VND | 1.08 | 24.6 | 6.3 | "25.750đ" |
| World Cup 2026 | 0.93 | 24.3 | 6.3 | "Mỹ, Canada và Mexico" |
| Python là ngôn ngữ gì | 1.05 | 32.2 | 7.0 | tóm tắt đúng |
| Nhiệt độ Hà Nội hiện tại | 1.06 | 19.1 | 8.0 | "không có thông tin cụ thể... không được đáp ứng" |
Thống kê: search_s avg 1.09 (0.86-1.80), luôn nhanh và ổn định. model_s
avg 28.1 (18.8-53.2). gen_tps avg 6.60, median 7.00, nhưng có 1 outlier
rơi xuống 0.5 — trùng đúng lúc RAM pressure cao nhất (máy lúc đo chỉ
~1.6GB available/7.1GB, swap 5.2/11GB) và cũng là câu trả lời hỏng nhất.
Hai quan sát đáng chú ý:
- Ràng buộc "chỉ dùng snippet" hoạt động đúng thiết kế — câu hỏi về nhiệt độ Hà Nội, model tự nhận không đủ thông tin và từ chối trả lời thay vì bịa, đúng ý ban đầu của ý tưởng grounding.
- RAM pressure có thể ảnh hưởng cả chất lượng, không chỉ tốc độ — outlier 0.5 t/s trùng với câu trả lời tệ nhất (lặp tiêu đề snippet). Mới quan sát được 1 lần, n=8 chưa đủ để khẳng định tương quan, cần thêm dữ liệu.
compare_models.py chạy cùng 3 câu hỏi lần lượt trên mỗi model (dừng
server, đổi model, khởi động lại, để không chạy đồng thời 2 model):
| model | total_s (avg) | gen_tps (avg) |
|---|---|---|
| 1.5B | 30.1 | 7.01 |
| 0.5B | 10.0 | 17.12 |
0.5B nhanh hơn ~2.44x về gen_tps và giảm total_s xuống 1/3. Câu trả lời của 0.5B với 3 câu hỏi test (Everest, tổng thống Mỹ, Python) vẫn đúng và bám sát snippet, không thấy khác biệt rõ về chất lượng ở mức test nhỏ này — cần thử nhiều câu hỏi khó hơn (đa bước, số liệu) để thấy giới hạn thật của 0.5B.
So sánh 4 cặp câu hỏi cùng nội dung, một bằng tiếng Việt một bằng tiếng
Anh (Dân số VN 2026, Everest, World Cup 2026, thủ đô Pháp). 3/4
cặp cho chất lượng tương đương ở cả hai ngôn ngữ; chỉ 1 cặp (dân số VN)
tiếng Việt bị hỏng (model lặp lại câu hỏi, không ra số liệu) trong khi
tiếng Anh trả lời đúng. gen_tps giữa hai ngôn ngữ không khác biệt đáng
kể. Với n=4, chưa đủ để khẳng định tiếng Anh tốt hơn hệ thống — có thể chỉ
là 0.5B ngẫu nhiên yếu ở câu có số liệu phức tạp, không phải do ngôn ngữ.
Cần thêm dữ liệu nếu muốn kết luận chắc.
explore_formats.py chạy 8 dạng câu hỏi khác nhau (câu hỏi đầy đủ, có/không,
kiểu từ khóa như gõ vào ô search, so sánh, liệt kê, mở/rộng, ngoài phạm vi,
định nghĩa ngắn) qua 2 mode prompt:
- synth: model được phép ghép câu, miễn bám sát snippet (như hiện tại).
- extract: ép model chỉ được copy nguyên văn một đoạn trong snippet, không diễn giải, không ghép câu — cố tình cho nó hoạt động gần với một "công cụ tìm kiếm" hơn là một model đang suy luận, đúng ý tưởng gốc "chỉ được search, không được suy luận".
Kết quả: synth 6/8 đúng, extract chỉ 3/8 đúng rõ ràng. Ép 0.5B vào vai
"chỉ trích xuất nguyên văn" làm giảm độ tin cậy, không tăng — lỗi thường
gặp ở mode extract: model trả về [2] (chỉ số thứ tự snippet, không có
nội dung), hoặc trích tiêu đề snippet (trùng cấu trúc với câu hỏi) thay vì
nội dung trả lời thật. Ở cả 2 mode, khi search không có snippet đủ tốt,
model có xu hướng lặp lại câu hỏi thay vì tuân theo chỉ dẫn "nói rõ không
có thông tin" / in KHONG_CO — cho thấy 0.5B theo instruction có điều
kiện ("nếu... thì...") kém ổn định, đây là giới hạn thật của quy mô model,
không phải vấn đề prompt engineering.
Điểm tích cực: câu hỏi kiểu từ khóa ngắn (không phải câu hoàn chỉnh, ví dụ "dân số hà nội 2026") hoạt động tốt ở cả 2 mode — đúng với việc dùng model gần với một search engine thật.
explore_adaptive.py chạy 8 câu hỏi khó hơn mức đã test trước đó qua
answer_question_per_link: multi-hop (cần suy luận địa lý 2 bước), số liệu
biến động (giá vàng), so sánh cần tính toán, tin gần đây, phủ định + kiến
thức cần cập nhật, câu nên bị từ chối (riêng tư), tính toán nhiều mệnh đề
theo thời gian, và một câu tiếng Anh phức tạp. Chạy trên máy này cần
Termux ở foreground (xem lưu ý CPU throttling ở trên) - lúc đó tốc độ ổn
định 3-46s/câu, không còn timeout 20s.
Kết quả ban đầu (consensus_threshold=0.6, min_votes=3): 3/8 câu
"confident" nhưng sai - nguy hiểm nhất với một tool tự nhận là
"fact-check":
- "Thủ đô nước láng giềng phía bắc VN?" -> "Lào" (đúng là Trung Quốc; Lào ở phía Tây/Tây Nam) - agreement 0.67.
- "Anh/Pháp/Thụy Điển, nước nào không thuộc NATO?" -> "Anh" (thực ra sau 2024 cả 3 đều thuộc NATO - câu hỏi test có premise lỗi thời, nhưng model cũng không phát hiện ra) - agreement 0.67.
- "GDP đầu người 2025 so với 2015 tăng mấy lần?" -> chia sai, lấy USD chia cho năm (2015) thay vì GDP năm 2015 - agreement 0.6, đúng ngay ngưỡng.
Tăng consensus_threshold lên 0.75 và min_votes lên 4, chạy lại: case
GDP (lỗi tính toán ngẫu nhiên giữa các lần sinh) biến mất - đúng như dự
đoán, ngưỡng chặt hơn triệt tiêu được sự đồng thuận ngẫu nhiên. Nhưng 2
case địa lý/NATO vẫn confident=True và vẫn sai - agreement chạm đúng
0.75 vì nhiều nguồn/lần sinh cùng lặp lại một lỗi giống nhau một cách có hệ
thống (echo chamber giữa các trang SEO tiếng Việt copy nhau, hoặc model
liên tục nhầm cùng một kiểu địa lý).
Kết luận quan trọng: agreement/consensus voting chỉ triệt tiêu được sai
số ngẫu nhiên, độc lập giữa các lần sinh (như lỗi tính toán GDP) - không
triệt tiêu được sai số có hệ thống, tương quan giữa các nguồn (nhiều
nguồn cùng sai theo cùng một cách). Đây là giới hạn toán học của voting
theo đa số (majority voting không khử được correlated error), không phải
bug tham số - tăng threshold/min_votes cao hơn nữa cũng không chắc giải
quyết được nếu lỗi lặp lại ở >75% nguồn. Câu hỏi tốt tin: các case
"không đồng thuận" (giá vàng, so sánh Eiffel/Tokyo, Nobel 2026 - model bịa
"Nguyễn Thị Minh Khai", câu riêng tư, câu tiếng Anh) đều đúng bị đánh
confident=False, đúng ý thiết kế.
Đã cập nhật default trong adaptive.py: consensus_threshold=0.75,
min_votes=4 (giữ - vẫn có ích cho lỗi ngẫu nhiên dù không phải toàn bộ
giải pháp).
Thêm xử lý song song theo batch (N_PARALLEL=3 link/lúc, llama-server -np 3,
-c 3072) để chạy được max_n lớn trong thời gian hợp lý - phát hiện kèm 1
race condition thật: ensure_server() không thread-safe, nhiều thread cùng
gọi lúc server chưa sẵn sàng sẽ cùng tự Popen process mới, giành port. Đã
khóa bằng threading.Lock.
Chạy lại 3 câu hỏi còn sai với điều kiện khắc khe hơn nhiều:
max_n=15 (search trả 10), min_votes=8, consensus_threshold=0.85:
- Multi-hop ("thủ đô nước láng giềng phía bắc VN?"): đi qua 9 link,
agreement tăng lên 0.89 (v.11 test ở n nhỏ chỉ 0.67) - "Lào" vẫn sai,
nhưng tự tin hơn. Soi nội dung: 5/9 nguồn nói "Lào", 3/9 nói "Hà Nội"
(cũng sai), chỉ 1/9 nói đúng "Trung Quốc". Nhiều trang giáo án/SEO tiếng
Việt cùng lặp lại một lỗi giống nhau (có vẻ từ cùng dạng bài địa lý lớp 5)
- mở rộng search không cứu được vì lỗi nằm ở corpus bị lệch, không phải ở model hay ở tham số voting.
- NATO: agreement giảm xuống 0.40 (đúng, hết confident) - nguồn chia phe ~50/50 giữa "Anh" và "Thụy Điển" (khác nhau do bài viết cũ/mới, lỗi độc lập giữa nguồn, không phải cùng một lỗi lặp lại).
- GDP: vẫn confident=False (0.40) như ở lần tune trước, thêm lộ ra 2/10 nguồn cho đáp án khác hẳn ("tăng 7 lần") - tín hiệu bổ sung chưa khai thác.
Kết luận quan trọng nhất: hệ thống không phân biệt được "đồng thuận vì đúng" và "đồng thuận vì nhiều nguồn cùng lặp lại một lỗi giống nhau". Mở rộng max_n/min_votes/threshold chỉ giúp khi lỗi ngẫu nhiên/độc lập giữa nguồn (case NATO, GDP) - khi lỗi mang tính hệ thống ở cấp corpus search (case multi-hop), mở rộng tìm kiếm có thể khiến kết luận sai tự tin hơn, phản trực giác. Đây là giới hạn kiến trúc thật của "search + voting" với model nhỏ, không phải bug tham số - cần một cơ chế khác (ví dụ: đối chiếu với nguồn có uy tín cao hơn, hoặc phát hiện câu hỏi dạng multi-hop để hạ tin cậy mặc định) nếu muốn giải quyết, không chỉ chỉnh số.
agreement (cỡ cụm thắng / số link có phản hồi) không lộ ra khi nhiều
link bị timeout/lỗi, và không phân biệt được đồng thuận độc lập thật với
đồng thuận do trùng nguồn. Thêm 2 chỉ số vào output của
answer_question_per_link, tính hoàn toàn từ dữ liệu đã có, không tốn thêm
lượt gọi model:
- coverage = cỡ cụm thắng / tổng số link đã thử (kể cả timeout/lỗi,
qua counter
n_attempted) - khácagreementkhi có link fail; agreement cao mà coverage thấp nghĩa là nhiều link đã "rớt" trước khi tính đồng thuận. - bias = 1 - (số domain khác nhau / cỡ cụm thắng) trong cụm thắng, parse
hostname từ
urlcủa snippet (urllib.parse). 0 = mọi link trong cụm đến từ domain khác nhau (đồng thuận độc lập), gần 1 = cụm thắng chủ yếu dựa vào rất ít domain lặp lại (rủi ro echo-chamber).
Test lại case NATO: bias=0.0 - mọi domain trong cụm thắng khác nhau (2
phiên bản Wikipedia, ai-hay.vn, tuoitre.vn...) - khớp đúng, đây là bất đồng
thật giữa nguồn độc lập.
Test lại case multi-hop (chạy lại, search trả bộ link khác do DDG không ổn
định giữa các lần gọi - lần này đa số ra đúng "Bắc Kinh"): bias=0.12, khá
thấp, dù đã biết nội dung các nguồn tương quan cao (đều paraphrase cùng một
bài "Địa Lí lớp 5 - Bài 19"). Hạn chế thật đã lộ ra: bias theo domain chỉ
bắt được kiểu tương quan "cùng 1 site lặp lại", không bắt được kiểu sâu hơn
"nhiều domain độc lập nhưng cùng chép/paraphrase một nguồn gốc" (ví dụ cùng
một sách giáo khoa) - đây chính là dạng tương quan đã gây ra case "Lào" sai
mà agreement cao ở lần test trước. Muốn bắt được dạng này cần so khớp nội
dung snippet gốc (không chỉ domain), chưa làm.
Tái dùng _cluster() (đang dùng để gom câu trả lời giống nhau) cho
title+snippet gốc của các nguồn trong cụm thắng - content_bias = tỉ lệ
cụm nội dung lớn nhất / cỡ cụm thắng, cùng thang đo với bias (0 = mọi
snippet khác nhau thật, gần 1 = phần lớn là bản chép/diễn giải của nhau).
Test lại case multi-hop: bias (domain) = 0.0 (9 domain khác nhau), nhưng
content_bias = 0.5 - bắt được đúng cái domain-bias bỏ lỡ: nhìn snippet
thật, phần lớn đều mở đầu giống nhau kiểu "Bài 19: Các nước láng giềng của
Việt Nam..." / "Giáo án Địa lí lớp 5...", tức cùng một giáo án gốc dù đăng
trên domain khác nhau. Case NATO (nguồn thật độc lập): content_bias = 0.25,
thấp hơn rõ ràng - phân biệt được 2 case tốt.
Lưu ý quan trọng: content_bias không suy ra đúng/sai - test case dễ
"Thủ đô Việt Nam là gì?" (câu trả lời đúng) vẫn ra content_bias=0.4 vì
nhiều nguồn diễn đạt giống nhau khi cùng nói một fact đơn giản, phổ biến.
Đây chỉ là chỉ số phụ trợ đo độ độc lập của bằng chứng, không thay được
việc phải nhìn cả agreement + bias + content_bias cùng nhau.
Hướng đầu vào: thay vì search 1 lần cho cả câu hỏi rồi trông chờ voting/bias sửa lỗi hệ thống (không sửa được - xem case "thủ đô nước láng giềng phía bắc VN" ở trên), thử tách câu hỏi multi-hop thành các câu hỏi con để search riêng từng bước trước khi search. Thử 4 cách tiếp cận, từ để model tự sinh đến thuần quy tắc:
- LLM tự sinh câu hỏi con (mode
decompose, zero-shot): đúng format JSON 100%, nhưng bỏ sót chính case multi-hop cần tách nhất, sinh placeholder degenerate ("câu hỏi con 1"), hoặc mất thuộc tính khi tách câu so sánh (mất "chiều cao" khi tách "Eiffel cao hơn Tokyo Skytree bao nhiêu mét"). - LLM tự sinh, few-shot (3 ví dụ): sửa được 2 lỗi trên, nhưng lộ lỗi nguy
hiểm hơn - câu hỏi multi-hop tiếng Anh ("...borders Vietnam to the
north") bị model copy nhầm nội dung ví dụ ("phía Tây" từ ví dụ, thay
vì "phía Bắc" từ câu hỏi thật) - lỗi tự tin nhưng sai nội dung, nguy hiểm
hơn hẳn lỗi degenerate rõ ràng vì không lọc được bằng
score.py. - LLM chỉ gắn nhãn cụm có sẵn (mode
tag), Python tự ráp câu hỏi con: giảm việc "sinh" xuống còn "định vị" (gần giống mode extract). Tốt hơn về lý thuyết nhưng thực tế vẫn ~4/10 case ra tag rác hoặc JSON key trùng lặp, một case còn tự dịch cả câu hỏi tiếng Anh sang tiếng Việt rồi tag bản dịch. Điểm sáng: bắt đúng được case genitive ẩn (không có "của" tường minh) mà regex thuần không làm được. - Quy tắc thuần (regex + từ loại thật cho tiếng Anh), không gọi model:
decompose.py. Bắt 2 loại cấu trúc: (a) genitive lồng nhau "X của Y [là gì/là ai/ở đâu/là bao nhiêu]" / "What is X of Y" - chỉ tách khi Y cần tra cứu riêng; (b) so sánh 2 thực thể "A hơn B bao nhiêu" / so sánh theo 2 mốc thời gian "...năm A so với năm B tăng/giảm bao nhiêu lần". Tín hiệu "Y cần tra cứu riêng" khác nhau có chủ đích giữa 2 ngôn ngữ: tiếng Việt phân tích tính, so sánh nhất luôn dùng tiểu từ rời "nhất" nên 1 từ khóa bắt hết mọi tính từ; tiếng Anh biến hình "-est" theo từng từ ("tallest"/"largest"/"richest"...) nên từ khóa không đủ, phải dùng POS tag thật (nltk, nhãn JJS/RBS/WDT/WP) mới tổng quát hóa được. Kết quả: 10/10 case test (VN + EN, gồm cả 2 case so sánh nhất dùng từ gốc khác nhau) đúng, không case nào tách sai - đổi lại bỏ sót có chủ đích các case không theo cấu trúc đã liệt kê (vd nối bằng động từ như "...chơi cho câu lạc bộ nào?", hoặc genitive ẩn hoàn toàn không có "của").
Thử pyvi/underthesea (NLP tiếng Việt) trước khi chọn nltk+regex - bỏ vì
kéo theo build scipy/scikit-learn từ source (tự bootstrap cả cmake),
quá nặng cho Termux/Android, không đáng cho việc chỉ cần bắt 1-2 tiểu từ ngữ
pháp cố định.
Kết luận chung của cả 4 cách: độ tin cậy tỉ lệ nghịch với mức độ phải "hiểu"/"sinh" của model - quy tắc dựa tín hiệu hình thức có thật (từ nối, POS tag) không bao giờ tách sai (chỉ có thể bỏ sót), còn mọi mức độ để model tự suy luận/sinh chữ đều có tỷ lệ lỗi đáng kể, kể cả khi đã thu hẹp xuống "chỉ gắn nhãn".
Nối decompose.py vào cơ chế đồng thuận per-link (adaptive.py) - chain.py
(độc lập, chưa nối vào llm.py chính). Câu hỏi loại bridge (có {X})
thì chain tuần tự: search+extract câu hỏi con đầu, điền answer vào {X} của
câu hỏi con sau, search+extract tiếp. Loại comparison thì chạy 2 câu hỏi con
song song, không phụ thuộc nhau. Chưa có bước tổng hợp câu trả lời cuối cho
câu hỏi gốc (vd rút gọn/tính toán từ answer thô) - đó là việc của "đầu ra",
cố tình chưa làm ở giai đoạn này.
Test đầu tiên (case "thủ đô nước láng giềng phía bắc VN") lộ ngay 1 bug: answer
thô của bước 1 (đồng thuận thấp, dài dòng) vẫn bị nhét thẳng vào {X}, tạo
query bước 2 vỡ vụn. Vá bằng 2 việc:
- Cổng confidence trước khi chain: chỉ điền
{X}và chạy bước sau nếu bước trướcconfident=True(đồng thuận đã đạt ngưỡng) - không tự bịa rule mới, dùng thẳng tín hiệuadaptive.pyđã có sẵn. adaptive_entity.py- fork riêng củaadaptive.py, dùng prompt mớiextract_entity(llm.py) ép trả lời đúng 1 tên riêng thay vì 1 câu/cụm bất kỳ. Lý do fork: bridge entity luôn là tên ngắn (nước/thành phố/người), cònextract_unconditionalgốc để model tự do trích "câu/cụm phù hợp" nên hay lan man nhắc lại tiêu đề snippet trước khi vào nội dung - đã thử chỉ cắt ngắnn_predicttrước, không ăn thua (chỉ cắt cụt thói quen lan man giữa chừng, có lúc còn tạo "đồng thuận giả" khi nhiều link cùng bị cắt cụt ở cùng 1 điểm do chung tiêu đề bài viết) - phải chặn từ prompt. Kết quả: case "cà phê" agreement bước 1 tăng 0.12 → 0.75 (confident=True, ra đúng "Brazil"), tốc độ cũng nhanh hơn hẳn (n_predict=20thay vì 80).
Áp adaptive_entity luôn cho bước 2 khi khung ngoài cũng hỏi tên riêng (vd
"Thủ đô của {X} là gì") - route bằng _expects_number() (regex/từ khóa
"bao nhiêu"/"how much"/"how many"), giữ adaptive.py gốc khi khung ngoài hỏi
số liệu (vd "Dân số của {X} là bao nhiêu"). Case cà phê→Brazil→Brasília chạy
hết cả chain, bước 2 vẫn chưa đạt confident=True (0.38) nhưng agreement giờ
phản ánh đúng bất đồng nội dung thật (1 nguồn nhầm "Rio de Janeiro" - hiểu lầm
phổ biến có thật), không còn bị nhiễu do format câu trả lời khác nhau.
Case "thủ đô nước láng giềng phía bắc VN" (chạy lại với adaptive_entity):
đồng thuận về "Trung Quốc" tăng lên thành đa số (4/8, các link còn lại rải
rác Cam-pu-chia/Thái Lan/Việt Nam/1 answer degenerate) nhưng vẫn dưới ngưỡng
0.75 - hệ thống đúng đắn dừng lại, không chain tiếp. Đây là lần đầu
confident=False phản ánh đúng bất đồng thật giữa nguồn (SEO/giáo án tiếng
Việt vẫn lệch, xem phần Tune adaptive.py ở trên), không phải nhiễu do cách
trích xuất.
search.py trước chỉ lấy được ~10 kết quả (1 trang HTML của DDG, không phân
trang) - max_n lớn hơn 10 vô nghĩa. Thêm phân trang (theo dõi form ẩn DDG
trả kèm mỗi trang, có offset s + token phiên vqd, dừng khi đủ n hoặc hết
trang, giới hạn MAX_PAGES=6 chặn vòng lặp vô hạn) - lấy được 26-30 kết quả
độc lập (đã dedupe theo url) cho các câu hỏi test, gần chạm max_n=50 (DDG
không có đủ 50 kết quả khác nhau cho các câu hỏi này).
Chạy lại 3 câu hỏi khó có thể chain được (bridge "thủ đô nước láng giềng phía
bắc VN", comparison "Eiffel/Tokyo", comparison-theo-năm "GDP 2025 vs 2015")
với max_n=50, consensus_threshold=0.80 (thay vì 8/0.75 mặc định):
- Mẫu lớn hơn làm agreement giảm đúng hướng ở case vốn đã biết khó, không
tăng giả: case "nước láng giềng phía Bắc VN" agreement 0.50 (n=8) → 0.31
(n=29) -
bias/content_biasở case này đều thấp (~0.11), tức bất đồng đến từ nguồn thật sự độc lập, không phải echo-chamber - mẫu lớn hơn đang phơi bày đúng bất đồng thật, không phải nhiễu định dạng. Về mặt thống kê đây là dấu hiệu tốt, nhưng chỉ đúng khi lỗi giữa nguồn độc lập/ngẫu nhiên - README mục "đẩy đến giới hạn" ở trên từng ghi nhận trường hợp ngược lại (agreement tăng sai khi lỗi mang tính hệ thống/echo-chamber, mở rộng search càng vớt thêm bản sao của cùng 1 lỗi gốc) - phải nhìn cả agreement lẫn bias/content_bias cùng nhau, không kết luận chỉ từ n hay agreement một mình. - Case Eiffel/Tokyo lộ ra 1 điểm về cách diễn đạt câu hỏi: câu test dùng
"tháp Tokyo" (không ghi "Skytree" như lần test trước) nên hội tụ đúng về
Tokyo Tower (333m, tháp cũ,
confident=True, agreement 0.83) thay vì Tokyo Skytree (634m, tháp mới) - không phải lỗi hệ thống, chỉ làdecompose.pycắt nguyên văn thực thể từ câu hỏi gốc, không tự khử nhập nhằng tên. Vế Eiffel agreement giảm còn 0.14 (đi hết 28 link) - chiều cao bị báo lệch nhau nhiều giữa nguồn (300/324/325/330m, tùy có tính ăng-ten). - Case GDP 2025 vs 2015: vế 2025 đạt ngưỡng nhanh (6 link, agreement 0.83, ra "5.026 USD" nhất quán). Vế 2015 đi hết 30 link vẫn agreement 0.27, và câu trả lời thắng cuộc ("8,9 triệu đồng/người/tháng") là chỉ số khác hẳn (thu nhập bình quân/tháng, không phải GDP bình quân đầu người/năm) - nguồn bị lẫn đơn vị/chỉ số, mở rộng max_n không cứu được vì lỗi nằm ở nội dung nguồn, đúng loại vấn đề "corpus lệch" đã ghi nhận trước đó, không phải bug cơ chế.
Câu hỏi ban đầu: máy này (Samsung SM-S7110, chip Qualcomm SM8450/Snapdragon 8
Gen 1) có trick nào giống cách Claude Code chạy trên Termux (glibc-runner vá
ELF interpreter để chạy binary glibc trên Bionic) không? Kiểm tra thì không -
llama-server cài qua pkg install đã là native Bionic build sẵn
(built with Clang for Android aarch64), không có mismatch ABI nào để vá.
Nhưng tìm ra một vấn đề khác thật: gọi thẳng các hàm ggml_cpu_has_*()
trong libggml-cpu.so (qua Python ctypes, không cần build gì để kiểm tra)
thấy dotprod và matmul_int8 (= i8mm) đều trả 0, dù /proc/cpuinfo của
máy có đủ 2 feature này (asimddp, i8mm). Trên ARM, ggml không
runtime-dispatch kernel theo CPU như x86 - phải bật -march=...+dotprod+i8mm
lúc compile. Bản pkg install của Termux build generic (không chạy trên
đúng chip đích lúc build) nên không bật được.
Build lại từ source ngay trên máy (cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF -DLLAMA_BUILD_TESTS=OFF, giữ
GGML_NATIVE=ON mặc định) sửa được việc này: GGML_NATIVE=ON tự compile và
chạy thử thật một đoạn code dùng lệnh vdotq_s32/vmmlaq_s32 ngay trên
CPU đang build (check_cxx_source_runs) - vì build trực tiếp trên chip đích,
test này pass, bật đúng dotprod+i8mm. Verify lại bằng ctypes:
ggml_cpu_has_dotprod/_matmul_int8 chuyển từ 0 sang 1.
Đo bằng llama-bench (cùng model 0.5B, cùng máy, -t 4):
| pp128 (t/s) | tg64 (t/s) | |
|---|---|---|
Bản pkg install (generic) |
71.91 | 39.75 |
| Bản build từ source (dotprod+i8mm) | 90.75 (+26%) | 46.70 (+17%) |
llm.py trỏ thẳng LLAMA_SERVER_BIN sang binary build mới
(~/vendor/llama.cpp/build/bin/llama-server, nằm ngoài repo và ngoài
$PREFIX của Termux) thay vì bản trên PATH - không đè lên gói do pkg
quản lý, dễ rollback (đổi lại hằng số là xong) nếu pkg upgrade sau này
đổi ABI/version không tương thích.
Đã khảo sát nhưng loại bỏ vì không đáng công cho dự án này: GPU offload qua
OpenCL (Adreno 730 trên chip này chỉ "được ghi nhận là chạy được", chưa
verify chính thức - Adreno 750/830 trở lên mới được hỗ trợ chính thức; phải
ép KV-cache về f16, tắt flash-attention, dễ OOM RAM khi kết hợp --mlock);
Hexagon NPU (SM8450 chính thức "không được Qualcomm hỗ trợ" cho LLM NPU, dự
án cộng đồng khả dụng duy nhất dùng ExecuTorch .pte - khác hoàn toàn
toolchain GGUF/llama-server đang dùng, phải export lại model từ đầu).
Bản chạy được đầu tiên hoàn chỉnh: search + model (server warm) + benchmark
ghi log. Đã test: 8 câu hỏi đa dạng lĩnh vực với 1.5B; so sánh tốc độ 1.5B
vs 0.5B; tiếng Anh vs tiếng Việt; 8 dạng câu hỏi khác nhau ở 2 mode prompt
(synth/extract) với 0.5B; tune adaptive.py (per-link consensus) với 8 câu
hỏi khó hơn; khảo sát phân rã câu hỏi multi-hop trước khi search (4 cách,
chốt ở quy tắc thuần regex+POS); nối vào chain.py (search+extract theo
từng câu hỏi con, có cổng confidence + fork adaptive_entity.py cho bridge
entity, xem 2 mục trên) - chạy được, chưa nối vào llm.py chính, chưa có
bước tổng hợp câu trả lời cuối; build lại llama-server từ source để bật
dotprod/i8mm bị bản pkg install bỏ phí (+17-26% t/s, xem mục trên).
Phát hiện quan trọng nhất: ép model nhỏ chỉ được "search, không được suy luận" theo nghĩa chặt (mode extract) làm giảm độ tin cậy so với cho nó tổng hợp nhẹ (mode synth) — 0.5B không đủ khả năng theo instruction có điều kiện phức tạp một cách ổn định. Đây ngược với giả định ban đầu của ý tưởng, và là giới hạn thật của quy mô model, không phải vấn đề prompt.
Phát hiện thứ hai (từ tune adaptive.py): consensus voting giữa nhiều nguồn/lần sinh chỉ khử được lỗi ngẫu nhiên, không khử được lỗi hệ thống lặp lại giống nhau giữa các nguồn - "confident" không đồng nghĩa "đúng" ở các câu multi-hop hoặc dùng kiến thức có thể lỗi thời.
Bước tiếp theo hợp lý: nối chain.py vào llm.py chính (câu hỏi tách
được thì chain, không tách được thì chạy như cũ); test nhánh _expects_number
(route câu hỏi số liệu về adaptive.py gốc) với 1 case bridge thật sự đủ
confident ở bước 1 để đi tới bước 2 - chưa có case nào verify được nhánh này
bằng thực nghiệm; nghĩ bước tổng hợp câu trả lời cuối cho câu hỏi gốc (đầu ra,
cố tình chưa làm); thử mode extract với model 1.5B (có tuân theo instruction
có điều kiện tốt hơn không?); chạy nhiều câu hỏi hơn để xác nhận tương quan
RAM pressure/chất lượng câu trả lời; thử câu hỏi tiếng Anh/Việt với n lớn hơn
để kết luận chắc về sự khác biệt ngôn ngữ; thử lại 2 case multi-hop/NATO còn
sai với model 1.5B để xem lỗi có phải do quy mô model hay do search/snippet
chất lượng thấp.