Publish AI, ML & data-science insights to a global community of data professionals.

RAG Questions Need Parsing Too: Turn the User’s String Into Briefs for Retrieval and Generation

Enterprise Document Intelligence [Vol.1 #6a] – Why a user question deserves the same parsing as the document, and how it splits into a retrieval brief and a generation brief before either runs

Photo by Christoph Georges, via Pexels.

This article is the 本文是question-parsing brick of Enterprise Document Intelligence, a series that builds an enterprise RAG system from four bricks: parsing, question parsing, retrieval, and generation.本文是企业文档智能系列中的问题解析模块,该系列通过四个模块构建企业RAG系统:解析、问题解析、检索和生成。

Question parsing is the second brick. This is the first of its three parts:问题解析是第二个模块。这是其三个部分中的第一部分:

  • why it exists, what it produces, and the split that motivates the rest.它为何存在、它产生什么,以及推动其余部分的拆分。
  • The next part covers what the parser extracts from a user string (Article 6 B, extraction) and下一部分介绍解析器从用户字符串中提取的内容(第6条B,提取)以及
  • how the parsed row is dispatched to retrieval and generation (Article 6 C, dispatch).如何将解析后的行分派到检索和生成(第6条C,分派)。
where this article sits in the series: Article 6 (question parsing), inside Part II (the four bricks) – Image by author本文在系列中的位置:第6条(问题解析),第二部分(四个模块)内——作者图片

Article 1 (minimal RAG) showed the basic pattern: take the user’s question, send it to an LLM, and get an answer back.第1条(最小RAG)展示了基本模式:获取用户问题,发送给LLM,然后得到答案。

The series pushed it a step further with prompt engineering, asking the LLM to return a JSON object with the answer plus the source passages it used. With a well-parsed document and structured output, finding where the answer comes from is no longer the hard part.该系列通过提示工程更进一步,要求LLM返回一个包含答案及其所用源段落的JSON对象。有了良好解析的文档和结构化输出,找到答案来源不再是难题。

That’s the starting point. What’s left is the question itself, and that’s where most of the work in a real RAG pipeline ends up living. Three levels of “working the question” show up in enterprise projects, and they tend to arrive in this order.这是起点。剩下的就是问题本身,而这正是实际RAG管道中大部分工作所在。在企业项目中,“处理问题”的三个层次通常按此顺序出现。

1. Natural questions from users. A user types whatever comes to mind: “What’s the cap on liability?”, “Tell me about the exclusions”, “How does this compare to last year’s policy?” The system has to make sense of a string it didn’t author, in vocabulary that isn’t necessarily the document’s. This is the case most demos show, and the one with the most variance on the input side.1. 用户的自然问题。用户输入任何想到的内容:“责任上限是多少?”“告诉我除外条款。”“这与去年的保单相比如何?”系统必须理解它未编写的字符串,且词汇不一定与文档一致。这是大多数演示展示的情况,也是输入侧变化最大的情况。

2. Templates the developer writes ahead of time. Pretty quickly, the team notices that the same questions come back over and over. For every contract, someone wants to know who the client is, who the insurer is, the annual premium, the guarantees, the exclusions, the renewal date. Rather than wait for users to type those one by one, the developer writes them once, jointly with the business team that owns the documents, and runs them across the whole corpus. The output is a JSON record per document. In practice this is what most companies build first; free-form chat tends to come later, on top of this base.2. 开发人员预先编写的模板。很快,团队注意到相同的问题反复出现。对于每份合同,有人想知道客户是谁、保险人是谁、年保费、保证、除外条款、续保日期。与其等待用户逐一输入,开发人员与拥有文档的业务团队一起编写一次,然后在整个语料库中运行。输出是每个文档的JSON记录。实际上,这是大多数公司首先构建的;自由形式的聊天通常在此基础上后来添加。

3. Helping users formulate. A pattern that shows up once a system is in front of real users: business teams know what they’re after, but have trouble phrasing it tightly. “What’s the cap?” leaves a lot open. The system can step in and ask back: “do you mean liability, indemnification, deductible, or premium?” Same machinery as the other two levels (a typed object with named fields), but now the empty fields drive a small dialogue with the user.3. 帮助用户表述。当系统面向真实用户时出现的一种模式:业务团队知道他们想要什么,但难以精确表述。“上限是多少?”留下很多不确定性。系统可以介入并反问:“你是指责任、赔偿、免赔额还是保费?”与另外两个层次相同的机制(带有命名字段的类型化对象),但现在空字段驱动与用户的小型对话。

The three levels share the same machinery: turn the question into a structured object, then route the parts to retrieval and to generation. What changes is who fills the structure (the user, the developer, or both in dialogue) and what the system does when a field is empty (proceed with defaults, fail loudly, or ask back).这三个层次共享相同的机制:将问题转化为结构化对象,然后将各部分路由到检索和生成。变化的是谁填充结构(用户、开发人员或两者对话),以及当字段为空时系统做什么(使用默认值继续、大声失败或反问)。

A user types one string into the chat box. “What is the maximum coverage amount? Don’t confuse it with the deductible, they’re often listed together.”用户在聊天框中输入一个字符串。“最高保额是多少?不要与免赔额混淆,它们通常列在一起。”

There’s a lot in that string. A topic (maximum coverage amount). An expected shape (an amount, a number). A negative cue (don’t confuse with deductible). A hint about how the document presents the answer (often listed together). If you hand the whole string to retrieval, none of those parts land where they should: the embedding pulls deductible-bearing lines closer, the format hint never reaches generation, the disambiguation gets either embedded with the rest or stripped at preprocessing. The result is a confident wrong answer (the deductible’s number, not the coverage’s), and a retriever that no observability dashboard can debug.这个字符串包含很多信息。一个主题(最高保额)。一个预期形状(金额、数字)。一个负面提示(不要与免赔额混淆)。一个关于文档如何呈现答案的提示(通常列在一起)。如果你将整个字符串交给检索,这些部分都不会落在正确的位置:嵌入将包含免赔额的行拉近,格式提示从未到达生成,消歧要么与其余部分一起嵌入,要么在预处理中被剥离。结果是自信的错误答案(免赔额的数字,而不是保额的),以及任何可观测性仪表板都无法调试的检索器。

The fix is to parse the question first. Turn the noisy user string into a typed, structured brief that downstream bricks can act on. Then split the brief in two: the retrieval brick reads what it can act on (topic, rewrites, scope), the generation brick reads what it can act on (the original wording, the format constraint, the disambiguation). Neither brick gets confused by the other’s signal. Both stay focused on what they do best.解决方法是先解析问题。将嘈杂的用户字符串转化为下游模块可以操作的类型化、结构化简报。然后将简报一分为二:检索模块读取它可以操作的部分(主题、重写、范围),生成模块读取它可以操作的部分(原始措辞、格式约束、消歧)。两个模块都不会被对方的信号混淆。两者都专注于自己最擅长的部分。

Question parsing produces one row in question_df plus satellite tables, with two derived views feeding retrieval and generation – Image by author问题解析在question_df中生成一行以及卫星表,有两个派生视图分别供给检索和生成——作者图片

1. Question parsing mirrors document parsing1. 问题解析镜像文档解析

1.1 Same approach: a relational set of tables1.1 相同方法:一组关系表

Document parsing produces a relational set: line_df with one row per text line, page_df with one row per page, toc_df with one row per TOC entry, plus a few satellites.文档解析产生一组关系表:line_df每行一个文本行,page_df每行一页,toc_df每行一个目录条目,加上一些卫星表。

Question parsing has the same goal: turn the unstructured input into structured form before the next steps run on it. The artifact is the same kind of thing: a relational set.问题解析有相同目标:在下一步运行之前将非结构化输入转化为结构化形式。产物是同类事物:一组关系表。

The shape diverges in one obvious way. A document fills line_df with hundreds or thousands of rows. A question is a single string, so it fills one row in a question_df table. The columns of that row are what the parser computes: the spell-corrected text, the extracted keywords, the type of answer expected, the pages or sections the user mentioned, the decomposition pattern, the activations the dispatcher should turn on. Adding a parsing capability is adding a column.形状在一个明显方面有所不同。文档用数百或数千行填充line_df。问题是一个字符串,因此在question_df表中填充一行。该行的列是解析器计算的内容:拼写纠正的文本、提取的关键词、期望的答案类型、用户提到的页面或章节、分解模式、分派器应激活的开关。添加解析能力就是添加一列。

The other half of the relational picture is satellite tables the question row links into. They’re as open-ended as the columns themselves: a project starts with the ones it needs, and adds others as new cases push for them.关系图的另一半是问题行链接到的卫星表。它们与列本身一样开放:项目从需要的表开始,随着新案例推动而添加其他表。

The most common one is the project’s expert keyword dictionary, split in two: concepts_df (one row per concept, with its definition) and concept_keywords_df (one row per (concept, language, keyword)). Together they hold the domain-specific synonyms: premium → prime, cotisation, tarif annuel; non-compete → restrictive covenant; side effects → adverse events. Another we’ll meet later is answer_types_df, which registers the kinds of answers questions can expect (amount, date, iban, …).最常见的是项目的专家关键词词典,分为两部分:concepts_df(每行一个概念及其定义)和concept_keywords_df(每行一个(概念、语言、关键词))。它们一起保存领域特定的同义词:premium → prime, cotisation, tarif annuel;non-compete → restrictive covenant;side effects → adverse events。另一个我们稍后会遇到的是answer_types_df,它注册问题可以期望的答案类型(金额、日期、iban等)。

Real projects usually grow more:实际项目通常会增长更多:

  • a regulations_df mapping legal codes to their texts for a legal RAG一个regulations_df,用于法律RAG将法律代码映射到其文本
  • an entity_alias_df for company name variants in a corporate corpus一个entity_alias_df,用于公司语料库中的公司名称变体
  • a unit_conversions_df in a scientific one一个unit_conversions_df,用于科学语料库

The question’s columns link to these satellites the same way line_df linked to image_df in document parsing.问题的列链接到这些卫星表,方式与文档解析中line_df链接到image_df相同。

This relational framing matters in practice. Once questions are rows in a table, you can SQL them: “how many questions of type amount did users ask last month?”, “which questions triggered a clarification request?”, “which expert-dictionary entries got hit most often?”. The question history becomes ops data, not just a log file. A follow-up storage chapter develops the layout that makes question_df a first-class table.这种关系框架在实践中很重要。一旦问题成为表中的行,你就可以用SQL查询它们:“上个月用户问了多少个金额类型的问题?”“哪些问题触发了澄清请求?”“哪些专家词典条目被命中最多?”问题历史成为运营数据,而不仅仅是日志文件。后续的存储章节将开发使question_df成为一流表的布局。

A note on terminology. The usual names for this are “query understanding” and “question understanding.” Both are vague: they suggest the system understands the question, which doesn’t say much. Question parsing names what happens: take a string, return an enriched relational row.关于术语的说明。通常的名称为“查询理解”和“问题理解”。两者都很模糊:它们暗示系统理解问题,但这并没有说明太多。问题解析命名了实际发生的事情:获取一个字符串,返回一个丰富的关系行。

1.2 Where this fits in pdf_qa1.2 这在pdf_qa中的位置

Article 1 (minimal RAG) introduced the top-level call as a single function: result = pdf_qa(contract_pdf, question="What is the maximum coverage amount?"). The user passes a PDF path and a question, gets back a structured JSON answer. The name follows the <format>_<intent> convention: pdf is the format, qa the intent. Volume 2 opens both axes (excel_qa, pdf_translate, …) with a dispatcher doc_qa; Volume 1 calls the handler directly.第1条(最小RAG)将顶层调用引入为单个函数:result = pdf_qa(contract_pdf, question="最高保额是多少?")。用户传递PDF路径和问题,得到结构化的JSON答案。名称遵循<格式>_<意图>约定:pdf是格式,qa是意图。第2卷通过分派器doc_qa打开两个轴(excel_qa, pdf_translate等);第1卷直接调用处理程序。

Inside, pdf_qa wires the four bricks in sequence: parse_pdfparse_question(question, doc_profile=...)retrievegenerate, then wraps the answer with an _meta audit block. The extraction companion (Article 6_b) details which columns the parser fills from the user string; the dispatch companion (Article 6_c) details how each column is routed to the brick that consumes it.在内部,pdf_qa按顺序连接四个模块:parse_pdf → parse_question(question, doc_profile=...) → retrieve → generate,然后用_meta审计块包装答案。提取伴侣(第6条B)详细说明解析器从用户字符串中填充哪些列;分派伴侣(第6条C)详细说明每列如何路由到消费它的模块。

The rest of this article covers the thesis: why splitting the parsed row into two consumer briefs is the right move, and the one specific case that proves it (negative cues like “not the deductible”).本文的其余部分涵盖论点:为什么将解析后的行拆分为两个消费者简报是正确的举措,以及证明这一点的一个具体案例(像“不是免赔额”这样的负面提示)。

2. Two consumer briefs from one parsed row2. 从一个解析行生成两个消费者简报

The question row in question_df carries everything the pipeline could possibly need, but retrieval and generation don’t need the same subset. The parser emits two derived views of the row, each shaped for the stage that consumes it: RetrievalQuery and GenerationBrief. Why split them, and not just hand the full row to both?question_df中的问题行携带管道可能需要的所有内容,但检索和生成不需要相同的子集。解析器发出该行的两个派生视图,每个视图针对消费它的阶段进行塑造:RetrievalQuery和GenerationBrief。为什么拆分它们,而不是将整行交给两者?

2.1 The split between retrieval and generation2.1 检索与生成之间的拆分

Because the two consumer bricks have very different strengths:因为两个消费者模块有非常不同的优势:

  • Retrieval is similarity matching: Good at finding what’s close to the query. Bad at rejecting precisely. It cannot tell you what’s related to but not equal to the query.检索是相似性匹配:擅长找到与查询接近的内容。不擅长精确拒绝。它无法告诉你与查询相关但不等于查询的内容。
  • Generation is reading and reasoning: Good at distinguishing, excluding, contrasting. It can hold both concepts in mind, compare them, and select one. It’s the only stage in the pipeline that can do this.生成是阅读和推理:擅长区分、排除、对比。它可以同时持有两个概念,比较它们,并选择一个。它是管道中唯一能做到这一点的阶段。

Each derived view contains only the columns its consumer can act on. The retrieval brief gets the topic, rewrites in document vocabulary, anchor keywords (codes, IDs), and the scope filters that pre-filter the candidate space. The generation brief gets the original question (so the user’s intent is preserved), the format constraint, the disambiguation cues, and any distractors the LLM should not confuse with the real answer.每个派生视图只包含其消费者可以操作的列。检索简报获取主题、文档词汇的重写、锚点关键词(代码、ID)以及预过滤候选空间的范围过滤器。生成简报获取原始问题(以便保留用户意图)、格式约束、消歧提示以及LLM不应与真实答案混淆的干扰项。

The most common mistake is to send everything to both. The retrieval side gets confused by “don’t confuse with deductible” (it has no way to act on a negation); the generation side gets confused by rewrites (it should answer in the user’s terms, not the document’s). The two derived views keep each stage focused on what it can do.最常见的错误是将所有内容发送给两者。检索端被“不要与免赔额混淆”混淆(它无法处理否定);生成端被重写混淆(它应该用用户的术语回答,而不是文档的术语)。两个派生视图使每个阶段专注于它能做的事情。

2.2 Why exclusions belong to generation, not retrieval2.2 为什么排除属于生成,而不是检索

A natural reflex, when a user says “don’t include X”, is to filter X out at retrieval. Don’t. It’s almost always the wrong move.当用户说“不要包括X”时,自然的反应是在检索时过滤掉X。不要这样做。这几乎总是错误的举动。

Take a concrete example. The user asks:举一个具体例子。用户问:

“What is the limit per claim, not the deductible, in this contract?”“这份合同中每次索赔的限额是多少,不是免赔额?”

The naive approach is to remove from retrieval any passage that mentions “deductible”. Three problems, one for each level you might try to exclude at:天真的方法是从检索中删除任何提到“免赔额”的段落。三个问题,对应你可能尝试排除的每个级别:

Problem 1: line-level exclusion: Suppose you exclude any line containing “deductible”. Then you lose lines like:问题1:行级排除:假设你排除任何包含“免赔额”的行。那么你会丢失像这样的行:

“The limit per claim is €1,500,000, with a deductible of €1,000.”“每次索赔的限额为1,500,000欧元,免赔额为1,000欧元。”

That line is the answer. The exclusion just deleted it. Limit and deductible are often stated next to each other precisely because they’re related: that’s the whole reason the user warned you about the confusion.那行就是答案。排除操作刚刚删除了它。限额和免赔额经常并列出现,正是因为它们相关:这正是用户警告你混淆的原因。

Problem 2: page-level exclusion: Even worse. The page that contains the limit also contains the deductible (in the same table, in the same section). Excluding the page throws away the answer entirely.问题2:页面级排除:更糟。包含限额的页面也包含免赔额(在同一表格、同一部分中)。排除页面完全丢弃了答案。

Problem 3: section-level exclusion: Worst of all. The section “Limits and Deductibles” is, by name, exactly the section that contains the answer. Excluding it removes everything relevant.问题3:章节级排除:最糟。名为“限额和免赔额”的章节,顾名思义,正是包含答案的章节。排除它移除了所有相关内容。

Beyond granularity, there’s a bigger issue. Embeddings don’t do exclusion. Adding “NOT deductible” to the query string doesn’t work. Embeddings ignore negation, a failure mode walked through with measurements in Article 2 (embeddings’ failure modes). The vector for “limit per claim NOT deductible” is almost identical to the one for “limit per claim deductible”. You haven’t excluded anything. You’ve just added a word. BM25 with negative queries is fragile too. You can write query = "limit per claim" AND NOT "deductible" in some retrieval engines. But this excludes any passage where both words appear, including the exact passages that contain the answer alongside its contrast.除了粒度之外,还有一个更大的问题。嵌入不执行排除。在查询字符串中添加“NOT deductible”不起作用。嵌入忽略否定,这是第2条(嵌入的失败模式)中通过测量详细说明的失败模式。“limit per claim NOT deductible”的向量与“limit per claim deductible”几乎相同。你什么也没排除。你只是添加了一个词。带否定查询的BM25也很脆弱。你可以在某些检索引擎中编写query = "limit per claim" AND NOT "deductible"。但这会排除两个词同时出现的任何段落,包括包含答案及其对比的确切段落。

The right answer is simple: retrieve broadly, exclude at generation. Retrieval brings back any passage where “limit” or “deductible” or “claim” is mentioned. The generation step receives all of them, plus the explicit instruction “the user is asking about the limit, not the deductible.” The LLM then reads the passages, identifies the limit, reads the deductible, and reports only what was asked.正确的答案很简单:广泛检索,在生成时排除。检索带回任何提到“限额”或“免赔额”或“索赔”的段落。生成步骤接收所有这些段落,加上明确的指令“用户询问的是限额,而不是免赔额。”然后LLM阅读段落,识别限额,阅读免赔额,并仅报告被询问的内容。

The pattern repeats: detect the disambiguation cue at the parser, route it to the generation brief, let the LLM apply it.模式重复:在解析器处检测消歧提示,将其路由到生成简报,让LLM应用它。

Once this split clicks, the same logic applies to almost any negative instruction: “…not from the previous version” → retrieve all versions, filter at generation. “…except the optional clauses” → retrieve all, filter at generation. “…without the marketing language” → retrieve all, summarize cleanly at generation.一旦这种拆分生效,同样的逻辑适用于几乎任何否定指令:“…不是来自之前的版本”→检索所有版本,在生成时过滤。“…除了可选条款”→检索所有,在生成时过滤。“…没有营销语言”→检索所有,在生成时清晰总结。

Retrieval should be broad. Generation should be selective.检索应该广泛。生成应该选择性。

3. What comes next3. 接下来是什么

The question is parsed. The two consumer briefs are ready. Two follow-up articles close the brick:问题已解析。两个消费者简报已准备好。两篇后续文章将关闭该模块:

  • Article 6 B (extraction) walks what the parser extracts from a user string. Keywords (with several sources combined), the expected answer shape and type, scope hints, decomposition for compound questions, and the clarification field for vague inputs. Each becomes a column on question_df.第6条B(提取)介绍解析器从用户字符串中提取的内容。关键词(结合多个来源)、期望的答案形状和类型、范围提示、复合问题的分解以及模糊输入的澄清字段。每个都成为question_df上的一列。
  • Article 6 C (dispatch) walks how the parsed row is routed. The dispatch decisions (chunk strategy, model, answer context) the parser makes on top of what the user said, using the document’s profile. The activation flags that adapt the pipeline to the document. And the audit _meta block that records every decision for replay.第6条C(分派)介绍解析后的行如何路由。解析器根据用户所说的内容,使用文档的配置文件做出的分派决策(块策略、模型、答案上下文)。使管道适应文档的激活标志。以及记录每个决策以供重放的审计_meta块。

Each of those two articles stands on this one’s thesis: parse first, route by brief, never let a generation-only signal pollute retrieval.这两篇文章都基于本文的论点:先解析,按简报路由,绝不让仅用于生成的信号污染检索。

4. Sources and further reading4. 来源和进一步阅读

The two-brief shape this article argues for is a refinement of “function-calling-style” question parsing that most production RAG systems converge on once free-form chat hits its first hundred users. The right cross-reading is the original embedding-failure paper (where retrieval-side negation breaks down empirically) and the production RAG playbooks that converged independently on the same answer: broad retrieval, strict generation.本文主张的双简报形状是对“函数调用风格”问题解析的改进,大多数生产RAG系统在自由形式聊天遇到前一百个用户时会收敛于此。正确的交叉阅读是原始的嵌入失败论文(其中检索端否定在经验上失效)和生产RAG手册,它们独立地收敛于相同的答案:广泛检索,严格生成。

Same direction as the article:与本文方向相同:

  • Embeddings Aren’t Magic: The Predictable Failure Modes of RAG Retrieval. The empirical case for why retrieval cannot do exclusion: embeddings ignore negation, BM25 with negative queries is fragile, and the right place to apply “not X” is the LLM call, not the search.嵌入不是魔法:RAG检索的可预测失败模式。为什么检索不能执行排除的经验案例:嵌入忽略否定,带否定查询的BM25很脆弱,应用“不是X”的正确位置是LLM调用,而不是搜索。

Different angle:不同角度:

  • Most RAG-in-a-day tutorials treat the question as a black box passed verbatim to the embedder. The thesis here is the opposite: the question carries structure (topic, scope, shape, disambiguation) that the retrieval and generation stages need to read separately. Adopting that view is what makes the brick exist.大多数RAG一日教程将问题视为黑盒,逐字传递给嵌入器。本文的论点相反:问题携带结构(主题、范围、形状、消歧),检索和生成阶段需要分别读取。采用这种观点使该模块存在。

Earlier in the series:系列早期:

Part I:第一部分:

Part II:第二部分:


Towards Data Science is a community publication. Submit your insights to reach our global audience and earn through the TDS Author Payment Program.

Write for TDS

Related Articles

Some areas of this page may shift around if you resize the browser window. Be sure to check heading and document order.
正在发送到服务端翻译...