大模型的工作方式
从 token、上下文和概率生成理解大模型,把能力、事实来源与应用责任分清。
本页内容
这一章解决什么问题#
你已经能用 Vue 写复杂后台,接下来要解决的不是“如何把一句回复显示出来”,而是“怎样把不确定的语言能力放进可交付的产品”。本章用于建立判断依据:客服助手为什么会编造退款规则,长对话为什么会忘记前面的条件,同一个问题为什么会得到不同答案。达到要求的标志,是你能向产品和后端解释这些现象,并指出应由检索、代码还是模型解决。
前置是理解 HTTP 请求、异步函数和 JSON。暂时不需要训练神经网络,也不需要购买模型服务。下面的演示是人为设定概率的教学模型,只解释生成过程,不能用于判断真实模型质量。
从文字到下一步选择#
模型首先把输入转换成 token 序列。token 是分词器定义的编码单位,可能是一个字、一部分单词、标点或其他片段。它不是 JavaScript 字符,也不是固定字节数。中文字符数、字符串 length 和 token 数没有通用换算公式。编程时 length 甚至以 UTF-16 代码单元计数;一个表情可能占多个单元,所以拿它计费或判断上下文边界都不可靠。
文本生成可以先理解为:给定之前的序列,模型对下一个 token 给出条件概率,然后选择一个,把它追加到序列中,再重复。现代模型还可能包含推理、多模态及工具相关机制,但“逐步生成有条件的输出”仍是理解接口的起点。输出看起来像一个整体,并不意味着服务端先查到了一篇完整标准答案。
训练阶段调整参数,让模型学习语言规律、知识关联和任务行为。推理阶段利用已有参数处理当前输入。普通聊天中的一句“记住这条规则”,通常只影响当前提供的上下文,不等于把数据库记录永久写入模型参数。需要跨会话记住偏好时,应用必须负责保存、授权、读取和更新,然后选择性放回后续请求。
模型能在上下文里模仿模式。例如给出三张合法工单及其分类,第四张工单可能按同样规则分类。这叫利用上下文学习任务,并不等同于重新训练。示例的标签如果相互矛盾,模型也可能学到错误规则,因此提示词里的样例应像测试数据一样维护。
参数为什么会改变产品表现#
上下文窗口约束一次请求可处理的信息规模。应用规则、用户输入、历史消息、检索材料、工具说明和部分模型的推理开销都可能消耗预算,具体计数以选定模型文档为准。窗口容量大不代表任何位置的信息都能被同样可靠地使用。几十页材料里的一个关键例外,仍可能被忽略。
采样会影响输出的变化程度。temperature 常用于调节分布,top_p 常用于裁剪累积概率范围,但支持情况依模型和端点而异;不要把二者当成所有模型都有的通用开关。降低随机性不能保证事实正确,也不等于严格可重复。生产中更稳妥的目标是让一组代表性任务稳定达标,而不是要求每次措辞完全一致。
max_output_tokens 是输出预算,不是要求模型必须输出这么多内容。预算过低可能截断回答,也可能因推理占用预算而导致可见文本很少。响应成功与业务完成应分开判断:检查状态、截断原因、拒绝信息、实际内容及用量。OpenAI Responses 的输入输出形式与其他供应商可能不同,本教材只把它作为明确的接口实例。官方文本生成文档
完整实验:同一分布如何得到不同输出#
环境:Node.js 22,文件名 sampling.mjs。不安装依赖;保存后执行 node sampling.mjs。下面的候选词和概率是手工 fixture,不是模型计算结果,也没有调用网络。
import assert from 'node:assert/strict';
// 人工给出“今天的天气”后面可能接的词。
const candidates = [
{ text: '晴朗', probability: 0.6 },
{ text: '多云', probability: 0.3 },
{ text: '下雨', probability: 0.1 },
];
function choose(items, randomValue) {
if (!(randomValue >= 0 && randomValue < 1)) {
throw new RangeError('随机数必须位于 [0, 1)');
}
const total = items.reduce((sum, item) => sum + item.probability, 0);
if (Math.abs(total - 1) > 1e-9) throw new Error('概率之和必须为 1');
let cumulative = 0;
for (const item of items) {
if (item.probability < 0) throw new Error('概率不能为负');
cumulative += item.probability;
if (randomValue < cumulative) return item.text;
}
throw new Error('未命中候选');
}
// 固定随机数让实验可复现;真实采样的来源由运行系统决定。
const outputs = [0.2, 0.7, 0.95].map(value => choose(candidates, value));
assert.deepEqual(outputs, ['晴朗', '多云', '下雨']);
console.log(outputs.join('、'));
console.log('生成结果并不能证明真实天气。');
预期输出第一行是“晴朗、多云、下雨”,第二行提醒输出不构成天气证据。把第二个概率改为 0.4,程序会因为总和不为一而报错。这个错误属于演示算法输入错误,不是模型服务错误。
第一段把概率显式写出来,是为了把“语言上合理”与“世界中正确”分开。第二段累计概率,相当于把零到一的线段按比例切开,再看随机点落在哪一段。第三段使用固定输入和断言,使你能检查边界条件;真实模型的概率并不是固定表格,通常会随前文变化。
把认识转成工程决策#
设想用户问“订单今天能到吗”。模型可以整理物流信息并写成通顺的回答,却不应从语言概率推断这笔真实订单的状态。后端先查有权限访问的订单和物流接口,再把结果交给模型组织语言。订单是否存在、是否属于当前用户、退款是否成功,都属于外部世界的事实与规则。
类似地,金额计算、时间比较、库存扣减和字段校验应交给确定性代码。模型适合意图理解、文字转换、摘要、候选分类等任务。需要最新资料时补充检索或工具;需要固定规则时编程;需要改变输出格式时先改提示与校验。只有掌握这些低成本手段后,才有依据考虑微调。
幻觉不只是凭空出现一段假话,也包括把旧规则说成现行规则、把相关但不同的产品混为一谈、给没有证据的判断配上真实来源。不能靠一句“请不要胡说”完成质量保证。应用要允许“资料不足”“需要补充信息”“本次调用未完成”等结果状态,并让界面如实表达。
性能也要拆开看。用户感受到的等待包括排队、首个 token 延迟、持续生成和工具调用;一次总耗时不能指出瓶颈。成本除了单次输出,还包括重试、历史重复输入和多轮工具循环。先记录用量与耗时,再讨论换模型或缩短上下文,才不会凭印象优化。
模型内部的表示:为什么它能处理没见过的句子#
token 进入模型后,首先对应到可计算的向量表示。向量不是给每个词手工贴的一张中文释义表,而是训练过程中形成的数字参数。相同词在不同上下文里的作用也不同:“苹果手机坏了”与“苹果放久了”需要联系周围信息判断对象。应用开发者不必手工计算所有矩阵,但需要理解模型不是通过简单关键词查表完成每个任务。
注意力机制可以先理解为按当前任务需要,让序列里的位置相互获取信息。一个位置如何解释,取决于其他相关位置提供的线索。真实模型使用多层、多头与非线性变换,过程比“给词算相关性”复杂得多;这个直觉的用途,是解释为什么前文、顺序和相互冲突的材料会改变输出,而不是把注意力分数当成可靠的因果解释报告。
位置与上下文也会影响可用信息。模型能接收很长输入,不代表它总能从长输入中准确找到所有例外条件。资料过长时,重要细节可能被大量重复内容稀释,模型也可能混淆相似对象。工程上的应对是明确任务、减少噪声、保留来源和关键条件,并用长文档样本评测,而不是只比较宣传中的最大窗口。
模型参数保存的是训练形成的分布规律,不是按客户编号组织的实时业务数据库。因此,它可能知道一般的退款概念,却不知道今天这笔订单是否已经退过款;可能会解释某个框架,却不了解你的私有项目当前实现。给模型补充资料不是承认它“能力不够”,而是为任务提供本来就不在参数里的当前事实。
训练、微调与当前对话分别改变什么#
预训练通常让模型从大量数据中学习语言和世界关联;后续训练可以塑造任务遵循、回答风格和工具使用等行为。应用中的提示和上下文则在不改变参数的前提下影响本次推理。三者作用位置不同,所以故障的修复方式也不同:提示里漏了规则,优先补规则;知识频繁更新,优先接资料;稳定格式与行为仍难以达到要求,才有理由评估进一步训练。
微调并不适合承担每分钟变化的订单状态。即使把订单记录加入训练,更新、删除和访问权限也会变得难以管理,而且模型仍不保证按精确键值返回事实。对于个人资料和公司内部记录,通常应保持在受控数据库与检索系统中,由应用决定本次能提供什么。训练是改变模型行为的一种手段,不是替代普通数据工程。
所谓“模型记住了我”,可能只是应用把历史或用户偏好重新放进请求。要判断实际机制,应看数据如何保存、什么时候回传、用户能否修改和删除。普通对话中的纠正不会自动成为所有未来请求的参数更新。若产品承诺持续记忆,就需要明确的用户数据模型,而不是依赖一条“以后都记住”的自然语言指令。
上下文学习可以通过少量示例展示任务规则,但示例选择具有偏差。若所有“紧急工单”的例子都含感叹号,模型可能过度依赖语气,而不是实际影响范围。设计示例时要覆盖反例:语气平静但全站不可用,也应紧急;语气激动但只是咨询,也未必紧急。这样才能让模型学习任务语义,而不只模仿表面特征。
生成过程中的概率、随机性与复现#
模型通常先产生未归一化的数值分数,再转成候选 token 的概率分布。概率描述的是在给定上下文和当前模型下的输出选择,不直接等于现实事实为真的概率。一句话读起来很自然,只能说明它符合模型学到的语言模式;验证订单、政策或医学事实仍需要外部证据和适当方法。
温度常被描述为改变分布尖锐程度。较低温度让高分候选更占优势,较高温度让其他候选更有机会。但具体模型可能限制参数,推理型接口也可能不允许任意设置。教材里的温度函数是数学演示,不应据此把 temperature: 0 发给所有接口。调用时按选定型号核对支持范围,避免把参数不支持误判成网络故障。
可复现也有多个层次。固定测试数据与固定随机数,可以让教学算法完全重现;真实服务即使参数相同,也可能受后端实现、模型版本和其他因素影响。工程上应记录型号、配置、提示版本与输入快照,并评价任务级稳定性。对分类接口要求结果稳定合理,但对文案生成要求每个字完全相同,往往不是有用的验收标准。
输出预算与生成长度的关系也容易误解。模型可以在达到上限前结束,上限不是目标长度;对于包含推理开销的型号,可见文本与总输出 token 之间也不一定相等。若程序得到 incomplete,不能把半个对象当成合法结果,也不能只靠加一个括号修补。先理解停止原因,再决定缩短任务、提高预算或采用分阶段处理。
示例二:亲自计算温度对分布的影响#
保存为 temperature.mjs,Node.js 22.22+,无需依赖,执行 node temperature.mjs。下面的 logits 完全手工指定;它不加载神经网络,也不模拟真实型号的质量。我们只观察同一组分数经过不同温度后如何改变分布,并验证数值稳定性。
import assert from 'node:assert/strict';
function probabilities(logits, temperature = 1) {
if (!Array.isArray(logits) || logits.length === 0 || !logits.every(Number.isFinite)) {
throw new TypeError('LOGITS_MUST_BE_FINITE');
}
if (!Number.isFinite(temperature) || temperature <= 0) {
throw new RangeError('TEMPERATURE_MUST_BE_POSITIVE');
}
// 先减去最大值,避免直接 exp(1000) 溢出;不改变归一化比例。
const max = Math.max(...logits);
const weights = logits.map(value => Math.exp((value - max) / temperature));
const total = weights.reduce((sum, value) => sum + value, 0);
return weights.map(value => value / total);
}
function sample(probabilities, randomValue) {
if (!(randomValue >= 0 && randomValue < 1)) throw new RangeError('INVALID_RANDOM_VALUE');
let sum = 0;
for (let i = 0; i < probabilities.length; i++) {
sum += probabilities[i];
if (randomValue < sum) return i;
}
return probabilities.length - 1; // 仅兜底浮点累加的舍入误差。
}
const logits = [3, 2, 0];
const cold = probabilities(logits, 0.5);
const warm = probabilities(logits, 2);
assert.ok(cold[0] > warm[0]);
assert.ok(warm[2] > cold[2]);
assert.ok(Math.abs(cold.reduce((a, b) => a + b, 0) - 1) < 1e-12);
assert.deepEqual(probabilities([1003, 1002, 1000]), probabilities(logits));
assert.throws(() => probabilities(logits, 0), /POSITIVE/);
const words = ['晴朗', '多云', '下雨'];
console.log('低温分布=' + cold.map(x => x.toFixed(3)).join(','));
console.log('高温分布=' + warm.map(x => x.toFixed(3)).join(','));
console.log('固定随机数 0.7:' + words[sample(cold, 0.7)] + ' / ' + words[sample(warm, 0.7)]);
预期低温下第一个候选占比更高,高温下第三个候选占比上升;固定随机数零点七可能落入不同候选区间。程序对全部 logits 同时加一千仍得到相同概率,说明减最大值只是为了数值稳定,没有改变相对关系。温度为零在本函数中明确拒绝,这是本地 API 合同,不代表所有服务端都采用相同边界。
阅读这个例子时,要把两个问题分开:改变分布是否增加输出变化,以及变化后的内容是否更符合任务。对于创意标题,变化可能有价值;对于金额抽取,变化可能降低稳定性。不能根据“更有创意”判断“更准确”,也不能根据“每次都一样”判断“事实正确”。
token、字符与成本应该如何测量#
一个中文字符可能对应一个或多个 token,英文单词也可能拆成多个片段。代码中的符号、缩进、长标识符和罕见词会影响编码。使用字符串 length 得到的是 UTF-16 代码单元数量;使用展开运算得到码点数量;用户看到的一个表情还可能由多个码点组成。三者都不是模型 token 计数器。
真正做预算时,应使用匹配模型的 tokenizer 或官方计数能力,并把消息包装、工具 schema、历史与证据一起考虑。只数用户输入会漏掉大部分上下文成本。为了离线学习可以使用字符近似做保守预筛,但要明确标为估算;上线阈值必须通过真实计数与用量数据校准,不应拿经验比例当硬保证。
成本不是单次回答长度的唯一函数。输入历史重复发送、多轮工具循环、失败后重试和评测批次都会增加消耗。一个输出很短的工具 Agent 可能进行了许多轮模型调用,因此比一次长摘要更贵。对产品做预算时,要按完整用户任务统计输入、输出、工具和重试,而不是只按聊天气泡字数估算。
延迟同样要看任务链。首字很快能改善感受,但如果工具还没查到关键事实,过早展示确定结论会误导用户。流式界面可以先显示查询状态,再在证据到达后输出答案;不应把“看起来马上有反应”作为掩盖后台未完成的理由。性能目标应同时包含等待体验和正确的状态语义。
练习:用可重复实验评价一种模型能力#
练习任务是评价客服分类,而不是凭一次回答判断模型好坏。准备三条人工标注问题,每条保留两次候选结果,计算准确率、合法输出率和同题一致率。模型由固定 fixture 代替,因此只能证明评测程序正确,不能证明任何真实模型达到这些分数。保存为 model-eval.mjs,Node.js 22.22+,无依赖,执行 node model-eval.mjs。
完整参考实现:结果合法性、准确率和稳定性
import assert from 'node:assert/strict';
const allowed = new Set(['billing', 'technical', 'other']);
const cases = [
{ id: 'q1', text: '会员费扣了两次', expected: 'billing', runs: ['billing', 'billing'] },
{ id: 'q2', text: '登录按钮无法点击', expected: 'technical', runs: ['technical', 'other'] },
{ id: 'q3', text: '你好', expected: 'other', runs: ['other', 'unknown'] },
];
function evaluate(rows) {
let total = 0, valid = 0, correct = 0, consistent = 0;
const failures = [];
for (const row of rows) {
if (row.runs.length < 2) throw new Error('至少两次结果才能观察一致性');
if (new Set(row.runs).size === 1) consistent++;
row.runs.forEach((answer, attempt) => {
total++;
const legal = allowed.has(answer);
if (legal) valid++;
if (legal && answer === row.expected) correct++;
else failures.push({ id: row.id, attempt, answer, reason: legal ? 'wrong_label' : 'invalid_label' });
});
}
return {
total, validRate: valid / total, accuracy: correct / total,
consistency: consistent / rows.length, failures,
};
}
const result = evaluate(cases);
assert.equal(result.total, 6);
assert.equal(result.accuracy, 4 / 6);
assert.equal(result.validRate, 5 / 6);
assert.equal(result.consistency, 1 / 3);
assert.deepEqual(result.failures.map(x => x.reason), ['wrong_label', 'invalid_label']);
console.log(JSON.stringify(result, null, 2));
这里把非法输出计入准确率分母,否则只删除不合法结果再算准确率,会人为美化表现。同题一致率只说明多次结果是否相同;一个模型每次都答错也可以完全一致。三个指标互相补充,不能拿其中一个替代所有质量判断。真正接入后,还应按不同问题类型分组,避免大量简单问题掩盖少量重要失败。
练习可以继续增加一个“语气激动但只是咨询”的反例,再增加“语气平静但影响所有用户”的故障。观察模型是否过度依赖情绪词。更换提示后保持相同测试集与判断规则,记录改进和退步的样本,而不是只展示变好的例子。固定测试集也要有未参与调试的保留集,避免对少数样本过度优化。
如何把能力边界写成产品需求#
面对一个新需求,先问产物是文本、建议还是实际操作。文本产物可以允许一定表达变化,但需要正确范围和内容规则;建议需要依据与置信度表达;实际操作需要权限、确认、幂等和结果核验。三者可以出现在同一个聊天界面里,却不能共用“模型回复成功”这一种完成条件。
再问错误是否容易被用户发现。把标题写得不够好,用户通常能直接判断;把内部政策中的例外漏掉,用户可能完全不知道。后者需要更强证据与评测,而不是仅仅换一个更自信的模型。把可验证性作为设计维度,能帮助你决定哪些任务适合自动完成,哪些应该保留人工确认或明确澄清。
最后问事实变化速度。稳定的编程概念、刚更新的公司制度、实时订单状态需要不同数据来源。模型可以负责解释和组织,但应用负责选择资料、核对权限和处理变化。只有把这条边界说清楚,后续学习提示工程、RAG 与工具调用时,才不会把它们当成互相替代的流行名词。
验收标准#
你应能运行概率实验并解释三种输出,能指出 token 与字符的差别,能把“能生成一句话”与“知道事实”区分开。面对具体功能,至少能列出一个必须交给代码的规则、一个外部事实来源、一个失败时给用户的状态。只记住术语而不能做这些判断,还不能算完成。
三个自测问答#
- 问:把温度降到最低,为什么仍不能保证答案正确?答:它主要影响选择过程,不会自动补齐缺失事实,也不验证外部状态。
- 问:多轮聊天是否自动永久保存记忆?答:不是;上下文如何保存和回传由应用及具体接口机制决定,普通消息不会直接修改模型参数。
- 问:为什么摘要可以用模型,退款执行不能只靠文本回答?答:摘要的产物就是文本;退款有实际副作用,必须由受权限和事务保护的业务服务执行并验证。
本章示例验证记录#
已在 Node.js 22 离线运行采样、温度分布和分类评测三个完整程序,验证固定随机输入、概率归一化、非法温度与质量指标。没有运行真实模型;这些实验解释算法和测量方法,不构成任何供应商模型能力排名。
官方资料#
继续阅读 OpenAI 关键概念、文本生成 与 Node.js 断言。模型窗口、参数支持与计费在真正接入前按所选型号核对;本章不承诺任何账号的可用型号或额度。