最近在给一个数据密集型项目做 AI 能力接入时,遇到了一个很现实的问题:大模型的上下文窗口再大,也塞不下一个生产环境的 PostgreSQL 数据库结构。表有几十张,字段几百个,加上索引、约束、视图、枚举类型,一次性全丢给模型,要么直接报maximum context length exceeded,要么上下文被自动压缩后丢失关键表结构。后来看到 Hacker News 上有人分享了一个叫 Dbctx 的工具,思路是把 PostgreSQL 数据库“编译”成紧凑、可查询的上下文,正好踩中了这个痛点。这篇文章就来完整拆解 Dbctx 的使用思路、核心原理,以及如何把它接入实际项目中。
1. Dbctx 是什么:把数据库结构变成大模型能读懂的“压缩包”
1.1 从“数据库很大”到“模型读不完”的困境
先看一个常见的开发场景。你希望让 ChatGPT、Claude 或本地大模型直接根据数据库表结构生成 SQL、填写数据字典,或者回答“订单表和用户表怎么关联”这类问题。最朴素的做法是:
-- 把整个库的结构导出成 SQL 文件 pg_dump --schema-only -U postgres -d mydb > schema.sql表面上看 schema.sql 只有几百 KB,但一旦包含注释、默认值、索引、约束、触发器,内容会迅速膨胀。更重要的是,把 SQL 原样丢给模型,模型需要在大量 DDL 语法中自己“找重点”,既浪费 token,又容易漏掉关键字段关系。
1.2 Dbctx 的核心思路
Dbctx 做的事情,简单说就是三步:
- 连接 PostgreSQL 数据库,读取系统目录(catalog)中的元数据。
- 把表、字段、类型、约束、索引、视图、外键关系等整理成结构化的紧凑表示。
- 输出一份可以被大模型直接消费的文本上下文,让模型能快速“理解”数据库的全貌。
和pg_dump --schema-only相比,Dbctx 的侧重点不是重建数据库,而是“描述数据库”。它输出的内容更像一份结构化的数据库说明书,而不是重建脚本。
1.3 适合哪些场景
这类“可查询上下文”在下面几种场景里特别有用:
- 让 AI 根据自然语言生成 SQL,模型先读上下文再写语句,准确率通常比裸写高不少。
- 把数据库结构作为辅助材料交给 AI 做数据字典、字段注释生成。
- 基于仓库级代码生成工具(如大模型的 Agent 模式)自动探索项目时,快速注入数据库模型。
- 在 RAG 应用里,把表结构文本切片后作为向量化的知识片段。
简单来说,Dbctx 想做的是在“数据库原始 DDL”和“LLM 能理解的 schema 描述”之间搭一座桥。
2. 环境准备:先有一个可连接的 PostgreSQL
2.1 版本与环境说明
Dbctx 本质上是一个连接 PostgreSQL 并读取系统元数据的工具。不同版本的实现细节可能不同,但核心思路一致。本文以常见环境为例:
| 组件 | 说明 |
|---|---|
| 操作系统 | Windows / macOS / Linux 均可 |
| PostgreSQL | 建议使用 13 及以上版本,本文示例基于 PostgreSQL 15/16 |
| 连接工具 | 支持 PostgreSQL 协议的客户端或驱动 |
| 运行环境 | 取决于 Dbctx 的具体安装方式,可能是 Python、Node、Go 或命令行工具 |
需要特别说明的是,Dbctx 目前可能有不同的实行形态,比如 Python 包、CLI 命令行工具、Node 模块等。你在使用时,以官方仓库 README 为准。本文重点演示通用思路,并用示例代码展示核心流程。
2.2 准备一个测试数据库
为了演示,我们创建一个简单的订单系统数据库,包含用户表、订单表、订单明细表和商品表。先准备 PostgreSQL 环境:
# 以 Ubuntu/Debian 为例,安装 PostgreSQL sudo apt update sudo apt install postgresql postgresql-client # 启动服务 sudo systemctl start postgresql如果使用 Docker,可以用这种方式快速起一个实例:
docker run --name pg-test -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:16创建测试库和基础表:
-- 创建数据库 CREATE DATABASE shop; -- 连接 shop 库后执行 CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, nickname VARCHAR(100), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE products ( id BIGSERIAL PRIMARY KEY, sku VARCHAR(64) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, price NUMERIC(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, is_on_sale BOOLEAN NOT NULL DEFAULT true ); CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), order_no VARCHAR(64) NOT NULL UNIQUE, status VARCHAR(32) NOT NULL DEFAULT 'pending', total_amount NUMERIC(12,2) NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE order_items ( id BIGSERIAL PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES orders(id), product_id BIGINT NOT NULL REFERENCES products(id), quantity INT NOT NULL, price NUMERIC(10,2) NOT NULL ); CREATE INDEX idx_orders_user_id ON orders(user_id); CREATE INDEX idx_order_items_order_id ON order_items(order_id);2.3 确认连接信息
Dbctx 通常需要数据库连接串或独立参数。常见格式如下:
postgresql://postgres:postgres@localhost:5432/shop这里面包含:
- 用户名:postgres
- 密码:postgres
- 主机:localhost
- 端口:5432
- 数据库名:shop
请确保测试环境允许本地连接,并且当前用户有读取系统目录的权限。生产环境务必使用最小权限账号,不要泄露高权限密码。
3. 核心原理拆解:数据库上下文如何“编译”
3.1 为什么不能直接把 DDL 丢给大模型
把数据库结构喂给大模型,直接倒 SQL 文件是一种可行但很低效的方式。原因有几个:
- token 浪费严重:
COMMENT ON、CREATE INDEX、ALTER TABLE ADD CONSTRAINT这些语句会占据大量 token,而模型真正关心的字段名、类型、主外键关系反而淹没在大量语法中。 - 语义不直接:模型需要先“理解”DDL 语法,再从中提取语义。提取过程中可能遗漏
UNIQUE约束、默认值、外键关系等业务规则。 - 上下文碎片化:当表非常多时,模型容易忽略前面出现过的表结构,导致生成的 SQL 引用了不存在的字段。
Dbctx 的目标就是生成一份“语义密度更高”的数据库描述。
3.2 数据库元数据的读取
PostgreSQL 的系统目录是信息最全的元数据来源。常用的几个视图:
pg_class/pg_namespace:表、视图、索引和 schema 的关系。pg_attribute:表的字段信息。pg_constraint:主键、外键、唯一约束、检查约束。pg_index:索引定义。pg_enum:枚举类型的取值。information_schema.tables/information_schema.columns:标准 SQL 风格的表和字段信息。
一个简单的查询示例,读取所有用户表的字段信息:
SELECT c.relname AS table_name, a.attname AS column_name, format_type(a.atttypid, a.atttypmod) AS data_type, a.attnotnull AS not_null, a.attnum AS ordinal_position FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_attribute a ON a.attrelid = c.oid WHERE c.relkind = 'r' AND n.nspname = 'public' AND a.attnum > 0 AND NOT a.attisdropped ORDER BY c.relname, a.attnum;如果你打算自己实现类似 Dbctx 的轻量工具,这个查询就是最初级的 schema 提取器。
3.3 把元数据压缩成紧凑文本
拿到元数据后,接下来就是“压缩”环节。Dbctx 的核心目标通常是输出类似下面的文本块:
Table: users - id: bigserial primary key - email: varchar(255) not null unique - nickname: varchar(100) - created_at: timestamptz default now() Table: orders - id: bigserial primary key - user_id: bigint references users(id) - order_no: varchar(64) not null unique - status: varchar(32) default 'pending' - total_amount: numeric(12,2) default 0 - created_at: timestamptz default now()这种格式的优势在于:
- 没有
CREATE TABLE、ALTER TABLE等恢复性语法。 - 字段类型、默认值、约束非常紧凑。
- 外键关系用
references users(id)直接表达。 - 模型很容易按表名定位相关信息。
更进一步,还可以输出外键关系图、表数量统计、枚举取值集合等,帮助模型建立数据库整体心智模型。
3.4 上下文分块与查询策略
当数据库很大、表很多时,全部塞进一个上下文仍然可能超出窗口限制。常见的做法是分块:
- 先输出“数据库总览”,包含所有表名、每张表的行数估计、表说明。
- 再按业务域把表分组,比如
user、order、product各一组。 - 让 AI 根据问题的关键词,去“查看”对应组的具体字段结构。
这种思路和 RAG(检索增强生成)类似:先粗筛后精读。Dbctx 的“queryable context”就是从“静态文本”发展到“可检索、可定位”的动态上下文。
4. 实战:用 Dbctx 生成 PostgreSQL 查询上下文
4.1 安装与基础命令
由于 Dbctx 是一个较新的工具,具体安装方式以官方仓库为准。这里提供一个典型的假设安装方式(如果你的环境是 Python/Node,按对应包管理器操作):
# 示例:通过 pip 安装(假设包名与官方发布一致) pip install dbctx如果使用 npx 风格的工具:
npx dbctx --connection postgresql://postgres:postgres@localhost:5432/shop如果没有现成工具,也可以通过脚本实现同样的效果。下面给出一段 Python 示例,用于生成紧凑 schema 上下文。
4.2 编写一个轻量级“Dbctx 风格”脚本
假设你想在项目内实现类似 Dbctx 的能力,下面这段代码可以快速上手。它使用psycopg2连接 PostgreSQL,读取元数据,生成 Markdown 格式的数据库上下文。
# 文件路径:generate_db_context.py import psycopg2 from psycopg2 import sql # 连接数据库,请将连接串替换为你自己的 conn = psycopg2.connect( dbname="shop", user="postgres", password="postgres", host="localhost", port="5432" ) def get_columns(conn, table_name): """读取表的所有字段信息""" query = """ SELECT a.attname AS column_name, format_type(a.atttypid, a.atttypmod) AS data_type, a.attnotnull AS not_null, COALESCE(pg_get_expr(d.adbin, d.adrelid), '') AS default_expr FROM pg_attribute a LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum WHERE a.attrelid = %s::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum """ with conn.cursor() as cur: cur.execute(query, (table_name,)) return cur.fetchall() def get_primary_key(conn, table_name): """获取主键字段""" query = """ SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = %s::regclass AND i.indisprimary """ with conn.cursor() as cur: cur.execute(query, (table_name,)) rows = cur.fetchall() return [r[0] for r in rows] def get_foreign_keys(conn, table_name): """获取外键关系""" query = """ SELECT conname, a.attname AS column_name, confrelid::regclass AS referenced_table, af.attname AS referenced_column FROM pg_constraint con JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = ANY(con.conkey) JOIN pg_attribute af ON af.attrelid = con.confrelid AND af.attnum = ANY(con.confkey) WHERE con.contype = 'f' AND con.conrelid = %s::regclass """ with conn.cursor() as cur: cur.execute(query, (table_name,)) return cur.fetchall() def is_foreign_key_column(column_name, foreign_keys): """判断字段是否外键""" for fk in foreign_keys: if fk[1] == column_name: return True return False def table_to_context(conn, table_name): """把一张表转换为紧凑上下文""" columns = get_columns(conn, table_name) pks = get_primary_key(conn, table_name) fks = get_foreign_keys(conn, table_name) foreign_columns = {fk[1]: fk[2] for fk in fks} lines = [f"Table: {table_name}"] for col_name, data_type, not_null, default in columns: parts = [f"- {col_name}: {data_type}"] if col_name in pks: parts[0] += " primary key" if col_name in foreign_columns: parts[0] += f" references {foreign_columns[col_name]}" if not_null: parts[0] += " not null" if default: parts[0] += f" default {default}" lines.append(parts[0]) return "\n".join(lines) def get_all_tables(conn): """获取所有用户表""" query = """ SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename """ with conn.cursor() as cur: cur.execute(query) return [r[0] for r in cur.fetchall()] if __name__ == "__main__": tables = get_all_tables(conn) context_parts = [] for table in tables: context_parts.append(table_to_context(conn, table)) # 输出上下文 output = "\n\n".join(context_parts) print(output) # 可选:保存到文件,方便后续拼接到 Prompt with open("db_context.md", "w", encoding="utf-8") as f: f.write(output) print("\n\n已保存到 db_context.md") conn.close()这个脚本的流程是:
- 读取
publicschema 下所有表名。 - 对每张表读取字段、主键、外键、默认值、非空信息。
- 格式化为紧凑 Markdown 文本。
- 输出到终端并保存到
db_context.md文件。
运行命令:
python generate_db_context.py预期输出片段:
Table: order_items - id: bigserial primary key - order_id: bigint references orders(id) not null - product_id: bigint references products(id) not null - quantity: integer not null - price: numeric(10,2) not null Table: orders - id: bigserial primary key - user_id: bigint references users(id) not null - order_no: varchar(64) not null - status: varchar(32) default 'pending'::character varying - total_amount: numeric(12,2) default 0 - created_at: timestamptz default now()这份文本比原始 DDL 精炼得多,可以直接拼接到 Prompt 中供模型阅读。
4.3 把上下文接入 Prompt
有了db_context.md,接下来就是把它喂给大模型。一个典型的 Prompt 结构如下:
你是电商系统的数据库专家。请根据下面的数据库结构,回答用户的问题。 数据库结构: {db_context} 用户问题:查询最近 7 天每个用户的订单总金额,输出用户邮箱和总金额,按金额降序排列。模型看到结构后,能清楚知道:
orders表有user_id、total_amount、created_at。- 可以关联
users表取email。 - 金额字段是
total_amount,类型是numeric。
生成的 SQL 示例:
SELECT u.email, SUM(o.total_amount) AS total_spent FROM orders o JOIN users u ON u.id = o.user_id WHERE o.created_at >= now() - INTERVAL '7 days' GROUP BY u.email ORDER BY total_spent DESC;相比不给出上下文,模型生成错误字段名的概率会明显降低。
4.4 处理超大数据库:分片与摘要
如果数据库有几百张表,即使是最紧凑的格式也可能超过模型上下文限制。这时可以按业务域切分。以“订单系统”为例:
python generate_db_context.py --tables users,orders,order_items --output order_context.md你也可以在脚本中增加--table-prefix参数,只提取特定前缀的表,比如ods_、dim_、fact_。
分片后,还可以生成一个“总览上下文”,只包含表名、行数估计和字段数量:
Tables overview: - users: 9 columns - products: 6 columns - orders: 6 columns - order_items: 5 columns总览上下文帮助模型判断应该查看哪些分片,再通过二次检索读取具体结构。这也是“queryable context”的精髓所在:先定位,再精读。
5. 常见问题与排查思路
5.1 提示词太长或上下文溢出
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 模型报 context length 超限 | 表过多或字段描述过于详细 | 分批注入;只注入相关表;减少默认值/约束描述 |
| 上下文自动压缩后内容丢失 | 单次请求塞入了过多内容 | 将上下文拆成多轮对话,按需加载 |
| 生成 SQL 引用不存在的字段 | 注入的 schema 不全 | 检查是否遗漏了相关表的分片 |
建议在工具脚本中增加--max-tables或--max-chars参数,强制限制输出长度。
5.2 连接 PostgreSQL 报错
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
connection refused | PostgreSQL 未启动或端口错误 | 检查服务状态,确认端口,pg_isready测试 |
password authentication failed | 密码错误或认证方式不受支持 | 检查pg_hba.conf配置,确认密码 |
database "shop" does not exist | 数据库名称错误 | 用\l列出所有数据库 |
| SSL 相关错误 | 服务端要求 SSL | 在连接串中指定sslmode=require或sslmode=disable |
连接 PostgreSQL 时,推荐先用命令行客户端验证:
psql "postgresql://postgres:postgres@localhost:5432/shop" -c "SELECT 1;"如果命令行能连上,工具连不上,多半是驱动或连接参数的问题。
5.3 生成的上下文缺少外键关系
如果脚本没有展示外键关系,通常是因为查询pg_constraint时没有正确关联pg_attribute。上面示例已经处理了这个问题。实际使用时,可以单独验证某张表的外键:
SELECT conname, conrelid::regclass AS table_name, a.attname AS column_name, confrelid::regclass AS referenced_table FROM pg_constraint con JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = ANY(con.conkey) WHERE con.contype = 'f' AND conrelid = 'orders'::regclass;5.4 枚举类型、数组类型显示不友好
PostgreSQL 的format_type函数对复杂类型可能输出较长的类型签名,比如character varying(64)。可以在脚本中做类型别名替换:
def normalize_type(raw_type): """把冗长类型名改成紧凑形式""" replacements = { "character varying": "varchar", "timestamp with time zone": "timestamptz", "numeric": "numeric", "bigint": "bigint", "integer": "int", } for k, v in replacements.items(): raw_type = raw_type.replace(k, v) return raw_type这样输出更紧凑,也更贴合日常 SQL 写作习惯。
6. 最佳实践与工程建议
6.1 权限与安全
读取数据库元数据本身是低风险操作,但仍要注意:
- 使用只读账号,避免使用 superuser 或高权限账号。
- 生产环境禁止将连接串硬编码在脚本或前端代码中,使用环境变量或密钥管理工具。
- 生成的
db_context.md属于内部结构信息,不要提交到公开仓库,尤其不要放到公开 AI 对话里。 - 如果上下文文件已经包含敏感的生产表名、字段名,建议脱敏后再分享给协作方。
推荐的环境变量方式:
import os DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/shop")6.2 上下文增量与版本管理
数据库结构会随着迭代而变化,因此上下文文件也需要版本管理:
- 把生成脚本纳入 Git 仓库。
- 在 CI 流程中增加一步:当表结构变更时自动生成新的
db_context.md。 - 用 PgModeler、SchemaSpy 等工具对比结构差异,及时更新 AI 提示词。
如果使用迁移工具(如 Alembic、Flyway),可以在迁移完成后自动触发上下文生成。
6.3 选择注入内容:不是越全越好
给大模型的上下文不是越详细越好。字段注释、默认值、约束对生成 SQL 有用,但外键级联规则、触发器逻辑、索引定义通常没有必要。重点保留:
- 表名、字段名、类型。
- 主键、唯一约束。
- 外键关系。
- 枚举取值(如果业务依赖)。
- 常用过滤字段的注释。
这样能最大限度地控制 token 消耗,同时保证模型掌握核心结构。
6.4 结合 SQL 生成评估体系
把 Dbctx 生成的上下文接入 AI 后,建议建立简单的 SQL 评估集。准备 10 到 20 条真实业务问题,每道题记录:
- 模型生成的 SQL 是否可执行。
- 是否使用了正确的表名、字段名。
- 是否考虑了空值、去重、排序等边界。
- 对复杂查询,是否使用了正确的 join 类型。
通过评估发现上下文缺失的信息,再反向优化生成脚本。比如评估结果显示模型经常把orders.status的默认值搞错,就可以在上下文中显式写出状态枚举值。
6.5 与向量数据库结合
如果表非常多,可以把每张表的上下文文本向量化,存放在 PostgreSQL 的pgvector扩展或专门的向量数据库中。查询时先做语义检索,找到最相关的表,再拼接最终上下文。这种方式对超大规模 schema 尤其有效。
一个简单流程:
- 每张表生成一行“表级摘要”。
- 使用 embedding 模型生成向量。
- 用户提问时,检索最相关的 3 到 5 张表。
- 把这 5 张表的紧凑结构注入 Prompt。
这样既能控制上下文长度,又能确保回答不偏离相关表结构。
7. 一些更深的思考:数据库上下文工程化的方向
Dbctx 这类工具背后,其实是“上下文工程”(context engineering)的思路:不是让模型“碰运气”理解数据,而是主动为模型准备一份高质量、高密度的输入。这对生成 SQL、数据分析问答、BI 助手这类方向都很重要。
如果你正在搭建企业内部的 AI 数据分析助手,建议先花时间设计好 schema 上下文的格式。以 Dbctx 输出的紧凑文本为起点,逐步增加:
- 表注释和常用查询模式提示。
- 指标口径说明(比如“销售额 = 订单金额总和,不包含退款”)。
- 敏感字段标记信息(比如“员工邮箱脱敏后才可展示”)。
- 近期迁移记录或大表分区说明。
这些信息都是模型生成准确 SQL 的关键约束。
如果你想自己实现类似 Dbctx 的工具,本文提供的 Python 脚本已经是一个可运行的最小版本。接下来可以考虑:
- 增加元数据缓存,避免频繁读取系统目录。
- 支持输出 JSON 格式,方便程序化处理。
- 支持按 schema 或表前缀过滤。
- 提供 CLI 子命令,比如
dbctx extract、dbctx query、dbctx stats。 - 增加与 LangChain、LlamaIndex 的集成示例。
8. 总结与继续探索的方向
本文从“大模型读不完数据库结构”这个实际问题出发,介绍了 Dbctx 的核心思路:把 PostgreSQL 数据库编译成紧凑、可查询的上下文。通过一个完整的 Python 示例,演示了如何自行生成数据库上下文,并把它接入 Prompt 中辅助模型生成 SQL。
需要强调的是,Dbctx 这类工具的核心价值不是输出一个静态文本,而是围绕数据库结构做“语义压缩”和“上下文组织”。当表数量少时,直接拼接即可;当表数量大时,需要分片、摘要、检索结合来使用。你可以先动手跑通脚本,用自己本地的测试数据库生成一份上下文,观察模型生成 SQL 的变化。
下一步可以继续探索的内容包括:
- 深入了解 PostgreSQL 系统目录结构,掌握
pg_class、pg_attribute、pg_constraint、pg_index之间的关系。 - 尝试把上下文文本向量化后存入
pgvector,做一个可检索的表结构知识库。 - 学习 LangChain 中
SQLDatabaseToolkit的工作机制,理解它如何利用表结构信息执行 text-to-SQL。 - 在真实项目中建立 SQL 生成评估集,持续优化 Promot 和上下文质量。
上下文工程是一个值得长期投入的方向。Dbctx 只是起点,你可以基于这个思路,发展出适合自己项目的数据库上下文生成工具。如果本文对你有帮助,可以收藏备用,也欢迎在评论区分享你给大模型“喂”数据库结构时的经验和坑。