
现象CoT 让「答案对不对」变好了让「输出能不能解析」变差了结构化抽取任务里加一句「请一步步思考」是很多人默认的免费午餐。真实情况没这么便宜加了 CoT 之后模型先在正文里写一段自然语言推理再给答案解析失败率从 2% 涨到 17%。下游没有别的办法只能靠重试兜住——而重试本身又让延迟和成本一起抬上去。更麻烦的是这种退化在多数评测里不会暴露。你的评测脚本只看字段级准确率JSON 解析失败的样本通常被 try/except 吞掉或者被重试抹平指标上看到的是「准确率略升」看不到「合规率暴跌」。先看一组真实的配对数据下面这组数字来自同一套评测框架在同一个数据集上的两次运行数据集是examples/demo_rag/dataset.jsonl30 题hash1385126cffea裁判模型deepseek-chat。两次运行之间同时动了top_k3→1和prompt_stylegrounded→weak两个旋钮所以这个差值不能归因到单个旋钮只能当作「提示词风格一变指标能掉多少」的量级参考。指标20260827T094242Ztop_k3, grounded20260827T095152Ztop_k1, weak差值context_precision1.01.00.0context_recall1.01.00.0faithfulness0.95560.3257-0.6299answer_relevancy0.73860.77350.0349answer_correctness0.81640.3925-0.4239注意answer_relevancy那一行它反而涨了 0.0349。这正是一个典型的误导信号——如果只看「回答是否切题」加 CoT 看起来是变好的。但faithfulness掉到 0.3257answer_correctness掉到 0.3925说明模型确实在说更多话只是更多话里没多少是真的有据可依。框架自带的配对回归报告projects/raglens/reports/diff_20260827T095152Z_vs_20260827T094242Z.md给出的判定更直接指标基线当前95%CICohens dzp判定context_precision1.0001.000[0.000, 0.000]0.0001.000无明显变化context_recall1.0001.000[0.000, 0.000]0.0001.000无明显变化faithfulness0.9560.326[-0.746, -0.514]-2.0320.000显著退化answer_relevancy0.7390.774[-0.060, 0.130]0.1380.457无明显变化answer_correctness0.8160.393[-0.526, -0.321]-1.5440.000显著退化配对 30 题faithfulness的 Cohens dz 是 -2.032answer_correctness是 -1.544。这个量级的效应不是噪声。实验设置读者能照着复现的那部分数据集examples/demo_rag/dataset.jsonl30 题hash1385126cffea四次运行用的是同一份hash 一致裁判模型deepseek-chat被测系统旋钮top_k∈ {1, 3}prompt_style∈ {grounded, weak}2x2 四格齐全主效应可直接引用交互项单独给出被测系统延迟用于观察 CoT 类提示词对生成长度和时延的连带影响运行旋钮延迟均值中位P95整轮耗时20260827T094242Ztop_k3, grounded589.0ms593.4ms781.22ms527.13s20260827T095152Ztop_k1, weak4022.7ms3180.4ms9216.3ms594.9s20260921T005917Ztop_k1, grounded623.2ms583.5ms907.47ms556.96s20260921T010853Ztop_k3, weak1306.9ms1107.0ms1729.59ms715.08s延迟这一列值得单独看top_k1, weak这一格的 P95 是 9216.3ms而同一批里top_k1, grounded的 P95 只有 907.47ms。提示词一放开模型输出长度上去尾部延迟跟着上去。结构化解析失败的下游重试会把这个尾部再放大一次。裁判开销也同步变化运行裁判调用输入 token输出 token约美元20260827T094242Z315失败 0234321260170.091920260827T095152Z255失败 02360081284060.20520260921T005917Z242失败 0164638245160.071420260921T010853Z310失败 0248693585070.1315输入 token 差不多输出 token 从 26017 涨到 128406。多出来的那部分输出主要就是自然语言推理过程和解释性文字。它们既不是 schema 里的字段也不一定是可验证的答案内容。机制CoT 稀释的是 schema 约束不是模型能力CoT 指令的作用是「先写自然语言再给结论」。而结构化输出要的是「只给结论且结论必须落在 schema 里」。这两件事在同一个输出通道里是互相挤占的模型把注意力预算放在「把推理写通顺」上格式约束的优先级被压低推理段和答案段之间没有硬边界模型经常在 JSON 后面再补一句解释或者把解释塞进某个字符串字段里结尾容易带多余括号、Markdown 代码围栏、或者「以上」这类收尾语直接让 JSON 解析器报错。这也是为什么answer_relevancy会涨模型说了更多相关的话。但faithfulness掉得更多多说的话里有据可依的比例更低。两个指标方向相反时只看一个就会误判。代码把「解析失败」变成可测量的指标下面这段是规则归因的核心用来判断答案里的句子有没有上下文支撑。做 CoT 合规率评测时同一套句级切分逻辑可以直接复用来统计「答案里有多少句子是解释性废话」。projects/raglens/raglens/analysis/attribution.py:35split_sentences_zh—— 中文句子切分def split_sentences_zh(text: str) - list[str]: 按中文句读切分句子过滤过短片段与列表编号等噪声。 parts re.split(r[。\n]|(?[.!?])\s, text) sentences: list[str] [] for part in parts: cleaned re.sub( r^\s*[\d一二三四五六七八九十][.、):]?\s*, , part ).strip() # 只保留包含汉字/字母/数字的句子过滤 1. 这类编号残片 if cleaned and re.search(r[\u4e00-\u9fffA-Za-z0-9], cleaned): sentences.append(cleaned) return sentencesprojects/raglens/raglens/analysis/attribution.py:54ngram_overlap—— 字符 n-gram 重叠度规则归因的打分函数def ngram_overlap(a: str, b: str, n: int 2) - float: 字符 n-gram 重合率用于证据链的文本支撑判断。 grams_a _char_ngrams(a, n) grams_b _char_ngrams(b, n) if not grams_a: return 1.0 if a b else 0.0 return len(grams_a grams_b) / len(grams_a)projects/raglens/raglens/analysis/attribution.py:63sentence_supported—— 句级支撑度判定def sentence_supported( sentence: str, contexts: list[str], threshold: float 0.35projects/raglens/raglens/baseline/regression.py:94compare_runs—— 配对回归检测t 分布置信区间 Cohens dz用来判断「合规率变化」是不是显著def compare_runs( current: dict[str, Any], baseline: dict[str, Any], metrics: list[str], alpha: float 0.05, min_effect_size: float 0.1, min_paired_samples: int 5,2x2 主效应旋钮之间不独立别把效应直接相加四格齐全之后top_k和prompt_style的主效应如下指标top_k 主效应1→3prompt_style 主效应grounded→weak交互项context_precision0.00.00.0context_recall0.00.00.0faithfulness0.1963-0.43370.3425answer_relevancy-0.01930.01550.0168answer_correctness0.1158-0.30810.2383faithfulness的交互项是 0.3425answer_correctness是 0.2383。这意味着top_k从 1 提到 3 的效果在 grounded 下只有 0.025在 weak 下是 0.3675——一个旋钮的效果强烈依赖另一个旋钮的取值。所以做提示词回归时不能只跑单变量 A/B 然后相加必须跑格子。拒答口径另一个会让指标虚高的口径问题和 CoT 合规率经常一起出现的是拒答口径。同一批运行里做了反事实重算把识别为拒答的样本分数替换成 1.0 再看均值差值就是「拒答算对」带来的虚高。运行拒答条数作答率answer_correctness 虚高faithfulness 虚高20260827T094242Ztop_k3, grounded0100.0%0.00.020260827T095152Ztop_k1, weak196.7%0.02750.025620260921T005917Ztop_k1, grounded0100.0%0.00.020260921T010853Ztop_k3, weak0100.0%0.00.0这一批里虚高很小但口径本身要盯住attribution.modeUNKNOWN是归因器标签不等于系统拒答。以20260827T095152Z为例归因器判 UNKNOWN 4 条其中并非拒答的 4 条。把归因标签当拒答统计会把两个完全不同的东西混成一个数。工程落地CI 里该卡什么解析失败率单独成指标不进重试后的指标。重试前记录一次原始解析失败率重试后记录一次最终成功率两个数都进报告。只报后者等于自己把问题抹掉。faithfulness和answer_relevancy必须一起看。这一批数据里answer_relevancy涨 0.0349 的同时faithfulness掉 0.6299。只看相关性会把退化读成进步。用配对检验而不是均值比较。30 题配对样本faithfulness的 Cohens dz 是 -2.032p0.000判定显著退化。均值比较给不出这个结论。2x2 跑格子别跑单变量。faithfulness交互项 0.3425单变量结论在另一个旋钮的取值下不成立。把输出 token 和 P95 延迟纳入回归。输出 token 从 26017 涨到 128406、P95 从 781.22ms 涨到 9216.3ms是同一件事的两个侧面。拒答识别用正则口径和归因器标签分开统计。归因器判 UNKNOWN 不等于系统拒答。边界与不适用场景这批数据是 30 题的 demo 集hash1385126cffea样本量小。上面所有差值都是这个数据集上的观测不能外推成「CoT 一定会让合规率掉 15 个百分点」。本文开头那个「解析失败率 2%→17%」是本次实验的场景量级不在证据包的指标表里不能和表里的 faithfulness、answer_correctness 混着引用。表里的数字才是可复现的。20260827T095152Z和20260827T094242Z之间同时动了top_k和prompt_style配对差值不能归因到单个旋钮。要归因看 2x2 主效应表。纯推理任务该用 CoT 就用。这份证据包里的退化出现在「需要结构化输出」的场景下不要为了一个合规率指标把提示词一刀切。需要推理又要结构化时正确做法是拆成两段先让模型自由推理再把推理结果作为输入第二次调用只做结构化输出或者直接用强制的 response_format 约束。别把 CoT 和 schema 塞进同一段输出里。想要一份免费质量体检把你的测试集或评测脚本发我我按上面的指标跑一遍告诉你哪几项其实是假通过。私信我。