Việc phụ thuộc hoàn toàn vào các API đám mây (như OpenAI hay Anthropic) mang lại nhiều rủi ro lớn cho các ứng dụng doanh nghiệp: rò rỉ dữ liệu nội bộ nhạy cảm, chi phí sử dụng tăng vọt theo số lượng token đầu vào, và rủi ro ngừng hoạt động khi mất kết nối mạng Internet.

Giải pháp là Cục bộ hóa AI (Local LLM) — chạy trực tiếp các mô hình ngôn ngữ lớn nguồn mở ngay trên máy tính của bạn, qua công cụ Ollama. Đây cũng là bài đầu tiên trong series mà chương trình gọi tới một máy chủ LLM thật, chứ không phải bản giả lập như Bài 11 và 12. Chúng ta sẽ dựng máy chủ đó, đo xem mô hình chiếm bao nhiêu bộ nhớ và sinh được bao nhiêu token mỗi giây, rồi tự viết một chat client stream chữ thời gian thực hoàn toàn offline.

✅ Bạn cần gì trước khi bắt đầu
Phần mềm: cài Ollama từ ollama.com/download (có bản macOS, Windows, Linux). Sau khi cài, tải về ít nhất một mô hình — ollama pull qwen2.5:7b chẳng hạn. Lưu ý dung lượng: một mô hình 7B lượng tử hóa 4-bit nặng khoảng 4-5 GB, nên lần tải đầu tiên mất vài phút và cần chừng đó dung lượng đĩa trống. Đây là lần duy nhất trong series cần tải dữ liệu lớn.

Thư viện Python: không cần cài gì — dự án chỉ dùng urllibjson trong thư viện chuẩn, cố ý không dùng requests hay gói ollama, để bạn nhìn thấy chính giao thức HTTP thay vì để thư viện che đi.

Kiến thức cần có: Bài 11 cho mảng messages phân vai — Ollama dùng đúng định dạng đó, nên mọi thứ bạn học ở Bài 11 chuyển sang đây nguyên vẹn.

13.1 Vì sao cần chạy LLM cục bộ (Local LLM)?

Sự bùng nổ của các mô hình nguồn mở chất lượng cao (như Llama của Meta, Gemma của Google, Qwen của Alibaba, Mistral của Pháp) đã thay đổi hoàn toàn cuộc chơi. Giờ đây, chỉ với một máy tính cá nhân tầm trung, bạn đã có thể sở hữu một trợ lý AI hoạt động độc lập.

Các ưu điểm khi cục bộ hóa AI:

  • Bảo mật dữ liệu tuyệt đối: Mọi thông tin trò chuyện, tài liệu doanh nghiệp hay mã nguồn bảo mật chỉ chạy trong RAM của máy tính cục bộ. Không một byte dữ liệu nào bị gửi lên các máy chủ bên ngoài. Với dữ liệu như email khách hàng ở Bài 12, đây không phải tiện lợi mà là điều kiện pháp lý.
  • Không có hóa đơn theo token: Không còn nỗi lo về hóa đơn API cuối tháng. Bạn suy luận bao nhiêu tùy ý mà chi phí biên bằng không.
  • Không phụ thuộc mạng: Hoạt động ổn định ở những khu vực không có sóng Internet hoặc trên các hệ thống mạng nội bộ cô lập (Intranet).
⚠️ Cạm bẫy: "miễn phí" không có nghĩa là "không giới hạn"
Chi phí biên bằng không, nhưng thông lượng thì không hề vô hạn. Đo trên chính máy viết bài này (MacBook, Apple Silicon, mô hình 7B lượng tử 4-bit): 36 đến 48 token mỗi giây tùy độ dài câu trả lời. Lấy tròn 40, muốn sinh một triệu token thì cần khoảng 7 giờ chạy liên tục, và trong suốt thời gian đó GPU bị chiếm dụng nên máy nóng và pin tụt nhanh. So sánh: một API đám mây phục vụ hàng trăm yêu cầu song song. Cục bộ hóa đổi tiền lấy thời gian và phần cứng, chứ không phải bữa trưa miễn phí.
⚠️ Cạm bẫy: Kỳ vọng khả năng suy luận tương đương mô hình đám mây
Một sai lầm phổ biến là kỳ vọng một mô hình local nhỏ tầm 7 tỷ đến 8 tỷ tham số (7B/8B) chạy trên laptop có khả năng tư duy logic ngang bằng các mô hình đám mây lớn hơn hàng trăm lần. Mô hình local nhỏ rất mạnh về trích xuất thông tin, tóm tắt và phân loại dữ liệu — chính là những tác vụ ở Bài 12 — nhưng sẽ bộc lộ điểm yếu rõ rệt khi giải các bài toán logic hoặc lập trình nhiều bước.

13.2 Giới thiệu Ollama & Quản lý mô hình

Trong quá khứ, việc tự cấu hình để chạy một mô hình AI cục bộ là cực kỳ phức tạp: bạn phải cài đặt driver đồ họa CUDA, thư viện PyTorch, tải các file trọng số hàng chục GB và biên dịch code C++.

Ollama đóng gói tất cả các thư viện suy luận tối ưu nhất (nhân là llama.cpp) vào một ứng dụng chạy nền gọn nhẹ duy nhất, và tự động phát hiện phần cứng để kích hoạt tăng tốc:

  • Trên macOS: dùng Metal, API đồ họa cấp thấp của Apple, để tận dụng GPU tích hợp của chip Apple Silicon (M1 trở lên).
  • Trên Windows/Linux: dùng CUDA cho card đồ họa NVIDIA, hoặc ROCm cho card AMD.
  • Không có GPU phù hợp: vẫn chạy được bằng CPU, chỉ là chậm hơn nhiều — mục 13.3 sẽ cho thấy chậm đến mức nào và vì sao.

Các câu lệnh quản lý Ollama CLI kinh điển:

Terminal
# Download a model and start chatting with it immediately
ollama run qwen2.5:7b

# Only download it, without starting a chat
ollama pull gemma2

# List the models stored on this machine
ollama list

# Show which models are loaded in memory right now, and on what hardware
ollama ps

# Unload a model from memory (the file stays on disk)
ollama stop qwen2.5:7b

# Delete a model to reclaim disk space
ollama rm qwen2.5:7b

Hai lệnh hay bị bỏ qua là ollama psollama stop, và chúng chính là công cụ để hiểu mục tiếp theo: mô hình nằm ở đâu, chiếm bao nhiêu, và ai đang tính toán cho nó.

13.3 Bộ nhớ và tốc độ: con số quyết định mọi thứ

Mọi hướng dẫn về local LLM đều nói "cần đủ VRAM", nhưng ít chỗ nói rõ đủ là bao nhiêu và vì sao con số đó lớn hơn dung lượng file. Hãy bắt đầu từ lý thuyết rồi đối chiếu với số đo thật.

💡 Khái niệm lượng tử hóa trọng số (Quantization) là gì?
Mặc định, các trọng số nơ-ron của mô hình được lưu dưới định dạng số thực dấu phẩy động 16-bit hoặc 32-bit (FP16/FP32). Điều này đòi hỏi dung lượng lưu trữ và VRAM cực kỳ lớn. Quantization (Lượng tử hóa) là kỹ thuật nén mô hình bằng cách ép các số thực này về dạng số nguyên 4-bit hoặc 8-bit (INT4/INT8). Kỹ thuật này giảm dung lượng mô hình đi 4-8 lần mà chỉ làm suy giảm một lượng nhỏ chất lượng, cho phép chạy được trên phần cứng dân dụng. Nhãn Q4_K_M bạn thấy trong ollama list chính là tên một sơ đồ lượng tử hóa 4-bit cụ thể.
📐 Công thức tính dung lượng, và sai số của nó khi đối chiếu thực tế
Công thức: dung lượng (byte) $\approx$ số tham số $\times$ số bit mỗi tham số $\div 8$. Với mô hình 7,6 tỷ tham số dùng ở dự án cuối bài:
  • FP32 (32-bit): $\approx 30.40$ GB.
  • FP16 (16-bit): $\approx 15.20$ GB.
  • INT8 (8-bit): $\approx 7.60$ GB.
  • INT4 (4-bit): $\approx 3.80$ GB.
Nhưng file thật mà Ollama báo là 4.68 GB, tức lớn hơn con số lý thuyết 23%. Phần chênh đến từ việc Q4_K_M không lượng tử hóa đồng loạt: các lớp nhạy cảm (đặc biệt là lớp nhúng từ Embedding và một phần các lớp attention) được giữ ở độ chính xác cao hơn 4-bit để tránh suy giảm chất lượng. Chữ "M" trong tên chính là "Medium" — mức thỏa hiệp giữa dung lượng và chất lượng.

Đến đây mới là phần thường bị bỏ sót. Dung lượng file không phải lượng bộ nhớ mô hình chiếm khi chạy. Chạy ollama ps ngay sau khi hỏi mô hình một câu, trên chính máy viết bài này:

Terminal
$ ollama ps
NAME                ID              SIZE      PROCESSOR    CONTEXT    UNTIL
qwen2.5-coder:7b    dae161e27b0e    6.6 GB    100% GPU     32768      4 minutes from now

File trên đĩa là 4.68 GB, nhưng khi chạy nó chiếm 6.6 GB. Gần 2 GB chênh lệch là KV cache: bộ đệm lưu các vector Key và Value của mọi token trong cửa sổ ngữ cảnh, đúng những K và V bạn đã gặp ở Bài 10. Cửa sổ càng dài thì bộ đệm càng lớn — ở đây cấu hình 32768 token, và đó là lý do quy tắc ngón tay cái luôn đòi bộ nhớ trống nhiều hơn dung lượng file.

⚡ Ranh giới quyết định tốc độ: cột PROCESSOR
Dòng 100% GPU ở trên là con số quan trọng nhất trong cả bài. Nếu bộ nhớ đồ họa không đủ chứa trọn mô hình cộng KV cache, Ollama buộc phải để một phần chạy trên CPU, và ollama ps sẽ hiện dạng 60% GPU / 40% CPU. Khi đó tốc độ không giảm 40% mà giảm hàng chục lần, vì mỗi token sinh ra phải chờ phần chạy trên CPU — vốn có băng thông bộ nhớ thấp hơn nhiều lần.

Quy tắc ước lượng bộ nhớ trống cần có:
  • Mô hình 7B/8B lượng tử 4-bit (file 4-5 GB): cần tối thiểu 8 GB.
  • Mô hình 70B lượng tử 4-bit (file khoảng 40 GB): cần tối thiểu 48 GB.
Nếu bạn thấy máy chạy chậm bất thường, hãy chạy ollama ps trước khi đổ lỗi cho mô hình — cột PROCESSOR trả lời câu hỏi đó ngay.

13.4 Tích hợp API Ollama vào ứng dụng

Khi khởi động, Ollama tự động mở một REST API nội bộ tại http://localhost:11434. Đây là điểm làm cho Ollama hữu ích với lập trình viên: mọi thứ bạn viết ở Bài 11 và 12 chuyển sang đây gần như nguyên vẹn, chỉ đổi URL và bỏ header xác thực.

Endpoint chính để gửi yêu cầu chat là /api/chat, và cấu trúc thân request rất quen thuộc:

request body
{
  "model": "qwen2.5:7b",
  "messages": [{ "role": "user", "content": "Why is the sky blue?" }],
  "stream": true
}

Đúng mảng messages ba vai trò của Bài 11. Điểm khác nằm ở "stream": true: thay vì đợi mô hình viết xong cả câu trả lời rồi mới trả về một khối JSON, Ollama gửi về từng dòng JSON độc lập, mỗi dòng chứa một mẩu chữ vừa sinh ra. Với tốc độ khoảng 40 token mỗi giây, một câu trả lời 200 token mất gần 5 giây — stream biến 5 giây im lặng thành chữ chạy dần ngay lập tức.

🧠 Ba dòng stream đầu tiên và dòng cuối cùng, bắt trực tiếp từ Ollama
Dự án cuối bài in ra ba dòng đầu của luồng dữ liệu (đã bỏ hai trường modelcreated_at lặp lại ở mọi dòng cho dễ đọc):
{"message": {"role": "assistant", "content": "The"}, "done": false}
{"message": {"role": "assistant", "content": " sky"}, "done": false}
{"message": {"role": "assistant", "content": " appears"}, "done": false}
Ghép tuần tự: "The""The sky""The sky appears". Vòng lặp for raw in response đọc từng dòng, trích content rồi ghi thẳng ra console bằng sys.stdout.write — không gộp chuỗi trong bộ nhớ, nên chữ hiện ra đúng nhịp mô hình sinh.

Dòng cuối cùng (done: true) có nội dung rỗng, nhưng không hề vô dụng — nó mang toàn bộ đồng hồ đo:
{"total_duration": 2067325583, "load_duration": 1502167333,
 "prompt_eval_count": 43, "prompt_eval_duration": 203854000,
 "eval_count": 13, "eval_duration": 358924000}
Đơn vị là nano-giây. Từ đây tính ra tốc độ thật: $13 \div (358924000 \div 10^9) \approx 36.2$ token mỗi giây. Đây là cách bạn đo máy mình thay vì tra bảng benchmark của người khác.

13.5 Dự án thực hành bài 13: Chat client offline có đo đạc

Dự án là một script Python nói chuyện trực tiếp với Ollama trên máy bạn. Nó làm bốn việc: hỏi máy chủ xem có những mô hình nào, chọn một mô hình thực sự tồn tại, stream câu trả lời ra màn hình, và in ra tốc độ đo được từ dòng cuối của luồng dữ liệu.

💡 Vì sao script tự dò mô hình thay vì viết cứng tên
Cách hỏng phổ biến nhất của loại script này là viết cứng "model": "llama3". Nếu máy bạn chưa pull đúng cái tên đó, máy chủ trả về HTTP 404 kèm thông báo model 'llama3' not found — Ollama vẫn đang chạy hoàn hảo, chỉ là thiếu mô hình. Nhưng trong Python, HTTPError là lớp con của URLError, nên một khối except urllib.error.URLError đặt trước sẽ nuốt luôn lỗi 404 và in ra "không kết nối được tới Ollama". Người học đi khởi động lại Ollama, và tất nhiên không giải quyết được gì.

Vì vậy script này gọi /api/tags trước để lấy danh sách mô hình có thật, và bắt HTTPError trước URLError để hai tình huống ra hai thông báo khác nhau.
local_chat.py
"""Lesson 13 project: talk to a local Ollama server, and measure it.

Run:  python3 local_chat.py
Needs: Ollama running, plus at least one model pulled (`ollama pull qwen2.5:7b`).

Standard library only - no `requests`, no `ollama` package. The point is to see
the HTTP stream itself rather than have a library hide it.
"""

import json
import sys
import urllib.error
import urllib.request

OLLAMA = "http://localhost:11434"
PREFERRED = ["qwen2.5-coder:7b", "qwen2.5:7b", "llama3.2", "llama3.1", "gemma2"]


def list_models():
    """Ask the server which models are pulled. Returns [] if it is not running."""
    try:
        with urllib.request.urlopen(f"{OLLAMA}/api/tags", timeout=5) as response:
            payload = json.loads(response.read())
    except urllib.error.URLError as exc:
        print(f"Cannot reach Ollama at {OLLAMA} - {exc.reason}")
        print("Start the Ollama app (or run `ollama serve`) and try again.")
        return []
    return payload.get("models", [])


def pick_model(models):
    """Choose a model that actually exists here, instead of hardcoding a name.

    Hardcoding "llama3" is the most common way this script fails for a reader:
    the server is running fine, the model simply was never pulled.
    """
    names = [m["name"] for m in models]
    for wanted in PREFERRED:
        for name in names:
            if name == wanted or name.startswith(wanted + ":"):
                return name
    return names[0] if names else None


def report_models(models):
    """Print what is installed, with the size and quantisation of each."""
    print("=== Models available on this machine ===")
    for model in models:
        details = model.get("details", {})
        print(f"  {model['name']:<30} {model['size'] / 1e9:5.2f} GB"
              f"  params={details.get('parameter_size', '?'):>7}"
              f"  quant={details.get('quantization_level', '?')}")
    print()


def stream_chat(prompt, model, show_raw_lines=0):
    """Send one chat request and print tokens as they arrive.

    Returns the final `done: true` object, which carries the timing counters.
    """
    body = json.dumps({
        "model": model,
        "messages": [{"role": "user", "content": prompt}],
        "stream": True,
    }).encode("utf-8")
    request = urllib.request.Request(
        f"{OLLAMA}/api/chat", data=body,
        headers={"Content-Type": "application/json"},
    )

    print(f"[{model}] {prompt}")

    final = {}
    kept = []
    try:
        with urllib.request.urlopen(request) as response:
            print("  ", end="")
            sys.stdout.flush()
            for raw in response:
                line = raw.decode("utf-8").strip()
                if not line:
                    continue  # a blank line is not JSON; json.loads() would raise
                chunk = json.loads(line)
                if len(kept) < show_raw_lines:
                    # Same line, minus two fields that repeat on every chunk,
                    # so the part that changes stays readable at this width.
                    kept.append({k: v for k, v in chunk.items()
                                 if k not in ("model", "created_at")})
                sys.stdout.write(chunk.get("message", {}).get("content", ""))
                sys.stdout.flush()
                if chunk.get("done"):
                    final = chunk
    except urllib.error.HTTPError as exc:
        # NOT the same failure as the server being down, and the message must
        # say so. HTTPError is a subclass of URLError, so the order matters:
        # catching URLError first would swallow this and blame the connection.
        detail = json.loads(exc.read() or b"{}").get("error", "no detail given")
        print(f"\n  the server answered HTTP {exc.code}: {detail}")
        print(f"  Ollama is running. Pull the model first: `ollama pull {model}`")
        return {}
    except urllib.error.URLError as exc:
        print(f"\n  cannot reach Ollama at {OLLAMA} - {exc.reason}")
        print("  Start the Ollama app (or run `ollama serve`) and try again.")
        return {}
    print()
    for number, chunk in enumerate(kept, 1):
        print(f"  chunk {number}: {json.dumps(chunk, ensure_ascii=False)}")
    if kept and final:
        counters = {k: v for k, v in final.items()
                    if k.endswith(("_count", "_duration"))}
        print(f"  last chunk carries the counters: "
              f"{json.dumps(counters, ensure_ascii=False)}")
    return final


def report_speed(final):
    """Turn the counters in the last chunk into numbers you can compare."""
    if not final:
        return
    tokens = final.get("eval_count", 0)
    eval_ns = final.get("eval_duration", 0)
    load_ns = final.get("load_duration", 0)
    prompt_tokens = final.get("prompt_eval_count", 0)
    if not eval_ns:
        return
    print(f"  generated {tokens} tokens in {eval_ns / 1e9:.2f}s"
          f"  ->  {tokens / (eval_ns / 1e9):.1f} tokens/s")
    print(f"  prompt was {prompt_tokens} tokens;"
          f" loading the model took {load_ns / 1e9:.2f}s")
    print()


def main():
    models = list_models()
    if not models:
        print("No models found. Pull one first, for example:")
        print("  ollama pull qwen2.5:7b")
        return
    report_models(models)

    model = pick_model(models)

    # First call: show the raw stream lines, so the wire format is visible.
    print("=== What the stream actually looks like ===")
    final = stream_chat("Why is the sky blue? Answer in under 12 words.",
                        model, show_raw_lines=3)
    report_speed(final)

    # Second call: the same model is already loaded, so load_duration collapses.
    print("=== Same model, second call ===")
    final = stream_chat("Name three primary colours, comma separated.", model)
    report_speed(final)

    # A model that is certainly not installed, to see the right error message.
    print("=== Asking for a model that was never pulled ===")
    stream_chat("hello", "definitely-not-a-real-model")


if __name__ == "__main__":
    main()

Chạy nó ra như sau — đây là kết quả thật trên máy viết bài, không phải mô phỏng:

Terminal
=== Models available on this machine ===
  bge-m3:latest                   1.16 GB  params=566.70M  quant=F16
  qwen2.5-coder:7b                4.68 GB  params=   7.6B  quant=Q4_K_M
  qwen2.5:14b-instruct-q4_K_M     8.99 GB  params=  14.8B  quant=Q4_K_M
  translategemma:latest           3.30 GB  params=   4.3B  quant=Q4_K_M

=== What the stream actually looks like ===
[qwen2.5-coder:7b] Why is the sky blue? Answer in under 12 words.
  The sky appears blue because of Rayleigh scattering of sunlight.
  chunk 1: {"message": {"role": "assistant", "content": "The"}, "done": false}
  chunk 2: {"message": {"role": "assistant", "content": " sky"}, "done": false}
  chunk 3: {"message": {"role": "assistant", "content": " appears"}, "done": false}
  generated 13 tokens in 0.36s  ->  36.2 tokens/s
  prompt was 43 tokens; loading the model took 1.50s

=== Same model, second call ===
[qwen2.5-coder:7b] Name three primary colours, comma separated.
  Red, Blue, Green
  generated 6 tokens in 0.13s  ->  47.2 tokens/s
  prompt was 37 tokens; loading the model took 0.14s

=== Asking for a model that was never pulled ===
[definitely-not-a-real-model] hello
  the server answered HTTP 404: model 'definitely-not-a-real-model' not found
  Ollama is running. Pull the model first: `ollama pull definitely-not-a-real-model`
🔢 Con số thật: vì sao lượt gọi đầu tiên luôn chậm hơn
So hai dòng loading the model took: lượt đầu 1.50 giây, lượt sau 0.14 giây — chênh nhau hơn 10 lần. Lượt đầu Ollama phải nạp 4.68 GB trọng số từ đĩa vào bộ nhớ đồ họa; lượt sau mô hình đã nằm sẵn ở đó. Đây là lý do lần chat đầu tiên trong ngày luôn có cảm giác "đơ" vài giây, và cũng là lý do Ollama giữ mô hình trong bộ nhớ thêm 5 phút sau lần dùng cuối thay vì giải phóng ngay. Muốn tự kiểm chứng: chạy ollama stop <tên-mô-hình> rồi chạy lại script — con số 1.50 giây sẽ quay lại.

Lưu ý: khác với các bài trước, đầu ra của bài này không cố định. Đây là mô hình thật đang lấy mẫu thật, nên câu chữ và tốc độ trên máy bạn sẽ khác — phần cứng khác, mô hình khác. Cái cần giống là hình dạng: ba dòng chunk, đồng hồ đo ở dòng cuối, và lượt đầu chậm hơn lượt sau.

Cách chạy dự án này trên máy bạn

  1. Cài Ollama, rồi tải một mô hình: ollama pull qwen2.5:7b (khoảng 4.7 GB). Kiểm tra bằng ollama list.
  2. Chạy: python3 local_chat.py. Script tự tìm mô hình trong danh sách PREFERRED; nếu không có cái nào khớp, nó lấy mô hình đầu tiên bạn có.
  3. Rồi thử phá nó theo ba cách:
    • Tắt hẳn ứng dụng Ollama rồi chạy lại. Bạn nhận được thông báo "cannot reach Ollama" — khác hẳn thông báo 404 ở cuối màn hình trên. Hai lỗi khác nhau phải nói hai câu khác nhau.
    • Trong stream_chat, đổi thứ tự hai khối except, đặt URLError lên trước. Lỗi 404 lập tức bị báo nhầm thành lỗi kết nối — đúng cái bẫy mô tả ở trên, và bạn thấy nó bằng mắt thay vì phải tin.
    • Nếu máy bạn có mô hình lớn hơn (14B trở lên), đưa tên nó lên đầu PREFERRED rồi chạy lại và so tokens/s. Sau đó chạy ollama ps để xem cột PROCESSOR — nếu nó không còn là 100% GPU nữa, bạn vừa nhìn thấy đúng ranh giới ở mục 13.3.

Tóm tắt bài học & Cầu nối kiến thức

🔑 Bài học đạt được:
  • Đạt được: Chạy một mô hình ngôn ngữ lớn offline 100% trên máy cá nhân bằng Ollama, và gọi nó từ Python chỉ bằng thư viện chuẩn.
  • Đạt được: Đọc và xử lý luồng stream JSON theo dòng, kể cả dòng cuối chứa đồng hồ đo.
  • Đạt được: Đo được tốc độ thật của máy mình (token/giây) và giải thích được vì sao lượt gọi đầu chậm hơn.
  • Đạt được: Phân biệt bộ nhớ file và bộ nhớ khi chạy, và dùng ollama ps để biết mô hình đang chạy trên GPU hay đã tràn xuống CPU.

Cầu nối bài tiếp theo: Giờ mô hình đã chạy trên máy bạn và không gửi dữ liệu đi đâu cả — đúng điều kiện để cho nó đọc tài liệu nội bộ. Nhưng nhét hàng nghìn trang PDF vào cửa sổ ngữ cảnh là bất khả (Bài 11 đã cho thấy cái giá của token). Bài 14 giải bài toán đó bằng kiến trúc RAG.

Tải file code thực hành minh họa bài học

File Python local_chat.py — mã nguồn gọi API Ollama cục bộ và stream phản hồi trực tiếp ra console Terminal không đệm dòng (chạy python local_chat.py):

Tải về local_chat.py

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 12: Structured Outputs & Function Calling Bài 14: Hệ thống RAG Cơ Bản: Hỏi đáp tài liệu Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận