首页/数据能力/知识库

知识库

语义层负责编译,知识库负责喂给它原料:指标口径、字段含义、枚举取值、 可复用的问答对、以及踩过的坑。这一页讲这些原料从哪来、怎么被检索到、以及为什么不能自动写进去。

五个知识文件,按「谁能写」分工

KB 的形态是一组 YAML,人可读可改,也是唯一真源。运行时另有一份 SQLite 镜像 (kb.sqlite,带 FTS5 虚拟表)——但镜像只服务检索, 删掉它可以从 YAML 重建,反过来则不行。

文件承载什么写入方
schema_notes.yml 表/列注释、枚举取值、统计画像 /kb init 起草,人可改
semantics.yml 语义模型(数据集 / 指标 / 字段 / 关系) /kb init 起草,人审
examples.yml 问答对(example)与模板(template) /kb init + /kb learn + 反馈回写
rules.yml 生成时必须遵守的规则文本 纯人工——代码里没有任何写入器
lessons.yml 失败模式与规避方法 蒸馏/反馈路径懒创建,两档确认

service.py:3 human-editable service.py:530 _parse_file merge.py:54 纯人工文件

同目录下的另外两个文件

它们住在同一个目录、同一套 git 版本管理之下,但不属于上面那五个「知识」文件—— 前者是策略不是知识,后者还不是真源。

文件承载什么写入方
decisions.yml 判定规则——同一份语义模型上「算不算有问题」的阈值与条件,供定时任务求值 管理端接口整篇替换,落盘前强制 lint
semantic_drafts.yml 硬 MISS 反问产出的待确认语义模型扩展草案 反问路径自动写,确认后才并入 semantics.yml

service.py:1123 save_decisions decisions.py:648 put_decisions manage.py:1129 create_draft

把它们折进「知识」里会糊掉一件事:已确认的事实与待确认的变更 不是同一种东西。判定规则与草案各自有别的消费者,也各有自己的确认门。 判定规则的完整机制见判定规则。

为什么 rules.yml 要「纯人工」到这个地步

合并逻辑里对它的处理是原样保留,没有任何生成器会产出它。这不是疏忽,是分工:schema_notes 与 semantics 描述的是数据长什么样,可以从库里探测出来;rules 描述的是组织要求怎么算,库里没有这个信息。

把它交给 LLM 起草,等于让模型替组织定口径——那是它不该有的权力。

/kb init:能确定性生成的才生成

初始化一个数据源的 KB,走的是一条确定性优先的流水线。 顺序上先做不花钱的部分,再让 LLM 润色:

  1. 读活库 schema(表、列、类型)。
  2. 没有 LLM 凭证时的降级路径:只写一份 schema_notes.yml 骨架并返回提示,让人补注释后 /kb reload。
  3. 枚举采样与列统计画像(这一步会真的查库)。
  4. LLM 起草表/列注释。
  5. 确定性生成词表与模板——generate_terms / generate_templates。
  6. 合成示例,生成语义模型结构。
  7. LLM 起草同义词与描述。
  8. 原子写盘:三个文件连续写、中间不 await——避免留下写了一半的 KB。

init_pipeline.py:441 init_kb deterministic_gen.py:460 generate_terms deterministic_gen.py:623 generate_templates semantic_gen.py:210 generate_semantic_document

产出的文件是三个:schema_notes.yml、semantics.yml、examples.yml。 rules.yml 与 lessons.yml 不在其中—— 前者的原因是上面说的分工,后者是反馈路径首次写入时才创建。

init_pipeline.py:438 INIT_FILES

防作弊约束的形状

这条流水线的产物边界,就是「什么算真实的准确率提升」的边界:凡是 /kb init 不能确定性生成的内容,手工塞进去都不算。特别是把评测集的 gold SQL 抄进 examples.yml——那不是系统变准了,是答案被泄露了。

相应地,模板快径也只用 kind='template' 的条目,因为那是确定性产物,人塞的东西进不了这条快径。

/kb learn:把一次好的问答固化下来

交互式的沉淀路径是半自动的:模型从上一轮对话起草一条示例(外加候选术语), 把草稿展示出来,只有用户明确确认才写盘。

kb_cmds.py:49 _cmd_learn service.py:2487 append_example service.py:2495 append_term

注意这里的确认与后面的「两档门禁」不是同一件事:/kb learn --yes 是当下的人工确认, 写进去就是 confirmed;而自动回写的示例进来时是 pending,需要另一次确认。 区别在来源——人当场看过并点过头的,和系统自己觉得不错的,不该同等对待。

检索:先确定性过滤,再打分

检索的入口在图上(一次问答会并行取示例、教训、术语、指标、实体), 每条通道内部都遵循同一条原则:先用确定性规则排除不可能的,再排序。

graphs.py:470 search_examples service.py:1537 search_examples service.py:1595 _rank_examples

最重要的那条过滤是表锚定:语义链接阶段已经算出了这个问题涉及哪些表 (matched_tables),那么一条示例如果提到了别的表,它就不该被检索到—— 哪怕文字上再像。这是一个纯确定性的判断,不依赖任何模型。

service.py:1618 keep service.py:411 _lesson_table_ok

排序分由几路信号合成:术语命中、标签命中、词重叠、表锚定加分, 再乘上聚合/日期模板的折扣(模板类示例的泛化性弱一些,不该压过具体示例)。 与检索通道给出的相似度做融合时,权重是 0.5——这个数字是刻意的中庸值, 因为确定性打分和向量相似度各有所长,谁压倒谁都不合适。

service.py:289 _score_example service.py:462 _fuse_extra_sim

教训的排序多两个因子:投票与时效。 一条被反复验证有效的教训应当排前面,一条很久没被触发的教训应当淡出—— 后者用衰减函数实现,而不是靠人工清理。

service.py:1775 _rank_lessons service.py:445 _recency_factor

镜像与 FTS

YAML 是给人读的,逐条扫描它做检索太慢,所以运行时维护一份 SQLite 镜像。 FTS5 虚拟表是 contentless 的——它只存索引,元数据列标为 UNINDEXED, rowid 与镜像表的主键对齐。

service.py:122 _CREATE_FTS service.py:1180 _ensure_mirror_schema service.py:934 _sync_file_fts

在 PostgreSQL 生产环境里,检索不走这条 SQLite 路径,而是切到 pg_hybrid 后端—— 切换逻辑见 混合检索。两者的分界点是配置与能力探测, 不是数据源类型。

取值画像:让「枚举」这件事有依据

很多问数错误来自取值层的误解:「华东区」到底是 '华东' 还是 'east'? 状态字段有哪些合法值?这些不属于 schema,但决定了 SQL 里的字面量写什么。

画像流程对每个可疑列做一次取值采样,取 Top-K 高频值与频次,连同形状判断 (是枚举?是 ID?是自由文本?)一起合并进 schema_notes.yml 的 stats。

profiling.py:31 TOP_VALUES_LIMIT profiling.py:55 detect_shape profiling.py:195 _probe_top_values profiling.py:253 merge_into_stats

独立脚本 scripts/probe_enums.py 做同一件事,但带两道护栏: 行数上限与超时。因为「对一亿行的大表做全表 GROUP BY」是一个很容易写出来、 又很容易把生产库拖垮的操作。

enum_probe.py:37 DEFAULT_MAX_ROWS enum_probe.py:48 probe_enums

lint:把 KB 的问题在提问之前发现

scripts/lint_kb.py 是专门挑 KB 毛病的静态检查工具 (另有一把专查「抄 gold」的尺子,见本节末尾)。 它检查的都是「不报错但会一直悄悄降低准确率」的问题:

类别典型问题
静态 术语 mapping 引用了不存在的列;对 ID 类列写 SUM/AVG;示例 SQL 解析不了、引用了不存在的表、含写操作
静态 纯中文问题(英文检索不可达);列描述为空;教训的 pattern 过长或 note 为空
需要活库 schema_notes 里声明的枚举值漏了库里真实存在的值(如 loan.status 少了一个 'C')
需要活库 库里存在但 KB 完全没记录的列

live_lint.py:19 check_enums live_lint.py:47 check_undocumented_columns

最后一条尤其值得注意:漏掉一个枚举值不会让任何查询报错, 它只会让「状态 = C 的那批」这类问题永远算错——因为编译器认为 'C' 不是一个合法取值。 这类错误没有报错信号,只能靠主动比对发现。

同一套检查还有个写盘前的门禁版本:语义模型的写入路径在落盘之前先跑一遍 lint_semantics,只要有 issue 就 raise——不落盘、不刷新检索镜像, 坏语义根本进不了 KB。命令行上的 lint_kb.py 是给整套 KB 做体检, 这里挡的是「这一次写入」。

反背题:一把挂在 CI 上的尺子

scripts/check_kb_anti_cheat.py 做三级比对(字符串归一化、语法树相等、 字面量盲化后结构相等)来抓「KB 里的 SQL 抄了 gold」。检查范围是所有可检索条目—— pending 草稿按项目规则不进检索,所以跳过,但跳过条数每次都打印, 免得「查了 622 条」被读成「全查了」。

判定分两档,这是本脚本最容易被误读的地方:template: true 的条目命中 gold 判 违规(模板是 kb init 确定性生成的,命中只可能是有人塞进去的); 已确认的人工条目命中只告警不判违规(捕获的成功案例本来就允许接近 gold), 但一定会打出来——那条豁免通道同时也是抄 gold 最省事的形状,静默的豁免等于假绿。

零 LLM、零网络、零数据库,已经挂在 CI 上,每个 PR 都跑。

两档门禁:pending 不进检索

所有自动写入的内容默认是 pending。读侧的过滤器是硬的: 待确认的示例直接跳过,教训列表默认只返回 confirmed。

flowchart TB
    Q["提问 → 回答"]:::entry --> D{"这条内容从哪来"}:::entry
    D -- "人当场点头(/kb learn --yes)" --> K
    D -- "自动回写 / 蒸馏" --> P["pending 草稿
示例 · 教训"]:::entry P --> G{"管理端确认"}:::gate G -- "确认" --> K[("KB YAML
examples / lessons")]:::entry G -- "拒绝" --> X["丢弃"]:::entry K --> R["检索:先表锚定过滤,再打分"]:::exec R --> Q classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px; classDef exec fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px; classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px; linkStyle 7 stroke:#8b5cf6,stroke-width:1.5px
图 1 · 学习闭环:两条写入路径共用同一本 KB,自动回来的一律先在 pending 里等一次人工确认——这就是「准确率随使用增长」的机制。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。

service.py:617 pending 草稿不参与检索 service.py:1725 confirmed_only service.py:1823 setdefault

门禁档内容确认方式
同步确认 /kb learn 的草稿 用户当场看见并加 --yes
异步确认 自动回写的示例、蒸馏出的教训、自动偏好 管理端列表逐条或批量确认

service.py:1900 confirm_pending_lessons service.py:2003 confirm_pending_examples service.py:2221 reject_lesson

管理端待审批队列(KB · financial):顶部 KPI 待审批 6(教训 2 · 示例 4)与 tab 行;下面每条待审项带来源、票数、置信度与说明,各配「确认 / 编辑后确认 / 拒绝」三个按钮 1 2 3 4
图 2 · 管理端的待审批队列(KB · financial):两档门禁里「异步确认」那一档落到人眼前的样子——自动回来的内容在这里逐条等一次确认。
  1. 1待审批 KPI——数字即缺口:6 条待审,拆开是教训 2 · 示例 4。待审的不是「脏数据」,是还没人背书的内容。
  2. 2tab 行的数字是过滤器——待审批 / 资产 / 术语与指标 / 示例 / 教训 / 规则,每一类各自计数;点哪个数字就按哪一类过滤。
  3. 3每条待审项带着它是怎么来的——来源(用户投票 / 自动捕获)、时间、票数 ▲1 · ▼0 与置信度(这条 0% 照实显示,不粉饰),下面一行是它的说明;这条教训卡还写明确认后的去向:进 Hint Bank 检索、注入后续生成的上下文。
  4. 4逐条处置:确认 / 编辑后确认 / 拒绝——确认写入认证块(批准人 / 时间)并进入检索;「编辑后确认」是让确认人先改再背书,而不是先背书再指望有人回来修。

资产治理:这条 SQL 是谁确认的

确认不只是一个布尔开关。示例条目带五个治理维度, 在确认的那一刻逐条写进 examples.yml——与 SQL 同一份文件,随 git 一起回滚:

维度取值含义
status draft / certified / deprecated 治理状态。缺省即 draft——draft 不是「未知」,是「还没人背书」,两者对检索的含义相同
owner 自由文本 这条资产归谁维护
approved_by 用户名 谁背的书。status: certified 时必须与 approved_at 一起在场,缺一就降回 draft——机器不能自己给自己背书
approved_at 日期 什么时候背的书。只降 status、不擦这两个字段,因为它们是修好这条记录唯一的线索
source human / memory / kb_init / user_feedback 这条从哪来。存量资产标 kb_init——那是从历史推断出来的,不是谁写上去的

governance.py:36 GOVERNANCE_FIELDS service.py:2003 confirm_pending_examples

台账已实现,但还没接线

AssetLedger(每条资产被用了几次、成没成、p50 耗时)已实现、有测试钉住, 但没有任何生产构造点——答案是「不猜、不编」:拿不到统计就只报档位那一档, 而不是编一个数出来。另外 status 今天只披露、还没参与检索加权。

统计不写进 examples.yml 是有理由的:那个目录每次写入都触发一次 git commit,高频运行时数据会把资产库的历史淹在统计噪声里,真正想回看的那次「某条 SQL 被谁改坏了」也就找不到了。

一条会咬人的约束:KB 语言要跟着问题语言走

这是一个实测过一次的定性结论,值得写在这里而不是埋在文档角落: KB 内容的语言应当与提问语言一致。用中文 KB 去回答英文问题时, 准确率会明显下降,而且主要缺口不在生成、在少样本检索的命中—— 检索不到对的示例,后面再强也没用。

这是设计约束,不是运行期检查

代码里没有任何地方会强制或警告语言不匹配。它写在约束文档里,靠使用者和 lint 的「纯中文问题」检查来兜。所以如果你换了提问语言,记得配套重建 KB。

相关但不同的一件事:scripts/check_kb_anti_cheat.py 那把反背题的尺子(见 lint 一节)。

继续读

想了解去哪
KB 内容怎么被编译成 SQL语义层
两路召回与 RRF 融合的具体参数混合检索
自动回写的完整路径与记忆分层记忆子系统
数据漂移了 KB 怎么知道数据漂移治理
同一份语义模型还能用来做什么判定规则
本页源码锚点的基准提交:anchor-base: 7c6df87 GitHub 仓库 文档首页