Skip to content

LLM 基础概念与模型选型:先算清楚再动手 ​

1. 本节产出 ​

你能在不看任何资料的情况下回答三个问题:一次对话到底发给模型的是什么、花多少钱、该选哪个模型。并且能用一段 20 行的 Java 代码,把任意一段中文文本估出 Token 数、算出单次调用成本。

2. 前置依赖 ​

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

选型三步法:

  1. 先用旗舰模型跑通,确认这件事模型能做好;
  2. 再换中档模型跑同一批评测集,看质量掉多少;
  3. 掉得少就降级,掉得多就把「只有旗舰能做的部分」单独路由到旗舰。

绝大多数项目做完这三步,成本能降 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
// ch01-basics/src/main/java/com/aitech/basics/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
// ch01-basics/src/main/java/com/aitech/basics/util/CostResult.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. 生产避坑 ​

  1. 不要在生产里依赖估算值计费。估算只用于预算告警阈值和前端提示,实际对账一律用厂商返回的 usage 字段落库。两者混用会导致月底对不上账,且排查成本极高。
  2. maxTokens 必须显式设置。不设时模型按自己的默认值走,一次「失控的长输出」可能吃掉你几百倍于预期的费用。做法是给每类任务定死上限:分类 50、摘要 500、生成 2000。
  3. 上下文超限的错误往往表现为「答非所问」而不是「报错」。超出窗口时部分厂商会静默截断前文,模型就开始乱答。所以要在调用前主动检查估算 Token 数,超限就压缩历史——这条在 01-06 对话记忆里会落地成代码。

8. 延伸与锚点 ​

  • 思考题:如果业务方要求「单次问答成本必须低于 0.002 元」,你会从哪三个地方省钱?(提示:模型档位、上下文长度、缓存——答案分布在 04-07 与 02-15)
  • 代码锚点:git checkout ch01-01-concepts
  • 下一课时:01-02 第一个 ChatClient
  • 对应课件:L01-01 LLM 基础概念与选型