news 2026/10/13 6:50:23

LangChain4j RAG重排序实战:从向量召回到Cross-Encoder精排

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LangChain4j RAG重排序实战:从向量召回到Cross-Encoder精排

前阵子维护一个基于 LangChain4j 的内部知识库问答机器人,遇到的第一个尴尬不是模型不会说,而是它“引错书”。用户问“数据库连接池的 maxLifetime 设置不合理会有哪些隐患”,系统从 500 多篇技术文档里找回 4 篇,结果第一篇讲的是连接池参数大全,第二篇讲的是慢 SQL 排查,真正把 maxLifetime 和孤儿连接讲清楚的那篇排在第三。LLM 只能拿着前两篇开始组织答案,结果自然是答非所问。

后来解决方案其实不复杂:给 LangChain4j 的 RAG 流程加了一级重排序(Rerank)。这篇文章把整个改造过程、核心原理、代码写法和踩坑记录都整理出来,Java 技术栈做 RAG 的同学可以直接抄作业。

1. 为什么“召回准”不等于“命中准”

1.1 两段式检索:召回是海选,重排才是决赛

普通 RAG 的默认流程很直观:拿用户 query 做向量化,去向量库取 TopK 文档,拼进 Prompt 丢给 LLM。大多数教程里 K 取 4 或 5,文章也只讲到这里。但真实项目跑久了就会发现,问题恰恰出在“这个 K 到底怎么取”。

向量检索的设计目标从来不是“排序”,而是“召回”。它用双塔结构把 query 和文档分别编码成向量,再做余弦相似度或 ANN 近似搜索,目的是在百万级候选里快速圈出“可能有关系”的几十条。信息检索领域把这种策略叫作“先召回、再精排”,向量检索只负责前半段,后半段精排才是 RAG 里真正决定答案质量的部分。

我用一个类比来理解这件事:海选的任务是不漏掉有潜力的人,而不是直接决定冠亚季军。向量检索在线上花几十毫秒把库里可能相关的文档找回来,但它没能力从“相似”里再判断“哪个更相关”。这是两塔结构的天然限制,不是调个 embedding 模型就能解决的。

在 LangChain4j 里,默认的EmbeddingStoreContentRetriever干的就是海选的活。如果没人告诉它要精排,它就拿着一堆“看起来相似”的文档去喂 LLM,结果自然不可控。

1.2 典型场景:向量分数高,答案却跑偏

我实际遇到过三类典型问题。

第一类叫“语义重叠但意图偏离”。用户问“退款需要提交哪些材料”,知识库里有《退货与退款操作手册》,两个文本的向量相似度非常高。但手册的大篇幅都在讲物流单号、快递面单,真正回答问题的只有最后一小节。向量检索只做全局语义对齐,不会把“问题意图”和“文档重点”做细粒度匹配。

第二类叫“长文档稀释”。一份 8000 字的手册,向量是整个文本的平均语义。用户问的是手册第 6000 字附近才出现的细节,全篇向量的相似度反而不如另一篇泛泛而谈的概念性文章。这种场景在大规模产品手册、技术规范文档里非常常见。

第三类叫“关键词幻觉”。query 里出现一个专有名词,比如Graceful Shutdown,知识库里恰好有篇讲 Kubernetes Pod 终止流程的文档,向量分数被这个词顶得很高。但实际上用户问的是 Spring Boot 的优雅停机,词汇命中不等于语义相关。

这三类问题靠换 embedding 模型、调切分粒度能缓解,但很难根治。关键思路是把“相似度”换成“相关性”判断,这正是 Cross-Encoder 这类重排模型擅长的。

1.3 没有评估指标的优化都是耍流氓

动手优化之前,得先定义什么叫“命中准”。我习惯盯三个指标。

Recall@K看真实相关文档在 TopK 里出现的比例。K=10 这个数如果只有 60%,说明文档在召回阶段就被过滤掉了,重排救不回来,要先回去调切分粒度、embedding 模型或索引结构。

MRR(Mean Reciprocal Rank)看第一条相关文档排名的倒数,能直接反映“第一条是不是就对了”。LLM 对排序最敏感的其实就是前几个文档,第一条对了,答案质量基本有保证。

NDCG适合标注了“完全相关 / 部分相关 / 不相关”的评估集,能区分排序质量,而不只是“有没有出现”。

我当时写了 50 条测试 query,人工标注相关文档,跑出来的基线 MRR 只有 0.38,大量 query 的第一条就是错的。这个数据证明问题不在代码逻辑,而在排序策略,这才决定认真做重排。

2. 重排序的原理:从 Bi-Encoder 到 Cross-Encoder

2.1 Bi-Encoder 的“偷懒”本质

向量检索用的双塔结构就是 Bi-Encoder:query 过一个编码器,document 过另一个编码器,两者在整个编码过程中互不知道对方的存在,最后只在向量比较的瞬间碰一次面。

好处很突出:文档向量可以离线预计算,在线只需要算 query 向量,再走一次 ANN 索引就能拿到结果,毫秒级响应。坏处也恰恰在这里,query 和 document 的唯一交互就是最后算那一下距离,好像两个人分别在电话里描述自己对某件事的理解,再由第三方判断谁说得更接近。细节、否定关系、核心实体所在的上下文,都在两个固定长度向量做余弦距离时被压扁了。

更现实的问题是 embedding 向量一般是 768 或 1536 维,这个维度看着不小,但要容纳一篇几百 token 文档的全部语义其实非常紧张。像“maxLifetime 设置过大”这种含义,可能只在段落尾部占十几个 token,很容易被其他高频信息淹没。

2.2 Cross-Encoder 的逐字对话

Cross-Encoder 的思路完全不同。它把 query 和 document 拼成一个 token 序列:

[CLS] query [SEP] document [SEP]

整个序列一起过 Transformer。模型内部的注意力机制能让 query 里的每个词直接观察 document 里每一个词,判断这个文本到底是否真正回应了问题。

比如查询“maxLifetime 设置过大会导致连接什么时候被回收”,Cross-Encoder 在计算“过大”和文档中“连接耗尽后,回收线程需要遍历活跃连接并等待空闲超时”这些词之间的关联时,会明显比另一篇只讲连接池参数的文件打更高的分。这是逐 token 交互带来的能力,不是最终向量那一次比较能比的。

代价也很明确:没法预计算。每个候选文档都要当场和 query 拼起来过一次模型,候选多了耗时线性增长。典型的模型是 BERT-base 结构,输入限制 512 token,单个候选毫秒级,30 个候选大概几十到几百毫秒。这也是为什么 RAG 流水线不能直接全部用 Cross-Encoder,必须先让 Bi-Encoder 把候选缩小到合理规模。

生活里的类比:Bi-Encoder 是看照片认人,快但容易脸盲;Cross-Encoder 是当面问几个问题再判断,慢一点但很难认错。

2.3 本地模型还是 API 服务

重排模型/服务的选型很影响落地方式,我把自己调研过的几个方案整理成一张表。

模型/服务类型语言精度参考延迟表现部署成本
BGE-Reranker-v2-m3Cross-Encoder中英等多语言高,中文友好中,GPU 推理几十毫秒需要 GPU/ONNX 推理服务
ms-marco-MiniLM-L-6-v2Cross-Encoder英文为主中低,CPU 几十毫秒内模型几十 MB,成本极低
Cohere RerankAPI多语言高主要看网络 RTT按量付费,零运维
Jina Reranker APIAPI多语言高主要看网络 RTT按量付费,零运维
基于 LLM 的 Listwise 排序LLM 推理多语言很高但贵高成本很高,中小项目慎选

我的选型建议分三档。第一档是验证可行性阶段,用 MiniLM 本地模型或者对应 API 的免费额度,先确认“重排是否真的有效”,避免一上来就花大钱。第二档是生产环境中文场景,本地 GPU 跑 BGE-Reranker v2/m3,或者改用 API 省运维。第三档是延迟敏感场景,用本地 ONNX 量化版 MiniLM,控制在几十毫秒内。

表格里的“精度参考”只是相对的,真实项目里差距要看数据集。我自己最终选的是 API 方案,因为内部知识库查询量不大,按月付费和零运维对我来说比自建 GPU 服务更划算。

3. LangChain4j 接线:把 Rerank 嵌进 RAG 流水线

3.1 ContentRetriever、ContentAggregator、ReRankingModel 三者的关系

LangChain4j 把 RAG 拆成了几个可插拔的组件,完整路径是:

QueryTransformer -> ContentRetriever -> ContentAggregator -> LLM

每个环节只做一件事。QueryTransformer负责改写或扩展用户问题;ContentRetriever负责召回,可以一个也可以多个,返回一批Content;ContentAggregator拿到所有候选之后做聚合、过滤、排序,最终输出给 LLM 的就是它的结果;ReRankingModel是独立的小接口,对候选文档和 query 一起打分重排。

所以正确的接线方式不是去硬改检索器,而是在ContentAggregator这一层插入重排。LangChain4j 社区提供了一个现成的ReRankingContentAggregator,内部逻辑就是:合并所有 retriever 的候选 -> 调用ReRankingModel.rerank-> 按分数过滤和截断 -> 输出。

注意一点:老版本里出现过ReRankingContentRetriever这种写法,它是把重排包在检索器外层。新版改成ReRankingContentAggregator之后语义更准确,旧版项目如果看到前者,不用慌,核心逻辑一样,只是摆放位置从“检索阶段”挪到了“聚合阶段”。

3.2 手写一个可用的 ReRankingModel:HTTP API 实现

先看接口长什么样:

public interface ReRankingModel { ReRankingResult rerank(List<TextSegment> segments, String query); }

TextSegment就是候选文档片段,query是检索问题,返回结果里带每个候选的原始索引、重排后的顺序和分数。这个接口非常克制,所以各种实现都不难写。

下面是一个调用 Jina Reranker API 的实现,我在生产环境跑过。换 Cohere 只需要改请求体结构,用 Java 原生HttpClient就可以,不需要额外引 SDK。

public class JinaReRankingModel implements ReRankingModel { private static final String API_URL = "https://api.jina.ai/v1/rerank"; private final String apiKey; private final HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); public JinaReRankingModel(String apiKey) { this.apiKey = apiKey; } @Override public ReRankingResult rerank(List<TextSegment> segments, String query) { // 候选超过模型上限时按原顺序截断,避免请求失败 List<TextSegment> candidates = segments.stream() .limit(32) .toList(); String payload = buildPayload(query, candidates); String response = post(payload); return parse(response, candidates); } private String buildPayload(String query, List<TextSegment> candidates) { List<String> documents = candidates.stream() .map(TextSegment::text) .toList(); // 这里用 JSON 序列化库拼请求体, // 生产环境用 Jackson/Gson 生成即可 return """ { "model": "jina-reranker-v2-base-multilingual", "query": %s, "documents": %s, "top_n": %d } """.formatted( toJsonString(query), toJsonString(documents), candidates.size()); } private String post(String payload) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(API_URL)) .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .timeout(Duration.ofSeconds(30)) .POST(HttpRequest.BodyPublishers.ofString(payload)) .build(); try { HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() / 100 != 2) { throw new RuntimeException( "rerank failed, status=" + response.statusCode()); } return response.body(); } catch (Exception e) { throw new RuntimeException("rerank exception", e); } } }

解析响应时最容易踩的坑是index字段。API 返回的results数组里每个元素带index,这个index是候选数组的原始下标。如果你在解析前对segments做了排序或过滤,再拿index去查就会错位。我的做法是解析时先按 index 还原,再统一排序:

private ReRankingResult parse(String responseBody, List<TextSegment> candidates) { try { JsonNode root = MAPPER.readTree(responseBody); JsonNode results = root.path("results"); List<ReRankedContent> ranked = new ArrayList<>(); for (JsonNode node : results) { int index = node.path("index").asInt(); double score = node.path("relevance_score").asDouble(); ranked.add(new ReRankedContent(index, score, candidates.get(index))); } ranked.sort(Comparator.comparingDouble( ReRankedContent::score).reversed()); return new ReRankingResult(ranked); } catch (Exception e) { throw new RuntimeException("parse rerank response failed", e); } }

注意ReRankedContent的构造方法在不同版本里可能有差异,重点理解index和score的处理逻辑,然后按你用的版本 Adjust。top_n我直接设成候选数量,让服务端把所有候选都返回,再由自己的聚合器做阈值过滤和截断。

3.3 本地 ONNX 版重排的工程实现思路

如果不想依赖外部 API,也可以在本地跑 ONNX Runtime,加载 MiniLM 或 BGE Reranker 的 ONNX 模型。核心步骤有四步:

  1. 从 Hugging Face 下载模型仓库,用optimum-cli export onnx转成 ONNX 格式。
  2. 准备 Tokenizer,通常找一个 Java 端可用的分词实现,或者直接调transformers的 tokenizer API 做一次转换。
  3. 对每个候选构造[CLS] query [SEP] doc [SEP]的input_ids、attention_mask。
  4. 交给 ONNX Session 推理,取logits[0]作为该候选的相关性分数。

这一步的工程细节比 API 版本多不少,但好处是完全本地、无外部依赖、延迟可控。无论本地还是 API,ReRankingModel接口都是一样的,切换实现不影响上层代码。

3.4 组装进 AiServices 的完整 RAG 链

最关键的改造发生在ContentRetriever的maxResults和ContentAggregator的配置上。先看完整代码:

EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey(embeddingApiKey) .build(); ContentRetriever retriever = EmbeddingStoreContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(20) // 重点:先扩大召回,交给重排精挑 .build(); ReRankingModel rerankingModel = new JinaReRankingModel(rerankApiKey); ReRankingContentAggregator aggregator = ReRankingContentAggregator.builder() .reRankingModel(rerankingModel) .maxResults(5) .minScore(0.3) .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .rag(RagConfiguration.builder() .contentRetriever(retriever) .contentAggregator(aggregator) .build()) .build();

这里最值得注意的变化是retriever.maxResults(20)。很多教程默认写成 4-5,因为没有重排,不敢多召回。接上重排之后,召回量要主动加大,让ReRankingContentAggregator来承担精排职责。向量检索负责把真正的相关文档圈进候选池,Cross-Encoder 负责把候选池里真正能回答问题的文档顶到前面。

4. 实测案例:内部知识库问答从“能用”到“好用”

4.1 数据准备与 baseline

我在一个项目里做了完整实验。数据源是 500 多篇 Markdown 技术文档,先用DocumentSplitters.recursive(500, 100)切分,得到大约 8000 个文本块。embedding 用多语言模型生成,向量存InMemoryEmbeddingStore。这个存储方案演示够用,生产环境换成 Qdrant、Milvus 或 Pgvector 都行。

无重排基线配置:retriever 只取 Top4,不接 aggregator。先用 30 条手工标注 query 跑评测,MRR 只有 0.40,Top1 命中率 33%。这个数据反映在真实对话里的感觉就是:答案经常引用无关段落,或者逻辑有明显拼凑痕迹。

4.2 接入重排后的直观变化

把 retriever 的maxResults调到 20,接上重排,maxResults=4,minScore=0.3。同样 30 条 query 上,MRR 从 0.40 升到 0.72,Top1 命中率从 33% 升到 60%。

最典型的一个 query 是:“数据库连接池 maxLifetime 设置过大会导致连接什么时候被回收?”

无重排时,Top3 大概是这样:第一篇是《连接池参数大全》,里面对initialSize、maxWait讲得很全,但没回答 maxLifetime 过大带来的孤儿连接问题;第二篇是《生产环境慢 SQL 排查》,相关性更弱;真正包含答案的《连接泄漏排查记录》排到第三位。

重排之后,《连接泄漏排查记录》直接跳到 Top1。原因就是向量检索只看全局语义相似,而 Cross-Encoder 能判断这个上下文里是否真的在回答“maxLifetime 过大”这件事。

另一个间接收益是 LLM 的拒答率下降了。之前模型经常把两个不相关段落拼在一起一本正经地回答,重排之后相关段落集中在靠前位置,答案的引用链明显干净很多。

4.3 分数阈值怎么定

minScore这个参数我没有照搬网上常见的 0.5。不同 Rerank API/模型的分数分布差异极大,照搬阈值会踩大坑。

Jina 的 v2 模型在真实场景里分数普遍偏高,但“相关文档”的分数也经常落在 0.3 到 0.5 之间。我跑了几天日志,统计出正确被引用文档的最低分是 0.27,错误文档的最高分是 0.61。固定阈值 0.5 会把不少真正相关的文档直接过滤掉,0.3 是根据分位数试出来的。

更稳的做法是:不设死阈值,只取 TopN。如果候选整体分数都很低(比如最高分低于 0.15),就让系统返回“无相关上下文”,在 Prompt 里告诉 LLM 不要硬答。这比硬塞一个低质量文档进去安全得多。

生产环境我还会在 aggregator 外层加一层兜底逻辑:重排后 top1 分数低于阈值,就返回空内容。LangChain4j 的 RAG 对这个场景是支持的,关键在于触发空结果的时候,Prompt 模板里要给 LLM 一个明确的“无可信依据”信号。

5. 工程决策:候选数量、阈值、缓存与降级

5.1 为什么我坚持“先多召回、再重排”

有一种偷懒做法是把向量检索 TopK 从 4 缩小到 2,觉得这样噪音就少了。实测这种方式经常误伤。很多真正相关的文档语义距离并不近,向量排序靠后,提前截断就直接漏掉了。

正确思路是反过来的。向量检索宁可多召回一些“可能相关”的候选,把判断交给更聪明的 Cross-Encoder。我测试过不同候选池大小的效果:

召回数量重排后 Top1 命中率重排总耗时(API)
545%约 150ms
1050%约 300ms
2060%约 600ms
5062%约 1.3s

候选从 5 扩到 20,收益非常明显;再往上到 50,收益增速变缓,延迟却线性上升。20-30 是大多数场景的甜点区间。

5.2 多路召回 + 重排的统一融合

向量检索只代表“语义相似”,BM25 或全文检索则能覆盖字面匹配场景。现在我的检索层是双路:向量召回 15 条,全文检索召回 15 条,合并后去重,统一交给同一个 Rerank。

Rerank 天然是打分融合器,它把两套量纲完全不同的分数全部抛弃,只按自己的相关性判断重排,这省掉了为“向量分和 BM25 分怎么加权”写一堆调参逻辑的麻烦。

实测对比:单路向量 + Rerank 和双路召回 + Rerank,Recall@10 大约涨 7 个百分点,MRR 涨 5 个点。对于内部知识库这种文档量级,这个收益非常值得。

5.3 缓存、限流、降级三件套

重排服务是外部依赖,必须做容错。我维护的 Java 服务里做了三件事。

缓存以(query, docId)为 key,存 Rerank 分数。热点问题一天内被反复咨询,LRU 缓存可以省掉大量重复调用。但要先对 query 做归一化:去标点、小写、去掉常见口头语,否则缓存命中率会很难看。

限流用固定线程池加有界队列。线程数压到 5-10,宁可队首等待,也不要把上游 API 打挂。队列满时走降级逻辑,不要无限堆积。

降级是必须的:重排服务不可用时,自动回到“候选按原顺序截断”的策略。实现方式是把ReRankingContentAggregator包装成带兜底的ContentAggregator,捕获异常后直接返回原候选并截断到 Top4。

public class FallbackReRankingAggregator implements ContentAggregator { private final ContentAggregator delegate; private final ContentAggregator fallback; @Override public List<Content> aggregate(List<Content> contents, String query) { try { return delegate.aggregate(contents, query); } catch (Exception e) { log.warn("rerank unavailable, fallback to truncate", e); return fallback.aggregate(contents, query); } } }

5.4 延迟预算怎么控制

线上版本 P95 响应时间要求 4 秒以内。向量召回一般在 50-200ms,Rerank 是主要新增开销。不同部署形态的耗时不一样:

  • 本地 MiniLM CPU:单文档 2-5ms,30 个候选约 100ms。
  • BGE-Reranker GPU:单文档 1-3ms,30 个候选约 50ms。
  • API 服务:网络 RTT 200-500ms,推理时间另算,30 个候选可能到 1-2 秒。

所以 API 版重排一定要配合缓存和候选截断,并把重排单独设置超时。我的重排超时是 3 秒,超时直接按降级处理,不回滚整个请求。

6. 踩坑记录:高分错误结果与分数归一化陷阱

6.1 一次被“高分错误结果”支配的排查

有几天线上数据很奇怪:Rerank 分数很高、明显不该排前面的文档,却稳居第一。我按下面的链路排查:

  1. 打印聚合前后的完整候选列表,确认 Rerank 分数确实把错误文档排到第一。
  2. 单独用 curl 把同一个 query 和 document 发给 API,分数又恢复正常。
  3. 对比两边文本,发现在 Java 端发送的 documents 数组里出现了大量重复段落。
  4. 最终定位到根因:知识库切分时,模板页脚被重复切进多个文本块,embedding 存储里重复向量太多,检索召回的候选一堆内容几乎相同。Rerank 对同一内容的重复文档反复打分,互相干扰。

修复方式是在索引前对文档块做一次 MD5 去重,保留第一份;同时给切分器加页面元数据过滤,把页脚模板剥掉。

这个教训很典型:重排只能把现有候选排好,如果候选池本身被垃圾污染,效果再好也救不回来。数据层面的治理,永远是 RAG 项目绕不开的功课。

6.2 分数归一化的陷阱

不同 Rerank 模型/API 的分数分布差异极大,不能直接跨模型做阈值或对比。MiniLM 的原始输出是 logit,范围大概是 [-5, 5];Jina 的relevance_score是 sigmoid 后的 0-1,实际集中在 0.3-0.8。把 MiniLM 的 3.2 分和 Jina 的 0.72 分直接划等号,是没法比较的。

建议只做“同模型内的相对排序”。如果要把分数存下来分析,先对当前候选集做一次 min-max 归一化:

double min = scores.stream().min().orElse(0); double max = scores.stream().max().orElse(1); double normalized = (score - min) / (max - min);

注意这个归一化只对当下这一批候选有意义,不能跨请求使用。两个请求之间没有可比性,因为候选集不同。

6.3 Query 改写与重排的顺序关系

LangChain4j 的QueryTransformer可以在检索前改写用户问题,比如压缩对话历史、把口语改成更正式的检索式表达。但如果你开了 QueryTransformer,Rerank 拿到的 query 也会是改写后的版本。

我测试过几种组合:改写 query 同时给 retriever 和 rerank,改写 query 只给 retriever 而 rerank 用原 query,原 query 直接给两端。差别不小。在我这个数据集上,rerank 用原 query 的效果更好,因为 Cross-Encoder 自己有能力处理口语化表达,而改写后的 query 往往会丢掉一些原词细节。

这不是普适结论,但值得你做个 A/B。关键提醒是:重排用的 query 不要悄然变成“改写后形态”,要明确测试一下再决定。

6.4 调试日志与对照实验

RAG 优化工作中,日志是最容易被低估的环节。我习惯在ContentAggregator前后各打一行结构化日志:前一行记录检索原始分数和排名,后一行记录 Rerank 后的分数和排名。这样任何排序异常都能很快定位到是召回问题、Rerank 问题还是阈值问题。

更稳妥的做法是在配置里保留一个开关,随时切回“无重排基线”。很多优化效果不明显,往往是因为没有做严格对照实验。开了重排之后,回头把召回量调大、阈值调好、日志打足,你才能从日志和指标里看到这套水磨工夫的真实价值。

我把这个开关做成:

ContentAggregator finalAggregator = useRerank ? new FallbackReRankingAggregator(rerankAggregator, truncateAggregator) : truncateAggregator;

功能上线后,两套策略跑同一批线上 query,日志和指标一对,谁优谁劣一目了然。

最后分享两个小建议

第一,做重排实验不要一上来就花大钱接最贵的 API 服务。先用本地 MiniLM 这种几十 MB 的模型,把“重排是否有效”验证清楚,再决定要不要上更强模型。我见过不少项目第一步就接最贵的 Rerank API,效果提升有限,于是得出“重排没用”的结论。其实问题往往出在候选召回质量、切分粒度、阈值都没调好,换再强的模型也救不了基础问题。

第二,重排不是银弹。embedding 模型决定“能不能找到”,重排模型决定“找得准不准”,但两者都救不了“知识库本身乱”。先花时间把文档去重、去模板、切分合理,再接重排,你会看到组件的价值被放大很多。重排是我在 RAG 项目里做过成本最低、收益最明显的一次优化,如果只推荐一个改动,我会推荐先试试这个。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 6:50:02

Fortran语法介绍(一):用TaoToken统一Key跑通第一个数值计算示例

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 6:49:08

aiogram 中 Telegram Update 对象全解析:字段、事件类型与分发机制

后端即时通讯API设计 【免费下载链接】aiogram aiogram is a modern and fully asynchronous framework for Telegram Bot API written in Python using asyncio 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ai/aiogram 点击查看 免费下载 本文基于 aiogram 官方 AP…

作者头像 李华
网站建设 2026/10/13 6:47:56

共享单车大数据毕业设计:从Hive到可视化看板全流程实践

共享单车这个题目&#xff0c;在毕业设计里算是长盛不衰的经典选题。每年都有大量学生选它&#xff0c;但坦率讲&#xff0c;真正能把这个题目做出彩的并不多。大多数人的做法是&#xff1a;拉一份数据&#xff0c;用Pandas跑几个统计图&#xff0c;再加个地图撒点&#xff0c;…

作者头像 李华