
前阵子有个做后端管理的同事跟我吐槽新来的实习生问“咱们库到底有哪些表”翻了半天文档也没理清楚最后只能把Navicat里的表结构截图一张张发过去。我当时就在想与其给人一张张截图表名不如让数据库自己开口说话。后来我抽空做了个小工具用Java写调用逻辑本地跑Ollama起一个大模型服务用户直接输入“数据库里有哪些表”模型理解意图后由Java后台去查PostgreSQL的元数据把所有数据表名称列出来。整个过程数据不出内网模型本地推理Java侧只执行白名单里的查询SQL安全性和可控性都有保障。今天就把完整的实现思路、代码和踩过的坑都整理出来给同样想做数据库自然语言查询入口的同学一个参考。1. 为什么要把Ollama、Java和PostgreSQL串起来一个“问表”工具的真实需求1.1 三种“查表名”的做法反差在哪里先说清楚一个事实如果想查PostgreSQL里有哪些数据表名称本质上是一条SQL就能解决的问题。比如SELECT tablename FROM pg_tables WHERE schemaname public;但问题从来不在SQL本身而在“谁有资格执行这条SQL”。我把常见的查表方式拉了一个对比你会发现它们是递进关系不是替代关系。查表方式上手门槛适合人群典型缺点数据库客户端直接执行SQLpgAdmin、DBeaver低但必须会SQLDBA、后端开发业务人员不会用新员工不知道该查哪张表Java JDBC / DatabaseMetaData 编程查询中需要会写代码开发人员做成内部接口依然是面向程序员的工具非技术人员无法自助使用自然语言 Ollama JDBC模型层零门槛Java层封装所有角色包括产品、运营、新入职同学需要维护本地模型环境设计Prompt和安全边界我做这个工具的初衷就是想让第三种方式在内部落地。输入“帮我看看订单相关的表”系统返回订单域的几张表名输入“这个库有哪些表”系统把全部表名列出来。自然语言入口的价值不在于“替代SQL”而在于把一个需要技术背景的操作变成了一个所有人都能对话的动作。1.2 技术栈选择的理由私有化、Java存量、PG元数据规范为什么偏偏是Ollama、Java、PostgreSQL这个组合三个理由每一个都是实际业务场景逼出来的。第一数据安全限制。表名听起来没什么但实际上表名会暴露业务结构users、orders、payment_records一路看下来你的系统架构就透明了。很多企业内部明确禁止把任何数据相关的内容发送到外部API所以必须走本地推理模型。Ollama在这类场景里确实方便一条命令起服务HTTP API风格接近OpenAIJava调用成本极低还天然支持离线部署。我在内网服务器上部署一次之后局域网内所有机器都能通过一个内网地址访问体验非常接近云端API。第二Java是存量技术栈。公司内部管理后台、工单系统、数据平台基本是Java/Spring Boot把“自然语言查表”的能力做成一个Java组件塞进现有系统比引入一套独立技术栈要平滑得多。Java 11之后自带的HttpClient类就能完成Ollama API的调用连额外的HTTP客户端依赖都不用加这对轻量工具来说很友好。第三PostgreSQL的元数据查询有标准答案。information_schema是SQL标准定义的元数据视图集合PostgreSQL对它的支持很完整。查表名、查字段、查约束都能在information_schema里找到对应视图写出来的SQL语义清晰基本不依赖数据库私有语法。这给后续扩展“查字段”“查索引”留了很大的余地。1.3 整条链路的流程概述为了后面看不乱我先给整个链路搭个框架。整个流程可以拆成四步用户输入自然语言例如“数据库里有哪些表”。Java调用本地Ollama服务传入设计好的Prompt让模型输出一个结构化的意图JSON。Java解析这个JSON通过白名单判断只放行预先定义好的操作比如list_tables。Java使用JDBC连接PostgreSQL执行写死的元数据查询SQL把表名列表返回给用户。这里有一个设计上的关键点Ollama只做“意图识别”不生成SQL。市面上很多方案会让大模型直接生成SQL然后交给数据库执行我明确不建议这么做原因在后面“安全边界”部分展开。LLM做翻译Java做控制数据库做查询各司其职整条链路才不会失控。2. 环境搭建Ollama模型准备、PostgreSQL初始化、Java依赖配置2.1 Ollama本地部署的完整实操Ollama的安装本身不复杂真正麻烦的是模型下载。我先说安装Windows直接下载安装包双击macOS同理Linux服务器上用官方脚本一条命令或者下载二进制包手动解压到/usr/local/bin。装完启动服务默认监听11434端口。模型选择上我推荐先从qwen2.5:7b起步。查表名这个任务本质上是一个意图分类任务对模型的推理能力要求不高7B参数级别完全够用8GB左右显存就能跑起来。如果机器显存很紧张可以换3B/4B级别的小模型如果对输出稳定性有更高要求且机器配置允许再上14B。我实测下来7B模型配合好的Prompt意图识别准确率已经很高。模型下载慢的问题可能是大家问得最多的。我的经验是三种方式组合使用一是优先找国内能访问的镜像站拉取把模型文件下载下来再通过ollama create导入或者找离线安装包在局域网内分发二是把下载任务放在网络空闲时段执行不要在工作时间盯着进度条焦虑三是如果只是做开发验证先用体积最小的模型版本跑通链路再换正式的7B模型。这里顺便提醒一句模型下载失败时别反复重试同一个源先看是不是磁盘空间不足。Ollama的模型文件动辄几个GBdf -h看一眼往往比排查网络更有效。模型默认存储路径也值得提前规划。Linux下Ollama默认存在/usr/share/ollama/.ollama/models或当前用户目录下系统盘空间不够时非常被动。建议提前设置环境变量OLLAMA_MODELS指向数据盘比如export OLLAMA_MODELS/data/ollama/models然后重启Ollama服务再重新拉模型。Windows下可以在系统环境变量里同样设置这个变量路径指向D盘或E盘的某个目录。这个动作最好在一开始就做否则模型已经下好了再迁移要重新拷好几GB文件。安装完成后用一行命令验证服务状态curl http://localhost:11434/api/tags能返回一个包含已安装模型的JSON数组说明Ollama服务正常可以进入下一步。2.2 PostgreSQL版本选择和测试库准备PostgreSQL版本选择很多人会纠结。我的建议很直接如果公司有生产环境就跟随生产环境的大版本省心如果是从零开始直接用16或以上版本。16版本在查询优化和并发控制上做了不少改进而且作为较新的稳定版本社区资料和工具链支持都很成熟。PostgreSQL的升级路径也相对平滑不用太担心版本绑死的问题。安装方式本地开发我强烈推荐Docker干净、可变现、删了重来没负担docker run --name pg-test -e POSTGRES_PASSWORDpostgres -p 5432:5432 -d postgres:16如果是Ubuntu物理机用apt安装也一样只是要记得设置postgres用户的密码并确认pg_hba.conf的认证方式和监听地址。默认情况下PostgreSQL只监听localhost局域网访问需要改listen_addresses本地测试不用动。装好之后建一个测试库再随便造几张表CREATE DATABASE testdb; \c testdb CREATE TABLE customers (id serial PRIMARY KEY, name text); CREATE TABLE orders (id serial PRIMARY KEY, customer_id int, amount numeric); CREATE TABLE products (id serial PRIMARY KEY, title text, price numeric);这几张表名都设计成业务含义明确的词方便后面验证Ollama的意图识别效果。实际上测试模型的时候表名最好也准备一些带业务色彩的比如vip_user_info、tmp_order_backup这种看看模型会不会被干扰。2.3 Java Maven工程与JDBC最小连通验证Java侧我用的是Maven工程核心依赖只有三个dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId version42.7.3/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.17.1/version /dependency dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.1.0/version /dependencypostgresql是JDBC驱动jackson-databind负责解析Ollama返回的JSONHikariCP是连接池。先别急着接Ollama第一步先在Java里把PostgreSQL的表名查出来确认数据库链路通再做模型接入这样排查问题能少一半的干扰。最小验证代码很简单String url jdbc:postgresql://localhost:5432/testdb; String user postgres; String password postgres; try (Connection conn DriverManager.getConnection(url, user, password)) { String sql SELECT table_name FROM information_schema.tables WHERE table_schema public AND table_type BASE TABLE ORDER BY table_name ; try (Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { System.out.println(rs.getString(1)); } } }这里我直接用了information_schema而不是pg_tables原因后面细说。跑通这一步数据库侧的基础就算打好了。3. 核心实现Java调用Ollama理解意图再查询PostgreSQL所有表名3.1 Ollama API调用逻辑/api/chat接口与Java HttpClient封装Ollama对外提供的REST API里有两个接口经常被用到/api/generate和/api/chat。前者是纯文本补全适合“给一段文本让模型续写”的场景后者是对话式接口接收messages数组每个元素带role和content适合做意图识别这样的多轮任务。我选/api/chat因为意图识别本身就是一个“系统提示 用户提问”的对话结构。Java 11之后自带的java.net.http.HttpClient完全可以胜任调用任务不需要额外引入OkHttp或RestTemplate。封装一个OllamaClient很简单public class OllamaClient { private final String baseUrl; private final HttpClient httpClient; private final ObjectMapper objectMapper new ObjectMapper(); public OllamaClient(String baseUrl) { this.baseUrl baseUrl; this.httpClient HttpClient.newHttpClient(); } public String chat(String model, String systemPrompt, String userMessage) throws IOException, InterruptedException { MapString, Object payload new HashMap(); payload.put(model, model); payload.put(stream, false); MapString, String systemMsg new HashMap(); systemMsg.put(role, system); systemMsg.put(content, systemPrompt); MapString, String userMsg new HashMap(); userMsg.put(role, user); userMsg.put(content, userMessage); ListMapString, String messages new ArrayList(); messages.add(systemMsg); messages.add(userMsg); payload.put(messages, messages); MapString, Object options new HashMap(); options.put(temperature, 0.1); payload.put(options, options); String requestBody objectMapper.writeValueAsString(payload); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(baseUrl /api/chat)) .header(Content-Type, application/json) .POST(BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8)) .build(); HttpResponseString response httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() ! 200) { throw new RuntimeException(Ollama API error: response.statusCode() , body: response.body()); } JsonNode root objectMapper.readTree(response.body()); return root.path(message).path(content).asText(); } }几个细节请注意。第一stream必须显式设为false否则Ollama会返回流式chunkJava侧处理起来麻烦很多。第二temperature设置成0.1让模型输出尽可能确定性意图识别这种任务不需要创造性越稳定越好。第三用Jackson的writeValueAsString来构建请求体而不是手动拼字符串可以避免中文和特殊字符转义问题。3.2 Prompt设计把自然语言收敛成结构化意图模型输出稳不稳定七成看Prompt。我的做法是让模型输出一个固定结构的JSON而不是直接输出SQL。设计如下system: 你是一个数据库元数据查询助手。用户会用自然语言询问数据库结构相关问题。 你的任务识别用户的意图只输出一个JSON对象不要输出任何其他文字、解释或代码。 可用意图枚举 - list_tables用户想查看数据表名称例如“有哪些表”“列出所有表”“数据库里都有什么表”。 输出格式{action: list_tables, schema: public} 示例 用户数据库里有哪些表 输出{action: list_tables, schema: public} 用户请帮我列出这个库的全部数据表名称 输出{action: list_tables, schema: public} 如果无法识别输出{action: unknown, schema: public}这个设计里有几个值得说道的点。首先严格要求“只输出JSON对象”杜绝了“好的数据库中的表有”这类废话输出Java侧解析失败率大幅下降。其次few-shot示例给了两个不同的问法让模型理解“有哪些表”和“列出全部表名”是同一个意图这比只给一个示例泛化能力强得多。第三schema字段也给默认值public为后续扩展多schema场景留了口子。另外Ollama支持在请求体里加format: json参数强制模型输出JSON结构这是2024年之后新版本才支持的特性值得打开能进一步约束输出。在调用时给payload加一行就行了。3.3 查询PostgreSQL所有数据表名称的SQL三种写法与选型对比PostgreSQL里查表名至少有三种写法我做个对比方便你按场景选择写法SQL示例特点pg_tablesSELECT tablename FROM pg_tables WHERE schemaname publicPostgreSQL私有系统视图写法最简洁但不跨库information_schema.tablesSELECT table_name FROM information_schema.tables WHERE table_schema public AND table_type BASE TABLESQL标准元数据视图语义清晰跨数据库兼容性好推荐pg_class pg_namespaceSELECT c.relname FROM pg_class c JOIN pg_namespace n ON n.oid c.relnamespace WHERE n.nspname public AND c.relkind r直接查底层系统表性能最优但可读性差适合表数量极大时的性能优化我推荐information_schema方案原因有三。一是它是SQL标准的一部分在MySQL、PostgreSQL和一些国产数据库上都能跑将来切换数据库成本低。二是它有明确的table_type字段可以精确过滤出“BASE TABLE”也就是普通表把视图排除在外。三是字段名table_name、table_schema语义一目了然后面接ODBC、接BI工具都方便。如果业务里存在多个schema需要把所有非系统schema下的表都列出来SQL改成这样SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN (pg_catalog, information_schema) AND table_type BASE TABLE ORDER BY table_schema, table_name;这个写法会同时输出schema名和表名更贴近企业库“一堆schema各管一堆表”的真实结构。内网管理工具里我建议默认用这个而不仅仅是查public。不过标题这个场景简化一点只查public也没问题。3.4 完整可运行的Java代码把上面的模块串起来主流程代码长这样。先定义一个意图类JDK 16及以上可以用recordpublic record Intent(String action, String schema) {}然后是建表查询类public class TableLister { public ListString listTables(Connection conn, String schema) throws SQLException { ListString tables new ArrayList(); String sql SELECT table_name FROM information_schema.tables WHERE table_schema ? AND table_type BASE TABLE ORDER BY table_name ; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, schema); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { tables.add(rs.getString(1)); } } } return tables; } }最后是组装主流程public class Main { public static void main(String[] args) throws Exception { String userInput args.length 0 ? args[0] : 数据库里有哪些表; String model qwen2.5:7b; String systemPrompt 你是一个数据库元数据查询助手... // 完整Prompt见上文 OllamaClient ollama new OllamaClient(http://localhost:11434); String modelOutput ollama.chat(model, systemPrompt, userInput); ObjectMapper mapper new ObjectMapper(); Intent intent; try { intent mapper.readValue(modelOutput, Intent.class); } catch (JsonProcessingException e) { throw new RuntimeException(模型输出不是合法JSON: modelOutput, e); } if (!list_tables.equals(intent.action())) { System.out.println(抱歉我只支持查询数据表名称。); return; } String url jdbc:postgresql://localhost:5432/testdb; try (Connection conn DriverManager.getConnection(url, postgres, postgres)) { TableLister lister new TableLister(); ListString tables lister.listTables(conn, intent.schema()); System.out.println(当前数据库共有 tables.size() 张表); tables.forEach(System.out::println); } } }运行效果就是一行对话输入数据库里有哪些表 输出当前数据库共有 3 张表 customers orders products到这一步一个最基础的“Java Ollama PostgreSQL查表名”链路已经通了。但“能跑”和“能稳定用”之间还有一段距离下面这部分才是真正的实战干货。4. 实测踩坑从demo到稳定可用的五个细节4.1 为啥查出来多了很多系统表schema过滤的必要性第一次跑通的时候我印象最深的一个坑是表名列表里全是pg_开头的系统表几百条刷屏。原因很简单pg_tables和information_schema.tables默认会把数据库内部的系统表也暴露出来。PostgreSQL内置了pg_catalog和information_schema两个系统schema里面存的是数据库自己的元数据表。如果不加过滤条件查出来的列表几百行业务表淹没在系统表里毫无可用性。解决办法就是前面的SQL里写死的两个条件table_schema public或者table_schema NOT IN (pg_catalog, information_schema)。这个坑看着不起眼但实际遇到时特别迷惑因为系统表名大多以pg_或sql_开头一眼看上去还以为是数据被污染了。我的建议是从第一版代码开始就把schema过滤条件写进SQL常量里不要等到用户反馈“表名列表不对”再去排查。4.2 模型输出偶尔不乖temperature参数、JSON解析、重试策略Ollama默认的temperature是0.8这个参数下模型“更有创造力”容易在JSON前后加解释文字甚至输出“好的下面是你要的表名列表”这种废话。意图识别任务是确定性的分类任务不需要创造力必须把temperature降到0.1左右。即使temperature很低模型依然有小概率输出不合法JSON比如单引号代替双引号、末尾多一个逗号。我实测处理办法是加一道“解析失败重试”逻辑第一次模型输出解析失败把失败原因拼进对话重新请求一次让模型修正。String modelOutput null; for (int attempt 0; attempt 2; attempt) { String output ollama.chat(model, systemPrompt, userInput); try { intent mapper.readValue(output, Intent.class); break; } catch (JsonProcessingException e) { if (attempt 1) { throw new RuntimeException(模型连续两次输出非法JSON: output, e); } userInput userInput \n注意你刚才的输出不是合法JSON请重新只输出JSON对象。; } }如果Ollama版本支持强烈建议在请求里加上format: json。这个参数会约束解码器只生成JSON结构我在qwen2.5上开了之后解析失败率基本归零。4.3 JDBC连接不能每次新建HikariCP连接池配置我上面给的示例代码用DriverManager.getConnection每次查询都新建连接这在命令行demo里没问题但一旦接到HTTP接口上并发一高就会暴露两个问题PostgreSQL每建一次连接都要完成TCP握手和身份认证耗时几十毫秒到几百毫秒不等连接数没有上限压力大的时候能把数据库的连接数打爆。换成HikariCP是标准做法配置非常轻量HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:postgresql://localhost:5432/testdb); config.setUsername(postgres); config.setPassword(postgres); config.setMaximumPoolSize(5); config.setMinimumIdle(2); config.setConnectionTimeout(3000); DataSource dataSource new HikariDataSource(config);然后所有查询都从dataSource.getConnection()拿连接用完即还。maximumPoolSize设置成5就够内部工具用了不需要贪多。这个优化做完接口在并发查询下依然稳定不需要其他花活。4.4 安全边界意图白名单 最小权限只读账号这一点是我最想强调的。很多人做LLM 数据库的第一反应是让模型直接生成SQL然后传给数据库执行。我强烈不建议这么做至少在内部工具阶段不要做。原因有两个第一模型生成的SQL很可能有语法错误SELECT拼错一个字母整个查询就崩了第二如果Prompt被绕过模型完全可能生成DROP TABLE、DELETE FROM之类的危险语句而数据库并不知道这句话来自“意图识别失败”还是“恶意构造”。我采用的安全策略是“意图白名单 最小权限账号”。意图白名单的意思是Ollama模型允许输出的action只有我们预先定义的那几个枚举值比如list_tables、list_columns。Java侧拿到模型输出之后必须校验action在枚举集合内否则直接拒绝执行。模型永远没有机会生成SQLSQL都是我们自己写死、参数化绑定的。最小权限账号的意思是不要用postgres超级用户连接数据库单独建一个只读账号CREATE ROLE meta_reader LOGIN PASSWORD readonly; GRANT CONNECT ON DATABASE testdb TO meta_reader; GRANT USAGE ON SCHEMA public TO meta_reader; GRANT SELECT ON ALL TABLES IN SCHEMA public TO meta_reader;以后JDBC的连接串里就用这个只读账号。就算将来安全边界被突破能执行的最多也只是SELECT不会造成数据破坏。这个成本极低但价值极高内部工具至少要做到这一层。4.5 模型未加载时的兜底处理Ollama服务起来了并不代表模型已经下载好了。调用/api/chat时如果模型不存在Ollama默认会返回404模型不存在的错误。第一次部署时我就在这个环节卡了几分钟还以为是Java代码写错了。建议在启动阶段做一次模型探测curl http://localhost:11434/api/tagsJava侧也写一个等价方法启动时检查目标模型是否在已安装列表里不在就给出明确提示“正在拉取模型首次下载可能需要较长时间”然后调用/api/pull触发下载或者干脆抛错让运维处理。不要把“模型不存在”的原始堆栈直接暴露给用户内部工具的用户通常不是程序员一句“模型服务异常请联系管理员”比一堆Python堆栈友好得多。5. 扩展思路从“问表名”到“问字段、问结构、生成SQL”的升级路线5.1 增加list_columns意图用同一个套路解决更细的问题思路一旦跑通“只查表名”很快就满足不了需求了。紧接着的自然扩展是“查某张表的字段”。实现方式完全一样只是在Prompt的意图枚举里加一项再加一条查询SQL。Prompt扩展示例可用意图枚举 - list_tables用户想查看数据表名称 - list_columns用户想查看某张表的字段信息需要额外的table参数 输出格式{action: list_columns, schema: public, table: orders} 示例 用户订单表有哪些字段 输出{action: list_columns, schema: public, table: orders}对应的SQLSELECT column_name, data_type FROM information_schema.columns WHERE table_schema public AND table_name ? ORDER BY ordinal_position;Java侧只需在TableLister里加一个queryColumns方法并把Intent record加一个table字段。整个架构不用动加意图、加SQL、加方法三步完成。这就是把模型输出收敛成结构化意图的最大好处新增功能像填表一样简单。5.2 整理成Spring Boot服务结合API Key和代理命令行工具验证完了下一步通常就是把它包成一个Spring Boot服务供内部系统通过HTTP调用。OllamaClient和TableLister都可以直接做成Service组件。如果服务要暴露给局域网内多个系统建议在前置网关做一层反代并校验API Key防止未经认证的请求打到Ollama上。Ollama新版本也支持配置OLLAMA_API_KEY环境变量开启后调用方需要在请求头里带Authorization: Bearer才能访问这个功能值得打开省得自己写一套权限系统。数据库连接直接用上一节配置好的HikariCP数据源连接数可控查询稳定。5.3 个人实测体会与建议最后说点个人体会。这个工具做下来我最大的感触是不要把大模型放进数据库的执行路径里而是让它站在入口处做“翻译”。翻译错了最多是用户得到一条“没有匹配的操作”的提示执行错了可能就是把整张表清空。意图白名单这个设计从一开始就决定了这个工具是安全的。另外如果你是第一次做类似项目我建议先跳过Ollama把“Java查PostgreSQL表名”这条SQL链路跑通再加模型层。两层解耦之后无论哪一边出问题排查范围都能缩小一半。本地模型的推理速度虽然不是毫秒级但查一个表名清单几秒钟内返回完全能接受。这种“本地大模型 白名单意图 标准元数据查询”的组合后续接查询表注释、表索引、甚至生成简单的数据字典文档都是顺理成章的事。