一、三个参数,决定了你的 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 条件

注意: 主流平台官方建议 temperaturetop_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 应用就具备了成本可控、质量稳定的基础。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐