这篇重点学什么
本篇重点:记录请求用了多少 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["结束原因与任务账本"]按顺序看,每一步做了什么
- 确认模型窗口及接口计量规则,选择匹配的Token计数方式。
- 计算必须项、输出预留和安全余量,得到历史可用预算。
- 按完整回合从新到旧选择连续后缀,预算不足即停止。
- 对最终组装请求复算并执行受限调用。
- 核对完整结束、用量和结果契约,更新全任务预算。
03对照实现代码
OpenJDK 17 GA,jdk-17+35 · ArrayDeque.addFirst / pollLast
下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。
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字段必须来自外部计数契约,本篇算法没有实现分词。
队列操作通常为摊还常数时间,扩容会复制元素。本算法也可通过确定起始索引再复制后缀实现,重要的是完整性与预算不变量,不是特定容器名称。
代码先调用谁,再调用谁
- 外部计数器提供完整回合成本
- 扣除必须项和输出预留
- 反向扫描历史
addFirst恢复时间正序- 最终请求复算与调用
04动手写一个例子
预算内最新完整回合后缀
输入非负历史Token成本和可用预算,返回最长可容纳的连续后缀索引;最新回合放不下则返回空。程序不计算真实Token,Java17 main运行边界自测。
- 输入 1
成本[4,3,5],预算9- 输出 1
索引[1,2]
- 输入 2
成本[2,8],预算5- 输出 2
空
从最新回合向旧扫描,每一步都检查当前成本是否不超过剩余预算。使用cost>remaining,避免先相加再比较引发整数溢出。
一旦某回合放不下就停止,因为任何更长的连续后缀都必须包含它;跳过它继续找更小旧回合,会变成另一个非连续选择问题。
成本为零的回合可以继续加入,所以预算零不一定意味着历史必为空。先统一检查负数,避免被截断到未访问区域的错误成本悄悄漏过验证。
为什么这样写是对的每次选择后,已选回合构成原历史的连续后缀且总成本不超过预算;下一项不能加入时,任何更长连续后缀都不满足预算。
n为历史回合数,先校验再逆序扫描;k为返回回合数。额外队列存k个索引,输出复制也为O(k),不包含外部分词成本。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
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 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能完成短输入、长输入和超时三组调用记录,并明确结束状态与未知用量。
本篇重点加练
输出预算与结束原因决定应用能否把一次模型响应当作完整结果,需要在连续请求中反复验证。
- 预留输出预算
- 结束原因与截断
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
给一份带系统约束、用户问题和历史消息的请求草图,标出各字段归属;解释上下文窗口限制在应用里何时检查,以及哪些内容由模型决定。
- 提交内容
- 一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
- 怎样算完成
- 逐项回应题目要求,区分事实、假设与个人判断。
- 能用自己的例子解释适用条件,记录尚未弄懂的点。
练到的知识点消息角色与请求选项 · 上下文窗口
- 作业 02代码推演
设窗口上限为4096,当前消息估算占3100,工具定义占420,希望预留800输出Token。自行制定超限策略并逐项记账;不要把字符数直接当作Token数。
- 提交内容
- 手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
- 怎样算完成
- 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
- 主动检查至少一个边界或异常条件,能解释手算与运行差异。
练到的知识点输入和输出 Token · 预留输出预算
- 作业 03原理问答
某应用把“文本以句号结束”作为成功条件。定义请求预算、结束标记与业务完成三者的独立契约,列出仅凭文本无法判定的状态。
- 提交内容
- 概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
- 怎样算完成
- 关键概念有明确边界,能用案例检验声明。
- 未获证据支持的假设单独标注。
练到的知识点预留输出预算 · 结束原因与截断
综合应用
- 作业 04面试场景
两次调用返回相同文本,一次结束原因正常,一次为长度截断。设计响应记录与下游接收规则,说明需要哪些元数据才能判断是否完成。
- 提交内容
- 场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
- 怎样算完成
- 方案回应给定约束,列出至少一种替代方案及代价。
- 每项关键结论说明所需证据,不能核实的部分明确标注。
练到的知识点完整响应元数据 · 结束原因与截断
- 作业 05编程实践
写一个假LLM适配器,依次产生认证失败、限流、超时、流已开始但无结束事件四种结果;设计调用端状态及测试,禁止把所有失败转为空字符串。
- 提交内容
- 可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
- 怎样算完成
- 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
- 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。
练到的知识点限流、认证和超时 · 流式开始与完整结束
- 作业 06排错实验
用自造长历史触发上下文不足,再改变工具定义大小;记录请求前检查、服务端拒绝与输出截断分别出现在哪一层,无法访问真实模型时明确模拟范围。
- 提交内容
- 最小故障输入、复现步骤、观测记录、定位依据及回归用例。
- 怎样算完成
- 其他人可按记录复现故障,并区分现象与已验证原因。
- 定位过程有观测依据,回归用例可以再次暴露原问题。
练到的知识点上下文窗口 · 预留输出预算 · 结束原因与截断
- 作业 07编程实践
编写请求账本模拟器:同一任务的工具结果长度逐轮增加,每轮自行给出输入占用、可用窗口与输出预留;检查每轮接收结果时能否满足你声明的完成条件。
- 提交内容
- 可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
- 怎样算完成
- 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
- 验证目标由合同确定,模拟结果不冒充真实服务证据。
练到的知识点预留输出预算 · 结束原因与截断
- 作业 08排错实验
构造响应正文已收齐但结束元数据丢失、元数据到达却正文只收一半的两组离线记录。验证应用的结果提交规则,并记录未决状态如何被保留。
- 提交内容
- 最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
- 怎样算完成
- 故障可以稳定重现,现象与已验证原因分开。
- 观测记录足以判断声明的合同是否被违反。
练到的知识点预留输出预算 · 结束原因与截断
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
设计多个模型供应商共用的响应接口,列出必须保留、可以标准化和暂不能统一的信息,并给出迁移时的兼容性检查清单。
- 提交内容
- 设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
- 怎样算完成
- 比较使用一致条件与事先声明的标准,记录失败与未知结果。
- 说明方案的适用前提、未覆盖风险及后续验证方法。
练到的知识点消息角色与请求选项 · 完整响应元数据 · 限流、认证和超时 · 流式开始与完整结束
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
为“只要预留了足够输出Token,就不会出现不完整回答”构造一个不依赖窗口超限的反例,给出观察证据与该反例能否推广的边界。
- 提交内容
- 具体反例、原主张的适用前提、复现或逻辑证明及结论边界。
- 怎样算完成
- 反例满足原主张所声称的前提,确实针对其结论。
- 说明一个反例能推翻什么、不能进一步证明什么。
练到的知识点预留输出预算 · 结束原因与截断
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Spring AI:ChatClient (新标签页)↗
按所选 Spring AI 版本阅读请求构造、完整响应、实体映射及流式响应;保留元数据用于验收。
- 实践教程英文
Hugging Face:Tokenizers (新标签页)↗
阅读词级、字符级与子词分词,以及编码解码过程;用来理解字符数不能直接替代 Token 数。
- 深入参考文档英文
Transformers:Generation 配置参考 (新标签页)↗
查阅长度、采样与生成配置参数的定义和约束,核对所接框架真正支持哪些选项。
- 深入参考文档英文
OpenJDK 17 ArrayDeque 源码 (新标签页)↗
按指定版本定位
ArrayDeque.addFirst/pollLast;对照源码分析理解实际调用边界。