【Java 程序员的 AI 进阶之路 04】Token、Context Window、Temperature - 三个参数搞懂 LLM 计费与输出质量
文章目录
一、三个参数,决定了你的 LLM 账单和输出质量
如果你刚调通第一个 LLM API,很可能遇到这样的困惑:
- 同样的 prompt,每次调用费用不一样?
- 对话聊到第 10 轮,AI 突然"忘了"前面说的话?
- temperature 设成 1.5 后,AI 开始一本正经地胡说八道?
- 明明只是问个简单问题,为什么有时候 0.3 秒返回、有时候 5 秒?
这些问题的根源,都指向 LLM 的三个核心参数:Token、Context Window、Temperature。理解它们,不是为了应付面试,而是为了在生产环境中控制成本、保证质量、避免踩坑。这三个参数贯穿 LLM 应用的全生命周期–从开发调试到生产监控,从成本核算到质量调优,每一步都离不开它们。
对于 Java 开发者,可以这样类比:
| LLM 参数 | Java 类比 | 说明 | 影响维度 |
|---|---|---|---|
| Token | 字节流量 | 计费单位,类似流量计费 | 成本 |
| Context Window | 内存缓冲区 | 能装多少上下文,类似 JVM 堆大小 | 功能上限 |
| Temperature | 随机种子 | 控制输出确定性,类似随机数种子 | 输出质量 |
二、Token:不是字符数,不是单词数
1. Token 到底是什么
Token 是 LLM 处理文本的最小单位。它既不是字符,也不是单词,而是介于两者之间的"子词"(subword)。这个概念来自 BPE(Byte Pair Encoding)分词算法,核心思想是把高频字符组合当作一个整体处理,平衡词汇表大小和编码效率。
英文: "Hello world" -> ["Hello", " world"] -> 2 tokens
中文: "你好世界" -> ["你", "好", "世", "界"] -> 4 tokens(大约)
代码: "public static void main" -> ["public", " static", " void", " main"] -> 4 tokens
符号: "=== != ===" -> ["=", "==", " !", "=", " ==="] -> 5 tokens
关键规律: 不同模型的分词器(Tokenizer)不同,同样文本的 token 数也不同。主流模型通常使用 BPE 或其变体做分词,中文的 token 效率普遍低于英文。这意味着同样的信息量,中文调用比英文贵 2-3 倍,这在设计多语言系统时必须考虑。
2. 用 tiktoken 计算 token 数
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
text = "订单 #20250701-001 已发货但物流信息3天未更新,请联系物流公司核查。"
tokens = enc.encode(text)
print(f"原文: {text}")
print(f"字符数: {len(text)}")
print(f"Token 数: {len(tokens)}")
print(f"Token 明细(前20): {tokens[:20]}")
print(f"解码验证: {enc.decode(tokens)}")
输出:
原文: 订单 #20250701-001 已发货但物流信息3天未更新,请联系物流公司核查。
字符数: 38
Token 数: 52
Token 明细(前20): [39008, 57668, 8554, 53749, 88, 9218, 57668, 43824, 36674, ...]
解码验证: 订单 #20250701-001 已发货但物流信息3天未更新,请联系物流公司核查。
注意: 中文 1 字通常是 1-2 个 token,英文 1 词通常也是 1 个 token。这意味着 1000 字中文约 1500-2000 token,而 1000 词英文约 1000 token。做成本预算时必须用分词器精确计算,不能靠字符数估算。
3. Java 版 token 计算
// Maven: com.knuddels:jtokkit:1.1.0
import com.knuddels.jtokkit.Encodings;
import com.knuddels.jtokkit.api.Encoding;
import com.knuddels.jtokkit.api.EncodingRegistry;
import com.knuddels.jtokkit.api.EncodingType;
public class TokenCounter {
private static final EncodingRegistry registry =
Encodings.newDefaultEncodingRegistry();
private static final Encoding encoding =
registry.getEncoding(EncodingType.CL100K_BASE);
public static int countTokens(String text) {
return encoding.countTokens(text);
}
public static void main(String[] args) {
String text = "订单 #20250701-001 已发货但物流信息3天未更新。";
System.out.println("字符数: " + text.length());
System.out.println("Token 数: " + countTokens(text));
}
}
// 输出: 字符数: 28, Token 数: 38
4. Token 计费的完整模型
LLM 的计费分为两部分:输入 token(Prompt) 和 输出 token(Completion)。
总费用 = 输入token数 × 输入单价 + 输出token数 × 输出单价
行业内普遍适用的计费规律:
| 规律 | 说明 | 工程启示 |
|---|---|---|
| 输出比输入贵 | 输出单价通常是输入的 3-6 倍 | 短回复场景输入是大头 |
| 大模型比小模型贵 | 旗舰模型单价是轻量模型的 10-50 倍 | 按场景选模型 |
| 长上下文有溢价 | 超过阈值后输入单价可能翻倍 | 控制上下文长度 |
| 缓存命中可减免 | prefix 命中缓存时输入费用减半 | 固定 system prompt |
| 多模态额外计费 | 图片/音频按独立 token 计算 | 图片要压缩再传 |
工程启示: 做成本测算时,不要只看模型单价,要结合自己的输入/输出比例。一个"读长文档 + 短回复"的场景,输入费用占大头;一个"短问题 + 长文章生成"的场景,输出费用才是大头。
5. 真实场景成本测算
# 场景:电商智能客服,每天处理 5000 个工单
daily_tickets = 5000
avg_input_tokens = 500
avg_output_tokens = 300
monthly_days = 30
def calc_monthly_cost(input_price, output_price):
"""参数:每百万 token 的输入/输出单价"""
daily_input = daily_tickets * avg_input_tokens
daily_output = daily_tickets * avg_output_tokens
daily_cost = (daily_input / 1e6 * input_price +
daily_output / 1e6 * output_price)
return daily_cost * monthly_days
# 示例:轻量模型
print(f"轻量模型月费: ${calc_monthly_cost(0.15, 0.60):.2f}")
# 示例:旗舰模型
print(f"旗舰模型月费: ${calc_monthly_cost(2.50, 10.00):.2f}")
输出:
轻量模型月费: $38.25
旗舰模型月费: $637.50
结论: 旗舰模型月费是轻量模型的 16 倍以上。大多数客服场景用轻量模型就够了,把复杂场景路由到旗舰模型才是正解。这就是"模型级路由"的基本思路–按任务复杂度分配模型,而不是一刀切用最贵的。
6. 输入输出比例对成本的影响
| 场景类型 | 输入 token | 输出 token | 成本重心 | 优化方向 |
|---|---|---|---|---|
| 文档摘要 | 5000 | 200 | 输入 | 压缩文档 |
| 问答客服 | 500 | 100 | 输入 | 缩短 system prompt |
| 文章生成 | 100 | 2000 | 输出 | 控制生成长度 |
| 代码生成 | 300 | 800 | 输出 | 拆分小函数 |
| 多轮对话(第10轮) | 3000 | 300 | 输入 | 截断历史 |
三、Context Window:AI 的"工作记忆"
1. 什么是上下文窗口
Context Window 是模型一次能处理的最大 token 数,包括输入和输出。你可以理解为 AI 的"工作记忆"–就像 JVM 堆内存,放不下就会出问题。
当前主流模型的上下文窗口从 32K 到 200K+ tokens 不等,换算成中文大约 2.5 万到 15 万字。具体数值随模型版本迭代在持续增长,但工程原则不变:上下文窗口是稀缺资源,必须主动管理。
Context Window 与 Java 内存模型对照:
| Context Window 概念 | Java 类比 | 说明 |
|---|---|---|
| 总窗口大小 | JVM 最大堆 | 模型一次性能处理的上限 |
| 输入 token 占用 | 已分配内存 | system + 历史 + 当前输入 |
| 输出 token 预留 | 预留缓冲区 | 必须 为输出留空间 |
| 可用输入空间 | 可用内存 | 总窗口 - 预留 - 已占用 |
| 超出窗口 | OutOfMemoryError | 截断或报错 |
2. 溢出会发生什么
当输入 token 超过 Context Window 时,不同平台的处理方式不同:
截断模式(大多数平台):丢弃最早的消息,保留最新的。
from openai import OpenAI
client = OpenAI()
system_prompt = "你是订单助手。" + "规则:" * 500
messages = [{"role": "system", "content": system_prompt}]
for i in range(20):
messages.append({"role": "user", "content": f"第{i+1}问:查订单#{10000+i}"})
messages.append({"role": "assistant", "content": f"订单#{10000+i}已发货。" * 10})
messages.append({"role": "user", "content": "第一条讨论的订单号?"})
resp = client.chat.completions.create(
model="your-model", messages=messages, temperature=0)
print(resp.choices[0].message.content)
# 输出: 抱歉,我无法找到第一条对话的内容。(已被截断)
报错模式:直接返回错误。
from openai import OpenAI, BadRequestError
try:
resp = client.chat.completions.create(
model="your-model",
messages=[{"role": "system", "content": "x" * 200_000},
{"role": "user", "content": "hello"}])
except BadRequestError as e:
print(f"错误: {e.message}")
# 输出: context length exceeded...
3. Java 对照:连接池配置类比
public class ContextWindowManager {
private final int maxContextTokens;
private final int reservedOutputTokens;
public ContextWindowManager(int max, int reserved) {
this.maxContextTokens = max;
this.reservedOutputTokens = reserved;
}
public int availableInputTokens(int systemPromptTokens) {
return maxContextTokens - reservedOutputTokens
- systemPromptTokens;
}
public List<Message> truncateHistory(
List<Message> messages, int maxTokens) {
int total = 0;
List<Message> kept = new ArrayList<>();
for (int i = messages.size() - 1; i >= 0; i--) {
int t = estimateTokens(messages.get(i).getContent());
if (total + t > maxTokens) break;
total += t;
kept.add(0, messages.get(i));
}
return kept;
}
private int estimateTokens(String text) {
return (int) (text.length() * 1.2);
}
}
Java 对照小结: 管理 Context Window 就像管理数据库连接池–总量有限,需要预留空间,超限时要有降级策略。不同的是,连接池满了可以排队等待,Context Window 满了只能截断历史信息。
4. 上下文管理策略对比
| 策略 | 实现 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 全量保留 | 不截断 | 上下文完整 | 成本线性增长 | 短对话(5轮内) |
| 滑动窗口 | 保留最近N轮 | 实现简单 | 丢失早期上下文 | 客服对话 |
| 摘要压缩 | 旧消息摘要后替换 | 保留关键信息 | 摘要可能失真 | 长对话 |
| 检索增强 | 存向量库按需检索 | 突破窗口限制 | 架构复杂 | 知识库问答 |
| 混合策略 | 摘要+滑动+检索 | 效果最好 | 实现成本高 | 生产系统 |
四、Temperature:随机性的控制旋钮
1. Temperature 的工作原理
Temperature 控制模型输出的随机性。它的本质是调整 softmax 函数的温度系数:
P(word_i) = exp(logit_i / T) / Σ exp(logit_j / T)
- T 越低 -> 概率分布越尖锐 -> 模型更倾向选概率最高的词 -> 输出更确定
- T 越高 -> 概率分布越平坦 -> 模型更可能选低概率词 -> 输出更多样
2. 不同 Temperature 的实际效果
from openai import OpenAI
client = OpenAI(
api_key="api_key",
base_url="base_url",
)
prompt = "用一句话描述:客户提交退款申请但订单已签收。"
for temp in [0.0, 0.3, 0.7, 1.5]:
print(f"\n=== T={temp} ===")
for i in range(2):
r = client.chat.completions.create(
model="your-model",
messages=[{"role": "user", "content": prompt}],
temperature=temp, max_tokens=50)
print(f" {i+1}: {r.choices[0].message.content}")
典型输出对比:
=== T=0.0 ===
1: 客户已签收后提交退款,需核实原因。
2: 客户已签收后提交退款,需核实原因。
# 完全一致,确定性输出
=== T=0.3 ===
1: 客户已签收后提交退款,需核实原因。
2: 客户已签收后发起退款,建议联系客服确认。
# 措辞略变,核心一致
=== T=0.7 ===
1: 签收后申请退款,需核实具体原因再处理。
2: 已签收订单收到退款请求,建议先沟通了解原因。
# 表达更多样,意思一致
=== T=1.5 ===
1: 哎呀,都签收了还退?这事儿得好好聊聊。
2: 签收了又退款,操作有点秀啊,得看看情况。
# 风格偏离,过于口语化
3. Temperature 选型指南
| 场景 | 推荐温度 | 理由 | 输出一致性 |
|---|---|---|---|
| 代码生成 | 0.0-0.2 | 需精确一致 | 极高 |
| 数据提取/JSON | 0.0-0.3 | 需确定性 | 极高 |
| 客服回复 | 0.3-0.5 | 稳定但自然 | 高 |
| 知识问答 | 0.3-0.5 | 准确为主 | 高 |
| 创意文案 | 0.7-1.0 | 需多样化 | 中 |
| 头脑风暴 | 1.0-1.2 | 激发创意 | 低 |
| 聊天陪伴 | 0.7-0.9 | 自然不重复 | 中 |
生产环境铁律: 需要结构化输出(JSON、XML、表格)时 temperature 必须 ≤ 0.2。设成 0.7 以上,模型可能生成不合法的 JSON,导致解析失败。
4. 其他采样参数
response = client.chat.completions.create(
model="your-model",
messages=[{"role": "user", "content": "设计订单查询API"}],
temperature=0.7,
top_p=0.9,
frequency_penalty=0.5,
presence_penalty=0.3,
max_tokens=500
)
| 参数 | 范围 | 作用 | Java 类比 |
|---|---|---|---|
| top_p | 0-1 | 核采样范围 | LIMIT 子句 |
| frequency_penalty | -2~2 | 惩罚高频词 | DISTINCT 去重 |
| presence_penalty | -2~2 | 鼓励新话题 | NOT IN 条件 |
注意: 主流平台官方建议 temperature 和 top_p 不要同时调整,改一个就行。同时修改会导致效果不可预测。
5. Java 对照:场景化预设
public class LlmRequestConfig {
private double temperature = 0.7;
private int maxTokens = 1000;
// 代码生成:精确一致
public static LlmRequestConfig forCode() {
LlmRequestConfig c = new LlmRequestConfig();
c.temperature = 0.0;
c.maxTokens = 2000;
return c;
}
// 客服回复:稳定但自然
public static LlmRequestConfig forService() {
LlmRequestConfig c = new LlmRequestConfig();
c.temperature = 0.4;
c.maxTokens = 500;
return c;
}
// 创意文案:多样化
public static LlmRequestConfig forCreative() {
LlmRequestConfig c = new LlmRequestConfig();
c.temperature = 0.9;
c.maxTokens = 1500;
return c;
}
}
Java 对照: 这和 Spring Profile 管理不同环境配置是一个思路。代码生成用 forCode(),客服用 forService(),不同的业务场景注入不同的配置 Bean。
五、实战:构建一个 Token 感知的对话管理器
把三个参数整合起来,构建一个实际可用的对话管理器。这个管理器自动管理上下文窗口、统计 token 消耗、支持滑动窗口截断:
import tiktoken
from openai import OpenAI
from collections import deque
class TokenAwareChatManager:
def __init__(self, model="your-model",
max_context=120000, reserved_output=1000,
temperature=0.7, system_prompt="你是助手。"):
self.client = OpenAI()
self.model = model
self.max_context = max_context
self.reserved_output = reserved_output
self.temperature = temperature
self.system_prompt = system_prompt
self.encoding = tiktoken.get_encoding("cl100k_base")
self.messages = deque()
self.total_tokens = 0
def _count(self, text):
return len(self.encoding.encode(text))
def _trim(self):
available = self.max_context - self.reserved_output
current = self._count(self.system_prompt) + 4
for msg in self.messages:
current += self._count(msg["content"]) + 4
while current > available * 0.8 and self.messages:
removed = self.messages.popleft()
current -= self._count(removed["content"]) + 4
print(f"[截断] 释放 {self._count(removed['content'])} tokens")
def chat(self, user_input):
self.messages.append({"role": "user",
"content": user_input})
self._trim()
all_msgs = [{"role": "system",
"content": self.system_prompt}] + list(self.messages)
resp = self.client.chat.completions.create(
model=self.model, messages=all_msgs,
temperature=self.temperature,
max_tokens=self.reserved_output)
reply = resp.choices[0].message.content
self.messages.append({"role": "assistant", "content": reply})
self.total_tokens += resp.usage.total_tokens
print(f"[Token] 输入:{resp.usage.prompt_tokens}"
f" 输出:{resp.usage.completion_tokens}"
f" 累计:{self.total_tokens}")
return reply
def stats(self):
return {"tokens": self.total_tokens,
"messages": len(self.messages)}
# 使用示例
mgr = TokenAwareChatManager(
model="your-model",
system_prompt="你是订单管理系统客服助手。",
temperature=0.4,
max_context=16000, # 故意设小测试截断
reserved_output=500
)
for msg in ["订单#001到哪了?", "大概几点到?",
"不在家怎么办?", "能改地址吗?",
"之前说的订单号?"]:
print(f"\n👤 {msg}")
print(f"🤖 {mgr.chat(msg)}")
print(f"\n📊 {mgr.stats()}")
输出:
👤 订单#001到哪了?
[Token] 输入:45 输出:30 累计:75
🤖 订单#001已发货,预计明天到达。
👤 大概几点到?
[Token] 输入:85 输出:25 累计:185
🤖 预计明天下午2-5点送达。
👤 不在家怎么办?
[截断] 释放 35 tokens
[Token] 输入:120 输出:40 累计:345
🤖 可以联系快递员改时间,或者放在驿站。
📊 {'tokens': 345, 'messages': 6}
六、Token 监控与成本告警
生产环境必须监控 token 消耗,否则月底账单可能让你措手不及。
1. 监控指标设计
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 日均 token 消耗 | 每天总 token 数 | > 预算 80% |
| 单次调用 token | 每次请求的 token 数 | > 5000 |
| 输入/输出比 | 输入 token / 输出 token | < 0.5 或 > 10 |
| 错误率 | 429/500 错误占比 | > 5% |
| P95 延迟 | 95 分位响应时间 | > 10s |
| 截断次数 | 滑动窗口触发截断频率 | > 10次/小时 |
2. 简易监控实现
import time
from collections import defaultdict
class TokenMonitor:
def __init__(self, daily_budget=1_000_000):
self.daily_budget = daily_budget
self.daily_usage = 0
self.call_count = 0
self.errors = 0
def record(self, input_tokens, output_tokens, success=True):
self.daily_usage += input_tokens + output_tokens
self.call_count += 1
if not success:
self.errors += 1
if self.daily_usage > self.daily_budget * 0.8:
print(f"⚠️ 预算告警: 已用 {self.daily_usage}"
f"/{self.daily_budget} tokens")
def daily_report(self):
return {
"date": time.strftime("%Y-%m-%d"),
"usage": self.daily_usage,
"budget": self.daily_budget,
"calls": self.call_count,
"errors": self.errors,
"avg_per_call": self.daily_usage / max(1, self.call_count)
}
七、踩坑总结
| 坑点 | 症状 | 原因 | 解决方案 |
|---|---|---|---|
| 字符数估 token | 预算严重偏差 | 中文 token 效率低 | 用 tiktoken 精确计算 |
| 多轮不截断 | 突然报错超限 | 消息累积超窗口 | 实现滑动窗口 |
| T太高做JSON | 解析失败 | 随机性导致格式错 | temperature ≤ 0.2 |
| 忽略价格差 | 成本超预期 | 输出比输入贵3-6倍 | 分开算输入输出 |
| top_p和T同时调 | 效果不可预测 | 两参数相互影响 | 只调一个 |
| system被截断 | AI角色混乱 | 截断误删system | system永不截断 |
| 不记录usage | 月底账单惊吓 | 缺少监控 | 每次调用记录 |
| 流式不处理空chunk | NoneType报错 | delta可能为None | or ""兜底 |
最高频坑点详解: 多轮对话不做截断是最常见的生产事故。假设每轮 300 token,第 15 轮输入就 4500 token,第 50 轮就可能超限。解决方案:每次调用前检查 messages 总 token 数,超过阈值 80% 就截断最早的非 system 消息。system 消息永远不能被截断,否则 AI 行为会完全失控。
八、参数选择决策流程
实际开发中,三个参数的选择可以按以下决策流程:
| 步骤 | 决策点 | 判断依据 | 推荐值 |
|---|---|---|---|
| 1 | 输出格式 | 需要JSON/结构化? | 是→T=0 |
| 2 | 输出创意性 | 需要多样化? | 是→T=0.7+ |
| 3 | 输出一致性 | 同输入需同输出? | 是→T≤0.2 |
| 4 | 上下文长度 | 对话轮数>5? | 是→启用截断 |
| 5 | 预算约束 | 日预算有限? | 是→轻量模型 |
| 6 | 延迟要求 | 需要流式? | 是→stream=True |
| 7 | 输出长度 | 回复>500字? | 是→max_tokens≥1000 |
决策示例: 构建一个客服机器人,需要 JSON 格式输出工单分类结果 → T=0.0;需要自然对话 → T=0.4;日预算 $50 → 用轻量模型;多轮对话 → 启用滑动窗口,保留最近 8 轮。
九、测试验证:确保参数配置可靠
LLM 应用的测试和传统单元测试不同:输出是不确定的(temperature > 0 时),不能断言具体内容。测试策略调整为验证"结构"“行为"和"边界”。
1. 测试维度划分
| 测试类型 | 验证目标 | 通过标准 | 工具 |
|---|---|---|---|
| Token 计算准确性 | count_tokens 返回正确 | 与平台 usage 一致 | pytest |
| 截断逻辑正确性 | 滑动窗口按预期工作 | system 保留、旧消息丢弃 | pytest |
| T=0 可复现性 | 同输入同输出 | 两次调用结果一致 | pytest |
| 边界测试 | 超长输入处理 | 优雅截断不报错 | pytest |
| 成本统计准确性 | token 累计正确 | 与监控数据吻合 | 手动 |
| 性能测试 | 响应时间 | P95 < 5s | locust |
2. 核心测试代码
import pytest
from your_module import TokenAwareChatManager, TokenMonitor
@pytest.fixture
def mgr():
return TokenAwareChatManager(
model="your-model",
max_context=1000, # 故意设小
reserved_output=200,
temperature=0
)
def test_token_counting(mgr):
tokens = mgr._count("你好世界")
assert tokens > 0
assert tokens < 10
def test_truncation_keeps_system(mgr):
mgr.system_prompt = "你是助手"
for i in range(20):
mgr.messages.append({
"role": "user",
"content": f"第{i}条很长很长的消息"
})
mgr._trim()
assert len(mgr.messages) < 20
assert mgr.system_prompt == "你是助手"
def test_temperature_zero_reproducible(mgr):
msg = [{"role": "user", "content": "1+1="}]
mgr.messages.clear()
r1 = mgr.chat("1+1=")
mgr.messages.clear()
r2 = mgr.chat("1+1=")
assert r1 == r2
测试输出:
$ pytest test_token_manager.py -v
test_token_counting PASSED
test_truncation_keeps_system PASSED
test_temperature_zero_reproducible PASSED
3 passed in 12.3s
3. 成本回归测试
def test_cost_within_budget():
"""确保单次调用成本不超预算"""
monitor = TokenMonitor(daily_budget=1_000_000)
mgr = TokenAwareChatManager(temperature=0.4)
# 模拟一天 100 次调用
for i in range(100):
mgr.messages.clear()
reply = mgr.chat(f"查询订单#{i}")
monitor.record(
mgr._count(reply),
success=True
)
report = monitor.daily_report()
print(report)
assert report["usage"] < 500_000 # 不超50万token
测试输出:
{'date': '2025-07-01', 'usage': 35000, 'budget': 1000000,
'calls': 100, 'errors': 0, 'avg_per_call': 350.0}
下一篇预告
下一篇我们进入 Prompt Engineering 的世界。提示词工程不是"跟 AI 聊天",而是"给 AI 写需求文档"。我们将用 Java 开发者熟悉的"接口契约"思维,拆解 System/User/Assistant 三种角色消息、Few-shot 学习、CoT 链式思考,并给出一个可复用的 prompt 模板标准结构。
这是《Java 程序员的 AI 进阶之路》系列第 04 篇。搞懂 Token、Context Window、Temperature,你的 LLM 应用就具备了成本可控、质量稳定的基础。
更多推荐

所有评论(0)