三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

RAG查询改写后数字、缩写和型号消失?Token清洗与双路检索完整排查

RAG查询改写后数字、缩写和型号消失?Token清洗与双路检索完整排查

文章摘要

技术文档、设备手册、API错误、商品目录和企业业务系统中,真正决定召回结果的往往不是自然语言,而是HTTP 429Spring AI 2.0ZX-4100BSKU-00981V2.3.1EUDRPUC等高信息密度Token。很多RAG系统为了“清洗输入”和“提升语义”,会删除标点、停用词、连字符、大小写或数字,最终导致BM25失去精确匹配,Dense Embedding也丢失关键区分度。

常见故障包括:HTTP 429只剩“限流问题”;Spring AI 2.0被改成Spring AI;型号中的连字符被拆开;C++被清洗成CA/B测试被改写为“测试”;日期和版本号被错误归一化。由于改写后的句子仍然流畅,这类问题常被误判为向量数据库召回差或Embedding模型不适合中文。

本文将查询处理拆成Raw、Normalized、Sparse和Dense四种视图,建立受保护Token词法分析器、业务缩写词典、版本与错误码解析器、原始Query稀疏检索、受控Dense改写、Token保真校验和回归数据集,并给出Spring Boot实现、指标和排查步骤。

一、一个最典型的错误

用户输入:

Spring AI 2.0升级后调用MCP Streamable HTTP返回429,ttlMs配置是否有问题?

错误清洗:

spring ai 升级 调用 mcp streamable http 返回 限流 ttlms 配置 问题

进一步改写:

Spring AI升级后MCP连接被限流,如何调整缓存时间?

丢失信息:

  • 2.0
  • 429
  • ttlMs的大小写;
  • “是否有问题”这一诊断语义;
  • Streamable HTTP作为协议名称的整体性。

后果:

  • BM25无法精确命中包含429ttlMs的排障文档;
  • Dense检索会泛化到所有“限流”和“缓存”文章;
  • Reranker难以恢复已经被删除的Token;
  • 最终回答可能讨论错误的限流策略。

二、为什么技术Token特别脆弱

1. 正则把非中文和字母数字当噪声

错误代码:

query.replaceAll("[^\\p{IsHan}a-zA-Z\\s]"," ");

这会删除:

  • 数字;
    -小数点;
    -连字符;
    -斜杠;
    -加号;
    -井号。

2. 停用词表误删缩写

ITUSORIN在英文中可能是停用词,但也可能是:

  • 信息技术;
    -国家代码;
    -逻辑运算;
    -SQL关键字。

3. 分词器拆散型号

ZX-4100B → ZX / 4100 / B

拆分有时有利于召回,但如果没有保留完整Token,精确匹配能力会下降。

4. 大小写归一化破坏业务含义

mcp MCP Mcp

自然语言可能等价,但大小写敏感代码、字段名和型号可能不同。

5. LLM“纠错”

模型可能把低频Token改为常见Token:

ttlMs → TTL

6. 翻译改写

A/B Test → 对照实验

自然语言等价,但技术文档可能只出现A/B Test

三、不要只有一个Query字符串

推荐建立四种视图。

publicrecordQueryViews(Stringraw,Stringnormalized,StringsparseQuery,StringdenseQuery,Set<ProtectedLexeme>protectedLexemes,QueryNormalizationTracetrace){}

Raw

用户原始输入,不允许覆盖。

Normalized

只做可逆或确定性规范化:

  • Unicode;
    -空白;
    -全角半角;
    -不可见字符;
    -常见引号。

Sparse Query

面向BM25或倒排索引,最大程度保留精确Token。

Dense Query

面向Embedding,可进行受控改写,但必须保留关键Token和语义约束。

四、第一原则:Normalization必须可解释

错误:

Stringclean=raw.toLowerCase().replaceAll("[^a-z0-9\\u4e00-\\u9fa5 ]"," ").replaceAll("\\s+"," ").trim();

问题:

  • 不知道删除了什么;
  • 无法恢复;
  • 大小写全部丢失;
  • 符号语义消失;
  • 排障无法复现。

正确做法:

publicrecordNormalizationChange(NormalizationRulerule,Stringbefore,Stringafter,intstartOffset,intendOffset){}

每个规则有版本:

publicrecordQueryNormalizationTrace(StringnormalizerVersion,List<NormalizationChange>changes,Set<String>removedFragments,Set<String>changedFragments){}

五、哪些字符不能随便删除

技术Query常见符号:

符号示例可能含义
.2.0.1版本
-ZX-4100B型号或编号
_tenant_id字段名
/A/BHTTP/2协议或比较
+C++语言名
#C#语言名
:error:429键值或错误
@@McpTool注解
$${tenantId}模板变量
%95%比例

处理策略不应是“保留所有符号”或“删除所有符号”,而是先识别Token类型。

六、受保护Token类型

publicenumLexemeType{VERSION,ERROR_CODE,HTTP_STATUS,MODEL_ID,SKU,DEVICE_MODEL,CONTRACT_ID,CLASS_NAME,METHOD_NAME,CONFIG_KEY,ANNOTATION,ACRONYM,DATE,PERCENTAGE,MONEY,FILE_PATH,URL_FRAGMENT,UNKNOWN_TECH_TOKEN}
publicrecordProtectedLexeme(Stringraw,Stringcanonical,LexemeTypetype,intstart,intend,ProtectionModemode,doubleconfidence){}

保护模式:

publicenumProtectionMode{EXACT,CASE_INSENSITIVE,CANONICAL_EQUIVALENT,TOKEN_SET_EQUIVALENT}

七、技术Token词法分析器

@ComponentpublicclassTechnicalLexemeExtractor{privatefinalList<LexemeRecognizer>recognizers;publicSet<ProtectedLexeme>extract(Stringquery){Map<String,ProtectedLexeme>result=newLinkedHashMap<>();for(LexemeRecognizerrecognizer:recognizers){for(ProtectedLexemelexeme:recognizer.recognize(query)){result.merge(lexeme.raw(),lexeme,this::preferHigherConfidence);}}returnSet.copyOf(result.values());}}

Recognizer示例:

publicinterfaceLexemeRecognizer{List<ProtectedLexeme>recognize(Stringquery);}

八、版本号识别

privatestaticfinalPatternVERSION_PATTERN=Pattern.compile("(?i)(?<![a-z0-9])v?\\d+(?:\\.\\d+){1,4}(?:[-+_][a-z0-9.]+)?(?![a-z0-9])");

能够匹配:

2.0 V2.3.1 1.0.0-RC1 3.4.0+build7

版本比较不能转为普通小数:

2.10 并不小于 2.9

需要语义版本模型:

publicrecordSemanticVersion(intmajor,intminor,intpatch,Stringprerelease,StringbuildMetadata){}

九、错误码与HTTP状态

privatestaticfinalPatternHTTP_STATUS=Pattern.compile("(?i)\\b(?:HTTP[/\\s]?[12](?:\\.\\d)?\\s*)?([1-5]\\d{2})\\b");privatestaticfinalPatternERROR_CODE=Pattern.compile("\\b[A-Z][A-Z0-9]{1,15}[-_][A-Z0-9-]{1,24}\\b");

429在普通文本中可能只是数字,因此需要上下文:

HTTP 429 状态码429 返回429

高置信识别后使用EXACT保护。

十、字段名、类名和方法名

ttlMs SearchRequest filterExpression similaritySearch spring.ai.vectorstore.qdrant

规则:

  • CamelCase;
  • snake_case;
  • dotted.key;
  • 包名;
  • 方法调用;
  • 注解。
privatestaticfinalPatternCAMEL_CASE=Pattern.compile("\\b[a-z]+(?:[A-Z][a-zA-Z0-9]*)+\\b");privatestaticfinalPatternDOTTED_KEY=Pattern.compile("\\b[a-z][a-z0-9_-]*(?:\\.[a-z0-9_-]+){1,8}\\b");

十一、业务缩写词典

publicrecordAcronymEntry(Stringacronym,Set<String>expansions,Set<String>domains,booleanpreserveExact,Stringversion){}

例如:

EUDR MCP RAG SKU PUC SSE RRF DBSF

不要直接把缩写替换为全称。

推荐Sparse Query:

MCP Streamable HTTP

Dense Query可以:

MCP(Model Context Protocol)Streamable HTTP

即:

扩展 但不删除原缩写

十二、占位符保护法

在把Query交给LLM前,将高风险Token替换为不可修改占位符。

publicrecordPlaceholderMap(StringprotectedText,Map<String,String>placeholders){}

原始:

ZX-4100B在V2.3后出现E1027

保护后:

__LEXEME_001__在__LEXEME_002__后出现__LEXEME_003__

模型改写后再恢复。

@ServicepublicclassQueryTokenProtector{publicPlaceholderMapprotect(Stringraw,Set<ProtectedLexeme>lexemes){Stringresult=raw;Map<String,String>map=newLinkedHashMap<>();List<ProtectedLexeme>ordered=lexemes.stream().sorted(Comparator.comparingInt(ProtectedLexeme::start).reversed()).toList();intsequence=1;for(ProtectedLexemelexeme:ordered){Stringplaceholder="__LEXEME_%03d__".formatted(sequence++);result=result.substring(0,lexeme.start())+placeholder+result.substring(lexeme.end());map.put(placeholder,lexeme.raw());}returnnewPlaceholderMap(result,map);}}

注意:Offset替换必须从后向前执行。

十三、占位符也可能被模型修改

模型可能输出:

LEXEME_001

或删除占位符。

恢复前校验:

publicvoidassertAllPlaceholdersPresent(PlaceholderMapmap,Stringrewritten){Set<String>missing=map.placeholders().keySet().stream().filter(key->!rewritten.contains(key)).collect(Collectors.toSet());if(!missing.isEmpty()){thrownewQueryTokenLossException(missing);}}

十四、Sparse Query如何构建

Sparse检索强调词面保真。

publicStringbuildSparseQuery(QueryViewsviews){returnString.join(" ",views.raw(),views.protectedLexemes().stream().map(ProtectedLexeme::raw).distinct().collect(Collectors.joining(" ")));}

也可以针对搜索引擎构建Boost:

"ZX-4100B"^5 "E1027"^5 "V2.3"^3 设备 离线^1

不要把整句话全部Exact Match,否则召回过窄。

十五、Dense Query如何构建

Dense Query目标是提升语义,但保留关键Token。

publicStringbuildDenseQuery(Stringrewritten,Set<ProtectedLexeme>lexemes){Stringsuffix=lexemes.stream().map(ProtectedLexeme::raw).distinct().collect(Collectors.joining(" "));returnrewritten+"\n关键技术Token:"+suffix;}

是否追加Token需通过数据集评测,避免过度影响Embedding。

十六、双路检索

List<Document>sparseDocuments=sparseRetriever.search(views.sparseQuery(),accessContext,40);List<Document>denseDocuments=vectorStoreRetriever.similaritySearch(SearchRequest.builder().query(views.denseQuery()).topK(40).filterExpression(tenantFilter).build());List<Document>fused=fusionService.fuse(sparseDocuments,denseDocuments);

技术Token查询通常不能只依赖Dense。

十七、为什么Reranker不能修复Token丢失

Reranker只能在已有候选中重新排序。

如果正确文档因为ZX-4100B被删除而没有进入Top K:

Reranker无文档可排

因此Token保真是召回前问题,不是重排问题。

十八、查询改写后的保真校验

publicrecordLexemePreservationReport(booleanpassed,Set<String>missingExact,Set<String>caseChanged,Set<String>canonicalMismatch,Set<String>unexpectedTechnicalTokens){}
publicLexemePreservationReportvalidate(Set<ProtectedLexeme>required,Stringrewritten){Set<String>missing=required.stream().filter(lexeme->lexeme.mode()==ProtectionMode.EXACT).map(ProtectedLexeme::raw).filter(token->!rewritten.contains(token)).collect(Collectors.toSet());returnnewLexemePreservationReport(missing.isEmpty(),missing,findCaseChanges(required,rewritten),findCanonicalMismatch(required,rewritten),findUnexpectedTokens(required,rewritten));}

失败时:

Dense Query回退到Normalized或Raw Sparse Query继续使用Raw

十九、大小写处理策略

推荐同时保存:

raw_token token_lowercase canonical_token

检索索引可以存多个字段:

content content_lowercase technical_tokens technical_tokens_keyword

查询时:

  • technical_tokens_keyword精确;
  • content全文;
    -向量字段语义。

二十、中英文混合查询

Spring AI的toolcallback.enabled=false为什么仍然注册Tool?

不要把:

toolcallback.enabled

翻译成中文。

Translation Query策略应:

  1. 先保护技术Token;
    2.只翻译自然语言片段;
    3.恢复Token;
    4.执行保真校验。

二十一、分词调试

排障时打印不同阶段Token:

publicrecordTokenizationDebug(List<String>rawTokens,List<String>normalizedTokens,List<String>sparseTokens,List<String>denseTokens,Set<String>protectedTokens){}

但生产日志中不要直接输出敏感Query。可以在受控调试环境或对Token做Hash。

二十二、指标

rag_query_protected_lexeme_total{ type } rag_query_lexeme_loss_total{ type, stage } rag_query_normalization_change_total{ rule } rag_query_sparse_exact_hit_rate rag_query_dense_recall_rate rag_query_hybrid_required_document_recall rag_query_rewrite_fallback_total{ reason } rag_query_technical_token_query_total

关键指标:

技术Token查询的正确文档Recall@K

不能只看所有Query平均召回。

二十三、回归样本

HTTP 429 HTTP/2 C++ C# A/B Test Spring AI 2.0 v1.2.10 ZX-4100B E1027 tenant_id filterExpression @McpTool ttlMs 95% 2026-08-08 HT-2026-0081

每条Case定义:

  • 必须保留Token;
    -允许的Canonical形式;
    -期望文档;
    -禁止文档;
    -Sparse/Dense/Hybrid结果。

二十四、自动化测试

@ParameterizedTest@MethodSource("technicalTokenCases")voidprotectedTokensMustSurvive(Stringquery,Set<String>expected){QueryViewsviews=queryViewService.build(query);assertThat(views.protectedLexemes().stream().map(ProtectedLexeme::raw)).containsAll(expected);assertThat(views.sparseQuery()).contains(expected.toArray(String[]::new));}

检索集成测试:

@TestvoidmodelNumberMustRetrieveExactManual(){RetrievalResultresult=ragRetriever.retrieve("ZX-4100B出现E1027怎么办?");assertThat(result.documents()).extracting(Document::getId).contains("MANUAL-ZX-4100B-E1027");}

二十五、完整排查顺序

1. 检查query_raw 2. 检查Unicode和全角半角转换 3. 检查清洗正则 4. 检查停用词表 5. 检查分词结果 6. 检查技术Token提取 7. 检查占位符是否被删除 8. 检查改写输出 9. 检查Sparse实际Query 10. 检查Dense实际Query 11. 检查索引是否存储完整Token 12. 检查融合与Reranker候选

如果索引阶段已经把连字符和大小写全部丢失,仅修复查询侧仍不够,需要重建相应字段。

二十六、常见错误

为了中文分词删除全部非中文字符 对所有Query统一转小写 将版本号当普通小数 停用词表直接应用于技术Query 只保存改写Query,不保存Raw 只做Dense,不做Sparse 依赖Reranker修复召回缺失 翻译Query时不保护代码和字段名

二十七、上线前检查清单

□ Raw Query不可变保存 □ Normalization规则可追踪且版本化 □ 技术Token在清洗前抽取 □ 版本、错误码、型号和字段名有专用Recognizer □ 业务缩写词典可版本化 □ 高风险Token使用占位符保护 □ 占位符恢复前检查完整性 □ Sparse Query保留原始Token □ Dense Query允许语义改写但通过保真校验 □ Hybrid检索使用稳定融合 □ 索引中存在技术Token精确字段 □ 技术Query有独立黄金数据集 □ Token损失指标进入质量门禁

总结

技术RAG的查询处理目标不是把句子变得更自然,而是最大限度保留检索信息。

推荐架构:

Raw Query → 技术Token抽取 → 可解释Normalization → Sparse Query保真 → Dense Query受控增强 → Token校验 → Hybrid Retrieval

数字、缩写、型号和错误码一旦在检索前丢失,后面的向量库、Reranker和大模型通常无法恢复。把这些Token视为一等数据,而不是清洗噪声,是技术知识库从Demo走向生产的基础。

← 返回列表