Appearance
LLM 基础:Token、上下文与幻觉
看在前面
B 轨开篇。素材全部来自 star-site 平台 AI 服务(star-site-platform-ai-service/app/,Python + LangChain)的真实生产代码。来源包括:llm.py 的 chat_model 工厂(temperature 选型、lru_cache、mock-key)。还有 agents/finance_diagnosis.py 与 agents/report_interpret.py 的提示词与字数约束。以及 config.py 的 effective_mock 沙箱设计。
写法上刻意避开公式堆砌。面试里能把注意力机制、token、温度、幻觉用"一句人话 + 一个自己项目的例子"讲清。这比推导 QKV 矩阵乘法更加分。姊妹篇:RAG体系与项目定位、系统提示词工程-从财务诊断说起。
一、Transformer 与注意力:一句话讲清
**注意力机制一句话:生成每个字时,模型回头看一遍前文。**它给每个已出现的 token 算一个"相关度权重",然后加权混合所有 token 的信息来预测下一个。 查询(Q)、键(K)、值(V)只是这个"回头看"的三种投影。Q 是"我现在想找什么",K 是"每个位置能提供什么"。Q 和 K 相似度越高权重越大,V 是实际被取走的内容。多层堆叠后,底层学局部搭配("空腹血糖"是一个词组),高层学语义关系("偏高"呼应前文的参考区间)。
面试常被追问的两点,用人话答:
- 为什么注意力能"理解"长依赖:每个位置到每个位置都有直连边(全连接),不像 RNN 要逐步传递。"月供占比过高"出现在开头,结论段照样能直接引用它,路径长度是 1,不是 N;
- 位置编码是干嘛的:注意力本身对顺序无感(打乱输入它也照算)。必须额外把"这是第几个 token"编码进向量,模型才知道"上月收入"在"本月支出"前面这种顺序语义。
把一次生成展开看(自回归):模型拿到 system + human 消息的全部 token,逐个生成回复 token。每生成一个,它就并入前文参与下一轮注意力。所以"流式输出打字机效果"不是产品炫技,是生成机制的本来面目。第 N 个 token 生成时,前面 N-1 个 token 是它唯一的上下文。这也解释了为什么模型偶尔"说到一半改口"——它写后半句时才"看到"自己前半句写了什么。
再往下(多头、KV cache、FlashAttention)属于加分项。答出"多头 = 多组独立的回头看,各组关注不同类型的关系(语法/数值/位置)"即可,不必展开。KV cache 值得多记一句:推理时已算过的 K/V 会缓存复用,避免每生成一个 token 都重算全前文。上下文越长,KV cache 显存占用越大,这才是长上下文贵的底层原因(不只是计费贵)。
二、Token:计费、限流与上下文的统一单位
token 是模型处理文本的最小单位,也是一切工程账的记账单位。 英文约 4 字符一个 token;中文取决于分词器,一个汉字大致 0.6~1 个 token(GLM 系对中文较友好)。三件工程的事都以 token 计:
- 计费:输入 token 和输出 token 分开计价,输出通常贵数倍。所以提示词里"全文 350 字以内"既是产品约束,也是成本约束。输出被压住,单次调用成本就有了上限;
- 上下文窗口:模型一次能"看见"的 token 总量(输入 + 输出共享)。可以把窗口理解为工作记忆。窗口外的内容模型完全不知道。窗口内的内容也未必都记牢(见第四节);
- 限流与超时:厂商按 tokens/min 限流;
llm.py里timeout=60就是因为长输入推理慢。不给足超时会误杀正常请求。
ai-service 里"控制输入 token"的设计是自觉的:finance_diagnosis._render 把三张表渲染成 6 行清单,report_interpret._render_indicators 把指标渲染成行式列表(- 收缩压: 138mmHg(参考 90~140)【偏高】)。几十项指标 + 一段提示词,总输入远小于窗口的一个零头。输入越小,注意力越集中,成本越低,延迟越短,三个收益同向。
拿 report_interpret 单次调用粗算一笔账(数量级示意,非精确值):
| 组成 | 内容 | 量级 |
|---|---|---|
| 输入 | 系统提示词(约 130 汉字) | ~100-130 token |
| 输入 | 40 项指标 × 行式清单 | ~600-800 token |
| 输出 | 不超过 300 字的解读 | ≤ ~300 token |
结论一目了然:这类链路的成本大头在输出(单价更高)而不是输入。所以"300 字以内"这条提示词约束,每月账单上是有对应行的。反过来,如果哪天把全家庭成员历年报告都塞进这个 human 消息,输入 token 先膨胀一个数量级。再撞注意力稀释的墙(第四节)。
工程上还有一条配套纪律:上下文预算要显式管理。链路设计时回答三个问题。哪些内容必须进窗口(本次报告指标)、哪些可以裁剪(与问题无关的账户明细)。哪些该换成按需获取(检索/工具调用,见RAG体系与项目定位)。
三、temperature 与解码策略:ai-service 的真实选型
解码是"有了下一个 token 的概率分布之后怎么挑"。三种基本策略先列成表:
| 策略 | 做法 | 适合 | 副作用 |
|---|---|---|---|
| 贪心(greedy) | 永远挑概率最高的 | 分类、抽取、固定格式输出 | 车轱辘话,重复循环 |
| 采样 + temperature | 按重分布后的概率随机抽 | 创作类、对话 | 温度高时不稳定 |
| top-p(核采样) | 只在累计概率达 p 的候选集里抽 | 通用,砍长尾胡话 | p 太小表达力下降 |
temperature 的作用机制一句话:它不改变候选词排名,只改变分布的陡峭程度。temperature < 1 削峰(高概率的更高、低概率的更低),趋近 0 就近似贪心;> 1 拉平分布,冷门词有了可观的出场机会——创意场景要它,事实场景怕它。top-p 与 temperature 一个砍候选集、一个调权重,方向不同可叠加。但工程惯例是固定其一调另一个,免得两个旋钮互相打架没法归因。
app/llm.py 的工厂把参数钉死在生成处而不是散在各业务:
python
@lru_cache(maxsize=4)
def chat_model(vision: bool = False) -> ChatOpenAI:
return ChatOpenAI(
base_url=settings.llm_base_url, # OpenAI 兼容协议,换厂商零改码
api_key=settings.llm_api_key or "mock-key", # 沙箱不真调,占位符过非空校验
model=settings.llm_vision_model if vision else settings.llm_model,
temperature=0.2, # 健康财务场景:同样输入应得类似回答,不要发散
timeout=60,
max_retries=1,
)三条设计理由值得在面试里讲。0.2 是产品语义决定的——报告解读和财务诊断是"蒸馏"式任务。这类任务把确定性数据压成人话,发散是缺陷不是创意。
参数收敛在工厂——温度这类生成参数不归业务代码管,避免各链路各写一个魔法数。@lru_cache 复用实例——ChatOpenAI 构造涉及 HTTP 客户端建立。同进程缓存后调用方拿到的还是线程安全的同一实例。maxsize=4 覆盖 vision 真假组合绰绰有余。
如实记录一个已知边界:对话链路(chat_graph.py 的闲聊助手)与蒸馏链路共用这个工厂,也被锁在 0.2。对话场景本可用默认温度(0.7~1.0)让回答更自然。改进方向是给 chat_model() 加 temperature 参数并按场景传值。代价是缓存键变多、各调用点要显式声明选型。"统一默认 + 按需分化"的演进节奏,比一上来就全参数化更稳。
四、幻觉的三个成因:概率本质、知识截止、上下文挤压
幻觉 = 模型流畅地输出了不真实的内容。 它不是 bug,是概率式生成的固有属性:
- 概率本质:模型的输出是"下一个 token 的概率分布"。训练目标是"像训练语料一样说话",不是"说真话"。财务诊断里模型完全可能编一个"建议配置年化 4% 的稳健理财"。原因:语料里财务建议长这样。流畅 ≠ 正确,这句话要在面试第一分钟说出来;
- 知识截止:参数里的知识冻结在训练数据截止日。问它今天的汇率、最新政策,它没有"我不知道"的默认行为。它会用旧知识补一个像样的答案;
- 上下文挤压:窗口大 ≠ 记得住。长上下文里中间位置的信息注意力权重会被稀释(lost in the middle 现象)。无关内容塞多了,真正关键的指标行反而被"挤"出注意力。这也是第二节强调小输入的深层原因——省 token 不只是省钱,是在保护注意力质量。
三条成因对应三种不同的"治法",混为一谈是常见误区。概率本质 → 用输出约束和结构化输入缩小自由发挥空间。知识截止 → 用上下文注入(RAG/工具调用)而不是换更新的模型。上下文挤压 → 裁剪输入、把相关内容放到靠近指令的位置。先诊断成因,再选手段——这句方法论在面试里比任何单点技巧都值钱。
五、工程缓解:ai-service 的四层手段
幻觉无法根除,只能用工程手段圈住它的活动范围。ai-service 的四层,从输入到输出到降级:
5.1 结构化输入:把判断依据预先算好
_render 系列函数把数据库里的强类型字段渲染成清单行。指标行直接带参考区间和 abnormalFlag 渲染出的【偏高】【偏低】标记。判断所需的证据预先算好喂进去,模型只负责组织语言,不需要自己推断"138 对 90~140 意味着什么"。输入越结构化,模型自由发挥(= 幻觉)的空间越小。反面做法是把原始 JSON 或整段自由文本甩给模型让它"自己看着办"。那是把确定性计算外包给了概率机器。
5.2 约束输出:把输出压进可解析的窄轨道
report_ocr 的系统提示词钉死"只输出一个 JSON 对象,不要任何其他文字或 Markdown 代码块标记"。看不清的字段用 null——null 这条尤其关键,它给了模型一个合法的"我不知道"出口。没有出口时模型倾向于编一个像样的值。解析侧再用宽容策略兜底(剥掉 ```json 围栏后 json.loads,字段缺失靠 pydantic 默认值)。提示词约束是概率性的,解析兜底是确定性的,两层都要。解读侧则约束"全文 300 字以内 + 不要 Markdown",输出形态可预期。
5.3 行为红线写进提示词
chat_graph 的系统提示词明写"不确定就说不确定"。它把"承认无知"从模型的弱默认行为提升为指令级要求。report_interpret 要求"异常指标请遵医嘱就诊",这是幻觉的后果管理。即使解读有偏差,兜底话术防止用户拿 AI 输出替代就医。红线的写法有讲究(为什么用禁令不用形容词、免责为什么放结尾)。完整拆解见系统提示词工程-从财务诊断说起。
5.4 沙箱降级:确定性的极端形态
config.py 的 effective_mock 干脆不调模型,从根上消掉当次调用的幻觉可能:
python
@property
def effective_mock(self) -> bool:
# 优先级:显式 true 强制沙箱 → 显式 false 但无 key 仍回沙箱 → 未设置看 key
explicit = self._explicit_mock()
if explicit in ("true", "1", "yes"):
return True
if explicit in ("false", "0", "no"):
return not self.llm_api_key
return not self.llm_api_key设计要点:"没配 key 自动进沙箱"让全链路(含演示、联调、CI)在零费用、零幻觉的方式下跑通。返回的 MOCK_TEXT 带显眼的【沙箱模拟】前缀,明确标记"这不是模型输出"。注意第三条分支的细节——显式关沙箱但没 key,仍然回落沙箱而不是在调用时炸掉。降级判断收拢在配置层一处,业务代码只看 effective_mock 一个布尔。配套细节在 llm.py:沙箱下 api_key 传占位符 "mock-key" 过构造函数的非空校验。占位符永远不会被真正使用,因为分支在调模型之前就返回了。
这四层的共同思想:不要指望模型自律。用输入结构、输出约束、提示红线、确定性降级把幻觉圈进笼子。至于"数据量大了输入塞不下怎么办",出路是检索——见RAG体系与项目定位。
追问链
面试官:temperature 设成 0 是不是就没有幻觉了?
不是。temperature 只影响"怎么从分布里挑",不改变分布本身。模型对事实的错误置信是学出来的,挑最贪心的那个 token 照样是错的。低温降低的是随机波动,不是事实错误率——所以缓解幻觉靠第二节的输入/约束手段,不靠调参。
面试官:上下文窗口都 128K 了,为什么还要做检索/压缩?
三个原因。一是成本按 token 计,长输入每次调用都付费。二是注意力稀释,中间内容召回变差(lost in the middle)。三是时效内容模型根本不在参数里,塞进窗口才有。窗口大解决的是"放得下",不是"用得好"。
面试官:你们项目里幻觉是怎么量化发现的?
诚实答:当前靠约束降低发生面 + 免责话术兜底,尚无系统性评测集。改进方案是构造固定输入的金标准集(同一份指标清单,人工审定期望输出)。对"建议是否有数据支撑""免责声明是否在场"做逐项核对。这也是提示词 A/B 的基础设施(见姊妹篇第四节)。
面试官:top-p 和 temperature 同时调会怎样?
先砍长尾再削峰,两个都激进会让分布过度集中,输出变呆板。工程惯例是固定其一调另一个;ai-service 只钉 temperature=0.2,不动 top-p。
面试官视角小结
- 注意力一句话:生成每个 token 时加权回看全前文。Q/K/V 是"找什么/有什么/取什么"的投影,位置编码补顺序感;
- token 三账:计费(输出更贵,350 字上限是成本上限)。窗口(工作记忆,输入输出共享)。限流/超时(
timeout=60的由来)。输入小 = 省钱 + 护注意力 + 低延迟; - 温度选型:蒸馏式任务(解读/诊断)钉 0.2。参数收敛在
chat_model工厂 +lru_cache复用。对话场景应放宽是已知改进项。能讲清"为什么统一、何时分化"比背参数值加分; - 幻觉三成因:概率本质(流畅≠正确)、知识截止、上下文挤压;
- 四层缓解:结构化输入(预计算判断依据)→ 约束输出(JSON only / 字数 / 禁 Markdown)。提示红线(不确定就说不确定 + 遵医嘱)。沙箱降级(
effective_mock,无 key 自动确定性输出)。
2026-10 新增(P3 AI 栈 · 素材来自 ai-service)。姊妹篇:ReAct与LangGraph工程实践、智能体的身份链与工具安全。