把PostgreSQL编译成紧凑上下文:Dbctx如何让LLM理解数据库结构 Dbctx 这个项目做的事情可以用一句话概括把一个 PostgreSQL 数据库编译成紧凑、可查询的上下文文件供下游程序或大语言模型使用。它的价值不在于提供一个全新的查询引擎而在于把数据库这张“大表”压缩成一份有结构的快照让下游系统在不需要连接数据库的情况下也能理解库里的表结构、表关系和数据特征。在 AI 应用、数据交付和自动化分析场景里这个需求非常常见。LLM 不擅长直接查询 PostgreSQL因为数据库响应体积大、字段语义不明确、关系复杂直接塞进模型上下文既浪费 token 又容易丢失关键信息。Dbctx 这类工具的思路是把数据库预先“编译”成紧凑上下文再用关键词检索、SQL 生成或向量检索去消费它。这篇文章会从概念、环境、最小实现、验证、排错到生产实践把这条链路完整拆开。适合阅读这篇文章的读者有两类一类是正在做 LLM 应用、RAG 或智能问答需要把 PostgreSQL 业务数据接入模型上下文的开发者另一类是希望把数据库快照结构化交付给外部系统又不想暴露完整连接信息的后端工程师。读完可以自己实现一个最小可运行的“数据库上下文编译器”并知道如何评估它对生产系统的价值。1. 先理解 Dbctx 要解决的上下文问题1.1 数据库为什么不能直接当上下文用很多开发者第一次做 LLM 问答时会尝试把数据库查询结果直接拼进 Prompt。比如查出订单表的所有行然后塞给模型让它分析。这种做法的第一个问题是数据量不可控。一张用户表可能有一千万行每行几十个字段。即使只取一部分传输和解析成本也会非常高。更重要的是模型未必需要全部原始记录。它需要的是“这些数据大概是做什么的、有哪些字段、字段之间有什么关系、数据分布长什么样”而不是每一行的完整内容。第二个问题是原始数据库结果缺少语义。数据库里常见status int、created_at timestamptz、product_id integer这样的字段。开发者和模型看到status3并不知道 3 代表“已发货”还是“已取消”。上下文如果只保留数据库返回值不保留字段注释、枚举含义和业务规则模型就很难给出准确回答。第三个问题是耦合。让下游程序直接连接生产数据库意味着它要持有账号、密码、网络白名单权限还要面对复杂 SQL 注入风险、大查询拖垮数据库的风险。如果只是给模型或分析系统提供一份只读上下文快照连接耦合就可以彻底解除。1.2 “编译成紧凑可查询上下文”到底意味着什么Dbctx 标题里用了 “Compile” 这个词。它不是传统意义上把高级语言翻译成机器码的编译而是把数据库这张“动态系统”固化成一个“静态知识文件”。编译过程通常包括四件事结构提取、内容压缩、关系索引和格式导出。结构提取是把每个表的字段名、字段类型、是否为空、默认值、主外键关系抓出来。内容压缩是对数据做采样、聚合、截断只保留最能描述数据特征的记录。关系索引是把外键关联、常用查询路径标注出来方便下游知道表与表之间怎么连接。格式导出是生成 JSON、JSON Lines、Markdown、SQLite 或向量索引文件供不同场景消费。“紧凑”这个词很关键。它意味着输出文件必须比原始数据库小几个数量级。数据库可能几个 GB上下文文件应该控制在几十 KB 到几 MB。否则就失去了“编译后上下文”的意义和直接导出全量数据没有区别。“可查询”意味着上下文不是盲盒。下游程序可以用关键词定位相关表用结构化字段过滤结果或者通过语义向量召回相关内容。没有查询能力的上下文文件只是一个难以使用的转储文件。1.3 Dbctx 在真实开发链条中的位置在一套典型的 LLM 应用架构里Dbctx 可以出现在两条链路中。第一条是离线分析链路。运营人员想快速了解数据库里有哪些表、表里有多少数据、最近新增了哪些记录。这时 Dbctx 生成的 Markdown 摘要可以直接作为分析报告的输入不需要每次现写 SQL。第二条是在线问答链路。用户问“最近一周哪些商品缺货”系统先用上下文检索找到products表和stock字段的定义再结合自然语言生成 SQL或者直接把相关字段和采样数据注入 Prompt让模型基于真实结构回答。Dbctx 解决的是这两条链路里共同的痛点数据库结构、数据特征和语义信息不能一直只存在于 DBA 的脑子里或散落在不同 SQL 脚本中。它应该被编译成一份稳定、可版本化、可缓存、可审计的上下文产物。链路输入输出Dbctx 的辅助作用离线分析数据库依赖摘要报告生成结构摘要和采样数据LLM 问答用户问题SQL 或自然语言回答提供表结构、字段语义和关系说明数据交付内部库表外部系统快照脱敏、采样、限定字段范围自动化工具多环境数据库统一上下文文件统一不同库的结构表示2. 环境准备先有一个能跑的 PostgreSQL 实例2.1 本机安装 PostgreSQL 的几种方式要理解 Dbctx不一定需要先安装它。但你需要一个 PostgreSQL 实例来观察“数据库到上下文”的转换过程。安装方式取决于操作系统这里给出三种常见路径。第一种是包管理器安装。Ubuntu 和 Debian 上可以使用aptmacOS 上可以使用 Homebrew。Windows 上建议直接使用官方安装器安装时记得勾选psql命令行工具和 pgAdmin。# Ubuntu / Debian sudo apt update sudo apt install postgresql postgresql-client # macOS 使用 Homebrew brew install postgresql15第二种是 Docker 方式。这种方式最干净不会污染宿主机也容易清理。如果你的机器上已经装了 Docker可以直接运行一个容器作为测试库。docker run --name dbctx-postgres \ -e POSTGRES_PASSWORDpostgres \ -e POSTGRES_DBdbctx_demo \ -p 5432:5432 \ -d postgres:15第三种是云数据库。阿里云 RDS、腾讯云 PostgreSQL、AWS RDS 等都可以创建一个测试实例。使用云数据库时要注意网络白名单和连接地址本地开发机需要把公网 IP 加入白名单。需要特别注意版本问题。Dbctx 及其类似工具对 PostgreSQL 的元数据查询依赖information_schema或pg_catalog这两个系统 schema 在 PostgreSQL 9.6 到 16 之间变化不大但细节会有差异。学习阶段建议使用 PostgreSQL 14 或 15避免太老或太新的版本带来额外变量。2.2 启动、停止和连接检查安装完成后第一个要确认的事情不是建表而是服务能不能正常启动。不同系统的启动命令差异很大下面是常见做法。Docker 启动非常简单不需要 systemd。docker start dbctx-postgres docker stop dbctx-postgres docker logs -f dbctx-postgres如果是本机安装Ubuntu 上通常使用 systemd 管理。sudo systemctl start postgresql sudo systemctl status postgresql sudo systemctl restart postgresqlmacOS 上如果使用 Homebrew 安装可以用brew services管理。brew services start postgresql15 brew services stop postgresql15启动之后用psql做一次最小连接检查。这里要理解localhost和127.0.0.1的区别localhost可能走 Unix socket也可能走 IPv6127.0.0.1明确走 TCP。很多连接失败其实是 socket 和 TCP 认证策略不同造成的。psql postgresql://postgres:postgres127.0.0.1:5432/dbctx_demo -c SELECT version();如果看到 PostgreSQL 版本信息说明服务正常。如果提示password authentication failed说明密码不对或认证方式配置有问题需要在 PostgreSQL 的pg_hba.conf里确认127.0.0.1/32的认证方式。2.3 准备测试表和测试数据为了后面演示编译效果建议建两张有外键关系的小表。一张是商品表一张是订单表。下面的 SQL 可以直接粘贴到psql里执行。CREATE TABLE products ( product_id serial PRIMARY KEY, name text NOT NULL, category text NOT NULL, price numeric(10,2) NOT NULL, stock integer NOT NULL DEFAULT 0, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE orders ( order_id serial PRIMARY KEY, product_id integer NOT NULL REFERENCES products(product_id), quantity integer NOT NULL, status text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); INSERT INTO products (name, category, price, stock) VALUES (机械键盘, 外设, 399.00, 120), (无线鼠标, 外设, 89.00, 300), (27寸显示器, 显示设备, 1499.00, 45), (USB扩展坞, 配件, 129.00, 0), (笔记本支架, 配件, 79.00, 210); INSERT INTO orders (product_id, quantity, status, created_at) VALUES (1, 2, 已付款, now() - interval 1 day), (2, 5, 已发货, now() - interval 3 hours), (3, 1, 待支付, now() - interval 30 minutes), (1, 1, 已完成, now() - interval 10 days), (4, 3, 已取消, now() - interval 2 days);为什么要选外键关系因为上下文文件里如果能体现orders.product_id - products.product_id这种关系LLM 或分析师才能正确理解多表 join 的方向。如果只导出独立的表上下文会丢失关系语义查询质量会明显下降。3. 设计 Dbctx 的编译流程从数据库到上下文文件3.1 编译流程的阶段划分一个可靠的数据库上下文编译器通常不是一次查询就能完成的。它的流程可以拆成四个阶段发现元数据、读取统计信息、采样业务数据、组装输出文件。发现元数据是查询information_schema或pg_catalog拿到表清单、字段清单、约束和索引。这一步的目的是理解库的“骨架”。读取统计信息是获取行数、每列是否为空、值分布范围等。统计信息让下游知道数据规模而不用打开每一个字段。采样业务数据是挑选几行有代表性的记录让下游看到真实数据长什么样。组装输出是把这些内容序列化成目标格式。这个流程有一个重要原则不同阶段之间应该是松耦合的。如果元数据采集失败不应该影响已经生成的上下文文件。如果采样表不存在应该记录告警而不是让整个编译崩溃。否则在生产环境里一个表结构变化就能让整个上下文生成任务失败。3.2 Schema 提取与关系摘要Schema 提取是最核心的一步。它需要拿到的信息包括表名、字段名、字段类型、是否可空、默认值、主键、外键、唯一约束和索引。字段类型值得特别说明。PostgreSQL 的numeric(10,2)和integer虽然都是数字但语义完全不同。前者表示精确到两位小数的金额后者表示整数 ID。如果上下文里只保留number模型就分不清到底哪个是金额、哪个是数量。所以类型不能简化。外键关系更是如此。没有外键说明模型可能把orders.product_id当作商品名称或订单编号。有了关系说明它才知道要 join 到products表的product_id。下面是一个典型的 schema 摘要可以从 PostgreSQL 的information_schema中查询出来。SELECT tc.table_name, kcu.column_name, ccu.table_name AS foreign_table, ccu.column_name AS foreign_column FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name kcu.constraint_name JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name tc.constraint_name WHERE tc.constraint_type FOREIGN KEY AND tc.table_schema public;在输出上下文时可以把外键关系单独放置形成类似下面的简写结构{ table: orders, column: product_id, references: { table: products, column: product_id } }3.3 数据采样、聚合与压缩光有 schema 还不够下游需要知道数据长什么样。最直接的方式是采样。比如每张表取前 5 行到 10 行记录展示真实值。采样需要处理两个问题。第一个问题是选择哪些列。数据库中经常有content text、description text、avatar_url varchar这类大字段直接采进上下文会让文件膨胀。做法是限制字段最大长度或者干脆排除指定的列。第二个问题是敏感字段。用户表里的手机号、邮箱、身份证号订单表里的支付单号都不应该进入上下文快照。采样之前应该先做列名单过滤而不是采完之后再脱敏。列不在上下文中泄漏风险就少一层。聚合信息也很有价值。比如行数、空值数量、最小值、最大值、最常见值。对分类字段来说常见值其实就是枚举语义。例如status字段最常见的值是“已发货”“待支付”“已取消”下游一看就能理解这个字段的业务含义。products: 5 rows - category 最常见值: 外设(2), 配件(2), 显示设备(1) - stock 最小 0, 最大 300 orders: 5 rows - status 最常见值: 已付款(1), 已发货(1), 待支付(1), 已完成(1), 已取消(1)3.4 输出格式JSON Lines 与 Markdown 摘要上下文的输出格式取决于消费方。主流的做法是双输出一份结构化文件一份人类可读文件。JSON Lines 适合程序解析。每行一个 JSON 对象对应一张表的完整描述。程序可以用json.loads逐行读取不需要一次性把整个文件载入内存。这种格式对关键词检索、向量化、SQL 生成器都非常友好。Markdown 适合给 LLM 当 Prompt 前缀。模型对 Markdown 表格和列表的理解通常比对 JSON 更好因为层级结构更清晰。下面是一个 Markdown 摘要的例子# Database Context: dbctx_demo Generated: 2025-01-10T12:00:00Z ## products - row_count: 5 - product_id: integer - name: text, nullable no - category: text, nullable no - price: numeric(10,2), nullable no - stock: integer, default 0 - created_at: timestamptz ## orders - row_count: 5 - order_id: integer - product_id: integer, references products.product_id - quantity: integer - status: text - created_at: timestamptz两种格式可以同时生成因为它们来自同一份内存数据结构。生成代码只需要写一次数据组装逻辑再分别序列化到两种目标文件即可。4. 用 Python 实现一个最小 Dbctx 编译示例4.1 项目结构与依赖这里用一个最小 Python 项目演示整个思路文件名取dbctx-lite。它不代表 Dbctx 官方实现而是帮助理解核心设计。实际项目接入时请以你要使用的版本的文档和 CLI 行为为准。项目结构如下dbctx-lite/ ├── requirements.txt ├── compile_db.py └── query_ctx.py依赖只有 psycopg。推荐使用 psycopg 3.x 版本它的连接方式和现代 Python 风格更一致。psycopg[binary]3.1安装依赖pip install -r requirements.txt环境变量或命令行参数用来传入数据库连接串。不要把密码写死在代码里尤其是要提交到仓库的示例代码。4.2 读取数据库元数据第一个函数负责拿到所有表名。这里查询information_schema.tables只取BASE TABLE不取视图避免把视图和物化视图混在一起。def fetch_tables(conn, schemapublic): with conn.cursor() as cur: cur.execute( SELECT table_name FROM information_schema.tables WHERE table_schema %s AND table_type BASE TABLE ORDER BY table_name , (schema,)) return [row[0] for row in cur.fetchall()]第二个函数读取指定表的字段信息。ordinal_position保证字段顺序和建表顺序一致这点在生成上下文时很重要。def fetch_columns(conn, schema, table): with conn.cursor() as cur: cur.execute( SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema %s AND table_name %s ORDER BY ordinal_position , (schema, table)) return [ { name: row[0], type: row[1], nullable: row[2] YES, default: row[3], } for row in cur.fetchall() ]关键点是不要用SELECT *去推断字段。information_schema本身就是 PostgreSQL 提供的标准元数据视图准确且稳定。生产环境如果追求性能可以改查pg_attribute、pg_class、pg_namespace这些系统目录但示例阶段用标准视图更不容易出错。4.3 采样数据并生成紧凑上下文拿到字段之后可以查询每张表的行数和采样数据。这里要小心两件事一是表名拼接要校验二是大字段要截断。import json import re def safe_identifier(ident): if not re.fullmatch(r[a-z_][a-z0-9_]*, ident): raise ValueError(finvalid identifier: {ident}) return ident def fetch_count(conn, schema, table): with conn.cursor() as cur: cur.execute( fSELECT count(*) FROM {safe_identifier(schema)}.{safe_identifier(table)} ) return cur.fetchone()[0] def fetch_sample(conn, schema, table, limit5, max_text_length200): with conn.cursor() as cur: cur.execute( fSELECT * FROM {safe_identifier(schema)}.{safe_identifier(table)} LIMIT %s, (limit,), ) column_names [desc[0] for desc in cur.description] rows [] for raw in cur.fetchall(): row {} for name, value in zip(column_names, raw): if hasattr(value, isoformat): value value.isoformat() elif isinstance(value, str) and len(value) max_text_length: value value[:max_text_length] ... row[name] value rows.append(row) return rows在拼接表名时safe_identifier的作用是阻止 SQL 注入。生产环境更好的做法是使用psycopg.sql.Identifier进行标识符转义而不是用正则白名单。这里为了代码简洁先用正则做一层约束。接下来把这些信息组装成一张表的上下文记录并写入 JSON Lines 文件。def compile_table(conn, schema, table, sample_size, out_fp): columns fetch_columns(conn, schema, table) row_count fetch_count(conn, schema, table) sample fetch_sample(conn, schema, table, sample_size) record { table: table, schema: schema, row_count: row_count, columns: columns, sample: sample, } out_fp.write(json.dumps(record, ensure_asciiFalse) \n) return recordensure_asciiFalse是关键配置。如果数据库里存了中文商品名不关闭 ASCII 转义的话JSON 里会是\u673a\u68b0\u952e\u76d8阅读和模型理解都变得困难。主函数负责连接数据库、遍历所有表、生成上下文def main(): import argparse parser argparse.ArgumentParser() parser.add_argument(--db-url, requiredTrue) parser.add_argument(--schema, defaultpublic) parser.add_argument(--sample-size, typeint, default5) parser.add_argument(--out, defaultcontext.jsonl) args parser.parse_args() with psycopg.connect(args.db_url) as conn: tables fetch_tables(conn, args.schema) with open(args.out, w, encodingutf-8) as f: for table in tables: compile_table(conn, args.schema, table, args.sample_size, f) print(fcompiled {len(tables)} tables - {args.out}) if __name__ __main__: main()运行命令python compile_db.py \ --db-url postgresql://dbctx_user:dbctx_pass127.0.0.1:5432/dbctx_demo \ --sample-size 5 \ --out context.jsonl4.4 增加一个简单的查询入口编译后的 JSONL 如果没有查询入口就只能手工翻文件。下面写一个最小关键词检索脚本。它逐行读取 JSONL把整行记录转成 JSON 字符串再判断关键词是否在其中。import argparse import json def search_keyword(ctx_path, keyword, limit10): hits [] keyword_lower keyword.lower() with open(ctx_path, encodingutf-8) as f: for line in f: line line.strip() if not line: continue record json.loads(line) blob json.dumps(record, ensure_asciiFalse).lower() if keyword_lower in blob: hits.append(record) if len(hits) limit: break return hits def main(): parser argparse.ArgumentParser() parser.add_argument(--file, defaultcontext.jsonl) parser.add_argument(--keyword, requiredTrue) args parser.parse_args() hits search_keyword(args.file, args.keyword) for record in hits: print(f{record[table]} ({record[row_count]} rows)) for col in record[columns]: print(f - {col[name]}: {col[type]}, nullable{col[nullable]}) print(ftotal: {len(hits)} table(s)) if __name__ __main__: main()运行python query_ctx.py --file context.jsonl --keyword category这个脚本只处理精确关键词。生产系统如果要做模糊匹配或语义召回可以把 JSONL 内容向量化后存入 pgvector再做相似度搜索。后面会有专门说明。5. 运行验证检查上下文质量与查询结果5.1 编译后文件长什么样正常运行后context.jsonl里应该有两行分别是products和orders的上下文。第一行对应的 JSON 大致如下这里只展示结构不展示完整数据。{ table: products, schema: public, row_count: 5, columns: [ {name: product_id, type: integer, nullable: false, default: nextval(products_product_id_seq::regclass)}, {name: name, type: text, nullable: false, default: null}, {name: category, type: text, nullable: false, default: null}, {name: price, type: numeric(10,2), nullable: false, default: null}, {name: stock, type: integer, nullable: false, default: 0}, {name: created_at, type: timestamptz, nullable: false, default: now()} ], sample: [ { product_id: 1, name: 机械键盘, category: 外设, price: 399.00, stock: 120, created_at: 2025-01-09T10:00:0000:00 } ] }注意几个细节。price在 JSON 中变成了字符串399.00这是因为 psycopg 返回的numeric是Decimal类型而json.dumps默认不能直接序列化Decimal。示例代码里没有专门转换Decimal实际运行中有可能需要加一层str()转换。这个很容易踩坑后面会专门说。created_at已经转换成 ISO 格式字符串。这样即使离开了 PostgreSQL 会话下游系统仍然能理解时间语义。5.2 用关键词和 SQL 双层查询上下文文件生成后可以用两种方式验证它“可查询”。第一种是上面的 JSON 关键词检索。第二种更适合验证结构是否完整方法是把context.jsonl重新载入内存检查是否存在外键关系、字段数量是否完整、采样记录是否包含业务关键字。import json with open(context.jsonl, encodingutf-8) as f: records [json.loads(line) for line in f] tables {r[table] for r in records} print(tables:, tables) for record in records: print(f{record[table]}: {len(record[columns])} columns, {record[row_count]} rows, sample{len(record[sample])})正常输出应该类似tables: {products, orders} products: 6 columns, 5 rows, sample5 orders: 5 columns, 5 rows, sample5如果发现sample是空的而如果表里明明有数据说明查询用户可能没有权限访问表内容或者采样逻辑被授权遮断了。5.3 在 LLM 场景下注入上下文的两种方式上下文文件最有价值的消费方是 LLM。常见注入方式有两种全量注入和检索后注入。全量注入适合小库。把 Markdown 摘要直接放在 Prompt 前面让模型在回答时参考。例如你是一个数据库分析助手。下面是数据库上下文 # Database Context: dbctx_demo ## products - row_count: 5 - category: text, 常见值 外设、配件、显示设备 - price: numeric(10,2) - stock: integer 请根据上下文回答用户问题。检索后注入适合大库。先用query_ctx.py这类脚本从 JSONL 中命中与问题相关的表再只把相关表的上下文拼接进 Prompt。这样可以控制 token 数量不丢失关键结构。无论哪种方式都要记住一个原则上下文是给模型看的参考信息不是让模型盲目相信的权威数据。模型仍然可能犯错尤其是当采样数据不能代表全量数据时。所以在线回答类应用最好同时返回它参考了哪个表、哪个字段方便人工校验。6. 核心参数与取舍紧凑度、保真度、查询成本6.1 上下文编译器的关键参数在实际使用 Dbctx 或自己实现编译器时参数设计决定了产物质量。下面是一些常见参数具体默认值以你使用的项目 README 为准。参数含义调小的影响调大的影响建议sample_size每张表采样行数文件更小但可能错过代表值更贴近真实分布但体积变大小表 5 到 10 行大表按比例降低max_text_length单个文本字段最大保留长度上下文更紧凑但丢失长文本内容保留信息更完整但 token 消耗高200 到 500 字符之间include_data是否保留采样数据只保留 schema 和统计提供真实数据示例学习环境可开生产环境视敏感程度exclude_schemas排除的 schema 列表可能漏掉业务表上下文更全但噪声更多默认排除pg_catalog、information_schemainclude_views是否导出视图忽略视图逻辑覆盖更完整但视图有时涉及权限按业务需求决定token_budget上下文总 token 预估上限更紧凑但可能不完整更完整但超过模型窗口根据模型窗口大小设置留 20% 余量6.2 紧凑度与保真度如何平衡紧凑和保真在上下文编译里是天然矛盾的。想要上下文小就要少采样、短字段、少关系。想要模型回答准确就要保留更多字段含义、枚举值和关系结构。关键在于“识别什么是保真的核心”。对大多数库来说字段名、字段类型、外键关系和常见枚举值是核心。这四样信息量不大但对回答准确率影响极大。可以先保证这些完整再根据模型窗口剩余空间决定采样多少行原始数据。有些列可以彻底丢弃。比如内部的自增 ID、审计时间戳、二进制内容、临时标记位。丢弃前要考虑下游是否真的需要。如果下游要生成 SQL join主键和关联字段就不能丢。6.3 采样策略与枚举语义采样不是简单的LIMIT 5。LIMIT取的是物理顺序的前几行往往不能代表数据分布。更好的做法是分类采样比如按枚举字段分组后每组取几行。SELECT * FROM products WHERE category 外设 LIMIT 3;这样能让上下文覆盖不同品类而不是只看到同一个分类下的商品。如果后续接入 LLM 生成 SQL这种采样方式会让模型更容易理解category字段的可选值。从 PostgreSQL 视角看频繁换组采样会带来额外查询开销。编译任务是低频离线任务可以接受。但如果是在线注入上下文应该直接使用缓存快照不要每次实时采样。7. 常见问题与排查链路7.1 连接失败、权限不足和元数据为空连接失败是最常见的第一个坑。现象通常是psycopg.OperationalError: connection failed或FATAL: password authentication failed。排查顺序是先确认端口和主机是否连通再检查密码和账号再检查pg_hba.conf认证方式。pg_isready -h 127.0.0.1 -p 5432如果想确认用户是否能看到表可以先手动查询元数据视图SELECT table_name FROM information_schema.tables WHERE table_schema public;如果这条 SQL 返回空很可能是连接用户没有USAGE权限或者连错了数据库。要注意连接串里不写数据库名时psycopg 会默认连接到用户名同名的数据库这很容易导致元数据为空。问题现象常见原因检查方式处理建议连接超时端口未开放或防火墙拦截telnet 127.0.0.1 5432检查云安全组或本机防火墙password authentication failed密码错误或认证方式为 peer查看pg_hba.conf改用 MD5/scram 认证元数据正常但 SELECT 报权限错误用户只有元数据权限没有表数据权限用同一账号执行SELECT * FROM products LIMIT 1给用户授权目标表 SELECT表数量为 0连错了 database 或 schema执行SELECT current_database(), current_schema()修改连接串和 search_path7.2 Decimal、时间类型和中文 JSON 序列化运行 Python 示例时如果采样字段包含numeric类型会看到这样的报错TypeError: Object of type Decimal is not JSON serializable原因很简单json.dumps不认识Decimal。解决办法是在序列化前对值做转换。可以在fetch_sample里对每个值判断类型也可以写一个默认转换函数。def default_serializer(value): if hasattr(value, isoformat): return value.isoformat() if isinstance(value, Decimal): return str(value) raise TypeError(fnot serializable: {type(value)})调用时传入json.dumps(record, ensure_asciiFalse, defaultdefault_serializer)中文乱码问题通常来自两个地方。一个是 JSON 文件写入时没有指定encodingutf-8另一个是json.dumps默认开启ensure_asciiTrue会把中文转成\u转义序列。写入时指定 UTF-8序列化时指定ensure_asciiFalse就能解决。7.3 编译结果超过模型窗口编译后的上下文文件可能远大于预期。常见原因有三个采样行数过多、长文本字段没有截断、表数量太多。出现这个情况时第一步不是调参而是看文件里哪张表占用的体积最大。可以简单统计每行 JSON 的字符数import json with open(context.jsonl, encodingutf-8) as f: for line in f: record json.loads(line) size len(line) print(record[table], size)找到体积最大的表后再决定是减少sample_size、排除大字段还是对长文本做更激进的截断。这里要注意上下文大小不是唯一的评判标准。如果模型需要根据具体内容回答过度压缩反而会导致答非所问。7.4 敏感数据泄漏风险把数据库编译成上下文文件本质上是一次数据导出。手机号、身份证号、地址、支付信息这些敏感字段如果原样进入上下文文件一旦文件被共享或提交到仓库就构成数据泄漏。最稳妥的策略是在编译前就排除敏感列。可以在编译脚本里维护一份“排除字段名单”。exclude_columns { users: {phone, email, id_card}, orders: {payment_method, card_no}, }字段不在上下文里后面所有环节都不需要担心它。如果确实需要展示脱敏后的数据可以在采样阶段直接替换字符串value value[:3] **** value[-2:] if len(value) 8 else ****但脱敏逻辑容易出错特别是当数据格式不统一时。推荐做法是默认排除只有显式确认的安全字段才允许进入上下文。7.5 数据库结构变更后上下文过期编译出来的上下文文件是静态快照。数据库结构一旦变化旧的上下文不会自动更新。工程上要用版本号来标识每次快照。{ snapshot_version: 2025-01-10T12:00:00Z, db_version: PostgreSQL 15.6, generator_version: dbctx-lite/0.1.0 }下游系统消费上下文时要先读版本号再决定是否使用缓存。如果发现快照版本早于某个关键迁移时间就触发重新编译。结构变更的典型现象是模型生成的 SQL 引用了已经不存在的字段或者描述的是旧表结构。排查时对比最新数据库 schema 和上下文文件里的字段差异即可。7.6 pgvector从关键词检索走向语义检索如果上下文文件里的表很多关键词检索容易漏掉同义表达。例如用户问“键盘缺货吗”关键词检索可能命中name字段里的“键盘”但不会命中描述键盘的“输入设备”。这时可以引入 pgvector把上下文内容向量化。方向是先把 JSONL 里的表描述、字段描述、采样记录拼成文本用 embedding 模型编码成向量再存到 PostgreSQL 的向量列里。检索时对用户问题同样做向量编码查询最近的上下文片段。CREATE EXTENSION IF NOT EXISTS vector; ALTER TABLE context_snapshot ADD COLUMN embedding vector(384);向量维度取决于 embedding 模型。有的模型输出 384 维有的输出 1536 维要按模型文档配置。需要注意pgvector 只是存储和检索向量它并不负责生成向量。生成向量的 embedding 服务是另一个独立的依赖。语义检索能提升召回率但会引入模型服务、向量维度、索引参数等额外复杂度。生产环境建议先跑通关键词检索和 SQL 生成确认上下文质量没有大问题后再引入向量化。8. 生产环境最佳实践与扩展方向8.1 学习环境最小化生产环境做隔离学习阶段可以直接连接主库读元数据和采样表。生产环境不应该这样。生产环境的建议是创建独立的只读账号只授权目标 schema 的SELECT权限使用网络隔离让编译任务运行在数据源同一内网输出文件不要落在共享目录控制在特定服务账号可见。上下文文件里不包含数据库密码但文件本身仍然是敏感数据访问权限要按数据导出件管理。如果编译任务要定时运行建议把输出文件名带上时间戳保留最近几个版本。这样新版本出问题时可以快速回滚到旧上下文而不是重新从数据库拉全量。环境数据库账号输出文件刷新策略安全要求学习环境当前用户或管理员任意本地目录每次手动编译低测试环境只读业务账号测试服务器每次发布前编译不包含真实用户隐私数据生产环境最小只读账号安全存储或对象存储定时 版本化敏感字段排除、访问审计、权限收敛8.2 发布前检查清单在把数据库上下文接入 LLM 应用或交付给下游系统之前建议按这份清单逐项检查。上下文文件里是否包含敏感字段是否已经排除或脱敏。每张表的字段名和类型是否与当前数据库一致有没有过期的表或列。外键关系是否完整关键 join 路径是否在上下文中有说明。采样数据是否覆盖主要枚举值至少包含表中最常见的分类。文件总大小是否在模型窗口或下游解析能力内。时间字段是否统一成 ISO 格式数值字段是否序列化正确。文件是否有版本号是否能够追溯到生成时间、数据库版本和生成器版本。下游程序消费上下文时是否有异常分支比如表不存在、字段类型变化。该上下文文件的访问权限是否收敛是否会被误提交到代码仓库。8.3 扩展方向增量编译、缓存和审计当前文章里的示例是全量编译每次跑都把整个 schema 读一遍。如果数据库有几百张表全量编译会比较耗时。可以改成增量方式记录上次编译时间只导出information_schema中被修改过的表。但增量编译也有风险。表结构没变不代表数据分布没变。尤其是采样数据可能因为新增记录而变得过时。折中方案是schema 做增量更新采样数据按时间窗口或表大小做定期全量刷新。缓存是另一个重要方向。线上 LLM 问答不应该每次请求都读取上下文文件。更合理的做法是启动时加载一次上下文文件到内存监听文件变化后热更新或者通过 Redis 缓存上下文内容减少 IO。缓存更新时要注意原子性不能出现下游读取到一半旧文件、一半新文件的情况。审计在生产环境尤其重要。每次上下文生成任务都应该记录谁触发、从哪个数据库、使用哪个账号、筛查了哪些字段、输出到哪个位置、文件大小和耗时多少。这些日志能帮助你回答“这份上下文是从哪里来的”这个基本问题。8.4 实际项目落地建议Dbctx 这类工具在生产里落地不要只把它当成一个导出脚本。它实际上是在做数据库知识的“结构化管理”哪些信息进上下文、哪些被丢弃、多久刷新一次、谁有权看快照这些决策比导出代码本身更重要。如果你的项目刚开始优先做最小闭环一个 PostgreSQL 测试库、一张业务表、一个能生成 JSONL 或 Markdown 的脚本、一次向 LLM 注入上下文的实验。跑通之后再考虑增加外键关系、排除敏感字段、支持多 schema、接入 pgvector。一个值得记住的判断标准是上下文文件不是数据库的备份而是数据库的“摘要”。它的职责是让下游快速理解数据库结构而不是替代数据库回答所有问题。所以能丢弃的要果断丢弃能截断的要截断该暴露的关系和枚举语义一个都不能少。