Khóa học
Các LLM chạy cục bộ đã đến bước ngoặt. Với cửa sổ ngữ cảnh dài, hỗ trợ đa phương thức gốc của Gemma 4 và tính sẵn dùng của Ollama, giờ đây việc chạy trọn vẹn một trợ lý lập trình dạng agent mạnh mẽ trên chính máy của bạn đã trở nên khả thi.
Trong hướng dẫn này, tôi sẽ chỉ bạn cách xây dựng một trợ lý lập trình sử dụng Gemma 4 qua Ollama, với giao diện Gradio. Ứng dụng có bố cục hai khung: bên trái là trình soạn thảo mã trực tiếp, bên phải là bảng chat dạng agent. Bạn có thể tải lên hình ảnh hoặc tệp mã làm ngữ cảnh, bật sử dụng công cụ để mô hình chạy và kiểm chứng mã, và bật chế độ suy nghĩ cho các bài toán khó.
Kết thúc hướng dẫn, bạn sẽ có một ứng dụng cục bộ hoạt động có thể:
- Viết, giải thích và gỡ lỗi mã trong hơn 15 ngôn ngữ
- Thực thi mã Python trong subprocess cách ly và trả về kết quả
- Nhận hình ảnh và tệp văn bản làm ngữ cảnh đa phương thức
- Stream phản hồi theo thời gian thực từ mô hình Gemma 4 chạy cục bộ
- Chạy theo vòng lặp agent, gọi công cụ và phản hồi dựa trên kết quả
Mã hoàn chỉnh cho hướng dẫn này có sẵn tại đây.
Gemma 4 là gì?
Gemma 4 là họ mô hình open-weights của Google DeepMind, được thiết kế cho cả triển khai cục bộ và nghiên cứu. Nó kế thừa dòng Gemma với khả năng tuân thủ hướng dẫn tốt hơn, cửa sổ ngữ cảnh dài hơn và xử lý đầu vào đa phương thức gốc, được xây dựng trên cùng hạ tầng nghiên cứu như Gemini 3.

Hình: Hiệu năng mô hình so với Kích thước (nguồn: blog Gemma 4)
Các mô hình như Gemma-4-26B(MOE) và Gemma-4-31B đạt điểm Elo tương đương các mô hình lớn hơn nhiều, cho thấy hiệu năng theo tham số rất mạnh. Mô hình 31B hiện xếp hạng mở thứ 3 thế giới trên bảng xếp hạng văn bản Arena AI, và mô hình 26B giữ vị trí thứ 6. Điều này khiến Gemma 4 đặc biệt phù hợp cho các triển khai cục bộ và hạn chế tài nguyên mà không hy sinh năng lực.
Họ mô hình Gemma 4
Gemma 4 được phát hành với bốn kích thước linh hoạt gồm Effective 2B (E2B), Effective 4B (E4B), 26B Mixture of Experts (MoE), và 31B Dense. Họ mô hình này chia thành hai tầng riêng dựa trên mục tiêu triển khai:
|
Model |
Architecture |
Total Params |
Active/Effective Params |
Context Length |
Modalities |
|
Gemma-4-31B |
Dense Transformer |
31B |
31B |
256K tokens |
Text, Vision, Video |
|
Gemma-4-26B-A4B |
MoE (128 Experts) |
26B |
3.8B active |
256K tokens |
Text, Vision, Video |
|
Gemma-4-E4B |
Dense Transformer |
7.9B (with embeddings) |
4.5B effective |
128K tokens |
Text, Audio, Vision, Video |
|
Gemma-4-E2B |
Dense Transformer |
5.1B (with embeddings) |
2.3B effective |
128K tokens |
Text, Audio, Vision, Video |
Hãy tìm hiểu sâu hơn về từng biến thể:

Hình: Hướng dẫn trực quan về Gemma 4 (nguồn)
- 31B Dense là mô hình chủ lực tối ưu cho triển khai tại trung tâm dữ liệu và các tác vụ suy luận phức tạp. Hỗ trợ cửa sổ ngữ cảnh 256K token với cửa sổ trượt 1024 token để xử lý ngữ cảnh dài hiệu quả.
- 26B-A4B (MoE) là mô hình MoE đầu tiên của Gemma, định tuyến token qua 128 chuyên gia trong khi chỉ giữ 3.8B tham số hoạt động cho mỗi bước lan truyền xuôi. Nhờ đó đạt chất lượng gần 31B với chi phí tính toán mỗi token chỉ bằng một phần nhỏ, rất phù hợp cho phục vụ thông lượng cao.
- E4B và E2B thuộc tầng trên thiết bị và di động. Khác với các biến thể lớn hơn, chúng hỗ trợ đầu vào âm thanh gốc cho nhận dạng giọng nói bên cạnh thị giác và video, khiến chúng trở thành các mô hình đa phương thức nhất trong họ cho triển khai biên.
Cả bốn mô hình đều theo giấy phép Apache 2.0 và có thể triển khai cục bộ qua Ollama, vLLM, llama.cpp hoặc Unsloth.
Cho tác vụ lập trình, Gemma 4 nổi trội ở:
- Viết mã đầy đủ, có cấu trúc kèm giải thích
- Suy luận trên codebase hiện có khi được cung cấp làm ngữ cảnh
- Sử dụng công cụ và quy trình agent khi kết hợp với lớp điều phối
- Xử lý hình ảnh cùng với mã (ví dụ: đọc ảnh chụp UI và tạo HTML tương ứng)
Trong hướng dẫn này, chúng ta dùng biến thể gemma4:e4b (9.6GB) qua Ollama, là bản đã lượng tử hóa phù hợp cho suy luận cục bộ trên phần cứng phổ thông.
Chạy Gemma 4 qua Ollama
Ollama đảm nhận tải mô hình, lượng tử hóa, phục vụ và cung cấp HTTP API tương thích OpenAI. Trong hướng dẫn này, Ollama đóng vai trò backend suy luận và ứng dụng của chúng ta giao tiếp với nó qua localhost:11434.
Để tải và chạy mô hình:
curl -fsSL https://ollama.com/install.sh | sh
ollama pull gemma4:e4b
ollama serve
Các lệnh trên cài đặt Ollama cục bộ bằng script chính thức. Sau đó chúng ta tải về biến thể mô hình gemma 4:e4b để suy luận trên thiết bị. Cuối cùng, ollama serve khởi động máy chủ Ollama, cho phép ứng dụng gửi yêu cầu tới mô hình.
Khi máy chủ chạy, ứng dụng Gradio sẽ tự động kết nối.
Demo Gemma 4: Xây dựng Trợ lý Lập trình với Ollama
Ở phần này, chúng ta sẽ xây dựng trợ lý mã theo từng bước. Ở cấp độ khái quát, ứng dụng thực hiện:
-
Nhận thông điệp ngôn ngữ tự nhiên trong bảng chat, kèm tùy chọn đính kèm hình ảnh hoặc tệp
-
Bơm mã hiện có trong trình soạn thảo làm ngữ cảnh cho mô hình
-
Gửi yêu cầu streaming tới Gemma 4 qua endpoint
/api/chatcủa Ollama -
Tùy chọn gọi công cụ (chạy mã, đánh giá toán) trong vòng lặp agent
-
Đẩy các khối mã trích xuất từ phản hồi vào trình soạn thảo trực tiếp
Hãy xây dựng từng bước.

Bước 1: Cài đặt phụ thuộc
Ứng dụng cần một số thư viện nhỏ cho UI, xử lý ảnh và giao tiếp HTTP. Vì mô hình chạy cục bộ qua Ollama, không có phụ thuộc SDK đám mây.
pip install gradio requests pillow
Trong dự án này, chúng ta sẽ dùng:
-
gradiocho giao diện trình soạn thảo hai khung và chat -
requestsđể giao tiếp với HTTP API của Ollama -
pillowđể hỗ trợ xử lý ảnh trong UI
Điều này giúp môi trường nhẹ và hoạt động trên mọi máy chạy Ollama, gồm macOS với Apple Silicon, Linux và Windows qua WSL.
Bước 2: Cấu hình và hằng số
Trước khi viết logic, ta định nghĩa import, tên mô hình, base URL của Ollama, các ngôn ngữ hỗ trợ và system prompt mặc định. Những hằng số này điều khiển hành vi của toàn bộ ứng dụng.
import base64
import json
import math
import os
import re
import subprocess
import tempfile
from pathlib import Path
import gradio as gr
import requests
OLLAMA_BASE = "http://localhost:11434"
MODEL = "gemma4:e4b"
LANGUAGES = [
"python", "javascript", "typescript", "bash", "sql",
"rust", "go", "java", "c", "cpp", "html", "css",
"json", "yaml", "markdown", "plaintext",
]
TEXT_EXTS = {
".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css",
".json", ".yaml", ".yml", ".toml", ".md", ".txt",
".csv", ".sql", ".sh", ".bash", ".rs", ".go",
".java", ".c", ".cpp", ".h", ".hpp", ".rb", ".php",
}
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"}
DEFAULT_SYSTEM = """\
You are an expert coding assistant. When you write code:
- Always wrap it in a markdown code block with the language tag
- Write complete, working code — not fragments
- Briefly explain what the code does
You have a code-runner tool. Use it to validate logic when helpful.\
"""
Các tập TEXT_EXTS và IMAGE_EXTS điều khiển hai hành vi đính kèm khác nhau. Tệp văn bản/mã được đọc và bơm vào prompt làm ngữ cảnh, trong khi tệp ảnh được base64-encode và gửi trong trường images để khai thác khả năng thị giác của Gemma 4.
System prompt DEFAULT_SYSTEM định hình phong cách sinh mã của mô hình, cụ thể yêu cầu khối mã hoàn chỉnh có thẻ ngôn ngữ, vì ứng dụng sẽ phân tích các khối này để đẩy mã vào trình soạn thảo.
Bước 3: Định nghĩa công cụ agent
Trợ lý có thể hoạt động ở chế độ agent, nơi nó gọi công cụ trong lúc suy luận. Ta định nghĩa hai công cụ theo schema gọi hàm tiêu chuẩn mà Ollama hỗ trợ:
TOOLS = [
{
"type": "function",
"function": {
"name": "run_code",
"description": (
"Execute Python code in a sandboxed subprocess and return "
"stdout + stderr. Use this to validate, test, or demonstrate code."
),
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Python code to run (max ~50 lines, 5 s timeout).",
}
},
"required": ["code"],
},
},
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression precisely.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Python-compatible math expression, e.g. 'math.sqrt(2) * 100'",
}
},
"required": ["expression"],
},
},
},
]
Đoạn mã trên định nghĩa công cụ run_code, thực thi Python trong tệp tạm qua subprocess với timeout 5 giây và bắt cả stdout lẫn stderr. Nhờ đó mô hình có thể kiểm chứng logic, chạy ví dụ hoặc tạo đầu ra trực tiếp, không chỉ sinh văn bản tĩnh.
Trong khi đó, công cụ calculate đánh giá biểu thức toán trong namespace hạn chế, chỉ dùng mô-đun math và các built-in an toàn. Nó xử lý trường hợp mô hình cần kết quả số chính xác mà không cần khởi chạy quy trình Python đầy đủ.
Bước 4: Thực thi công cụ
Lớp thực thi ánh xạ tên công cụ tới hiện thực của chúng. Lớp này được gọi trong vòng lặp agent bất cứ khi nào mô hình trả về khối tool_calls.
def _run_python(code: str) -> str:
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
tmp = f.name
try:
r = subprocess.run(
["python3", tmp],
capture_output=True, text=True, timeout=5,
)
out = (r.stdout + r.stderr).strip()
return out[:3000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "⏱ Timed out (>5 s)"
except Exception as e:
return f"Error: {e}"
finally:
os.unlink(tmp)
def _calculate(expr: str) -> str:
ns = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
ns.update({"abs": abs, "round": round})
try:
return str(eval(expr, {"__builtins__": {}}, ns)) # noqa: S307
except Exception as e:
return f"Error: {e}"
def execute_tool(name: str, args: dict) -> str:
if name == "run_code":
return _run_python(args.get("code", ""))
if name == "calculate":
return _calculate(args.get("expression", ""))
return f"Unknown tool: {name}"
Một số lựa chọn thiết kế an toàn đáng chú ý trong ngăn xếp thực thi công cụ:
-
Hàm _run_python()ghi ra tệp tạm và dọn dẹp, tránh tệp sót lại kể cả khi lỗi -
Giới hạn đầu ra ở 3.000 ký tự để tránh làm ngập ngữ cảnh bởi các lệnh in quá lớn
-
Hàm
_calculate()dùngeval()với__builtins__rỗng, ngăn truy cập built-in của Python ngoài các hàmmathđược cho phép rõ ràng -
Timeout cố định 5 giây, đủ dài cho hầu hết tác vụ kiểm chứng, đủ ngắn để ngăn tiến trình chạy mất kiểm soát
Bước 5: Tiện ích hỗ trợ
Trước khi xây dựng logic chat lõi, ta cần vài hàm tiện ích xử lý đường dẫn tệp, mã hóa ảnh và trích xuất mã. Các hàm này được gọi xuyên suốt pipeline streaming.
def encode_image(path: str) -> str | None:
if not path:
return None
try:
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode()
except Exception:
return None
def file_as_context(path: str) -> str | None:
if not path:
return None
p = Path(path)
if p.suffix.lower() not in TEXT_EXTS:
return None
try:
content = p.read_text(encoding="utf-8", errors="replace")[:8000]
lang = p.suffix.lstrip(".")
return f"\n\n**Attached file — {p.name}:**\n```{lang}\n{content}\n```"
except Exception:
return None
def resolve_gradio_path(val) -> str | None:
if val is None:
return None
if isinstance(val, Path):
s = str(val)
return s if s.strip() else None
if isinstance(val, str):
s = val.strip()
return s if s else None
if isinstance(val, dict):
p = val.get("path")
if isinstance(p, str) and p.strip():
return p.strip()
nested = val.get("file")
if isinstance(nested, dict):
np = nested.get("path")
if isinstance(np, str) and np.strip():
return np.strip()
return None
name = getattr(val, "name", None)
if isinstance(name, str) and name.strip():
return name.strip()
return None
def is_image_path(path: str | None) -> bool:
return bool(path) and Path(path).suffix.lower() in IMAGE_EXTS
def extract_last_code_block(text: str) -> tuple[str | None, str]:
blocks = re.findall(r"```(\w*)\n(.*?)```", text, re.DOTALL)
if blocks:
lang, code = blocks[-1]
return code.strip(), lang.strip() or "python"
return None, "python"
def ollama_ok() -> bool:
try:
requests.get(f"{OLLAMA_BASE}/", timeout=2)
return True
except Exception:
return False
def _gradio_content_to_text(content) -> str:
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
parts: list[str] = []
for block in content:
if isinstance(block, dict):
if block.get("type") == "text":
parts.append(str(block.get("text", "")))
elif "text" in block:
parts.append(str(block["text"]))
elif isinstance(block, str):
parts.append(block)
return "".join(parts)
return str(content)
def _append_chat_turn(
history: list | None, user_text: str, assistant_text: str
) -> list:
base = list(history) if history else []
return base + [
{"role": "user", "content": user_text},
{"role": "assistant", "content": assistant_text},
]
def run_code_btn(code: str) -> str:
if not code.strip():
return "Nothing to run."
result = _run_python(code)
return result
Các tiện ích này giúp xử lý mượt đầu vào đa phương thức và đầu ra có cấu trúc:
-
Hàm encode_image(): Hàm này chuyển ảnh thành chuỗibase64để truyền vào trườngimagescủa Ollama. Nhờ đó Gemma có thể xử lý ảnh cùng văn bản cho suy luận đa phương thức. -
Hàm file_as_context(): Hàm này đọc tệp văn bản/mã và bơm trực tiếp vào prompt dưới dạng khối markdown định dạng sẵn. Nhờ đó không cần hệ thống truy xuất riêng vì mọi thứ được truyền inline làm ngữ cảnh. -
Hàm resolve_gradio_path(): Hàm này chuẩn hóa các định dạng tệp khác nhau mà Gradio trả về thành đường dẫn dùng được. Điều này ngăn lỗi biên trong xử lý tệp. -
Hàm is_image_path(): Kiểm tra nhanh một tệp có phải ảnh hay không, giúp định tuyến đầu vào ảnh đúng cách. -
Hàm extract_last_code_block(): Tiếp theo, ta trích xuất khối mã cuối cùng từ phản hồi của mô hình. -
Hàm ollama_ok(): Xác minh máy chủ Ollama cục bộ đang chạy trước khi gửi yêu cầu. -
Hàm _gradio_content_to_text()và_append_chat_turn(): Làm phẳng định dạng thông điệp có cấu trúc của Gradio thành văn bản thuần để xử lý nhất quán và duy trì lịch sử chat. -
Hàm run_code_btn(): Cuối cùng, chúng ta thực thi mã được sinh khi người dùng bấm nút Run code trong UI bằng hàm này.
Giờ khi đã có các tiện ích, ta có thể hiện thực hàm chat() lõi.
Bước 6: Trình tạo stream chat lõi
Hàm này điều khiển toàn bộ vòng tương tác, từ chuẩn bị đầu vào đến stream phản hồi và xử lý thực thi công cụ.
Ở mức cao, hàm chat() sẽ:
- xây dựng lịch sử hội thoại
- làm giàu đầu vào hiện tại của người dùng bằng ngữ cảnh (mã, tệp, ảnh)
- gửi yêu cầu streaming tới Ollama
- tùy chọn thực thi công cụ (chế độ agent)
- trả dần phản hồi để cập nhật UI theo thời gian thực
def chat(
message, history, image_path, file_path,
editor_code, language, system_prompt,
agentic, thinking, temperature,
):
Hàm thực hiện theo chuỗi bước sau:
Bước 1: Xây dựng lịch sử thông điệp
Bắt đầu bằng cách chuyển các lượt chat trước về định dạng message của Ollama. System prompt được đặt ở đầu để định hướng hành vi mô hình, đảm bảo mô hình có đầy đủ ngữ cảnh hội thoại trước khi sinh phản hồi.
for h in history or []:
if not isinstance(h, dict):
continue
role = h.get("role")
if role not in ("user", "assistant"):
continue
messages.append(
{"role": role, "content": _gradio_content_to_text(h.get("content"))}
)
content = message
Bước 2: Soạn lượt của người dùng
Tiếp theo, ta dựng thông điệp hiện tại của người dùng bằng cách bơm thêm ngữ cảnh. Nếu người dùng đang làm việc với mã, ta nối nội dung trình soạn thảo:
if editor_code.strip() and editor_code.strip() != STARTER_CODE.strip():
content += (
f"\n\n**Current code in editor ({language}):**\n"
f"```{language}\n{editor_code}\n```"
)
Tương tự, tệp tải lên được thêm inline làm ngữ cảnh và ảnh được mã hóa base64 và truyền qua trường images. Điều này cho phép mô hình suy luận đồng thời trên văn bản, mã và ảnh.
Bước 3: Gửi yêu cầu tới Ollama
Payload bao gồm tên mô hình, lịch sử thông điệp, cờ stream và các tùy chọn như sau:
payload = {
"model": MODEL,
"messages": messages,
"stream": True,
"options": options,
}
if agentic:
payload["tools"] = TOOLS
Nếu bật chế độ suy nghĩ, đặt options["think"] = True, kích hoạt lập luận chuỗi suy nghĩ mở rộng ở những mô hình hỗ trợ.
Bước 4: Xử lý gọi công cụ (chế độ agent)
Nếu bật agent, mô hình có thể trả về lời gọi công cụ thay vì văn bản. Khi đó mỗi công cụ được thực thi và kết quả được thêm vào hội thoại. Sau đó, gửi yêu cầu tiếp theo để tiếp tục sinh.
if agentic and msg.get("tool_calls"):
for tc in msg["tool_calls"]:
fn_name = tc["function"]["name"]
fn_args = tc["function"]["arguments"]
result = execute_tool(fn_name, fn_args)
if fn_name == "run_code" and fn_args.get("code"):
new_code = fn_args["code"]
messages.append(msg)
messages.append({"role": "tool", "content": result})
resp2 = requests.post(f"{OLLAMA_BASE}/api/chat", ...)
Điều này tạo vòng lặp agent một lượt: mô hình gọi công cụ, xem kết quả rồi tiếp tục sinh phản hồi cuối. Nhật ký gọi công cụ được chèn lên chat cho người dùng.
Bước 5: Trích xuất mã và cập nhật trình soạn thảo
Cuối cùng, khi streaming hoàn tất, ta trích xuất khối mã cuối từ phản hồi:
extracted, _ = extract_last_code_block(full_response)
if extracted:
new_code = extracted
Như vậy, mô hình sinh mã, trình soạn thảo tự động cập nhật, không cần sao chép thủ công.
Bước 7: Bố cục UI Gradio
Khi đã có logic lõi, ta thiết kế giao diện bằng Gradio. Mục tiêu là tạo bố cục hỗ trợ song song cả luồng công việc lập trình và tương tác với AI.
Ta dùng gr.Blocks để tạo bố cục hai cột:
- bảng bên trái tập trung vào soạn thảo và thực thi mã
- bảng bên phải xử lý chat, tệp đính kèm và điều khiển mô hình
with gr.Blocks(title="Gemma 4 · Code Assistant") as demo:
with gr.Row(equal_height=False):
# LEFT: Code Editor
with gr.Column(scale=11):
with gr.Row():
lang_sel = gr.Dropdown(choices=LANGUAGES, value="python", label="Language")
run_btn = gr.Button("Run Code", elem_classes=["run-btn"])
clear_ed = gr.Button("Clear")
code_editor = gr.Code(
value=STARTER_CODE,
language="python",
label="Editor",
lines=24,
interactive=True,
)
run_output = gr.Textbox(
label="Output",
lines=6,
interactive=False,
elem_id="run-output",
)
# RIGHT: Chat
with gr.Column(scale=9):
chatbot = gr.Chatbot(value=[], elem_id="chatbot", height=430)
with gr.Row():
image_upload = gr.Image(label="Image (vision)", type="filepath")
file_upload = gr.File(label="Code / text file")
with gr.Row():
msg_input = gr.Textbox(placeholder="Ask the agent...", scale=6)
send_btn = gr.Button("Send", variant="primary")
with gr.Row():
agentic_cb = gr.Checkbox(label="Enable Agentic", value=True)
thinking_cb = gr.Checkbox(label="Enable Thinking", value=False)
clear_chat = gr.Button("Clear chat")
with gr.Accordion("Settings", open=False):
sys_prompt = gr.Textbox(value=DEFAULT_SYSTEM, label="System prompt")
temperature = gr.Slider(minimum=0.0, maximum=2.0, value=0.7)
Hãy xem UI phối hợp với các thành phần đã mô tả ở trên như thế nào:
- Không gian làm việc mã: Bảng bên trái bao gồm bộ chọn ngôn ngữ, trình soạn thảo mã và nút chạy/xóa. Kết quả thực thi hiển thị trong ô văn bản được làm nổi bật.
- Chat và tương tác: Bảng bên phải chứa chatbot để trò chuyện, ô nhập câu hỏi và hỗ trợ đính kèm như hình ảnh cho suy luận đa phương thức và tệp để chèn ngữ cảnh. Các công tắc bổ sung cho phép dùng công cụ agent và chế độ suy nghĩ tùy chọn.
- Bố cục và khả năng dùng: Giao diện dùng bố cục hai cột với tỉ lệ 55/45, ưu tiên trình soạn thảo mã đồng thời giữ chat dễ truy cập. Sự cân bằng này đảm bảo luồng công việc liền mạch giữa lập trình, trợ giúp AI và thực thi.
Bước 8: Gắn sự kiện
Sự kiện gửi và submit dùng chung đầu vào/đầu ra, nên cả bấm nút và nhấn Enter trong ô văn bản đều kích hoạt hành vi giống nhau:
_inputs = [
msg_input, chatbot, image_upload, file_upload,
code_editor, lang_sel,
sys_prompt, agentic_cb, thinking_cb, temperature,
]
_outputs = [chatbot, code_editor, msg_input]
send_btn.click(fn=respond, inputs=_inputs, outputs=_outputs)
msg_input.submit(fn=respond, inputs=_inputs, outputs=_outputs)
Wrapper respond() ở đây chuyển đổi đối tượng đường dẫn tệp của Gradio thành chuỗi đường dẫn thuần trước khi truyền vào trình tạo stream. Sau đó, hàm trả dần (history, code, "") ở mỗi chunk để chuỗi rỗng xóa ô nhập ngay từ lần trả đầu tiên.
Bước 9: Chủ đề và CSS
Để cải thiện khả năng dùng và tính nhất quán thị giác, ta thêm chủ đề và lớp CSS tùy biến trên nền mặc định của Gradio. Điều này mang lại diện mạo trau chuốt, lấy cảm hứng từ GitHub, hỗ trợ cả chế độ sáng và tối. Hoàn toàn tùy chọn.
Bắt đầu bằng cách định nghĩa chủ đề cơ sở với gr.themes.Base:
THEME = gr.themes.Base(
primary_hue = "blue",
secondary_hue = "slate",
neutral_hue = "slate",
font = [gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
font_mono = [gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
)
Thiết lập bảng màu nhất quán và kiểu chữ UI gọn gàng (Inter) với phông monospace thân thiện cho lập trình viên (JetBrains Mono). Tiếp theo, ta ghi đè các biến CSS chính để điều khiển giao diện ứng dụng:
gradio-app {
--body-background-fill: #f0f3f7;
--block-background-fill: #ffffff;
--body-text-color: #1f2328;
--button-primary-background-fill: #1a7f37;
}
body.dark gradio-app {
--body-background-fill: #0d1117;
--block-background-fill: #1c2128;
--body-text-color: #e6edf3;
--button-primary-background-fill: #238636;
}
Khối đầu là chủ đề sáng, khối thứ hai ghi đè cho chế độ tối. Dù tùy chọn, thêm chủ đề và CSS tùy biến giúp trải nghiệm người dùng tốt hơn đáng kể.
Với UI đã hoàn chỉnh, ta sẵn sàng cho bước cuối cùng: khởi chạy ứng dụng.
Bước 10: Khởi chạy
Cuối cùng, khởi chạy ứng dụng bằng:
if __name__ == "__main__":
print(f"Model : {MODEL}")
print(f"Ollama : {OLLAMA_BASE}")
if not ollama_ok():
print("Ollama not detected — run ollama serve before chatting")
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
theme=THEME,
css=CSS,
)
Ở đây, server_name="0.0.0.0" giúp ứng dụng truy cập được trên mạng nội bộ của bạn và thiết lập share=False giữ mọi thứ ở cục bộ. Kiểm tra ollama_ok() gửi yêu cầu GET nhanh tới localhost:11434 và cảnh báo khi khởi động nếu Ollama chưa chạy, thay vì chỉ báo lỗi khi người dùng gửi thông điệp đầu tiên.
Cuối cùng, truy cập http://localhost:7860 khi ứng dụng đang chạy.
Dưới đây là luồng thực thi đầy đủ từ góc nhìn người dùng:

-
Người dùng nhập thông điệp trong bảng chat, tùy chọn tải ảnh hoặc tệp mã và bật chế độ agent/suy nghĩ
-
Khi gửi, hàm
respond()dựng yêu cầu Ollama đầy đủ, gồm mã hiện có trong trình soạn thảo và mọi đính kèm làm ngữ cảnh -
Yêu cầu stream từng token và phản hồi từng phần được đẩy tới chatbot Gradio để hiển thị trực tiếp
-
Nếu bật agent và mô hình quyết định gọi công cụ, công cụ sẽ chạy cục bộ, kết quả được thêm vào hội thoại, và yêu cầu tiếp theo stream câu trả lời cuối
-
Sau khi stream xong, khối mã rào cuối trong phản hồi được trích xuất và đẩy vào trình soạn thảo tự động
Kết luận
Trong hướng dẫn này, chúng ta đã xây dựng một trợ lý lập trình AI hoàn toàn cục bộ bằng Gemma 4, Ollama và Gradio. Ứng dụng hỗ trợ đầu vào đa phương thức, sử dụng công cụ thực tế, phản hồi streaming và trình soạn thảo mã trực tiếp, tất cả đều chạy trên máy của bạn mà không cần API bên ngoài.
Cách tiếp cận toàn-ngữ-cảnh là bơm trực tiếp mã trong trình soạn thảo vào mọi prompt, đơn giản hơn RAG cho luồng công việc một tệp và hoạt động đặc biệt tốt cho các tác vụ như giải thích, tái cấu trúc hoặc mở rộng mã bạn đang chỉnh sửa.
Từ đây, bạn có thể mở rộng dự án theo vài hướng:
- Thêm bảng cây tệp để hỗ trợ dự án đa tệp và bơm các tệp đã chọn làm ngữ cảnh
- Thêm hỗ trợ các mô hình Ollama khác qua dropdown, cho phép chuyển giữa Gemma 4, Llama 3 và các mô hình khác
- Lưu lịch sử hội thoại xuống đĩa để phiên làm việc tồn tại sau khi khởi động lại ứng dụng
Tôi là Chuyên gia Google Developers trong lĩnh vực ML (Gen AI), Chuyên gia Kaggle 3x và Đại sứ Women Techmakers với hơn 3 năm kinh nghiệm trong ngành công nghệ. Tôi đồng sáng lập một startup công nghệ y tế vào năm 2020 và hiện đang theo học thạc sĩ khoa học máy tính tại Georgia Tech, chuyên sâu về học máy.
Gemma 4 FAQs
Có cần GPU không?
Không nhất thiết. Gemma 4 e4b là mô hình đã lượng tử hóa có thể chạy trên CPU, dù GPU sẽ cải thiện đáng kể tốc độ suy luận.
Khác biệt giữa chế độ agent và chat thường là gì?
Ở chế độ thường, mô hình chỉ stream văn bản. Ở chế độ agent, mô hình có thể gọi run_code hoặc calculate, kiểm tra kết quả và đưa chúng vào trước khi hoàn tất phản hồi.
Trình soạn thảo tự động cập nhật khi agent viết mã như thế nào?
Hàm tiện ích extract_last_code_block() quét phản hồi của trợ lý để tìm khối mã rào cuối cùng và đẩy nó vào trình soạn thảo sau khi stream xong. Vì vậy system prompt hướng dẫn mô hình luôn bọc mã trong khối rào có thẻ ngôn ngữ.
Điều gì xảy ra nếu Ollama không chạy?
Kiểm tra ollama_ok() phát hiện điều này khi khởi động và ở mỗi lần gửi chat. Nếu không thể kết nối Ollama, chat trả về thông báo lỗi định dạng thay vì sập.
