“模型输出垃圾”的真相:从代码链路定位LLM垃圾输出
很多人第一次做 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. 输出序列解码解码参数、停止词生成不终止、截断、含特殊 token6. 后处理与解析正则、字符串处理、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_logllm_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: fBearer {self.api_key}, Content-Type: application/json, } payload {model: self.model, messages: messages, **params} resp requests.post(url, headersheaders, jsonpayload, timeout120) 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, encodingutf-8) as f: f.write(json.dumps(trace, ensure_asciiFalse) \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_fieldsNone): 检查 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, n3): 统计 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_tokens512), } print(json.dumps(result, ensure_asciiFalse, indent2)) 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 模板、上下文截断、采样参数、解码参数、后处理解析、业务校验每一个环节都可能生产“垃圾”。如果不读代码你只会看到一个孤零零的糟糕结果然后用“模型不行”给自己一个草率结论。从实践路径来看第一步永远是记录完整调用日志第二步是用规则检查器量化垃圾特征第三步才是判断模型本身是否真的有问题。当你掌握了这条排查路径你会发现自己对模型输出的判断力会有明显提升——不再靠感觉而是靠证据。建议把这套脚本和排查思路直接放进你的项目里。下一次再遇到“模型输出垃圾”的时候先打开日志读一遍代码再决定要不要怪模型。大概率你会发现问题比你想的更接近代码也更容易修复。