很多人第一次做 LLM 应用,遇到“模型输出垃圾”的时候,第一反应是骂模型:“这个模型怎么这么蠢”“是不是选错了模型”“还是得换个大参数模型”。但如果你真的把请求链路完整读一遍,往往会发现另一件事:模型确实被冤枉了。真正让输出变得“垃圾”的,是调用代码。
这个场景每天都在发生。群里有人问“为什么 GPT 模型让它返回 JSON,它非要夹带一段解释”,实际上代码把 system prompt 漏传了;有人说“上下文材料给了 20 页,模型输出跟材料完全无关”,实际上是拼接上下文时被静默截断;还有人吐槽“同一个 prompt 跑三次,三次都不一样,有时候直接开始复读”,大概率是采样参数设置得太激进。
所以这篇文章的核心判断是:不读代码,就无法识别模型垃圾输出。模型输出质量不是结果,而是一条完整链路上的产物。识别“垃圾”的责任归属,必须从读代码开始。本文会拆解模型输出的完整链路,分析垃圾输出在哪个环节产生,并给出一个可直接复用的输出质量检查工具。
1. 不读代码,你根本分不清“模型垃圾”和“代码垃圾”
把“模型输出垃圾”当作症状,背后可能是两种完全不同的病。
第一种,模型本身能力不足,或者模型被错误部署、错误量化、错误使用。比如你拿一个小参数模型去做复杂推理任务,它怎么调都很难稳定输出正确答案。第二种,调用代码在某个环节破坏了输入或输出,导致模型原本可以输出高质量结果,却在链路中被“加工”成了垃圾。
这两种病的处理方式完全不同。前者需要换模型、换部署策略、优化微调数据;后者需要改代码、修参数、补解析逻辑。如果你不读代码,就会把它们混为一谈。
这里可以做一个类比:摄像头拍不清楚,你先别急着骂摄像头传感器不行。有可能是镜头没擦、焦距没对准、传输线接触不良、显示器分辨率太低。模型就是传感器,代码是镜头、传输线和显示器。输出画面模糊时,画面本身已经不能告诉你问题出在哪,你必须逐个环节排查。
读代码能读到什么?至少能读到以下内容:
- 输入侧:prompt 模板是怎么渲染的,system prompt 是否真的传给了模型,few-shot 示例格式是否正确,RAG 拼出来的上下文是否完整。
- 参数侧:temperature、top_p、max_tokens、repetition_penalty 等采样参数到底设置成了什么值。
- 模型侧:真正调用的模型名称是什么,是 base model 还是 chat model,上下文窗口大小是多少。
- 输出侧:后处理代码是否用正则误删了合法内容,JSON 解析失败后是否有兜底逻辑,流式输出是否丢包。
- 业务侧:对输出结果的校验规则、重试机制、缓存策略是否合理。
识别模型垃圾输出的第一原则就是先分清责任链。输出结果只是最下游的“症状”,真正的病灶可能在任何一个环节。这也是为什么这篇文章强调“不读代码就无法识别”——因为只有读代码,才能把“模型不行”和“代码有问题”这两个结论真正区分开。
2. 模型输出链路拆解:垃圾可能藏在七个环节
一次完整的模型调用,从业务产生请求到最终展示给用户,至少要经过七个环节。任何一个环节出错,都可能让最终结果变成“垃圾输出”。
| 环节 | 核心代码对象 | 常见垃圾输出表现 |
|---|---|---|
| 1. Prompt 模板渲染 | 模板字符串、变量替换 | 指令残缺、示例错位、变量为空 |
| 2. Token 化与上下文截断 | Tokenizer、上下文窗口处理 | 长文本被静默截断、特殊 token 丢失 |
| 3. 采样参数设置 | temperature、top_p、max_tokens 等 | 发散、复读、输出过短 |
| 4. 模型推理 | 模型服务、量化、批处理 | 内容混乱、幻觉加重 |
| 5. 输出序列解码 | 解码参数、停止词 | 生成不终止、截断、含特殊 token |
| 6. 后处理与解析 | 正则、字符串处理、JSON 解析 | 合法输出被误删、字段解析失败 |
| 7. 业务校验与兜底 | 校验规则、异常捕获、重试逻辑 | 错误输出被当成成功结果返回 |
先说第一个环节,Prompt 模板渲染。这是最容易出问题,也最容易被忽略的地方。比如你设计了一个很漂亮的模板,里面写着“请只输出 JSON,不要输出额外解释”,但是代码里把 system prompt 放错了位置,或者拼接变量时用了错误的字段名,导致模型真正收到的 prompt 和你的设计完全不一致。等到输出看起来很奇怪时,你已经不会想到是模板渲染的问题了。
第二个环节,Token 化与上下文截断。很多模型有固定的上下文窗口,比如 4096 token。如果你的 RAG 代码拼接了大量文档,就很容易超过这个限制。更麻烦的是,不少框架的默认截断策略是“从头截断”或“从尾截断”,它不会告诉你丢了哪部分内容。模型看到的是残缺的上下文,输出自然跑偏。
第三个环节,采样参数设置。temperature 过高会让模型生成更随机的内容;top_p 设置得太大也会增加发散;repetition_penalty 缺失时,长文本很容易出现“复读机”现象。这些问题通常不会让输出“完全不能看”,但会让输出质量明显下降,尤其是长文本生成任务。
第四个环节,模型推理本身。这里要关注模型部署方式、量化等级、批次大小。同样的模型,FP16 和 INT4 量化后的输出质量会有差异;并发请求过多时,部分推理框架会降低生成质量。这些因素都写在代码里,不读代码就看不到。
第五个环节,输出序列解码。比如生成停止词设置错误,模型可能会一直生成下去,直到撞上 max_tokens 上限,留下一个半截句子;或者没有正确过滤特殊 token,导致输出里夹杂一堆“<|endoftext|>”之类的标记。
第六个环节,后处理与解析。这是“代码制造垃圾”的重灾区。一个常见的极端例子是:开发同学为了让输出更干净,用正则把花括号内的内容全部删掉,结果把整个 JSON 对象也删没了。模型输出的明明是合法内容,到了业务侧却变成了空字符串。
第七个环节,业务校验与兜底。很多应用在拿到模型输出后,直接交给业务逻辑使用,没有校验、没有重试、没有降级。一旦模型输出格式异常,系统就会把“垃圾输出”原样送到用户面前,甚至写入数据库。
理解这七个环节之后,你就会明白:所谓“模型输出垃圾”,其实是链路问题的最终呈现。排查问题的方向,应该是沿着这七个环节往回走,而不是盯着最后的输出发呆。
3. 垃圾输出的真正反模式:代码里最常见的六种坑
这一节重点讲实际开发中最常见的六种“垃圾输出”反模式。它们都有一个共同特征:只看输出,会觉得是模型有问题;一旦读代码,问题会立刻暴露。
3.1 截断型垃圾:max_tokens 设置太小
你要求模型生成一篇完整方案,它写到一半就断掉了,最后留下一句没说完的话。这时候大多数人会觉得“模型生成能力不行”。但代码里的真相往往是这样的:
# 错误示例:max_tokens 被写死成了 128 params = { "temperature": 0.7, "max_tokens": 128, }模型原本需要 500 个 token 才能把回答写完,你只给它 128 个 token 的预算,它只能在半路停下来。这种问题的特征非常明显:输出末尾通常是一个不完整的句子,或者内容在一个奇怪的位置戛然而止。
排查时,需要看 trace 日志中的完整响应,统计实际生成 token 数,再和 max_tokens 对比。如果实际输出长度常常顶到上限,就说明预算确实不够。
3.2 请求遗漏型垃圾:system prompt 根本没有传进去
你明明在 prompt 设计文档里写了“你是客服助手,请只输出 JSON”,模型却输出了一大段自然语言解释。此时不要急着怀疑模型理解能力,先看请求体构造代码:
# 错误示例:System Prompt 没有被放进 messages messages = [ {"role": "user", "content": "你是客服助手,请只输出JSON。查一下订单状态"} ]模型此时收到的消息只有一条 user 消息,没有任何 system 约束。它当然会按照普通用户消息的语义去理解,把“你是客服助手”当成对话内容的一部分,而不是指令。于是,它会一边解释自己“是客服助手”,一边输出 JSON 之外的废话。
这种问题,如果不读代码,你永远不会发现 system prompt 丢了。因为报错信息里不会有任何提示,模型只是安静地输出了一个“不合预期”的结果。
3.3 上下文超限型垃圾:长文档被静默截断
你的 RAG 系统从知识库中捞回了 5 段文档,拼在一起超过了模型上下文窗口。于是框架在送入模型前自动做了截断。但截断策略可能是“只保留前 4096 个 token”,导致真正有用的内容被丢弃。模型拿到的上下文缺失了关键信息,输出自然与预期不符。
这种问题很隐蔽,因为代码不会报错。唯一的线索是:输出内容与提供的上下文看似相关,但关键的论据或数据却没有被引用。排查方法是记录拼接后的实际 token 数,检查是否接近上下文窗口上限,以及截断后的上下文是否保留了核心内容。
3.4 参数失控型垃圾:高温导致长文本复读
“同一个 prompt,跑三次,三次都不太一样,有一次甚至开始复读同一句话。”这通常不是模型 bug,而是采样参数设置得太激进。尤其生成长文本时,temperature 和 top_p 没有随长度动态调整,又没有设置 repetition_penalty,模型很容易陷入重复循环。
这类问题可以通过 n-gram 重复度指标来量化。如果一段输出中连续 3 个词的组合反复出现,说明重复问题已经很严重了。缓解方案是降低 temperature,调低 top_p,设置 repetition_penalty 在 1.05 到 1.2 之间。
3.5 解析误伤型垃圾:合法输出被正则删没了
这是“代码制造垃圾输出”的经典例子。模型明明输出了结构完整的数据,但后处理代码用正则表达式做了一轮“清理”:
# 错误示例:用正则删除所有花括号内容 import re cleaned = re.sub(r'\{.*?\}', '', raw)结果是把整个 JSON 对象删成了空字符串。此时系统返回给用户的是“什么都没有”,但从模型侧来看,它输出的是完全合法的内容。这类问题只看输出会让你误以为“模型生成失败”,实际上代码是罪魁祸首。
3.6 流式拼接型垃圾:流式响应丢包
流式输出会让服务端一个 chunk 一个 chunk 地返回内容。如果客户端在拼接 chunk 时处理不当,比如没有正确处理中断、超时、乱序,最终展示给用户的内容就是残缺的。而且流式拼接错误往往在压力测试或弱网环境下才暴露,复现难度高,更容易让人误判为“模型偶尔抽风”。
4. 调试前的环境准备
要开始“从代码视角识别模型垃圾输出”,你需要准备一套可复现的调试环境。不需要复杂的工具,但建议满足下面几个条件。
- 操作系统:Windows / Linux / macOS 均可,本文示例以命令行执行为主。
- Python 版本:3.8 及以上,建议使用虚拟环境。
- Python 依赖:requests 库用于调用模型 API。
- 模型服务:一个可用的 OpenAI 兼容接口,可以是本地部署的 vLLM、TGI 等推理服务,也可以是任何提供
/chat/completions兼容接口的远端服务。 - 测试用例:固定一组业务 prompt,确保同一个问题可以反复复现。
依赖安装命令:
pip install requests准备阶段最重要的一件事是:开启完整日志。如果当前项目没有为 LLM 调用添加日志,请先补上“请求参数 + 完整响应 + 耗时 + 出错信息”的落盘记录。没有日志,就无法回放问题,后续所有排查都是盲人摸象。
版本信息方面,不建议照搬其他项目写死的版本号。本文重点是通用思路,模型名称和 API 地址请以实际项目为准。
5. 完整示例代码:构建一个输出质量检查器
下面给出三个可以组合使用的 Python 脚本。它们各自承担一个职责:记录完整调用链路、检查输出质量、按环节定位问题。
5.1 记录完整调用链路
文件路径:llm_call_with_logging.py
# 文件路径:llm_call_with_logging.py """ 目的:在调用 LLM 的每个必经环节记录关键信息, 出现“垃圾输出”时,可以快速回放完整请求和响应。 """ import json import time import requests class LLMTracer: def __init__(self, api_base, api_key, model, output_log="llm_trace.jsonl"): self.api_base = api_base self.api_key = api_key self.model = model self.output_log = output_log def chat(self, messages, params): trace = { "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "model": self.model, "messages": messages, "params": params, "response": None, "latency_s": None, "error": None, } start = time.time() try: url = f"{self.api_base}/chat/completions" headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } payload = {"model": self.model, "messages": messages, **params} resp = requests.post(url, headers=headers, json=payload, timeout=120) resp.raise_for_status() data = resp.json() trace["response"] = data trace["latency_s"] = round(time.time() - start, 3) return data["choices"][0]["message"]["content"] except Exception as e: trace["error"] = str(e) trace["latency_s"] = round(time.time() - start, 3) raise finally: self._save(trace) def _save(self, trace): with open(self.output_log, "a", encoding="utf-8") as f: f.write(json.dumps(trace, ensure_ascii=False) + "\n")关键逻辑说明:chat方法把完整的 messages、params、response、耗时、错误信息统一写入 JSONL 日志文件。这样每次调用出现“垃圾输出”时,你都有完整的请求快照,而不是靠记忆去猜当时传了什么。
5.2 输出质量规则检查
文件路径:output_quality_checker.py
# 文件路径:output_quality_checker.py """ 输出质量检查器:通过规则自动判断输出中是否存在垃圾特征。 不依赖人工肉眼观察,适合接入自动化检查和 CI 流程。 """ import json import re from collections import Counter def extract_json(raw: str): """优先提取 markdown 代码块中的 json,其次尝试全文解析""" if not isinstance(raw, str): return None pattern = r"```(?:json)?\s*(.*?)```" matches = re.findall(pattern, raw, re.DOTALL) candidates = [m.strip() for m in matches] + [raw.strip()] for c in candidates: try: return json.loads(c) except Exception: continue return None def check_json_quality(raw: str, required_fields=None): """检查 JSON 输出是否完整,缺失哪些字段""" obj = extract_json(raw) if obj is None: return {"ok": False, "reason": "json_parse_failed", "detail": raw[:200]} if required_fields: missing = [f for f in required_fields if f not in obj] if missing: return {"ok": False, "reason": "json_missing_field", "missing": missing} return {"ok": True, "data": obj} def repetition_score(text: str, n=3): """统计 n-gram 重复程度,判断是否陷入复读模式""" tokens = text.split() if len(tokens) < n + 1: return 0.0 grams = [" ".join(tokens[i:i + n]) for i in range(len(tokens) - n)] counter = Counter(grams) repeated = {g: c for g, c in counter.items() if c > 1} if not repeated: return 0.0 repeated_tokens = set() for g in repeated: repeated_tokens.update(g.split()) return round(len(repeated_tokens) / len(tokens), 3) def check_truncation(raw: str, expected_max_tokens: int): """判断输出是否接近/超过 max_tokens 限制,疑似被截断""" if not expected_max_tokens: return {"ok": True, "reason": "no_limit"} # 估算 token 数:中文约 1 字约 1 token,英文约 1 token 对应 4 字符 has_cjk = any("\u4e00" <= ch <= "\u9fff" for ch in raw) est_tokens = len(raw.replace(" ", "")) if has_cjk else len(raw) // 4 return { "ok": est_tokens < expected_max_tokens * 0.9, "est_tokens": est_tokens, "expected_max_tokens": expected_max_tokens, }这个脚本的意义在于把“垃圾特征”量化成规则。肉眼判断输出“是否太重复”是不可靠的,但 n-gram 重复度可以给出明确数值;肉眼判断输出“是否被截断”容易漏判,但估算 token 数可以和 max_tokens 做对比。
5.3 按环节定位问题的诊断流程
文件路径:diagnose_flow.py
# 文件路径:diagnose_flow.py """ 按环节定位“垃圾输出”: 输入侧 -> 采样参数 -> 模型输出 -> 后处理解析 -> 业务结果 """ import json from llm_call_with_logging import LLMTracer from output_quality_checker import ( check_json_quality, repetition_score, check_truncation, ) def main(): api_base = "http://localhost:8000/v1" # 以实际服务地址为准 api_key = "EMPTY" # 以实际鉴权方式为准 model = "your-model-name" # 以实际模型名为准 tracer = LLMTracer(api_base, api_key, model) messages = [ {"role": "system", "content": "你是教务助手,只能输出JSON,不要输出其他解释。"}, {"role": "user", "content": "帮我查询课程《数据库原理》的上课时间。"}, ] params = {"temperature": 0.7, "top_p": 0.9, "max_tokens": 512} try: raw = tracer.chat(messages, params) result = { "json_check": check_json_quality( raw, required_fields=["course", "time", "classroom"] ), "repeat_score": repetition_score(raw), "truncation_check": check_truncation(raw, expected_max_tokens=512), } print(json.dumps(result, ensure_ascii=False, indent=2)) except Exception as e: print("调用失败,已写入 trace 日志:", e) if __name__ == "__main__": main()该脚本的运行逻辑是:先调用一次模型,拿到原始输出;然后分别从 JSON 合法性、重复度、截断可能性三个维度输出检查结果。如果 JSON 检查失败,说明输出格式有问题;如果重复度偏高,说明采样参数需要调整;如果截断检查不通过,说明 max_tokens 设置过小。
注意:以上脚本只做规则检查,不做主观评价。它不会告诉你“模型输出内容是否正确”,但能快速暴露格式层面的垃圾特征,帮助定位问题发生在哪个环节。
6. 运行结果与判定方法
执行诊断脚本:
python diagnose_flow.py假设模型正常返回 JSON,但没有包含要求的全部字段,输出可能是:
{ "json_check": { "ok": false, "reason": "json_missing_field", "missing": ["classroom"] }, "repeat_score": 0.0, "truncation_check": { "ok": true, "est_tokens": 120, "expected_max_tokens": 512 } }判定方法:
json_check.ok为 false,说明输出没有被正确解析,或者缺少业务字段。先不要怀疑模型“不会输出 JSON”,打开llm_trace.jsonl看原始响应,确认模型是不是真的返回了 JSON 但缺少字段。如果原始 JSON 本身没有classroom字段,那就是 prompt 指令没有约束好字段列表;如果原始输出根本不是 JSON,再查 system prompt 是否传入。repeat_score如果大于 0.3,需要重点检查 temperature 是否过高、repetition_penalty 是否缺失。truncation_check.ok为 false,说明输出长度已经顶到 max_tokens 上限,大概率是内容被截断。此时需要调大 max_tokens,或者优化 prompt 让回答更精炼。
真正需要重视的是:即使输出检查全绿,也不能说明模型输出“好”。规则检查器只能排除明显的格式垃圾。更复杂的语义垃圾,需要结合业务规则和评测集来判断。
如果你在本地已经部署了服务,也可以直接测试不同的 prompt 和参数组合。推荐做法是每次只改一个变量,然后对比检查结果。这样能快速找到影响输出质量的关键变量。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 输出 JSON 解析失败 | 输出被截断或 system prompt 丢失 | 查看 trace 日志中的原始响应 | 调大 max_tokens,确认 system prompt 在 messages 中 |
| 输出内容与上下文无关 | 长文本被静默截断 | 检查拼接后文本 token 数 | 优化分段策略,控制上下文长度 |
| 模型输出不断重复同一句 | temperature 过高,缺少重复惩罚 | 计算 repetition_score | 降低 temperature,设置 repetition_penalty |
| 中文乱码 | 编码不一致 | 检查 API 响应编码设置 | 统一 UTF-8 编码 |
| 合法 JSON 被业务侧误删 | 后处理正则或解析代码有误 | 对比原始输出和后处理结果 | 修复正则逻辑,增加字段级校验 |
| 输出到一半突然结束 | max_tokens 设置太小 | 估算实际 token 数 | 调大 max_tokens |
| 流式输出内容残缺 | chunk 拼接逻辑有误 | 打印每个 chunk 的原始内容 | 修复流式拼接代码,确保按序追加 |
下面挑几个高频问题展开说明。
第一,输出 JSON 解析失败。这个问题最常见的原因是 max_tokens 不足。模型在生成 JSON 时,如果内容还没写完就撞到了 token 上限,返回的字符串就会是一个残缺的 JSON,自然无法解析。排查顺序是:先打开 trace 日志,查看原始响应末尾是否有明显截断痕迹;再算一下估算 token 数,和 max_tokens 做对比。
第二,输出内容与上下文无关。这个问题往往不在模型侧,而在输入侧。你可能在代码里拼接了大量文档,但模型上下文窗口有限,框架自动做了截断。如果截断策略把关键部分丢弃了,模型就会“一本正经地胡说八道”。排查时需要在代码里打印拼接后的文本长度,确认实际送入模型的上下文到底是什么内容。
第三,模型反复输出同一句话。这种“复读机”现象在长文本生成中特别常见。原因通常是 temperature 偏高、top_p 偏大,且没有 repetition_penalty。建议先把 temperature 降到 0.7 以下,再为长文本生成设置 repetition_penalty。
8. 最佳实践与工程建议
8.1 让每次调用都可回放
这是识别模型垃圾输出的第一工程要求。所有 LLM 调用都需要记录完整参数,包括模型名、完整 messages、采样参数、原始响应、耗时、错误信息。日志按行追加到 JSONL 文件即可,不需要引入复杂系统。有了回放能力,任何“垃圾输出”都可以被还原成一次可调试的请求。
8.2 用规则断言代替肉眼检查
在开发阶段,人眼可以判断输出质量;但进入测试和灰度阶段,必须用规则自动检查。上面给出的 output_quality_checker 就是一个起点。你还可以结合业务,增加字段类型校验、枚举值校验、长度校验。
8.3 采样参数要分级管理
不要在全项目共用一套采样参数。建议为不同任务配置不同的参数模板:
params = { "short_extract": {"temperature": 0.2, "top_p": 0.8, "max_tokens": 256}, "creative_generation": {"temperature": 0.8, "top_p": 0.95, "max_tokens": 2048}, "structured_output": {"temperature": 0.1, "top_p": 0.7, "max_tokens": 1024}, }结构化输出任务要尽量降低随机性;创意生成任务可以适当放宽;长文本任务必须设置重复惩罚。
8.4 把输出质量检查纳入 CI
如果你的项目里已经有自动化测试,完全可以把规则检查器接进 CI。每次 prompt 模板或代码变更,自动跑一组固定用例,检查输出是否满足 JSON 合法、字段完整、重复度达标、无明显截断等条件。一旦某个 commit 让输出质量下降,CI 就能及时拦下,而不是等上线后用户来反馈。
8.5 归因纪律:先看代码后换模型
遇到“垃圾输出”,不要第一时间去换更大的模型。更稳妥的顺序是:先回放 trace 日志,确认输入侧和参数侧没有问题;再用规则检查器确认输出侧没有格式问题;最后才考虑模型能力不足的可能性。因为换模型成本很高,但很多时候问题并不在模型。
9. 总结
“不读代码就无法识别模型垃圾输出”,这句话的真正含义是:模型输出质量是一个链路问题,而不是一个单点问题。Prompt 模板、上下文截断、采样参数、解码参数、后处理解析、业务校验,每一个环节都可能生产“垃圾”。如果不读代码,你只会看到一个孤零零的糟糕结果,然后用“模型不行”给自己一个草率结论。
从实践路径来看,第一步永远是记录完整调用日志,第二步是用规则检查器量化垃圾特征,第三步才是判断模型本身是否真的有问题。当你掌握了这条排查路径,你会发现自己对模型输出的判断力会有明显提升——不再靠感觉,而是靠证据。
建议把这套脚本和排查思路直接放进你的项目里。下一次再遇到“模型输出垃圾”的时候,先打开日志,读一遍代码,再决定要不要怪模型。大概率你会发现,问题比你想的更接近代码,也更容易修复。