
The runnable code paths in this article call the OpenAI gpt-4.1 family for question parsing; that service is proprietary and governed by OpenAI’s Terms of Use.本文中的可运行代码路径调用OpenAI gpt-4.1系列进行问题解析;该服务是专有的,受OpenAI使用条款约束。
1. The fields the parser decides with the document profile1. 解析器根据文档概要决定的字段
Take a one-page CV and the question “what is the name?”. Question parsing on its own returns keywords=["name"] and retrieval looks for the literal word name in the file. A CV never says name. Nothing matches, the answer comes back empty. A human would not answer that question with nothing else to go on: they would glance at the document first, see a resume, and read name as a request for the candidate’s name. The parser needs the same starting point. As soon as it sees that the document is a resume and that the candidate’s name sits at the top of page 1, the keyword name resolves to a person, not a literal token to grep for.以一份单页简历和问题“姓名是什么?”为例。仅靠问题解析返回关键词=["name"],检索会在文件中查找字面词name。简历中从不出现name。无匹配,答案为空。人类不会在没有其他信息的情况下回答这个问题:他们会先浏览文档,看到是简历,然后将name理解为请求候选人的姓名。解析器也需要同样的起点。一旦它发现文档是简历且候选人姓名位于第1页顶部,关键词name就解析为一个人,而不是要grep的字面标记。
Two more column families get filled by the dispatcher right after question parsing returns, using the parsed question PLUS the document’s profile. In the shipped code, that profile is the semantic zone of parsing_summary, the doc-level dict produced by the parser. It carries doc_type (resume, contract, invoice, …), typical_fields (the fields questions about this kind of document usually ask about), and a short LLM-written summary that fits at the head of a system prompt. The dispatcher reads these three fields and uses them to set chunk strategy and answer context. They land on the same question_df row, so retrieval and generation see one record.在问题解析返回后,调度器立即使用解析后的问题加上文档概要填充另外两个列族。在已发布的代码中,该概要是parsing_summary的语义区域,即解析器生成的文档级字典。它包含doc_type(简历、合同、发票等)、typical_fields(关于此类文档的问题通常询问的字段)以及一段简短的LLM编写的摘要,适合放在系统提示的开头。调度器读取这三个字段,并用它们设置块策略和答案上下文。它们落在同一个question_df行上,因此检索和生成看到一个记录。
1.1 Dispatch: how much context, which chunk strategy, which model1.1 调度:多少上下文、哪种块策略、哪个模型
Once the parser has the literal info, three more decisions follow: how much surrounding text to read and return, whether to combine the top-k chunks in one LLM call or feed them in sequence, and which model to call. All three are defaults the project can override per concept, per answer type, or per question. The cascade is the same every time: concept-level override > shape/type default > project fallback.一旦解析器获得字面信息,接下来要做三个决定:读取并返回多少周围文本,是将top-k块合并到一次LLM调用中还是顺序输入,以及调用哪个模型。这三个都是项目可以按概念、答案类型或问题覆盖的默认值。级联顺序每次都相同:概念级覆盖 > 形状/类型默认值 > 项目回退。
How much context to read and return: Three fields on StructuralHints carry this:读取并返回多少上下文:StructuralHints上的三个字段承载此信息:
detection_context: the granularity of the regex confirmation zone (Article 6_b, answer shape and answer type)."line"for amounts and dates,"paragraph"for narrative.detection_context:正则确认区域的粒度(第6_b篇,答案形状和答案类型)。金额和日期用"line",叙述用"paragraph"。answer_context: how much surrounding text the generator receives."line"for a single value,"paragraph"for an explanation,"page"for a recap,"section"for a topic,"chapter"or"document"for a broad summary.answer_context:生成器接收的周围文本量。单个值用"line",解释用"paragraph",概述用"page",主题用"section",广泛摘要用"chapter"或"document"。needs_summary:Truewhen the answer spans more than fits in a verbatim quote.needs_summary:当答案跨度超过逐字引用时设为True。
Defaults come from the same two satellite tables we already met: answer_shapes_df for shape-level defaults (the default_answer_context column), concepts_df for concept-level overrides. “What is the annual premium?” is (single, amount) with no specific concept; it gets answer_context = "line" from answer_shapes_df. “What are the exclusions of this contract?” matches the exclusions concept; it gets answer_context = "chapter" from concepts_df, which overrides the listing-shape default of "section".默认值来自我们已经见过的两个卫星表:answer_shapes_df提供形状级默认值(default_answer_context列),concepts_df提供概念级覆盖。“年保费是多少?”是(single, amount)且无特定概念;它从answer_shapes_df获取answer_context = "line"。“本合同的除外责任有哪些?”匹配exclusions概念;它从concepts_df获取answer_context = "chapter",覆盖了listing形状的默认值"section"。
Why shape AND length matter beyond retrieval. The same hints feed the generation brick’s combined-vs-sequential dispatch. When the answer is a single fact in one chunk (an amount, a date, an IBAN, a yes/no), generation calls the LLM sequentially, chunk by chunk in retrieval rank order, and stops as soon as answer_found=True and complete_answer_found=True. That saves ~⅔ of the input tokens at k=3 when the answer is in the top-1 chunk. When the answer is synthesised across passages (a list of exclusions scattered across pages, a definition plus its footnote, a comparison), generation combines all k chunks into one call. The decision is made once, here, by the parser; retrieval and generation just execute. At enterprise scale (millions of documents × top-k chunks per question), the per-question saving compounds into the bulk of the LLM bill.为什么形状和长度在检索之外也重要。相同的提示为生成模块的合并与顺序调度提供信息。当答案是一个块中的单个事实(金额、日期、IBAN、是/否)时,生成按检索排名顺序逐块顺序调用LLM,并在answer_found=True且complete_answer_found=True时停止。当答案在top-1块中时,这节省了约2/3的输入token(k=3时)。当答案跨段落综合(分散在各页的除外责任列表、定义加脚注、比较)时,生成将所有k个块合并为一次调用。该决定在此处由解析器一次性做出;检索和生成仅执行。在企业规模(数百万文档×每个问题的top-k块)下,每个问题的节省累积成LLM账单的大头。
The strategy itself lives at the top level of ParsedQuestion.chunk_strategy, and the default value comes from the satellite tables, with this resolution order:策略本身位于ParsedQuestion.chunk_strategy的顶层,默认值来自卫星表,解析顺序如下:
def resolve_chunk_strategy(
answer_shape: str,
matched_concept: str | None,
answer_shapes_df: pd.DataFrame,
concepts_df: pd.DataFrame,
) -> Literal["combined", "sequential"]:
"""Concept-level override > answer-shape default > hard default."""
if matched_concept is not None:
row = concepts_df[concepts_df["concept"] == matched_concept]
if not row.empty and pd.notna(row.iloc[0].get("default_chunk_strategy")):
return row.iloc[0]["default_chunk_strategy"]
row = answer_shapes_df[answer_shapes_df["shape"] == answer_shape]
if not row.empty:
return row.iloc[0]["default_chunk_strategy"]
return "combined"
The deterministic dispatcher (section 2.1, approach B) calls this right after question parsing returns, writes the result onto parsed.chunk_strategy (top-level), and the same cascade runs for answer_context (also driven by shape). needs_summary stays on structural_hints because it describes the document, not the dispatch. The LLM is allowed to override either default in PARSE_PROMPT (sub-task 6) when the question itself contradicts the convention. “Give me a one-line summary of the exclusions” overrides the exclusions concept’s default_answer_context = "chapter". The defaults are conventions, not constraints.确定性调度器(第2.1节,方法B)在问题解析返回后立即调用,将结果写入parsed.chunk_strategy(顶层),相同的级联也用于answer_context(也由形状驱动)。needs_summary保留在structural_hints上,因为它描述文档而非调度。当问题本身与惯例矛盾时,LLM允许在PARSE_PROMPT(子任务6)中覆盖任一默认值。“给我一行总结除外责任”覆盖了exclusions概念的default_answer_context = "chapter"。默认值是惯例,而非约束。
Picking the model: two satellites in cascade. The same idea applies to model choice. Extracting an amount from one line doesn’t need the same model as reading three pages of dense legalese. A small model is enough for the first; the second wants a stronger one. Hard-coding gpt-4.1 everywhere is wasteful on the easy cases and cheap-looking on the hard ones. We split this into two satellites for a reason: a conceptual llm_model_tiers_df to reason about the buckets, and a precise llm_models_df with one row per specific model. The per-question default points to a precise model name, because at runtime we have to call something concrete. The models referenced in this article (the OpenAI gpt-4.1 family and Anthropic’s Claude family) are proprietary cloud services governed respectively by OpenAI’s Terms of Use and Anthropic’s Usage Policy.选择模型:两个卫星级联。相同的思路适用于模型选择。从一行中提取金额不需要与阅读三页密集法律术语相同的模型。前者小模型足够,后者需要更强的模型。在所有地方硬编码gpt-4.1在简单情况下浪费,在困难情况下显得廉价。我们将其拆分为两个卫星是有原因的:一个概念性的llm_model_tiers_df用于推理桶,一个精确的llm_models_df每行对应一个具体模型。每个问题的默认值指向一个精确的模型名称,因为运行时必须调用具体的东西。本文引用的模型(OpenAI gpt-4.1系列和Anthropic的Claude系列)是专有云服务,分别受OpenAI使用条款和Anthropic使用政策约束。
The conceptual grouping first. Four tiers that survive vendor catalogue churn:首先是概念分组。四个层级,可应对供应商目录变更:

Then the precise registry. One row per specific model the project can call, with the characteristics a dev needs to pick:然后是精确注册表。每行对应项目可调用的一个具体模型,包含开发人员选择所需的特征:

Prices and context windows refresh every few months; the schema doesn’t. Pinning the table to one query (“as of 2026-05, what models is the project allowed to call?”) gives the deployment a single source of truth. When the team validates gpt-4.5 six months from now, it’s one row update plus a re-run of the eval suite, not a code change.价格和上下文窗口每几个月刷新一次;模式不变。将表固定到一个查询(“截至2026年5月,项目允许调用哪些模型?”)为部署提供单一事实来源。当团队六个月后验证gpt-4.5时,只需更新一行并重新运行评估套件,而非代码更改。
Defaults point to a precise model, not a tier. A nano-tier question doesn’t ask the dispatcher to “use some nano model”; it asks for the project’s blessed nano model, the one the team has evaluated on the corpus. So answer_types_df.default_model and concepts_df.default_model hold a precise name (FK to llm_models_df.model). The cascade resolves to that name directly:默认值指向精确模型,而非层级。nano层级的问题不会要求调度器“使用某个nano模型”;它要求项目认可的nano模型,即团队在语料库上评估过的模型。因此answer_types_df.default_model和concepts_df.default_model保存精确名称(FK到llm_models_df.model)。级联直接解析为该名称:
def resolve_model(
answer_type: str,
matched_concept: str | None,
answer_types_df: pd.DataFrame,
concepts_df: pd.DataFrame,
fallback: str = "gpt-4.1-mini",
) -> str:
"""Concept-level override > answer-type default > project fallback. Returns a precise model name."""
if matched_concept is not None:
row = concepts_df[concepts_df["concept"] == matched_concept]
if not row.empty and pd.notna(row.iloc[0].get("default_model")):
return row.iloc[0]["default_model"]
row = answer_types_df[answer_types_df["type"] == answer_type]
if not row.empty and pd.notna(row.iloc[0].get("default_model")):
return row.iloc[0]["default_model"]
return fallback
The dispatcher writes the result onto parsed.suggested_model (top-level). The generation brick reads that name, fetches the row from llm_models_df for context-window / pricing / capability checks, and calls. When the team wants to swap gpt-4.1 for gpt-4.5 after evaluation, it’s a UPDATE llm_models_df SET model='gpt-4.5' WHERE tier='standard' (or two row inserts plus a default-column update), not a code change.调度器将结果写入parsed.suggested_model(顶层)。生成模块读取该名称,从llm_models_df获取行以进行上下文窗口/定价/能力检查,然后调用。当团队在评估后想将gpt-4.1换成gpt-4.5时,只需执行UPDATE llm_models_df SET model='gpt-4.5' WHERE tier='standard'(或插入两行并更新默认列),而非代码更改。
1.2 Activations: adapting to the document profile1.2 激活:适应文档概要
So far we’ve assumed the document plays along. It doesn’t always.到目前为止,我们假设文档配合。但并非总是如此。
Take “What does it say on page 1?” On a PDF, “page 1” is a real thing: pages are physical artifacts of the format and the parser knows their boundaries. On a Word file, “page 1” is renderer-dependent: the user’s font, the screen width, the print driver all shift the page breaks. The “page 1” the user saw may be different from “page 1” in another viewer. If the parser hard-codes extract_page_numbers=True, the system returns “see page 2” on a Word doc, wrong with high confidence.以“第1页上写了什么?”为例。在PDF上,“第1页”是真实存在的:页面是格式的物理产物,解析器知道其边界。在Word文件上,“第1页”取决于渲染器:用户的字体、屏幕宽度、打印驱动程序都会改变分页符。用户看到的“第1页”可能与其他查看器中的“第1页”不同。如果解析器硬编码extract_page_numbers=True,系统会在Word文档上返回“见第2页”,高置信度地错误。
The same trap applies whenever the question references a structural element the document doesn’t carry: a TOC that doesn’t exist, a section heading that’s not declared, a table the parser couldn’t extract. The fix is for the parser to look at the document’s profile (metadata returned by the document parsing brick’s parse_pdf) and downgrade activations that don’t fit. The profile is a small typed object:每当问题引用文档不包含的结构元素时,也会出现同样的陷阱:不存在的目录、未声明的章节标题、解析器无法提取的表格。解决方法是让解析器查看文档概要(文档解析模块的parse_pdf返回的元数据),并降级不适用的激活。概要是一个小型类型化对象:
class DocumentProfile(BaseModel):
format: Literal["pdf", "docx", "html", "txt", "xlsx"]
has_toc: bool = False
has_tables: bool = False
n_pages: int | None = None # None when the format has no real pages
languages: list[str] = Field(default_factory=list)
is_scanned: bool = False # OCR'd, expect more spelling noise
The parser then consults the profile to keep activations honest:然后解析器查阅概要以保持激活诚实:
class ExecutionPlan(BaseModel):
use_toc_navigation: bool = True
use_keyword_retrieval: bool = True
use_embeddings: bool = False
follow_cross_references: bool = False
decompose_compound: bool = False
iterate_on_feedback: bool = True
extract_page_numbers: bool = True
def parse_question(question, doc_profile) -> ParsedQuestion:
parsed = base_parse(question)
if doc_profile.format == 'docx':
parsed.activations.extract_page_numbers = False
if not doc_profile.has_toc:
parsed.activations.use_toc_navigation = False
return parsed
The parsing_notes field captures what the parser noticed but couldn’t enforce. It flows through to the answer’s _meta block on the generation side so the user knows the system understood the limitation. They don’t get a wrong with high confidence “page 2” answer; they get an answer with a note that page references are approximate in this format.parsing_notes字段捕获解析器注意到但无法强制执行的内容。它流到生成侧的答案_meta块中,以便用户知道系统理解了限制。他们不会得到高置信度错误的“第2页”答案,而是得到带有注释的答案,说明在此格式中页面引用是近似的。
The same idea applies elsewhere:同样的思路也适用于其他地方:

Common pitfall: Hard-coding activation flags as defaults regardless of document type. A pipeline that always sets
extract_page_numbers=Trueproduces page citations even when the document has no real pages. Activations have to come from the document’s actual properties, not from project-wide defaults.常见陷阱:无论文档类型如何,都将激活标志硬编码为默认值。总是设置extract_page_numbers=True的流水线即使文档没有真实页面也会生成页面引用。激活必须来自文档的实际属性,而非项目范围的默认值。
1.3 The full schema1.3 完整模式
At this point, the schema covers everything built up section by section, both in Article 6_b (extraction) and so far in this article. A few fields appear here for the first time: they are the relational links between the question row and the satellite tables, worth naming explicitly so the two consumer briefs introduced in Article 6_a make sense once assembled.至此,模式涵盖了第6_b篇(提取)和本文中逐节构建的所有内容。一些字段首次出现:它们是问题行与卫星表之间的关系链接,值得明确命名,以便第6_a篇中介绍的两个消费者简报在组装后有意义。
class ParsedQuestion(BaseModel):
# The raw input, kept for audit
original_question: str
corrected_question: str = "" # spell-corrected (section 2.1)
# What the user is asking
keywords: list[Keyword] = Field(default_factory=list) # → concept_keywords_df → concepts_df
# Two orthogonal axes for the expected answer (section 2.2)
answer_shape: Literal["single", "listing", "table", "tree", "nested_json"] = "single"
answer_type: str = "text" # → FK into answer_types_df
# How the question is structured (sections 2.4 + 2.3)
decomposition: Decomposition = Field(default_factory=Decomposition)
scope_filters: ScopeFilters = Field(default_factory=ScopeFilters)
structural_hints: StructuralHints = Field(default_factory=StructuralHints)
# How the pipeline dispatches the LLM calls (cascade from concept/type, section 2.6)
chunk_strategy: Literal["combined", "sequential"] = "combined" # generation dispatch
suggested_model: str = "gpt-4.1-mini" # → FK into llm_models_df
# When the LLM should distinguish related concepts (section 3.2)
disambiguation: str | None = None
distractors: list[str] = Field(default_factory=list)
# What the system should do (section 2.7)
activations: ExecutionPlan = Field(default_factory=ExecutionPlan)
# What the parser noticed about its own choices
parsing_notes: list[str] = Field(default_factory=list)
suggested_clarification: str | None = None
ambiguity_reason: str | None = None
# The two consumer briefs (derived assemblies, section 3)
retrieval: RetrievalQuery | None = None
generation: GenerationBrief | None = None
Several relational layers come into play, mirroring document parsing. The central table is fixed; the satellites listed here are examples a project typically ends up needing, not a closed set:涉及多个关系层,反映文档解析。中心表是固定的;此处列出的卫星表是项目通常最终需要的示例,而非封闭集合:
question_df(always). One row per parsed question, with the columns above. The row is what the_metablock (section 2.3) logs to disk for audit, and what corpus-level dashboards SQL over: “how many questions of typeamountdid users ask last month?”, “which questions triggered a clarification?”, “which keywords got hit most?”.question_df(始终存在)。每行对应一个解析后的问题,包含上述列。该行是_meta块(第2.3节)记录到磁盘以供审计的内容,也是语料库级仪表板SQL查询的对象:“上个月用户问了多少个金额类型的问题?”、“哪些问题触发了澄清?”、“哪些关键词命中率最高?”。concepts_df(typical). One row per concept (premium,non_compete, …) with itsdocument_typeanddefinition. Project-wide, maintained by domain experts.concepts_df(典型)。每行对应一个概念(premium、non_compete等),包含其document_type和定义。项目范围,由领域专家维护。concept_keywords_df(typical). One row per(concept, language, keyword). Joined toconcepts_dfonconcept. The big growth area: every missed retrieval that turns out to be a vocabulary mismatch becomes a new row here.concept_keywords_df(典型)。每行对应一个(概念,语言,关键词)。通过concept连接到concepts_df。主要增长领域:每次因词汇不匹配导致的检索失败都会成为这里的新行。answer_types_df(typical). One row per registered answer type (amount,date,iban,text,address, …) withretrieval_patterns(used by the retrieval brick),output_schema_ref(used by the generation brick),definition, and the per-typedefault_model. Adding a new type is a single insert.answer_types_df(典型)。每行对应一个注册的答案类型(amount、date、iban、text、address等),包含retrieval_patterns(由检索模块使用)、output_schema_ref(由生成模块使用)、定义和每种类型的default_model。添加新类型只需一次插入。answer_shapes_df(small, fixed). One row per registered answer shape (single,listing,table,tree,nested_json) with per-shape defaults forchunk_strategyandanswer_context. Shape drives how the answer is laid out; type drives what each value contains. The split makes “List the annual premiums” (a(listing, amount)question) a different dispatch decision from “What is the premium?” (a(single, amount)question).answer_shapes_df(小型,固定)。每行对应一个注册的答案形状(single、listing、table、tree、nested_json),包含chunk_strategy和answer_context的每形状默认值。形状驱动答案的布局方式;类型驱动每个值的内容。这种拆分使得“列出年保费”((listing, amount)问题)与“保费是多少?”((single, amount)问题)的调度决策不同。llm_model_tiers_df(small, conceptual). Four rows (nano,mini,standard,reasoning) with relative cost, latency, and the use cases each tier suits. Lets the team reason about model choice in vendor-agnostic terms.llm_model_tiers_df(小型,概念性)。四行(nano、mini、standard、reasoning),包含相对成本、延迟和每个层级适合的用例。让团队以与供应商无关的方式推理模型选择。llm_models_df(one row per precise model the project is allowed to call). Includes provider, tier (FK tollm_model_tiers_df), context window, structured-output support, per-1M-token pricing, and notes. The per-question default points to a precise model name in this table; swappinggpt-4.1forgpt-4.5after evaluation is a row update.llm_models_df(每行对应项目允许调用的一个精确模型)。包含提供商、层级(FK到llm_model_tiers_df)、上下文窗口、结构化输出支持、每百万token定价和备注。每个问题的默认值指向此表中的精确模型名称;评估后将gpt-4.1换成gpt-4.5只需更新一行。
Other satellites get added when the domain calls for them. A legal RAG often grows a regulations_df mapping codes (“L131-1”) to their actual texts so the parser can resolve references. A corporate corpus grows an entity_alias_df so “BNP” and “BNP Paribas” and “the Bank” resolve to the same entity. A scientific one grows a unit_conversions_df. Same pattern as columns: start with what you need, add when a real case pushes for it.其他卫星表在领域需要时添加。法律RAG通常会增加一个regulations_df,将代码(“L131-1”)映射到实际文本,以便解析器解析引用。企业语料库会增加entity_alias_df,使“BNP”、“BNP Paribas”和“the Bank”解析为同一实体。科学语料库会增加unit_conversions_df。与列相同的模式:从所需开始,在真实案例推动时添加。
Two of the columns on question_df (retrieval, generation) are built from the others: the parser assembles them from the raw columns so retrieval and generation each receive only what they need. The next section is about why they’re split this way.question_df上的两列(retrieval、generation)由其他列构建:解析器从原始列组装它们,以便检索和生成各自只接收所需内容。下一节关于为什么这样拆分。
Recap of question_df columns: For each column: what it carries, when it’s set, who consumes it downstream.question_df列回顾:每列:承载内容、设置时间、下游消费者。

2. Architecture choices2. 架构选择
Section 1 walked what the dispatcher decides on top of the parsed row: dispatch defaults, activation flags, the assembled schema. This one steps back: who writes each of those decisions (the user, a deterministic rule, or an LLM at runtime), how the choices land on the top-level call, and how every decision is audited.第1节介绍了调度器在解析行之上决定的内容:调度默认值、激活标志、组装后的模式。本节退一步:谁做出每个决定(用户、确定性规则或运行时LLM),选择如何落在顶层调用上,以及每个决定如何被审计。
2.1 Three approaches to deciding activations2.1 决定激活的三种方法
The execution plan field on the parsed question contains a set of activation flags: use_toc_navigation, use_keyword_retrieval, decompose_compound, and so on. Section 1.2 introduced them; this one covers who decides what they’re set to on a given run.解析后问题上的执行计划字段包含一组激活标志:use_toc_navigation、use_keyword_retrieval、decompose_compound等。第1.2节介绍了它们;本节涵盖在给定运行中谁决定它们的设置。
This is one of the main architecture choices in the series. Three approaches.这是系列中主要的架构选择之一。三种方法。
Approach A. User explicit overrides. The user passes activation flags as arguments to pdf_qa. To force semantic retrieval and skip both decomposition and feedback loops, the call reads pdf_qa(contract, question="What are all the obligations?", use_embeddings=True, decompose_compound=False, iterate_on_feedback=False).方法A:用户显式覆盖。用户将激活标志作为参数传递给pdf_qa。要强制语义检索并跳过分解和反馈循环,调用为pdf_qa(contract, question="What are all the obligations?", use_embeddings=True, decompose_compound=False, iterate_on_feedback=False)。
Pro: total control, fully reproducible, debuggable. Con: the user has to understand the system to choose intelligently. In practice, no one does this for routine queries; it’s a manual override for development and debugging.优点:完全控制、完全可重现、可调试。缺点:用户必须理解系统才能明智选择。实际上,没有人对常规查询这样做;它是开发和调试的手动覆盖。
Approach B. Deterministic dispatcher. The system looks at the parsed question and the document profile, and applies code-based rules to decide activations. The function below is illustrative; a production dispatcher carries 15-30 such rules, accumulated over the deployment’s lifetime:方法B:确定性调度器。系统查看解析后的问题和文档概要,并应用基于代码的规则来决定激活。下面的函数是说明性的;生产调度器包含15-30条这样的规则,在部署生命周期中积累:
def decide_activations(parsed: ParsedQuestion, doc_profile: DocumentProfile) -> ExecutionPlan:
plan = ExecutionPlan() # defaults
if parsed.decomposition.pattern == "independent":
plan.decompose_compound = True
if doc_profile.format == "docx":
plan.extract_page_numbers = False
if parsed.answer_shape == "listing":
plan.iterate_on_feedback = True
return plan
Pro: reproducible, debuggable, the team’s accumulated wisdom lives in code. Con: requires writing and maintaining the rules. Each new question pattern that doesn’t fit is a rule to add.优点:可重现、可调试,团队的积累智慧存在于代码中。缺点:需要编写和维护规则。每个不符合的新问题模式都需要添加一条规则。
Approach C. LLM-decides-everything (autonomous). The system describes the available sub-functions to an LLM and asks it to choose. Pro: flexible, handles cases the team hadn’t planned for. Con: non-reproducible (the LLM may decide differently each run), expensive (every question costs an extra LLM call for routing), hard to debug (the reasoning is in the LLM’s weights).方法C:LLM决定一切(自主)。系统向LLM描述可用的子函数,并要求其选择。优点:灵活,处理团队未计划的情况。缺点:不可重现(LLM每次运行可能做出不同决定)、昂贵(每个问题多一次LLM调用用于路由)、难以调试(推理在LLM的权重中)。
The series’s position: Approach B as default, Approach A as manual override, Approach C rejected for enterprise.系列的立场:方法B为默认,方法A为手动覆盖,方法C被企业拒绝。
This is the same argument that recurs whenever “agentic RAG” comes up. For enterprise contexts (legal, insurance, financial services), reproducibility, auditability, and bounded cost matter more than whatever extra flexibility Approach C buys. Approach B gives you all three. Approach A lets you override when you need to test a specific configuration.每当“智能体RAG”出现时,这也是同样的论点。对于企业环境(法律、保险、金融服务),可重现性、可审计性和可控成本比方法C带来的任何额外灵活性更重要。方法B提供所有三者。方法A允许在需要测试特定配置时覆盖。
This is also why “agentic RAG” works better than naive RAG, when it’s done well. The agentic part isn’t magic. It’s that the system parses the question before searching, instead of treating retrieval as a mechanical first step. Once you split the work between preparing for retrieval and preparing for generation, the rest of the pipeline becomes much easier to reason about, without needing the LLM to be in the control loop.这也是为什么“智能体RAG”在做得好的情况下比朴素RAG效果更好。智能体部分并非魔法。它在于系统在搜索之前解析问题,而不是将检索视为机械的第一步。一旦你将工作分为准备检索和准备生成,管道的其余部分就变得更容易推理,无需LLM处于控制循环中。
2.2 The top-level call: five argument families2.2 顶层调用:五个参数族
Once the dispatcher decides activations automatically, the user’s pdf_qa(pdf_path, question) call is enough for most cases. But sometimes the user wants to override a specific behavior: tweak retrieval top_k, skip TOC routing on a document that has no usable outline, inject a pre-loaded PromptContext. The top-level call has to handle this without getting messy.一旦调度器自动决定激活,用户的pdf_qa(pdf_path, question)调用在大多数情况下就足够了。但有时用户想覆盖特定行为:调整检索top_k、跳过没有可用大纲的文档的TOC路由、注入预加载的PromptContext。顶层调用必须处理这一点而不变得混乱。
The pattern that works: organize override arguments into five families, each named after the brick it affects. The current pdf_qa from docintel.pipeline.qa.pdf ships eight kwargs grouped that way:有效的模式:将覆盖参数组织成五个族,每个以其影响的模块命名。当前来自docintel.pipeline.qa.pdf的pdf_qa以这种方式分组了八个kwargs:
def pdf_qa(
pdf_path: str | Path,
question: str,
*,
# Parsing overrides (Article 5 / 10)
method: str = "fitz",
# Question-parsing overrides (this article)
expert_dict: dict[str, list[str]] | None = None,
# Retrieval overrides (Articles 7 / 9)
top_k: int = 5,
use_toc: bool = True,
# Generation overrides (Article 8)
include_bbox: bool = False,
# Pipeline-behavior overrides
store: "Store | None" = None,
client: "OpenAI | None" = None,
context: PromptContext | None = None,
) -> AnswerWithEvidence:
...
The user who wants no overrides just calls pdf_qa(contract_pdf, "What is the premium?"). The user who wants to disable the LLM TOC router on a document with a broken outline does pdf_qa(contract_pdf, "What is the premium?", use_toc=False). None of the overrides is required; all have defaults driven by the dispatcher. The cross-document sibling corpus_pdf_qa mirrors the same pattern with project_id in front and a top_k_docs cap.不需要覆盖的用户只需调用pdf_qa(contract_pdf, "What is the premium?")。想要在轮廓损坏的文档上禁用LLM TOC路由器的用户执行pdf_qa(contract_pdf, "What is the premium?", use_toc=False)。没有覆盖是必需的;所有都有由调度器驱动的默认值。跨文档的兄弟函数corpus_pdf_qa镜像相同的模式,前面加上project_id和top_k_docs上限。
What’s coming. A few overrides the architecture has room for but the package does not ship today: an
answer_schema=MyCustomSchemato override the registry per question (Article 8 (generation), section 3.5),retrieval_methods=["keyword", "embedding"]to pick the method-stack at runtime (Article 7, retrieval),iterate_on_feedback=True+max_iterations=3to run the same-run retry on incomplete answers (Article 13 (the workflow pipeline) and Article 14 (the corpus problem)). Each one extends one of the five families above without rearranging the rest. The article keeps the families-by-brick layout precisely so adding kwargs later is mechanical.即将推出的功能。架构有空间但包今天未提供的一些覆盖:answer_schema=MyCustomSchema以按问题覆盖注册表(第8篇(生成),第3.5节)、retrieval_methods=["keyword", "embedding"]以在运行时选择方法栈(第7篇,检索)、iterate_on_feedback=True + max_iterations=3以在相同运行中重试不完整答案(第13篇(工作流管道)和第14篇(语料库问题))。每个扩展上述五个族之一而不重新排列其余部分。文章保持按模块分族的布局,正是为了以后添加kwargs是机械性的。
2.3 The _meta block in the output2.3 输出中的_meta块
The parsed question is internal to pdf_qa. But traces of it appear in the output, and that matters for the user.解析后的问题对pdf_qa是内部的。但它的痕迹出现在输出中,这对用户很重要。
The output JSON has the answer (the result of generation), and a _meta block that records what was done:输出JSON包含答案(生成的结果)和一个_meta块,记录所做的事情:
{
"answer": "The premium is €125,000 annually.",
"page_number": 4,
"line_start": 12,
"line_end": 14,
"quote": "Annual premium: €125,000",
"_meta": {
"decomposition": "single",
"activations": {
"use_toc_navigation": true,
"use_keyword_retrieval": true,
"use_embeddings": false,
"extract_page_numbers": true
},
"skipped": [],
"parsing_notes": [],
"iterations": 1,
"retrieval_methods_used": ["toc", "keyword"],
"model": "gpt-4.1",
"prompt_versions": {"question_parsing": "v2.4", "generation": "v4.2"}
}
}
The _meta block carries the decomposition pattern, which activations were on or off, what was skipped (and why, from parsing_notes), how many iterations the pipeline went through, which retrieval methods fired, and the model and prompt versions for reproducibility (the same fields a per-failure-mode evaluation reads from later)._meta块携带分解模式、哪些激活开启或关闭、跳过了什么(以及原因,来自parsing_notes)、管道经历了多少次迭代、哪些检索方法触发,以及模型和提示版本以实现可重现性(与每个故障模式评估稍后读取的字段相同)。
This isn’t optional. It’s what makes the system auditable. When a user disputes an answer, the _meta block is the explanation. When the team debugs a regression, the _meta block is the trace. When compliance asks “why did the system give this answer?”, the _meta block is the answer.这不是可选的。它使系统可审计。当用户质疑答案时,_meta块是解释。当团队调试回归时,_meta块是跟踪。当合规部门问“为什么系统给出这个答案?”时,_meta块就是答案。
The user who doesn’t want to see _meta in their UI can hide it. But it’s always generated and always logged, because making it costs nothing and the audit trail is what production deployments need.不想在UI中看到_meta的用户可以隐藏它。但它总是生成并总是记录,因为生成它不花费任何成本,而审计跟踪是生产部署所需要的。
The parsed question is also persisted to disk, following the convention the document parsing brick installs: save_parsed_question(pdf_path, question, parsed_question) writes the full ParsedQuestion to output/<subdir>/<stem>/questions/<question_slug>/parsed_question.json. The slug combines a readable prefix of the question with a short hash so near-identical questions never collide. The next brick (retrieval) reads the same file. No re-call to the LLM for question parsing when iterating on retrieval or generation downstream.解析后的问题也会持久化到磁盘,遵循文档解析模块建立的约定:save_parsed_question(pdf_path, question, parsed_question)将完整的ParsedQuestion写入output/<subdir>/<stem>/questions/<question_slug>/parsed_question.json。slug结合了问题的可读前缀和短哈希,因此几乎相同的问题永远不会冲突。下一个模块(检索)读取同一文件。在下游迭代检索或生成时,无需重新调用LLM进行问题解析。
3. In practice3. 实践
3.1 parse_question end-to-end3.1 parse_question端到端
Article 6_b walked each parser concern as its own helper, each with its own LLM call. That’s how the prose builds up the schema column by column. In production, one consolidated LLM call returns the whole row at once. One round-trip, one prompt to maintain, one place where the LLM sees the full question.第6_b篇将每个解析关注点作为自己的辅助函数,每个都有自己的LLM调用。这是散文逐列构建模式的方式。在生产中,一次合并的LLM调用返回整个行。一次往返,一个要维护的提示,一个LLM看到完整问题的地方。
The schema the LLM fills:LLM填充的模式:
class FullParse(BaseModel):
"""Everything the LLM produces in a single call."""
corrected_question: str
keywords_extracted: list[str]
keywords_rewritten: list[str]
answer_shape: str # single | listing | table | tree | nested_json
answer_type: str # FK into answer_types_df
decomposition: Decomposition
structural_hints: StructuralHints
chunk_strategy: Literal['combined','sequential'] = 'combined'
suggested_model: str = 'gpt-4.1-mini'
suggested_clarification: str | None = None
disambiguation: str | None = None
distractors: list[str] = Field(default_factory=list)
The prompt walks the LLM through the sub-tasks. Each sub-task in the prompt corresponds to one column in FullParse:提示引导LLM完成子任务。提示中的每个子任务对应FullParse中的一列:
def build_parse_prompt(
answer_types_df: pd.DataFrame,
answer_shapes_df: pd.DataFrame,
) -> str:
"""The answer-type and answer-shape lists are injected from the satellites
so adding a new type or a new shape is a single row insert, no prompt edit."""
types_label = ", ".join(answer_types_df["type"])
shapes_label = ", ".join(answer_shapes_df["shape"])
return (
"You parse user questions into a structured object that downstream retrieval and "
"generation will consume. Return JSON matching the FullParse schema.\n\n"
"Sub-tasks:\n"
"1. corrected_question: fix typos. No meaning change.\n"
"2. keywords_extracted: 1-3 content noun phrases from the question.\n"
"3. keywords_rewritten: 3-5 short phrases matching how the answer is likely to "
"appear in the document. Document vocabulary, not the user's casual phrasing.\n"
f"4. answer_shape: one label from {{{shapes_label}}}. The cardinality of the "
"answer: 'single' for one value, 'listing' for a flat enumeration, 'table' for "
"rows x columns, 'tree' for nested hierarchy, 'nested_json' for a structured "
"object with named sub-fields.\n"
f"5. answer_type: one label from {{{types_label}}}. The value type each element "
"of the answer carries. 'List the annual premiums' is (listing, amount) ; 'What "
"is the premium?' is (single, amount) ; 'List the exclusions' is (listing, text).\n"
"6. decomposition: pattern (single/independent/sequential/unified/conditional), "
"sub-questions if compound, and conditional_filter if the pattern is conditional.\n"
"7. structural_hints: WHERE (toc_section_hint, pages_hint, layout_hint) and HOW "
"MUCH (detection_context, answer_context, needs_summary). Leave answer_context, "
"needs_summary, chunk_strategy, suggested_model at their defaults UNLESS the "
"question itself contradicts them (e.g. 'one-line summary of the exclusions' "
"overrides the exclusions concept's chapter-level default ; 'compare the indemnity "
"clauses in this contract and the previous version' bumps suggested_model to a "
"reasoning-tier model like o4-mini).\n"
"8. suggested_clarification: short follow-up question if the input is too vague. "
"null otherwise.\n"
"9. disambiguation + distractors: 'limit, not deductible' patterns."
)
PARSE_PROMPT = build_parse_prompt(answer_types_df, answer_shapes_df)
The pipeline. The single LLM call carries the parsing work. Two non-LLM steps stay separate: anchor keywords (regex, deterministic, fast) and the expert dictionary lookup (pandas filter, no model).管道。单次LLM调用承担解析工作。两个非LLM步骤保持独立:锚点关键词(正则表达式,确定性,快速)和专家字典查找(pandas过滤,无模型)。
def parse_question(
question: str,
*,
expert_kw_df: pd.DataFrame | None = None,
system_prompt: str = PARSE_PROMPT,
) -> ParsedQuestion:
resp = client.responses.parse(
model="gpt-4.1-mini",
input=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": question},
],
text_format=FullParse,
)
full = FullParse.model_validate_json(resp.output_text)
anchor_kw = extract_anchor_keywords(full.corrected_question) # regex
dict_kw_df = (
lookup_expert_keywords(full.corrected_question, expert_kw_df)
if expert_kw_df is not None else pd.DataFrame()
)
keywords = (
[Keyword(text=t, source="direct") for t in full.keywords_extracted]
+ [Keyword(text=t, source="anchor") for t in anchor_kw]
+ [Keyword(text=t, weight=0.7, source="llm_expansion") for t in full.keywords_rewritten]
+ [Keyword(text=row["keyword"], weight=row["weight"],
source="expert_dictionary", semantic_group=row["concept"])
for _, row in dict_kw_df.iterrows()]
)
return ParsedQuestion(
original_question=question,
corrected_question=full.corrected_question,
keywords=keywords,
answer_shape=full.answer_shape,
answer_type=full.answer_type,
decomposition=full.decomposition,
structural_hints=full.structural_hints,
chunk_strategy=full.chunk_strategy,
suggested_model=full.suggested_model,
suggested_clarification=full.suggested_clarification,
disambiguation=full.disambiguation,
distractors=full.distractors,
# scope_filters, activations, retrieval, generation are filled by the
# dispatcher (section 4.1) once the document profile is available.
# parsing_notes is appended later when activations downgrade.
)
The trade-off versus the step-by-step pipeline of Article 6_b:与第6_b篇的逐步管道的权衡:
- Step-by-step (one helper per concern): easy to debug per concern, easy to override one prompt without touching the others, easy to A/B test individual sub-prompts. 5+ LLM calls per question.逐步(每个关注点一个辅助函数):易于按关注点调试,易于覆盖一个提示而不影响其他,易于A/B测试单个子提示。每个问题5次以上LLM调用。
- Consolidated (one call, one schema): one round-trip, one prompt, one model context. Harder to attribute an error to a specific sub-task. ~5x cheaper and ~5x faster.合并(一次调用,一个模式):一次往返,一个提示,一个模型上下文。更难将错误归因于特定子任务。约5倍便宜,约5倍快。
The series’s default is consolidated for production. The step-by-step pipeline stays useful for tests and debugging. When one field looks wrong, swap that field’s standalone helper in, rerun, compare.系列的默认值是生产中使用合并。逐步管道在测试和调试中仍然有用。当一个字段看起来错误时,将该字段的独立辅助函数换入,重新运行,比较。
Most questions don’t fill every column. A simple lookup needs corrected_question + keywords + answer_shape + answer_type. A compound listing-question fills decomposition, structural_hints.answer_context = "chapter", needs_summary = True. The schema’s defaults handle the unused fields, so parse_question always returns a complete row.大多数问题不会填充每一列。简单查找需要corrected_question + keywords + answer_shape + answer_type。复合列表问题填充decomposition、structural_hints.answer_context = "chapter"、needs_summary = True。模式的默认值处理未使用的字段,因此parse_question总是返回完整行。
3.2 Examples on the broker corpus3.2 经纪人语料库示例
A few concrete cases from the insurance broker context that comes back through Parts IV and V. Each example shows only the columns the case uses; the rest of the ParsedQuestion schema (corrected_question, structural_hints, retrieval, generation, …) keeps its defaults.来自保险经纪人上下文的一些具体案例,这些案例在第四和第五部分中反复出现。每个示例仅显示案例使用的列;ParsedQuestion模式的其余部分(corrected_question、structural_hints、retrieval、generation等)保持默认值。
Example 1. A point lookup with expert keywords.示例1:带专家关键词的点查找。
User question: “Quel est le montant de la prime annuelle?”用户问题:“Quel est le montant de la prime annuelle?”
ParsedQuestion(
original_question="Quel est le montant de la prime annuelle ?",
answer_shape="single",
answer_type="amount",
keywords=[
Keyword(text="prime", weight=1.0, source="direct"),
Keyword(text="montant", weight=0.8, source="direct"),
Keyword(text="annuelle", weight=0.7, source="direct"),
Keyword(text="premium", weight=0.9, source="expert_dictionary", semantic_group="prime"),
Keyword(text="cotisation", weight=0.9, source="expert_dictionary", semantic_group="prime"),
Keyword(text=r"\d+[\s.,]?\d*\s*(?:EUR|€)", weight=0.8,
source="expert_dictionary", is_regex=True),
],
decomposition=Decomposition(pattern="single"),
activations=ExecutionPlan(
use_toc_navigation=True,
use_keyword_retrieval=True,
extract_page_numbers=True,
),
parsing_notes=["Question in French; expert dictionary applied."],
)
The amount regex in the keyword list is the type-confirmation pattern from Article 6_b (extraction), section 1.2: retrieval will require a monetary amount in the matched zone, not just keyword overlap.关键词列表中的金额正则表达式是第6_b篇(提取)第1.2节中的类型确认模式:检索将要求匹配区域中存在货币金额,而不仅仅是关键词重叠。
Example 2. A compound question, independent decomposition.示例2:复合问题,独立分解。
User question: “What is the annual premium and what are the main exclusions?”用户问题:“What is the annual premium and what are the main exclusions?”
ParsedQuestion(
original_question="What is the annual premium and what are the main exclusions?",
decomposition=Decomposition(
pattern="independent",
sub_questions=[
"What is the annual premium?",
"What are the main exclusions?",
],
),
activations=ExecutionPlan(decompose_compound=True),
parsing_notes=["Compound question detected. Decomposed into 2 independent sub-questions."],
)
The orchestrator sees decompose_compound=True and runs pdf_qa twice in parallel (once per sub-question), then assembles a combined output.编排器看到decompose_compound=True,并行运行两次pdf_qa(每个子问题一次),然后组装组合输出。
Example 3. An ambiguous question that triggers clarification.示例3:触发澄清的模糊问题。
User question: “What’s the limit?”用户问题:“What’s the limit?”
ParsedQuestion(
original_question="What's the limit?",
suggested_clarification=(
"Several types of limits exist in this contract: coverage limit, sublimit, "
"deductible, aggregate limit. Which one are you asking about?"
),
ambiguity_reason="single_term_with_multiple_referents",
parsing_notes=["Ambiguous question; clarification suggested before running pipeline."],
)
Example 4. A document-aware activation downgrade.示例4:文档感知的激活降级。
User question: “What does it say on page 3 of the contract?”, document is Word format.用户问题:“What does it say on page 3 of the contract?”,文档为Word格式。
ParsedQuestion(
original_question="What does it say on page 3 of the contract?",
activations=ExecutionPlan(
extract_page_numbers=False,
),
parsing_notes=[
"User mentioned 'page 3' but document is Word format. "
"Page numbers in Word depend on renderer; treating as approximate location.",
],
)
In the wild: Six months into production on the broker system:实际数据:经纪人系统上线六个月后:
- Average parsing latency: 280 ms (one mid-tier LLM call for decomposition + keyword expansion)平均解析延迟:280毫秒(一次中端LLM调用用于分解+关键词扩展)
- Distribution by decomposition pattern: single 71%, independent 19%, conditional 6%, sequential 3%, unified 1%分解模式分布:single 71%,independent 19%,conditional 6%,sequential 3%,unified 1%
- Clarification triggered on 4% of questions4%的问题触发了澄清
- Expert dictionary entries: 340, growing by 5-10 per month专家字典条目:340,每月增长5-10个
- Document-aware activation downgrades: 12% of questions hit at least one文档感知的激活降级:12%的问题至少命中一次
Ablation: with parsing turned off (questions treated as flat strings), accuracy dropped from 91% to 76%. The 15-point gap is what parsing buys.消融实验:关闭解析后(问题被视为平面字符串),准确率从91%降至76%。15个百分点的差距就是解析带来的收益。
3.3 Common implementation traps3.3 常见实现陷阱
A few traps when implementing question parsing in practice.实践中实现问题解析时的一些陷阱。
Treating question parsing as just “extract keywords.” The keywords are one output among many. Pipelines that stop at keyword extraction miss decomposition, scope filters, format constraints, and activation decisions, all of which affect quality further down the pipeline.将问题解析视为仅“提取关键词”。关键词是众多输出之一。停留在关键词提取的管道会错过分解、范围过滤器、格式约束和激活决策,所有这些都会影响下游质量。
Caching the parsed question across documents. The parsed question depends on the document profile. The same question parsed for a PDF and for a Word document will have different activation flags. The cache key must include the document profile, not just the question text.跨文档缓存解析后的问题。解析后的问题依赖于文档概要。为PDF和Word文档解析的同一问题会有不同的激活标志。缓存键必须包含文档概要,而不仅仅是问题文本。
Skipping the expert dictionary because “embeddings will handle synonyms.” They handle dictionary synonyms. They do not handle internal acronyms, jurisdiction-specific terms, or business-coded vocabulary the embedding model has never seen. The expert dictionary is something the project keeps growing.因为“嵌入会处理同义词”而跳过专家字典。它们处理字典同义词。但它们不处理嵌入模型从未见过的内部缩写、管辖特定术语或业务编码词汇。专家字典是项目持续增长的东西。
Decomposing aggressively: Over-decomposition produces answers that don’t hang together. “What are the exclusions and limitations?” is unified, not independent, in most policy contexts. The disambiguation test (replacing “and” with “; also”) is the cheap check; the LLM classification is the safety net.过度分解:过度分解会产生不连贯的答案。“除外责任和限制是什么?”在大多数保单上下文中是统一的,而非独立的。消歧测试(将“and”替换为“; also”)是廉价检查;LLM分类是安全网。
Mixing format constraints into the retrieval query. “Premium amount, formatted as integer, in EUR” should produce a retrieval query of “premium amount” and a generation brief that carries the format constraint. Mixing the format into retrieval pollutes the search.将格式约束混入检索查询。“保费金额,格式化为整数,以欧元计”应产生检索查询“保费金额”和携带格式约束的生成简报。将格式混入检索会污染搜索。
Setting all activation flags by hand at every call. This skips the whole point of the dispatcher. The default pdf_qa(pdf_path, question) should produce sensible activations from the parsed question and document profile. Explicit overrides are for the cases where the team knows better than the default.每次调用都手动设置所有激活标志。这跳过了调度器的全部意义。默认的pdf_qa(pdf_path, question)应从解析后的问题和文档概要产生合理的激活。显式覆盖用于团队比默认更了解的情况。
Forgetting that the parsed question is data. The parsed question is the artifact the rest of the pipeline reads. It’s worth making it inspectable, loggable, version-controlled. Production systems should be able to show “for question X, the parsed structure was Y, and that’s why the system did Z.”忘记解析后的问题也是数据。解析后的问题是管道其余部分读取的工件。值得使其可检查、可记录、版本控制。生产系统应能显示“对于问题X,解析结构是Y,这就是系统执行Z的原因。”
4. Conclusion4. 结论
A parsed brief is only as useful as the routing layer that turns its columns into pipeline behaviour. The routing has three pieces: a RetrievalQuery view that hands retrieval the columns it can act on (keywords, rewrites, anchors, scope filters) and nothing else; a GenerationBrief view that gives generation what it needs (original question, format constraints, disambiguation, distractors); and an activations map that turns specific bricks off when the document profile makes them useless. The _meta block records every routing decision, so a misrouted question shows up as a diff in the audit trail, not as a mystery answer.解析后的简报只有在其路由层将列转化为管道行为时才有用。路由有三个部分:一个RetrievalQuery视图,向检索提供它可以操作的列(关键词、重写、锚点、范围过滤器)而不提供其他;一个GenerationBrief视图,向生成提供所需内容(原始问题、格式约束、消歧、干扰项);以及一个激活映射,当文档概要使其无用时会关闭特定模块。_meta块记录每个路由决策,因此路由错误的问题在审计跟踪中显示为差异,而不是神秘答案。
A pipeline that tunes embedding models and chunk sizes but routes the raw user string to every brick is leaving most of its quality on the table before retrieval has even started. The dispatch step doesn’t add a model. It directs the ones that are already there.一个调整嵌入模型和块大小但将原始用户字符串路由到每个模块的管道,在检索开始之前就将其大部分质量留在了桌面上。调度步骤不添加模型。它指导已经存在的模型。
5. Sources and further reading5. 来源与延伸阅读
This article takes a position on the architecture choice behind question dispatch. The series defaults to a deterministic dispatcher (approach B in section 2.1): reproducible, auditable, bounded-cost. The contrast point is the agentic line where an LLM decides routing at runtime, which the literature has converged on under several names. Volume 3 (Agentic Bricks) develops the agentic alternative on top of the structured plan this article defines; here we cite the position the deterministic dispatcher is contrasted against.本文对问题调度背后的架构选择采取了立场。系列默认使用确定性调度器(第2.1节中的方法B):可重现、可审计、成本可控。对比点是智能体路线,其中LLM在运行时决定路由,文献中已以多个名称收敛。第3卷(智能体模块)在本文定义的结构化计划之上开发了智能体替代方案;此处我们引用确定性调度器所对比的立场。
Different angle, different context:不同角度,不同背景:
- Schick et al., Toolformer: Language Models Can Teach Themselves to Use Tools, NeurIPS 2023 (arXiv:2302.04761). The model decides when and which tool to call inline, with no upfront question parsing. The opposite of the deterministic dispatcher this article ships: routing pushed into the LLM at runtime, not extracted upfront into a typed plan.Schick等人,Toolformer: Language Models Can Teach Themselves to Use Tools,NeurIPS 2023(arXiv:2302.04761)。模型决定何时以及调用哪个工具,无需预先的问题解析。与本文发布的确定性调度器相反:路由在运行时推入LLM,而非预先提取到类型化计划中。
- Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023 (arXiv:2210.03629). The agentic loop pattern that runtime-routes between reasoning and tool calls. The same trade-off as Toolformer: flexibility at the cost of reproducibility and bounded cost. Volume 3 covers the audit envelope that makes this workable in regulated contexts.Yao等人,ReAct: Synergizing Reasoning and Acting in Language Models,ICLR 2023(arXiv:2210.03629)。智能体循环模式,在推理和工具调用之间运行时路由。与Toolformer相同的权衡:灵活性以可重现性和可控成本为代价。第3卷涵盖使这在受监管环境中可行的审计框架。
Earlier in the series:系列早期内容:
- Document Intelligence: series intro. What the series builds, brick by brick, and in what order.文档智能:系列介绍。系列构建的内容,逐模块,以及顺序。
Part I:第一部分:
- Baseline Enterprise RAG, from PDF to highlighted answer. The four-brick pipeline end to end: PDF in, highlighted answer out.基线企业RAG,从PDF到高亮答案。端到端的四模块管道:PDF输入,高亮答案输出。
- 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检索的可预测故障模式。嵌入相似性在何处胜出(同义词、拼写错误、释义),在何处可预测地失效(未知术语、否定、术语与答案相关性),以及如何仍然使用它。
- Rerankers Aren’t Magic Either: When the Cross-Encoder Layer Is Worth the Cost. What a cross-encoder adds over bi-encoder embeddings, measured, and when it is worth the latency.重排序器也非魔法:交叉编码器层何时值得成本。交叉编码器在双编码器嵌入之上增加了什么,经过测量,以及何时值得延迟。
- 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.RAG不是机器学习,ML工具包解决错误问题。为什么块大小扫描和微调优化了错误的东西;改为按问题类型路由。
- 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.从正则表达式到视觉模型:哪种RAG技术适合哪种问题。两个轴,文档复杂度和问题控制,为每种情况选择技术。
- 10 common RAG mistakes we keep seeing in production. Ten production mistakes, organized brick by brick, with the fix for each.我们在生产中不断看到的10个常见RAG错误。十个生产错误,按模块组织,每个都有修复方法。
Part II:第二部分:
- Beyond extract_text: the two layers of a PDF that drive RAG quality. The first half of the parsing brick: the document’s nature, signals, and summary.超越extract_text:驱动RAG质量的PDF的两个层次。解析模块的前半部分:文档的性质、信号和摘要。
- Stop returning flat text from a PDF: the relational shape RAG needs. The second half of the parsing brick: the relational tables every downstream brick reads.停止从PDF返回平面文本:RAG需要的关系形状。解析模块的后半部分:每个下游模块读取的关系表。



