Gọi LLM API: temperature, top_p và max tokens
1. Tổng quan
Khi gọi một LLM API, ngoài prompt, bạn còn gửi kèm vài tham số sinh văn bản. Ba tham số hay dùng nhất là temperature, top_p và max_tokens. Chúng quyết định câu trả lời ổn định hay sáng tạo, và dài bao nhiêu.
Bài này giải thích cơ chế của từng tham số bằng một đoạn mô phỏng Java nhỏ. Sau đó bạn sẽ cấu hình chúng với Spring AI và phát hiện câu trả lời bị cắt cụt. Đọc xong, bạn sẽ biết chọn giá trị nào cho từng loại tác vụ.
2. Mô hình chọn token như thế nào
Ở mỗi bước, LLM tính một điểm số (logit) cho mọi token có thể xuất hiện tiếp theo. Hàm softmax đổi các logit này thành xác suất. Sau đó mô hình lấy mẫu một token theo phân phối đó.
temperature và top_p không thay đổi “kiến thức” của mô hình. Chúng chỉ can thiệp vào bước lấy mẫu này.
2.1. Temperature: làm phân phối nhọn hơn hoặc phẳng hơn
Công thức softmax có temperature T:
p_i = exp(z_i / T) / Σ exp(z_j / T)
Đoạn code sau mô phỏng mô hình đang chọn từ tiếp theo cho câu “Chiếc nón lá màu …”:
import java.util.*;
public class SamplingDemo {
static final List<String> TOKENS = List.of("xanh", "đỏ", "vàng", "tím");
static final double[] LOGITS = {2.0, 1.0, 0.5, 0.1};
static double[] softmax(double[] logits, double temperature) {
double[] exp = new double[logits.length];
double sum = 0;
for (int i = 0; i < logits.length; i++) {
exp[i] = Math.exp(logits[i] / temperature);
sum += exp[i];
}
for (int i = 0; i < exp.length; i++) {
exp[i] /= sum;
}
return exp;
}
public static void main(String[] args) {
for (double t : new double[]{0.5, 1.0, 1.5}) {
double[] p = softmax(LOGITS, t);
StringBuilder line = new StringBuilder("T=" + t + ": ");
for (int i = 0; i < p.length; i++) {
line.append(String.format(Locale.ROOT, "%s=%.2f ", TOKENS.get(i), p[i]));
}
System.out.println(line);
}
}
}
Hàm softmax chia từng logit cho temperature trước khi lấy exp. Hàm main in phân phối với ba mức temperature:
T=0.5: xanh=0.83 đỏ=0.11 vàng=0.04 tím=0.02
T=1.0: xanh=0.57 đỏ=0.21 vàng=0.13 tím=0.09
T=1.5: xanh=0.46 đỏ=0.24 vàng=0.17 tím=0.13
Kết quả cho thấy rõ tác dụng của temperature:
- T thấp (0.5): token “xanh” chiếm 83%. Mô hình gần như luôn chọn đáp án phổ biến nhất. Câu trả lời ổn định và dễ đoán.
- T cao (1.5): xác suất dàn đều hơn. “tím” tăng từ 2% lên 13%. Câu trả lời đa dạng hơn, nhưng dễ lạc đề hơn.
- T = 0: về lý thuyết là luôn chọn token có xác suất cao nhất (greedy).
Lưu ý:
temperature = 0không đảm bảo kết quả giống hệt nhau giữa các lần gọi. Phép tính số thực trên GPU và cách nhà cung cấp gom batch vẫn có thể tạo khác biệt nhỏ. Đừng viết test so sánh chuỗi chính xác với output của LLM thật.
2.2. Top_p: cắt bỏ phần đuôi xác suất thấp
top_p (nucleus sampling) hoạt động theo cách khác. Mô hình sắp xếp token theo xác suất giảm dần. Nó giữ lại nhóm nhỏ nhất có tổng xác suất đạt ngưỡng p, rồi chỉ lấy mẫu trong nhóm đó.
Bạn thêm phương thức sau vào SamplingDemo:
static List<String> nucleus(double[] probs, double topP) {
Integer[] idx = {0, 1, 2, 3};
Arrays.sort(idx, (a, b) -> Double.compare(probs[b], probs[a]));
List<String> kept = new ArrayList<>();
double cumulative = 0;
for (int i : idx) {
kept.add(TOKENS.get(i));
cumulative += probs[i];
if (cumulative >= topP) {
break;
}
}
return kept;
}
Gọi nucleus(softmax(LOGITS, 1.0), 0.8) sẽ trả về:
[xanh, đỏ, vàng]
Tổng “xanh” và “đỏ” là 0.785, chưa tới 0.8. Vì vậy “vàng” được thêm vào và tổng thành 0.914. Token “tím” bị loại hẳn, dù temperature cao tới đâu.
Điểm khác biệt chính giữa hai tham số:
temperaturethay đổi hình dạng phân phối.top_pthay đổi tập ứng viên được phép chọn.
Lưu ý: Hầu hết nhà cung cấp khuyên chỉ chỉnh một trong hai tham số và để tham số còn lại ở mặc định (
top_p = 1.0). Chỉnh cả hai cùng lúc khiến bạn khó biết thay đổi nào gây ra kết quả.
3. Max tokens: giới hạn độ dài đầu ra
max_tokens là số token tối đa mô hình được sinh ra trong câu trả lời. Nó không phải độ dài mục tiêu. Mô hình không “cố viết cho đủ” con số này. Khi chạm giới hạn, nó dừng ngay, kể cả khi đang giữa câu.
Có ba điểm bạn cần nắm:
- Token không phải là từ. Với tiếng Anh, một token xấp xỉ 3/4 từ. Tiếng Việt có dấu thường tốn nhiều token hơn cho cùng lượng nội dung.
- Chi phí tính theo token.
max_tokenslà chốt chặn chi phí và độ trễ. - Context window gồm cả input và output. Nếu prompt đã dùng gần hết context window, phần còn lại cho output sẽ rất ít.
Khi bị cắt, API trả về finish_reason là length thay vì stop. Phản hồi rút gọn từ một API kiểu OpenAI trông như sau:
{
"choices": [{
"message": { "role": "assistant", "content": "Nón lá Huế được làm từ lá cọ non, phơi" },
"finish_reason": "length"
}],
"usage": { "prompt_tokens": 42, "completion_tokens": 20, "total_tokens": 62 }
}
Nội dung dừng giữa chừng vì max_tokens = 20. Nếu code của bạn chỉ đọc content, bạn sẽ hiển thị cho người dùng một câu cụt mà không hề biết.
4. Cấu hình với Spring AI
Phần này dùng Spring AI để gọi OpenAI từ Spring Boot 3.x.
4.1. Dependency và cấu hình mặc định
Bạn thêm BOM và starter vào pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
Giá trị mặc định cho toàn ứng dụng nằm trong application.properties:
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini
spring.ai.openai.chat.options.temperature=0.3
spring.ai.openai.chat.options.max-tokens=300
Mọi request không ghi đè sẽ dùng temperature = 0.3 và tối đa 300 token output. API key lấy từ biến môi trường, không hard-code vào file.
4.2. Một hồ sơ tham số cho từng loại tác vụ
Mỗi tác vụ cần một bộ tham số khác nhau. Thay vì rải số “magic” khắp code, bạn gom chúng vào một enum:
import org.springframework.ai.openai.OpenAiChatOptions;
public enum GenerationProfile {
EXTRACTION(0.0, 1.0, 200), // trích xuất dữ liệu, phân loại
SUPPORT_REPLY(0.4, 1.0, 400), // trả lời khách hàng
MARKETING_COPY(0.9, 1.0, 600); // viết mô tả sản phẩm, slogan
private final double temperature;
private final double topP;
private final int maxTokens;
GenerationProfile(double temperature, double topP, int maxTokens) {
this.temperature = temperature;
this.topP = topP;
this.maxTokens = maxTokens;
}
public OpenAiChatOptions toOptions() {
return OpenAiChatOptions.builder()
.temperature(temperature)
.topP(topP)
.maxTokens(maxTokens)
.build();
}
}
Mỗi hằng số mô tả một kiểu tác vụ. EXTRACTION cần kết quả nhất quán nên dùng temperature 0. MARKETING_COPY cần sự đa dạng nên dùng 0.9. Cả ba giữ top_p = 1.0, tức là chỉ chỉnh temperature như đã khuyến nghị ở mục 2.2.
4.3. Ghi đè tham số cho từng request và phát hiện bị cắt
Spring Boot tự cấu hình bean ChatClient.Builder. Bạn inject nó vào service:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.stereotype.Service;
@Service
public class ProductDescriptionService {
private final ChatClient chatClient;
public ProductDescriptionService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("Bạn là copywriter của cửa hàng Nón Lá Shop. Viết tối đa 3 câu.")
.build();
}
public String describe(String productName) {
ChatResponse response = chatClient.prompt()
.user(u -> u.text("Viết mô tả cho sản phẩm: {name}")
.param("name", productName))
.options(GenerationProfile.MARKETING_COPY.toOptions())
.call()
.chatResponse();
Generation result = response.getResult();
String finishReason = result.getMetadata().getFinishReason();
if ("length".equalsIgnoreCase(finishReason)) {
throw new TruncatedResponseException(
"Mô tả bị cắt sau " + response.getMetadata().getUsage().getCompletionTokens() + " token");
}
return result.getOutput().getText();
}
}
Phân tích từng phần:
.options(...)ghi đè các giá trị trongapplication.properties, chỉ cho request này..chatResponse()trả về toàn bộ phản hồi kèm metadata, thay vì chỉ chuỗi như.content().getFinishReason()cho biết lý do dừng. Spring AI có thể trả vềLENGTHviết hoa, nên code dùngequalsIgnoreCase.getUsage().getCompletionTokens()cho biết số token output thực tế. Con số này hữu ích để log và theo dõi chi phí.
Ngoại lệ được định nghĩa đơn giản như sau:
public class TruncatedResponseException extends RuntimeException {
public TruncatedResponseException(String message) {
super(message);
}
}
Khi gặp ngoại lệ này, bạn có hai lựa chọn. Cách thứ nhất là tăng max_tokens rồi thử lại. Cách thứ hai là siết prompt để yêu cầu câu trả lời ngắn hơn. Cách thứ hai thường rẻ hơn và cho kết quả gọn hơn.
5. Kiểm thử
Bạn không nên test output của LLM thật bằng so sánh chuỗi. Phần nên test là logic của chính bạn: các hồ sơ tham số và thuật toán mô phỏng.
import org.junit.jupiter.api.Test;
import org.springframework.ai.openai.OpenAiChatOptions;
import static org.assertj.core.api.Assertions.assertThat;
class GenerationProfileTest {
@Test
void extractionProfile_shouldBeDeterministicAndShort() {
OpenAiChatOptions options = GenerationProfile.EXTRACTION.toOptions();
assertThat(options.getTemperature()).isZero();
assertThat(options.getTopP()).isEqualTo(1.0);
assertThat(options.getMaxTokens()).isEqualTo(200);
}
@Test
void marketingProfile_shouldBeMoreCreativeThanSupport() {
assertThat(GenerationProfile.MARKETING_COPY.toOptions().getTemperature())
.isGreaterThan(GenerationProfile.SUPPORT_REPLY.toOptions().getTemperature());
}
@Test
void lowerTemperature_shouldConcentrateProbabilityOnTopToken() {
double[] cold = SamplingDemo.softmax(SamplingDemo.LOGITS, 0.5);
double[] hot = SamplingDemo.softmax(SamplingDemo.LOGITS, 1.5);
assertThat(cold[0]).isGreaterThan(hot[0]);
assertThat(cold[3]).isLessThan(hot[3]);
}
@Test
void nucleus_shouldDropLowProbabilityTail() {
double[] probs = SamplingDemo.softmax(SamplingDemo.LOGITS, 1.0);
assertThat(SamplingDemo.nucleus(probs, 0.8))
.containsExactly("xanh", "đỏ", "vàng");
}
}
Hai test đầu đảm bảo không ai vô tình đổi tham số của một hồ sơ quan trọng. Hai test sau khẳng định các hành vi đã mô tả ở mục 2: temperature thấp dồn xác suất vào token đứng đầu, còn top_p = 0.8 loại token “tím”.
6. Lỗi hay gặp và khi nào không nên dùng
Dùng temperature cao cho tác vụ cần độ chính xác. Trích xuất JSON, phân loại ticket hay sinh SQL cần kết quả nhất quán. Temperature 0.8 trở lên làm tăng khả năng sai định dạng và bịa thông tin.
Nghĩ rằng temperature thấp chống được “ảo giác”. Temperature thấp chỉ khiến mô hình chọn đáp án nó cho là có khả năng nhất. Nếu mô hình không biết câu trả lời, nó vẫn có thể tự tin nói sai. Muốn giảm ảo giác, bạn cần cung cấp ngữ cảnh đúng (RAG) và kiểm chứng đầu ra.
Đặt max_tokens quá thấp để tiết kiệm. Bạn không tiết kiệm được gì nếu câu trả lời bị cắt và phải gọi lại. Hãy giới hạn độ dài bằng prompt (“tối đa 3 câu”), còn max_tokens chỉ đóng vai lưới an toàn.
Không kiểm tra finish_reason. Đây là lỗi phổ biến nhất. Người dùng thấy câu cụt, còn log của bạn không ghi nhận gì.
Mang nguyên tham số sang mô hình khác. Mỗi nhà cung cấp có quy ước riêng:
- Thang temperature khác nhau. OpenAI dùng 0–2, Anthropic dùng 0–1, nên cùng giá trị 1.0 có ý nghĩa khác nhau.
- Các reasoning model (như dòng o-series của OpenAI) có thể từ chối
temperaturevàtop_p. - Một số mô hình yêu cầu
max_completion_tokensthay chomax_tokens. Spring AI hỗ trợ trường này quaOpenAiChatOptions.builder().maxCompletionTokens(...).
Lưu ý: Với reasoning model, token “suy nghĩ” nội bộ cũng bị tính vào giới hạn output. Nếu đặt giới hạn quá thấp, bạn có thể nhận về nội dung rỗng với
finish_reason = length.
7. Kết luận
Bài viết đã đi qua ba tham số cốt lõi khi gọi LLM API:
temperaturethay đổi độ nhọn của phân phối xác suất. Giá trị thấp cho kết quả ổn định, giá trị cao cho kết quả đa dạng.top_pgiới hạn tập token ứng viên. Bạn nên chỉnh một trong hai tham số này, không chỉnh cả hai cùng lúc.max_tokenslà trần độ dài và chi phí. Hãy luôn kiểm trafinish_reasonđể phát hiện câu trả lời bị cắt.
Với Spring AI, bạn đặt giá trị mặc định trong application.properties và ghi đè theo từng request bằng .options(...). Gom các bộ tham số vào một enum như GenerationProfile giúp code dễ đọc, dễ test và dễ chỉnh khi bạn đổi sang mô hình khác.