🚕 缺料就出门买:给 RAG 装上"信息够不够"的判断力

写在前面:前两篇我们搭了 RAG、打了两个补丁(分诊、拆题)。这篇补最后也是最关键的一块------让 RAG 知道自己"不知道"。 朴素 RAG 最要命的地方在于:它搜到什么就用什么。哪怕书里压根没有相关内容,它也会硬着头皮编一个答案。readme 把这个问题直白地写了出来------"本地知识库没有的内容,不会主动去网络搜索补充,容易编造答案。" 今天这个 demo 干的就两件事:先评估"手头的资料够不够",不够就出门联网买料,买回来再评估一遍。以下所有代码均来自课堂真实文件。


一、先看场景:一道故意设计的问题

rag-webfallback.mjs 的测试问题,是整篇的题眼:

javascript 复制代码
const question = `请回答《天龙八部》小说里"雁门关事件"的主谋是谁,并说明其儿子的最终结局;
另外请补充:在《天龙八部》2013版电视剧中,这段"雁门关事件"主要出现在哪几集?
请给出可核对的来源链接。
`

这个问题被精心设计成了"一半本地能答、一半本地答不了"。

拆开看:

子问题 本地知识库(《天龙八部》EPUB)能答吗
雁门关事件的主谋是谁 ✅ 能------小说里有明确情节
其儿子的最终结局 ✅ 能------小说里写了
2013 版电视剧中出现在哪几集 ❌ 不能------小说原文里没有电视剧信息
给出可核对的来源链接 ❌ 不能------本地文本给不了链接

而且这些信息在小说里的推理路径也是清晰的:

复制代码
雁门关事件的主谋 → 慕容博(散布假消息,挑起雁门关伏击)
慕容博的儿子   → 慕容复
慕容复的结局   → 精神失常(在坟头做皇帝梦)

前两问,向量检索能搞定。后两问,本地库里根本没有------这就逼出了"联网兜底"的流程。

好的测试用例不是"随便问一句",而是"精确地卡在能力边界上"。 这个问题正好卡在"本地能答"和"本地答不了"的分界线上。


二、状态设计:把"料"分成三堆

javascript 复制代码
const GraphState = Annotation.Root({
    question: Annotation,
    k: Annotation,
    strategy: Annotation,
    routeReason: Annotation,
    // 召回
    retrievedDocs: Annotation,
    localContext: Annotation,   // RAG 上下文
    webContext: Annotation,     // 网络搜索上下文
    evaluation: Annotation,     // { enough, missing, reason }
    generation: Annotation,
})

跟上篇的多跳版本比,这里的状态设计有个明显变化------上下文被拆成了两堆:

字段 装什么
localContext 本地知识库检索到的内容
webContext 联网搜索回来的内容

为什么要分开存,而不是合并成一个 context?

三个理由:

  1. 来源可追溯------生成答案时可以标注"这段来自书里,那段来自网络"
  2. 流程可控 ------webContext 有值就意味着"已经联网过了",可以据此避免重复联网
  3. 评估更精确------评估节点能分别知道"本地料够不够"和"网络料补上没"

那个 evaluation 字段的注释也点明了结构:

javascript 复制代码
evaluation: Annotation,     // { enough, missing, reason }

它存的是一个 JSON 字符串(后面会看到为什么)。


三、备料检查:那个救命的评估节点

这是全篇的核心。EvaluateSchema:

javascript 复制代码
const EvaluateSchema = z.object({
    enough: z.boolean(),                    // 是否足够生成
    missing: z.array(z.string()).max(6),    // 上下文缺的方面
    reason: z.string(),
    web_query: z.string().optional()        // 可选的 web 搜索的关键词
})

四个字段,各有使命:

字段 类型 作用
enough boolean 总开关------够不够回答
missing string\[\](最多6条) 缺料清单
reason string 判断理由
web_query string(可选) 给联网搜索用的查询词

这个 Schema 的设计非常讲究------它不只是"判断够不够",还顺手把"下一步该干什么"也定好了。

逐个看:

enough 是 boolean(不是字符串)------布尔值让代码判断可以直接用,不用做字符串比较。

missing 用 .max(6) 限长------防止模型长篇大论地罗列缺失点。 缺失点列太多也没意义,抓重点就行。

web_query 标了 .optional()------因为不是每次评估都需要它。 如果料够了,就不需要联网,这个字段也就没意义。

节点实现

javascript 复制代码
const evaluateNode = async (state) => {
    const hasWeb = Boolean(state.webContext && String(state.webContext).trim());
    console.log(hasWeb ? "___EVALUATE_CONTEXT_WITH_WEB___" : "___EVALUATE_WEB_CONTEXT___");
    // llm 大脑,规划,分析,分步骤
    const evaluator = llm.withStructuredOutput(EvaluateSchema);
    const out = await evaluator.invoke(`
        你是信息充分评估专器。判断当前上下文是否足以回答用户问题。
        用户问题: ${state.question}
        已检索上下文(来自本地知识库):
        ${state.localContext || " (空) "}
        ${hasWeb ? `联网搜索结果:\n ${state.webContext || " (空) "}` : ""}
    
        输出字段:
        - enough: 是否足够回答(true/false)
        - missing: 若不够,列出缺失信息点(最多6条)
        - reason: 简短原因
        ${hasWeb ? "" :
            "- web_query: 若不够,给出一个适合互联网搜索的中文查询句(完整句,不用代码:为空也可)"}
    `);
    console.log(`${hasWeb ? "二次评估": "评估"} :
        enough=${out.enough}`)
    if (!out.enough && out.missing?.length) {
        out.missing.forEach((m, i) => console.log(`缺失 ${i+1}: ${m}`))
    }
    return {
        evaluation: JSON.stringify(out),
    }
}

这个节点会被调用两次------第一次评估本地料,第二次评估"本地 + 网络"的合并料。 一个函数,两个身份。

实现上做了几处巧妙的区分:

第一,hasWeb 判断

javascript 复制代码
const hasWeb = Boolean(state.webContext && String(state.webContext).trim());

三层处理:state.webContext 存在?→ 转字符串 → 去掉空白后还有内容吗?

String(...).trim() 这一步是必要的------防止一个全是空格的字符串被误判成"有内容"。

第二,日志名字随状态变化

javascript 复制代码
console.log(hasWeb ? "___EVALUATE_CONTEXT_WITH_WEB___" : "___EVALUATE_WEB_CONTEXT___");

一个日志名,隐含了"这是第几次评估"。调试时一眼能看出来。

第三,prompt 是动态拼装的

javascript 复制代码
${hasWeb ? `联网搜索结果:\n ${state.webContext || " (空) "}` : ""}

没联网时,这段内容压根不出现。 而不是"联网结果:(空)"------少一段噪音就少一分干扰。

第四,web_query 的说明只在第一次出现

javascript 复制代码
${hasWeb ? "" :
    "- web_query: 若不够,给出一个适合互联网搜索的中文查询句(完整句,不用代码:为空也可)"}

第二次评估(已经联网过了)就不再要求 web_query ------因为不需要再搜了。这个细节说明设计者很清楚流程走到哪一步。

第五,web_query 的措辞要求

arduino 复制代码
"给出一个适合互联网搜索的中文查询句(完整句,不用代码:为空也可)"

"完整句,不用代码" ------这个约束很实在。因为本地知识库是《天龙八部》,模型可能习惯性地给出小说式的查询词。但在互联网上搜"2013版电视剧 雁门关 集数",需要的是一句自然的中文查询。

一个值得注意的存储细节

javascript 复制代码
return {
    evaluation: JSON.stringify(out),
}

把结构化对象转成 JSON 字符串再存。

为什么要这样?因为 LangGraph 的状态字段是共享的 ------Annotation 里声明的是通用类型,存字符串最安全、最不容易出岔子。取用的时候再 JSON.parse 回来:

javascript 复制代码
const parsed = (() => {
    try {
        return JSON.parse(state.evaluation || "{}");
    } catch {
        return {};
    }
})();

这段"立即执行函数"(IIFE)配合 try/catch,是一个很稳的防御式写法:

  • state.evaluation || "{}" ------ 空值兜底
  • try/catch ------ 解析失败兜底
  • 返回 {} ------ 保证后面访问 .enough 不会崩

因为 evaluation 是字符串,任何解析失误都会导致 JSON.parse 抛异常。 这两层兜底,是把"可能崩的地方"都焊死了。


四、路由与本地检索

路由复用:还是那个分诊台

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

    console.log(`路由策略:${route.strategy} ${route.reason}`)
    // 可选的,不需要全部state的设置
    // 为后面的节点提供服务的
    return {
        strategy: route.strategy,
        routeReason: route.reason,
        retrievedDocs: [],
        localContext: "",
        webContext: "",
        evaluation: "",
        generation: "",
    }
}

RouteSchema 和上篇一模一样------分诊台被完整复用了。 这是模块化设计的好处:一个可靠的判断节点,可以在不同流程里搬运。

注意这段注释:

javascript 复制代码
// 可选的,不需要全部state的设置
// 为后面的节点提供服务的

这个节点把后面要用的字段全部初始化了一遍 (retrievedDocs、localContext、webContext、evaluation、generation)。

这是个好习惯------在流程入口把状态"归零",后面的节点就不用到处写"如果为空就......"的兜底判断。 相当于每次启动前先把桌面擦干净。

而这个测试问题(雁门关 + 电视剧集数)会被判为 complex------因为它明确要求"具体情节、原文细节或证据支持"。

本地检索:只取内容

javascript 复制代码
const retrieveLocalNode = async (state) => {
    console.log("___LOCAL_RETRIEVE___");
    const retrieveDocs = await retrieveRelevantContent(state.question, state.k);
    console.log(`本地检索命中 ${retrieveDocs.length} 条`);
    const localContext = (retrieveDocs ?? []).map((d) => d.content).join("\n\n");
    return {
        retrievedDocs: retrieveDocs,
        localContext,
    }
}

这个节点返回两个东西:

返回值 内容
retrievedDocs 完整的文档对象(含分数、章节等元数据)
localContext 纯文本拼接的上下文

为什么要两个都存?

  • localContext 直接喂给 prompt------模板字符串里需要的就是一段纯文本
  • retrievedDocs 保留结构化数据------评估节点和调试输出可能需要看分数、章节号

一份数据,两种形态。 朴素 RAG 里只有 documents,这里拆成了两个字段------流程变复杂后,数据形态也要跟着分化。

注意检索用的是原始问题 (state.question),而不是拆解后的子问题------这个 demo 没有引入多跳拆解,它专注于"评估 + 兜底"这一件事。 每个 demo 只演一个能力,这是很清醒的做法。


五、出门采购:联网搜索节点

评估说"料不够",该出门买料了。

决策逻辑:先看有没有买过

javascript 复制代码
const afterEvaluateLocal = (state) => {
    if(state.webContext && String(state.webContext).trim()) {
        return "generate"
    }
    const parsed = (() => {
        try {
            return JSON.parse(state.evaluation || "{}");
        } catch {
            return {};
        }
    })();
    return parsed.enough === true ? "generate" : "web_search"; 
}

三行判断,逻辑很严密:

ini 复制代码
已经联网过了?(webContext 有内容)
  → 是 → 直接 generate(防止无限联网!)
  → 否 → 看评估结果够不够
          → enough === true → generate
          → 否则 → web_search

第一行的判断是防死循环的关键。

想一下:web_search 之后会回到 evaluate_local 做二次评估。如果二次评估又说"不够",会怎么样?→ 又去联网 → 又评估 → 死循环。

所以这里设了一道硬闸:只要 webContext 已经有内容,无论如何都去 generate。

"最多联网一次" ------这不是能力限制,是安全边界。

parsed.enough === true 用 === 严格比较而不是 ==------因为 enough 必须真的是布尔 true,字符串 "true" 不算数。

博查搜索:一个完整的 API 封装

javascript 复制代码
async function bochaWebSearch(query, count) {
    const apiKey = process.env.BOCHA_API_KEY;
    if (!apiKey) {
        throw new Error("Bocha Web Search 的 API Key 未配置(环境变量 BOCHA_API_KEY)");
    }
    const url = process.env.BOCHA_API_URL;
    const body = {
        query,
        freshness:"noLimit",
        summary: true, // 返回的内容,做个总结
        count: count ?? 10
    }

用的是博查(Bocha)搜索 API------国内一个面向 AI 应用的搜索服务。配置全部走环境变量,这是标准做法(前面 FastAPI 那篇讲过)。

请求参数:

参数 值 含义
query 查询句 来自评估节点的 web_query
freshness "noLimit" 不限制时间范围
summary true 返回内容做摘要(注释说明了)
count 传进来的数量 要几条结果

summary: true 这个参数很关键------搜索引擎直接返回摘要,省掉了自己抓网页正文的麻烦。

这一点体现了"选对工具"的价值:直接调一个"为 AI 设计的搜索 API",比自己写爬虫去抓网页、解析 HTML、提取正文要省太多事。

然后是请求发送:

javascript 复制代码
    let response;
    try {
        response = await fetch(url, {
            method: "POST",
            headers: {
                "Authorization": `Bearer ${apiKey}`,
                "Content-Type": "application/json",
            },
            body: JSON.stringify(body),
        });
    } catch {
        throw new Error(`搜索API 请求失败(网络错误):${error.message}`);
    }

标准的 fetch POST 请求------Bearer Token 认证、JSON 请求体。

错误处理分了两层,这是个很专业的写法:

javascript 复制代码
    // 先处理失败
    // ok 200 语义化更好
    if (!response.ok) {
        // 二进制 json() text()
        // 出错,打印出错信息
        const errorText = await response.text().catch(() => "");
        throw new Error(`搜索API 请求失败,状态码:${response.status} 
            错误信息:${errorText}`);
    }
    
    let json;
    try {
        json = await response.json();
    } catch {
        throw new Error(`搜索结果解析失败:${error.message}`);
    }
层次 检查什么 怎么报错
HTTP 层 response.ok(状态码 2xx) 带状态码 + 错误文本
解析层 response.json() 能不能解析 单独报"解析失败"

为什么要把这两层分开? 因为它们是完全不同的故障:

  • HTTP 层失败 = 请求没成功(Key 错了、欠费了、服务挂了)→ 去查账号
  • 解析层失败 = 请求成功了但返回格式不对(接口变了)→ 去查接口文档

如果混在一起报"搜索失败了",你根本不知道该往哪个方向查。

注释里那句"ok 200 语义化更好"------说的是 response.ok 这个属性比自己写 response.status === 200 更直观,而且它能覆盖所有 2xx 状态码。

.catch(() => "") 也是个细节------读错误信息本身也可能失败,再套一层兜底防止报错时报错。

结果解析与格式化

javascript 复制代码
console.log(json, "///////////");
    const webpages = json.data.webPages?.value ?? [];
    if (webpages.length) {
        return "未找到相关结果。"
    }

    return webpages
        .map((page, idx) => `引用:${idx+1}
        标题:${page.name}
        URL:${page.url}
        摘要:${page.summary}
        网站名称:${page.siteName}
        网站图标:${page.siteIcon}
        发布时间:${page.dateLastCrawled}
        `)
        .join("\n\n")
}

结果格式化成带出处的结构------每条结果包含序号、标题、URL、摘要、网站名、发布时间。

这个格式化直接影响最终答案的质量。

还记得测试问题的最后一句吗?------"请给出可核对的来源链接 "。为什么这里要把 URL 和网站名都带上?因为生成节点要靠这些信息给出引用。

如果只返回摘要文本,模型没法给出链接。格式化的字段,决定了最终答案能不能满足需求。

这是一个"下游需求倒推上游格式"的经典例子------不是"搜索返回什么就存什么",而是"最终答案需要什么,就格式化出什么"。

搜索节点:拿到采购清单

javascript 复制代码
const webSearchNode = async (state) => {
    console.log("___WEB_SEARCH___");
    const parsed = (() => {
        try {
            return JSON.parse(state.evaluation || "{}");
        } catch {
            return {};
        }
    })();
    const query = (parsed.web_query ?? "").trim() || state.question;
    console.log(`联网搜索: ${query}`);
    // 封装
    // 方便切换其他服务
    const webContext = await bochaWebSearch(query, 8);
    console.log(`联网搜索结果长度: ${webContext.length}`);
    return {
        webContext
    }
}

查询词的选取逻辑值得细看:

javascript 复制代码
const query = (parsed.web_query ?? "").trim() || state.question;

三步:

  1. parsed.web_query ?? "" ------ 取评估节点给的查询词,没有就空字符串
  2. .trim() ------ 去空格
  3. || state.question ------ 如果还是空的,就用原始问题兜底

这是一个"有则用模型优化过的,无则退回原始问题"的降级策略。 评估节点可能给了更好的查询词(它知道缺什么),也可能没给(模型没填),那就直接用原问题。

bochaWebSearch(query, 8) 传了 8------要 8 条结果。 因为网络信息质量参差不齐,多取几条再让模型自己筛选。

注释里还有一句很有工程意识的话:

javascript 复制代码
// 封装
// 方便切换其他服务

这就是"封装成独立函数"的意义 ------如果哪天想从博查换成必应、Google,只要改 bochaWebSearch 这一个函数,图的结构完全不用动。


六、生成节点:把两堆料合起来

javascript 复制代码
const generateNode = async (state) => {
    // 增强prompt
    // localContext
    // webContext
    console.log('___GENERATE___');
    const context = [state.localContext, state.webContext]
        .filter(Boolean)// Boolean 函数
        .join("\n\n==联网补充==\n\n");

两行代码,完成合并:

步骤 作用
.filter(Boolean) 过滤掉空值(本地没检索到 / 没联网)
.join("\n\n==联网补充==\n\n") 用显式分隔符拼接

.filter(Boolean) 是个很聪明的写法------Boolean("") 是 false,Boolean("内容") 是 true。用它来过滤空字符串,比写 .filter(s => s && s.length > 0) 简洁得多。

分隔符 ==联网补充== 是给模型看的信号,告诉它"从这里开始是网络来的信息"。这样模型回答时能区分两类来源。

然后是 prompt:

javascript 复制代码
    const stream = await llm.stream(`你是一个严谨的中文回答助手,
        优先依据上下文回答,不要编造。
        上下文(本地知识库 + 可选联网补充):
        ${context || "(空)"}
        用户问题:${state.question}

        回答要求:
        1.如果上下文足够,给出清晰,可核对的回答:需要时引用:n / URL 
        "或小说片段来支撑。"
        2. 如果上下文仍不满足已确定关键事实,明确说明"不确定/无法从上下文确认",
        并说明缺失点
        3. 不要输出表情符号。

        回答:
    `);

三条要求,对应前两篇的教训:

要求 解决什么
1. 要求可核对,引用 URL 或原文 提供出处,让用户能验证
2. 不确定就明说,并说明缺失点 防幻觉的最后一道防线
3. 不要输出表情符号 严谨场景的输出规范

第 2 条是整篇的收口。

前面做了那么多------评估够不够、缺什么、联网补充------最终还是要落到"如果还是不够,就老实说不够"。这才是"可纠错"的完整含义:不是保证永远答对,而是保证不会假装答对。

prompt 开头那句"优先依据上下文回答,不要编造"也值得注意------"优先"两个字用得好,它意味着"上下文里有就用,没有才靠模型自身知识",而不是"只能依据上下文"。


七、完整的图:一个会自我检查的闭环

javascript 复制代码
const graph = new StateGraph(GraphState)
    .addNode("route_question",routeQuestionNode)
    .addNode("direct_answer",directAnswerNode)
    .addNode("local_retrieve",retrieveLocalNode)
    .addNode("evaluate_local",evaluateNode)
    .addNode("generate",generateNode)
    .addNode("web_search",webSearchNode)
    .addEdge(START,"route_question")
    .addConditionalEdges("route_question", afterRoute, {
        direct_answer: "direct_answer",
        local_retrieve: "local_retrieve",
    })
    .addEdge("local_retrieve", "evaluate_local")
    .addConditionalEdges("evaluate_local", afterEvaluateLocal, {
        generate: "generate",
        web_search: "web_search",
    })
    .addEdge("web_search", "evaluate_local")     // ← 回到评估!
    .addEdge("direct_answer", END)
    .addEdge("generate", END)
    .compile();

流程图:

sql 复制代码
                 ┌→ direct_answer → END
START → route_question
                 └→ local_retrieve → evaluate_local ─┐
                                          ↑          │
                                          │      ┌───┴───┐
                                          │      ↓       ↓
                                          └─ web_search  generate → END
                                          (联网后回来再评估)

注意 web_search → evaluate_local 这条边------它形成的是一个"评估循环"。

完整走一遍:

arduino 复制代码
1. route_question   → 判定 complex
2. local_retrieve   → 从《天龙八部》里检索(能查到慕容博、慕容复)
3. evaluate_local   → 评估:enough = false
                      missing: ["2013版电视剧具体集数", "可核对的来源链接"]
                      web_query: "天龙八部 2013版电视剧 雁门关事件 第几集"
4. web_search       → 带着 web_query 出门联网
5. evaluate_local   → 二次评估(现在有 webContext 了)
6. generate         → 合并本地 + 网络,生成最终回答

这个闭环的精妙之处在第 5 步 ------联网回来不是直接生成,而是再评估一次。

为什么?因为联网搜回来的东西可能:

  • 也没查到(那评估会再判定不够,但因为有防重复机制,直接进 generate 并如实说"无法确认")
  • 查到了但质量差(评估能看到内容,可以指出问题)
  • 完全够了(判定 enough,正常生成)

"检查 → 补料 → 再检查" ------这是任何认真做事的流程都该有的样子。

日志会把这些步骤都打出来:

ini 复制代码
___ROUTE-QUESTION___
路由策略:complex 需要具体情节和证据
___LOCAL_RETRIEVE___
本地检索命中 8 条
___EVALUATE_WEB_CONTEXT___
评估 : enough=false
缺失 1: 2013版电视剧具体集数
缺失 2: 可核对的来源链接
___WEB_SEARCH___
联网搜索: 天龙八部 2013版电视剧 雁门关事件 第几集
联网搜索结果长度: 1234
___EVALUATE_CONTEXT_WITH_WEB___
二次评估 : enough=true
___GENERATE___

这一串日志,就是整个 Agent 的"思考过程"。 节点名前后的 ___ 下划线让它们在输出流里一眼可辨------又一个调试友好设计。


八、五个硬伤,全部收工

回到第一篇那张表,现在全部打完了:

# 硬伤 补丁 状态
1 所有问题都检索 路由分流(simple/complex) ✅
2 没有评估机制 评估节点(enough/missing/web_query) ✅
3 处理不了多跳 问题拆解(子问题 + 顺序检索) ✅
4 语义匹配不准 关键词检索(ES) ⬜ 下一篇
5 不会联网兜底 网络搜索 + 二次评估闭环 ✅

readme 对这套架构的总结只有一句话,但每个字都有分量:

"基于 LangGraph 实现闭环的 Agentic RAG,也就是 Agent 自主决策要不要检索,用什么检索(web search + milvus + es),信息够不够,效果怎么样,要不要重新搜。"

四个决策,全部由 Agent 自己做:

决策 对应节点
要不要检索 route_question
信息够不够 evaluate_local
要不要补充 afterEvaluateLocal(web_search or generate)
补充后够了吗 二次评估

这就是"闭环"和"线性"的区别------线性流程是"检索 → 生成 → 结束",闭环是"检索 → 评估 → 补充 → 再评估 → 生成"。

readme 还有一句提醒,很重要:

"具体 Agentic RAG 要根据业务场景设计,理解这个闭环思路就可以。"

意思是:别照抄这个图。

不同业务的闭环长得不一样:

  • 客服系统可能不需要联网,但需要"查订单状态"
  • 代码助手可能不需要多跳,但需要"读文件"
  • 合规场景可能不需要多路召回,但必须"每条结论都要出处"

变的是节点,不变的是那个思想------让 Agent 自己判断"我手上的信息够不够",不够就想办法补。


九、这一篇的三个收获

收获一:评估是"防幻觉"最有效的手段。

prompt 里写一百句"不要编造",不如加一个节点问模型"你手上的资料够回答吗"。前者是嘱咐,后者是流程。 嘱咐靠自觉,流程靠机制。

收获二:好的 Schema 设计是"判断 + 行动"二合一。

EvaluateSchema 不只是判断"够不够",还给出了"缺什么"和"下一步搜什么"。一个结构化输出同时完成了评估、诊断、规划三件事。

收获三:错误处理要分层,防循环要设闸。

bochaWebSearch 里把 HTTP 失败和解析失败分开报错------故障定位的前提是故障分类。 而 afterEvaluateLocal 里那句"已经联网过就直接生成"------任何允许循环的设计,都必须配一个终止条件。


PS:这篇写下来,"Agentic"这个词的含义清晰了很多------它不是"用 AI 做更多事",而是"让 AI 知道自己的边界"。知道自己不知道,才会去查;知道自己查过了,才会停。下一篇我们补最后一个硬伤:为什么"高血糖"和"低血糖"在向量数据库里几乎一样,以及 ES 怎么救场。

相关推荐
Liora_Yvonne1 小时前
同样用 Element Plus,为什么你的后台总有一股“模板味”?
前端
葡萄城技术团队1 小时前
用 Sheet 页导航视图管理复杂 SpreadJS 工作簿
前端
志尊宝1 小时前
Vue3 零基础每日笔记(054):Pinia 三件套详解——state / getters / actions 全搞懂
前端·javascript·vue.js·vue·前端开发
Caroline5161 小时前
GPT Image 2.5还能这么玩!一套提示词生成IP吉祥物,附完整案例
前端·gpt·tcp/ip
wangyadong3172 小时前
# el-table 复选框无法选中问题记录
前端·javascript·vue.js
葡萄城技术团队2 小时前
给 SpreadJS 单元格装上自动完成
前端
恋猫de小郭2 小时前
Flutter 多窗口重要优化合并,多窗口性能和实用性大幅提升
android·前端·flutter
StudyAndLife2 小时前
开源一个 API 开放平台门户前端:本地 npm run dev 就能逛接口文档
前端
广州华水科技2 小时前
GNSS变形监测一体机在大坝安全监测中的应用与优势分析
前端