qinghuan
← 理解模型与输入 / 全部笔记

单元 07 / 理解模型与输入

P0 · 必会主线L1 · 基础

从一次 LLM 请求理解 Token 与上下文

从一次 Java 请求入手,看清模型收到了什么、返回了什么,以及用量、耗时和失败怎样记录。

本篇目录 · 按需要跳转

这篇重点学什么

本篇重点:记录请求用了多少 Token、输出是否被截断以及为什么结束,检查调用是否超出预算。

开始读正文 ↓
学过相近内容?查看哪些可以跳过

两条路线有些基础相同。已经学会的部分可以跳过,另一条路线只用来补你当前需要的内容。

Token 与 Embedding

共同基础Token、上下文窗口、词表与向量表示的术语可复用。

另一条路线补充什么?

用 BPE 观察词表怎样建立,再把 Token ID 转成可训练的 Embedding,弄清训练实际接收的输入。

读完这篇,你能做什么

能完成短输入、长输入和超时三组调用记录,并明确结束状态与未知用量。

学习理由、准备要求与知识点

为什么学它

只保存回答正文,就看不出是否截断、花了多少用量、哪里变慢。完整响应是控制预算和排查错误的依据。

先读这些内容

知识点清单

  • 消息角色与请求选项
  • 上下文窗口
  • 输入和输出 Token
  • 预留输出预算
  • 完整响应元数据
  • 结束原因与截断
  • 限流、认证和超时
  • 流式开始与完整结束

运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。

01先弄懂它是什么

接入语言模型时,可以先把它看成一次有时间和用量限制的远程请求。应用发出消息和选项,收到正文或工具请求,以及结束原因、用量等附加信息。模型这次能看到哪些历史,取决于应用实际发送的内容和接口保存会话的方式,不能假定它自动知道所有聊天记录。因此客户端应明确记录本次发了什么、没有发什么。

估算一次请求能装下多少内容,要用 token,而不能只数汉字或英文单词。Token 是模型分词后的单位,长度并不固定。上下文窗口可以理解为单次调用可处理的容量,通常要同时考虑输入和允许生成的输出,具体以接口规则为准。系统消息、历史、工具定义和消息包装也可能占用空间,不能只计算用户输入的那一句话。

一次长任务可能发起多轮请求,所以“这次最多生成多少”和“整个任务最多花多少”要分别限制。预留输出空间、设置总轮数和总用量上限,可以让任务到达限制时明确说明为什么停止。响应开始到达不等于完整成功;还要检查最终结果里的结束原因、是否截断,以及最终用量。

文中这几个词,先认一认

上下文窗口
一次模型调用可使用的内容容量,怎么计量、是否与输出共享额度,要看具体模型和接口。
完整回合
应用规定必须放在一起的一组消息,例如一次工具请求及它的所有结果,避免只保留半段信息。
结束原因
响应中说明为什么停止的字段,例如正常回答完、达到长度上限,或因其他情况中止。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

本篇选择一个保守截取规则:系统约束和当前问题必须保留,历史从最新完整回合往前选,预算不足时停止。若历史成本按旧到新为四、三、五,可用九,选三与五共八;再加入最旧四会超限。这个规则保留连续最新后缀,不是追求任意历史组合中信息最多。

先从总窗口减去系统消息、当前问题、工具描述、输出预留和安全余量,余下才是历史预算。所有成本由匹配当前模型与消息格式的计数器提供;代码不假装用字符串长度实现真实Tokenizer。必须项已经超限时应缩小输入或明确失败,不能偷偷删掉系统约束。

完整回合是预算单位。工具调用和结果拆开可能破坏消息协议,摘要也要保留来源与未完成操作。截取后再次按最终请求格式核算,避免模板包装带来的差异。调用结束时同时保存请求版本、结束原因和用量,之后才能判断是输入不足、长度截断还是远端错误。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart TD
  W["单次窗口"] --> B["扣除必须项和输出预留"]
  B --> H["历史可用预算"]
  H --> S["按完整回合选择最新后缀"]
  S --> C["最终请求复算"]
  C --> R["受限调用"]
  R --> E["结束原因与任务账本"]

按顺序看,每一步做了什么

  1. 确认模型窗口及接口计量规则,选择匹配的Token计数方式。
  2. 计算必须项、输出预留和安全余量,得到历史可用预算。
  3. 按完整回合从新到旧选择连续后缀,预算不足即停止。
  4. 对最终组装请求复算并执行受限调用。
  5. 核对完整结束、用量和结果契约,更新全任务预算。

03对照实现代码

OpenJDK 17 ArrayDeque 源码 (新标签页)↗

OpenJDK 17 GA,jdk-17+35 · ArrayDeque.addFirst / pollLast

下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。

核心机制代码 · java
Deque<Turn> selected = new ArrayDeque<>();
for (int i = turns.size() - 1; i >= 0; i--) {
    Turn turn = turns.get(i);
    if (turn.tokens() > remaining) break; // 本策略保留连续后缀,不跳过中间回合。
    selected.addFirst(turn); // 反向选择,正向组织请求。
    remaining -= turn.tokens();
}

ArrayDeque用循环数组表示双端队列,addFirst在队首插入,pollLast移除队尾并在空时返回null。队列不接纳null元素,适合作为明确的回合容器。

从后向前扫描再addFirst,使最终遍历顺序仍是时间正序;若改成addLast,历史顺序会被反转,即使Token总数完全正确也会改变上下文含义。

JDK容器只管理对象顺序,不知道Token、消息角色或模型窗口。tokens字段必须来自外部计数契约,本篇算法没有实现分词。

队列操作通常为摊还常数时间,扩容会复制元素。本算法也可通过确定起始索引再复制后缀实现,重要的是完整性与预算不变量,不是特定容器名称。

代码先调用谁,再调用谁

  1. 外部计数器提供完整回合成本
  2. 扣除必须项和输出预留
  3. 反向扫描历史
  4. addFirst恢复时间正序
  5. 最终请求复算与调用

04动手写一个例子

预算内最新完整回合后缀

输入非负历史Token成本和可用预算,返回最长可容纳的连续后缀索引;最新回合放不下则返回空。程序不计算真实Token,Java17 main运行边界自测。

输入 1
成本[4,3,5],预算9
输出 1
索引[1,2]
输入 2
成本[2,8],预算5
输出 2
空

从最新回合向旧扫描,每一步都检查当前成本是否不超过剩余预算。使用cost>remaining,避免先相加再比较引发整数溢出。

一旦某回合放不下就停止,因为任何更长的连续后缀都必须包含它;跳过它继续找更小旧回合,会变成另一个非连续选择问题。

成本为零的回合可以继续加入,所以预算零不一定意味着历史必为空。先统一检查负数,避免被截断到未访问区域的错误成本悄悄漏过验证。

为什么这样写是对的每次选择后,已选回合构成原历史的连续后缀且总成本不超过预算;下一项不能加入时,任何更长连续后缀都不满足预算。

时间 O(n)额外空间 O(k)

n为历史回合数,先校验再逆序扫描;k为返回回合数。额外队列存k个索引,输出复制也为O(k),不包含外部分词成本。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static List<Integer> suffix(long[] costs,long budget){
        if(costs==null||budget<0)throw new IllegalArgumentException("budget");
        for(long c:costs)if(c<0)throw new IllegalArgumentException("negative cost"); // 校验全部输入,不漏掉未选历史。
        Deque<Integer> selected=new ArrayDeque<>();long left=budget;
        for(int i=costs.length-1;i>=0;i--){
            if(costs[i]>left)break; // 不能跳过中间回合,否则不再是连续后缀。
            selected.addFirst(i);left-=costs[i]; // 减法保持非负,避免求和溢出。
        }
        return new ArrayList<>(selected);
    }
    static void check(List<Integer> e,List<Integer> a){if(!e.equals(a))throw new AssertionError(a);}
    public static void main(String[] args){
        check(List.of(1,2),suffix(new long[]{4,3,5},9));
        check(List.of(),suffix(new long[]{2,8},5));
        check(List.of(),suffix(new long[]{},0));
        check(List.of(0,1),suffix(new long[]{0,0},0));
        check(List.of(1),suffix(new long[]{1,Long.MAX_VALUE},Long.MAX_VALUE));
        try{suffix(new long[]{-1,8},2);throw new AssertionError();}catch(IllegalArgumentException expected){}
        System.out.println("PASS 6 tests");
    }
}

这些特殊情况也要试一试

  • 最新回合超限时不能跳过它伪造连续上下文。
  • 零成本回合仍可进入零预算后缀。
  • 总预算与成本使用非负long,并以剩余量避免溢出。
进阶选读 · 后续再读:价值与预算共同约束的上下文选择

场景较早但重要的事实被最新后缀策略淘汰。

做法先把必须状态单独结构化保存,再对可选完整证据组估值,可用背包或检索选择;任何摘要都保留来源和不确定性。

复杂度整数预算背包可用O(nB)伪多项式动态规划,B是预算数值;一般语义价值并不可精确量化,不能保证答案质量最优。

好处与代价连续后缀简单可审计,价值选择更灵活但依赖估值且可能破坏对话连贯,需要独立评测。

05项目里怎么用

客户端应区分认证错误、输入超限、限流、超时和服务端错误。是否重试取决于错误类别及是否发生外部副作用;模型请求超时后用量也可能未知,账本需要未知状态而不是填零。限制重试次数与总截止时间,避免局部恢复耗尽整个任务。

流式页面可以展示增量,但正式结果必须等完整终止并通过解析。达到输出限制时,JSON可能只生成半截,不能作为正常结构化结果入库。保存适量脱敏元数据帮助复现,避免为了调试把所有用户资料永久记录在日志中。

验收时准备窗口边缘样本、超大工具结果和多轮累积场景。检查最终请求实际计数、必须消息保留、回合顺序与终止状态。单个短问题成功只能说明最小通路可用,不能证明长任务在预算边界上稳定。

容易踩的坑

  • 用汉字数或字符串长度冒充当前模型Token计数。
  • 只限单次输出,不限制任务总轮数和总预算。
  • 截取时拆散工具请求与结果。
  • 看到流式首字就把运行标成成功。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
最新完整后缀策略明确,保留最近对话可能丢失较早关键事实短会话和可靠基线
带来源摘要减少重复文字并保留主旨有损且需验证关键约束长会话工作记忆
检索历史证据按当前问题选择相关资料增加索引、权限与召回风险较大授权历史库

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01上下文窗口和任务预算相同吗?

展开参考答案与推导

窗口限制一次调用可用范围,任务预算累计多次调用;两个约束要分别检查。

02为何必须保留完整工具回合?

展开参考答案与推导

请求ID与结果有关联关系,拆散可能违反消息协议或造成模型误读;预算应以不可拆单元计算。

03结束原因为什么不能丢?

展开参考答案与推导

它区分正常完成和截断等情况;只保存文本会让半份结果看起来像完整答案,影响解析、评估和重试。

换个情况,还会处理吗

01最后一个回合比预算大,怎样处理?

展开参考答案与推导

选择缩小受控工具结果、摘要或明确告知超限;本后缀算法返回空,不跳到更早回合假装仍保留最新连续历史。

02同样文字换模型突然超限,先查什么?

展开参考答案与推导

核对Tokenizer、窗口与消息包装计量是否随模型配置更新;相同字符数不意味着Token成本相同。

03用户看到半份JSON,后台该记什么?

展开参考答案与推导

保留流式中断或长度截断状态,禁止当成功DTO提交;已显示片段与正式结果分开记录。

写代码试一试

01零预算还能选什么?

输入 / 条件
成本[3,0,0],预算0
输出 / 预期
索引[1,2]

提示 不要用预算为零直接退出。

展开参考答案与推导

最新两个零成本回合可加入,遇到成本三时停止;不变量仍是总成本不超预算。

02为何不用累计成本加当前成本比较?

输入 / 条件
当前总和接近Long.MAX_VALUE
输出 / 预期
使用剩余预算减法

提示 先比较再更新。

展开参考答案与推导

相加可能溢出为负而错误通过检查;cost不超过remaining时再相减,结果始终合法非负。

03如何测回合顺序?

输入 / 条件
成本[1,1,1],预算2
输出 / 预期
[1,2]而非[2,1]

提示 反向扫描与输出次序分开。

展开参考答案与推导

用addFirst插入所选索引,最终自然恢复时间正序;测试不能只比较元素集合。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能完成短输入、长输入和超时三组调用记录,并明确结束状态与未知用量。

本篇重点加练

输出预算与结束原因决定应用能否把一次模型响应当作完整结果,需要在连续请求中反复验证。

  • 预留输出预算
  • 结束原因与截断

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    给一份带系统约束、用户问题和历史消息的请求草图,标出各字段归属;解释上下文窗口限制在应用里何时检查,以及哪些内容由模型决定。

    提交内容
    一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
    怎样算完成
    • 逐项回应题目要求,区分事实、假设与个人判断。
    • 能用自己的例子解释适用条件,记录尚未弄懂的点。

    练到的知识点消息角色与请求选项 · 上下文窗口

  2. 作业 02代码推演

    设窗口上限为4096,当前消息估算占3100,工具定义占420,希望预留800输出Token。自行制定超限策略并逐项记账;不要把字符数直接当作Token数。

    提交内容
    手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
    怎样算完成
    • 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
    • 主动检查至少一个边界或异常条件,能解释手算与运行差异。

    练到的知识点输入和输出 Token · 预留输出预算

  3. 作业 03原理问答

    某应用把“文本以句号结束”作为成功条件。定义请求预算、结束标记与业务完成三者的独立契约,列出仅凭文本无法判定的状态。

    提交内容
    概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
    怎样算完成
    • 关键概念有明确边界,能用案例检验声明。
    • 未获证据支持的假设单独标注。

    练到的知识点预留输出预算 · 结束原因与截断

综合应用

  1. 作业 04面试场景

    两次调用返回相同文本,一次结束原因正常,一次为长度截断。设计响应记录与下游接收规则,说明需要哪些元数据才能判断是否完成。

    提交内容
    场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
    怎样算完成
    • 方案回应给定约束,列出至少一种替代方案及代价。
    • 每项关键结论说明所需证据,不能核实的部分明确标注。

    练到的知识点完整响应元数据 · 结束原因与截断

  2. 作业 05编程实践

    写一个假LLM适配器,依次产生认证失败、限流、超时、流已开始但无结束事件四种结果;设计调用端状态及测试,禁止把所有失败转为空字符串。

    提交内容
    可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
    怎样算完成
    • 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
    • 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。

    练到的知识点限流、认证和超时 · 流式开始与完整结束

  3. 作业 06排错实验

    用自造长历史触发上下文不足,再改变工具定义大小;记录请求前检查、服务端拒绝与输出截断分别出现在哪一层,无法访问真实模型时明确模拟范围。

    提交内容
    最小故障输入、复现步骤、观测记录、定位依据及回归用例。
    怎样算完成
    • 其他人可按记录复现故障,并区分现象与已验证原因。
    • 定位过程有观测依据,回归用例可以再次暴露原问题。

    练到的知识点上下文窗口 · 预留输出预算 · 结束原因与截断

  4. 作业 07编程实践

    编写请求账本模拟器:同一任务的工具结果长度逐轮增加,每轮自行给出输入占用、可用窗口与输出预留;检查每轮接收结果时能否满足你声明的完成条件。

    提交内容
    可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
    怎样算完成
    • 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
    • 验证目标由合同确定,模拟结果不冒充真实服务证据。

    练到的知识点预留输出预算 · 结束原因与截断

  5. 作业 08排错实验

    构造响应正文已收齐但结束元数据丢失、元数据到达却正文只收一半的两组离线记录。验证应用的结果提交规则,并记录未决状态如何被保留。

    提交内容
    最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
    怎样算完成
    • 故障可以稳定重现,现象与已验证原因分开。
    • 观测记录足以判断声明的合同是否被违反。

    练到的知识点预留输出预算 · 结束原因与截断

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    设计多个模型供应商共用的响应接口,列出必须保留、可以标准化和暂不能统一的信息,并给出迁移时的兼容性检查清单。

    提交内容
    设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
    怎样算完成
    • 比较使用一致条件与事先声明的标准,记录失败与未知结果。
    • 说明方案的适用前提、未覆盖风险及后续验证方法。

    练到的知识点消息角色与请求选项 · 完整响应元数据 · 限流、认证和超时 · 流式开始与完整结束

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 反例构造

    为“只要预留了足够输出Token,就不会出现不完整回答”构造一个不依赖窗口超限的反例,给出观察证据与该反例能否推广的边界。

    提交内容
    具体反例、原主张的适用前提、复现或逻辑证明及结论边界。
    怎样算完成
    • 反例满足原主张所声称的前提,确实针对其结论。
    • 说明一个反例能推翻什么、不能进一步证明什么。

    练到的知识点预留输出预算 · 结束原因与截断

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗