Skip to content

About

从零手写的 Text-to-SQL Agent:自实现 ReAct 循环与工具调用,含失败归因

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

自然语言查数据库(Text-to-SQL Agent)

从零手写的 Text-to-SQL Agent。不依赖 LangChain / LangGraph, 自己实现 ReAct 循环、工具调用与 SQL 执行反馈。 重点是失败归因与评估驱动的优化。

python framework progress


30 秒看懂

项 结果
数据库 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 次评估脚本自身的)

十个核心发现(详见「实验与发现」)

  1. 干净的小 schema 上,一次生成 SQL 就能答对 4/5 —— Agent 的价值不在这种场景
  2. ⭐ 模型会编造解释来包装错误答案:同一个字段,它先说「亿元」后说「万元」
  3. 最危险的失败不是语法错,是语义错:有效 SQL 率 100%,但准确率不是 100%
  4. ⭐⭐ 评估指标本身会骗人:我的尺子出过四次错,每次都差点得出错误结论
  5. Agent 知道自己不知道,却没有工具去查证 —— 工具的信息密度决定它的上限
  6. Agent 会把该由数据库做的计算留给自己(口算)
  7. ⭐⭐ "让模型自己从数据推断单位"被实验证伪 —— 采样只能排除一个数量级,无法在"万元"和"亿元"之间判断
  8. 减少无效步骤有隐性收益 —— 省下的步数预算救了那些真正需要多步的题
  9. ⭐⭐ "表多了会迷失"也被证伪 —— 3 张表 → 292 张表,准确率 0.9231 完全不变,它还识破了 3 张陷阱表
  10. ⭐⭐⭐ 真正的瓶颈是"信息给太多" —— 完整 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 的价值: 可以把问题拆成几步,每步基于上一步的真实结果决定下一步。


为什么不用 LangChain / LangGraph

整套机制一共 100 行左右:一个 for 循环 + 一张工具分发表。 自己写一遍才知道框架到底替你做了什么。

具体收获:

  • "工具调用"的本质是:模型只负责"决定调哪个",真正执行的是你的代码。 模型自己什么都做不了,它只会输出文字。
  • messages 数组为什么会越来越长:模型的思考(assistant)和工具的反馈(user) 都被追加进对话历史 —— 这就是 Agent 的"记忆",也是它上下文会爆的原因。
  • 工具返回什么信息,直接决定模型能不能纠错(见发现 ⑤)。

实验与发现

① 干净的小 schema 上,"一次生成"就够了

做法:把 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% 我的尺子太严

⑤ Agent 知道自己不知道,却没有工具去查证

引入 ReAct 循环后,看它回答的结尾措辞:

"营业收入为 32122.2(单位与表中一致)"
"净利润为 1611.5(单位与表中一致,通常为亿元)"     ← 它在猜

它多次主动声明"不知道单位",还补了"通常为亿元"—— 它在猜,而且知道自己是在猜。

但它的工具给不了答案:

describe_table(financials) →
  revenue REAL        ← 只有字段名和类型,没有单位、没有口径、没有样例数据
  net_profit REAL

更关键的是:它从头到尾没有运行过 SELECT * FROM financials LIMIT 3—— 它从来没"看过"数据长什么样,只知道字段叫 revenue, 不知道这个数是 32122 还是 32122000000。

两个结论:

  1. 加循环修不了单位问题 —— SQL 执行成功、没有报错, 循环里没有任何信息告诉它单位错了。它需要的是更好的上下文,不是更多的尝试机会。
  2. 工具的信息密度,决定 Agent 的能力上限。

⑥ 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 是白纸黑字的证据;"它在脑子里减了一下"没法追溯

评估判定它 ❌ 是正确的 —— 它抓住的是"设计原则被违反",不是"格式不好看"。

⑦ 每次都要重新探索 schema,完全重复

平均 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)。

两句话总结:

  1. "给模型更多信息"不总是好事 —— 信息给多了,它会以为自己已经知道答案。
  2. 提示词是"建议",代码才是"约束"。

评估结果

评估集: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 的工具改进有没有用"。

工具变体对照(阶段 4,15 题 × 1 次)

变体 宽松准确率 严格 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
  • 数据:示例数据,用于跑通链路;后续接入公开财报数据

About

从零手写的 Text-to-SQL Agent:自实现 ReAct 循环与工具调用,含失败归因

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages