使用 RAG、LangChain、FastAPI 和 Streamlit 构建 Text-to-SQL 聊天机器人

在这个项目使用 RAG、LangChain、FastAPI 和 Streamlit 构建 Text-to-SQL 聊天机器人中,我构建了一个由 AI 驱动的聊天机器人,它将自然语言问题转换为 SQL 查询,并直接从真实的 SQLite 数据库中检索答案。借助 LangChain、Hugging Face 向量嵌入以及 Chroma 向量库,这个应用演示了如何通过检索增强生成(RAG)工作流把非结构化的用户输入连接到结构化数据------并配套 FastAPI 后端与 Streamlit 前端界面。

1 引言:为什么是 Text-to-SQL?

设想一下:

你正在会议上,经理突然问:

"我们能看看上个月加入的所有客户吗?"

你看了一眼 SQL 编辑器......意识到你得手写一个查询、检查表名,甚至可能还要调试一个缺失的 JOIN。与此同时,另外一个人只是对着 AI 聊天机器人提了同样的问题------并立刻拿到了整齐格式化的结果。

这就是 Text-to-SQL 的魔力------把自然语言转换成数据库查询。

1.1 问题所在

SQL(结构化查询语言)是数据分析与数据工程的基石。它强大、灵活且精准------但并不是对所有人都"友好"。

很多业务用户、分析师、甚至一些开发者都觉得 SQL 语法令人望而生畏,或者在快速获取洞察时效率不高。

在大多数团队里,这会形成一个鸿沟:

  • 数据是可用的,但不容易"被提问"。
  • 洞察是存在的,但被 SQL 专业技能所"锁住"。

1.2 解决方案:Text-to-SQL

Text-to-SQL 通过桥接这道鸿沟来解决问题。

它允许任何人------无论是否具备技术背景------提出像:

"上一季度我们卖得最好的 5 个产品是什么?"

这样的自然语言问题,并直接从你的数据库里得到答案,AI 会在幕后完成所有翻译工作。

在幕后,Text-to-SQL 系统通常做这四件事:

  1. 检索相关的模式与上下文(知道有哪些表存在)。
  2. 从自然语言问题生成一个有效的 SQL 查询。
  3. 安全地验证并在数据库上执行该 SQL。
  4. 以人类友好的格式返回结果。

2 理解 RAG 方法

到目前为止,我们已经看到 Text-to-SQL 如何让用户提出自然问题并拿到数据库结果------但 AI 实际上是如何知道你的表、列与关系的呢?

答案在一种叫做 RAG(Retrieval-Augmented Generation,检索增强生成) 的技术里。

2.1 什么是 RAG?

从本质上看,RAG 是一种将检索生成结合的混合式 AI 方法:

  1. 检索------系统提取相关信息(在本案例中是数据库模式、表名与关系)。
  2. 生成------LLM 使用检索到的上下文来生成准确且有据可依的输出(即 SQL 查询)。

你可以把 RAG 想象成给你的 LLM 一张"备忘单"------它在开始写 SQL 之前需要看到的精确模式信息。

2.1.1 为什么不直接用一个普通的 LLM?

如果你直接问一个大型语言模型(LLM)比如 GPT 或 Claude:

"展示上个月加入的所有客户"

它可能会生成类似这样的 SQL:

sql 复制代码
SELECT * FROM users WHERE signup_date >= '2025-09-01'

看起来没问题------直到你意识到你的实际数据库里并没有叫 users 的表。也许真实的表是 customer_datacrm_clients

问题在于:当缺乏上下文时,LLM 会产生幻觉(hallucinate)

它会猜测表名、遗漏 JOIN 关系,或使用错误的列名------因为默认情况下,它并不了解你的数据库模式。

2.1.2 RAG 如何解决这个问题

RAG 通过把模型扎根在"真实、已检索的知识"中来修复幻觉。

在一个 Text-to-SQL 的 RAG 循环里会发生什么:

  1. 检索模式上下文
    系统会从一个向量数据库或内存索引中搜索你的模式(表名、列描述、关系)。
  2. 生成 SQL
    LLM 把你的自然语言问题和已检索到的模式一起作为输入,生成一个有效的 SQL 查询。
  3. 执行并返回结果
    查询被验证、在真实数据库上运行,并把结果返回给用户。

这确保每个 SQL 查询都由具备模式意识的推理支撑,而不是模型的"猜测"。

2.1.3 RAG 循环的实际运行

一个简化的流程图如下:

复制代码
[用户问题]
      ↓
[检索模式信息]
      ↓
[生成 SQL(LLM)]
      ↓
[验证 + 执行]
      ↓
[返回结果]
      ↺

每个循环都确保模型在生成查询之前拥有最新准确的模式知识------减少幻觉并提升可靠性。

2.2 系统架构

理解了 Text-to-SQL 为什么依赖 RAG(检索增强生成) 方法后,让我们看看它的底层结构。

为了更具体,设想你正在构建一个聊天机器人,它可以回答关于公司客户数据库的自然语言问题------从"上个月谁加入了?"到"本季度我们的平均订单金额是多少?"。

下面是系统的整体架构 👇

1. SQLite 数据库------结构化数据源

每一个 Text-to-SQL 系统都从数据源开始。

为了简化,我们使用 SQLite------一种轻量级、文件型数据库,非常适合原型开发与测试。

数据就真实地存放在这里:比如 customersordersproducts 等表。

当用户提问时,我们的目标是将这个问题转换成一个有效的 SQL 查询并在这个数据库上执行。

2. 嵌入层------把模式转换为向量

在模型生成 SQL 之前,它需要"理解"数据库结构------表名、列名以及它们的含义。

我们使用**向量嵌入(embeddings)**来实现这一点:它是对文本的数值表示,可以捕捉语义。

借助 Hugging Face Embeddings (比如 all-MiniLM-L6-v2),我们把模式元数据转换为向量:

python 复制代码
# 函数:为每一行生成唯一哈希(用于增量与重复检测)
def row_hash(values):
    """为一行生成唯一哈希值。"""
    import hashlib
    return hashlib.sha256("|".join(map(str, values)).encode()).hexdigest()

# 函数:将 SQLite 的一行转换为可读的文本块(便于嵌入)
def row_to_text(table, cols, row):
    """把 SQLite 行记录转为可读的文本块。"""
    return f"Table: {table}\n" + "\n".join([f"{c}: {v}" for c, v in zip(cols, row)])

# 函数:将单个表写入向量库
def index_table(conn, table):
    """把一个表索引进向量库(Chroma)。"""
    import sqlite3
    from langchain_community.vectorstores import Chroma
    from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings

    cur = conn.cursor()
    cur.execute(f"PRAGMA table_info({table});")
    cols = [c[1] for c in cur.fetchall()]
    cur.execute(f"SELECT {', '.join(cols)} FROM {table}")
    rows = cur.fetchall()

    embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
    vectorstore = Chroma(collection_name="sqlite_docs", persist_directory="./chroma_persist", embedding_function=embeddings)

    docs, ids, metas = [], [], []
    for r in rows:
        txt = row_to_text(table, cols, r)
        pk = str(r[0])
        hid = row_hash(r)
        ids.append(f"{table}:{pk}")
        docs.append(txt)
        metas.append({"table": table, "pk": pk, "hash": hid})

    vectorstore.add_texts(texts=docs, metadatas=metas, ids=ids)

# 函数:主索引流程
def main():
    """主索引流程:遍历所有业务表并写入向量库。"""
    import os, sqlite3
    from tqdm import tqdm

    SQLITE_PATH = os.getenv("SQLITE_PATH", "sample_db/sample.db")
    conn = sqlite3.connect(SQLITE_PATH)
    cur = conn.cursor()
    cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
    tables = [t[0] for t in cur.fetchall()]

    for t in tqdm(tables, desc="Indexing tables"):
        index_table(conn, t)

    conn.close()
    print("索引完成,并持久化到 Chroma。")

if __name__ == "__main__":
    main()

现在,每个表与列都被表示为高维空间中的向量------后续可以实现智能检索。

3. 向量库(Chroma)------检索相关模式

接下来,我们把这些嵌入存入向量数据库 ------本例使用 Chroma

当用户提出问题时,我们会:

  1. 在同一向量空间中嵌入这个问题。
  2. 在 Chroma 中搜索最接近的模式元素。

例如,用户问"最近注册的用户有哪些?"时,检索器可能会拉取到像 customers.signup_datecustomers.name 这样的模式项。

这确保模型只看到相关的模式上下文------让 SQL 生成扎根于现实。

4. LangChain 组件------系统的"大脑"

LangChain 把一切串联起来。

它提供了检索、推理与 SQL 生成的模块化组件:

  • 检索器:从 Chroma 拉取相关模式块。
  • :定义步骤序列------检索 → 生成 → 验证 → 执行。
  • LLM:在用户问题与检索上下文的条件下生成 SQL。

示例片段:

python 复制代码
# 函数:异步检索节点(RAG 的检索阶段)
async def retriever_node(state):
    """根据用户问题从向量库检索相关模式与数据片段。"""
    docs = await state["retriever"].ainvoke(state["question"])
    state["retrieved_docs"] = [d.page_content for d in docs]
    return state

# 函数:SQL 生成节点(清洗输出,仅保留 SELECT)
async def sql_generator_node(state):
    """从检索到的上下文与用户问题生成只读的 SQLite SELECT 语句。"""
    import re
    sql_prompt = state["sql_prompt"]
    llm = state["llm"]

    context = "\n\n".join(state.get("retrieved_docs", []))
    prompt_text = sql_prompt.format(context=context, question=state["question"])
    out = await llm.ainvoke(prompt_text)

    if hasattr(out, "content"):
        out = out.content
    out = str(out).strip()

    # 移除 Markdown 代码围栏与多余内容
    out = re.sub(r"```(?:sql)?\n?", "", out, flags=re.IGNORECASE).replace("```", "").strip()

    # 仅提取 SELECT 开头的语句
    match = re.search(r"(select\b.*)", out, flags=re.IGNORECASE | re.DOTALL)
    if match:
        out = match.group(1).strip()
    else:
        out = ""

    # 去掉末尾分号
    out = out.rstrip(";").strip()
    state["generated_sql"] = out
    return state

LangChain 作为协调器,确保每个问题都顺畅地走完整个 RAG 循环。

5. FastAPI 后端------服务化管线

为了让系统具有交互性,我们将所有内容封装进 FastAPI 后端。

FastAPI 提供一种简单、高性能的方式把 RAG 管线暴露为 API。

当用户发起一个携带问题的 POST 请求时,API 会:

  1. 检索模式信息。
  2. 生成并验证 SQL。
  3. 执行查询。
  4. 返回格式化的结果。

这一层相当于你的 Text-to-SQL 聊天机器人的"机房"。

python 复制代码
# 函数:API 入参模型(Pydantic)
class QueryRequest(BaseModel):
    """查询请求:包含自然语言问题与是否返回 SQL 的标记。"""
    question: str
    show_sql: bool = True

# 函数:主查询端点(RAG→SQL→验证→执行→返回)
@app.post("/query")
async def query(req: QueryRequest):
    """接收问题,运行完整 RAG 管线,返回 SQL 与结果。"""
    state = {"question": req.question, "messages": []}

    # 检索
    state = await retriever_node(state)

    # 生成 SQL
    state = await sql_generator_node(state)
    sql = state["generated_sql"]

    # 验证:只允许 SELECT,且只允许白名单表
    ok, reason = validate_sql(sql, ALLOWED_TABLES)
    if not ok:
        raise HTTPException(status_code=400, detail=f"SQL 验证失败: {reason}. SQL: {sql}")

    # 执行:只读连接 + LIMIT 保护
    cols, rows = execute_sql(sql)
    result = [dict(zip(cols, r)) for r in rows]
    return {"sql": sql if req.show_sql else None, "cols": cols, "rows": result}

6. Streamlit 前端------聊天界面

最后,我们构建一个 Streamlit 前端------一个轻量的 Web 界面供用户交互。

用户可以输入问题、(可选)查看生成的 SQL,并实时查看结果。

Streamlit 简洁的特性使其非常适合原型化 AI 工具:几行 Python,你就能搭一个和数据"对话"的交互面板。

7.汇总串联

端到端架构图:

复制代码
用户(Streamlit UI)
        ↓
FastAPI 后端
        ↓
LangChain RAG 管线
   ├── 检索器(Chroma 向量库)
   ├── 嵌入层(Hugging Face)
   ├── LLM(SQL 生成)
        ↓
SQLite 数据库(执行 SQL)
        ↓
结果 → 返回到前端

这条流程覆盖了一个查询的完整生命周期------从纯英文到执行 SQL,再到可读结果。

GitHub 上可以查看完整代码并开始实验:

GitHub 仓库链接

3 分步实现

3.1 环境准备

  • 安装依赖:
bash 复制代码
pip install -r requirements.txt

3.2 SQLite 数据库搭建

  • 说明示例模式(如 customersorders)。
python 复制代码
# 函数:创建示例数据库与表并插入数据
def create_sample_db():
    """创建 SQLite 示例库与两张业务表,并插入样例数据。"""
    import os, sqlite3
    os.makedirs("sample_db", exist_ok=True)
    DB = "sample_db/sample.db"
    conn = sqlite3.connect(DB)
    cur = conn.cursor()

    cur.execute("""
    CREATE TABLE IF NOT EXISTS customers (
      id INTEGER PRIMARY KEY,
      name TEXT,
      email TEXT,
      created_at TEXT
    );
    """)

    cur.execute("""
    CREATE TABLE IF NOT EXISTS orders (
      id INTEGER PRIMARY KEY,
      customer_id INTEGER,
      total_amount REAL,
      status TEXT,
      created_at TEXT,
      notes TEXT,
      FOREIGN KEY(customer_id) REFERENCES customers(id)
    );
    """)

    customers = [
        (1, "Alice Johnson", "alice@example.com", "2024-12-01"),
        (2, "Bob Lee", "bob@example.com", "2024-12-05"),
        (3, "Carol Singh", "carol@example.com", "2024-12-10"),
        (4, "David Kim", "david.kim@example.com", "2024-12-12"),
        # ...
    ]

    orders = [
        (1, 1, 120.50, "completed", "2025-01-03", "First order"),
        (2, 1, 15.00,  "pending",   "2025-01-07", "Gift wrap"),
        (3, 2, 250.00, "completed", "2025-02-10", "Bulk order"),
        # ...
    ]

    cur.executemany("INSERT OR REPLACE INTO customers VALUES (?,?,?,?)", customers)
    cur.executemany("INSERT OR REPLACE INTO orders VALUES (?,?,?,?,?,?)", orders)
    conn.commit()
    conn.close()

运行下面命令创建并插入数据:

bash 复制代码
python sample_db/create_sample_db.py

3.2.1 嵌入与模式索引

在 AI 能生成准确 SQL 之前,它需要"理解"我们数据库的结构------有哪些表、包含哪些列、承载什么样的数据。

这正是向量嵌入语义索引发挥作用的地方。

3.2.2 什么是嵌入?

嵌入是文本的数值化表示------一个位于高维空间的向量,能够捕捉语义。

例如,短语:

"customer name"

"client full name"

在这个空间里的向量距离会很接近,因为它们大致表达的是同一个意思

通过为每个表名列名 甚至样例数据 生成嵌入,模型在用户提问时就可以检索 相关上下文------比如"展示最近注册的用户",会语义匹配到 signup_datecreated_atjoin_date 等列。

3.2.3 我们将使用的工具

  • Hugging Face Embeddings------把文本转为向量。
  • Chroma 向量库------高效存储与检索嵌入。
  • SQLite------实际要索引的数据库。

下面是索引脚本 👇

python 复制代码
# 函数:主索引流程(简版)
def run_indexing():
    """遍历 SQLite 表,将行级文本块写入 Chroma 并持久化。"""
    import os, sqlite3, hashlib
    from tqdm import tqdm
    from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
    from langchain_community.vectorstores import Chroma

    def row_hash(values):
        """为一行生成唯一哈希值。"""
        return hashlib.sha256("|".join(map(str, values)).encode()).hexdigest()

    def row_to_text(table, cols, row):
        """把 SQLite 行记录转为可读文本块。"""
        return f"Table: {table}\n" + "\n".join([f"{c}: {v}" for c, v in zip(cols, row)])

    SQLITE_PATH = os.getenv("SQLITE_PATH", "sample_db/sample.db")
    CHROMA_DIR = os.getenv("CHROMA_DIR", "./chroma_persist")
    EMBED_MODEL = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")

    embeddings = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
    vectorstore = Chroma(collection_name="sqlite_docs", persist_directory=CHROMA_DIR, embedding_function=embeddings)

    conn = sqlite3.connect(SQLITE_PATH)
    cur = conn.cursor()
    cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
    tables = [t[0] for t in cur.fetchall()]

    for t in tqdm(tables, desc="Indexing tables"):
        cur.execute(f"PRAGMA table_info({t});")
        cols = [c[1] for c in cur.fetchall()]
        cur.execute(f"SELECT {', '.join(cols)} FROM {t}")
        rows = cur.fetchall()

        docs, ids, metas = [], [], []
        for r in rows:
            txt = row_to_text(t, cols, r)
            pk = str(r[0])
            hid = row_hash(r)
            ids.append(f"{t}:{pk}")
            docs.append(txt)
            metas.append({"table": t, "pk": pk, "hash": hid})

        vectorstore.add_texts(texts=docs, metadatas=metas, ids=ids)

    conn.close()
    print("索引完成,并已持久化到 Chroma。")

生成嵌入并保存到向量库:

bash 复制代码
python ingestion/index_sqlite.py

3.2.4 步骤详解

  1. 提取模式与数据
    对每张表,抓取列名与若干行。每一行都变成一个小"文档",描述该表包含什么。

  2. 把文本转为向量
    使用 HuggingFaceEmbeddings 将每个文本块(如 "Table: customers\nname: John\nsignup_date: 2025-09-10")转为数值向量。

  3. 向量存储到 Chroma
    这些嵌入被存入 Chroma 向量库,并与元信息关联------比如表名、主键与哈希 ID。

  4. 启用语义检索
    当用户提出问题时,在同一向量空间里检索匹配的模式块,为 SQL 生成提供精准上下文。

    SQLite 数据库

    抽取表 + 列

    Hugging Face 向量嵌入

    存入 Chroma 向量库

    查询时语义检索

4 RAG 查询流程

我们已经构建了模式索引。现在,是把它用起来的时候了。

RAG 查询流程是魔力发生的地方------系统把用户问题变成可执行的 SQLite 查询,并把结果返回。

4.1 步骤 1:检索相关模式

当用户提问------比如:

"展示上个月加入的所有客户"

系统把这个问题嵌入到与数据库模式相同的向量空间(多亏我们之前生成的嵌入)。

使用 Chroma 检索器 ,它会拉取最相关的模式片段(如与 customerssignup_date 相关的表和列)。

python 复制代码
# 函数:RAG 检索节点(返回文本内容列表)
async def retriever_node(state):
    """根据问题在 Chroma 中检索 TOP-K 相关文档。"""
    docs = await state["retriever"].ainvoke(state["question"])
    state["retrieved_docs"] = [d.page_content for d in docs]
    return state

现在,模型具备了真实的模式认知------一张数据库实际存在内容的"备忘单"。

4.2 步骤 2:通过 LLM 生成 SQL

我们把问题已检索的模式上下文一起传给 LLM(例如 OpenAI、Mistral 或 Gemini)。

我们使用一个专门的提示词,让输出 是一个有效、只读的 SELECT 语句。

python 复制代码
# 函数:构造 SQL 生成提示词
def build_sql_prompt():
    """创建只读 SELECT 的提示词模板。"""
    from langchain_core.prompts import PromptTemplate
    return PromptTemplate.from_template("""
    你是一个 SQL 生成器。基于下面的上下文,生成一个单条只读的 SQLite SELECT 查询(不加分号,不多语句)。
    
    上下文:
    {context}
    
    问题:
    {question}
    
    仅返回 SQL 的 SELECT 语句。
    """)

# 函数:清洗 LLM 输出并仅保留 SELECT 语句
def clean_llm_sql_output(text):
    """移除 Markdown 围栏与无关字符,保留且截断到 SELECT 语句。"""
    import re
    out = str(text or "").strip()
    out = re.sub(r"```(?:sql)?\n?", "", out, flags=re.I).replace("```", "").strip()
    m = re.search(r"(select\b.*)", out, flags=re.I | re.DOTALL)
    out = m.group(1).rstrip(";").strip() if m else ""
    return out

至此,聊天机器人可以把英文输入 变成SQL 输出

4.3 步骤 3:验证 SQL(安全优先)

在执行之前,我们要验证查询。这一步用来保护数据库并确保正确性。

我们用 sqlglot 解析器来:

  • 拒绝任何非 SELECT 语句(不允许 DELETEUPDATE 等)。
  • 检查只引用允许的白名单表。
  • 安全解析语法。
python 复制代码
# 函数:验证 SQL 只读与表白名单
def validate_sql(sql, allowed_tables):
    """验证 SQL:不含分号、只允许 SELECT、只使用白名单表。"""
    import sqlglot
    ALLOWED_STATEMENTS = {"select"}
    DISALLOWED = {"delete", "update", "insert", "drop", "alter"}

    if ";" in sql:
        return False, "不允许分号或多语句"
    for kw in DISALLOWED:
        if f" {kw} " in f" {sql.lower()} ":
            return False, f"不允许的关键字: {kw}"

    try:
        parsed = sqlglot.parse_one(sql, read="sqlite")
    except Exception as e:
        return False, f"SQL 解析错误: {e}"

    if parsed.key.lower() not in ALLOWED_STATEMENTS:
        return False, "只允许 SELECT 语句"

    # 简单提取表名并校验白名单
    def extract_tables(s):
        import re
        return set(re.findall(r"\bfrom\s+([a-zA-Z0-9_]+)", s, flags=re.I))
    tables = extract_tables(sql)
    if not tables.issubset(set(allowed_tables)):
        return False, f"使用了不在白名单的表: {tables - set(allowed_tables)}"
    return True, "ok"

这一层让系统安全且只读

4.4 步骤 4:在 SQLite 上执行

一旦验证通过,SQL 就会在 SQLite 数据库上执行。

我们增加保护措施以限制返回行数,避免重负载查询。

python 复制代码
# 函数:只读执行 SQL,并限制行数
def execute_sql(sql, row_limit=1000, timeout=5.0):
    """只读执行 SQL,限制最大返回行数并设置忙等待。"""
    import sqlite3

    # 强制 LIMIT 保护
    def enforce_limit(q, limit):
        q_low = q.lower()
        if " limit " in q_low:
            return q
        return f"{q} LIMIT {int(limit)}"

    def open_ro_conn():
        conn = sqlite3.connect("sample_db/sample.db")
        conn.execute("PRAGMA query_only = 1;")  # 只读保护
        return conn

    sql = enforce_limit(sql, row_limit)
    conn = open_ro_conn()
    conn.execute(f"PRAGMA busy_timeout = {int(timeout*1000)};")
    cur = conn.cursor()
    cur.execute(sql)
    cols = [c[0] for c in cur.description] if cur.description else []
    rows = cur.fetchmany(row_limit)
    conn.close()
    return cols, rows

结果随后会被打包成 JSON 并返回给用户。

5 FastAPI 后端

构建并测试完 RAG 管线后,下一步就是把它暴露为 API,让用户与前端客户端可以实时发送问题、拿到 SQL 结果并互动。

这正是 FastAPI 的用武之地------它快速、类型安全、原生支持异步,非常适合为 AI 工作流提供服务。

5.1 为什么选择 FastAPI?

FastAPI 提供:

  • 速度------异步 I/O 与自动文档(Swagger / ReDoc)
  • 集成------易于与 LangChain、Chroma 或本地模型连接
  • 验证------Pydantic 确保入参干净
  • 扩展性------可部署到任意环境:Docker、Serverless 或本地

简言之:它是你的 RAG Text-to-SQL 逻辑的理想封装。

5.2 API 设计概览

我们暴露一个端点:

复制代码
POST /query

请求体:

json 复制代码
{
  "question": "Show me all customers who joined last month",
  "show_sql": true
}

响应:

json 复制代码
{
  "sql": "SELECT * FROM customers WHERE join_date >= '2025-09-01'",
  "cols": ["id", "name", "join_date"],
  "rows": [
    {"id": 1, "name": "Alice", "join_date": "2025-09-05"},
    {"id": 2, "name": "Bob", "join_date": "2025-09-12"}
  ]
}

5.3 完整示例代码

python 复制代码
# 函数:FastAPI 应用定义与主查询端点
def create_app():
    """创建 FastAPI 应用并注册 /query 端点。"""
    import os
    from fastapi import FastAPI, HTTPException
    from pydantic import BaseModel
    from dotenv import load_dotenv

    from server.langgraph_nodes import retriever_node, sql_generator_node
    from server.sql_validator import validate_sql
    from server.executor import execute_sql
    from server.utils import allowed_tables_from_db

    load_dotenv()
    SQLITE_PATH = os.getenv("SQLITE_PATH", "sample_db/sample.db")
    ALLOWED_TABLES = allowed_tables_from_db(SQLITE_PATH)

    app = FastAPI(title="RAG Text->SQL API")

    class QueryRequest(BaseModel):
        """查询请求模型。"""
        question: str
        show_sql: bool = True

    @app.post("/query")
    async def query(req: QueryRequest):
        """运行完整 RAG→SQL→验证→执行流程并返回结果。"""
        state = {"question": req.question, "messages": []}
        state = await retriever_node(state)
        state = await sql_generator_node(state)
        sql = state["generated_sql"]

        ok, reason = validate_sql(sql, ALLOWED_TABLES)
        if not ok:
            raise HTTPException(status_code=400, detail=f"SQL 验证失败: {reason}. SQL: {sql}")

        cols, rows = execute_sql(sql)
        result = [dict(zip(cols, r)) for r in rows]
        return {"sql": sql if req.show_sql else None, "cols": cols, "rows": result}

    return app

5.4 流程回顾

  1. 接收问题
  2. 检索模式上下文(Chroma)
  3. 生成 SQL(LLM)
  4. 安全校验(只读 + 白名单)
  5. 执行与返回(SQLite)

设计之所以有效:

  • 模块化:检索、生成、验证、执行各函数可替换或扩展。
  • 安全 :仅允许 SELECT,并且表受白名单约束。
  • 异步:整条管线支持并发,易随用户负载扩展。
  • 可部署:Docker 打包后可部署到多种环境。

6 Streamlit 前端

后端已经跑起来了,现在让它可交互

目标:做一个简洁的 Web 界面,任何人都能输入自然语言问题,查看生成的 SQL,并在数秒内看到实时查询结果。

6.1 完整代码

python 复制代码
# 函数:Streamlit 应用(调用 FastAPI 并呈现结果)
def run_streamlit_app():
    """启动 Streamlit 前端,提交问题到后端并展示 SQL 与结果。"""
    import streamlit as st
    import requests

    API_URL = "http://localhost:8000/query"

    st.set_page_config(page_title="RAG Text→SQL Demo", layout="centered")
    st.title("RAG Text → SQL(SQLite 副本)")

    with st.form("query_form"):
        question = st.text_input(
            "对数据库提出一个自然语言问题",
            value="Show total orders per customer"
        )
        show_sql = st.checkbox("显示生成的 SQL", value=True)
        submitted = st.form_submit_button("提交")

    if submitted:
        question = str(question).strip()
        show_sql = bool(show_sql)

        if not question:
            st.warning("请输入一个问题。")
        else:
            payload = {"question": question, "show_sql": show_sql}
            with st.spinner("查询中..."):
                try:
                    resp = requests.post(API_URL, json=payload, timeout=60)
                    resp.raise_for_status()
                    data = resp.json()

                    if show_sql:
                        st.subheader("🧠 生成的 SQL")
                        st.code(data.get("sql", ""), language="sql")

                    st.subheader("📊 查询结果")
                    rows = data.get("rows", [])
                    if rows:
                        st.dataframe(rows)
                    else:
                        st.info("该查询未返回任何行。")

                except requests.exceptions.HTTPError as http_err:
                    st.error(f"HTTP 错误: {http_err} - {resp.text}")
                except requests.exceptions.ConnectionError:
                    st.error("无法连接 API。请确认 FastAPI 在 localhost:8000 运行中。")
                except requests.exceptions.Timeout:
                    st.error("请求超时,请稍后再试。")
                except Exception as e:
                    st.error(f"意外错误: {e}")

6.2 运行方式

bash 复制代码
uvicorn main:app --reload
streamlit run app.py

7 演示

问题:Show total orders per customer(按客户统计订单总数)

问题:Total revenue from completed orders(已完成订单的总收入)

8 下一步:增量嵌入与可扩展更新

到现在,你已经构建了一个完整的RAG 驱动的 Text-to-SQL 管线------从模式嵌入与检索,到 SQL 生成、验证与执行。

但在真实场景中,数据库是不断演变的。表会改变、新记录会插入、列定义会随时间变化。

我们如何让嵌入索引------也就是 RAG 系统------保持最新?

下面是让你的项目更"生产就绪"的几个方向。

1. 处理动态数据库

在原型里,我们在启动时索引了一次。

这对静态数据集很好,但生产系统需要在以下情况进行增量嵌入更新

  • 表结构变化(新增或删除列)。
  • 新行的插入使上下文显著变化。
  • 模式文档或元数据演进。

与其全量重建索引,不如仅嵌入已变化的部分

增量索引策略

  • 跟踪更新:通过时间戳或行级哈希。
  • 与 Chroma 元信息(如存储的哈希 ID)对比
  • 仅重嵌入修改过的行或新增的表。
  • 使用 Chroma 的增量 add_texts() API 持久化嵌入

这种方法能最小化成本、时间与冗余------对百万级数据尤为重要。

2. 通过后台作业实现自动化

重索引或更新嵌入不应阻塞用户查询。

你可以把这些操作交给后台任务

  • Celery(配 Redis/RabbitMQ)------适合分布式任务管理。
  • FastAPI BackgroundTasks------轻量的异步更新。

使用 FastAPI 后台任务的示例:

python 复制代码
# 函数:调度后台嵌入更新
def schedule_update_embeddings(app):
    """注册一个触发嵌入更新的端点,采用后台任务执行。"""
    from fastapi import BackgroundTasks

    @app.post("/update_embeddings")
    async def update_embeddings(background_tasks: BackgroundTasks):
        """触发增量重索引,不阻塞查询主流程。"""
        def reindex_changed_tables():
            # 在此对比哈希/时间戳并调用 add_texts() 等增量更新逻辑
            pass

        background_tasks.add_task(reindex_changed_tables)
        return {"status": "update scheduled"}

这让你的主查询端点(/query)保持响应,而索引更新在后台进行。

3. 未来增强

下面是几个值得探索的方向:

  • 对公司查询日志进行微调SQL 生成器。
  • 添加缓存用于高频问题。
  • 在 Streamlit 中集成可视化图表,动态展示查询结果。
  • 切换到更大的向量库(如 Pinecone 或 Qdrant)以支持海量数据集。
  • 加入反馈回路------让用户纠正 SQL 错误并用于模型再训练。

每一项改进都会让你的系统更接近一个自学习的数据助理,它能理解你不断演变的模式与业务逻辑。

9 关键收获

你刚刚构建了一个强大的东西------完整的基于 RAG 的 Text-to-SQL 系统,能把纯英文变成可执行的数据库洞察。让我们回顾一下你完成了什么:

1. 掌握了 Text-to-SQL 的 RAG

你学会了 RAG(检索增强生成) 如何在自然语言结构化数据库模式之间架起桥梁,大幅提升准确性并减少 SQL 生成中的幻觉。

  • ✅ 嵌入了数据库模式与样例行以提供上下文。
  • ✅ 在生成查询之前检索最相关的片段。
  • ✅ 将检索与 LLM 推理结合,产出可靠的 SQL。

2. 构建了模块化、可扩展的架构

你用 LangChainFastAPI 像生产级 AI 服务一样构建系统:

  • SQLite 作为结构化数据源。
  • Hugging Face 嵌入 + Chroma 做语义检索。
  • LangChain 编排检索器、提示词与模型。
  • FastAPI 作为轻量、异步的后端。
  • Streamlit 提供干净、友好的前端界面。

每一层都是模块化的------这意味着你可以在不破坏整体管线的情况下替换组件(比如模型或数据库)。

3. 部署了交互式聊天界面

最后,你用 Streamlit 前端把一切包起来,让用户可以:

  • 输入自然语言问题;
  • 查看生成的 SQL;
  • 即刻在动态表格中查看结果。

你把原来只属于开发者的工作(写 SQL)变成了和数据的自然对话

10 最后思考

这个项目展现了我们与数据交互方式中的强大转变:AI 正在民主化数据库访问 ,打破壁垒,让任何人都可以在不懂 SQL 的情况下提出复杂问题。

通过把检索增强生成与现代嵌入技术和对话界面结合起来,我们正在创造把数据直接交到人们手里的工具------让洞察更快速、更简单、更可触达。

欢迎分享你的反馈、想法,以及你构建自己的 Text-to-SQL 系统的经验。

查看 GitHub 上的完整代码并开始你的实验:

GitHub 仓库链接

相关推荐
louiX1 天前
初级 AI Agent 工程师
langchain·agent·客户端
幸福巡礼1 天前
【LangChain 1.2 实战(六)】 工具调用 (Function Calling)
langchain
Shan12051 天前
站在计算机领域视角看:SQL注入攻击
网络·数据库·sql
轻刀快马1 天前
别干背八股文了:从一场“双十一秒杀”惨案,看懂 InnoDB 事务、锁与索引的底层齿轮
数据库·sql
Irissgwe1 天前
LangChain之核心组件(少样本提示词)
人工智能·langchain·llm·langgraph
lbb 小魔仙1 天前
Python + LangChain 环境搭建完全指南:从零构建本地 RAG 知识库(附 Ollama 本地模型集成)
开发语言·python·langchain
风落无尘1 天前
我用 LangChain 写了一个带“定速巡航”的向量化工具,发布到 PyPI 了!
人工智能·python·langchain
BU摆烂会噶1 天前
【LangGraph】 流式处理入门
人工智能·python·langchain·人机交互
大模型真好玩1 天前
LangChain DeepAgents 速通指南(八)—— DeepAgents流式输出详解
人工智能·langchain·agent
沪漂阿龙1 天前
AI Agent爆火,但你真的懂LangChain吗?——大模型智能体开发完全指南
人工智能·langchain