基于 Rust 与 DeepSeek 构建高性能 Text-to-SQL 数据库代理
介绍如何使用 Rust 语言结合大模型构建高性能 Text-to-SQL 数据库代理服务。通过实现 PostgreSQL Wire Protocol,代理层可拦截自然语言查询并转换为 SQL 执行。文章涵盖环境搭建、架构设计、核心代码实现及测试验证,展示了利用异步运行时 Tokio 处理高并发连接的能力,为数据中台建设提供参考方案。

介绍如何使用 Rust 语言结合大模型构建高性能 Text-to-SQL 数据库代理服务。通过实现 PostgreSQL Wire Protocol,代理层可拦截自然语言查询并转换为 SQL 执行。文章涵盖环境搭建、架构设计、核心代码实现及测试验证,展示了利用异步运行时 Tokio 处理高并发连接的能力,为数据中台建设提供参考方案。

在当前数据库交互范式演进的过程中,将自然语言(Natural Language, NL)直接转化为结构化查询语言(Structured Query Language, SQL)已成为提升数据可访问性的关键技术路径。本文将深度剖析如何利用系统级编程语言 Rust 的高性能特性,结合 PostgreSQL Wire Protocol(数据库传输协议)与大语言模型的推理能力,构建一个透明的数据库代理层。该代理服务能够拦截客户端请求,智能识别自然语言指令,并在毫秒级时间内将其转换为可执行的高效 SQL 语句,最终在真实的 PostgreSQL 数据库中执行并返回结果。
本项目不仅仅是一个简单的转换脚本,而是一个完整的网络服务中间件。其核心技术栈选择经过了严谨的考量:
构建高性能 Rust 应用的第一步是搭建稳固的开发环境。这涉及操作系统底层的构建工具以及 Rust 自身工具链的配置。
在 Linux 环境下,编译 Rust 程序(特别是涉及底层网络库或加密库依赖时)往往需要 C 语言编译器的支持。build-essential 软件包提供了 GCC 编译器、GNU Make 构建工具以及 glibc 开发库,curl 则用于后续脚本的下载。
在终端中执行依赖安装命令:
sudo apt update && sudo apt install curl build-essential
系统开始解析依赖树并下载所需的二进制包。这一步确保了操作系统具备编译链接 C 代码段的能力,因为许多 Rust 的 crate(库)底层通过 FFI(外部函数接口)绑定了 C 语言库。
Rust 官方提供了 rustup 作为版本管理和安装工具。该工具不仅安装编译器,还管理标准库文档、Cargo 包管理器以及不同目标平台的交叉编译工具链。
执行以下命令启动安装脚本:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
该脚本通过 HTTPS 安全协议拉取安装逻辑,默认安装 stable(稳定版)的 Rust 编译器。稳定版保证了向后的兼容性以及经过充分测试的特性支持。
安装过程中,脚本会自动检测宿主机的 CPU 架构(如 x86_64)和操作系统类型,并下载对应的预编译二进制文件。完成后,脚本会提示环境变量已配置,但需要刷新 Shell 上下文。
为了让当前终端会话识别 cargo 和 rustc 命令,必须加载 cargo 的环境变量配置文件。
. "$HOME/.cargo/env"
加载完成后,通过版本查询命令验证安装完整性:
rustc --version
cargo --version
rustc 是 Rust 的编译器核心,负责将源码编译为 LLVM IR 并最终生成机器码;cargo 则是 Rust 的构建系统和包管理器,负责依赖下载、编译编排及测试运行。
上图明确显示了当前安装的 Rust 版本信息,版本号的正确输出标志着编译环境已准备就绪。为了避免每次登录都需要手动加载环境变量,通常将加载指令写入 Shell 的启动脚本(如 .bashrc)。
echo '. "$HOME/.cargo/env"' >> ~/.bashrc
这一步操作将环境加载逻辑持久化,确保每次开启新的终端窗口时,Rust 工具链均处于可用状态。
本项目依赖两个外部服务:推理 API 和 PostgreSQL 后端数据库。
在模型提供商平台创建 API Key。此 Key 是服务进行身份验证的唯一凭证,代理服务将使用该凭证向 LLM 发起 HTTP 请求。
访问模型提供商控制台获取凭证。
创建凭证后,需选择合适的模型版本。本项目采用通用代码生成版本,该版本在代码生成任务上进行了针对性微调,能够更准确地理解 SQL 语法规范。
为了隔离开发环境,使用 Docker 容器化部署 PostgreSQL 是最佳实践。以下命令启动一个 PostgreSQL 15 版本的实例,并设置初始密码和数据库名称。
docker run -d --name postgres-db -e POSTGRES_PASSWORD=huyunkai -e POSTGRES_DB=test -p 5432:5432 postgres:15
这里将容器内部的 5432 端口映射到宿主机的 5432 端口,使得代理服务可以通过 localhost 进行连接。
在云服务器或防火墙配置中,必须确保相应的端口(如 5432 以及代理服务将要使用的端口)已对白名单 IP 开放,以允许外部连接。
上图展示了 Docker 容器成功启动后的状态,容器 ID 的生成表明后台进程正在运行。
Rust 项目通过 Cargo.toml 管理依赖,通过配置文件解耦代码与环境参数。
项目使用 config.toml 文件集中管理服务端口、API 密钥及数据库连接信息。TOML 格式因其语义清晰、易于阅读且支持强类型解析,非常适合作为配置文件格式。
# Text-to-SQL Proxy Configuration
[server]
host = "127.0.0.1"
port = 5433
[deepseek]
api_key = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
api_url = "https://api.example.com/v1/chat/completions"
model = "/maas/deepseek-ai/DeepSeek-V3.2"
[database]
# Backend database connection
type = "postgres"
host = "localhost"
port = 5432
username = "postgres"
password = "password"
database = "your_database"
max_connections = 10
配置文件定义了三个核心部分:
在 Cargo.toml 中定义项目的依赖树。每一个 crate 都扮演着不可或缺的角色:
[package]
name = "text-to-sql-proxy"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.35", features = ["full"] }
pgwire = "0.20"
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "postgres", "mysql"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
anymore = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
toml = "0.8"
async-trait = "0.1"
bytes = "1.5"
full 特性开启了包括 TCP、多线程调度器在内的所有功能。项目代码结构清晰,划分为配置加载、数据库交互、大模型集成、协议处理与主程序入口。
config.rs)该模块利用 serde 的派生宏(derive macros)将 TOML 文本自动映射为 Rust 结构体。
use serde::{Deserialize, Serialize};
use std::fs;
use anyhow::Result;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub server: ServerConfig,
pub deepseek: DeepSeekConfig,
pub database: DatabaseConfig,
}
// ... 结构体定义 ...
impl Config {
pub fn from_file(path: &str) -> Result<Self> {
let content = fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
}
anyhow::Result 用于简化错误处理,当文件读取失败或 TOML 格式错误时,能够向上传播具体的错误上下文。这种类型安全的配置读取方式避免了运行时因配置错误导致的 panic。
database.rs)此模块封装了与真实 PostgreSQL 数据库的交互逻辑。它不仅负责执行 SQL,还负责获取数据库的 Schema 信息,这是 LLM 能够准确生成 SQL 的关键。
use anyhow::Result;
use sqlx::{postgres::PgPoolOptions, PgPool, Row};
use tracing::info;
#[derive(Clone)]
pub struct DatabaseBackend {
pool: PgPool,
}
impl DatabaseBackend {
pub async fn new(
host: &str,
port: u16,
username: &str,
password: &str,
database: &str,
max_connections: u32,
) -> Result<Self> {
let connection_string = format!("postgres://{}:{}@{}:{}/{}", username, password, host, port, database);
info!("Connecting to backend database: {}:{}/{}", host, port, database);
let pool = PgPoolOptions::new().max_connections(max_connections).connect(&connection_string).await?;
Ok(Self { pool })
}
pub async fn execute_query(&self, sql: &str) -> Result<Vec<Vec<String>>> {
info!("Executing SQL: {}", sql);
let rows = sqlx::(sql).(&.pool).?;
= ::();
rows {
= ::();
..row.() {
: <> = row.(i).();
row_data.(value.(|| .()));
}
result.(row_data);
}
(result)
}
(&) <> {
= ;
= sqlx::(query).(&.pool).?;
= ::();
rows {
: = row.()?;
: = row.()?;
: = row.()?;
schema.(&(, table, column, data_type));
}
(schema)
}
}
get_schema 方法至关重要。它查询系统表 information_schema.columns,提取所有公有表的表名、列名和数据类型。这些信息将被拼接成一段描述性文本,作为 Prompt 的一部分发送给 LLM,让 AI 理解数据库的结构。
execute_query 方法则展示了代理的通用性设计:它不尝试解析具体的 SQL 结果类型,而是将所有结果统统转换为 String。这种弱类型处理是为了兼容任意的 SQL 查询结果,因为代理层本身无法预知用户会查询什么数据。
deepseek.rs)该模块负责构造 HTTP 请求,将自然语言和 Schema 上下文发送给 LLM,并解析返回的 SQL。
use anyhow::Result;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
#[derive(Debug, Clone)]
pub struct DeepSeekClient {
api_key: String,
api_url: String,
model: String,
client: Client,
}
#[derive(Debug, Serialize)]
struct ChatRequest {
model: String,
messages: Vec<Message>,
temperature: f32,
}
#[derive(Debug, Serialize, Deserialize)]
struct Message {
role: String,
content: String,
}
#[derive(Debug, Deserialize)]
struct ChatResponse {
choices: Vec<Choice>,
}
#[derive(Debug, Deserialize)]
struct Choice {
message: Message,
}
impl DeepSeekClient {
pub fn new(api_key: String, api_url: String, model: String) -> Self {
Self {
api_key,
api_url,
model,
client: Client::new(),
}
}
pub async fn text_to_sql(&self, natural_language: &str, schema_context: &str) <> {
info!(, natural_language);
= (
,
schema_context
);
= ChatRequest {
model: .model.(),
messages: [
Message {
role: .(),
content: system_prompt,
},
Message {
role: .(),
content: natural_language.(),
},
],
temperature: ,
};
= .client
.(&.api_url)
.(, (, .api_key))
.(&request)
.()
.?;
!response.().() {
= response.().?;
warn!(, error_text);
anyhow::bail!(, error_text);
}
: ChatResponse = response.().?;
= chat_response
.choices
.()
.(|c| c.message.content.().())
.(|| anyhow::anyhow!())?;
info!(, sql);
(sql)
}
}
这里使用了 Prompt Engineering 技巧:在 System Prompt 中明确注入数据库 Schema,并设定严格的规则(如'只返回 SQL,不返回解释'、'使用 PostgreSQL 语法')。通过 temperature: 0.3 参数降低模型的随机性,确保生成的 SQL 稳定可靠。
proxy.rs)这是整个系统的中枢神经。它实现了 pgwire 的 SimpleQueryHandler trait,从而接管了通过 PostgreSQL 协议传入的查询请求。
use anyhow::Result;
use async_trait::async_trait;
use pgwire::api::auth::noop::NoopStartupHandler;
use pgwire::api::query::{PlaceholderExtendedQueryHandler, SimpleQueryHandler};
use pgwire::api::results::{DataRowEncoder, FieldFormat, FieldInfo, QueryResponse, Response, Tag};
use pgwire::api::{ClientInfo, Type};
use pgwire::error::{ErrorInfo, PgWireError, PgWireResult};
use std::sync::Arc;
use tracing::{error, info};
use crate::database::DatabaseBackend;
use crate::deepseek::DeepSeekClient;
pub struct SqlProxyProcessor {
deepseek: Arc<DeepSeekClient>,
database: Arc<DatabaseBackend>,
schema_cache: Arc<tokio::sync::RwLock<String>>,
}
impl SqlProxyProcessor {
pub fn new(deepseek: DeepSeekClient, database: DatabaseBackend) -> Self {
Self {
deepseek: Arc::new(deepseek),
database: Arc::new(database),
schema_cache: Arc::new(tokio::sync::RwLock::new(String::new())),
}
}
async fn get_schema(&self) -> Result<String> {
let cache = self.schema_cache.read().await;
if !cache.is_empty() {
(cache.());
}
(cache);
= .database.().?;
= .schema_cache.().;
*cache = schema.();
(schema)
}
(&, query: &) PgWireResult<Response<>> {
info!(, query);
= !query.().().()
&& !query.().().()
&& !query.().().()
&& !query.().().()
&& !query.().().()
&& !query.().().();
= is_natural_language {
info!();
= .()..(|e| {
error!(, e);
PgWireError::(::(ErrorInfo::(.(), .(), (, e))))
})?;
.deepseek.(query, &schema)..(|e| {
error!(, e);
PgWireError::(::(ErrorInfo::(.(), .(), (, e))))
})?
} {
info!();
query.()
};
= .database.(&sql)..(|e| {
error!(, e);
PgWireError::(::(ErrorInfo::(.(), .(), (, e))))
})?;
.(results)
}
(&, results: <<>>) PgWireResult<Response<>> {
results.() {
(Response::(QueryResponse::(Arc::([]), Arc::([]))));
}
= results.().(|r| r.()).();
= ::();
..num_columns {
fields.(FieldInfo::((, i + ), , , Type::TEXT, FieldFormat::Text,));
}
= ::();
results {
= DataRowEncoder::(Arc::(fields.()));
row {
encoder.(&value)?;
}
data_rows.(encoder.());
}
(Response::(QueryResponse::(Arc::(fields), Arc::(data_rows))))
}
}
{
<, C>(&, _client: & C, query: & ) PgWireResult<<Response<>>>
C: ClientInfo + Unpin + + ,
{
= .(query).?;
([response])
}
}
这里使用了 Arc<RwLock<String>> 来缓存 Schema 信息。数据库结构通常不会频繁变动,缓存可以避免每次请求都查询元数据,从而显著降低延迟。
逻辑判断 is_natural_language 采用了简单的关键字排除法。如果输入不以标准的 SQL 动词(SELECT, INSERT 等)开头,系统即认为这是自然语言请求,触发 AI 转换流程。
main.rs)主函数负责组装各个组件,并启动 TCP 监听循环。
mod config;
mod database;
mod deepseek;
mod proxy;
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use std::sync::Arc;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc;
use std::time::Duration;
use tempfile::NamedTempFile;
use std::io::Write;
// Mock structs for testing
struct MockDeepSeekClient {
should_fail: bool,
}
impl MockDeepSeekClient {
fn new() -> Self { Self { should_fail: false } }
fn new_failing() -> Self { Self { should_fail: true } }
async fn text_to_sql(&self, _query: &str, _schema: &str) -> Result<String> {
if self.should_fail { Err(anyhow::anyhow!("Mock DeepSeek API error")) } else { Ok("SELECT * FROM test_table".to_string()) }
}
}
{
should_fail: ,
}
{
() { { should_fail: } }
() { { should_fail: } }
(_host: &, _port: , _username: &, _password: &, _database: &, _max_connections: ) <> { (::()) }
(&) <> {
.should_fail { (anyhow::anyhow!()) } { (.()) }
}
(&, _query: &) <<<>>> {
.should_fail { (anyhow::anyhow!()) } { ([[.(), .()], [.(), .()]]) }
}
}
{
should_fail: ,
}
{
() { { should_fail: } }
() { { should_fail: } }
}
() <NamedTempFile> {
= ;
= NamedTempFile::()?;
temp_file.(config_content.())?;
(temp_file)
}
() {
= ()..();
= temp_config.().().();
= Config::(config_path);
(config_result.());
= config_result.();
(config.server.host, );
(config.server.port, );
(config.deepseek.api_key, );
(config.database.host, );
(config.database.port, );
}
() {
= Config::();
(config_result.());
}
() {
= Arc::(MockSqlProxyProcessor::());
= TcpListener::()..();
= listener.().();
(tx, rx) = mpsc::();
tokio::( {
(socket, _) = listener.()..();
tx.(socket)..();
});
= TcpStream::(addr)..();
= rx.()..();
= (server_socket, processor).;
(result.());
}
() {
tracing_subscriber::().(tracing::Level::INFO).().(());
= ()..();
= temp_config.().().();
= Config::(config_path).();
= MockDeepSeekClient::();
= MockDatabaseBackend::();
= Arc::(MockSqlProxyProcessor::());
(config.server.port, );
(config.deepseek.api_key, );
(config.database.host, );
= Arc::(&processor);
(processor_count, );
}
() {
tracing_subscriber::().(tracing::Level::INFO).().(());
= ()..();
= temp_config.().().();
= Config::(config_path).();
= (, config.server.host);
= TcpListener::(&server_addr).;
(listener_result.());
= listener_result.();
= listener.().();
(local_addr.() > );
}
() {
= NamedTempFile::().();
temp_file.().();
= temp_file.().().();
= Config::(config_path);
(config_result.());
(e) = config_result {
(e.().());
}
}
}
anyhow::;
pgwire::api::auth::noop::NoopStartupHandler;
pgwire::api::query::PlaceholderExtendedQueryHandler;
pgwire::tokio::process_socket;
std::sync::Arc;
tokio::net::TcpListener;
tracing::{error, info};
tracing_subscriber;
config::Config;
database::DatabaseBackend;
deepseek::DeepSeekClient;
proxy::SqlProxyProcessor;
() <()> {
tracing_subscriber::().(tracing::Level::INFO).();
info!();
= Config::()?;
info!();
= DeepSeekClient::(
config.deepseek.api_key.(),
config.deepseek.api_url.(),
config.deepseek.model.(),
);
info!();
= DatabaseBackend::(
&config.database.host,
config.database.port,
&config.database.username,
&config.database.password,
&config.database.database,
config.database.max_connections,
).?;
info!();
= Arc::(SqlProxyProcessor::(deepseek, database));
info!();
= (, config.server.host, config.server.port);
= TcpListener::(&server_addr).?;
info!(, server_addr);
info!();
{
(socket, addr) = listener.().?;
info!(, addr);
= processor.();
tokio::( {
(e) = (socket, processor_clone). {
error!(, addr, e);
}
});
}
}
(
socket: tokio::net::TcpStream,
processor: Arc<SqlProxyProcessor>,
) <()> {
= Arc::(NoopStartupHandler);
= Arc::(PlaceholderExtendedQueryHandler);
(socket, , authenticator, processor, extended_query_handler).?;
(())
}
tokio::spawn 是实现高并发的关键。它将每个客户端连接的处理逻辑提交给 Tokio 执行器,这些任务会在线程池中被高效调度。即使某个连接正在等待 LLM 响应,执行器也可以切换去处理其他连接的 I/O 事件,绝不阻塞线程。
代码编写完成后,目录结构清晰。src 目录下包含了所有核心模块,职责分明。
使用 Cargo 进行编译。--release 标志告诉编译器进行最高级别的优化(O3),移除调试符号,并进行死代码消除,以获得最佳的运行时性能。
cd text-to-sql-proxy
cargo build --release
编译过程会下载所有依赖并进行编译,最后生成 target/release/text-to-sql-proxy 二进制文件。Rust 的编译时间通常较长,这是因为其在编译期进行了大量复杂的静态分析和优化。
运行编译好的程序:
cargo run --release
控制台输出显示服务已成功监听在配置端口,数据库后端连接成功,大模型客户端初始化完毕,系统进入待命状态。
为了验证系统的实际效果,我们预先在后端数据库中插入了一些测试数据。
开启一个新的终端,使用标准的 PostgreSQL 客户端工具 psql 连接到我们的代理服务(注意端口是 8080 或配置文件中设置的 5433,而非数据库原本的 5432)。
psql -h 127.0.0.1 -p 8080 -U postgres -d test
psql 成功连接,提示符变为 test=#。在 psql 看来,它连接的是一个标准的 PostgreSQL 服务器,完全感知不到代理层的存在。
现在,我们输入自然语言指令而非 SQL:
帮我查询所有用户
系统瞬间返回了结果!这里发生了一系列复杂的后台操作:
SELECT * FROM user_info;。psql。接着测试另一个表:
帮我查询下 order_info 中的内容;
同样成功返回了 order_info 表的数据。
为了验证数据的准确性,我们可以对比直接查询数据库的结果。
上图证明,通过代理查询得到的数据与数据库中的原始数据完全一致。
在代理服务的运行终端,我们可以看到详细的结构化日志流。
日志清晰记录了每个步骤:
Received query: 收到用户的自然语言输入。Detected natural language...: 识别意图。Generated SQL: 打印出 LLM 生成的 SQL 语句,方便调试和审计。Executing SQL: 执行阶段。在模型提供商的控制台中,可以看到刚才的请求调用记录。
每一次自然语言查询都对应一次 API 调用,token 的消耗量取决于 Schema 的大小和用户输入的长度。
本文展示了如何利用 Rust 强大的网络编程能力和大语言模型的生成式 AI 能力,构建一个高性能的 Text-to-SQL 数据库代理。该架构打破了传统数据库交互的壁垒,让非技术人员也能通过自然语言获取数据。
项目优势在于:
未来优化方向可以包括:
这一技术方案为企业内部的数据中台建设、BI 报表生成以及低代码开发平台提供了极具价值的参考实现。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online