Bạn cài Ollama, pull một model 7B, wire vào agent CLI, gõ task đầu tiên. Mười phút sau terminal vẫn quay. Hai mươi phút sau bạn Ctrl+C, không có kết quả, không có log rõ ràng. Đây là kịch bản tôi đã chạy lại nhiều lần trên homelab Xeon E5 + Quadro K620 không driver, và mỗi lần nguyên nhân lại khác nhau một chút.

Bài này gom 5 failure mode hay gặp nhất khi dùng local LLM làm agent. Không phải lỗi cài đặt, không phải lỗi network. Là những kiểu fail mà chỉ khi bạn ngồi đọc log token-by-token hoặc nhìn htop mới phát hiện ra. Trước khi tin local agent reliable, bạn nên đã đụng qua cả 5.

Vì sao local agent dễ fail hơn cloud agent

Cloud agent (Claude Code, Cursor backend, Devin) chạy trên model frontier (Claude 4.5/4.6, GPT-5, Gemini 2.5+) đã được post-train rất kỹ để theo format tool-calling. Khi model trả lời sai format, harness của Anthropic/OpenAI/Google có hàng tá guardrail giấu kín: retry với constraint, regex parse cứu sản phẩm, server-side validation.

Local stack thì khác. Model 7B-13B Q4 quantize không thông minh bằng frontier, chưa kể có model như Gemma 1 7B base hoàn toàn không fine-tune cho function calling. Runner (Ollama, llama.cpp server) chỉ parse output thô, không có safety net. Agent harness (OpenCode, OpenClaw, gptme) chạy local thường thiếu retry logic, thiếu hard turn cap, thiếu observability.

Kết quả: lỗi nhỏ ở model output sẽ nhân ba lần (model + runner + harness) và bùng thành “treo terminal 20 phút không biết vì sao”.

Failure 1: agent loop hang vì model không biết khi nào dừng

Đây là failure phổ biến nhất. Agent loop hoạt động kiểu: model gọi tool, runner thực thi, kết quả nhét lại context, model gọi tool tiếp, lặp đến khi model trả “task done”. Khi model 7B base không biết format “task done” theo đúng schema harness chờ, nó tiếp tục gọi tool linh tinh hoặc lặp lại tool đã gọi.

Trên homelab CPU-only, mỗi turn LLM mất 30 đến 60 giây ở Gemma 7B (2-4 tok/s). Một agent loop 15 turn = 7 đến 15 phút. Tôi đã đo: OpenClaw + Gemma 7B trên Xeon E5 thường mất 20 đến 40 phút cho một task đơn giản, đa số do model retry cùng một bước.

Có hai biểu hiện phụ:

  • Loop Drift: model misinterpret tín hiệu kết thúc, sinh ra hành động lặp lại với state nội tại không nhất quán. Nguyên nhân vì model generate token-by-token, không tối ưu globally cho “đừng lặp lại”.
  • Idle hang: streaming response bị truncate (network glitch, runner crash giữa chừng), final message có stopReason: toolUse nhưng không có tool call block. Agent hiểu đây là một turn hợp lệ, tìm không thấy tool nào để chạy, im lặng đứng yên.

Cách phát hiện: bật log verbose ở harness (--verbose, LOG_LEVEL=debug). Đếm số turn. Nếu vượt 10 mà task chưa xong, gần như chắc là loop.

Cách giảm rủi ro:

  1. Hard cap số turn. Đặt MAX_TURNS=8 hoặc thấp hơn ở harness. Khi chạm cap, harness báo fail rõ ràng thay vì treo.
  2. Hard cap thời gian. Một wrapper timeout 600 ollama-agent ... sẽ ngắt sau 10 phút, đỡ phải Ctrl+C bằng tay.
  3. Repetition detector. So sánh hash của tool call gần nhất với 3 tool call trước. Nếu trùng 2 lần liên tiếp, dừng. Tôi viết một wrapper Python ~30 dòng làm việc này, đỡ phải sửa harness.

Failure 2: tool-call format mismatch (OpenAI vs Anthropic vs custom)

Có ba schema tool-calling chính đang dùng trong ecosystem 2026:

SchemaĐịnh dạngAi dùng
OpenAI function callingtool_calls, arguments là JSON stringOllama (default), llama.cpp /v1/chat/completions, gpt-oss, Qwen
Anthropic tool usetool_use block, arguments là JSON object, có tool_use_id match tool_result_idClaude, llama.cpp Anthropic Messages endpoint (mới 2026)
Custom (markdown / XML)Code block ```tool hoặc <tool>...</tool>gptme, một số agent frame cũ

Lý do mismatch hay xảy ra:

Lý do 1: model output không khớp với gì harness chờ. Ví dụ: Llama 3.1 8B fine-tune theo OpenAI format. Bạn dùng nó qua Claude Code (Anthropic format) với một adapter. Adapter convert sai vài field, harness reject toàn bộ conversation.

Lý do 2: argument type không khớp. llama.cpp đã có một bug nổi tiếng: llama-server trả arguments là JSON object trong khi OpenAI spec yêu cầu arguments là JSON string. Client nào strict-parse sẽ crash. Fix bằng cách thêm JSON.stringify(arguments) ở adapter, hoặc dùng version llama.cpp đã patch.

Lý do 3: tool_use_id mất match. Anthropic format yêu cầu mỗi tool_use block có ID, mỗi tool_result block phải reference cùng ID đó. Nếu adapter bỏ sót hoặc generate ID khác nhau ở hai bên, API reject với unexpected tool_use_id found in tool_result block. Cả Ollama lẫn llama.cpp đều phải tự generate ID khi convert ngược về Anthropic, dễ trật.

Cách phát hiện:

# Bật proxy log để xem raw request/response giữa harness và runner
mitmproxy -p 8081 --mode reverse:http://localhost:11434
# Trỏ harness vào localhost:8081 thay vì 11434

Đọc raw payload. Nếu thấy tool_calls mà harness mong tool_use, hoặc arguments là string mà parser mong object, bạn đã tìm thấy thủ phạm.

Cách giảm rủi ro:

  1. Match schema 1-1. Dùng harness OpenAI-compatible với Ollama (CLI gốc hoặc CrewAI). Đừng cố nhét Claude Code vào Ollama qua adapter trừ khi bạn sẵn sàng debug.
  2. Cập nhật llama.cpp. Anthropic Messages API endpoint mới ở llama.cpp 2026 convert nội bộ Anthropic format thành OpenAI format, giảm hẳn việc viết adapter tay.
  3. Chọn model fine-tune cho schema bạn cần. Llama 3.1+, Qwen 2.5+, Mistral Nemo đã post-train với OpenAI schema. Gemma base, Llama base, model 4B nhỏ thường không có. Đừng kỳ vọng tool-call hoạt động trên model chưa fine-tune.

Failure 3: OOM giữa generation vì KV cache phình theo context

Model weights là kích thước cố định. KV cache thì tăng tuyến tính theo độ dài context. Khi context vượt ngưỡng, KV cache đẩy tổng memory qua mức VRAM hoặc RAM, runner crash giữa generation.

Con số tham khảo 2026 cho 7B Q4_K_M:

  • 4K context: ~6 GB RAM tổng
  • 32K context: ~8 đến 9 GB RAM
  • 128K context: ~12 đến 16 GB RAM

Trên homelab 62 GB RAM, 7B-13B context dài chưa OOM. Nhưng nếu chạy 27B+ với 32K context, hoặc một laptop 16 GB RAM với model 7B + context 64K, bạn sẽ thấy oomkilled trong dmesg ngay khi context đi qua threshold.

Biểu hiện điển hình:

  • Conversation đi mượt 5 đến 10 turn đầu (context còn ngắn). Đột nhiên runner kill process giữa generation, không có error message rõ.
  • Kiểm tra dmesg | grep -i kill. Thấy Killed process <pid> (ollama) total-vm:... là OOM killer của kernel.
  • htop thấy RSS process Ollama tăng dần theo từng turn, không giảm.

Cách giảm rủi ro:

  1. Set context window phù hợp. Ollama mặc định num_ctx=2048 rất bé. Tăng lên 8192 hoặc 16384 nếu cần, nhưng đừng set 131072 nếu RAM không cho phép. OLLAMA_NUM_CTX=8192 ollama run ....
  2. GQA-aware model. Model dùng Grouped Query Attention (Llama 3.x, Mistral, Qwen 2.5+) tốn ít KV cache hơn model multi-head thông thường. Chọn model có GQA khi cần context dài.
  3. GPU offload nếu có VRAM. LM Studio và llama.cpp cho phép offload một phần layer hoặc KV cache xuống RAM hệ thống. Trên máy 8 GB VRAM + 32 GB RAM, có thể chạy Qwen3 14B context 32K bằng cách offload KV cache xuống RAM, đánh đổi tốc độ lấy không OOM.
  4. Quantize KV cache. llama.cpp hỗ trợ KV cache Q8 hoặc Q4, giảm bộ nhớ KV xuống ~50% với chất lượng vẫn chấp nhận được. Cờ --cache-type-k q4_0 --cache-type-v q4_0llama-server.

Failure 4: stop sequence không trigger, model “nói tràn”

Local model đôi khi sinh ra token vô tận, không tự dừng. Output kiểu: code block đúng, sau đó model tiếp tục viết thêm prose linh tinh, dán thêm code block khác, lặp lại đến khi đụng num_predict mặc định (thường 128 hoặc 256).

Có hai nguyên nhân chính:

Stop sequence mismatch. Mỗi model gia đình có token end-of-turn khác nhau. Llama 3 dùng <|eot_id|>, Qwen dùng <|im_end|>, ChatML khác. Nếu chat template ở runner cấu hình sai, hoặc bạn override stop sequence bằng cờ --stop "", model sinh xong response thật vẫn tiếp tục sinh tiếp.

Tokenization ambiguity. Khi model sinh ra prose chứa backtick fence ```, vài tokenizer trộn lẫn inline code và code block, gây loop vô hạn ở giữa generation. Đã có report ở VS Code và vài harness khác.

Cách phát hiện: chạy với --verbose để in raw token. Nếu thấy output đẹp xong vẫn tiếp tục sinh token, kiểm tra stop sequence đầu tiên.

Cách giảm rủi ro:

  1. Đừng override chat template. Dùng chat template default của model trên Ollama (ollama show <model> --template). Chỉ override khi bạn biết rõ format.
  2. Set num_predict hợp lý. Đặt num_predict=2048 cho task code, 512 cho task chat ngắn. Cap này là phòng tuyến cuối khi stop sequence fail.
  3. Repetition penalty. Ollama hỗ trợ repeat_penalty=1.1 đến 1.2. Trị cao quá làm output nghèo. Trị thấp quá không chặn được loop. Tinh chỉnh theo task.

Failure 5: generation lặp lại (repetition loop) ở model nhỏ

Khác failure 4 ở chỗ: model sinh ra một đoạn, rồi lặp lại y nguyên đoạn đó nhiều lần. Đặc trưng của model nhỏ (1B-4B) hoặc model quantize aggressive (Q2, Q3).

Nguyên nhân: token generation là một quá trình local “next token”, model không tối ưu global cho “đừng lặp lại toàn bài”. Khi model emit một pattern, pattern đó nằm trong context, làm xác suất sinh lại pattern đó cao hơn. Loop tự nhiên hình thành.

Greedy decoding (mặc định khi temperature=0) đặc biệt dễ rơi vào loop vì luôn chọn token xác suất cao nhất, không thoát được local minimum.

Cách giảm rủi ro:

  1. Tăng temperature lên 0.6 đến 0.8 cho task generic. Giữ temperature=0 chỉ khi cần output deterministic (test, eval).
  2. Beam search với early_stopping. llama.cpp có --beam-size 4 --early-stopping. Đắt hơn 4 lần về compute nhưng giải quyết được loop ở model nhỏ.
  3. Presence_penalty + frequency_penalty. OpenAI compatible API expose hai param này. presence_penalty=0.6, frequency_penalty=0.3 là điểm khởi đầu hợp lý.
  4. Up-quantize hoặc up-size. Nếu Gemma 2B Q4 lặp liên tục, thử Gemma 2B Q8 hoặc nâng lên Phi-3-mini 3.8B Q4. Đôi khi câu trả lời đơn giản là model quá nhỏ.

Checklist debug khi local agent treo

Khi terminal đứng yên hơn 2 phút, đi qua thứ tự sau:

  1. htop: process runner còn sống không? CPU % cao chứng tỏ vẫn đang generate. CPU % thấp + RAM tăng dần thì có thể đang load model lazy hoặc OOM sắp xảy ra.
  2. dmesg | tail -50: có oomkilled không? Có lỗi GPU driver không?
  3. journalctl -u ollama -n 100 (hoặc log tương đương): runner báo gì? inference compute id=cpu xác nhận CPU-only. Lỗi load model, lỗi context length thường xuất hiện ở đây.
  4. Verbose log của harness: đếm turn. Inspect tool call gần nhất. Có lặp tool không?
  5. mitmproxy giữa harness và runner: nếu nghi schema mismatch, đọc raw payload.
  6. Kiểm tra num_ctx vs token usage: nhiều harness có lệnh tokens hoặc /stats. Nếu sắp đầy context, gần như chắc sẽ OOM hoặc behave lạ ở turn tiếp theo.

Đi qua checklist này thường tìm ra nguyên nhân trong 5 phút thay vì restart blind nhiều lần.

Workaround thực dụng: dùng one-shot thay vì agent loop

Hands-on từ homelab của tôi: trên Xeon E5 CPU-only với Gemma 7B Q4, OpenClaw và OpenCode chạy agent loop 5-15 turn đều treo. Cùng hardware đó, gptme chạy one-shot (prompt vào, response ra, không loop) hoạt động ổn.

Lý do: gptme dùng markdown code block làm tool format thay vì OpenAI JSON. Gemma base xử lý được markdown. gptme single-process, không cache thrash. Mỗi tương tác là một LLM call duy nhất, không cộng dồn turn.

Bài học: nếu hardware local của bạn không đủ chạy frontier-grade tooling, đừng cố. Chia task thành các bước one-shot, người ngồi đầu prompt. Bạn dùng local LLM như “stack overflow trong terminal”, không phải “Claude Code”. Productivity vẫn cao, không treo, không tốn 30 phút đợi một loop fail.

Bước tiếp theo

Hết 5 failure mode + 1 workaround. Toàn bộ series 5 bài Local LLM 2026 đã đi từ chọn model, đo hardware, chọn runner, wire MCP, đến debug failure. Còn vài hướng đáng đào tiếp:

  • Self-host MCP server cùng máy với local LLM: cost zero, full privacy, nhưng cần debug nhiều hơn so với chạy MCP server cloud.
  • Hybrid stack: local model cho privacy-sensitive task, cloud frontier cho task khó. Phối hợp routing.
  • Upgrade GPU: RTX 3060 12GB là sweet spot 2026 cho local agent. Sẽ unlock thêm 70B model Q4 và context 32K-64K.

Tạm thời, nếu bạn vừa bị treo agent local lần đầu, đọc lại failure 1 và checklist debug. Lần thứ hai thường nhanh hơn nhiều.

Tham khảo