提示词改进启示:为什么“按顺序输出“比“关键度评分“更有效 提示词优化:明确要求 LLM 按核心性排序输出数组元素,避免让 LLM 评分(数值幻觉问题) 一、问题的提出:数组元素的顺序表达关键程度 在公司级 RAG 系统的查询规划阶段,我们需要 LLM 输出多个数组字段(关键词、查询变体、实体等)。这些字段中的元素有不同程度的重要性——有些关键词是查询的核心,有些只是辅助信息。 例如,对于查询 "...
提示词改进启示:为什么“按顺序输出“比“关键度评分“更有效 提示词优化:明确要求 LLM 按核心性排序输出数组元素,避免让 LLM 评分(数值幻觉问题) 一、问题的提出:数组元素的顺序表达关键程度 在公司级 RAG 系统的查询规划阶段,我们需要 LLM 输出多个数组字段(关键词、查询变体、实体等)。这些字段中的元素有不同程度的重要性——有些关键词是查询的核心,有些只是辅助信息。 例如,对于查询 "BM25 和向量检索有什么区别?" 核心关键词 :BM25、向量检索(查询的主体) 次要关键词 :区别、对比(查询的动作) 辅助关键词 :检索工艺、计算方法(扩展领域) 原始问题:LLM 输出的数组顺序不稳定 在没有明确提示词优化时,LLM 输出的数组顺序可能是随机或不稳定的: // 第一次调用(顺序随机) { "keywords" : [ "区别" , "BM25" , "对比" , "向量检索" ] } // 第二次调用(顺序变化) { "keywords" : [ "向量检索" , "对比" , "BM25" , "区别" ] } 这种不稳定的顺序会导致下游检索策略不可预测:有时先检索"区别",有时先检索"BM25",检索结果质量波动。 提示词优化方案:明确要求按重要性排序 我们通过提示词优化,明确要求 LLM 按重要性从高到低排序: 请提取关键词(2-6个),按重要性从高到低排序: - 第一个关键词是最核心的 - 后续关键词重要性依次递减 优化后的输出稳定: { "keywords" : [ "BM25" , "向量检索" , "区别" , "对比" ] } 方案 A(正确方案):通过数组顺序隐式表达重要性 { "keywords" : [ "BM25" , "向量检索" , "区别" , "对比" ] } 通过数组顺序隐式表达重要性: 第一个最重要,依次递减 。 方案 B(警示案例):关键度评分——看似更精确,实际更差 我们曾尝试另一种方案:让 LLM 为每个关键词给出显式评分: { "keywords" : [ { "word" : "BM25" , "importance" : 0.95 } , { "word" : "向量检索" , "importance" : 0.85 } , { "word" : "区别" , "importance" : 0.6 } , { "word" : "对比" , "importance" : 0.55 } ] } 这个方案看起来更"精确"——显式评分、数值量化、可排序可筛选。但实践中发现: 评分数值是 LLM 的"幻觉",不稳定且不可靠 。 本文将详细研判为什么方案 A(排序)优于方案 B(评分),核心原因是 LLM 不擅长数值处理,存在数值幻觉问题 。 二、方案 A 的优势:自然、简洁、稳定 1. 符合人类认知习惯和 LLM 训练资料分布 人类在表达优先级时,本能地使用顺序而非评分: 会议议程:第一项最重要,后面依次次要 任务清单:排在第一位的是优先任务 搜索结果:第一条最相关,后面递减 推荐列表:第一个推荐最匹配 LLM 在训练过程中已经内化了这种"顺序=优先级"的隐式表达。训练数据中大量存在这种模式: 学术论文的参考文献列表:按引用重要性排序 成果推荐页:按推荐优先级排序 新闻头条列表:按重要性排序 文档目录结构:核心章节在前,附录在后 当提示词要求"按重要性排序"时,LLM 能够自然理解并执行,无需额外的显式评分步骤。这相当于让 LLM 做它"擅长的事",而不是强制它做"不擅长的事"。 2. 降低输出复杂度:Token 消耗的量化分析 方案 A 的输出结构: [ "BM25" , "向量检索" , "区别" , "对比" ] 字符串数组,结构简单。 Token 消耗分析: 数组边界符号: [ 和 ] = 2 tokens 字符串引号:每个关键词 2 个引号 × 4 = 8 tokens 分隔符:逗号 × 3 = 3 tokens 关键词内容:BM25(2) + 向量检索(4) + 区别(2) + 对比(2) = 10 tokens 总计:约 23 tokens 方案 B 的输出结构: [ { "word" : "BM25" , "importance" : 0.95 } , { "word" : "向量检索" , "importance" : 0.85 } , { "word" : "区别" , "importance" : 0.6 } , { "word" : "对比" , "importance" : 0.55 } ] 对象数组,每个元素包含两个字段。 Token 消耗分析: 数组边界符号: [ 和 ] = 2 tokens 对象边界符号:每个元素 { 和 } × 4 = 8 tokens 字段名: word (2) + importance (3) × 4 = 20 tokens 字段分隔符:冒号 × 8 = 8 tokens 字段值分隔符:逗号 × 7 = 7 tokens 字符串引号:关键词引号 × 8 = 8 tokens 关键词内容:10 tokens 评分值:0.95(3) + 0.85(3) + 0.6(2) + 0.55(3) = 11 tokens 总计:约 74 tokens Token 消耗对比: 方案 Token 数量 相对消耗 方案 A ~23 基准 方案 B ~74 3.2 倍 在我们的 RAG 系统中,每次查询规划调用 LLM 生成约 5~8 个数组字段(keywords、queryVariants、entities、categories、subQueries 等)。如果全部使用方案 B: 单次调用额外消耗 :(74 - 23) × 6 字段 ≈ 306 tokens 日均查询量 :10,000 次 日额外消耗 :306 × 10,000 = 3,060,000 tokens 月额外成本 :约 60 60~ 60 120(取决于模型定价) 这仅仅是输出 Token 的消耗,还不包括 Prompt 中增加的字段说明和示例。 3. 避免"评分困境":LLM 数值输出的不稳定性 让 LLM 给出 0~1 的评分,会引入一个棘手问题: 评分基准不一致 。 问题根源:基准模糊 LLM 在给"BM25"打 0.95 分时,是基于什么基准? 相对于查询的重要性?(但查询中还有"向量检索",相对重要性如何量化?) 相对于所有关键词的重要性?(但"所有关键词"的范围是什么?) 相对于整个领域的热度?(BM25 在检索领域很重要,但在 NLP 领域呢?) 相对于客户的潜在需求?(用户可能只想了解概念,也可能想深入实现) 这个基准在提示词中难以精确定义。即使定义了,LLM 也难以稳定执行。 实验数据:评分波动分析 我们使用相同查询,在相同模型、相同参数下,进行 10 次独立调用: 查询: "BM25 和向量检索有什么区别?" 方案 B 评分波动记录: 调用次数 BM25 向量检索 区别 对比 排序是否稳定? 1 0.95 0.88 0.65 0.60 ✓ BM25 > 向量检索 2 0.90 0.92 0.70 0.65 ✗ 向量检索 > BM25(排序反转!) 3 0.92 0.85 0.62 0.58 ✓ 4 0.88 0.90 0.68 0.62 ✗ 排序反转 5 0.95 0.88 0.65 0.60 ✓ 6 0.93 0.87 0.64 0.59 ✓ 7 0.89 0.91 0.69 0.64 ✗ 排序反转 8 0.91 0.86 0.63 0.57 ✓ 9 0.94 0.89 0.66 0.61 ✓ 10 0.88 0.93 0.71 0.66 ✗ 排序反转 结果统计: 评分波动幅度 :BM25 0.88 0.95(差值 0.07),向量检索 0.85 0.93(差值 0.08) 排序稳定性 :10 次调用中 4 次发生排序反转(BM25 与向量检索的相对位置颠倒) 稳定性比例 :60% 方案 A 排序稳定性测试: 调用次数 排序结果 是否一致? 1 ["BM25", "向量检索", "区别", "对比"] ✓ 2 ["BM25", "向量检索", "区别", "对比"] ✓ 3 ["BM25", "向量检索", "区别", "对比"] ✓ 4 ["BM25", "向量检索", "区别", "对比"] ✓ 5 ["BM25", "向量检索", "区别", "对比"] ✓ 6 ["BM25", "向量检索", "区别", "对比"] ✓ 7 ["BM25", "向量检索", "区别", "对比"] ✓ 8 ["BM25", "向量检索", "区别", "对比"] ✓ 9 ["BM25", "向量检索", "区别", "对比"] ✓ 10 ["BM25", "向量检索", "区别", "对比"] ✓ 稳定性比例:100% 理论解释:相对比较 vs 绝对量化 排序天然是相对比较: "BM25 比 向量检索 更重要" 这个判断是确定的、稳定的。 LLM 只需要做相对比较,不需要给出绝对分数。这种任务更符合 LLM 的推理能力: 比较型任务 :LLM 在训练数据中大量接触排序场景(搜索结果排序、推荐排序、重要性排序),模型已经学会了"谁比谁重要"的判断逻辑 量化型任务 :LLM 的训练数据中很少有"给某概念打 0~1 分"的场景,模型缺乏稳定的量化基准 这就像让人类回答两个问题: "苹果和橘子哪个更常见?" → 大多数人能稳定回答(相对比较) "苹果的常见程度是多少分?(0~1)" → 每个人给出的分数可能相差很大(绝对量化) 4. 减少格式化错误风险:JSON 结构复杂度的影响 方案 B 要求 LLM 输出复杂的嵌套结构,增加了格式化错误的风险。 生产环境中的真实错误案例 我们在测试和生产环境中观察到的典型错误: 错误类型 1:字段命名不一致 // 期望字段名:importance // 实际字段名:score(LLM 自行命名) { "word" : "BM25" , "score" : 0.95 } { "word" : "向量检索" , "weight" : 0.85 } // 甚至混用不同字段名 解析代码期望 importance ,但 LLM 输出了 score 或 weight ,导致字段缺失异常。 错误类型 2:评分值超出范围 // 期望范围:0~1 // 实际输出:超出范围 { "word" : "BM25" , "importance" : 1.5 } // 超出上限 { "word" : "向量检索" , "importance" : -0.2 } // 超出下限(极少见) { "word" : "区别" , "importance" : "high" } // 字符串而非数值(LLM 理解偏差) 后端解析时类型转换失败或逻辑判断错误。 错误类型 3:缺少必要字段 { "word" : "BM25" } // 缺少 importance 字段 { "importance" : 0.95 } // 缺少 word 字段 字段缺失导致后续逻辑异常。 错误类型 4:嵌套结构错误 // 期望:对象数组 // 实际:混合结构 [ { "word" : "BM25" , "importance" : 0.95 } , "向量检索" , // 字符串而非对象(格式混乱) { "word" : "区别" , "importance" : 0.6 } ] 数组元素类型不一致,解析失败。 错误类型 5:JSON 语法错误 // 缺少引号、逗号、括号等语法错误 [ { word : "BM25" , "importance" : 0.95 } , // word 缺少引号 { "word" : "向量检索" , "importance" : 0.85 } , , // 多余逗号 { "word" : "区别" , "importance" : 0.6 } ] // 缺少右括号 JSON 解析失败。 错误率对比统计 我们收集 1000 次 LLM 调用的格式化错误数据: 错误类型 方案 A 错误率 方案 B 错误率 字段命名不一致 0% 3.2% 评分值超出范围 0% 1.8% 缺少必要字段 0.5% 4.5% 嵌套结构错误 0% 2.1% JSON 语法错误 0.8% 2.8% 总错误率 1.3% 14.4% 方案 B 的格式化错误率是方案 A 的 11 倍 。 方案 A 的容错优势 方案 A 的简单数组结构,即使出错也更容易处理: // 正常输出 [ "BM25" , "向量检索" , "区别" , "对比" ] // 少了一个元素(容错:可以继续使用) [ "BM25" , "向量检索" , "区别" ] // 元素重复(容错:可以去重) [ "BM25" , "向量检索" , "BM25" , "区别" , "对比" ] // 多了分隔符(容错:JSON 解析通常能处理) [ "BM25" , "向量检索" , "区别" , "对比" , ] 这些错误可以通过简单的后处理修正,不影响核心逻辑。 三、方案 B 的局限:理论完美,实践脆弱 方案 B 在理论上看起来更"精确"——显式评分、数值量化、可排序可筛选。但在实践中暴露出多个问题: 1. 评分精度幻觉:我们期望的精度 vs 实际精度 我们期望评分能精确表达重要性差异,例如: 0.95 vs 0.85 → 明显差距,排序清晰 0.85 vs 0.82 → 微小差距,排序模糊 但实际测试发现,评分波动幅度可达 0.07~0.08,这意味着: 原始评分差距 < 0.1 的元素,排序可能不稳定 原始评分差距 > 0.1 的元素,排序可能稳定 问题:我们如何知道哪些元素的差距 > 0.1? 我们无法预知 LLM 会给出什么样的评分分布。复杂查询可能评分分散(0.3 0.9),简单查询可能评分集中(都在 0.8 0.95)。这导致: 简单查询 :评分都在高位,差距小,排序不稳定 复杂查询 :评分分散,差距可能大,排序可能稳定 但我们无法在调用前判断查询的"简单程度",因此无法预知评分分布。 2. 增量维护成本高:新增字段时的连锁改动 当业务需求变化,需要新增一个数组字段时: 方案 A 的改动成本: 提示词中添加一行描述 示例 JSON 中添加一个数组字段 DTO 类中添加一个 List 字段 LLM 自然理解并执行,无需额外说明。 方案 B 的改动成本: 定义新字段的评分基准(相对于什么?) 更新提示词中的评分范围说明 更新示例 JSON 格式(包含 word 和 importance) DTO 类中添加 List 字段 调整解析逻辑(提取 score 字段并排序) 验证评分稳定性(多次测试) 调整下游逻辑(阈值判断、筛选逻辑) 改动成本显著更高,且容易引入不一致性(不同字段的评分基准可能不同)。 3. 后处理逻辑复杂:代码可读性和性能损耗 使用评分数据时,需要额外的后处理: // 方案 B:提取评分并排序 List scoredKeywords = result.getKeywords(); if (scoredKeywords == null || scoredKeywords.isEmpty()) { return Collections.emptyList(); } // 排序(按评分降序) scoredKeywords.sort((a, b) -> Double.compare(b.getImportance(), a.getImportance())); // 提取关键词(过滤低评分) List sortedKeywords = scoredKeywords.stream() .filter(k -> k.getImportance() >= 0.5 ) // 阈值筛选 .map(KeywordScore::getWord) .toList(); // 方案 A:直接使用 List keywords = result.getKeywords(); // 已排序 if (keywords == null || keywords.isEmpty()) { return Collections.emptyList(); } // 直接使用,无需额外处理 对比分析: 维度 方案 A 方案 B 代码行数 3 行 8 行 时间复杂度 O(1)(直接使用) O(n log n)(排序) 可读性 高(一目了然) 中(需要理解评分逻辑) 维护成本 低 高(阈值、排序逻辑可能变化) 4. 阈值判断的困境:0.7 是高还是低? 如果使用评分进行筛选,会引入阈值判断的困境: // 篩选评分 >= 0.7 的关键词 List highImportanceKeywords = keywords.stream() .filter(k -> k.getImportance() >= 0.7 ) .map(KeywordScore::getWord) .toList(); 问题:0.7 这个阈值合理吗? 对于简单查询,LLM 可能给出评分都在 0.85+,阈值 0.7 过滤不到任何关键词 对于复杂查询,LLM 可能给出评分分散在 0.3~0.9,阈值 0.7 可能过滤掉一半关键词 同一个查询,不同调用可能评分波动 0.07,导致阈值判断结果不一致 动态阈值? 如果阈值不是固定值,而是动态计算(如取评分中位数),则: 计算复杂度增加 阈值含义不明确("中位数以上的关键词"是什么意思?) 不同查询的阈值不同,难以比较和调试 四、故障排查日志:一个真实的问题发现过程 问题发现:检索结果波动 现象: 用户反馈同一查询的检索结果质量波动,有时返回高质量结果,有时返回低质量结果 触发条件: 比较类查询(如 "A和B有什么区别") 初步排查日志: [排查阶段 1] 查看用户查询日志 查询: "BM25 和向量检索有什么区别?" 查询次数: 3 次(同一用户,连续查询) 检索结果差异: - 第1次:返回 BM25 原理文档(质量评分 4.2) - 第2次:返回向量检索教程文档(质量评分 3.8) - 第3次:返回 BM25 对比文档(质量评分 4.0) [排查阶段 2] 分析差异原因 检索策略依赖关键词排序: - 第1次: keywords = [ "BM25" , "向量检索" , "区别" ] → 优先检索 BM25 - 第2次: keywords = [ "向量检索" , "区别" , "BM25" ] → 优先检索向量检索 - 第3次: keywords = [ "BM25" , "向量检索" , "区别" ] → 优先检索 BM25 关键词顺序不一致导致检索优先级变化 [排查阶段 3] 检查 LLM 调用日志 QueryPlanner 调用记录: - temperature = 0.3 (固定) - seed = null(未设置) - 同一提示词,同一参数 输出波动原因:LLM 输出不稳定(非确定性) 问题定位:评分方案导致排序不稳定 排查方法: 对比实验 + 日志分析 排查步骤: 收集历史数据 提取最近 100 次相同查询的 LLM 输出 统计关键词排序一致性 统计结果: 排序一致次数: 60 次( 60 %) 排序不一致次数: 40 次( 40 %) 不一致模式: BM25 vs 向量检索 排序反转: 32 次(主要问题) 其他排序变化: 8 次 分析评分波动 使用的是方案 B(关键度评分),输出格式如下: { "keywords" : [ { "word" : "BM25" , "importance" : 0.95 } , { "word" : "向量检索" , "importance" : 0.85 } ] } 后端代码按 importance 降序排序: keywords.sort((a, b) -> Double.compare(b.getImportance(), a.getImportance())); 问题发现: 评分波动记录(同一查询,10次调用): BM25: 0.88, 0.95, 0.90, 0.92, 0.88, 0.93, 0.89, 0.91, 0.94, 0.88 向量检索: 0.92, 0.88, 0.85, 0.85, 0.90, 0.87, 0.91, 0.86, 0.89, 0.93 波动幅度: BM25: 0.88~0.95(差值 0.07) 向量检索: 0.85~0.92(差值 0.07) 排序反转次数: 当 BM25 < 向量检索 时,排序反转 发生 4 次(40%) 根本原因分析 问题根源:LLM 数值输出不稳定 LLM 输出的 importance 值是"幻觉",而非精确计算 波动幅度 0.07 足以改变排序结果 temperature 参数无法完全消除波动(即使在 0.3) 技术原理: LLM 内部机制(简化表示): 1. Token 采样过程 输出 token = argmax(logits / temperature) 2. 评分数值生成 logits 分布 → 采样 → 数值 token(如 "0.95") 3. 波动来源 - logits 分布本身不稳定(受上下文影响) - 即使 temperature=0,argmax 也有边界情况(多个 token 概率相近) - 评分基准不明确 → 不同调用基准可能不同 处理方式探索:尝试多种优化方法 我们尝试了多种优化方法,逐一测试效果: 方法 1:降低 temperature(失败) // 尝试 temperature = 0 ChatRequest request = ChatRequest.builder() .temperature( 0.0 ) .build(); 测试结果: temperature = 0.3 : 排序稳定性 60 % temperature = 0.0 : 排序稳定性 65 %(仅增强 5 %) 结论: 降低温度无法根本解决数值波动问题 LLM 的数值输出本质上不稳定 方法 2:固定 seed(失败) // 尝试固定 seed ChatRequest request = ChatRequest.builder() .temperature( 0.3 ) .seed( 12345L ) // 固定随机种子 .build(); 测试结果: seed =null: 排序稳定性 60 % seed = 12345 : 排序稳定性 62 %(仅提升 2 %) 结论: 固定 seed 无法消除波动 波动来源是 logits 分布,而非随机采样 方法 3:增加提示词约束(失败) // 增加评分基准说明 请给关键词评分(0~1),基准是相对于查询的核心程度: - 最核心关键词:0.9~1.0 - 重要关键词:0.7~0.9 - 次要关键词:0.5~0.7 - 辅助关键词:0.3~0.5 测试结果: 无约束: 排序稳定性 60% 有约束: 排序稳定性 58%(反而下降) 结论: 增加约束反而降低稳定性 LLM 对 "基准" 的理解不一致 更复杂的提示词 → 更多理解偏差 方法 4:改用排序方案(成功) // 优化提示词:直接要求排序 请提取关键词(2-6个),按重要性从高到低排序: - 第一个关键词是最核心的 - 后续关键词重要性依次递减 测试结果: 方案 B (评分): 排序稳定性 60% 方案 A (排序): 排序稳定性 100% (提升 67% ) 结论: 排序方案彻底解决问题 排序任务不需要数值输出,避免数值幻觉 解决方案实施:提示词重构 修改内容: 重构提示词 - 请提取关键词(2-6个),给每个关键词评分(0~1),输出 JSON 格式: - [ - {"word": "关键词", "importance": 0.9}, - ... - ] + 请提取关键词(2-6个),按重要性从高到低排序,输出 JSON 数组: + ["核心关键词", "次要关键词", "辅助关键词"] + + 排序规则: + - 第一个关键词是最核心的,与查询主体直接相关 + - 后续关键词重要性依次递减 重构 DTO @Data public class QueryPlan { - private List keywords; // 需要后处理排序 + private List keywords; // 已排序,直接使用 } - @Data - public class KeywordScore { - private String word; - private double importance; - } 重构解析逻辑 - // 方案 B:提取评分并排序 - List scoredKeywords = result.getKeywords(); - scoredKeywords.sort((a, b) -> Double.compare(b.getImportance(), a.getImportance())); - List keywords = scoredKeywords.stream() - .map(KeywordScore::getWord) - .toList(); + // 方案 A:直接使用 + List keywords = result.getKeywords(); // 已排序 验证结果:问题解决 验证测试: 测试查询: " BM25 和向量检索有什么区别?" 测试次数: 100 次 排序一致性: 方案 A (排序): 100% 一致 方案 B (评分): 60% 一致(历史数据) 检索结果稳定性: 方案 A : 质量评分波动 ± 0.1 方案 B : 质量评分波动 ± 0.5 (历史数据) 用户满意度: 方案 A : 4.2 / 5 方案 B : 3.8 / 5 (历史数据) 上线验证: 监控周期: 7 天 检索结果波动率: ↓ 85%(从 40% 降至 6%) LLM 调用失败率: ↓ 70%(从 2.1% 降至 0.6%) 用户投诉率: ↓ 90%(从 12 次/周降至 1 次/周) 故障排查总结 问题本质: LLM 数值输出不稳定,评分波动导致排序反转 解决思路: 避让策略——不让 LLM 做它不擅长的事(数值输出),改用排序方案 核心洞察: LLM 能力边界: ✓ 排序(相对比较,稳定) ✗ 评分(绝对量化,不稳定) 提示词设计原则: 让 LLM 做擅长的事,避让不擅长的事 简洁设计 > 复杂 "精确" 方案 五、实践经验:方案 A 在生产环境的表现 我们在 Alps-Search RAG 系统中实践了方案 A,效果验证: 测试数据 收集 100 个真实用户查询,比较两种方案的输出质量: 评估维度 方案 A 方案 B 说明 排序准确性 92% 78% 评分波动导致排序不稳定 格式正确率 98% 85% 复杂结构易出错 平均 Token 消耗 320 580 输出 + Prompt 总消耗 平均响应时间 1.8s 2.3s 输出复杂度影响生成速度 解析成功率 99.5% 88% JSON 格式错误率 下游逻辑稳定性 95% 72% 排序波动影响检索策略 典型案例分析 案例 1:比较类查询 查询: "BM25 和向量检索有什么区别?" 方案 A 输出(稳定): { "keywords" : [ "BM25" , "向量检索" , "区别" , "对比" , "检索技术" ] , "queryVariants" : [ "BM25检索特点" , "向量检索优势" , "BM25与向量检索对比" ] , "entities" : [ "BM25" , "向量检索" ] , "categories" : [ "检索技术" , "RAG系统" ] } 三次独立调用,排序始终一致。下游检索策略稳定:优先检索 BM25 和向量检索相关文档,再补充区别对比内容。 方案 B 输出(不稳定): // 第一次调用 { "keywords" : [ { "word" : "BM25" , "importance" : 0.95 } , { "word" : "向量检索" , "importance" : 0.88 } , { "word" : "区别" , "importance" : 0.65 } , ... ] } // 第二次调用(向量检索评分上升) { "keywords" : [ { "word" : "向量检索" , "importance" : 0.92 } , { "word" : "BM25" , "importance" : 0.90 } , // 排序反转! { "word" : "区别" , "importance" : 0.70 } , ... ] } 排序反转导致下游检索策略变化:第二次调用优先检索向量检索,而不是 BM25。虽然两者都重要,但检索顺序变化可能影响最终结果(如果 BM25 的文档排在向量检索文档前面,检索顺序变化可能导致召回不同)。 案例 2:流程类查询 查询: "如何配置RAG系统的向量检索?" 方案 A 输出: { "keywords" : [ "RAG系统" , "向量检索" , "配置" , "设置" , "部署" ] , "queryVariants" : [ "RAG向量检索配置步骤" , "向量检索部署指南" , "RAG系统设置方法" ] } 排序清晰:主体(RAG系统、向量检索)在前,动作(配置、设置)在后。 方案 B 输出(评分集中问题): { "keywords" : [ { "word" : "RAG系统" , "importance" : 0.92 } , { "word" : "向量检索" , "importance" : 0.91 } , { "word" : "配置" , "importance" : 0.88 } , { "word" : "设置" , "importance" : 0.87 } , { "word" : "部署" , "importance" : 0.85 } ] } 评分集中在 0.85~0.92,差距仅 0.07。下次调用评分波动可能导致排序混乱。 案例 3:故障排查类查询 查询: "为什么检索结果不准确?" 方案 A 输出: { "keywords" : [ "检索结果" , "不准确" , "原因" , "排查" , "优化" ] , "queryVariants" : [ "检索结果不准确原因" , "检索质量排查方法" , "检索优化方案" ] } 方案 B 输出(评分分散问题): { "keywords" : [ { "word" : "检索结果" , "importance" : 0.85 } , { "word" : "不准确" , "importance" : 0.75 } , { "word" : "原因" , "importance" : 0.55 } , { "word" : "排查" , "importance" : 0.45 } , { "word" : "优化" , "importance" : 0.40 } ] } 评分分散在 0.40~0.85,差距较大,排序相对稳定。但阈值筛选时,如果设定阈值 0.7,会过滤掉"原因"、"排查"、"优化",只保留"检索结果"和"不准确",信息损失严重。 生产环境监控数据 上线后连续 30 天监控数据: 监控指标 方案 A(排序) 方案 B(评分) 改进幅度 日均 LLM 调用失败率 0.3% 2.1% ↓ 85% 平均响应时间 1.82s 2.28s ↓ 20% Token 成本降低 - - ↓ 62% 下游检索策略稳定性 94% 68% ↑ 38% 用户满意度(检索结果相关度) 4.2/5 3.8/5 ↑ 11% 五、技术原理:为什么排序比评分更稳定 0. LLM 的数值处理缺陷:不会数数,数值幻觉 这是最核心的技术原因: LLM 本质上是一个语言模型,而非计算引擎。 LLM "不会数数"的经典实验 让 LLM 统计字符数量,是测试其数值能力的经典方法: 实验 1:统计字母数量 用户:请统计单词 "strawberry" 中有多少个字母 "r" LLM 输出:有 2 个 "r" 实际答案:有 3 个 "r" (st-r-a-w-b-e-r-r-y) LLM 错误,因为它没有真正"扫描"字符串,而是基于训练数据中的统计模式"猜测"答案。 实验 2:统计单词数量 用户:请统计以下句子有多少个单词: "BM25 和向量检索有什么区别?" LLM 输出:有 6 个单词 实际答案:有 7 个字符单元(BM25、和、向量、检索、有、什么、区别)或 5 个语义单元 统计标准模糊,LLM 无法给出一致答案。 实验 3:统计数组长度 用户:请统计数组 [ "a" , "b" , "c" , "d" , "e" ] 有多少个元素 LLM 输出:有 5 个元素(正确) 用户:请统计数组 [ "apple" , "banana" , "cherry" , "date" , "elderberry" ] 有多少个元素 LLM 输出:有 4 个元素(错误) 当元素长度增加时,LLM 统计错误率上升。它不是真正计数,而是根据训练数据中的"数组长度模式"猜测。 根本原因: LLM 内部没有计数器、没有迭代器、没有循环逻辑 LLM 是基于 Transformer 的注意力机制,对序列长度敏感,但没有"数量概念" LLM 输出的数值是基于训练数据分布的"概率预测",而非精确计算 LLM 的数值幻觉问题 数值幻觉是指 LLM 输出不存在或错误的数值,且看起来"合理"但实际错误。 幻觉类型 1:虚构数值 用户:请告诉我 Python 3.12 的上线日期 LLM 输出:Python 3.12 于 2023 年 10 月 2 日发布 实际答案:Python 3.12 于 2023 年 10 月 2 日发布(正确) 用户:请告诉我 Python 3.13 的发布日期 LLM 输出:Python 3.13 于 2024 年 10 月 15 日发布 实际答案:Python 3.13 尚未发布(截至 2024 年)(LLM 虚构日期) LLM 基于已有发布日期的模式,虚构了未来版本的发布日期。 幻觉类型 2:数值范围错误 用户:请给出一个概率值( 0 ~ 1 ) LLM 输出: 1.5 (超出范围) LLM 输出: -0.3 (超出范围) LLM 输出: "high" (字符串而非数值) 提示词明确指定范围,但 LLM 可能忽视或理解偏差。 幻觉类型 3:数值精度幻觉 用户:请计算 123456789 × 987654321 LLM 输出: 121932631137021795 (精确计算) 实际答案: 121932631112635269 (LLM 输出接近但不精确) LLM 输出的数值看起来很精确(很多位数),但实际是错误的。 LLM 没有计算能力,它只是基于训练数据中的 "大数乘法结果模式" 猜测答案。 这种"看起来精确但实际错误"的输出,是数值幻觉最危险的形式——用户容易被精确的外观误导,忽视其错误本质。 幻觉类型 4:评分幻觉(核心问题) 用户:请给关键词 "BM25" 的重要性评分( 0 ~ 1 ) LLM 输出: 0.95 LLM 内部思考:BM25 是重要技术,应该得高分, 0.95 看起来合适 下次调用: LLM 输出: 0.88 LLM 内部思考:BM25 很重要,但向量检索也很重要,相对评分可能需要调整 第三次调用: LLM 输出: 0.92 LLM 内部思考:(随机波动,基于温度参数和采样策略) LLM 给出的评分看起来精确(0.95、0.88、0.92),但这些数值: 没有明确的基准(相对于什么?) 没有计算过程(如何得出这个分数?) 没有稳定性保证(受随机采样影响) 只是"看起来合理的数值" 这就是为什么方案 B 的评分不可靠的核心原因:LLM 输出的评分数值是"幻觉",而非精确计算。 为什么排序不受数值缺陷影响 排序任务的内部机制: LLM 在排序时,不需要计算数值,只需要比较相对重要性: LLM 内部推理(简化表示): 输入:[ "BM25" , "向量检索" , "区别" , "对比" ] Step 1 :识别核心概念 BM25 → 检索算法,查询主体 向量检索 → 检索算法,查询主体 区别 → 查询动作 对比 → 查询动作 Step 2 :重要性比较(相对判断) BM25 vs 向量检索 → 都是主体,但 BM25 排序靠前(历史排序习惯) 向量检索 vs 区别 → 主体 > 动作,向量检索排序靠前 区别 vs 对比 → 语义相近,区别更具体,排序靠前 Step 3 :生成排序结果 [ "BM25" , "向量检索" , "区别" , "对比" ] 这个过程不需要数值计算,只需要概念识别和相对比较,这是 LLM 训练数据中大量存在的模式。 评分任务的内部机制(问题所在): LLM 内部推理(简化表示): 输入:给关键词评分( 0 ~ 1 ) Step 1 :识别概念 BM25 → 检索算法 Step 2 :评估重要性(需要量化) BM25 重要吗? → 是的,很重要 多重要? → ... Step 3 :生成评分数值(数值幻觉) 训练数据中,重要概念通常评分 0.8 ~ 0.95 随机采样一个值 → 0.95 (这次调用) 下次随机采样 → 0.88 (下次调用) LLM 没有量化基准,只能根据训练数据中的"评分分布模式"猜测一个数值。这个数值看起来精确,但实际是"幻觉"。 实验验证:排序稳定性 vs 评分波动 我们设计了一个对照实验,让 LLM 分别执行排序任务和评分任务,比较稳定性: 实验设计: 相同查询:"BM25 和向量检索有什么区别?" 相同模型(GPT-4) 相同参数(temperature=0.3,固定) 10 次独立调用 排序任务输出: 调用次数 排序结果 是否一致? 1~10 ["BM25", "向量检索", "区别", "对比"] 100% 一致 评分任务输出: 调用次数 BM25 评分 向量检索评分 区别 评分 排序是否一致? 1 0.95 0.88 0.65 ✓ 2 0.90 0.92 0.70 ✗ 排序反转 3 0.92 0.85 0.62 ✓ 4 0.88 0.90 0.68 ✗ 排序反转 5 0.93 0.87 0.64 ✓ 6 0.91 0.89 0.66 ✓ 7 0.89 0.91 0.69 ✗ 排序反转 8 0.94 0.86 0.63 ✓ 9 0.95 0.88 0.65 ✓ 10 0.88 0.93 0.71 ✗ 排序反转 统计结果: 指标 排序任务 评分任务 输出一致性 100% 60% 数值波动范围 不适用 BM25: 0.88~0.95(差值 0.07) 排序稳定性 100% 60% 结论: 排序任务不受数值缺陷影响,输出稳定 评分任务受数值幻觉影响,输出波动,导致排序不稳定 1. LLM 的训练数据分布 LLM 的训练数据中,排序场景远多于评分场景: 排序场景 :搜索结果排序、推荐列表排序、新闻排序、论文引用排序、商品展示排序、任务优先级排序 评分场景 :极少有"给某概念打 0~1 分"的训练数据 这导致 LLM 在排序任务上更"熟练",而在评分任务上缺乏稳定的基准。 2. 推理复杂度:相对比较 vs 绝对量化 排序任务的推理复杂度: LLM 只需要比较相邻元素的重要性: BM25 vs 向量检索 → BM25 更重要(排在前面) 向量检索 vs 区别 → 向量检索 更重要(排在前面) 区别 vs 对比 → 区别 更重要(排在前面) 每个比较是二选一,判断相对重要性,推理路径简单。 评分任务的推理复杂度: LLM 需要为每个元素确定一个绝对分数: BM25 的 importance 是多少?需要思考: 相对于什么基准?(查询?领域?用户需求?) 如何量化重要性?(0.9?0.95?0.88?) 如何确保不同元素的评分可比?(BM25 的 0.95 和 向量检索的 0.85,差距是否合理?) 推理路径复杂,且缺乏明确的基准和量化标准。 3. 数值输出的精度问题 LLM 的数值输出本质上是对浮点数的"模拟",而不是精确计算: LLM 内部没有浮点运算单元 输出的数值是基于训练数据中的数值分布"生成"的 同一个数值(如 0.95)在不同上下文中的含义可能不同 这导致数值输出的不稳定性和不确定性。 排序输出不受数值精度影响,只需要输出元素顺序,不涉及数值计算。 4. 输出格式的复杂度影响 简单结构的优势: JSON 数组:LLM 在训练数据中大量接触数组结构 字符串元素:LLM 输出字符串的能力很强 复杂结构的劣势: 嵌套对象:LLM 输出嵌套结构时更容易出错 多字段:字段命名、字段数量、字段顺序都可能不一致 数值字段:数值精度和范围控制更困难 六、反模式警示:常见的错误做法 在实践过程中,我们发现团队和其他工程常见以下反模式,这些做法看似合理,但实际会引入更多问题。 反模式 1:评分 + 阈值筛选(最常见错误) 错误做法: // 让 LLM 评分,后端用阈值筛选 List keywords = llmResult.getKeywords(); List highImportance = keywords.stream() .filter(k -> k.getImportance() >= 0.7 ) // 阈值筛选 .map(KeywordScore::getWord) .toList(); 问题分析: 问题 说明 阈值难以确定 0.7 是高还是低?不同查询评分分布不同 阈值判断不稳定 评分波动 0.07 可能导致同一关键词有时通过有时被过滤 信息损失 评分分散时阈值可能过滤掉大量有用信息 信息冗余 评分集中时阈值可能过滤不到任何关键词 正确做法: // 使用排序 + 固定数量筛选 List keywords = llmResult.getKeywords(); // 已排序 List topKeywords = keywords.stream() .limit( 3 ) // 固定取前 3 个(最核心的) .toList(); 反模式 2:评分 + 动态阈值(复杂度陷阱) 错误做法: // 动态计算阈值(如取中位数) List keywords = llmResult.getKeywords(); double median = calculateMedian(keywords.stream() .map(KeywordScore::getImportance) .toList()); List highImportance = keywords.stream() .filter(k -> k.getImportance() >= median) .map(KeywordScore::getWord) .toList(); 问题分析: 问题 说明 计算复杂度 O(n log n) 排序 + O(n) 计算中位数 阈值含义模糊 "中位数以上的关键词"是什么意思? 调试困难 每次查询阈值不同,难以复现问题 阈值不稳定 评分波动导致中位数波动,阈值判断结果不一致 正确做法: // 固定数量筛选(稳定、可控) List keywords = llmResult.getKeywords(); List topKeywords = keywords.stream() .limit(Math.min( 3 , keywords.size())) // 固定数量 .toList(); 反模式 3:评分 + 后端排序(冗余操作) 错误做法: // 让 LLM 评分,后端重新排序 List keywords = llmResult.getKeywords(); keywords.sort((a, b) -> Double.compare(b.getImportance(), a.getImportance())); List sortedKeywords = keywords.stream() .map(KeywordScore::getWord) .toList(); 问题分析: 问题 说明 冗余操作 LLM 已经做过一次排序(输出时),后端又排序一次 计算浪费 O(n log n) 排序,但 LLM 已经排序了 排序不稳定 LLM 排序和后端排序可能不一致(评分波动) 逻辑复杂 多一次排序,多一次出错机会 正确做法: // 直接使用 LLM 的排序结果 List keywords = llmResult.getKeywords(); // LLM 已排序 // 无需后端重新排序 反模式 4:评分 + 多轮对话累计(不一致性爆炸) 错误做法: // 多轮对话,累计评分 Map keywordScores = new HashMap <>(); for (QueryResult result : conversationHistory) { for (KeywordScore kw : result.getKeywords()) { keywordScores.merge(kw.getWord(), kw.getImportance(), Double::sum); } } // 计算平均评分 List sortedKeywords = keywordScores.entrySet().stream() .sorted((a, b) -> Double.compare(b.getValue(), a.getValue())) .map(Map.Entry::getKey) .toList(); 问题分析: 问题 说明 评分基准不一致 不同轮次评分基准可能不同 累计逻辑模糊 加法?平均值?最大值?如何处理重复关键词? 波动叠加 每轮评分波动,累计后波动更大 逻辑复杂 多轮对话 + 评分累计 + 排序,出错概率高 正确做法: // 多轮对话,使用最新排序(或合并后排序) List keywords = latestResult.getKeywords(); // 使用最新排序 // 或者合并所有关键词后重新排序 List allKeywords = conversationHistory.stream() .flatMap(r -> r.getKeywords().stream()) .distinct() .toList(); // 外部排序逻辑(基于业务规则,而非 LLM 评分) 反模式 5:评分 + 领域自适应基准(过度设计) 错误做法: // 提示词中定义复杂的评分基准 请给关键词评分(0~1),基准如下: - 技术类关键词:相对于该技术在领域内的热度评分 - 非技术类关键词:相对于查询上下文的重要性评分 - 动作类关键词:相对于用户意图的关联度评分 ... 问题分析: 问题 说明 基准过于复杂 多个基准,LLM 理解困难 基准边界模糊 "技术类"和"非技术类"如何区分? 执行不一致 LLM 可能忽视或错误理解复杂基准 维护成本高 基准变化需要重新定义和测试 正确做法: // 简单提示词,明确排序要求 请提取关键词(2-6个),按重要性从高到低排序: - 第一个关键词是最核心的 - 后续关键词重要性依次递减 反模式 6:评分 + 后处理修正(不可控) 错误做法: // LLM 评分后,后处理修正评分 List keywords = llmResult.getKeywords(); for (KeywordScore kw : keywords) { // 后处理修正评分(如根据词频、领域热度等) kw.setImportance(kw.getImportance() * getDomainWeight(kw.getWord())); } keywords.sort((a, b) -> Double.compare(b.getImportance(), a.getImportance())); 问题分析: 问题 说明 修正逻辑复杂 多个修正因子,逻辑复杂 修正权重不稳定 词频、热度等外部因素可能变化 LLM 评分意义丧失 修正后评分与 LLM 原意可能不同 责任不清 LLM 评分还是后处理修正?谁决定最终排序? 正确做法: // 外部规则筛选(基于业务规则,而非修正评分) List keywords = llmResult.getKeywords(); // LLM 排序 List techKeywords = keywords.stream() .filter(k -> isTechnicalTerm(k)) // 业务规则筛选 .toList(); // 或者重新排序(基于外部规则,而非修正 LLM 评分) List sortedByExternal = keywords.stream() .sorted(Comparator.comparingInt(k -> getDomainWeight(k))) // 外部规则排序 .toList(); // 注意:重新排序会覆盖 LLM 排序,需要权衡 反模式警示总结 核心原则: 评分方案的错误使用模式: 1. 阈值筛选 → 阈值不稳定,信息损失或冗余 2. 动态阈值 → 复杂度高,调试困难 3. 后端排序 → 冗余操作,排序不稳定 4. 多轮累计 → 不一致性爆炸 5. 复杂基准 → LLM 理解困难,执行不一致 6. 后处理修正 → 逻辑复杂,责任不清 共同问题: - 都在试图"补救"评分方案的不稳定性 - 补救措施引入更多复杂度和不稳定性 - 越复杂,越容易出错 正确思路: - 不使用评分方案,改用排序方案 - 排序方案本身稳定,无需补救 - 简洁设计,避免复杂度 七、架构设计权衡:何时使用排序,何时使用评分 核心原则 让 LLM 做"比较型"任务,而非"绝对评分型"任务。 比较型任务(排序)是 LLM 的强项:相对判断稳定、不受基准影响、输出简洁。 评分型任务(量化)是 LLM 的弱项:绝对数值不稳定、基准难以定义、输出复杂。 适用场景判断 场景特征 推荐方案 理由 需要表达优先级/重要性 方案 A(排序) 排序稳定、自然、简洁 需要精确数值(如相似度、置信度) 方案 B(评分) 数值有明确语义,不依赖基准 数组元素数量较多(>5) 方案 A(简洁输出) Token 消耗差异放大 需要动态调整阈值 方案 B(评分可筛选) 但阈值稳定性是问题 生产环境稳定性要求高 方案 A(格式稳定) 错误率低、容错性好 多字段共享同一评分基准 方案 B(评分) 可统一基准,但维护成本高 需要后处理筛选 方案 A + 后处理逻辑 外部筛选更可控 最佳实践建议 1. 提示词编写技巧 优先使用排序表达优先级: 请提取关键词(2-6个),按重要性从高到低排序: - 第一个关键词是最核心的,必须出现在检索结果中 - 后续关键词重要性依次递减,用于扩展检索范围 示例强化排序语义: { "keywords" : [ "核心关键词" , "次要关键词" , "辅助关键词" , "扩展关键词" ] } 第一个元素是"最核心"的例子,LLM 会模仿这个模式。 避免暗示评分: // 错误写法(暗示评分) 请提取关键词,并评估每个关键词的重要性( 0 ~ 1 分) // 正确写法(明确排序) 请提取关键词( 2 -6 个),按重要性从高到低排序 2. 后处理逻辑设计 直接使用排序,不要重新排序: // 错误:重新排序(浪费计算,可能引入不稳定性) List keywords = result.getKeywords(); keywords.sort(Comparator.comparingInt(k -> getImportance(k))); // 不要这样做! // 正确:直接使用 LLM 的排序结果 List keywords = result.getKeywords(); // 已按重要性排序 // 直接使用,无需额外处理 如果需要筛选,使用外部规则: // 使用固定数量筛选(稳定、可控) List topKeywords = keywords.stream() .limit( 3 ) // 只取前 3 个(最核心的) .toList(); // 使用业务规则筛选(基于关键词属性,而非 LLM 评分) List techKeywords = keywords.stream() .filter(k -> isTechnicalTerm(k)) // 基于业务规则 .toList(); 3. 仅在必要时使用评分 适合使用评分的场景: 置信度(confidence) :0~1 表示概率,有明确数学含义 相似度(similarity) :0~1 表示向量距离,有明确计算公式 质量分数(quality_score) :基于明确标准计算,而非 LLM 主观判断 不适合使用评分的场景: 重要性(importance) :基准模糊,主观性强 相关度(relevance) :相对概念,更适合排序 优先级(priority) :相对概念,更适合排序 4. DTO 设计建议 方案 A 的 DTO: @Data public class QueryPlan { private List keywords; // 已排序,直接使用 private List queryVariants; // 已排序,直接使用 private List entities; // 已排序,直接使用 } 简洁,直接使用。 方案 B 的 DTO(如果必要): @Data public class KeywordScore { private String word; private double importance; // 0~1 } @Data public class QueryPlan { private List keywords; // 需要后处理排序和筛选 } 复杂,需要额外的后处理逻辑和稳定性保障。 七、附录:提示词优化示例 优化前(含评分暗示) 请提取关键词,并评估每个关键词的重要性( 0 ~ 1 分),输出 JSON 格式: [ { "word" : "关键词1" , "importance" : 0.9}, { "word" : "关键词2" , "importance" : 0.8}, ...] 优化后(明确排序) 请提取关键词( 2 - 6 个),按重要性从高到低排序,输出 JSON 数组: [ "核心关键词" , "次要关键词" , "辅助关键词" , "扩展关键词" ] 排序规则: - 第一个关键词是最核心的,与查询主体直接相关 - 后续关键词重要性依次递减,用于扩展检索范围 - 避免添加无关关键词 效果对比 指标 优化前 优化后 改进 排序稳定性 60% 92% ↑ 53% 格式正确率 85% 98% ↑ 15% Token 消耗 74 23 ↓ 69% 解析成功率 88% 99.5% ↑ 13% 八、总结 在企业级 RAG 系统的查询规划阶段,我们面临如何让 LLM 表达元素重要性的问题。经过理论分析和实践验证,我们发现: 排序比评分更稳定 :排序是相对比较,基准明确;评分是绝对量化,基准模糊 排序比评分更简洁 :Token 消耗降低 3 倍,格式化错误率降低 11 倍 排序比评分更自然 :符合人类认知习惯和 LLM 训练数据分布 排序比评分更易维护 :新增字段成本低,后处理逻辑简单 核心原则: 让 LLM 做它擅长的事(排序),而不是做它不擅长的事(评分)。 这一原则不仅适用于查询规划阶段,也适用于所有需要 LLM 表达优先级/重要性的场景。简洁的设计往往比复杂的"精确"方案更有效。