在调试大语言模型或传统机器学习模型时,最让人头疼的往往不是模型报错,而是模型“成功运行”后输出了一堆莫名其妙的垃圾。垃圾输出可能是一段重复的文本、一个完全错误的标签、一段格式混乱的 JSON,也可能是一段看似通顺但结论明显错误的回答。很多人遇到这种情况,第一反应是继续问模型、换 prompt、调 temperature,甚至怀疑模型文件坏了。但实际排查下来,真正的原因往往不在模型本身,而在调用模型的那段代码里。不读代码,就只能看到“输出很垃圾”这个结果;读了代码,才能知道垃圾是在哪个环节被生产出来的。下面会从概念、现象、定位链路、代码示例、常见坑和工程规范几个方向讲清楚:为什么识别模型垃圾输出必须读代码。
1. 先理解“模型垃圾输出”到底是什么
1.1 垃圾输出的定义和本质
“模型垃圾输出”并不是一个严格定义的学术词汇,在工程里通常指模型在推理阶段返回的无意义结果。它既包括肉眼可见的乱码,也包括结构上正确但语义错误的标签,甚至包括看起来非常流畅但完全不符合业务要求的回答。
从系统性角度看,输出是整条数据链路的产物。一个文本从原始输入到最终返回,至少经过预处理、tokenizer、前向计算、解码采样、后处理五个环节。任何一个环节出现 bug,都可能让模型输出变成垃圾。权重文件损坏会产生垃圾,tokenizer 词表错位会产生垃圾,prompt 模板拼接错误会产生垃圾,解码参数设置极端会产生垃圾,后处理解析失败也会产生垃圾。
正因为如此,光看输出结果去猜模型好坏,几乎不可能定位根因。通常的做法是,把输出当作一个“报警信号”,然后沿着代码调用链一层一层往回查。只有同时看到模型的前向计算代码、tokenizer 解码代码、后处理代码和数据映射代码,才能判断垃圾到底在哪一层产生。
1.2 从输出类型反推问题范围
不同形态的垃圾输出,往往指向不同代码层的问题。下面这张表可以作为快速分诊工具:
| 输出类型 | 典型现象 | 常见代码层原因 | 检查方向 |
|---|---|---|---|
| 乱码 | 输出包含无法识别的 Unicode、替换符 | tokenizer 词表错位、编码方式不一致 | tokenizer 加载路径与权重路径 |
| 重复文本 | 同一句话重复几十遍 | 采样参数缺失或温度过低 | no_repeat_ngram_size、repetition_penalty |
| 标签错位 | 输入正面内容,输出“愤怒”标签 | label2id/id2label 映射不一致 | 训练代码与推理代码的标签列表 |
| 格式错误 | JSON 被截断、括号不匹配 | 生成长度过短、停止词设置缺失 | max_new_tokens、stopping criteria |
| 看似合理但错误 | 文字通顺,数值或结论不匹配 | prompt 模板误导、数据清洗顺序不一致 | prompt 拼接代码、数据预处理代码 |
注意,这里的“输出类型”只是分诊入口。真正定位时,还是要落回代码。比如“标签错位”这一行,表面看是模型预测错了,但读代码后会发现,模型预测的索引其实是对的,错的是推理侧手动写的id2label。
1.3 为什么“输出通顺”会掩盖问题
大模型发展到今天,文本层面的流畅度已经很高,这带来一个副作用:人眼很容易被“看起来像人话”的输出误导。一段输出即使语法完全正确,也可能在业务上完全不可用。
举例来说,一个信息抽取模型把一句话完整返回,但缺少了 promise 的name字段;一个代码生成模型生成了一段可读但调用了不存在 API 的代码;一个分类模型把“正面情绪”识别成“negative”标签,但因为标签映射错误,显示出来的是“平静”。这些输出本身并不乱码,甚至可以被业务系统接收,但最终结果仍然是垃圾。
只有读代码,才能判断输出是否满足“契约”。这个契约包括:字段是否齐全、类型是否正确、标签是否属于预定义集合、生成内容是否被截断、特殊 token 是否被过滤。肉眼只能看见文本,代码才能看见约束。
判断模型输出是否正常,不能只看输出文本里有没有人话,还要看输出是否满足业务约束、是否符合模型接口的返回契约。
2. 不读代码时,你会被哪些表面现象误导
2.1 表面通顺但业务不可用
业务系统往往要求模型输出结构化内容,例如 JSON、SQL、分类标签或固定格式代码。此时,一段语句通顺的文本反而可能是垃圾输出。
一个典型场景是使用模型做客服工单分类。模型返回“这个问题需要转人工处理”,文本很通顺,但业务字段category_id是空字符串。最终下游系统无法路由工单,只能记录为失败。这个问题的根因通常在后处理代码:解析模型返回时没有校验字段是否存在,遇到缺失字段直接给默认值,或者try...except把异常吞掉。
要识别这类垃圾输出,必须读后处理代码。后处理是模型输出进入业务系统前的最后一道闸门,也是垃圾输出最容易隐藏的地方。
2.2 高置信度也可能来自错误 logits 映射
分类模型通常会输出每个类别的概率,很多人会认为softmax分数越高,结果越可靠。但如果id2label映射写错,置信度再高也是无用。
下面这段代码很常见:
import numpy as np logits = np.array([2.5, 0.2, 0.1]) prob = np.exp(logits - logits.max()) / np.exp(logits - logits.max()).sum() pred_id = int(np.argmax(prob)) # 训练时 id2label = {0: "positive", 1: "negative", 2: "neutral"} # 推理时却写成了 {0: "neutral", 1: "positive", 2: "negative"} print(id2label[pred_id])如果推理代码里的id2label和训练代码不一致,模型明明预测了positive,最终输出的却是negative。表面看是“模型预测错了”,实际是代码映射错了。此时无论怎么调模型参数,都改不了这个 bug。
2.3 “看起来像代码”的文本不一定可执行
当模型用来生成代码时,输出文本看起来可能很像代码,但直接运行会报错。常见情况包括:变量名拼写错误、调用不存在的库函数、函数参数位置错误、缩进被 markdown 代码块污染。这类输出只有在执行验证时才会暴露问题。
因此,代码生成场景不能只靠人眼阅读输出来判断,应该在评估脚本中对生成代码做语法解析、编译或单元测试。否则,一段“像模像样”的垃圾代码会一直被当作正常结果。
2.4 随机性让表面变化掩盖固定 bug
大模型解码阶段带有很多随机因素,do_sample=True、temperature>0、top_p都会让每次输出不同。同一个输入,第一次返回“好的”,第二次返回“没问题”,第三次返回长篇大论。这时候人很容易误以为模型状态正常,只是随机波动。
但如果固定 seed、固定参数后重新运行,输出仍然有问题,排查链路就会清晰很多。随机性不能掩盖代码 bug,却会让人把 bug 误判成“模型偶尔抽风”。不读代码、不确定随机种子,就很难区分固定 bug 和随机采样噪声。
import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42) torch.cuda.manual_seed_all(42)排查垃圾输出时,第一件事就是固定随机种子,复现同一条输出路径。
3. 从输出反查代码:一条可复用的定位链路
3.1 固定输入、固定 seed、固定参数
任何定位工作都需要稳定的复现条件。先保存触发垃圾输出的原始输入,再固定全局随机种子、解码参数、batch size 和运行设备,最后重新运行一次推理脚本。
python infer.py --input "今天天气不错" --seed 42 --max_new_tokens 128这样做的目的,是把问题从“模型不稳定”中剥离出来。如果固定条件下能复现,说明存在确定性的代码路径;如果不能复现,可能是上游输入、并发状态或显存初始化问题,也要从代码中继续排查。
3.2 从输出向前逐个环节检查
推荐按照下面的顺序检查:
- 打印原始输入字符串,确认不是上游传入了
None、空串或不可见字符。 - 打印输入经过 tokenizer 后的
input_ids,确认与文本能对应上。 - 打印模型原始
logits和预测索引,确认模型输出索引是否合理。 - 打印
id2label或tokenizer.decode结果,确认索引映射是否正确。 - 打印后处理函数的输入输出,确认结果是否被规则改写或截断。
每到一个环节,都把当前结果与预期对比。只要某一步的输出不符合预期,问题就定位到这一步。
下面是一个简单的中间结果检查示例:
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("my_model") inputs = tokenizer("今天天气不错") print(inputs["input_ids"]) print(tokenizer.convert_ids_to_tokens(inputs["input_ids"]))如果convert_ids_to_tokens打印出来的 token 与文本含义不一致,说明 tokenizer 加载或词表有问题,后续生成的垃圾输出就不难解释了。
3.3 用“最小样本 + monkey patch”定位数据流
在复杂项目里,原始输入到模型输入之间可能隔着很多自定义函数。为了不打断业务逻辑,可以用临时 monkey patch 在关键入口打印中间结果。
original_decode = tokenizer.decode def debug_decode(*args, **kwargs): result = original_decode(*args, **kwargs) print("[decode]", args[0], "->", result) return result tokenizer.decode = debug_decode这只是一个临时排查手段,定位完就要移除。它适合在本地或测试环境使用,生产环境不要保留此类调试代码。
3.4 区分训练代码与推理代码不一致
训练和推理最常见的冲突,是“同一份配置写了两遍”。训练代码里保存了一份label_list,推理代码里手写了一份,时间一长两处就不同步了。
| 配置项 | 训练时代码 | 推理时代码 | 不一致后果 |
|---|---|---|---|
| 标签顺序 | ["愤怒", "喜悦", "平静"] | ["平静", "喜悦", "愤怒"] | 所有标签错位 |
| 文本清洗 | 先去掉换行再分词 | 未去换行直接分词 | 输入分布不一致 |
| 字符编码 | 统一 UTF-8 写入 | 按系统默认编码读取 | 乱码 |
读代码时,要重点比对这两份代码对同一份数据处理方式。如果训练和推理的预处理逻辑不完全一致,垃圾输出几乎是必然的。
4. 用最小代码示例识别一次“垃圾输出”
4.1 场景描述
假设训练了一个中文情感分类模型,类别包括“喜悦”“愤怒”“平静”。训练结束后,验证集准确率 0.95。但到了推理阶段,输入“今天天气不错,我很开心”,模型返回“愤怒”。
直观判断,模型输出垃圾。但模型权重没有任何变化,为什么离线验证正常、在线推理出错?
4.2 表面现象:模型输出了错误标签
下面是一段常见的推理代码:
# infer.py import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer model = AutoModelForSequenceClassification.from_pretrained("my_sentiment_model") tokenizer = AutoTokenizer.from_pretrained("my_sentiment_model") # 注意:推理代码里手动写了 id2label id2label = {0: "平静", 1: "愤怒", 2: "喜悦"} text = "今天天气不错,我很开心。" inputs = tokenizer(text, return_tensors="pt") with torch.no_grad(): logits = model(**inputs).logits pred = int(logits.argmax(-1)) print("预测结果:", id2label[pred])运行后输出:
预测结果: 愤怒从文本来看,这句话应该是“喜悦”,输出“愤怒”明显是垃圾输出。
4.3 读代码后发现根因
在训练代码中,类别顺序可能是:
label2id = {"愤怒": 0, "喜悦": 1, "平静": 2} id2label = {v: k for k, v in label2id.items()}也就是说,模型内部预测的索引1本来对应“喜悦”。但推理代码里手写的id2label[1]却对应“愤怒”。模型没有错,错的是推理侧索引映射。
要验证这个判断,可以打印模型的预测索引和配置信息:
print("pred index:", pred) print("config id2label:", model.config.id2label)输出会显示类似:
pred index: 1 config id2label: {0: "愤怒", 1: "喜悦", 2: "平静"}到这里,问题已经定位:预测索引是 1,对应模型配置里的“喜悦”。推理代码手写的映射把 1 映射成了“愤怒”。
4.4 修复后的变化
不要手写id2label,直接使用 checkpoint 中的配置:
id2label = model.config.id2label再运行一次:
预测结果: 喜悦这个例子很小,但很能说明问题:垃圾输出并不一定来自模型权重,而可能来自推理代码与训练代码之间的不一致。不读代码,永远看不见映射错误。
4.5 这个案例说明什么
模型输出是“结果”,代码链路是“过程”。同一个结果可能对应完全不同的原因。面对垃圾输出时,先保存证据,再检查代码,而不是先换模型或调 prompt。一旦固定了输入和输出,再对代码做逐层反查,大多数问题都能在几分钟内定位。
很多看似“模型变笨了”的问题,最后查出来都是权重路径、tokenizer 或标签映射写错。不要急着怪模型。
5. 读代码时的关键检查点:从模型加载到输出返回
5.1 检查模型权重路径和版本
模型部署最隐蔽的问题是加载了错误的权重。服务进程没有重启,代码里写的是绝对路径,但在另一台机器上路径不同,导致加载了同名旧权重。这类问题不读代码很难发现。
建议在推理代码里增加启动日志,打印完整模型路径、文件大小和校验值:
ls -l model_dir/ sha256sum model_dir/pytorch_model.bin同时将模型版本与代码 commit 绑定,避免“权重更新了但代码没有同步更新”的错位。
5.2 检查 tokenizer 与 vocab 对齐
文本生成模型输出乱码,最常见的原因不是模型参数出了问题,而是 tokenizer 词表和模型权重不是同一份。
可以在代码里加一个检查:
print("tokenizer vocab size:", tokenizer.vocab_size) print("model vocab size:", model.config.vocab_size) print("len(tokenizer):", len(tokenizer))两个数字不一致时,要优先检查from_pretrained的目录是否指向了同一个 checkpoint。有些项目用本地目录加载模型权重,却用线上 tokenizer 目录加载词表,出现错位就不奇怪了。
5.3 检查解码参数
解码参数是生成类模型垃圾输出最集中的来源。下面是常见参数汇总:
| 参数 | 含义 | 常见范围 | 错误设置可能导致的垃圾输出 |
|---|---|---|---|
| temperature | 采样温度 | 0.1 ~ 1.0 | 过高时输出随机词,句子无逻辑 |
| top_p | 核采样累积概率 | 0.8 ~ 0.95 | 过低时句式单调、容易重复 |
| top_k | 候选词数量 | 20 ~ 100 | 过小时语义狭窄 |
| repetition_penalty | 重复惩罚 | 1.0 ~ 1.3 | 过高时语义混乱,过低时重复 |
| no_repeat_ngram_size | 禁止 n-gram 重复 | 3 ~ 5 | 缺失时出现死循环式重复 |
| max_new_tokens | 最大生成长度 | 任务相关 | 过短截断 JSON,过长输出废话 |
| do_sample | 是否随机采样 | false / true | 需要稳定结果时开启会产生波动 |
读代码时,不只要看参数有没有写,还要看它们被用在了哪里。有些推理代码定义了参数,但生成时忘记传给generate()方法,等于没有生效。
5.4 检查后处理中的过滤、截断和规则改写
后处理函数经常是垃圾输出的“最后一公里”。比如有人用字符串替换清理模型输出:
cleaned = output.replace("\n", "").replace(" ", "")这会把 JSON 字符串里的空格全部删除,导致json.loads失败,甚至改变代码语义。还有人在后处理里用正则截取子串,正则写错了,就会把有效内容全部截掉。
针对后处理代码,要检查三点:
- 是否有必要的字段完整性校验。
- 是否有异常分支,是否把异常静默吞掉。
- 对输出长度、特殊字符、特殊 token 的处理是否符合预期。
5.5 检查 batch inference 和 collate_fn
批量推理时,padding 和 attention_mask 一旦写错,模型会对被 padding 的位置也做注意力计算,导致整个 batch 的输出错乱。
检查collate_fn时,至少确认两点:
def collate_fn(batch): input_ids = [item["input_ids"] for item in batch] attention_mask = [item["attention_mask"] for item in batch] return { "input_ids": torch.tensor(input_ids), "attention_mask": torch.tensor(attention_mask), }第一,attention_mask是否真的被传入模型;第二,padding token 是否与模型配置一致。只有 padding,没有 mask,是批量推理垃圾输出的高频原因。
6. 常见“垃圾输出”的现象、根因与修复
6.1 模型输出全是同一个 token 或重复文本
现象:生成结果类似"好的好的好的好的...",几十甚至几百个字都在循环。
常见根因:no_repeat_ngram_size没有设置,temperature设置过低,top_p设置过小,导致模型陷入高概率循环带。部分模型自身也可能存在训练数据中的重复模式。
检查方式:打印每一步生成的 token 和对应概率,观察连续几步是否都在同一个 token 上。
修复方式:
generate_kwargs = { "max_new_tokens": 128, "temperature": 0.8, "top_p": 0.9, "no_repeat_ngram_size": 3, "repetition_penalty": 1.1, "early_stopping": True, }参数需要结合模型和任务微调,不要直接照搬。
6.2 输出包含</s>、<unk>、<pad>等特殊符号
现象:模型返回内容中出现<unk>、</s>、<pad>或[UNK]。
常见根因:
- 生成后调用
tokenizer.decode时没有设置skip_special_tokens=True。 - tokenizer 加载错误,
<unk>索引被当成正常 token 生成。 - 输入文本包含太多词汇表外的字符,模型只能生成
<unk>。
检查方式:打印tokenizer.special_tokens_map,确认特殊 token 对应的 id;再打印解码时的参数。
修复方式:
tokenizer.decode(generated_ids, skip_special_tokens=True)如果是词表错位,需要重新加载与权重匹配的 tokenizer 目录。
6.3 生成 JSON 或代码时被截断
现象:模型返回{"name": "张三", "age":,JSON 明显不完整。
常见根因:max_new_tokens设置得太小,生成到一半被截断;没有配置针对结束 token 的停止策略;后处理没有校验 JSON 是否合法。
检查方式:增加最大长度,查看完整生成结果;检查是否在结束 token 或停止词处截断。
修复方式:为结构化任务设置足够的生成长度,同时在代码中实现停止条件。示例:
from transformers import StoppingCriteria, StoppingCriteriaList class StopOnToken(StoppingCriteria): def __init__(self, stop_token_id): self.stop_token_id = stop_token_id def __call__(self, input_ids, scores, **kwargs): return input_ids[0, -1] in self.stop_token_id stop_criteria = StoppingCriteriaList([StopOnToken(stop_token_id)])不过停止 token 并不是万能方案。对 JSON 生成任务,更稳妥的做法是在输出后做语法校验,解析失败时触发重试或降级逻辑。
6.4 输出标签或分数与业务预期完全错位
现象:分类模型返回的标签名称看起来正常,但分布与业务完全对不上;或者排序模型给出的分数极高,但排序结果不合理。
常见根因:label2id和id2label映射不一致;训练时数据清洗了特殊字符,推理时没有清洗;模型输出 logits 被错误地套上了 softmax,导致阈值判断失效。
检查方式:打印训练时保存的映射和推理时使用的映射,逐项对比。
修复方式:训练结束时,把label2id和id2label写入模型配置:
model.config.label2id = label2id model.config.id2label = id2label model.save_pretrained("output_dir")推理时直接从model.config读取,禁止手写映射表。
6.5 多语言混合或乱码
现象:输入中文,模型输出中夹杂英文、拼音或不可读字符;输出显示é”™这类编码错误。
常见根因:模型权重与 tokenizer 不是同一份;预处理阶段编码不一致;生成时skip_special_tokens=True但 tokenizer 的解码逻辑有版本差异。
检查方式:
print(tokenizer.decode(tokenizer.encode("测试")))如果解码结果不是“测试”,说明 tokenizer 自身有问题。还要检查模型文件目录下是否真的包含vocab.txt、tokenizer_config.json等文件。
修复方式:使用与模型权重同目录的 tokenizer,重新保存并加载配置。不要在运行时用AutoTokenizer去远程自动下载,避免版本漂移。
7. 建立“输出可回溯”的工程规范
7.1 每次推理都留痕
垃圾输出只要发生一次,就必须能回到当时的输入、参数和模型版本。推荐为每次推理生成唯一的request_id,并把关键信息记录到日志或结构化存储中。
{ "request_id": "req_20250610_001", "model_version": "bert-sentiment-20250601", "commit_id": "a1b2c3", "input_text": "今天天气不错,我很开心。", "output": "喜悦", "params": { "temperature": 0.2, "top_p": 0.9, "max_new_tokens": 128 } }有了这份记录,读代码时就不需要靠“我记得当时怎么调的”来回忆,直接复现现场即可。
7.2 把模型版本和代码版本绑定
模型文件除了存放权重,还要在 config 或目录中记录训练代码 commit、训练数据版本、PyTorch 版本和 tokenizer 版本。否则换一台机器部署时,很容易出现“权重是新的,代码是旧的”的错位。
可以在保存模型时写一个额外的version.json:
{ "model_name": "sentiment_bert", "train_commit": "a1b2c3", "data_version": "20250601", "tokenizer_path": "models/sentiment_tokenizer" }7.3 准备回归用例集
针对垃圾输出建立一套固定回归用例,每个用例包含输入、期望输出、允许误差范围。当模型或代码变更后,先运行回归用例,再上线。
用例不需要多,但要覆盖易错点:
- 空字符串输入。
- 英文、中文、日文混合输入。
- 特殊符号和 emoji 输入。
- 超长输入。
- 需要输出 JSON 的指令。
- 需要输出固定标签的指令。
7.4 将“读代码”纳入模型交付评审
很多团队在评审模型时,只看评估指标,不看推理代码。但指标只反映验证集上的平均表现,代码 bug 会直接污染真实输出。评审清单至少包括:
- 模型权重路径是否可复现。
- tokenizer 与模型 vocab 是否一致。
label2id/id2label是否来自训练代码。- prompt 模板是否与训练时一致。
- 解码参数是否固定并有依据。
- 后处理是否校验输出格式。
- 失败分支是否记录日志并返回可理解错误。
这些项都要求评审者真正阅读代码,而不是只看输出样例。
7.5 发布前检查清单
实际部署前,可以按下面这份清单逐项打勾:
- 输入字符能在 tokenizer 中正确编码。
- 模型能加载权重,且打印的 vocab_size 与 config 一致。
generate传入的参数与预设完全一致。- 后处理能解析合法输出,非法输出不会被静默吞掉。
- 日志中包含 request_id、输入、输出、模型版本和代码 commit。
- 固定 seed 后回归用例输出稳定。
- 特殊 token、重复文本、JSON 截断三种垃圾输出有对应测试用例。
清单看起来琐碎,但在排查“模型输出时好时坏”的问题时,能节省大量时间。
8. 扩展:从一次定位到系统性监控
8.1 离线回归测试
把垃圾输出排查经验沉淀成自动化测试。每个根因对应一个用例,防止同类 bug 再次出现。
def test_output_without_special_tokens(): output = infer("请回答:天气怎么样") assert "<unk>" not in output assert "</s>" not in output def test_output_json_valid(): result = infer("请返回一个 JSON:{\"name\": \"张三\"}") assert json.loads(result)["name"] == "张三"这些测试不需要覆盖所有场景,只需要覆盖最容易出错的契约。每次改代码、改模型、改参数后运行一遍,能提前拦截大部分回归问题。
8.2 在线监控指标
生产环境需要对模型输出做实时监控。同一类垃圾输出出现频率升高时,说明可能有上游数据变化、代码配置变更或模型缓存未更新。
可以统计以下指标:
- 无效 JSON 率。
- 特殊 token 出现率。
- 输出完全重复率。
- 输出平均长度是否偏离基线。
- 分类标签分布是否发生漂移。
- 请求超时和 retry 次数。
监控代码通常不复杂,关键是把“垃圾输出”变成可量化的指标:
signals = { "invalid_json_rate": invalid_json_count / total_count, "special_token_rate": special_token_count / total_count, "duplicate_ngram_rate": duplicate_ngram_count / total_count, }8.3 日志与告警
不要让垃圾输出静默进入业务系统。在后处理阶段增加告警逻辑,一旦发现非法格式、特殊 token、重复文本,立即记录异常并返回降级结果。
告警信息至少包含:
- 当前模型版本。
- 当前代码 commit。
- 原始输入和输出。
- 触发告警的具体原因。
这样,下次再出现“模型垃圾输出”,你手头已经有一份完整现场,可以直接开始读代码,而不是从零排查。
8.4 最终判断:垃圾输出不是模型单方面的问题
在 Transformer 模型、扩散模型、模型蒸馏、模型融合以及各种部署实践中,垃圾输出的表现各不相同,但根因都落在代码链路里。vLLM在某些加速卡环境下无法启动 Embedding、Reranker 模型,输出为空或直接抛错,是部署代码与硬件不兼容;蒸馏模型输出分数分布不合理,是教师模型、学生模型和推理逻辑没有对齐;模型融合后标签混乱,是融合层的映射表写错。
识别模型垃圾输出的能力,不是靠猜,而是靠读代码建立起来的。一旦你在一条数据链路上反复定位过几回,就会明白输出只是最后一声警报,真正的病灶往往藏在读取权重、拼接 prompt、解析结果那几行代码里。下一次再看到模型输出异常,建议先把输出连同请求原始输入保存下来,然后从推理代码第一行开始往前读,你会比反复调 prompt 更快找到答案。