在过去三年里,很多人认为模型越大越“聪明”。他们觉得参数越多性能越好,GPU 越多就越强。
这个普遍认知本周被 Google 的开源模型 “Gemma 4” 彻底颠覆。Gemma 是 Google 发布的一系列 open-weight 模型。“Open-weight” 意味着模型的权重可以自由获取,任何人都可以下载并在自己的 PC、服务器或云端运行。
当 ChatGPT 和 Gemini Advanced 只能通过云端使用时,Gemma 最大的优势在于它可以直接安装并在本地环境运行。
为什么会突然出现这种变化?因为 Google 新发布的 “TurboQuant” 是一项能大幅降低 LLM 内存占用的技术。
据称它能将内存占用降低到原来的六分之一,这极大地降低了 AI 的运行成本。很多人因此期待:“本地 LLM 终于能更轻量了”“此前无法跑的大模型,现在在家用 GPU 上也能轻松跑起来”。
当 LLM 生成文本时,会使用一种称为 “Key-Value Cache (KV cache)” 的工作内存来保存已计算过的 token 信息。没有它的话,每生成一个新 token 都得从头计算一次。
问题在于,随着上下文长度增长,KV cache 的大小会线性增长。处理长对话或长文档时,真正吃 GPU 内存的是 KV cache,而不是模型权重。
结论是,TurboQuant 不是一条“能显著减小本地 LLM 整体权重”的魔法。它的核心在于强力压缩“推理过程中膨胀的内存”,主要是 KV cache。
下面用一个实时 chatbot 的小 demo,快速展示一下工作流程。
我会上传一张包含资产与负债的信息图片,你可以用任意格式上传。图片会直接显示。
观察 agent 生成输出的过程,你会看到它先把文件保存到磁盘的临时目录。文件名是随机的,但扩展名保持正确,因此格式能被正确识别。
如果输入是 PDF,会使用 Poppler 库中的 pdftoppm(Portable Pixmap)工具将每一页转换为 PNG。这一步是必要的,因为视觉模型只接受图像输入。
页面会按配置的 Dots Per Inch (DPI) 渲染,通常为 300。DPI 越高精度越好,但处理时间和文件大小也会增加。如果你选择了页码范围,就只处理那些页面。图像会临时保存,之后清理掉。
接着,会检查所有图片的尺寸。如果某张图片大于最大边(默认 1536 像素),就用高质量的 Lanczos 滤波按比例缩放。这样既能保持较快处理速度,又能保留足够的细节以保证文字识别准确。
如果文档类型设为 “auto”,agent 会先快速分类图片类型,比如 general、table、handwriting 或 scan,以便选择最合适的 OCR 提示词。如果你手动选择了类型,就会跳过这一步。
之后,会将图片编码,与选定的提示词一起发送到本地的 Ollama API。请求使用 streaming,因此文本会边生成边返回。agent 会把这些文本块汇总成最终结果,并记录 token 数、处理时间等元数据。
最后,会根据选择的输出类型进行格式化。Plain text 会把所有内容直接合并,Markdown 会增加结构化信息,JSON 会保留完整元数据。
这段代码会发布在我的 Patreon 上,因为它花费了我大量时间与精力。如果你喜欢我的创作并希望看到更多类似项目,在 Patreon 上支持我能帮助我持续产出高质量内容。非常感谢你的支持。
What makes Gemma 4 Unique?
Gemma 4 有四种规模:E2B、E4B、26B、A4B 和 31B。小规格针对智能手机与边缘设备,更大规格适合本地 PC 与工作站。
此外,它支持最长 256K tokens 的 long context,并可处理 140 多种语言。
较小(128K)与较大(256K)的上下文长度,让它在分享整套代码库或长篇设计文档时非常实用。它的功能设计也非常贴近实际应用。
它原生支持 function calls(调用外部工具与 API 的机制),并默认支持 system role。所有模型都能处理文本与图像,小模型还原生支持语音。
换句话说,它从一开始就不仅是为聊天而生,更是为“连接检索、执行、格式化与决策”的 agents 打造的基础。
不只是 “smart”,更是 “easy to integrate into the workflow”。
我认为这就是 Gemma 4 的精髓。
虽然模型本身的智能很重要,但真正落地的关键在于三点:读长文本的能力、调用工具的能力、以及本地运行的能力。
What makes Turboquant Unique?
我想明确一点:它“不是让模型本身更轻”的技术,并不意味着价值低。
相反,在本地 LLM 的实际运行中,后期更显著的反而是 KV cache 的压力,因此降低其负担的收益非常可观。
根据 Google Research,TurboQuant 追求极低比特深度的压缩;在 KV cache 量化上,在 3.5 bits/channel 可实现“absolute quality neutrality”,即质量绝对不受影响;即便降到 2.5 bits/channel,质量也仅有“marginal”轻微退化。
大致意味着:
-
用于上下文保留的内存占用可以显著降低。 -
同时,质量下降有望维持在最小程度。 -
如果 TurboQuant 在本地 LLM 中落地并广泛使用,可预期出现以下变化:
更容易处理长文本
这会让长文本摘要、代码库分析、文档输入与 RAG 等任务更容易处理。由于 KV cache 会随序列长度增长而增大,对其进行压缩能让长文本操作更实用。
在相同 GPU 上更容易保持性能
这能缓解“短文本没问题,但一到长文本就力不从心”的状况。对于显存约 16GB 的 GPU 尤其重要:即使模型本身能载入,长文本常常依然吃紧。
这也与 KV cache 压缩研究的总体趋势一致:内存减少会带来吞吐与 batch size 的提升。
对多轮与 agent 场景尤其有效
在需要长期对话、多任务、RAG、代码辅助等应用中,瓶颈往往不在模型本身,而在 KV cache,因此优化它价值极高。
Let's start coding:
我写了一个函数,能把像 “1–5” 或 “1,3,7–10” 这样的字符串解析为整洁的页码列表。它先按逗号分割,比如 “1,3,7–10” 会拆成小块。
每个小块如果带有短横线,就按范围展开;否则将其视为单个页码。
构建过程中用的是 set,因此像 “1–3,2” 这样的重复会自动去重。最后会转换为排序后的列表,保证页码有序返回。
def parse_pages(page_str: str) -> list[int]:
"""
Parse a page range string like '1-5' or '1,3,7-10'
into a sorted list of 1-based page numbers.
"""
pages =set()
for part in page_str.split(","):
part = part.strip()
if "-" in part:
start, end= part.split("-", 1)
pages.update(range(int(start), int(end) +1))
else:
pages.add(int(part))
return sorted(pages)
我写了一个函数,把 PDF 转成 PNG,便于 Gemma 4 读取。它会先检查是否安装了 pdftoppm——如果没有,就提示安装并退出。
然后设置输出路径,让图片命名为 “page-1”“page-2” 等。如果用户指定了页码,就对每一页分别调用一次 pdftoppm;否则就对整份 PDF 进行一次性转换。
用 -r 参数设置图像质量。转换后,它会收集所有 “page-*.png” 文件、排序;如果没找到就直接退出。
辅助函数 extract_page_number 会读取诸如 “page-01.png” 的文件名,去掉扩展名,抽取数字并转为整数,让应用知道每张图对应的页码。
def pdf_to_images(
pdf_path: str,
output_dir: str,
dpi: int = DEFAULT_DPI,
pages: list[int] | None = None,
) -> list[Path]:
"""
Convert a PDF to PNG images using pdftoppm (poppler).
If `pages` is provided, only those pages are converted.
Returns a sorted list of image paths.
"""
if not shutil.which("pdftoppm"):
print("Error: pdftoppm not found. Install poppler:")
print(" macOS: brew install poppler")
print(" Ubuntu: sudo apt install poppler-utils")
sys.exit(1)
output_prefix = str(Path(output_dir) / "page")
if pages:
# Convert each requested page individually
for p in pages:
cmd = [
"pdftoppm", "-png", "-r", str(dpi),
"-f", str(p), "-l", str(p),
pdf_path, output_prefix,
]
subprocess.run(cmd, check=True, capture_output=True)
else:
# Convert all pages at once
cmd = ["pdftoppm", "-png", "-r", str(dpi), pdf_path, output_prefix]
subprocess.run(cmd, check=True, capture_output=True)
images = sorted(Path(output_dir).glob("page-*.png"))
if not images:
print(f"Error: No page images extracted from {pdf_path}")
sys.exit(1)
return images
def extract_page_number(image_path: Path) -> int:
"""Extract the 1-based page number from a pdftoppm filename (e.g. page-03.png → 3)."""
returnint(image_path.stem.split("-")[-1])
接下来,我写了一个通用函数来决定如何处理任意文件。它会启动计时,判断文件类型,然后走两条路径之一。若是图片,就调用 ocr_single_image,并将结果包成一个包含文件信息、时间戳、模型名与页列表的字典。
若是 PDF,就创建临时文件夹,用 pdf_to_images 把页面转为 PNG,对每张图片执行 OCR,汇总结果,并构建同样格式的字典。临时文件夹会自动删除。
如果两者都不是,就打印错误并退出。无论哪种输入,这个函数都会返回同样结构的字典,因此 agent 能用完全一致的方式处理图片与 PDF。
# ── Single file processing ────────────────────────────────
def process_single_file(
file_path: str,
doc_type: str = "auto",
model: str = DEFAULT_MODEL,
dpi: int = DEFAULT_DPI,
pages: list[int] | None = None,
max_long_edge: int = MAX_IMAGE_LONG_EDGE,
) -> dict:
"""
Run OCR on a single PDF or image file.
Returns a unified result dict with per-page text and metadata.
"""
file_path = Path(file_path)
start_time = time.time()
if file_path.suffix.lower() in IMAGE_EXTS:
# Direct image OCR - single page result
print("Recognizing image...", end="", flush=True)
result = ocr_single_image(str(file_path), doc_type, model, max_long_edge)
secs = result["duration_ms"] / 1000
print(f" Done ({secs:.1f}s, {result['tokens']} tokens)")
total_ms = (time.time() - start_time) * 1000
return {
"file": str(file_path.resolve()),
"total_pages": 1,
"processed_pages": 1,
"model": result["model"],
"created_at": datetime.now().isoformat(),
"total_duration_ms": round(total_ms, 1),
"pages": [
{
"page": 1,
"text": result["text"],
"doc_type": result["doc_type"],
"tokens": result["tokens"],
"duration_ms": result["duration_ms"],
}
],
}
elif file_path.suffix.lower() == PDF_EXT:
# PDF: convert to images first, then OCR each page
with tempfile.TemporaryDirectory(prefix="ocr_") as tmpdir:
print(f"Converting PDF to images (DPI={dpi})...")
images = pdf_to_images(str(file_path), tmpdir, dpi, pages)
page_results = []
print(f"Starting OCR on {len(images)} page(s)\n")
for idx, img_path in enumerate(images):
page_num = extract_page_number(img_path)
print(f" [{idx+1}/{len(images)}] Page {page_num} - recognizing...", end="", flush=True)
result = ocr_single_image(str(img_path), doc_type, model, max_long_edge)
secs = result["duration_ms"] / 1000
print(f" Done ({secs:.1f}s, {result['tokens']} tokens)")
page_results.append({
"page": page_num,
"text": result["text"],
"doc_type": result["doc_type"],
"tokens": result["tokens"],
"duration_ms": result["duration_ms"],
})
total_ms = (time.time() - start_time) * 1000
return {
"file": str(file_path.resolve()),
"total_pages": len(images),
"processed_pages": len(page_results),
"model": model,
"created_at": datetime.now().isoformat(),
"total_duration_ms": round(total_ms, 1),
"pages": page_results,
}
else:
print(f"Error: Unsupported format '{file_path.suffix}'")
sys.exit(1)
接着,我实现了三个函数,将结果字典转换成可读字符串,但风格不同。format_as_json 最简单——直接把完整字典以漂亮缩进的 JSON 输出,保留时间戳、token 数等全部信息。
format_as_markdown 更有结构:它会把输入标准化为列表,添加包含文件名、模型、页数、耗时的头部,然后逐页输出,包含分隔线、页标题、隐藏的 HTML 注释和正文文本。
format_as_text 最简洁——同样先标准化输入;若有多个文档,就只在文档之间添加文件名分隔符;然后几乎不加任何额外格式,直接输出纯文本。
最后,FORMATTERS 会把 “json”“md”“txt” 映射到各自的格式化函数,agent 就能用一行代码选对格式化器。
# ── Output formatters ─────────────────────────────────────
def format_as_json(data: dict | list[dict]) -> str:
"""Serialize the result as pretty-printed JSON."""
return json.dumps(data, ensure_ascii=False, indent=2)
def format_as_markdown(data: dict | list[dict]) -> str:
"""Format the result as Markdown with per-page sections."""
items = data if isinstance(data, list) else [data]
parts = []
for doc in items:
filename = Path(doc["file"]).name
model = doc.get("model", "unknown")
created = doc.get("created_at", "")
total_sec = doc.get("total_duration_ms", 0) / 1000
parts.append(f"# OCR: {filename}\n")
parts.append(
f"> Model: `{model}` | Pages: {doc['processed_pages']} | "
f"Time: {total_sec:.1f}s | Date: {created}\n"
)
for page in doc["pages"]:
page_sec = page.get("duration_ms", 0) / 1000
parts.append("\n---\n")
parts.append(f"## Page {page['page']}\n")
parts.append(
f"\n"
)
parts.append(f"\n{page['text']}\n")
return "\n".join(parts)
def format_as_text(data: dict | list[dict]) -> str:
"""Format the result as plain text, one page after another."""
items = data if isinstance(data, list) else [data]
parts = []
for doc in items:
if len(items) > 1:
filename = Path(doc["file"]).name
parts.append(f"{'=' * 60}")
parts.append(f"FILE: {filename}")
parts.append(f"{'=' * 60}\n")
for i, page in enumerate(doc["pages"]):
if len(doc["pages"]) > 1:
parts.append(f"--- Page {page['page']} ---\n")
parts.append(page["text"])
if i < len(doc["pages"]) - 1:
parts.append("") # blank line between pages
return "\n".join(parts)
# Map format name → formatter function
FORMATTERS = {
"json": format_as_json,
"md": format_as_markdown,
"txt": format_as_text,
}
My impression :
Gemma 4 最重要的意义,并不是“又多了一个顶级模型”。
我认为真正的转折在于:过去仅依赖云端的 AI 开发,正在转向云 + 本地的 hybrid 形态。重推理与最终决策放在云端,日常支持与内部数据处理在本地完成。
另一方面,TurboQuant 有望显著提升 AI 的性能与效率,未来的关注度会越来越高。
它目前还不是面向普通用户的服务,但作为一项将深刻影响 AI 未来走向的技术,非常值得持续关注 ✨

