查询工作流
一句话提问进来,经过 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
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_tables | graphs.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;
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 预算 |
|---|---|
| simple | 1500 |
| standard | 2500 |
| complex | 4000 |
graphs.py:137 COMPLEXITY_BUDGET_TOKENS context_budget.py:116 assemble_context
预算由若干信息块竞争,块有优先级(少样本示例与用户事实最高,画像最低)。 装块之后还有一层块内裁剪——优先级决定「哪个块先被看到」, 相关度决定「块里哪几条先被看到」。设计意图见 Agent 能力 · 上下文工程。
继续读
| 想了解 | 去哪 |
|---|---|
| 生成节点内部的 agentic 循环 | Agent 能力总览 |
| 语义门禁与硬/软 MISS 的完整判据 | 语义层 |
| 每一步怎么观测、出问题看哪 | 可观测性 |
| 节点的输入输出状态定义 | API 参考 |