Appearance
LLM 基础概念与模型选型:先算清楚再动手
1. 本节产出
你能在不看任何资料的情况下回答三个问题:一次对话到底发给模型的是什么、花多少钱、该选哪个模型。并且能用一段 20 行的 Java 代码,把任意一段中文文本估出 Token 数、算出单次调用成本。
2. 前置依赖
- 00-04 成本速算:已理解 Token 计价公式
- 00-05 术语表:已知道 Token、Temperature、上下文窗口的字面含义
- 00-02 环境准备:JDK 与 Maven 可用
3. 为什么先讲概念而不是先写代码
这是最容易被跳过的课时,也是后面踩坑最集中的地方。三个真实场景:
场景一:把「上下文」当成「对话记录」。 有位同学把整个数据库表结构(约 3 万 Token)塞进 system prompt,说「让模型知道我们的表」。结果不是答得更准,而是每次调用成本翻了 30 倍,且前 20 轮的对话被挤出窗口,模型开始胡说。原因:上下文窗口是按 Token 计的总量上限,不是按轮数计的。
场景二:把 temperature 当成「随机程度」。 他给结构化抽取任务设了 0.9,抽取结果每次都不一样,最后加了一堆后处理正则去兜底。正确做法是抽取类任务设 0,创意类任务才调高。
场景三:认为「贵的模型一定更好」。 把全部流量打到旗舰模型上,月账单超出预算 8 倍。实际业务里 70% 的请求是「分类 + 摘要」这种简单任务,用轻量模型即可。
这三件事的共同点:不是代码问题,是心智模型问题。概念错了,代码写得再漂亮也是往错的方向加速。
4. 核心原理
4.1 模型看到的到底是什么
你以为发出的: "帮我看看这段代码"
实际发出的: [{"role":"system","content":"你是一个助手..."},
{"role":"user","content":"帮我看看这段代码"}]
模型看到的: 一个 Token 序列 + 一个概率分布
模型返回的: 按概率采样出的下一个 Token,循环直到结束符关键点:模型没有「理解」,只有「续写」。它在预测「给定这段前文,下一个最可能的 Token 是什么」。所有 Prompt 工程技巧,本质都是在操纵这个前文。
这条推论很重要,能解释大半现象:
| 现象 | 真实原因 |
|---|---|
| 模型会编造不存在的 API | 它在续写「看起来合理的下一个 Token」,不是在查数据库 |
| 长文档中间的信息被忽略 | 注意力对中间位置的权重偏低(Lost in the Middle 效应) |
| 让它「不要做 X」反而更容易做 X | 「不要」这个词本身把 X 的 Token 放进了上下文 |
| 同一个问题两次答案不同 | 采样是概率性的,temperature 控制分布的尖锐程度 |
4.2 三个必须记牢的参数
| 参数 | 作用机制 | 生产建议 |
|---|---|---|
temperature | 拉平或锐化概率分布。0 = 总选最高概率,1+ = 长尾也被选中 | 抽取/分类 0~0.2;对话 0.5~0.7;创意 0.8~1.0 |
maxTokens | 生成长度上限。超了会被截断,输出半句话 | 必须显式设置,否则按模型默认走,成本不可控 |
topP | 核采样:只在累计概率达到 P 的候选集里采样 | 一般不动,与 temperature 二选一调整即可 |
提示:
temperature和topP同时调是新手最常犯的配置错误。二者都是改采样分布,叠加后行为难以预测。只调一个,另一个保持默认。
4.3 上下文窗口的账怎么算
总占用 = system prompt
+ 历史对话(含模型的回复)
+ 检索到的文档片段(RAG 场景下最大头)
+ 本次用户输入
+ 预留给生成的额度(maxTokens)
─────────────────────────────
必须 ≤ 模型的上下文窗口中文的经验值:1 个汉字 ≈ 0.6~1 个 Token(不同分词器差异较大,保守按 1 算),英文 1 个单词 ≈ 1.3 个 Token。
举例:一个 32K 窗口的模型,system prompt 占 800 Token,历史对话 4000 Token,RAG 塞了 10 个片段共 12000 Token,用户输入 200 Token,预留生成 2000 Token——还剩 13000 Token 可用。看着宽裕,但如果前端允许多轮聊 50 轮,历史部分很快就会爆。
4.4 选型决策表
| 任务类型 | 推荐档位 | 判断依据 | 典型模型定位 |
|---|---|---|---|
| 分类、打标、意图识别 | 轻量 | 输出只有几个字,不需要推理 | 小参数 / 高性价比档 |
| 摘要、抽取、格式转换 | 中档 | 需要遵循指令,但不需要复杂推理 | 主流档 |
| 代码生成、多步推理、Agent | 旗舰 | 一步错满盘皆输,错误代价高 | 推理增强档 |
| embedding | 专用模型 | 不用对话模型做向量化 | 见 02-05 |
选型三步法:
- 先用旗舰模型跑通,确认这件事模型能做好;
- 再换中档模型跑同一批评测集,看质量掉多少;
- 掉得少就降级,掉得多就把「只有旗舰能做的部分」单独路由到旗舰。
绝大多数项目做完这三步,成本能降 50%~70%。这套方法在 04-02 模型网关里会做成可配置路由。
5. 代码走查
5.1 依赖与配置
xml
<!-- pom.xml:只需要 BOM,无需额外 starter 即可做本地估算 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>5.2 一个够用的 Token 估算器
java
// src/main/java/com/example/aibasics/util/TokenEstimator.java
public final class TokenEstimator {
/** 中文按 1 字 ≈ 0.7 token,英文按 4 字符 ≈ 1 token 的保守估算 */
public static int estimate(String text) {
if (text == null || text.isEmpty()) return 0;
int cjk = 0, other = 0;
for (char c : text.toCharArray()) {
if (isCjk(c)) cjk++;
else other++;
}
return (int) Math.ceil(cjk * 0.7 + other / 4.0);
}
private static boolean isCjk(char c) {
return Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN;
}
}提示:这是估算,不是精确值。精确值只有厂商的分词器能给。工程上够用的标准是「预算不失控」,不需要精确到个位数。
5.3 算单次成本
java
// src/main/java/com/example/aibasics/util/CostCalculator.java
public record CostResult(int inputTokens, int outputTokens, double cost) {
/** priceIn / priceOut 单位:元 / 百万 Token */
public static CostResult of(String prompt, String answer,
double priceIn, double priceOut) {
int in = TokenEstimator.estimate(prompt);
int out = TokenEstimator.estimate(answer);
double cost = in / 1_000_000.0 * priceIn + out / 1_000_000.0 * priceOut;
return new CostResult(in, out, cost);
}
@Override
public String toString() {
return String.format("in=%d out=%d cost=%.4f 元", inputTokens, outputTokens, cost);
}
}5.4 用真实响应校验估算
java
// 调用后从响应的 metadata 里读厂商返回的真实用量
ChatResponse resp = chatClient.prompt()
.user("介绍 Spring 的事务传播机制")
.call()
.chatResponse();
// 厂商返回的 usage 才是计费依据
var usage = resp.getMetadata().getUsage();
System.out.printf("prompt=%d completion=%d total=%d%n",
usage.getPromptTokens(), usage.getCompletionTokens(), usage.getTotalTokens());
// 对比自己的估算,偏差超过 30% 就调整系数
System.out.println("估算:" + TokenEstimator.estimate("介绍 Spring 的事务传播机制"));为什么要做这一步:估算器只用于前置预算(比如限流阈值、用户提示),真正对账必须看厂商返回的 usage。两者长期偏差过大说明你的文本分布和假设不符(比如混了大量英文日志),要重新校准系数。
6. 跑起来
bash
git checkout ch01-01-concepts
mvn -q test -Dtest=TokenEstimatorTest期望输出(数字会因文本不同略有差异):
中文估算:输入「Spring Boot 自动配置原理是什么」 → 14 tokens
英文估算:输入「What is dependency injection」 → 7 tokens
真实调用 usage:prompt=18 completion=213 total=231
单次成本(按中档模型 2 元/百万输入、8 元/百万输出):0.0017 元自检三件事:
| 检查项 | 通过标准 |
|---|---|
| 估算与厂商 usage 偏差 | 中文文本偏差 < 30% |
| 单次成本量级 | 一次普通问答在 0.001~0.01 元量级 |
| 换算成日成本 | 日活 1000 × 20 次问答 × 0.003 元 ≈ 60 元/天 |
最后这个换算必须在课堂上当场算出来——学员对「几分钱」没概念,对「一个月多少钱」才有概念。
7. 生产避坑
- 不要在生产里依赖估算值计费。估算只用于预算告警阈值和前端提示,实际对账一律用厂商返回的
usage字段落库。两者混用会导致月底对不上账,且排查成本极高。 maxTokens必须显式设置。不设时模型按自己的默认值走,一次「失控的长输出」可能吃掉你几百倍于预期的费用。做法是给每类任务定死上限:分类 50、摘要 500、生成 2000。- 上下文超限的错误往往表现为「答非所问」而不是「报错」。超出窗口时部分厂商会静默截断前文,模型就开始乱答。所以要在调用前主动检查估算 Token 数,超限就压缩历史——这条在 01-06 对话记忆里会落地成代码。
8. 延伸与锚点
- 思考题:如果业务方要求「单次问答成本必须低于 0.002 元」,你会从哪三个地方省钱?(提示:模型档位、上下文长度、缓存——答案分布在 04-07 与 02-15)
- 代码锚点:
git checkout ch01-01-concepts - 下一课时:01-02 第一个 ChatClient
- 对应课件:L01-01 LLM 基础概念与选型