行业动态
towardsdatascience.com ·
@Kezhan Shi
说‘本文档中没有’的RAG必须展示四种证据
企业文档智能系列文章,讨论RAG系统在回答‘无答案’时应提供四种证据,以避免错误否定。
AI推荐理由 针对RAG系统改进提出具体方法,对开发者有参考价值,但属于技术博客。
60
正文
RAG系统能给出的最有用的答案有时是“文档中没有这个内容”。但当被问及一个问题时,模型往往会回答,因此诚实的“未找到”必须被内置,而不是寄希望于偶然。让系统说“不”,并且在说“不”时是正确的,比让它说“是”更难。
本文是《企业文档智能》系列的一个额外篇章,该系列用四个模块构建企业RAG系统。本文论证了“我不知道”的合理性:自信的错误答案是一个缺陷,而赤裸裸的不回答几乎同样糟糕,四个模块中的每一个都有一项证据来展示。
🧭 系列新手?本系列的所有文章都位于我们的两个Towards Data Science作者页面:Angela Shi和Kezhan Shi。这是查看涵盖内容以及本文所处位置的最短途径。
本文在系列中的位置:与编号主线并列的额外文章 - 作者图片
📓 可运行的配套笔记本在GitHub上:doc-intel/notebooks-vol1。
公共配套代码仓库位于doc-intel/notebooks-vol1 - 作者图片
向企业聊天机器人提问“全球AI消耗多少电力?”它会返回“我在本文档中找不到答案。”换个说法再问:同样的答案。此时大多数用户放弃并转向谷歌。
系统做了正确的事。它所依据的文档是世界银行的《大宗商品市场展望》(2025年4月期;CC BY 3.0 IGO,如世界银行开放知识库使用条款所声明,该条款规定2023年前出版物的默认许可),这是一份关于石油、农业和金属价格的63页季度报告。AI电力消耗不在其中。没有可找的答案。系统没有撒谎。
但用户没有学到任何有用的东西。他们无法判断管道是否搜索了所有应该搜索的地方,还是在进行了一次浅层嵌入搜索后就放弃了。“我找不到答案”是一个需要附带辩护的结论。否则,它读起来像是失败,而不是事实。
该系列认为,结构化输出(第8条)通过强制模型引用其证据(引用、页码、置信度)使“是”答案可验证。同样的逻辑适用于“否”答案,并且相同的四个模块各自贡献一项证据。本文再次遍历这四个模块,针对一个正确输出为“否”的问题,并展示每个模块必须产生什么,以使“否”答案具有可辩护性。
1. 四个模块,四种证据
论证压缩为一张表。
四个模块,四种证据,对于可辩护的结论来说,它们都是必不可少的 - 作者图片
本文的其余部分按顺序遍历每一行,然后在CMO / AI电力消耗案例上将它们联系起来。
2. 解析:关系表就是证据
缺失解析的代价是不对称的。误报(提取噪声)会使检索更混乱,但在下游可恢复。漏报(未能提取真实标记)是无声的:不回答的结论看起来正确,但实际上是错误的。答案就在那里;管道只是从未看到它。
好消息是,解析模块(第5B条,关系数据模型)已经产生了缺席声明所需的确切数据。parse_pdf的输出不是一堵文本墙;而是一小组关系型DataFrame:文档包含的每种事物各一个。line_df是核心表:整个文档中每行文本一行,包含页码、页面上的边界框以及检测到的行类型(散文、标题、表格行、图注)。其他表都挂在它上面。
解析输出一组关系型DataFrame;主要扫描使用其中三个 - 作者图片
对于不回答的结论,我们不编写新的解析函数。我们聚合已经存在的表:
python
out = parse_pdf(pdf_path) # 给出 line_df, image_df, page_df, toc_df, ...# 解析模块的证据是对这些表的一些聚合。parse_coverage = { "pages_total": len(out["page_df"]), "pages_with_text": out["line_df"]["page_num"].nunique(), "images_total": len(out["image_df"]), "toc_entries": len(out["toc_df"]), "cross_refs_total": len(out["cross_ref_df"]), "objects_registered": len(out["object_registry"]),}# 在CMO 2025年4月期上:63页,63页有文本,426个图像注册,# 9个目录条目,76个正文交叉引用。聚合命名了缺席声明必须覆盖的表面——line_df中的文本是容易的部分,其他行是图像文本和命名对象(图、表、附件)的审计线索。
缺席声明在line_df之外仍需检查的三个地方:
携带文本的图像:图表通常将轴标签和图例作为图像嵌入文本,而不是排版跨度。image_df注册表列出了每个这样的图像;OCR过程向该表添加ocr_text列,而不触及line_df。解析模块暴露前后状态;覆盖率报告说明有多少图像被OCR处理。
表格:单元格中的数字解析为一行,其邻居是同一行的其他单元格,而不是页面上方的单词。如果答案是“AI在2024年消耗了460太瓦时”,而该数字位于表格中,且附近任何单元格都没有“AI”一词,那么对AI的扫描会错过它。object_registry标记哪些页面包含表格,以便检索模块可以在那里进行列感知扫描(第5条关于表格解析的部分)。
未解析的交叉引用。cross_ref_df列出了正文中每个“见附录B”、“图3.D”、“表11.E”的提及。任何目标不在object_registry中的行都是解析缺口:正文承诺了一个提取未找到的对象。每个缺口都是答案可能隐藏的候选位置。
因此,解析模块的证据是从它已经产生的表中派生的小摘要,而不是新组件。不回答的结论可以声明:“解析了63页,63页有文本,0个未解析
的交叉引用。”这足以让用户相信解析层完成了它的工作。
3. 问题解析:枚举词汇表
检索步骤的好坏取决于它扫描的关键词。如果专家列出了AI但忘记了人工智能,检索将错过每一个拼出该概念的文本行。问题解析模块的工作是将关键词集推向穷尽,以专家为真理来源,并在其下设置算法安全网。
3.1 专家了解词汇
在企业RAG中,专家几乎总是知道其领域的关键词。这是简单的情况,覆盖了大多数问题。他们列出答案会提到的概念,并为每个概念列举变体:同义词、缩写、首字母缩略词,如果语料库是多语言的,还包括英语/法语/德语形式。
对于CMO / AI电力示例,专家提供此表:
来自第6条的concept_keywords_df模式应用于“无答案”问题 - 作者图片
这正是第6条中的concept_keywords_df模式。驱动正常检索的相同数据结构也驱动缺席声明。签名相同;只有输出的解释发生变化。
3.2 概念聚类作为安全网
罕见的情况是专家不确定他们是否列出了所有变体。安全网是算法性的:通过嵌入相似性对语料库标记进行聚类,然后暴露专家关键词所在的聚类,并询问是否应添加任何聚类成员。
python
def expand_keywords_via_clustering( seed_keywords: list[str], corpus_tokens: list[str], *, n_neighbors: int = 10, cosine_floor: float = 0.85,) -> list[KeywordCandidate]: """对于每个种子关键词,返回语料库中余弦相似度高于下限的前N个最近标记。专家审查候选并接受或拒绝每个。""" seed_vecs = {k: embed(k) for k in seed_keywords} corpus_vecs = {t: embed(t) for t in corpus_tokens} candidates = [] for seed, sv in seed_vecs.items(): scored = [(t, cosine(sv, tv)) for t, tv in corpus_vecs.items()] scored.sort(key=lambda x: -x[1]) for t, sim in scored[:n_neighbors]: if sim >= cosine_floor and t not in seed_keywords: candidates.append(KeywordCandidate(seed=seed, candidate=t, similarity=sim)) return candidates
输出是一个列表,而不是一个决定。专家决定添加什么。候选是考虑的建议,而不是专家判断的替代品。该系列的编辑立场是:增强专家,而不是取代他们。聚类揭示了专家可能没有想到的;专家验证哪些属于关键词集。
由专家签署的关键词集是问题解析模块的证据。
4. 检索:扫描,而非top-k
对于正常问题(第7条),检索返回top-k:最可能包含答案的几页或几行。对于缺席声明,这种框架失败。系统无法通过查看前10页来证明答案不存在。它必须查看提及任何概念的每一页。
因此,检索调用的形状发生变化:
python
def sweep_for_absence( line_df: pd.DataFrame, concept_keywords_df: pd.DataFrame,) -> pd.DataFrame: """返回语料库中每个(概念、页面、行)匹配的一行。不是top-k。不是评分。每个概念的每个变体的每次命中。""" rows = [] for concept_row in concept_keywords_df.itertuples(): pattern = r"\b(" + "|".join(map(re.escape, concept_row.variants)) + r")\b" matched = line_df[line_df["text"].str.contains(pattern, regex=True, case=False, na=False)] for line in matched.itertuples(): rows.append({ "concept": concept_row.concept, "variant": extract_first_match(pattern, line.text), "page": line.page_num, "line": line.line_num, "snippet": line.text[:120], }) return pd.DataFrame(rows)
返回值是一个DataFrame,而不是排名列表。每一行是一个证据条目:“在第P页,第L行,概念C的变体V在片段S中找到”。结果基数是不回答结论所依赖的:
某个概念零行(任何变体都没有出现)→ 强烈证据表明文档根本不涵盖该概念。
几行但从未共现(概念出现在不同页面,从不在一起)→ 中等证据:概念存在,但没有段落讨论它们的交集。
共现行(多个概念在同一页或同一段)→ 缺席的弱证据。生成将不得不查看片段并决定它们是否回答问题或仅触及问题。
在CMO 2025年4月案例中,结果是明确的。对上述关键词集的完整扫描返回:
对报告7,829行的真实扫描:AI零命中,电力少量命中,无共现 - 作者图片
命中列表是检索模块的证据。在这个问题和这个文档上,结论不言自明。
5. 生成:带有理由的结构化“无答案”
最后一个模块获取解析覆盖率报告、验证过的关键词集和命中列表,并将它们转化为用户看到的实际响应。模式镜像了第8条中的AnswerWithEvidence,但活动字段不同。
python
class AbsenceJustification(BaseModel): """当系统无法从语料库回答时返回。与第8条的AnswerWithEvidence配套。""" answer: None = None # 此模式始终为None reason: Literal[ "concept_not_found", # 任何概念的任何变体都没有命中任何内容 "concept_found_but_off_topic", # 概念存在但从未共现 "ambiguous_question", # 问题本身太模糊
无法扫描 ] searched_concepts: list[ConceptSearch] # 查找了什么,带有命中计数 closest_mentions: list[Mention] # 任何附近的片段,带有原因 parse_coverage: ParseCoverage # 解析模块覆盖了什么 suggestion: str | None = None # 可选的重述或语料库外指针class ConceptSearch(BaseModel): concept: str variants: list[str] pages_with_hits: list[int] line_hits: intclass Mention(BaseModel): page: int line: int snippet: str why_not_an_answer: str # 一句话:为什么这个段落接近但不回答
生成步骤不发明任何字段。模式的前三个字段直接来自前面的模块(解析覆盖率、关键词集、命中列表)。模型的工作更窄:选择原因,选择哪些提及标记为最接近,为每个写一句话的“为什么这不是答案”,并可选地建议重述。
在CMO / AI电力案例中,模型的输出如下:
json
{ "answer": null, "reason": "concept_not_found", "searched_concepts": [ { "concept": "AI", "variants": [ "AI", "artificial intelligence", "machine learning", "deep learning", "neural network", "GenAI", "generative AI", "LLM", "large language model" ], "pages_with_hits": [], "line_hits": 0 }, { "concept": "electricity consumption", "variants": [ "electricity", "power consumption", "energy use", "kWh", "MWh", "TWh", "electrical load", "power demand", "electricity demand" ], "pages_with_hits": [30, 31], "line_hits": 7 }, { "concept": "data center", "variants": [ "data center", "data centre", "datacenter", "server farm", "hyperscaler", "cloud infrastructure" ], "pages_with_hits": [], "line_hits": 0 } ], "closest_mentions": [ { "page": 30, "line": 89, "snippet": "growing power demand in EMDEs is expected to", "why_not_an_answer": "提及一个国家在煤炭需求背景下的电力需求增长;AI行业不是主题,也没有给出AI消耗的数字。" } ], "parse_coverage": { "pages_total": 63, "pages_with_text": 63, "pages_ocred": 0 }, "suggestion": "《大宗商品市场展望》涵盖石油、农业、金属和化肥;它不讨论AI行业的能源需求。有关AI电力消耗的数字,请参阅IEA的《电力2024》报告或专门的AI能源展望。"}
阅读此输出的用户同时了解到三件事。系统确实以九种不同的名称搜索了AI,并在63页的任何地方都找到了零命中。有一段与电力相关的段落,但它涉及印度煤炭驱动的电力需求,而不是AI。如果他们想要答案,CMO是错误的问题语料库。不回答现在是一个有用的响应。
6. 此框架的边界
上述框架不能干净处理的三个边缘情况,按重要性排序。
部分答案:一个问题可能部分答案在语料库中,部分在外部。“欧盟如何根据《AI法案》监管AI,以及这与美国政策相比如何?”在一个仅限欧盟的法律语料库上。检索模块将返回欧盟方面的命中,而美国方面没有。正确的响应既不是自信的“是”,也不是干净的“否”;而是一个结构化的部分答案,显示找到的内容,并明确未找到的内容。该模式是AnswerWithEvidence和AbsenceJustification的第三个兄弟,具有answer_partial: str和missing_concepts: list[ConceptSearch]字段。
模糊问题:“覆盖范围怎么样?”在没有上下文的情况下提出。系统无法扫描,因为没有定义的概念集。正确的响应是澄清请求,而不是不回答。区分两者的信号是问题的解析(第6条):如果问题解析器无法提取概念,那么问题本身是问题,而不是语料库。
敌对或超出范围的问题:“生命的意义是什么?”在保险单语料库上。系统可以正确地说不回答,但花费精力在解析覆盖率报告和最接近提及上是浪费的。管道应该有一个上游的“此问题是否在范围内”检查,当问题解析器将概念标记为与任何语料库标签无关时,短路完整的扫描。当此类查询频繁时,管道成本很重要。
这些情况共享一个结构:上述不回答模式是缺席声明的正确形状,但它不是每个“我无法帮助”输出的正确形状。将其视为三个兄弟(是/部分/否)之一,而不是单个回退,可以干净地处理变化。
7. 结论
一个可辩护的“无答案”不是一句话;它是四个模块共同产生的一系列证据。解析报告覆盖率,问题解析报告专家关键词集,检索报告扫描,生成报告最接近的提及及其不回答的原因。用户看到工作,并可以质疑解析、关键词集、最接近的提及。
与系列其余部分的对称性是更大的观点。当模式强制模型引用其证据时,“是”答案是可验证的(第8条);当模式强制管道暴露其搜索时,“否”答案是可验证的。这是对相同模块的相同纪律,两个输出都结构化落地。
8. 来源和进一步阅读
将“无答案”确立为一等输出的规范基准是Rajpurkar等人(SQuAD 2.0,ACL 2018)。许多问题在给定段落内无法回答的框架是Choi等人(QuAC,EMNLP 2018)。此处使用的管道侧扫描的模型侧反射标记类似物是Asai等人(Self-RAG,ICLR 2024)。本文的框架:三兄弟答案模式(是/部分/否),在无答案分支上具有可辩护的证据链、解析覆盖率报告、专家关键词扫描、最接近提及,每一步都可审计。
系列早期文章:
文档智能:系列介绍。该系列逐步构建什么,以及顺序。
什么有效,什么失效
基线企业RAG,从PDF到高亮答案。四模块管道端到端:PDF输入,高亮答案输出。
嵌入不是魔法:RAG检索的可预测失败模式。嵌入相似性在哪些方面获胜(同义词、拼写错误、释义),在哪些方面可预测地失败(未知术语、否定、术语与答案相关性),以及如何仍然使用它。
RAG不是机器学习,ML工具包解决了错误的问题。为什么块大小扫描和微调优化了错误的东西;改为按问题类型路由。
从正则表达式到视觉模型:哪种RAG技术适合哪个问题。两个轴,文档复杂性和问题控制,为每种情况选择技术。
我们在生产中反复看到的10个常见RAG错误。十个生产错误,按模块组织,每个都有修复方法。
文档解析
使用循环工程构建文档结构:从正文排版恢复PDF大纲以用于RAG。当PDF没有目录页时,从正文排版重建大纲:六个信号,一个有界循环。
在完全代理式RAG之前:了解你如何决策,以及你选择的解析方法。解析方法作为目录,以及在将循环交给代理之前决定运行哪个。
生成
大多数RAG幻觉是提取错误:类型化生成契约的七种模式。模型在提取上出错的七种反复出现的方式,以及捕获每种方式的类型化契约。
RAG生成的循环工程:从廉价的本地模型到托管旗舰的LLM级联。从廉价的本地模型开始,仅在答案不成立时升级,并进行测量。
单文档管道
通过减少调用LLM来削减企业RAG管道的延迟和成本,而不是购买更快的模型。通过减少调用模型的频率和成本来削减管道的延迟和成本,而不是购买更快的模型。
RAG工作流和循环工程:决定何时循环和何时停止的调度器。反馈循环、有界迭代和调度器,组合成一个工作流。
RAG的循环工程:每个步骤内部的小循环,跨管道的大循环。循环的两个尺度:每个模块内部的小有界循环,跨模块的生成触发的大循环。
从单文档到语料库
三种RAG语料库,以及为错误类型构建的成本。三个问题告诉你文档集合的形状,以及为错误类型构建的代价。
深入探讨AI的财务可持续性
人工智能
AI代币的预算不可能是无限的,无论超大规模企业多么希望如此
Stephanie Kirmer
2026年6月16日7分钟阅读
使用LangChain、Google Maps API和Gradio构建智能旅行行程建议器(第1部分)
大型语言模型
了解如何构建一个可能激发你下一次公路旅行的应用程序
Robert Martin-Short
2023年9月26日15分钟阅读
微调BERT用于文本分类
深度学习
一个带有Python代码的可破解示例
Shaw Talebi
2024年10月17日6分钟阅读
LLM是随机算法
大型语言模型
最新AI模型与一个50年历史的学术领域之间的惊人联系
Udayan Kanade
2025年11月13日15分钟阅读
你的RAG系统检索到正确的数据——但仍然产生错误的答案。原因如下(以及如何修复)。
大型语言模型
即使检索完美,冲突的上下文也会破坏答案。这是大多数生产RAG系统忽略的隐藏失败模式——以及如何修复它。
Emmimal P Alexander
2026年4月18日16分钟阅读
ChatLLM为应对AI中的真正瓶颈提供了简化解决方案
人工智能
通过将GPT-5、Claude、Gemini、DeepSeek、Grok、Llama等的访问整合到一个订阅中,ChatLLM帮助预算有限的用户用单个灵活平台替换多个AI工具
Marcus Dawson
2025年12月22日7分钟阅读
如何微调LLM:端到端指南
大型语言模型
为现实世界微调LLM的实践指南
Sam Black
2026年8月20日26分钟阅读
亲手深入探讨Sora的扩散Transformer(DiT)✍︎
大型语言模型
探索Sora最先进视频背后的秘密
Srijanie Dey, PhD
2024年4月2日12分钟阅读
GenAI应用的构建与购买困境
战略
GenAI的构建与购买战略指南
Anurag Bhagat
2025年1月7日6分钟阅读
查看原文(中文正文为 AI 翻译)
The most useful answer a RAG system can give is sometimes “that is not in this document.” But when asked a question, a model tends to answer anyway, so the honest “not found” has to be built in, not hoped for. Getting the system to say no, and to be right when it does, is harder than getting it to say yes.
This article is a bonus in Enterprise Document Intelligence, a series that builds an enterprise RAG system from four bricks. It justifies the “I don’t know”: a confident wrong answer is a bug, a bare no-answer is almost as bad, and each of the four bricks has one piece of evidence to show.
🧭 New to the series? Every article in this series sits on our two Towards Data Science author pages, Angela Shi and Kezhan Shi. That is the shortest way to see what is covered and where this one sits.
where this article sits in the series: a bonus article alongside the numbered spine - Image by author
📓 Runnable companion notebooks are on GitHub: doc-intel/notebooks-vol1.
The public companion-code repo at doc-intel/notebooks-vol1 - Image by author
Ask the corporate chatbot “How much electricity does AI consume globally?” and it returns “I cannot find an answer in this document.” Rephrase and ask again: same answer. At that point most users give up and go to Google.
The system did the right thing. The document it runs on is the World Bank’s Commodity Markets Outlook (April 2025 issue; CC BY 3.0 IGO, as declared on the World Bank Open Knowledge Repository terms of use which states the default license for pre-2023 publications), a 63-page quarterly report on oil, agriculture, and metals prices. AI electricity consumption is not in there. There is no answer to find. The system was not lying.
But the user did not learn anything useful. They cannot tell whether the pipeline looked everywhere it should have, or whether it gave up after one shallow embedding search. “I cannot find an answer” is a verdict that needs to come with a defense. Otherwise it reads as failure, not as a fact.
The series argued that structured output (Article 8) makes “yes” answers verifiable, by forcing the model to cite its evidence: a quote, a page number, a confidence. The same logic applies to “no” answers, and the same four bricks contribute, one piece of evidence each. This article walks the four bricks once more, on a question whose right output is no, and shows what each brick has to produce so the no-answer is defensible.
1. The four bricks, four kinds of evidence
The argument compresses to one table.
Four bricks, four kinds of evidence, none of them optional for a defensible verdict - Image by author
The rest of the article walks each row in order, then ties them together on the CMO / AI electricity-consumption case.
2. Parsing: the relational tables are the evidence
The cost of a missing parse is asymmetric. A false positive (extracting noise) makes retrieval messier but is recoverable downstream. A false negative (failing to extract a real token) is silent: the no-answer verdict will look correct, but it is wrong. The answer was there; the pipeline just never saw it.
The good news is that the parsing brick (Article 5B, the relational data model) already produces exactly the data the absence claim needs. The output of parse_pdf is not a wall of text; it is a small relational set of DataFrames: one per kind of thing the document contains. line_df is the central table: one row per text line across the whole document, with its page number, bounding box on the page, and the detected line type (prose, heading, table row, figure caption). The other tables hang off it.
Parsing emits a relational set of DataFrames; the primary sweep uses three of them - Image by author
For a no-answer verdict, we do not write a new parsing function. We aggregate over the tables that already exist:
python
out = parse_pdf(pdf_path) # gives line_df, image_df, page_df, toc_df, ...# The parsing brick's piece of evidence is a few aggregations over the tables.parse_coverage = { "pages_total": len(out["page_df"]), "pages_with_text": out["line_df"]["page_num"].nunique(), "images_total": len(out["image_df"]), "toc_entries": len(out["toc_df"]), "cross_refs_total": len(out["cross_ref_df"]), "objects_registered": len(out["object_registry"]),}# On CMO April 2025: 63 pages, 63 with text, 426 image registrations,# 9 TOC entries, 76 in-body cross-references. The aggregates name the# surfaces an absence claim must walk -- text in line_df is the easy# one, the other rows are the audit trail for image text and named# objects (figures, tables, annexes).
The three places an absence claim still needs to check beyond line_df:
Images that carry text: Charts often hold their axis labels and legends as image-embedded text rather than typographic spans. The image_df registry lists every such image; an OCR pass adds an ocr_text column to that table without touching line_df. The parsing brick exposes both before and after states; the coverage report names how many images were OCR’d.
Tables: A number in a cell parses as a line whose neighbours are the other cells of the same row, not the words above it on the page. If the answer is “AI consumed 460 TWh in 2024” and the figure sits in a table without the word AI in any nearby cell, a sweep on AI misses it. object_registry flags which pages host tables so the retrieval brick can do a column-aware sweep there (Article 5 section on table parsing).
Cross-references that did not resolve. cross_ref_df lists every “see annex B”, “figure 3.D”, “table 11.E” mention in the body. Any row whose target is not in object_registry is a parsing gap: the body promises an object the extraction did not find. Each gap is a candidate spot where the answer could be hiding.
The parsing brick’s piece of evidence is therefore a small summary derived from the tables it already produces, not a new component. The no-answer verdict can state: “63 pages parsed, 63 with text, 0 unresolved cross-references.” That is enough for the user to trust the parsing layer did its work.
3. Question parsing: enumerate the vocabulary
The retrieval step is only as good as the keywords it sweeps on. If the expert lists AI but forgets artificial intelligence, the retrieval misses every line that spells the concept out. The work of the question-parsing brick is to push the keyword set to exhaustion, with the expert as the source of truth and an algorithmic safety net underneath.
3.1 The expert knows the vocabulary
In enterprise RAG, the expert almost always knows the keywords of their domain. This is the simple case and it covers most questions. They list the concepts that the answer would mention, and for each concept they enumerate the variants: synonyms, acronyms, abbreviations, English / French / German forms if the corpus is multilingual.
For the CMO / AI electricity example, the expert provides this table:
The concept_keywords_df schema from Article 6 applied to a ‘no answer’ question - Image by author
This is exactly the concept_keywords_df schema from Article 6. The same data structure that drives normal retrieval also drives the absence claim. The signature is identical; only the interpretation of the output changes.
3.2 Concept clustering as a safety net
The rare case is the expert who isn’t sure they listed every variant. The safety net is algorithmic: cluster the corpus tokens by embedding similarity, then expose the clusters that the expert’s keywords land in, and ask whether any cluster member should be added.
python
def expand_keywords_via_clustering( seed_keywords: list[str], corpus_tokens: list[str], *, n_neighbors: int = 10, cosine_floor: float = 0.85,) -> list[KeywordCandidate]: """For each seed keyword, return its top-N nearest tokens in the corpus above the cosine floor. The expert reviews the candidates and accepts or rejects each one.""" seed_vecs = {k: embed(k) for k in seed_keywords} corpus_vecs = {t: embed(t) for t in corpus_tokens} candidates = [] for seed, sv in seed_vecs.items(): scored = [(t, cosine(sv, tv)) for t, tv in corpus_vecs.items()] scored.sort(key=lambda x: -x[1]) for t, sim in scored[:n_neighbors]: if sim >= cosine_floor and t not in seed_keywords: candidates.append(KeywordCandidate(seed=seed, candidate=t, similarity=sim)) return candidates
The output is a list, not a decision. The expert decides what to add. The candidates are a suggestion to consider, not a replacement for the expert’s judgment. The series’ editorial position holds: amplify the expert, do not replace them. The clustering reveals what the expert may not have thought of; the expert validates what belongs in the keyword set.
The keyword set, signed off by the expert, is the question-parsing brick’s piece of evidence.
4. Retrieval: sweep, not top-k
For a normal question (Article 7), retrieval returns top-k: the few pages or lines most likely to contain the answer. For a no-answer claim, that framing fails. The system cannot prove the answer is absent by looking at the top-10 pages. It has to look at every page that mentions any of the concepts.
The shape of the retrieval call therefore changes:
python
def sweep_for_absence( line_df: pd.DataFrame, concept_keywords_df: pd.DataFrame,) -> pd.DataFrame: """Return one row per (concept, page, line) match across the corpus. NOT top-k. NOT scored. Every hit on every variant of every concept.""" rows = [] for concept_row in concept_keywords_df.itertuples(): pattern = r"\b(" + "|".join(map(re.escape, concept_row.variants)) + r")\b" matched = line_df[line_df["text"].str.contains(pattern, regex=True, case=False, na=False)] for line in matched.itertuples(): rows.append({ "concept": concept_row.concept, "variant": extract_first_match(pattern, line.text), "page": line.page_num, "line": line.line_num, "snippet": line.text[:120], }) return pd.DataFrame(rows)
The return value is a DataFrame, not a ranked list. Each row is an EVIDENCE entry: “on page P, line L, the variant V of concept C was found in the snippet S”. The cardinality of the result is what the no-answer verdict turns on:
Zero rows for some concept (no variant appears anywhere) → strong evidence the document does not cover that concept at all.
Several rows but never co-located (concepts appear on different pages, never together) → medium evidence: the concepts exist but no passage talks about their intersection.
Co-located rows (multiple concepts in the same page or paragraph) → weak evidence of absence. Generation will have to look at the snippets and decide whether they answer the question or only touch it.
On the CMO April 2025 case, the result is unambiguous. The full sweep on the keyword set above returns:
Real sweep over the 7,829 lines of the report: zero AI hits, a handful on electricity, none co-located - Image by author
The hit-list is the retrieval brick’s piece of evidence. On this question and this document, the verdict writes itself.
5. Generation: structured “no answer” with justification
The last brick takes the parse coverage report, the validated keyword set, and the hit-list, and turns them into the actual response the user sees. The schema mirrors AnswerWithEvidence from Article 8, but the active fields are different.
python
class AbsenceJustification(BaseModel): """Returned when the system cannot answer from the corpus. Companion to AnswerWithEvidence from Article 8.""" answer: None = None # always None for this schema reason: Literal[ "concept_not_found", # no variant of any concept hit anything "concept_found_but_off_topic", # concepts exist but never co-locate "ambiguous_question", # question itself is too vague to sweep ] searched_concepts: list[ConceptSearch] # what was looked for, with hit counts closest_mentions: list[Mention] # any nearby snippets, with why-not parse_coverage: ParseCoverage # what the parsing brick covered suggestion: str | None = None # optional reformulation or off-corpus pointerclass ConceptSearch(BaseModel): concept: str variants: list[str] pages_with_hits: list[int] line_hits: intclass Mention(BaseModel): page: int line: int snippet: str why_not_an_answer: str # one sentence : why this passage is near but does not answer
The generation step does not invent any of the fields. The schema’s first three fields come straight from the previous bricks (parse coverage, the keyword set, the hit-list). The model’s job is narrower: pick the reason, choose which mentions to flag as closest, write the one-sentence “why this is not an answer” for each, and optionally suggest a reformulation.
On the CMO / AI electricity case, the model’s output looks like this:
json
{ "answer": null, "reason": "concept_not_found", "searched_concepts": [ { "concept": "AI", "variants": [ "AI", "artificial intelligence", "machine learning", "deep learning", "neural network", "GenAI", "generative AI", "LLM", "large language model" ], "pages_with_hits": [], "line_hits": 0 }, { "concept": "electricity consumption", "variants": [ "electricity", "power consumption", "energy use", "kWh", "MWh", "TWh", "electrical load", "power demand", "electricity demand" ], "pages_with_hits": [30, 31], "line_hits": 7 }, { "concept": "data center", "variants": [ "data center", "data centre", "datacenter", "server farm", "hyperscaler", "cloud infrastructure" ], "pages_with_hits": [], "line_hits": 0 } ], "closest_mentions": [ { "page": 30, "line": 89, "snippet": "growing power demand in EMDEs is expected to", "why_not_an_answer": "Mentions a country's rising electricity needs in a coal-demand context ; the AI sector is not the subject and no figure is given for AI consumption." } ], "parse_coverage": { "pages_total": 63, "pages_with_text": 63, "pages_ocred": 0 }, "suggestion": "The Commodity Markets Outlook covers oil, agriculture, metals, and fertilizers ; it does not discuss AI sector energy demand. For figures on AI electricity consumption, see the IEA 'Electricity 2024' report or a dedicated AI-energy outlook."}
A user reading this output learns three things at once. The system did look for AI under nine different names and found zero hits anywhere in 63 pages. There is one electricity-related passage, but it concerns India’s coal-driven power demand, not AI. And if they want the answer, the CMO is the wrong corpus to ask. The no-answer is now a useful response.
6. Where this stops
Three edge cases the framework above doesn’t cover cleanly, in order of importance.
Partial answers: A question may have part of its answer in the corpus and part outside it. “How does the EU regulate AI under the AI Act, and how does that compare to U.S. policy?” on an EU-only legal corpus. The retrieval brick will return hits for the EU side and nothing for the U.S. side. The right response is neither a confident yes nor a clean no; it is a structured partial answer that shows what was found and is explicit about what was not. The schema for that is a third sibling of AnswerWithEvidence and AbsenceJustification, with both answer_partial: str and missing_concepts: list[ConceptSearch] fields.
Ambiguous questions: “What about coverage?” asked without context. The system cannot sweep because there is no defined concept set. The right response is a clarification request, not a no-answer. The signal that distinguishes the two is the parsing of the question (Article 6): if the question parser cannot extract concepts, the question is the problem, not the corpus.
Hostile or out-of-scope questions: “What is the meaning of life?” on an insurance-policy corpus. The system can correctly say no-answer, but spending effort on the parse-coverage report and the closest-mentions is wasted. The pipeline should have an upstream “is this question in scope” check that short-circuits the full sweep when the question parser flags the concepts as unrelated to any corpus tag. Pipeline cost matters when this kind of query is frequent.
These cases share a structure: the no-answer schema above is the right shape for absence claims, but it is not the right shape for every “I cannot help” output. Treating it as one of three siblings (yes / partial / no) rather than a single fallback handles the variation cleanly.
7. Conclusion
A defensible “no answer” is not a single sentence; it is a chain of evidence the four bricks produce together. Parsing reports coverage, question parsing reports the expert keyword set, retrieval reports the sweep, generation reports the closest mention and why it does not answer. The user sees the work and can dispute the parse, the keyword set, the closest mention.
The symmetry with the rest of the series is the bigger point. Yes answers are verifiable when the schema forces the model to cite its evidence (Article 8); no answers are verifiable when the schema forces the pipeline to expose its search. It is the same discipline on the same bricks, and both outputs land structured.
8. Sources and further reading
The canonical benchmark establishing “no answer” as a first-class output is Rajpurkar et al. (SQuAD 2.0, ACL 2018). The framing where many questions are unanswerable inside the given passage is Choi et al. (QuAC, EMNLP 2018). The model-side reflection-token analogue of the pipeline-side sweep used here is Asai et al. (Self-RAG, ICLR 2024). The article’s framing: the three-sibling answer schema (yes / partial / no) with a defensible evidence chain on the no-answer branch, parse-coverage report, expert-keyword sweep, closest-mentions, every step auditable.
Earlier in the series:
Document Intelligence: series intro. What the series builds, brick by brick, and in what order.
What works, what breaks
Baseline Enterprise RAG, from PDF to highlighted answer. The four-brick pipeline end to end: PDF in, highlighted answer out.
Embeddings Aren’t Magic: The Predictable Failure Modes of RAG Retrieval. Where embedding similarity wins (synonyms, typos, paraphrase), where it predictably breaks (unknown terms, negation, term-vs-answer relevance), and how to use it anyway.
RAG is not machine learning, and the ML toolkit solves the wrong problem. Why chunk-size sweeps and finetuning optimize the wrong thing; route by question type instead.
From regex to vision models: which RAG technique fits which problem. Two axes, document complexity and question control, that pick the technique for each case.
10 common RAG mistakes we keep seeing in production. Ten production mistakes, organized brick by brick, with the fix for each.
Document parsing
Building Document Structure with Loop Engineering: Recovering a PDF’s Outline from Body Typography for RAG. Rebuilding the outline from body typography when the PDF ships no contents page at all: six signals, one bounded loop.
Before Full Agentic RAG: Know How You Decide, and the Parsing Methods You Pick From. The parsing methods as a catalogue, and the decision of which to run, before handing the loop to an agent.
Generation
Most RAG Hallucinations Are Extraction Errors: Seven Patterns for a Typed Generation Contract. Seven recurring ways a model gets the extraction wrong, and the typed contract that catches each one.
Loop engineering for RAG generation: an LLM cascade from a cheap local model up to a hosted flagship. Starting on a cheap local model and escalating only when the answer does not hold up, measured.
One-document pipelines
Cut an Enterprise RAG Pipeline’s Latency and Cost by Calling the LLM Less, Not by Buying a Faster Model. Cutting a pipeline’s latency and cost by calling the model less often and cheaper, not by buying a faster one.
RAG workflow and loop engineering: the dispatcher that decides when to loop and when to stop. Feedback loops, bounded iteration, and the dispatcher, composed into one workflow.
Loop engineering for RAG: the small loops inside each step, the big loops across the pipeline. The two scales of loop: small bounded loops inside each brick, big generation-triggered loops across them.
One doc to a corpus
Three Kinds of RAG Corpus, and What It Costs to Build for the Wrong One. Three questions that tell you which shape a document collection has, and the price of building for the wrong one.
Drilling Into AI’s Financial Sustainability
Artificial Intelligence
Budgets for AI tokens can’t be infinite, no matter how much hyperscalers wish they were
Stephanie Kirmer
June 16, 20267 min read
Building a Smart Travel Itinerary Suggester with LangChain, Google Maps API, and Gradio (Part 1)
Large Language Models
Learn how to build an application that might inspire your next road trip
Robert Martin-Short
September 26, 202315 min read
Fine-Tuning BERT for Text Classification
Deep Learning
A hackable example with Python code
Shaw Talebi
October 17, 20246 min read
LLMs Are Randomized Algorithms
Large Language Models
A surprising connection between the newest AI models and a 50-year old academic field
Udayan Kanade
November 13, 202515 min read
Your RAG System Retrieves the Right Data — But Still Produces Wrong Answers. Here’s Why (and How to Fix It).
Large Language Models
Even with perfect retrieval, conflicting context breaks answers. This is the hidden failure mode most production RAG systems ignore — and how to fix it.
Emmimal P Alexander
April 18, 202616 min read
ChatLLM Presents a Streamlined Solution to Addressing the Real Bottleneck in AI
Artificial Intelligence
By consolidating access to GPT-5, Claude, Gemini, DeepSeek, Grok, Llama, and more into one subscription, ChatLLM helps budget-conscious users replace multiple AI tools with a single, flexible platform
Marcus Dawson
December 22, 20257 min read
How to Fine-Tune an LLM: An End-to-End Guide
Large Language Models
A hands-on guide to fine-tuning LLMs for the real world
Sam Black
August 20, 202626 min read
Deep Dive into Sora's Diffusion Transformer (DiT) by Hand ✍︎
Large Language Models
Explore the secret behind Sora's state-of-the-art videos
Srijanie Dey, PhD
April 2, 202412 min read
The Build vs. Buy Dilemma for GenAI Applications
Strategy
A strategic guide to build vs. buy for GenAI
Anurag Bhagat
January 7, 20256 min read