Hướng dẫn
Hàm đơn giản hóa giúp đọc xác suất lựa chọn của LLM và mô hình thị giác
(giờ Việt Nam)
Tóm tắt AI
Lấy cảm hứng từ Jev và OpenJev, tác giả chia sẻ một hàm duy nhất giúp ép LLM trả về đáp án dạng chữ cái, sau đó trích xuất xác suất từ top_logprobs, hỗ trợ cả các mô hình thị giác.
Chính văn · Bản dịch AI


Tôi cảm thấy bị thu hút bởi Jev và các dự án tự lưu trữ (self-hostable) xuất hiện xung quanh nó, chẳng hạn như OpenJev và SemIf. Việc đọc về chúng đã giới thiệu cho tôi một thủ thuật thú vị: đọc xác suất token của LLM.
Có vẻ như đây là một thủ thuật cũ đối với một số người. Xem ví dụ tại OpenAI's logprobs cookbook. Nhưng với tôi thì nó hoàn toàn mới.
Tôi tin rằng ý tưởng cơ bản là viết một câu lệnh (prompt) như thế này:
State: My order arrived broken and I want a refund.
Question: Which team should handle this?
[A] billing
[B] shipping
[C] returns
Answer with the letter of the best option only.Sau đó thêm một vài tham số yêu cầu JSON vào một yêu cầu Chat Completions tương thích:
{
"max_completion_tokens": 1,
"logprobs": true,
"top_logprobs": 20
}API của LLM sẽ trả về ký tự đó cùng với log probabilities (xác suất log) của mô hình cho các token thay thế.
Lặp lại cho mỗi câu hỏi. Việc ép mô hình chỉ tạo ra một token duy nhất giúp tránh câu trả lời dài dòng và cực kỳ nhanh chóng, mặc dù việc xử lý đầu vào vẫn tốn thời gian. Tuy nhiên, đối với mỗi câu hỏi, một tiền tố trạng thái dùng chung có thể được KV-cached nếu backend hỗ trợ.
Phần thú vị: điều này cũng hoạt động với các mô hình thị giác (vision models). Định dạng yêu cầu được ghi lại của Jev hiện chỉ mô tả trạng thái văn bản/JSON. Tôi đã thêm trường attachments cho hình ảnh để phục vụ các thử nghiệm cục bộ của mình.
Ví dụ của tôi ghi lại các khung hình từ webcam, gửi dưới dạng base64 JPEG và in ra một bảng: có người xuất hiện không, chúng ta đang ở trong nhà hay ngoài trời, và cảnh quay sáng đến mức nào? Với Gemma 4 12B trên RTX 3090 của mình, tôi đạt khoảng 1 khung hình mỗi giây, với ba câu hỏi cho mỗi khung hình. Tôi cũng đã chạy thử với OpenAI gpt-6-luna và đạt khoảng 0.2 FPS. Có lẽ vì tôi chưa thực hiện bất kỳ nỗ lực nào để tránh chi phí kết nối riêng biệt thông qua hệ thống của họ cho mỗi câu hỏi trên mỗi khung hình.
Các mô hình thị giác máy tính chuyên dụng chắc chắn hiệu quả hơn nhiều, nhưng điều tôi thích ở đây là sự linh hoạt: thay đổi một điều kiện bằng cách mô tả nó bằng văn bản thuần túy.
Đây là ví dụ Python độc lập (OpenCV chỉ được sử dụng để truy cập webcam thuận tiện, không phải cho bất kỳ tác vụ thị giác máy tính thực tế nào):
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["opencv-python"]
# ///
"""Preview and score webcam frames with llama.cpp or OpenAI.
uv run webcam.py
uv run webcam.py https://api.openai.com/v1 gpt-6-luna
OpenAI reads OPENAI_API_KEY.
"""
import argparse
import base64
import concurrent.futures
import datetime
import json
import math
import mimetypes
import os
import pathlib
import time
import urllib.parse
import urllib.request
import cv2
# attachments is our custom addition to the Jev request format.
data = json.loads("""
{
"state": "Inspect this webcam frame. Judge only what is visibly present.",
"attachments": [],
"questions": {
"person": {
"type": "noul",
"instructions": "Is a person visible?"
},
"plant": {
"type": "noul",
"instructions": "Is a plant visible?"
},
"setting": {
"type": "choice",
"instructions": "Where is the camera?",
"criteria": {
"indoors": null,
"outdoors": null,
"unclear": null
}
},
"light": {
"type": "score",
"instructions": "How bright is the scene?",
"criteria": [
"dark",
"dim",
"bright"
]
}
}
}
""")
def score(data, url, model):
state = data["state"]
if not isinstance(state, str):
state = json.dumps(state)
# Attachments are our extension to the Jev-style request format:
# image file paths or base64 data URLs. Load them once for all questions.
images = []
for attachment in data.get("attachments", []):
if attachment.startswith("data:image/"):
images.append(attachment)
continue
path = pathlib.Path(attachment).expanduser()
mime_type, _ = mimetypes.guess_type(path)
if mime_type not in {"image/png", "image/jpeg", "image/webp", "image/gif"}:
raise ValueError(f"Unsupported image file: {path}")
encoded = base64.b64encode(path.read_bytes()).decode()
images.append(f"data:{mime_type};base64,{encoded}")
# Send the API key only to OpenAI.
is_openai = urllib.parse.urlsplit(url).hostname == "api.openai.com"
headers = {"Content-Type": "application/json"}
if is_openai:
headers["Authorization"] = "Bearer " + os.environ["OPENAI_API_KEY"]
answers = {}
for name, question in data["questions"].items():
# Represent choices, booleans, and ordinal levels as lettered options.
if question["type"] == "choice":
options = question["criteria"]
elif question["type"] == "noul":
options = {"true": None, "false": None} | question.get("criteria", {})
elif question["type"] == "score":
options = {str(i): description for i, description in enumerate(question["criteria"])}
else:
raise ValueError(f"Unknown question type: {question['type']}")
if not 2 <= len(options) <= 20:
raise ValueError("Provide 2 to 20 criteria per question.")
letters = "ABCDEFGHIJKLMNOPQRST"[:len(options)]
# Ask for a single option letter, so its logprob represents that option.
instructions = question["instructions"]
if not isinstance(instructions, str):
instructions = json.dumps(instructions)
lines = [f"State:\n{state}\n\nQuestion: {instructions}\nOptions:"]
for letter, (key, description) in zip(letters, options.items()):
line = f"[{letter}] {key}"
if description is not None:
line += f": {description}"
lines.append(line)
prompt = "\n".join(lines) + "\n\nAnswer with the letter of the best option only."
# OpenAI needs Responses for enough alternatives; llama.cpp needs Chat for logprobs.
# top_p=1 avoids pruning alternatives.
if is_openai:
endpoint = "/responses"
content = [{"type": "input_text", "text": prompt}]
content.extend({"type": "input_image", "image_url": image} for image in images)
body = {
"model": model,
"input": [{"role": "user", "content": content}],
"reasoning": {"effort": "none"},
"max_output_tokens": 16,
"top_p": 1,
"top_logprobs": 20,
"include": ["message.output_text.logprobs"],
}
else:
endpoint = "/chat/completions"
content = [{"type": "text", "text": prompt}]
content.extend({"type": "image_url", "image_url": {"url": image}} for image in images)
body = {
"model": model,
"messages": [{"role": "user", "content": content}],
"max_completion_tokens": 1,
"temperature": 0,
"reasoning_effort": "none",
"logprobs": True,
"top_logprobs": 1024,
}
# Send the request and read the first output token's alternatives.
request = urllib.request.Request(
url.rstrip("/") + endpoint,
headers=headers,
data=json.dumps(body).encode(),
)
with urllib.request.urlopen(request) as response:
result = json.load(response)
if is_openai:
message = next(item for item in result["output"] if item["type"] == "message")
candidates = message["content"][0]["logprobs"][0]["top_logprobs"]
else:
candidates = result["choices"][0]["logprobs"]["content"][0]["top_logprobs"]
logprobs = {item["token"]: item["logprob"] for item in candidates}
# Normalize the returned option scores; missing options initially get zero.
missing = [letter for letter in letters if letter not in logprobs or logprobs[letter] <= -9999]
if len(missing) == lTập lệnh này xử lý các khác biệt về API: llama.cpp sử dụng Chat Completions và OpenAI sử dụng Responses để hiển thị các lựa chọn thay thế.
Tôi đã chạy Gemma 4 12B QAT thông qua llama.cpp. Trên Linux với các driver NVIDIA, curl, zstd và uv đã được cài đặt:
# Model (~7 GB) and multimodal projector (~175 MB).
mkdir -p ~/models/gemma-4-12b/
cd ~/models/gemma-4-12b/
curl -fL -C - -o gemma-4-12b-it-qat-q4_0.gguf https://huggingface.co/google/gemma-4-12B-it-qat-q4_0-gguf/resolve/main/gemma-4-12b-it-qat-q4_0.gguf
curl -fL -C - -o mmproj-gemma-4-12b-it-qat-q4_0.gguf https://huggingface.co/google/gemma-4-12B-it-qat-q4_0-gguf/resolve/main/mmproj-gemma-4-12b-it-qat-q4_0.gguf
# Standalone llama.cpp binary for RTX 3090 (CUDA architecture 86).
curl -fL -o llama.zst https://huggingface.co/buckets/ggml-org/install.sh/resolve/b11160/x86_64/linux/cuda/86/llama-app.zst
mkdir -p ~/bin/
zstd -d llama.zst -o ~/bin/llama
chmod +x ~/bin/llama
~/bin/llama serve --models-dir ~/models/ --port 8060Lưu ví dụ Python dưới tên webcam.py. Trong một terminal khác, từ thư mục đó:
uv run webcam.py http://localhost:8060/v1 gemma-4-12b
# Or use OpenAI, with OPENAI_API_KEY set in your environment.
uv run webcam.py https://api.openai.com/v1 gpt-6-lunaBài gốc còn tiếp — xem tiếp tại bài gốc ↗
Bài viết được AI dịch và tổng hợp tự động từ Hacker News: AI bài nổi bật. Liên kết bài gốc ở phía trên. Dữ liệu đồng bộ qua API công khai được ghi nguồn tại AI HOT (canonical) ↗. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.