Java开发者转型AI工程化Week 5:让RAG学会思考、看见与自检

Java开发者转型AI工程化Week 5:让RAG学会思考、看见与自检------从固定管道到会决策的知识系统

作者: 一位正在转型的Java开发者

时间: 2026年5月

系列: Java开发者AI工程化转型记录(Week 5/9)

标签: Agentic RAG, RAGAS, 多模态RAG, CLIP, 评估体系, RRF


前言:Week 4留下的三个"没解决"

Week 1建立认知,Week 2补齐生产级能力,Week 3让AI学会自主行动,Week 4给这个会行动的智能体配了一座专业图书馆------PGVector存向量,TokenTextSplitter做切分,混合检索加RRF融合,跑通了"检索→增强→生成"的完整链路。

但复盘时我写下三个没解决的问题,它们指向同一个缺陷:这条管道是写死的。它不会判断"这个问题需不需要检索",它看不懂PDF里的财务报表和架构图,它也不知道自己答得好不好------唯一的反馈来自用户投诉。

如果说Week 4是"给AI配一座图书馆",Week 5就是"让图书管理员学会思考、看得懂图表,并且能给自己的服务打分"。


Day 1:Agentic RAG------当检索学会了决策

三代演进:从"检索完就生成"到"边检索边思考"

Agentic RAG这个词很热,剥掉包装它指的就是一件事:把LLM从"生成器"变成"控制器"

维度 Naive RAG Advanced RAG Agentic RAG
检索次数 固定 1 次 固定 1-2 次 按需 0-N 次
是否自我评估 有,且决定下一步
是否有纠错回路 有(改写 / 换源 / 降级)
典型延迟 1-2s 2-3s 2-5s

核心洞察 :三代演进的本质不是"检索得更多",而是谁来决定检索多少 。前两代检索次数是代码里的常量,到了Agentic RAG它变成一次LLM判断的输出。所以max_iterations=3不是随手写的数字,而是准确率收益与P99延迟之间的权衡线。

四种范式:Router、CRAG、Self-RAG、Adaptive RAG

范式 它解决什么 代价(额外LLM调用)
Router 该走哪条检索路径 1 次(可用规则替代)
CRAG 检索结果够不够好 1 次评估 + 可能的 N 次重检
Self-RAG 到底需不需要检索 1 次判断
Adaptive RAG 问题有多复杂,用哪档策略 1 次分级

关键收获 :四类里Router性价比最高------它能用关键词映射和正则兜住80%的case,只在未命中时才调LLM,这也是Day 6"两级路由"的由来。Self-RAG看起来最优雅,但每次查询都要先花一次调用判断"要不要检索",在FAQ这类高频简单场景下反而是亏的。

核心实现:route → retrieve → critique → correct

这条闭环分四步:路由选策略、按策略检索、评判检索质量、不达标则纠正后重来。其中DIRECT_ANSWER那条虚线旁路是我特别加的------遇到"你好"这类问题直接跳过整条检索链路,省下的不只是延迟,还有真金白银。

路由策略定义成枚举,fromString做兜底------LLM返回的字符串永远不能直接信任

scss 复制代码
public enum RetrievalStrategy {
    VECTOR_SEARCH,
    KNOWLEDGE_GRAPH,
    WEB_SEARCH,
    DIRECT_ANSWER,   // 无需检索,直接回答
    HYBRID,
    RE_QUERY,        // 改写查询后重新检索
    CLARIFICATION;   // 信息不足,需要向用户澄清

    public static RetrievalStrategy fromString(String s) {
        return Arrays.stream(values())
            .filter(v -> v.name().equalsIgnoreCase(s))
            .findFirst()
            .orElse(VECTOR_SEARCH);  // 兜底,绝不抛异常
    }
}

主循环是"路由 → 检索 → 评判 → 纠正"的闭环:

ini 复制代码
public RAGResponse process(String question) {
    RouteDecision route = routingAgent.route(question);
    if (route.strategy() == RetrievalStrategy.DIRECT_ANSWER) {
        return generate(question, List.of());  // 省掉整条检索链路
    }

    List<Document> docs = retrieve(question, route.selectedTools());
    Critique critique = critiqueAgent.critique(question, docs);

    int iteration = 0;
    while (critique.requiresCorrection() && iteration++ < maxIterations) {
        docs = retrieve(critique.correctedQuery(), route.selectedTools());
        critique = critiqueAgent.critique(question, docs);
    }
    return generate(question, docs);
}

关键设计 :这是一版骨架实现 ------critiqueAgent依赖写死的阈值,纠正手段也只有改写查询一种(真实场景还应包括切换数据源、放宽阈值、降级直答)。但骨架的价值在于定下了控制流:检索不再是管道里的一段,而是一个可被评判、可被重试的"动作"。


Day 2:RAG评估体系------给"感觉不错"定一个刻度

RAGAS四指标:忠实度、答案相关性、上下文精确率、上下文召回率

这一天的素材有99KB,核心却只有四个指标:两个管生成,两个管检索

指标 它到底在问什么 怎么算 我的阈值
Faithfulness 答案有没有编造 有支撑的事实数 ÷ 总事实数 ≥ 0.85
Answer Relevancy 答没答到点上 反推N个问句与原问题的相似度均值 ≥ 0.80
Context Precision 有用的排得靠前吗 位置加权的精确率 ≥ 0.70
Context Recall 该找的都找到了吗 被覆盖的关键点 ÷ 总关键点 ≥ 0.80

Faithfulness的算法值得说清楚------它不是相似度分数,而是计数比值

scss 复制代码
// 1. 把答案拆成一条条可验证的"陈述"
List<String> claims = extractClaims(answer);
// 2. 逐条回原文比对,能被上下文支撑的才算数
long supported = claims.stream()
    .filter(c -> isSupportedBy(c, retrievedContext))
    .count();
// 3. 忠实度 = 有支撑的 / 全部
return claims.isEmpty() ? 1.0 : (double) supported / claims.size();

深层感悟 :这里藏着两个坑。一是claims.isEmpty()返回1.0------空答案等于"完全忠实" ,所以Faithfulness必须和Answer Relevancy配对,否则模型学会说"我不知道"就能拿满分。二是extractClaimsisSupportedBy各要一次LLM调用,评估成本会吃掉优化收益

检索侧四指标与综合评分等级

RAGAS管生成,检索侧另有四个指标:

指标 公式要点 关心什么
Recall@k 召回相关文档 ÷ 全部相关文档 漏没漏
Precision@k 前k个里相关的 ÷ k 噪声多不多
MRR 1/rank 的均值 第一个正确答案排第几
nDCG DCG ÷ IDCG,DCG = Σ rel_i / log2(i+1) 排序整体好不好

nDCG里那个log2(i+1)是位置衰减------排第1位的相关文档,价值远高于排第10位的。综合分我按等权处理:

ini 复制代码
double overall = 0.25 * contextRecall
               + 0.25 * contextPrecision
               + 0.25 * faithfulness
               + 0.25 * answerRelevancy;
等级 分数 含义
A+ / A ≥0.95 / ≥0.90 可放心上线
B+ / B ≥0.85 / ≥0.80 可用,有优化空间
C / D ≥0.70 / ≥0.60 需要针对性优化

核心洞察 :等权不是因为四个指标一样重要,而是因为项目早期我并不知道哪个更重要。等业务跑起来、看到真实分布之后,权重才该被重新分配------这正是Day 6"双Grader"要解决的问题。

基线 + 漂移检测:评估不是一次性动作

上线前跑一次得到基线,之后按天跑,用JS散度比较"今天的查询分布"与"基线分布",超过阈值(我设的0.15)就告警。

关键收获没有基线的评估分数毫无意义。0.82分是好是坏,取决于昨天是0.85还是0.75。漂移检测解决的是另一个问题------提问方式整体变了(比如新功能上线后问题类型迁移)时,旧基线和Golden Dataset会一起失效,这时分数下降不是系统变坏了,是尺子不准了。


Day 3:多模态RAG------让知识库看得懂图表

CLIP双塔:把图像和文本压进同一个坐标系

多模态检索的地基是CLIP,结构非常"Java友好"------两个Encoder,一个共享向量空间

ini 复制代码
// 文本塔与图像塔,输出同维度向量,可直接算相似度
float[] textVec  = textEncoder.encode("一只在晒太阳的猫");
float[] imageVec = imageEncoder.encode(imageFile);
double score = cosineSimilarity(textVec, imageVec);

训练用对比学习:一个batch里N个图文对,算出N×N相似度矩阵,对角线是正样本,其余全是负样本,损失目标就是把对角线拉近、非对角线推远。

深层类比 :这套结构和推荐系统里的双塔召回模型是同一个东西------用户塔、物料塔,各自编码到同一空间,向量检索做粗排。想明白这点,CLIP就不再神秘:它只是把"用户-物料"换成了"文本-图像",工程实现上几乎没有新东西。

版面分析与"表格必须转Markdown"

这张图是整条流水线:先做版面分析,切成文本块、表格、图片三类再分头处理 ,最后汇入同一个检索空间。表格是隐藏BOSS,我在这里踩了最实在的坑。直接抽成文本是这样一串字符:产品 单价 库存 iPhone15Pro 8999 150 MacBookAir 7999 80;转Markdown后则保留了结构:

yaml 复制代码
| 产品 | 单价 | 库存 |
|------|------|------|
| iPhone 15 Pro | 8999 | 150 |
| MacBook Air | 7999 | 80 |

工程挑战 :第一种形式在向量空间里几乎无法被召回------"iPhone的价格是多少"和"iPhone15Pro 8999 150"没有任何语义对齐关系。而Markdown保留了列名与值的对应关系,LLM能读懂,Embedding也能捕捉。这条经验在本系列还会遇到两次。

图片必须有Caption,否则等于没入库

图片处理分三步:视觉模型生成描述 → 把描述当文本向量化 → 原图另存对象存储,元数据里只留引用。

arduino 复制代码
// 图片入库前必须先"翻译"成文本
String caption = visionModel.describe(image);  // "架构图:API网关下挂三个微服务..."
Document doc = new Document(caption, Map.of(
    "modality", "image",
    "objectKey", objectStore.put(image),  // 原图另存
    "pageNo", pageNo
));
vectorStore.add(List.of(doc));

关键设计 :为什么不让图像向量直接进检索?存储成本高数倍检索到了也说不清为什么命中专业图表上几乎失效(CLIP在自然图片上很好,在架构图和财务报表上语义损失严重)。先生成描述再向量化是个又土又有效的折中,Week 8 Day 3会把它升级。


Day 4:四个Agent怎么配合------可用的Agentic RAG实现

四Agent分工与Grader的双判据

Day 4把骨架落成了可运行的实现,四个Agent各司其职:

Agent 职责 输入 输出
Router 选检索策略 用户问题 RouteDecision
Retriever 按策略取回文档 问题 + 策略 List<Document>
Grader 评判检索质量 问题 + 文档 GradingResult
Generator 生成最终答案 问题 + 文档 RAGResponse

Grader用了双判据,这是我在这一天最认可的设计:

ini 复制代码
double avgScore = gradings.stream()
    .mapToDouble(DocumentGrading::getRelevanceScore)
    .average().orElse(0.0);

long highQualityCount = gradings.stream()
    .filter(g -> g.getRelevanceScore() >= qualityThreshold)
    .count();

boolean isSufficient = avgScore >= qualityThreshold && highQualityCount >= 2;

感悟 :为什么必须两个条件?因为只看均分会被"一篇高分+三篇垃圾"骗过 ------一篇0.95加三篇0.2,均值0.39看着还行,实际只有一条能用。highQualityCount >= 2保证了"至少两个独立来源能支撑答案",这正是RAG避免幻觉的底气。

调试洞察:LLM返回的JSON一定要有正则兜底

这一天的调试时间,大半花在解析LLM返回的JSON上。三类故障反复出现:被 ```json 围栏包住、前后加了客套话、字符串值里漏转义引号。于是有了三段式兜底:

arduino 复制代码
private String extractJson(String raw) {
    String s = raw.replaceAll("```json|```", "").trim();       // 1. 去围栏
    int start = s.indexOf('{'), end = s.lastIndexOf('}');
    if (start >= 0 && end > start) return s.substring(start, end + 1);  // 2. 截取
    return fallbackByRegex(s);                                  // 3. 正则兜底
}

调试洞察永远不要相信LLM会返回干净的JSON。哪怕Prompt里写了"只返回JSON",temperature稍微一高它就开始加解释。兜底必须是"三层递减"而非"一次尝试",而且第三层的正则要针对具体字段各写一个,别指望一个通用正则解决所有情况。

ExecutionTrace:让每一次决策可回放

Agent系统最怕的不是出错,是出错后无法归因。办法是把每次决策都记进Trace:

字段 记什么 用来回答什么问题
routeDecision 选了哪个策略、为什么 是路由选错还是检索没找到
retrievalSteps 每次检索的策略与得分 第几次才找到的
gradingResult 均分、高质量数、是否达标 纠正因何触发
correctionApplied 改写前后的query 改写有没有把意思改偏

关键感悟Agent系统没有Trace就是黑盒。我在改写逻辑上吃过亏------"如何配置VPN"被改写成"VPN设置步骤",检索结果反而变差。有了Trace,对比改写前后的query和得分分布,问题一眼可见;没有Trace只能靠猜。


Day 5:把指标写成可插拔的Java代码

RAGSystemInterface:让评估器跨项目复用

评估框架最容易犯的错,是和某个具体RAG实现绑死。素材里用一个函数式接口解耦:

arduino 复制代码
public interface RAGSystemInterface {

    /** 被测系统只需实现这一个方法 */
    RAGResult query(String question);

    class RAGResult {
        public String generatedAnswer;
        public List<String> retrievedDocuments;
        public TokenConsumption tokenConsumption;
    }
}

// 调用方:不管底层是 LangChain4j 还是 Spring AI
EvaluationReport report = evaluator.runFullEvaluation(myRagSystem::query);

设计哲学 :这是我本周最满意的设计。评估框架不该依赖任何具体RAG实现,否则每换项目就要重写适配器。接口只有三个字段------答案、检索文档、Token消耗------恰好是RAGAS四指标所需的全部输入,多一个都多余。

Context Precision:语义相似 ×0.7 + 关键词命中 ×0.3

上下文精确率是两条路径的加权:语义相似度 × 0.7 + 关键词重叠率 × 0.3

深层感悟 :这个0.7/0.3不是拍脑袋。纯语义会漏掉专有名词的精确匹配 ------PGVectorMilvus在向量空间里可能很近,但用户问的就是前者;纯关键词扛不住同义改写------"怎么装"和"安装步骤"一个字不重叠。权重应该跟着业务词汇特点调:技术文档专有名词多,关键词权重就该更高。

并行评估:CompletableFuture + 独立线程池

评估一个测试集要跑上百次查询,串行要几十分钟:

scss 复制代码
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (TestCase testCase : dataset.getTestCases()) {
    futures.add(CompletableFuture.runAsync(
        () -> evaluateTestCase(testCase, ragSystem, metricScores, ...),
        evaluationExecutor   // 专用线程池,不是业务线程池
    ));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

工程挑战评估任务绝不能复用业务线程池。评估是CPU加IO混合的长任务,一旦和在线请求抢线程,延迟立刻劣化。线程池要单独配置,评估接口还要限流,否则一次全量评估就能把API配额打光。


Day 6:企业级智能知识库------把五天的零件装成系统

两级路由:正则快匹配优先,未命中才唤醒LLM

Day 6是综合实战,素材152KB,我只挑三个最值得写的点。第一个是两级路由------用规则挡住大部分请求,把LLM留给真正需要的case:

kotlin 复制代码
// 第一级:正则快匹配,零成本、零延迟
if (query.matches(".*(比较|对比|区别|联系).*")) {
    return new RoutingDecision(MULTI_HOP, 0.9);
}
if (query.matches(".*(多少|几个|统计|百分比).*")) {
    return new RoutingDecision(KEYWORD_SEARCH, 0.8);
}
// 第二级:规则未命中,才调用LLM做意图分类
return llmIntentClassifier.classify(query);
路由方式 覆盖请求比例 单次成本 延迟
正则快匹配 约 90% 0 < 1ms
LLM意图分类 约 10% 1次调用 300-800ms

设计哲学 :背后是一个反复出现的模式------规则能解决的,绝不要花钱调LLM(Week 9的图表选型和异常检测前置还会再遇到两次)。90%的请求用规则兜住,整体LLM调用量直接降到十分之一,这是所有成本优化里投入产出比最高的一招。

双Grader:相关性守前门,幻觉守后门

Grader 时机 判什么 不达标怎么办
RelevanceGrader 检索后、生成前 这段上下文和问题相关吗 过滤掉,阈值0.6
HallucinationGrader 生成后、返回前 答案的说法都在上下文里吗 >0.5 触发重查

感悟 :两道都要,因为它们防的是两类完全不同的错误。相关性Grader防"垃圾进"------检索到一堆无关文档,LLM再强也只能胡编;幻觉Grader防"垃圾出"------上下文明明是对的,模型自己加了料。只装前者,你会得到一个"检索很准但答案仍在编"的系统。

我改的坑:BM25的IDF是个占位符

混合检索里有BM25,顺着代码读下去我发现了这个:

arduino 复制代码
/**
 * 计算 IDF(逆文档频率)
 */
private double calculateIDF(String term) {
    // 简化实现
    return Math.log(1 + 1); // 假设 N=1, n_qt=0
}

调试洞察Math.log(1 + 1)恒等于ln2 ≈ 0.693每个词的IDF一模一样,BM25退化成了纯词频统计。后果很具体:出现十次的"的"和出现一次的"PGVector"被同等对待。这行代码本身没错------它是个诚实的占位符,注释也写清了"简化实现"------但它藏在一个看起来能跑的系统里。修法不难,维护一份词项到文档频率的映射:

arduino 复制代码
private final Map<String, Integer> docFrequency = new ConcurrentHashMap<>();
private volatile int totalDocs = 0;

private double calculateIDF(String term) {
    int nqt = docFrequency.getOrDefault(term, 0);
    return Math.log(1 + (double)(totalDocs - nqt + 0.5) / (nqt + 0.5));
}

补完再用JMH压一遍,顺带把HNSW三个参数也调了。这提醒我一件事:骨架代码里最危险的不是报错的部分,而是"能跑但结果不对"的部分------前者编译期就被抓出来,后者要等上线三个月才发现检索一直是个摆设。


Week 5复盘:三个核心突破

1. 从"固定管道"到"LLM当大脑"

Week 4的RAG是写死的管道,Week 5把LLM放到了控制器的位置:判断要不要检索、选哪种策略、评估质量、决定纠错。代价是延迟和成本,所以max_iterations=3是权衡的结果。管道是确定的,Agent是决策出来的------中间隔着一次范式转换。

2. 从"感觉还行"到"RAG质量可量化"

八个指标加一个综合分,让"这个RAG好不好"从主观判断变成可讨论的数字。但门槛不在算分而在建基线------0.82本身毫无意义,有意义的是"相比上周的0.75提升了0.07"。更根本的认识是:评估本身也花钱,所以采样、并行、独立线程池是必备组件而非优化项。

3. 从"只读文本"到"看得见非文本内容"

多模态真正的难点不是"加一个视觉模型",而是把非文本翻译成文本:表格转Markdown才有语义结构,图片生成Caption才能被检索,视频拆关键帧加音频转录才能按时间戳定位。CLIP的统一向量空间提供了理论优雅,但工程上最有效的往往是最朴素的做法。


待解决的深层问题

1. 评估器本身的可靠性由谁评估

Faithfulness和Answer Relevancy都依赖LLM当裁判,而LLM-as-Judge已知会偏向更长的答案、偏向与自己风格相近的表述。这个偏差怎么量化?用另一个模型来评,那又是谁来评那个模型?我目前只做到"人工抽检 + 一致性系数",离自动化还差得远。

2. 多模态统一向量空间在专业内容上的语义损失

CLIP在自然图片上表现优秀,但知识库里塞满了架构图、流程图和财务报表。这类图的语义恰恰藏在细节里------箭头的方向、数字的对应关系------而这些是对比学习损失不关心的。目前只能靠详尽Caption弥补,但Caption本身就是一次有损压缩。

3. 迭代次数与延迟预算的天然矛盾

Agentic RAG要求迭代最多3次,同时要求P99 < 5s。按单次检索加评判约1.5s算,三次就到4.5s,留给生成的余量几乎没有。出路只有并行化(多策略同时检索)和缓存(相似查询复用上轮结果),但两者都会引入新的复杂度。


给同行者的建议

如果你也在做RAG,我最想分享的是:先建评估,再谈优化

没有刻度的时候,所有优化都是自嗨------你改了分块大小,觉得"好像好一点了",然后就上线了。有了八指标和一条基线,你才知道那次改动是提升0.03还是劣化0.05。更微妙的是,评估会反过来指出方向:Context Recall低而Faithfulness高,问题在检索;两个都低,问题在文档处理;Context Precision低,问题在排序。指标会告诉你该改哪里,不用猜

先建评估,再谈优化------没有刻度的优化都是自嗨。

Week 5解决的是"怎么让RAG变好",Week 6要面对一个更难的问题------这些"会查资料的Agent",该怎么组织、怎么度量、怎么接入

相关推荐
一个Ai的杂货铺42 分钟前
exit 0 也会撒谎:一个"绿灯盲跑"了一个月的定时任务,尸检报告
后端
北漂EDA码路44 分钟前
现代 C++20:move、forward 与完美转发,EDA 大型程序为什么离不开它
后端
泡海椒1 小时前
JQuick-Java快速入门教程:轻量级规则引擎环境搭建与首个脚本运行实战
后端
stark张宇2 小时前
分布式事务最全图解(6种方案):从强一致的2PC到最终对账,彻底搞懂数据一致性
分布式·后端
IT毕设实战小研2 小时前
基于大数据的WTA职业网球赛事演变与竞技格局数据可视化分析
大数据·后端·爬虫·python·信息可视化·课程设计
IT_陈寒2 小时前
Java字符串判等踩坑记:==和equals真的不能乱用
前端·人工智能·后端
小马过河R2 小时前
微信小程序自定义登录态维护:从入门到生产级落地
后端·微信小程序·小程序·架构·登录态
计算机学姐3 小时前
基于SpringBoot的高校爱心慈善管理系统
java·vue.js·spring boot·后端·spring·tomcat·mybatis
vx+_bysj68693 小时前
springboot 旅行小程序97353
spring boot·后端·小程序·旅行小程序