首页/架构/查询工作流

查询工作流

一句话提问进来,经过 28 个节点,一个答案出去。 这一页把图拆开:哪些节点是主干、哪些是分支、失败时系统怎么决定「再试一次」 还是「退回上一轮」。节点的实测行号都在,可以逐个点去核对。

图的形状

图的装配入口是 build_graphs,默认走 reflection 这张图 (另外两张是 fixed——直通不反思,与 empty——调试用空转)。 生产调用只传检查点,其余开关取默认值。

graphs.py:1402 build_graphs graphs.py:2109 _build_reflection graphs.py:368 build_gen_sql_subgraph

主干、两条岔路、两个环——一张图看完(跟随 _build_reflection 的真实 连边;框里横排的名字表示连续经过的节点;几条只通向 output 的收敛边 从略,见图注):

flowchart TD
    %% 配色 = 站点 token 的半透明版,按阶段着色:路由/出口 zinc · 门禁 amber ·
    %% 生成 indigo · 执行与校验 teal · 失败分析 红;填充低透明度、描边实色,亮暗两档同源
    START(["提问"]) --> RI["route_intent 意图路由"]

    RI -- "问数 / 归因" --> PD["parse_date → schema_linking"]
    RI -- "元数据" --> AM["answer_metadata ⇄<br/>metadata_check"]
    RI -- "闲聊 / 纠错 / 确认 / 写操作" --> AN["answer_reject · chitchat<br/>answer_correction<br/>confirm_draft 草稿确认"]

    PD --> GATE{"语义门禁"}
    GATE -- "无模型 / 硬 MISS" --> RF["refuse:拒绝<br/>并起草扩展草案"]
    GATE -- "可答" --> FM{"fast_match 模板快径"}

    RF -- "自动确认后重答" --> PD
    RF -- "本轮回引导" --> OUT(["output 交付"])

    FM -- "命中:跳过生成链" --> EX["execute_sql"]
    FM -- "未命中" --> QS["query_sketch 出计划"]
    QS --> GEN["gen_retrieve → gen_assemble → gen_generate"]
    GEN --> HITL["semantics → hitl"]
    HITL -- "通过" --> EX
    HITL -- "否决" --> OUT

    EX --> SEL["select → validate 规则链"]
    SEL -- "通过" --> MK["masking → reflect"]
    SEL -- "失败" --> AE["analyze_error 失败分析"]
    MK -- "失败" --> AE
    MK -- "通过" --> INS["insights → chart → conclusion"]
    INS --> OUT

    AE -- "回滚重试" --> GEN
    AE -- "到顶 / 回不动" --> OUT

    class START,OUT,RI,PD,AM,AN entry
    class GATE,FM,RF gate
    class QS,GEN,HITL gen
    class EX,SEL,MK,INS exec
    class AE err
    classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px;
    classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px;
    classDef gen fill:#6366f114,stroke:#6366f199,stroke-width:1.5px;
    classDef exec fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px;
    classDef err fill:#dc262614,stroke:#dc262699,stroke-width:1.5px;
    linkStyle 21 stroke:#8b5cf6,stroke-width:1.5px
图 1 · 默认图(reflection)的形状:一条主干、两条跳过生成的岔路(元数据自校验环、模板快径)、两个环(生成回滚、反思回滚)。归因类问题在 insights 前多经过一个条件节点 attribution。为免两条贯穿全图的竖线,answer_metadata 与三个终态答案节点到 output 的收敛边已从略——它们都是 output 的直接上游。颜色按阶段读:门禁 amber · 生成 indigo · 执行与校验 teal · 失败分析红;紫色的边是「回滚重试」。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。

生成段在顶层就是三个节点:gen_retrieve → gen_assemble → gen_generate(2026-09 从单节点拆开, 中间态落检查点,分步可观测、可独立测试)。两种生成形态在 gen_generate 内部:默认是 agentic 循环,空手 / 守卫触发时降级到经典 generate → validate 重试子图 (build_gen_sql_subgraph)。回滚阶梯里的 gen_sql 只是一个标签——判定器点名 “gen_sql” 时实际路由回 gen_retrieve。

28 个节点

下表按 add_node 的注册顺序列出。默认配置(不传开关)下就是这 28 个。

节点做什么注册位置
route_intent七分类意图判定,决定走哪条路graphs.py:1645
parse_date把「上个月」这类相对时间落成具体区间graphs.py:1650
answer_metadata元数据问答(有哪些表、某表什么结构)graphs.py:1651
metadata_check元数据答案的自校验,不通过则重答graphs.py:1655
answer_reject拒绝并说明原因(终态答案节点)graphs.py:1660
answer_chitchat闲聊兜底(终态答案节点)graphs.py:1661
answer_correction用户纠正上一轮答案(终态答案节点)graphs.py:1662
confirm_draft对话内确认语义模型草案,确认后重答原问题graphs.py:1664
schema_linking算出问题涉及哪些表 → matched_tablesgraphs.py:1929
gen_retrieve并行检索 KB:示例、教训、术语、指标、实体graphs.py:1936
gen_assemble把检索结果按预算装进生成提示graphs.py:1937
gen_generate生成 SQL(agentic 循环或经典子图)graphs.py:1938
execute_sql执行,含只读校验与骨架保真校验graphs.py:1945
select多候选共识投票(拿执行结果投票)graphs.py:1950
validate确定性规则链校验结果graphs.py:1955
masking字段级脱敏:结果出库、进任何 LLM 节点之前改写graphs.py:1960
reflect反思裁决:这一轮该继续还是回退graphs.py:1964
semantics把 SQL 解释成人话(供人看懂与 HITL 确认)graphs.py:1966
hitl人在环:执行前拦下 SQL 等确认graphs.py:1967
insights从结果里提炼要点graphs.py:1968
attribution归因类问题的分析graphs.py:1970
chart判断要不要画图、画什么图graphs.py:1975
conclusion生成结论段落graphs.py:1978
output汇总成最终答案graphs.py:1980
fast_match模板快径:命中则跳过整个生成链路graphs.py:1985
query_sketch出查询计划,并尝试语义编译graphs.py:2033
refuse语义门禁拒绝 + 起草扩展草案graphs.py:2034
analyze_error失败分析,决定回滚到哪一轮graphs.py:2090

clarify 不在这张表里:它只在显式传入 clarify=True 时才挂上 (graphs.py:2186 clarify)。 把它算进来的话是 29 个——但那是非默认口径。

意图分流:一条主路,两条岔路

路由节点把问题分成七类(问数 / 元数据 / 写操作 / 闲聊 / 纠错 / 确认 / 归因), 但图上只有三条走向:

意图去哪为什么
问数 / 归因 主干(parse_date → schema_linking → …) 归因也是问数,只是问题复杂些
元数据 answer_metadata → metadata_check 自校验环 「有哪些表」不该走 SQL 生成,且有独立的自我校验
闲聊 / 纠错 / 确认 / 写操作 各自终态答案节点 这些有确定答案,不需要生成 SQL

graphs.py:1500 make_route_intent graphs.py:1765 _route_after_intent graphs.py:1781 _route_after_refuse

写操作是被显式拦下并拒绝的,而不是「生成一个写 SQL 然后被数据库拒绝」。 拒写在意图层就发生——这是一道保险,真正的安全边界在别处(见安全边界)。

语义门禁:三条出口

schema_linking 之后有一个条件分支,它有三种可能的结果。 这段逻辑看起来只是路由,其实是「语义优先」这个产品定位的落点:

graphs.py:1901 _route_semantic_gate_after_linking graphs.py:1912 _route_semantic_gate_after_linking_fast_match graphs.py:1920 _route_semantic_gate_after_linking_gen_sql

三种结果分别对应:硬 MISS → refuse(拒绝并引导扩展模型)、 正常 → 继续(快径或生成)、数据源没有语义模型 → refuse。 软 MISS 不走这条分支——它跟着计划一起流下去,产出骨架。

细节在语义层与产品概念。

结果校验:规则链,不是模型判断

执行完之后,结果要过一条确定性规则链。规则按注册顺序执行, 第一条失败的规则胜出——顺序就是优先级,最具体、最便宜的在前面。

rules.py:212 Rule rules.py:292 verify

族检查什么
F1 形状「最高是多少」这类单值问题必须只返回一行一列
F4 排序问题里说了「最高/最低/前几」,SQL 里得有对应的排序
F2 过滤问题里点名的实体,必须在 SQL 条件里出现
F3 取值类型、唯一性、取值范围是否合理

这些都是纯代码、零 LLM 的判断。它们的价值不只是「省一次模型调用」,而是 可回归测试、且换模型不退化——一条规则写对了,它永远对。

规则链之后是 masking:结果还在,但按语义模型里逐字段声明的 partial / hash / null 改写一遍,然后才交给 任何面向模型的节点。顺序是刻意的——先判原始数据对不对,再脱敏给人看: 规则链看的是真实值,脱敏后下游拿到的才是该给人看的形态。它在图上是独立节点而不是 挂在 select 后面的一个步骤,因为在快径命中时 select 会直接返回, 挂在里面的后置步根本不会执行。失败方向取严:该脱敏而 salt 或语义模型读不出来, 拒绝这次查询,不降级放行。

回滚阶梯:失败之后怎么走

失败会进入 analyze_error,由它决定回滚到哪一步。 这里最容易犯的错是「无脑重试」——那会让系统在两轮之间来回振荡,烧钱且不收敛。 所以每一轮的 SQL 与结果签名都被记下来,跟上一轮做确定性比对。

versions.py:60 record_version versions.py:32 result_sig versions.py:89 regression_state

判定含义含义背后的动作
无效修复 改了 SQL 但结果签名没变 这一轮白改,反馈里要明说「你刚才那个改动没生效」
没有进步 结果变了,但规则链仍然失败在同一处 问题不在被改的地方,别再沿着这条线试
问题漂移 原来失败的点过去了,却冒出一个新的失败点 可能是修一个坏一个,需要警惕

versions.py:126 regression_report graphs.py:1466 _route_after_reflect

回滚不是「打回上一步」一种:analyze_error 会从下面这几个落点里点名一个, 每一档对应一类不同的错法:

flowchart LR
    AE["analyze_error 失败分析"] -- "重写 SQL" --> GR["gen_retrieve"]
    AE -- "重做计划" --> QS["query_sketch"]
    AE -- "重新认表" --> SL["schema_linking"]
    AE -- "其实是元数据题" --> AM["answer_metadata"]
    AE -- "到顶 / 回不动" --> OUT["output 交付已知 + 说明不确定性"]
    class AE err
    class GR,QS gen
    class SL,AM,OUT entry
    classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px;
    classDef gen fill:#6366f114,stroke:#6366f199,stroke-width:1.5px;
    classDef err fill:#dc262614,stroke:#dc262699,stroke-width:1.5px;
图 2 · 回滚落点。(gen_sql 是“重写 SQL”这一档在判定器里的名字,实际路由回 gen_retrieve。) 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。
为什么「反馈」比「重试次数」重要

同一个模型、同样的提示,重试一次大概率得到同样的错误。真正让它改变方向的是告诉它上一次错在哪——而这恰恰是不该让模型自己判断的部分:模型倾向于认为自己上次是对的。

所以这里的反馈是算出来的,不是感觉出来的。

重试有硬上限。MAX_REFLECT_RETRIES 是执行错误、规则失败、一致性、 裁决共享的一个总闸——到顶就交付当前最好的结果,并把不确定性说清楚, 而不是无限循环。

graphs.py:134 MAX_REFLECT_RETRIES graphs.py:1459 _route_after_hitl graphs.py:2084 _route_after_query_sketch

上下文预算

生成提示的容量按问题复杂度分档。分档不是「大概估一下」,而是三个写死的常量:

档位token 预算
simple1500
standard2500
complex4000

graphs.py:137 COMPLEXITY_BUDGET_TOKENS context_budget.py:116 assemble_context

预算由若干信息块竞争,块有优先级(少样本示例与用户事实最高,画像最低)。 装块之后还有一层块内裁剪——优先级决定「哪个块先被看到」, 相关度决定「块里哪几条先被看到」。设计意图见 Agent 能力 · 上下文工程。

继续读

想了解去哪
生成节点内部的 agentic 循环Agent 能力总览
语义门禁与硬/软 MISS 的完整判据语义层
每一步怎么观测、出问题看哪可观测性
节点的输入输出状态定义API 参考
本页源码锚点的基准提交:anchor-base: 7c6df87 GitHub 仓库 文档首页