🚓 分诊台与拆题术:让 RAG 学会判断和规划

写在前面:上篇我们搭了一个能跑的朴素 RAG,也认领了它的五个硬伤。这篇开始打补丁------针对其中两个最"伤智商"的问题:所有问题都无脑检索 (1+1 也要翻书)和多跳问题搜不准(答案不在任何一段原文里,要跳好几次)。readme 给的药方很明确------前者靠"llm 来判断简单",后者靠"llm 规划能力,拆分,分步骤"。两个 demo 文件,一个教 RAG 分诊,一个教 RAG 拆题。以下所有代码均来自课堂真实文件。


一、补丁一:给 RAG 装一个分诊台

医院的分诊逻辑

去医院看病,进门先遇到分诊台------不是所有病人都直接推进手术室。护士问两句:

  • "挂个号开个药" → 普通门诊
  • "胸口疼、呼吸困难" → 急诊

分诊的价值在于把资源用在对的地方。对所有病人一律上全套检查,是浪费,也是灾难。

朴素 RAG 就是那种"全部上全套"的医院------你问"1+1 等于几",它也要去《天龙八部》里搜五段原文出来。readme 的判断:

"所有问题都走 RAG 检索?简单问题不需要检索,浪费资源(token 和 检索,增强流程)。1+1=?两个分支,一个简单的问题,一个复杂的问题。llm 来判断简单?"

"llm 来判断" ------ 这四个字就是解法。

路由状态:先加一个"路线"字段

rag-query-router.mjs 比上篇的朴素版本多了一个关键字段:

javascript 复制代码
const GraphState = Annotation.Root({
    question: Annotation,
    k: Annotation,
    strategy: Annotation,      // ← 新增:策略
    routeReason: Annotation,   // ← 新增:判断理由
    documents: Annotation,
    generation: Annotation,
})

strategy 存的是"走哪条路",routeReason 存的是"为什么这么走"------后者不只是为了好看,它是可观测性的基础。 出了问题你能看到模型当时是怎么想的。

路由节点:用结构化输出做判断

javascript 复制代码
const RouteSchema = z.object({
    // 枚举
    strategy: z.enum(["simple", "complex"]),
    reason: z.string(),
});

这里用了结构化输出 + Zod 枚举------前面专门学过的知识,现在派上大用场了。

z.enum(["simple", "complex"]) 意味着模型只能在这两个值里选一个。这就避免了模型回答"可能是简单的吧"这种没法用代码判断的模糊输出。

javascript 复制代码
const routeQuestionNode = async (state) => {
    console.log('___ROUTE-QUESTION___');
    // 结构化输出
    const router = model.withStructuredOutput(RouteSchema);
    const route = await router.invoke(`
    你是问答路由器,请判断用户问题是否需要外部检索。
    规则:
    - simple 常识问答、简短定义、无需特定小说细节即可回答。
    - complex 需要《天龙八部》具体情节、人物关系、章节事实、原文细节或证据支持。
    
    用户问题: ${state.question}
    `)

    console.log(`路由策略:${route.strategy} ${route.reason}`)
    return {
        question:state.question,
        k:state.k,
        strategy: route.strategy,
        routeReason: route.reason,
    }
}

这就是"分诊护士"的全部工作。 几句 prompt 说清判断标准,剩下的交给模型。

prompt 里的两句规则设计得很精准:

策略 判断标准
simple 常识问答、简短定义、无需特定小说细节
complex 需要《天龙八部》具体情节、人物关系、章节事实、原文细节或证据

关键词是"无需特定小说细节 "和"需要原文细节或证据"------把判断标准落在"是否需要查资料"这个本质问题上,而不是落在"问题长短"这种表面特征上。

这才是好的路由 prompt:给判断依据,不给判断捷径。

两条分支:直答 vs 检索

javascript 复制代码
const directAnswerNode = async (state) => {
    console.log('___DIRECT-ANSWER___');
    process.stdout.write("\n [AI 回答(流式)] \n")
    let generation = "";
    const stream = await model.stream(`你是一个中文回答助手,
    请简洁回答问题。
    问题:${state.question}
    `)
    // ... 流式输出 ...
    return {
        question:state.question,
        k:state.k,
        strategy: state.strategy,
        routeReason: state.routeReason,
        documents: [],          // ← 注意:没有文档
        generation: generation,
    }
}

注意 documents: [] ------直答节点不产生任何文档。这个细节很重要:它明确表达了"这条路没检索",而不是含糊地留空。

对比一下两条路的 prompt:

节点 prompt 特点
directAnswerNode "你是一个中文回答助手,请简洁回答问题" ------ 轻量、直接
generateNode(RAG 那条路) 五条防幻觉要求 + 拼接的 context ------ 重装备、严谨

两条路的 prompt 完全不同 ------这也是分诊的意义:不是省了一次检索,而是给不同复杂度的问题配不同规格的处理流程。

编排:一个条件边搞定

javascript 复制代码
const decideNext = (state) => {
    return state.strategy === "simple" ? "direct_answer" : "retrieve";
}

const graph = new StateGraph(GraphState)
    .addNode("route_question", routeQuestionNode)
    .addNode("direct_answer", directAnswerNode)
    .addNode("retrieve", retrieveNode)
    .addNode("rag_generate", generateNode)
    .addEdge(START, "route_question")
    .addConditionalEdges("route_question",decideNext, {
        direct_answer: "direct_answer",
        retrieve: "retrieve",
    })
    .addEdge("retrieve", "rag_generate")
    .addEdge("direct_answer", END)
    .addEdge("rag_generate", END)
    .compile()

流程图长这样:

sql 复制代码
                    ┌→ direct_answer → END      (简单问题,直接答)
START → route_question
                    └→ retrieve → rag_generate → END   (复杂问题,检索再答)

上篇那条"一条道走到黑"的直线,现在有了岔路口。

条件边 addConditionalEdges 前面学过------decideNext 返回 "direct_answer" 还是 "retrieve",决定了走哪条。

测试它:一个"不该检索"的问题

javascript 复制代码
const question = 'js写一个add函数';
const k = 5;

看到这个测试问题,我笑了------这是个用来问《天龙八部》助手的测试用例。

但它设计得太聪明了:js写一个add函数 跟《天龙八部》毫无关系,也完全不需要查小说。

  • 朴素 RAG:硬去书里搜 5 段最相似的文字(大概是"武功招式"之类的)→ 然后基于这些莫名其妙的内容回答 → 大概率胡说
  • 现在的路由版:判断为 simple → 走 direct_answer → 不检索,直接正常回答

这个测试用例的精妙之处在于:它验证的不是"能不能答对",而是"能不能不检索"。

一条负向测试------好的测试用例常常是反着设计的。


二、补丁二:给 RAG 装上拆题能力

分诊解决了"要不要检索",接下来解决"检索什么"。

多跳问题长什么样

readme 给的那道题,我上篇引用过,这里再挖深一点:

"天龙八部中 四大恶人 排行第二的是谁?此人之子在身世揭晓前,其生父在武林中的公开身份是什么?"

这道题有两个问句,但实际推理链条更长:

arduino 复制代码
问句 1:四大恶人排行第二的是谁?
    ↓ 答案:叶二娘("无恶不作")
问句 2 的第一环:叶二娘的儿子是谁?
    ↓ 答案:虚竹
问句 2 的第二环:虚竹的生父是谁?
    ↓ 答案:玄慈
问句 2 的第三环:玄慈在武林中的公开身份是什么?
    ↓ 答案:少林寺方丈

要回答它,得跳四步。 而每一步的答案,分散在书中不同的章节里。

文件开头的注释也点出了另一个典型例子:

javascript 复制代码
// 段誉遇到的第一个神仙姐姐画像,是谁的弟子?
// 直接把query 向量化匹配不够准确
// 支持子问题拆分的工作节点

"直接把 query 向量化匹配不够准确" ------ 这句话就是病因诊断。

为什么不准?因为整句话的向量是"混合语义":

arduino 复制代码
"四大恶人排行第二的是谁?此人之子在身世揭晓前,其生父在武林中的公开身份是什么?"
                            ↓ embedding
        一个混合了【四大恶人】【儿子】【生父】【公开身份】的向量
                            ↓ 去数据库搜
        找到的片段:可能在讲"四大恶人",也可能在讲"生父"
                    但很难有一段话同时覆盖这条链

一个向量,装不下一条推理链。 所以解法是------先拆,再分头检索。

拆解节点:让 LLM 当"出题老师"

javascript 复制代码
const DecomposeSchema = z.object({
    sub_question: z.array(z.string()).min(1).max(8),
    reason: z.string(),
})

同样用结构化输出约束------sub_question 必须是 1 到 8 条的字符串数组。.min(1).max(8) 是 Zod 的数组长度约束。

javascript 复制代码
const decomposeQuestionNode = async (state) => {
    console.log('___DECOMPOSE-QUESTION___');
    // 结构化输出
    const decomposer = model.withStructuredOutput(DecomposeSchema);
    const out = await decomposer.invoke(`
    你是《天龙八部》多跳问答的【子问题拆解器】。
    用户原始问题:
    ${state.question}
    
    任务:将问题拆成**有序**子问题列表 sub_questions,用于**依次向量检索**。要求:
    1. 链式推理、多层关系、因果先后的问题,必须拆成多条;单跳即可答也可输出一条。
    2. 每条子问题必须是**可独立检索**的完整中文问句,**禁止**使用「他/她/此人/上文」等指代;可写全人物名与事件名。
    3. 顺序必须符合推理链:先搞清前置实体/事实,再查后续结论。
    4. **不要**把整句原题原样复制成唯一一条(除非确实无法拆分);不要拆成过碎的关键词列表。
    5. 输出 1~8 条即可。

    请输出 sub_questions 与简短 reason。
    `)

这五条要求,每一条都在防一个具体的坑。 值得逐条分析:

要求 防的坑
1. 链式推理必须拆成多条 防止模型偷懒,整句照搬
2. 禁止指代,要写全人物名 最精彩的一条
3. 顺序符合推理链 防止检索顺序颠倒导致后续查不到
4. 不要原样复制 / 不要拆太碎 两头防:防不拆,也防拆烂
5. 数量限定 1-8 条 防爆炸

重点看第 2 条------为什么禁止指代?

假设模型拆出这样的子问题:

arduino 复制代码
❌ 他的儿子是谁?                      ← "他"指谁?检索时不知道
❌ 此人的公开身份是什么?               ← "此人"又是谁?

因为检索是"独立"的------第二个子问题去数据库搜的时候,它不知道第一个子问题查到了"叶二娘"。指代词在脱离上下文后完全失效,向量化出来就是一团模糊语义。

正确的拆法是:

复制代码
✅ 叶二娘的儿子是谁?
✅ 虚竹的生父是谁?
✅ 玄慈在武林中的公开身份是什么?

注意第三句------它用了"玄慈"这个只有走完前两步才知道的名字。 这说明模型在拆解时已经推演过整条链,把中间答案都补全了。

这就是 prompt 第 3 条"顺序必须符合推理链:先搞清前置实体/事实,再查后续结论"的深意------它要求模型先自己走一遍推理,再倒着把每一步写成可检索的问句。

一个"拆解器",其实干了两件事:规划 + 预演。

拿到子问题后的处理

javascript 复制代码
// 去除空格,排除不需要的question
const subQuestions = out.sub_question.map((s) => s.trim()).filter(Boolean);
if (subQuestions.length === 0) {
    throw new Error("decompose_question: sub_questions 为空");
}

console.log(`拆解${subQuestions.length}条子问题(${out.reason})`);
subQuestions.forEach((q, i) => {
    console.log(`[${i+1}] ${q}`);
});
return {
    subQuestions,
    nextSubIdx: 0,               // 从第 0 条开始
    currentQuery: subQuestions[0],
}

三层防御:

  1. .trim() 去空格
  2. .filter(Boolean) 过滤空字符串(Boolean("") 是 false)
  3. 空数组就抛异常------宁可报错,也不要带着空问题继续跑

nextSubIdx: 0 是"游标"------标记下一条要检索哪个子问题。 这是整个多跳流程的驱动器。

状态设计:12 个字段的巧思

多跳版本的状态膨胀到了 12 个字段:

javascript 复制代码
const GraphState = Annotation.Root({
    question: Annotation,
    k: Annotation,
    strategy: Annotation,
    routeReason: Annotation,
    subQuestions: Annotation,      // 拆解出的子问题列表
    nextSubIdx: Annotation,        // question[i]  跳出循环
    currentQuery: Annotation,      // 当前子问题
    retrievalCount: Annotation,    // 已检索轮次
    maxRetrievals: Annotation,     // 最大轮次上限
    plannedNext: Annotation,       // 下一步决策
    documents: Annotation,
    generation: Annotation,
})

三个字段是循环控制三件套:

字段 作用
nextSubIdx 走到第几条了(游标)
retrievalCount 已经查了几轮(计数)
maxRetrievals 最多查几轮(上限)

注释写得很明白:

javascript 复制代码
nextSubIdx: Annotation, // question[i]  跳出循环

question[i] 这个写法暗示了它的用法------像遍历数组一样,一条条处理子问题。 而"跳出循环"说明它兼作终止条件。

maxRetrievals 初始值 8:

javascript 复制代码
maxRetrievals: state.maxRetrievals ?? 8,

?? 8 是"没设置就用 8"------给循环上了一道安全阀。

检索节点:一次查一条,累积去重

javascript 复制代码
const retrieveNode = async (state) => {
    const subs = state.subQuestions ?? [];
    const idx = state.nextSubIdx ?? 0;
    const q = subs[idx]?.trim(); // 当前这一轮问题

    if (!q) {
        throw new Error(`retrieve: 子问题下标${idx} 无有效文本
            (共${subs.length}条)
        `);
    }

    const round = state.retrievalCount + 1;
    console.log(`----第${round}轮,子问题:${idx+1}/${subs.length}----`)
    console.log(`---查询:${q}---`);
    const newDocs = await retrieveRelevantContent(q, state.k);
    // 多轮retrieve 有可能重复,会浪费资源
    // 重复可能让llm 我们在强调,错觉
    const merged = mergeUnique(state.documents ?? [], newDocs);
    // ...
    return {
        documents: merged,
        retrievalCount: round,
        nextSubIdx: idx + 1,
        currentQuery: q
    }
}

这个节点每次只处理一条子问题 ------取出 subs[nextSubIdx],检索,然后把游标 +1。

代码注释点出了去重的必要性:

javascript 复制代码
// 多轮retrieve 有可能重复,会浪费资源
// 重复可能让llm 我们在强调,错觉

重复文档有两个害处:

  1. 浪费资源------同样的内容占两份 token
  2. 制造"错觉"------如果同一段文字出现三次,模型可能以为"这个信息被多次强调了,应该很重要",从而高估它的权重

第二条很微妙------冗余会被模型误读为强调。 这是很多人想不到的坑。

去重函数:一个经典的取舍

javascript 复制代码
const mergeUnique = (existingDocs, newDocs) => {
    // map key 可以是对象
    // has get
    const map = new Map();// es6新增的HashMap 数据结构 key:value
    for (const d of [...existingDocs, ...newDocs]) {
        const key = String(d.id);
        const prev = map.get(key);
        if(!prev || Number(d.score) > Number(prev.score)){
            map.set(key, d);
        }
    }
    return Array.from(map.values()).sort((a, b) => Number(b.score) - Number(a.score));
}

十几行代码,完成三件事:

步骤 做法
去重 用 Map 按 id 做 key,同一个 id 只保留一条
择优 保留分数更高的那条
排序 按分数降序排列

"保留高分"这个决策值得细想。

同一段文字可能被多个子问题命中------比如"叶二娘"那段,既被"叶二娘的儿子是谁"命中,也被"虚竹的生父"命中。两次的相似度分数可能不同:

ini 复制代码
第一次命中:score = 0.72
第二次命中:score = 0.85    ← 这次更相关

保留哪个?这段代码选择留高分 (Number(d.score) > Number(prev.score))。

为什么合理?因为分数代表"这段话对这个具体问题的相关度"------0.85 那次说明这个子问题跟它更贴合,用高分数更真实地反映它的价值。

Number(d.score) 的强制转换是必要的------分数有时是字符串(尤其经过 JSON 序列化后),不转成数字比较会出错。

注释还提了一句 ES6 知识:

javascript 复制代码
// es6新增的HashMap 数据结构 key:value
// map key 可以是对象
// has get

Map 和普通对象的区别------对象(Object)的 key 只能是字符串/符号,Map 的 key 可以是任何类型(包括对象)。这里虽然用的是字符串 key,但注释把这个特性点出来了。

Array.from(map.values()) 把 Map 的迭代器转成数组------因为 Map 不能直接 .sort()。

决策节点:该继续查,还是该回答了?

检索完一条,接下来要判断:够了吗?还要不要继续?

javascript 复制代码
const NextStepSchema = z.object({
    nextAction: z.string(["retrieve" , "generate"]),
    reason: z.string(),
})

又一次结构化输出------nextAction 只能是 "retrieve" 或 "generate"。

javascript 复制代码
const planNextStepNode = async (state) => {
    console.log('___PLAN-NEXT-STEP___');
    const subs = state.subQuestions ?? [];
    const nextIdx = state.nextSubIdx ?? 0;
    const remaining = subs.length - nextIdx;
    
    const subList = subs.map((s, i) => `[${i+1}] ${s}
    ${i < nextIdx ? "已检索" : i === nextIdx ? "(下一轮将检索,若选择继续)" : "未检索"}`).join("\n");

这段代码在给模型画一张进度表:

csharp 复制代码
[1] 四大恶人排行第二的是谁?         已检索
[2] 叶二娘的儿子是谁?               (下一轮将检索,若选择继续)
[3] 虚竹的生父是谁?                 未检索
[4] 玄慈在武林中的公开身份是什么?     未检索

让模型清楚知道"走到哪了、还剩什么"------这是让它做出正确判断的前提。

然后把已召回的文档也喂给它:

javascript 复制代码
const docStr = state.documents.length === 0
    ? "(尚无检索结果)"
    : state.documents
        .slice(0, 6)               // ← 只取前 6 条
        .map((d, i) => `
            [${i+1}] score=${Number(d.score).toFixed(4)} 第${d.chapter_num}章
            ${d.content.slice(0, 200)}     // ← 每条只取前 200 字
        `).join("\n\n");

两个截断:最多 6 条、每条 200 字。

这是上下文管理的经典手法------判断"够不够"不需要看全文,看摘要和分数就够了。全塞进去,反而浪费 token 还干扰判断。

prompt 部分写得很有意思,有一句特别关键:

javascript 复制代码
const prompt = `你是多跳 RAG 规则器。检索查询已由前置步骤拆解为**有序子问题**,
若需要继续检索,下一轮将自动使用[下一条子问题] 做向量检索,你**不要**自拟新的检索句
...

"你不要自拟新的检索句" ------ 这条约束很聪明。

因为模型看到检索结果后,可能冒出"我觉得应该查查 XX"的想法,自己编一个新的搜索词。但这个流程的设计是"严格按拆解好的顺序走",让它自由发挥就破坏了规划的严谨性。

这叫"约束模型的自由度"------在需要严格流程的地方,明确告诉它"别乱来"。

然后是判断规则:

javascript 复制代码
请判断下一步:
1. 已有足够依据回答用户原始问题 -> nextAction=generate
2. 仍缺关键事实、且仍存在未检索的子问题、且未超过轮数上限 -> nextAction=retrieve
硬性规则:
- 若剩余未检索子问题条数为0,必须 nextAction=generate
- 若已检索轮数已到达或超过最大检索轮数,必须 nextAction=generate。

注意"硬性规则"这四个字------prompt 里说了,但代码里还要再兜一遍:

javascript 复制代码
let finalNext = nextAction;
if(state.retrievalCount >= state.maxRetrievals) finalNext = "generate";
if(remaining <= 0) finalNext = "generate";
console.log(`[决策] plannedNext=${finalNext}
    (模型建议=${nextAction})(${reason})`)
return {
    plannedNext: finalNext,
}

这是"不信任模型"的正确姿势。

模型可能会忽略 prompt 里的"硬性规则"(LLM 对负向约束的遵守率并不完美)。所以代码层面再做一次强制校验------无论模型说什么,超轮数或没剩余子问题,一律转 generate。

日志还刻意区分了两个值:

ini 复制代码
[决策] plannedNext=generate (模型建议=retrieve)(...)
        ↑ 最终生效的      ↑ 模型原本想要的

把"模型建议"和"最终决策"都打出来------如果两者不一致,就能知道"是模型判断错了,还是被安全阀拦下来了"。这是非常好的调试设计。

完整的图:一个带反馈回路的闭环

javascript 复制代码
const graph = new StateGraph(GraphState)
    .addNode("route_question", routeQuestionNode)
    .addNode("direct_answer", directAnswerNode)
    .addNode("decompose_question", decomposeQuestionNode)
    .addNode("retrieve", retrieveNode)
    .addNode("plan_next_step", planNextStepNode)
    .addNode("generate", generateNode)
    .addEdge(START, "route_question")
    .addConditionalEdges("route_question",afterRoute, {
        direct_answer: "direct_answer",
        decompose_question: "decompose_question",
    })
    .addEdge("decompose_question", "retrieve")
    .addEdge("retrieve", "plan_next_step")
    .addConditionalEdges("plan_next_step",afterPlan, {
        retrieve: "retrieve",       // ← 回到 retrieve!
        generate: "generate",
    })
    .addEdge("direct_answer", END)
    .addEdge("generate", END)
    .compile()

看流程图:

sql 复制代码
                   ┌→ direct_answer → END
START → route_question
                   └→ decompose_question → retrieve → plan_next_step
                                                          │
                                          ┌───────────────┤
                                          ↓               ↓
                                      retrieve ────→  generate → END
                                      (循环回来)

retrieve → plan_next_step → retrieve 形成了一个环。 这就是 LangGraph 前面学的"循环"能力------条件边指回上游节点。

而跳出循环的条件有三个:

  1. 模型判断"够了"(nextAction=generate)
  2. 子问题查完了(remaining <= 0)
  3. 达到轮数上限(retrievalCount >= maxRetrievals)

一个受控的循环:能重复,但不会失控。

拆解效果

代码里的日志会打印拆解结果:

javascript 复制代码
console.log(`拆解${subQuestions.length}条子问题(${out.reason})`);
subQuestions.forEach((q, i) => {
    console.log(`[${i+1}] ${q}`);
});

如果模型拆得对,你会看到类似这样的输出:

scss 复制代码
拆解4条子问题(需要多跳推理才能确定生父身份)
[1] 《天龙八部》中四大恶人排行第二的是谁?
[2] 叶二娘的儿子是谁?
[3] 虚竹的生父是谁?
[4] 玄慈在武林中的公开身份是什么?

从一句"绕晕人"的长问句,变成四句"每句都能独立检索"的短问句------这就是拆题术的价值。

测试问题:两个版本

javascript 复制代码
async function main () {
    // const question = 'js写一个add函数';
    const question = `《天龙八部》中【四大恶人】排行第二的是谁?
        此人之子在身世揭晓前,其生父在武林中的公开身份是什么?`;

注意被注释掉的那行------js写一个add函数。

这是上篇路由 demo 的测试问题,在这个多跳版本里被注释掉了。 为什么?因为这个问题会走 direct_answer 分支,压根到不了拆解节点------在这个 demo 里它测不出东西来。

留下这行注释的价值在于:它记录了两个 demo 之间的关联------同一个测试用例,在不同的架构版本里,会走出完全不同的路径。


三、两个补丁,治好了哪两个硬伤

回头对照上篇的五个硬伤表:

# 硬伤 这篇的补丁 状态
1 所有问题都检索 路由分流(simple/complex 二选一) ✅ 已修
2 没有评估机制 决策节点部分涉及(判断"够了没") 🟡 部分
3 处理不了多跳问题 问题拆解(子问题 + 顺序检索 + 去重) ✅ 已修
4 语义匹配不准 关键词检索 ⬜ 待办
5 不会联网兜底 网络搜索 ⬜ 待办

两个补丁,两个新节点结构,两次结构化输出的实战应用。

值得留意的是------这两个补丁的技术手段其实是同一个:让 LLM 做判断。

  • 路由分诊:判断"简单还是复杂"
  • 下一步决策:判断"继续还是停止"

而多跳拆解是"让 LLM 做规划"------判断是"选择题",规划是"简答题"。 前面的结构化输出(z.enum)天然适合做选择题,后面的子问题拆解(z.array(z.string()).min(1).max(8))用来做简答题。

这就是上篇说的"在这个骨架上插节点"------骨架上插的每个节点,本质都是一次"让模型做一次结构化判断"。


四、从"流水线"到"有回路"

最后看一下这两个 demo 的架构变化。上篇的朴素 RAG:

sql 复制代码
START → retrieve → generate → END

直线,走完就完。

这篇的两个版本:

sql 复制代码
路由版:  START → route → (direct | retrieve→generate) → END
                       ↑ 岔路口

多跳版:  START → route → decompose → retrieve → plan ─┐
                                              ↑        │
                                              └────────┘
                                              (回路)

岔路口 + 回路。

这正是前面 LangGraph 那节课讲的核心------从"线性流水线"升级到"网状图"。 而这张网的每个分叉点、每个回路点,决策者都是 LLM 自己。

readme 的总结很到位:

"死板的检索生成流程,升级为可思考、可判断、可纠错的智能 RAG 架构。"

  • 可思考 → 拆解问题(规划)
  • 可判断 → 分诊、决定继续还是停止
  • 可纠错 → 还没做完(下一篇的评估节点)

PS:这两篇看下来,最大的感受是------RAG 的升级不是在"检索算法"上做文章,而是把决策权交给模型。以前是"所有问题都走同一条路",现在是"让模型看看这是什么问题、走到哪了、够不够了"。下一篇我们补上最关键的一块:让 RAG 知道自己"不知道",并且学会出门求助。

相关推荐
IT_陈寒1 小时前
Java中equals方法比了个寂寞?原来这才是正确的重写姿势
前端·人工智能·后端
CopyCode1 小时前
用 AI 迁项目有多爽?我把 Webpack 迁 Vite 的全过程记下来了
前端·架构
去伪存真1 小时前
Electron 自动化发布指南:GitHub Actions 跨平台打包全纪录
前端·electron
计算机魔术师1 小时前
OpenAI智能体失控闯进美国政府网站,53张用户图片外泄背后
前端
颜进强1 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
程序员Flycan1 小时前
🚀 跨域终结者:前端代理服务器(Proxy)原理解析与配置总结
前端
怕浪猫1 小时前
顶级模型一句话,AI 写出了能玩的 QQ飞车
前端·面试·github
huakoh1 小时前
MCP 报错分不清?先看响应里是 result 还是 error
前端
呃呃呃呃ex1 小时前
10. 现代前端工程化:ES6 React 项目实战与原理解析
前端·javascript