从零手写的 Text-to-SQL Agent。不依赖 LangChain / LangGraph, 自己实现 ReAct 循环、工具调用与 SQL 执行反馈。 重点是失败归因与评估驱动的优化。
| 项 | 结果 |
|---|---|
| 数据库 | SQLite。核心 3 张表;规模化实验扩展到 292 张表(含 3 张"陷阱表") |
| 核心链路 | 问题 → ReAct 循环(思考 → 行动 → 观察)→ 生成 SQL → 执行 → 纠错 → 自然语言回答 |
| Agent 的本质 | 模型(决策)+ 工具(行动)+ 循环(多步) |
| 评估集 | 15 题 6 类(简单 / 单位 / 聚合 / JOIN / 多步 / 不可答),每题带人工编写的标准 SQL |
| 宽松执行准确率 | baseline 0.85 → 最佳变体 1.00(15/15 全对) |
| 工具改进收益 | 把带注释的 schema 放进系统提示:单位题 0.50 → 1.00,平均步数 6.00 → 2.73(−54%)、工具调用 −68% |
| 规模化结论 | 表从 3 张扩到 292 张(schema 放大 76 倍):准确率不掉、步数不涨 —— "塞不下会迷失"的假设被证伪 |
| 给完整 schema 会让模型以为它已经知道数据:292 张表下 80% 跳过查询、凭记忆编造精确数字。加代码层护栏后降到 0% | |
| 拒答准确率 | 1.00 —— 问了数据库里没有的数据,两次都老实拒答 |
| 实验量 | 4 个工具变体对照 · 292 张表规模化实验 · 10 个 bug 修复(含 5 次评估脚本自身的) |
十个核心发现(详见「实验与发现」)
- 干净的小 schema 上,一次生成 SQL 就能答对 4/5 —— Agent 的价值不在这种场景
- ⭐ 模型会编造解释来包装错误答案:同一个字段,它先说「亿元」后说「万元」
- 最危险的失败不是语法错,是语义错:有效 SQL 率 100%,但准确率不是 100%
- ⭐⭐ 评估指标本身会骗人:我的尺子出过四次错,每次都差点得出错误结论
- Agent 知道自己不知道,却没有工具去查证 —— 工具的信息密度决定它的上限
- Agent 会把该由数据库做的计算留给自己(口算)
- ⭐⭐ "让模型自己从数据推断单位"被实验证伪 —— 采样只能排除一个数量级,无法在"万元"和"亿元"之间判断
- 减少无效步骤有隐性收益 —— 省下的步数预算救了那些真正需要多步的题
- ⭐⭐ "表多了会迷失"也被证伪 —— 3 张表 → 292 张表,准确率 0.9231 完全不变,它还识破了 3 张陷阱表
- ⭐⭐⭐ 真正的瓶颈是"信息给太多" —— 完整 schema 会让模型产生「我已经知道数据」的错觉,跳过查询直接编造精确数字;提示词拦不住,必须在代码层加护栏
用户用中文提问,系统自己决定查哪些表、写 SQL、执行、发现错误后自己改, 最后用自然语言回答。
问题:"原油产量最高的那家公司,2023年的净利润是多少?"
↓
[ReAct 循环]
Thought: 先看看有哪些表
Action: list_tables
Observation: companies, financials, production
Thought: 需要看 production 的字段
Action: describe_table
Action Input: production
Observation: id, company_id, year, product, amount, unit
Thought: 查原油产量最高的公司
Action: run_sql
Action Input: SELECT company_id, amount FROM production
WHERE product='原油' ORDER BY amount DESC LIMIT 1
Observation: [[1, 9.36, "亿桶"]]
Thought: 是 company_id=1,再查它的净利润
Action: run_sql
Action Input: SELECT c.name, f.net_profit FROM companies c
JOIN financials f ON f.company_id=c.company_id
WHERE c.company_id=1 AND f.year=2023
Observation: [["中国石油", 1611.5]]
Final Answer: 原油产量最高的公司是中国石油(9.36亿桶),
其2023年净利润为1611.5亿元。
注意它分了两步查,而不是硬写一条大 SQL —— 这正是 Agent 的价值: 可以把问题拆成几步,每步基于上一步的真实结果决定下一步。
整套机制一共 100 行左右:一个 for 循环 + 一张工具分发表。
自己写一遍才知道框架到底替你做了什么。
具体收获:
- "工具调用"的本质是:模型只负责"决定调哪个",真正执行的是你的代码。 模型自己什么都做不了,它只会输出文字。
messages数组为什么会越来越长:模型的思考(assistant)和工具的反馈(user) 都被追加进对话历史 —— 这就是 Agent 的"记忆",也是它上下文会爆的原因。- 工具返回什么信息,直接决定模型能不能纠错(见发现 ⑤)。
做法:把 3 张表的完整 schema 塞进提示词,让模型一次生成 SQL,执行,不循环。
结果:5 个问题答对 4 个,包括一道需要自连接的题。
结论:Agent 的价值不在"干净的小库"上。
这个 schema 只有 3 张表、字段名直白(revenue / net_profit)、数据干净。
真正的企业库是:表名像 T_FIN_2023Q4_BAK,字段是 A0101、yysr 这种代号,
同一个"营业收入"在三张表里口径不同。那才是 Agent 的主场。
同一个字段 revenue,它在两道题里给出了两个不同的单位:
| 题 | 回答里的"说明" | 实际用的阈值 |
|---|---|---|
| q005 超过 1 万亿 | "revenue 字段的单位为【亿元】,1 万亿元 = 10000 亿元" | revenue > 10000 |
| q006 超过 1 亿 | "revenue 字段单位为【万元】,超过1亿元即 revenue > 10000 万元" | revenue > 10000 |
它两次用了同一个阈值,只是倒推了两个不同的单位来解释。
q005 恰好蒙对了,q006 错了(正确答案 6 家,它说 2 家)。
而且它的措辞在反复横跳:
- q001:"(单位与表中一致)"
- q003:"(单位以数据库原始记录为准,通常为亿元)"
- q005:"(单位为亿元)" ← 开始用肯定语气了
- q006:"(单位为万元)" ← 换了一个,同样是肯定语气
这比"答错"危险得多。
单纯的错误,用户会怀疑;但错数字配上看起来很专业的"字段说明", 大多数人就信了 —— 错误被包装成了可信。
这是"引用幻觉"的变体:RAG 项目里是假答案配真格式的引用, 这里是错数字配编造的字段说明。
有效 SQL 率 1.0000 ← 生成的 SQL 全都能跑通
宽松准确率 0.8718 ← 但只有 87% 的答案是对的
"SQL 能执行"和"答案正确"是两回事。
单位错误那类问题尤其危险:
| 语法错(字段名写错) | 语义错(单位理解错) | |
|---|---|---|
| SQL 能执行吗 | ❌ 报错 | ✅ 正常执行 |
| 有异常吗 | ✅ 有 | ❌ 没有 |
| 返回什么 | 错误信息 | 一个看起来正常的结果 |
| 你能发现吗 | 一眼发现 | 得自己算一遍才知道 |
崩溃你知道要修;返回一个正常但错误的数字,你还以为它答对了。
我的尺子出过三次错,每一次都差点让我得出错误结论。
| # | 现象 | 真因 |
|---|---|---|
| 1 | 执行准确率只有 46.7% | 判定标准太严——它把"多查了一列"当成错误。用户问"营收最高的公司是哪家",Agent 返回 ('中国石化', 32122.2),多带了数值,其实答得更好 |
| 2 | 同一份代码跑两次,13 题里 4 题行为不同 | 一开始归因于 LLM 非确定性(temperature=0 也不保证确定) |
| 3 | q012 判"没有产出可执行的 SQL"、q010 判"✅" | 两个真 bug:① trace 里 arg[:200] 把长 SQL 截断了,评估重新执行时变成语法错;② 宽松判定放过了"查到过但没答出来"(回答是"达到最大步数,未得出结论") |
修完之后的对比:
| 指标 | 修之前 | 修之后 |
|---|---|---|
| 严格准确率 | 0.1538 | 0.2821(范围 0.23~0.31) |
| 宽松准确率 | 0.7692 | 0.8718(范围 0.85~0.92) |
| 噪声幅度 | 估计 ±20pp | 实际 ±4pp(约 1 道题) |
第 2 条尤其值得说:我原以为"两次结果不同"是模型不确定性的锅, 修完两个 bug 才发现 —— 一部分"随机性"其实是评估 bug 造成的假象。
把测量工具的缺陷误判成被测量对象的特性,是很容易犯的错误。
教训:
指标异常时,先怀疑指标,再怀疑模型。
这是我在两个项目里第二次踩同一个坑:
| 项目 | 表面现象 | 真因 |
|---|---|---|
| RAG 项目 | 重排把正确答案从第 1 打到第 7 | 我的标准答案定义太窄 |
| 本项目 | 执行准确率只有 46.7% | 我的尺子太严 |
引入 ReAct 循环后,看它回答的结尾措辞:
"营业收入为 32122.2(单位与表中一致)"
"净利润为 1611.5(单位与表中一致,通常为亿元)" ← 它在猜
它多次主动声明"不知道单位",还补了"通常为亿元"—— 它在猜,而且知道自己是在猜。
但它的工具给不了答案:
describe_table(financials) →
revenue REAL ← 只有字段名和类型,没有单位、没有口径、没有样例数据
net_profit REAL
更关键的是:它从头到尾没有运行过 SELECT * FROM financials LIMIT 3——
它从来没"看过"数据长什么样,只知道字段叫 revenue,
不知道这个数是 32122 还是 32122000000。
两个结论:
- 加循环修不了单位问题 —— SQL 执行成功、没有报错, 循环里没有任何信息告诉它单位错了。它需要的是更好的上下文,不是更多的尝试机会。
- 工具的信息密度,决定 Agent 的能力上限。
q011「中国石化和中国石油2023年的营业收入相差多少?」:
Agent 的 SQL: SELECT c.name, f.revenue FROM ... WHERE c.name IN (...) ← 只查了两个原始值
最终回答: "两者相差约 2012.1" ← 它自己心算的
它算对了(32122.2 − 30110.1 = 2012.1)。但注意它的措辞:"约 2012.1"—— 它自己也不确定算得对不对,所以加了"约"。
这违反了 Text-to-SQL 的设计原则:
Text-to-SQL 存在的全部理由,就是把计算交给数据库。 如果让模型在自然语言层做算术,那直接给它两个数字让它口算就行了,不需要 SQL。
三个具体问题:
| 问题 | 说明 |
|---|---|
| 它会算错 | 大模型算术不可靠,尤其大数、多步 |
| 没有校验 | SQL 的计算有数据库保证;口算错了没人知道 |
| 不可审计 | SQL 是白纸黑字的证据;"它在脑子里减了一下"没法追溯 |
评估判定它 ❌ 是正确的 —— 它抓住的是"设计原则被违反",不是"格式不好看"。
平均 5.76 步里,约一半浪费在 list_tables + describe_table 上,
而 schema 是静态的,每次探索结果一模一样。
→ 应该把 schema 直接放进系统提示。 但一旦 schema 大到放不下,就变成了更难的问题(见「下一步」)。
阶段 3 的主靶子是 q006:「营收超过 1 亿元的公司有几家」—— 基线答 2 家(实际 6 家),还编了个"revenue 单位是万元"的解释来自圆其说。
我做了三个对照方案:
| 变体 | 做法 | 依赖人工注释? |
|---|---|---|
A_schema |
把带注释的完整 schema 直接塞进系统提示 | ✅ 是 |
B_sample |
新增 sample_rows 工具,让模型自己看数据数量级推断单位 |
❌ 否 |
C_rich |
describe_table 返回时附上单位说明 |
✅ 是 |
结果(15 题,评估脚本已修正 4 个 bug):
| 变体 | 宽松准确率 | unit 类 | 平均步数 | 工具调用 |
|---|---|---|---|---|
| baseline | 0.8462 | 0.50 | 6.00 | 5.07 |
| A_schema | 1.0000 | 1.00 | 2.73 | 1.60 |
| B_sample | 0.8462 | 0.50 | 6.47 | 5.53 |
| C_rich | 1.0000 | 1.00 | 5.00 | 4.00 |
四个结论:
① 只有"人工注释"能修单位问题。 A 和 C 都做到 15/15;B 采样之后仍然推出"万元"—— 两次独立运行都错在同一题、同一个单位。
② "从数量级推单位"被实验证伪。 采样数据只能排除"元"(3 万元营收不合理), 无法在"万元"和"亿元"之间做判断—— 那需要"中石油营收约 3 万亿"这个先验知识,而模型没有。
而且同一个变体在两道题里推出了不同的单位(q006 说万元、q011 说亿元)—— 不稳定比错更糟糕:错了你能发现,不稳定你连规律都总结不出来。
③ A 的步数优势是结构性的,不是噪声。
从 6.00 降到 2.73 步(−54%),工具调用从 5.07 降到 1.60(−68%)——
因为 schema 已经在系统提示里,不需要 list_tables + describe_table 探索。
这是确定性的因果,不受 LLM 随机性影响,所以哪怕只跑一次结论也稳。
顺带还救了一道题:q010 在 baseline / B_sample 里都是"达到最大步数,未得出结论", 在 A 里只用 4 步就完成了 —— 省下的步数预算给了真正需要多步的题。
④ 但 A 不可扩展。 它把整个 schema 塞进系统提示。3 张表没问题, 真实企业库 800 张表塞不下、还会让模型迷失在无关表里。 这正是下一步要解决的问题。
| # | 现象 | 真因 |
|---|---|---|
| 1 | 准确率只有 46.7% | 判定标准太严 —— 把"多查了一列"当成错误 |
| 2 | 同一份代码跑两次,13 题里 4 题行为不同 | 一部分是评估 bug 造成的假象(见 #3) |
| 3 | q012 判"没有产出可执行的 SQL" | trace 里 arg[:200] 把长 SQL 截断了,评估重新执行时变成语法错 |
| 4 | q007 答对了却判错 | 评估只取最后一次 SQL 结果,而 Agent 先算出平均值、又查了一次明细来展示过程 |
修 #4 的办法:检查 trace 里所有 SQL 结果,任意一次匹配就算对。
四次里有三次,我第一反应都是"模型有问题"。 但每次查证之后,问题都在测量工具上。
教训:指标异常时,先怀疑指标,再怀疑模型。
原假设:表多了 schema 塞不下 → 模型迷失在无关表里 → 需要"选表"。
做法:把数据库从 3 张表扩到 292 张表(schema 从 ~530 字符涨到 29,584 字符,放大 76 倍),
其中埋了 3 张陷阱表 —— fin_bak_2023 / fin_parent_only / tmp_fin_import,
字段长得像财务表,但数据是错的(备份快照 / 母公司口径 / 未完成的临时表)。
真正的企业库里这种表遍地都是。
结果:
| 指标 | 3 张表 | 28 张表 | 292 张表 |
|---|---|---|---|
| 宽松准确率 | 1.0000 | 0.9231 | 0.9231 |
| 平均步数 | 2.73 | 2.80 | 2.53 |
| 工具调用 | 1.60 | 1.80 | 1.47 |
准确率一点没掉,步数还更少了。
而且它正确穿越了 292 张表:
q014(员工人数): "employees_2023 表虽然名字看起来像员工数据,
但实际内容是示例数据(字段为 code/name/amount/...)" ← 识破了
q015(2024 数据): "所有财务相关表(financials、fin_bak_2023、
fin_parent_only、tmp_fin_import)的年份范围都只到 2023 年" ← 4 张全查了
所以"塞不下 / 会迷失"这个假设是错的 —— 至少在 292 张表的规模下不成立。
这是整个项目里最重要的一个发现。
现象:在 292 张表下,A_schema 变体在 q011「两家公司营收相差多少」上
一次 SQL 都不执行就直接给答案:
它说:32122.15 / 30110.12 / 2012.03
真值:32122.2 / 30110.1 / 2012.1
三个数字全错,但内部完全自洽(32122.15 − 30110.12 = 2012.03), 而且看起来比真值更精确(多一位小数)。
为了确认这不是随机噪声,我写了个探针脚本,在两个库上各跑 5 次:
| 数据库 | 变体 | 跳过查询次数 |
|---|---|---|
| demo.db(3 张表) | A_schema | 0 / 5 |
| C_rich | 0 / 5 | |
| baseline | 0 / 5 | |
| big.db(292 张表) | A_schema | 4 / 5(80%) 🔴 |
| C_rich | 0 / 5 | |
| baseline | 0 / 5 |
四个关键特征:
① 只有 A_schema 中招。
C_rich 和 baseline 的 schema 是刚用工具查到的;
A_schema 的 schema 是写在系统提示里的"背景知识"。
模型把"背景知识"当成了"我已经掌握的数据"。
② 剂量依赖。 3 张表 0%,292 张表 80% —— 信息给得越多,错觉越强。
③ 数字确定且自洽。 四次跳过,四次都是同一组数字。
随机错误你能发现规律,确定性的错误你会以为它是对的。 而且它内部自洽 —— 你自己验算一遍也发现不了问题。
④ 问题相关。 完整评估里只有 q011 中招 —— 它是那种模型自认为 "我知道这两家公司大概多大"的问题。
越是你觉得它该知道的问题,它越可能不去查。
修法:代码层护栏,不是提示词。
提示词里早就写了「所有数字必须来自 run_sql 的真实返回结果」,
但它有 80% 的概率无视。所以在 run_agent 里加了一道硬约束:
# 模型想给最终答案时,先检查它到底查过库没有
if name is None and arg is not None:
queried = any(t["type"] == "tool" and t["tool"] == "run_sql" for t in trace)
if not queried:
# 拒绝这个答案,要求它先查
messages.append({"role": "user",
"content": "你还没有执行过任何 SQL 查询,不能凭记忆回答。"
"请先调用 run_sql 查询数据。"})
continue
return arg, trace验证:292 张表下 A_schema 的跳过率 80% → 0%
(5 次全部查库、全部给出正确的 2012.1)。
两句话总结:
- "给模型更多信息"不总是好事 —— 信息给多了,它会以为自己已经知道答案。
- 提示词是"建议",代码才是"约束"。
评估集:15 题 6 类,每题带人工编写的标准 SQL。 指标:执行准确率(Execution Accuracy)—— 沿用 Spider / BIRD 等学术基准的做法。
运行 3 次取平均(python src/stage3_eval.py --repeat 3):
| 指标 | 平均值 | 范围 | 说明 |
|---|---|---|---|
| 宽松执行准确率 | 0.8718 | 0.8462 ~ 0.9231 | gold 的值都在 Agent 结果里(用户视角) |
| 严格执行准确率 | 0.2821 | 0.2308 ~ 0.3077 | 结果集完全一致(学术口径) |
| 有效 SQL 率 | 1.0000 | — | 生成的 SQL 能跑通 |
| 拒答准确率 | 1.0000 | — | 不可答的问题被拒答 |
| 平均步数 | 5.76 | 5.67 ~ 5.80 | |
| 平均工具调用 | 4.76 | 4.67 ~ 4.80 |
按问题类型(宽松):
| 类型 | 准确率 | 说明 |
|---|---|---|
| simple | 1.00 (4/4) | |
| aggregate | 1.00 (2/2) | |
| multi_step | 1.00 (2/2) | |
| unanswerable | 1.00 (2/2) | 拒答 |
| join | 0.67 (2/3) | 错在 q011(口算) |
| unit | 0.50 (1/2) | 错在 q006(单位) |
两个稳定的失败点:
❌ q006 [unit] 2023年营业收入超过1亿元的公司有几家?
期望 6 家 / 实际 2 家 (编造了"单位为万元"的解释)
❌ q011 [join] 中国石化和中国石油2023年的营业收入相差多少?
期望 2012.1 / 实际 两行原始值(它自己心算了)
注意噪声只有 ±4pp(约 1 道题) —— 这个精度足够用来判断"阶段 4 的工具改进有没有用"。
| 变体 | 宽松准确率 | 严格 | unit 类 | 平均步数 | 工具调用 |
|---|---|---|---|---|---|
| baseline | 0.8462 | 0.2308 | 0.50 | 6.00 | 5.07 |
| A_schema | 1.0000 | 0.1538 | 1.00 | 2.73 | 1.60 |
| B_sample | 0.8462 | 0.1538 | 0.50 | 6.47 | 5.53 |
| C_rich | 1.0000 | 0.2308 | 1.00 | 5.00 | 4.00 |
失败题:
baseline ❌ q006[unit] ❌ q009[join]
A_schema (无)
B_sample ❌ q006[unit] ❌ q010[join]
C_rich (无)
q006 是唯一在多次运行间稳定失败的题,其他题目来回翻 —— 说明在这个样本量下,能可靠区分的信号只有"单位问题"。
| 环节 | 用的东西 |
|---|---|
| 数据库 | SQLite(标准库,零依赖) |
| 决策模型 | DeepSeek API |
| Agent 框架 | 无 —— 手写 ReAct 循环 |
| HTTP | requests |
- 阶段 0:环境 + 建库(3 张表,含外键)
- 阶段 1:最笨的 Text-to-SQL(理解"为什么一次不够")
- 阶段 2:手写 ReAct 循环(Agent 的本质)
- 阶段 3:评估体系(15 题 + 双指标 + 多次运行取平均)
- 阶段 4:工具集设计(三种补充单位信息的方案对照实验,最佳变体 15/15)
- 阶段 5:规模化实验(292 张表)—— 证伪了"塞不下"假设,找到真正的瓶颈是"信息过量导致跳过查询"
- 阶段 6:接入 RAG(数值 + 原因一起答)
- 阶段 7:服务化 + 演示
src/
stage0_build_db.py 建库:3 张核心表 + 示例数据
stage1_naive.py 一次生成 SQL(基线,会失败)
stage2_agent.py 手写 ReAct 循环 + 工具集
stage3_eval.py 评估:双指标执行准确率 + 多次运行 + 变体对比
stage4_agent.py 四个工具变体(baseline / A_schema / B_sample / C_rich)
★ 含「未查库不许作答」的代码层护栏
stage5_build_bigdb.py 把库扩展到 292 张表(含 3 张陷阱表),制造规模压力
stage5_probe_fabrication.py 探针:统计「跳过查询、凭记忆编造」的比例
eval/
questions.py 评估集:15 题 + 标准 SQL
data/
demo.db 核心库,3 张表(.gitignore 忽略,可重建)
big.db 规模化实验库,292 张表(.gitignore 忽略,可重建)
outputs/
eval_results.json 评估结果(.gitignore 忽略)
.env API 密钥(.gitignore 忽略)
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python src/stage0_build_db.py # 建库
python src/stage1_naive.py # 基线:一次生成 SQL
python src/stage2_agent.py # Agent:ReAct 循环
python src/stage3_eval.py # 跑评估(约 5 分钟)
python src/stage3_eval.py --repeat 3 # 跑 3 次取平均(约 15 分钟)在根目录建 .env:
DEEPSEEK_API_KEY=sk-xxxxxxxx
⚠️ .env已在.gitignore中,密钥不会被提交。
| 决策 | 理由 |
|---|---|
| 手写 ReAct 而不用 LangGraph | 机制一共 100 行;自己写才知道框架替你做了什么 |
| 工具统一接收一个字符串参数 | 分发表统一用 TOOLS[name](arg) 调用,不用为每个工具写特例 |
| 数据库报错原样返回给模型 | 模型看到 no such column: xxx 才知道怎么改;吞掉错误它就失去纠错依据 |
| 只允许 SELECT,且禁用危险关键字 | 企业场景的硬要求。两层检查防 CTE 绕过 |
| 工具返回结果截断到 20 行 | 防止 Result 太长把上下文撑爆 |
max_steps = 8 |
循环必须有上限,否则会失控烧 token |
temperature = 0 |
SQL 生成要确定性 |
| 评估用标准 SQL 而不是标准答案字符串 | 自然语言答案难比对;SQL 的执行结果可以精确比对 |
| 同时报严格 / 宽松两个准确率 | 只报一个都会误导——严格低估"多带信息"的回答,宽松会放过"碰巧包含" |
| 跑 3 次取平均并报范围 | LLM 输出有非确定性;单次结果无法区分改进和噪声 |
trace 里的 arg 不截断 |
它会被评估重新执行;截断就改变了 SQL 的语义 |
| 评估脚本要有"完成度检查" | 否则"查到过但没答出来"会被宽松判定误判成成功 |
| 评估要比对 trace 里所有 SQL 结果,而非最后一次 | Agent 可能先算出答案、又查一次明细来"展示过程";只看最后一次会把答对判成答错 |
A 变体不提供 describe_table |
schema 已在系统提示里,工具再返回一份无注释版本会造成信息冲突——模型会挑一个信,而你控制不了它挑哪个 |
| 命令行选项用「取值即删除」解析 | 分成"读"和"删"两步写,一定会漏——本项目在这个坑上栽过三次 |
| 禁止在模块级做删除/写文件 | DB.unlink() 写在模块级 → 任何 import 都会删库。import 不该有副作用 |
| "必须查过库才能作答"做成代码护栏 | 提示词约束的遵守率只有 20%(大 schema 场景),代码检查是 100% |
最后一条是本项目最重要的设计原则: 能被代码强制的规则,就不要只写在提示词里。
- 核心库只有 3 张表 / 24 行,数据是手写示例; 规模化实验用的 292 张表里,除核心和 3 张陷阱表外都是合成的干扰表
- 真实的企业库比合成的更难:同名不同 schema、权限受限的表、
字段名是拼音缩写(
yysr)、同一指标在不同表里口径不同 —— 这些我还没模拟 - 评估集只有 15 题,多次运行间有 2-4 题会翻转, 所以只有"单位问题"和"跳过查询"这两个信号是稳定可辨的
- 单位注释是手写的 —— 换一个数据库就要重写。 真实生产环境需要数据字典 / 元数据平台来提供
- "拒答"靠关键词判断,本身就不可靠(更严格要引入 LLM 裁判,但又引入新不确定性)
- 没有处理歧义问题("去年"是哪一年?"营收"是合并口径还是母公司口径?)
- 没有多轮对话能力(每次都是独立问题)
- 严格准确率(0.15 ~ 0.23)远低于宽松(0.85 ~ 1.00)—— 说明 Agent 习惯多查列,这在学术口径下会被判错
- ⭐ 把"未验证不许作答"做成通用护栏 —— 本项目只在 Text-to-SQL 上验证了这一条,但它适用于任何 Agent: 没有证据就不能下结论。这是最有价值的下一步
- 评估集扩到 50+ 题,把噪声压到可接受范围
- 单位元信息从哪来 —— 数据字典?元数据平台?还是自动推断?
- 模拟更真实的库(拼音字段名、同名不同 schema、权限受限的表)
- 阶段 6:接入 RAG(数值 + 原因一起答)
- 阶段 7:服务化 + 演示
- 代码:MIT
- 数据:示例数据,用于跑通链路;后续接入公开财报数据