1. 项目概述:当数据库不再只是“存数据的地方”

我干数据库这行快十四年了,从最早在机房里手动调优Oracle RAC集群,到后来带团队做金融级MySQL高可用架构,再到最近三年深度参与几家大型制造企业和医疗集团的AI知识中台建设——我越来越清楚一件事: 企业数据库正在经历一场静默但彻底的身份重构 。它不再是那个被业务系统“写完就扔”、被DBA半夜叫醒排查锁表、被审计部门翻来覆去查日志的被动存储容器;它正悄然蜕变为组织真正的“认知中枢”。这个转变不是靠堆硬件、换新版本,而是靠一种新的连接范式:让大语言模型(LLM)能真正“读懂”、能“提问”、能“推理”,甚至能“自主行动”的数据库。

你可能已经注意到,现在几乎所有技术会议的主论坛上,“Agentic AI”这个词出现的频率,已经超过了“微服务”或“云原生”。但很多人没想明白的是: Agent再聪明,如果它的“眼睛”看不到真实业务数据,“手”够不到核心交易系统,“脑子”里没有上下文语义,那它就是个会背诗的鹦鹉 。而企业里最真实、最结构化、最权威的数据,90%以上都安静地躺在Oracle、SQL Server、PostgreSQL、DB2这些传统关系型数据库里,或者藏在SAP HANA、Teradata、Greenplum这类分析型引擎中。它们不是“旧技术”,而是组织几十年沉淀下来的“ operational wisdom”——比如某条产线设备故障与备件库存周转率之间的隐性关联,某类医保结算规则在不同地区政策微调下的执行偏差,某次营销活动ROI异常背后隐藏的客户分群迁移路径……这些洞察,从来不在PPT里,只在数据库的字段、索引、约束和千万行记录里。

所以,这篇文章讲的,不是怎么用LLM写一封更漂亮的周报,也不是怎么把客服对话喂给模型生成话术库。它讲的是: 如何把一个运行了十年、承载着核心订单、库存、财务、HR数据的生产数据库,变成一个能实时响应自然语言查询、能主动发现数据异常、能跨系统串联业务逻辑、甚至能自动生成合规性检查报告的“活的知识系统” 。这背后没有魔法,只有三根支柱:一是对数据库语义层的深度解构与重建(不是简单建视图),二是LLM与数据库交互协议的实质性升级(远超SQL生成),三是组织内数据所有权与使用权的重新定义。接下来,我会用自己亲手落地的两个真实案例——一家三甲医院的临床决策支持系统改造,和一家汽车零部件制造商的供应链风险预警平台——把这套方法论拆开揉碎,告诉你每一步为什么这么走、踩过哪些坑、以及最关键的: 哪些事,绝对不能交给自动化脚本去干

2. 核心思路拆解:为什么是“知识系统”,而不是“智能查询接口”

2.1 从“SQL翻译器”到“业务语义理解者”的本质跃迁

很多团队一开始做的,都是“LLM+数据库”的初级形态:用户输入“上个月华东区销售额最高的三个产品”,模型把它翻译成一条SELECT语句,执行后返回结果。听起来很酷,但实操中你会发现,这种模式在企业环境里几乎寸步难行。为什么?因为真实业务场景的复杂度,远超自然语言到SQL的简单映射。

举个我亲身经历的例子。去年帮一家医疗器械公司做销售分析,业务方提的需求是:“请找出所有在Q2有采购意向、但最终未下单的潜在客户,并分析他们放弃的原因。” 这句话看似清晰,但落到数据库里,它涉及至少5张核心表: leads (线索)、 opportunities (商机)、 quotes (报价单)、 orders (订单)、 support_tickets (售后工单)。而“放弃原因”这个字段,在 quotes 表里叫 reason_for_rejection ,在 support_tickets 表里叫 resolution_summary ,在 opportunities 表里又可能通过 stage_name 字段的流转状态(如“Proposal Sent”→“Negotiation”→“Closed Lost”)间接体现。更麻烦的是, reason_for_rejection 字段里填的内容五花八门:“价格太高”、“交期太长”、“技术参数不匹配”、“竞争对手中标”,甚至还有“客户老板出国了”这种非结构化文本。

如果只靠LLM硬生生去“猜”SQL,它大概率会:

  • 只查 quotes 表,漏掉从售后工单里挖掘出的“产品稳定性差”这类深层原因;
  • stage_name 的字符串匹配写成 LIKE '%Closed Lost%' ,而实际数据库里这个字段是枚举值,正确写法是 stage_id = 7
  • 对“Q2”的理解停留在字面,生成 BETWEEN '2024-04-01' AND '2024-06-30' ,却忽略了该公司财务季度是从4月1日开始,而销售季度是从3月1日开始,导致时间范围错位。

所以,我们彻底放弃了“端到端SQL生成”的思路。取而代之的,是构建一个 三层语义桥接层

  1. 实体层(Entity Layer) :将数据库中的物理表、字段、主外键关系,映射为业务人员能理解的实体概念。比如, customers 表 + addresses 表 + contacts 表 → 统一抽象为“客户档案”实体,其属性包括“主联系人姓名”、“注册地址”、“最近一次沟通日期”等。这个映射不是静态配置,而是通过解析数据库的DDL、外键约束、索引命名规范,结合业务文档(如ERD图、数据字典)半自动完成。
  2. 关系层(Relationship Layer) :明确定义实体间的业务逻辑关系。例如,“客户档案”与“销售机会”之间,不是简单的1:N外键关系,而是存在“当前主推商机”、“历史成交商机”、“已关闭商机”三种业务状态关系。这些关系会被编码为可查询的元数据,供LLM在规划查询路径时调用。
  3. 意图层(Intent Layer) :将用户自然语言请求,分解为可执行的原子意图。比如“找出放弃采购的客户并分析原因”,会被LLM识别为两个意图: FIND_CUSTOMERS_WITH_STATUS('Closed Lost') ANALYZE_REJECTION_REASON_FROM_MULTIPLE_SOURCES 。每个意图都对应一组预定义的、经过DBA严格审核的SQL模板或存储过程,而非自由拼接的动态SQL。

提示:这个三层桥接层,是我们整个项目的基石。它的工作量占到前期准备的70%,但换来的是后续90%的查询稳定性和可维护性。千万别想着“先跑起来再说”,跳过这步,后面全是债。

2.2 Model Context Protocol(MCP):不是新协议,而是新思维

文章里提到的Model Context Protocol(MCP),常被误读为一个待发布的标准协议。其实,在我们落地的项目中,它更像是一种 设计哲学 确保LLM在每次交互中,所获得的上下文信息,是精确、最小、且业务相关的,而不是把整个数据库Schema一股脑塞过去

传统做法是让LLM“记住”整个数据库的表结构。这就像让一个刚入职的实习生,第一天就背下公司全部200页的《数据管理手册》。他记不住,也用不对。而MCP的实践,是给LLM配一个“随身业务顾问”——一个轻量级的、嵌入在查询流程中的上下文注入器。

它的运作流程是这样的:

  • 用户提问:“为什么上海分公司上季度的退货率比平均值高20%?”
  • 系统首先不急着查数据,而是启动一个“上下文探针”(Context Probe)模块。这个模块会基于问题中的关键词(“上海分公司”、“退货率”、“上季度”),快速扫描语义桥接层的元数据,定位到最相关的3个实体: branches (分公司)、 returns (退货记录)、 sales (销售记录)。
  • 接着,它会从这三个实体的定义中,提取出当前问题最需要的上下文片段:
    • branches 实体的 region_code 字段,其有效值列表是 ['SH', 'BJ', 'GZ', 'SZ'] ,其中 'SH' 明确标注为“上海”;
    • returns 实体的 return_reason 字段,其业务分类树是: [产品质量问题 → 外观缺陷, 功能失效] [物流问题 → 配送延迟, 包装破损] [客户侧问题 → 订单错误, 不需要了]
    • sales 实体的时间维度,其 fiscal_quarter 字段的计算逻辑是: CASE WHEN month IN (1,2,3) THEN 'Q1' ... END ,且当前系统时间是2024-07-15,因此“上季度”被精确解析为 'Q2'
  • 最后,这些精炼的、带业务标签的上下文片段(而非原始DDL),连同用户问题一起,作为Prompt的一部分,发送给LLM。

这个过程,把LLM从一个需要“全知全能”的神坛上拉下来,变成了一个专注解决具体问题的“领域专家”。它不需要知道 returns 表有多少个索引,只需要知道“退货原因”有哪些业务大类;它不需要理解 fiscal_quarter 字段的底层存储类型,只需要知道“上季度”对应哪个值。 MCP的本质,是把数据库的“机器语义”,翻译成LLM能高效处理的“人类语义”

2.3 Agent-to-Agent(A2A)通信:让数据库自己“开口说话”

如果说MCP解决了“LLM如何理解数据库”,那么A2A通信要解决的,就是“数据库如何主动影响LLM的决策”。这听起来有点玄,但其实在企业级应用中,它解决的是最痛的痛点: 数据变更的实时感知与响应

想象一下,你的供应链风险预警系统,依赖于供应商的“最新评级”和“近期交付准时率”。如果这个评级数据每天凌晨由ETL任务从ERP系统同步一次,那么白天发生的重大事件——比如某关键供应商突发火灾、某原材料价格单日暴涨50%——系统是完全不知道的,直到第二天早上才能“反应过来”。这在今天的商业环境中,是致命的延迟。

A2A通信的实践,就是给数据库装上一个“神经末梢”。我们不是在数据库外面加一层消息队列,而是直接利用数据库自身的 变更数据捕获(CDC)能力 ,将其转化为一种标准化的、面向Agent的事件流。

以PostgreSQL为例,我们的做法是:

  • 在关键业务表(如 suppliers purchase_orders )上启用 logical replication
  • 编写一个轻量级的CDC监听器(用Python + psycopg2实现),它不消费数据,只监听 INSERT/UPDATE/DELETE 事件;
  • 当监听到 suppliers 表中 rating 字段被更新为 'D' (严重风险)时,监听器不把它写入另一个表,而是直接向一个内部的“Agent事件总线”(我们用Redis Streams实现)发布一条结构化事件:
    {
      "event_type": "SUPPLIER_RATING_CHANGED",
      "source_entity": "suppliers",
      "primary_key": "SUP-2024-001",
      "old_value": "C",
      "new_value": "D",
      "timestamp": "2024-07-15T14:23:45Z",
      "business_impact": "HIGH"
    }
    
  • 所有订阅了该事件类型的Agent(比如“供应链风险评估Agent”、“采购建议Agent”)都会实时收到这条消息,并触发各自的响应逻辑:前者可能立即生成一份《高风险供应商应急评估报告》,后者则可能暂停向该供应商发起新的采购申请。

注意:这里的关键在于,事件的payload是高度业务化的,而不是原始的二进制WAL日志。 business_impact: "HIGH" 这个字段,是我们根据 rating 变化的幅度、供应商的采购占比、当前在途订单金额等多个维度,由一个独立的“业务规则引擎”实时计算出来的。这确保了下游Agent接收到的,是可直接用于决策的“信号”,而不是需要二次加工的“噪音”。

3. 核心细节解析与实操要点:从理论到落地的七道关卡

3.1 关卡一:数据库语义层的“考古式”重建

这是整个项目里最耗时、也最容易被低估的环节。很多团队以为,只要把数据库的 INFORMATION_SCHEMA 导出来,丢给LLM让它自己学习就行。我必须说,这是最大的误区。数据库的物理结构,和业务人员心中的逻辑结构,中间隔着一条马里亚纳海沟。

我们采用的方法,叫“三源印证法”:

  • 源一:数据库自身 。我们编写了一个SQL脚本(兼容主流数据库),它不只查表名和字段名,还深度分析:
    • 外键约束的 ON UPDATE/ON DELETE 行为,这往往暗示了业务上的强弱依赖关系(如 orders 表的 customer_id 外键设为 ON DELETE CASCADE ,说明客户删除必然导致其历史订单作废,这是强业务耦合);
    • 索引的 INCLUDE 列,这些列通常是高频查询的“覆盖列”,暗示了该组合查询的业务重要性;
    • 字段注释( COMMENT ON COLUMN )和表注释,这是DBA留下的最直接的业务线索。
  • 源二:遗留文档 。我们收集了所有能找到的文档:十年前的《XX系统需求规格说明书》、五年前的《数据仓库ETL开发手册》、甚至运维同事的个人Wiki笔记。重点不是看文字,而是看里面反复出现的术语、缩写、以及被特别标注为“注意:此字段含义特殊”的段落。
  • 源三:一线业务人员 。我们不是开大会,而是进行“影子观察”(Shadowing)。安排工程师跟着销售总监开一天的晨会,记录他提到的所有指标(如“新客首单转化率”、“老客复购周期”),然后立刻回到数据库里,用 pg_search 工具(PostgreSQL插件)搜索包含这些词的字段名、注释、甚至存储过程代码。有一次,我们发现销售总监口中的“黄金客户”,在数据库里对应的是 customers.segment_id IN (1, 3, 5) ,而 segment_id 字段的注释里只写了“客户分群ID”,没有任何业务含义说明。这个发现,直接让我们修正了整个客户画像模块的语义映射。

最终,我们产出的不是一张ER图,而是一份《业务语义词典》(Business Semantic Glossary),它包含:

  • 每个核心实体的业务定义、生命周期、关键属性;
  • 每个关键属性的业务计算逻辑(如“客户健康度”= 0.3 * (最近30天登录次数) + 0.4 * (最近90天订单总额/平均订单额) + 0.3 * (客服满意度评分) );
  • 实体间关系的业务规则(如“一个客户可以有多个销售代表,但同一时间只能有一个主销售代表”)。

这份词典,是后续所有LLM提示工程、Agent编排、权限控制的唯一真理来源。它必须由DBA、数据架构师、业务分析师三方共同签字确认。

3.2 关卡二:LLM选型:别迷信“最大参数”,要信“最熟方言”

市面上的LLM太多了,从GPT-4到Claude 3,再到国内的Qwen、GLM,参数规模一个比一个吓人。但在企业数据库知识系统这个场景里, 模型的“大小”远不如它的“适配度”重要

我们做过一轮严格的AB测试,针对同一个复杂查询:“对比分析2023年与2024年Q1,华东区各城市‘高端影像设备’品类的销售增长率、平均客单价、以及客户复购率的变化趋势,并找出增长最快的三个城市及其驱动因素。”

测试结果非常反直觉:

  • GPT-4 Turbo(128K上下文):能生成语法正确的SQL,但把“高端影像设备”的品类编码 CAT-IM-001 错写成了 CAT-MI-001 (字母顺序颠倒),导致查询结果为空;
  • Claude 3 Opus:对时间维度的理解极准,但把“客户复购率”的计算逻辑,错误地套用了零售业的通用公式( repeat_customers / total_customers ),而忽略了医疗行业的特殊性——他们的“复购”是指同一客户在不同科室购买不同设备,需要按 customer_id + department_id 去重;
  • Qwen2-72B-Instruct(中文版):在理解“华东区”这个地理概念上表现最好,因为它训练数据里包含了大量中国行政区划的官方表述,能准确识别出 city_name IN ('Shanghai', 'Nanjing', 'Hangzhou', 'Hefei') ,而其他模型常把“合肥”漏掉。

最终,我们选择了 混合模型策略

  • 主模型(Orchestrator) :Qwen2-72B-Instruct。它负责整体意图识别、查询路径规划、多步骤协调。选择它的核心原因是:它对中文业务术语、中国地理、行业缩写(如“DR”、“CT”、“MRI”)的理解,是目前所有开源模型里最扎实的。我们还用自己积累的2000+条医疗、制造行业的真实查询-SQL对,对它进行了LoRA微调,重点强化其对 fiscal_year sales_channel 等字段的敏感度。
  • 专项模型(Specialist) :一个轻量级的、仅1.5B参数的微调模型,专门负责“SQL生成”。它的训练数据,全部来自我们自己数据库的历史慢查询日志(Slow Query Log)和DBA的手动优化记录。它不追求通用性,只求在我们的127张核心表上,生成100%语法正确、且能命中最优索引的SQL。它的输出,永远只是一个 SELECT 语句,不带任何解释。

实操心得:不要试图用一个“全能模型”搞定所有事。把LLM当成一个“指挥官+特种兵”的组合。指挥官负责战略(理解问题、拆解步骤),特种兵负责战术(精准执行某一个动作)。这样,既保证了灵活性,又守住了底线——SQL的正确性。

3.3 关卡三:安全与权限:不是“加个防火墙”,而是“重写访问契约”

把LLM接入生产数据库,安全是悬在所有人头上的达摩克利斯之剑。常见的错误做法是:在LLM和数据库之间加一个API网关,然后用RBAC(基于角色的访问控制)去限制。这在理论上没错,但实操中会遇到一个根本性矛盾: LLM的查询是动态生成的,而RBAC的权限是静态定义的

比如,一个销售代表的角色,按RBAC规则,只能查看自己名下的客户数据。但如果LLM生成的SQL是:

SELECT c.name, c.phone, o.total_amount 
FROM customers c 
JOIN orders o ON c.id = o.customer_id 
WHERE c.sales_rep_id = 'SR-123' AND o.status = 'shipped';

这个SQL本身是合法的,但它通过 JOIN 操作,把 orders 表里本不该让该销售代表看到的 total_amount (可能涉及价格谈判机密)给“带”出来了。

我们的解决方案,是推行 动态数据掩码(Dynamic Data Masking, DDM)与查询重写(Query Rewriting)双轨制

  • DDM层 :在数据库驱动层面(如JDBC连接串)就启用。我们为每个数据库用户(对应一个业务角色)配置了精细的掩码规则。例如,对 sales_rep 角色, orders.total_amount 字段的掩码规则是: CASE WHEN user_role = 'sales_rep' THEN ROUND(total_amount * 0.95, 2) ELSE total_amount END 。这意味着,无论LLM生成什么SQL,只要它查 orders 表,拿到的 total_amount 永远是打了95折的近似值,真实金额对它是不可见的。
  • 查询重写层 :在LLM生成SQL之后、提交给数据库之前,由一个独立的“SQL净化器”(SQL Sanitizer)模块进行拦截。它会:
    1. 解析SQL AST(抽象语法树),识别出所有 SELECT 子句中的字段;
    2. 根据当前用户的业务角色,查询《业务语义词典》中该字段的“可见性策略”;
    3. 如果某个字段(如 customers.credit_score )被标记为“仅限风控部门可见”,而当前用户是销售代表,则自动将该字段从 SELECT 列表中移除,并在返回结果中添加一个占位符字段 credit_score: 'ACCESS_DENIED'
    4. WHERE 条件进行加固,自动追加 AND sales_rep_id = 'SR-123' (即使原SQL里没写),确保数据隔离。

这个双轨制,把安全控制点从“网络边界”下沉到了“数据访问的每一行、每一列”,而且是动态的、基于业务语义的,而不是僵硬的表级权限。

3.4 关卡四:性能与稳定性:当LLM成为“最挑剔的SQL客户端”

LLM对数据库的访问模式,和传统应用截然不同。它不是稳定的、可预测的连接池,而是一个“间歇性爆发”的查询源。它可能连续几秒内发出10个复杂的、带多层嵌套子查询的请求,然后又空闲一分钟。这对数据库的连接管理、内存分配、执行计划缓存,都是巨大挑战。

我们遇到过最惊险的一次事故:在一个POC演示中,LLM为了回答一个关于“跨季度销售趋势”的问题,自动生成了7个相互关联的 WITH RECURSIVE CTE查询,每个都扫描了数千万行的销售明细表。数据库的CPU瞬间飙到98%,连接数打满,连DBA的监控连接都被挤掉了。

解决这个问题,我们建立了 三级熔断机制

  • 第一级:LLM侧的“查询预算”(Query Budget) 。我们在LLM的提示词(System Prompt)里,明确写入了硬性约束:

    “你是一个严谨的数据库分析师。你生成的每一条SQL,必须满足以下条件:1) 最多包含2个JOIN;2) 子查询层级不超过2层;3) WHERE条件中必须包含至少一个高选择性索引字段(如 order_date , customer_id );4) 如果无法在10秒内生成符合上述条件的SQL,请明确告知用户‘该问题需要更详细的业务背景,建议拆分为子问题’。” 这个约束,不是靠模型“自觉”,而是通过一个轻量级的“SQL语法检查器”(用ANTLR4写的)在生成后实时校验。不合规的SQL,直接被拒绝执行。

  • 第二级:数据库侧的“资源组”(Resource Group) 。我们在PostgreSQL中创建了一个专用的 ai_analytics 资源组,为其分配了独立的 work_mem (64MB)、 maintenance_work_mem (256MB),并设置了 statement_timeout = 30s lock_timeout = 5s 。所有来自LLM的连接,都强制绑定到这个资源组。这意味着,即使一个查询真的卡住了,它也只会影响自己的资源槽,不会拖垮整个数据库。
  • 第三级:应用侧的“查询队列”(Query Queue) 。我们没有让LLM的请求直接打到数据库,而是先发到一个基于RabbitMQ的优先级队列。队列的消费者(Worker)会根据查询的预估成本(通过 EXPLAIN (FORMAT JSON) 提前估算)进行分级调度:低成本查询(<100ms)直通,中成本查询(100ms-5s)进入短队列,高成本查询(>5s)则被放入一个“人工审核队列”,由DBA在后台手动优化后再放行。

这套机制,让我们的系统在峰值QPS达到120时,数据库的平均响应时间依然稳定在320ms以内,从未发生过雪崩。

3.5 关卡五:可观测性:给LLM的每一次“思考”装上黑匣子

当一个自然语言问题最终变成了一张报表,中间经历了什么?LLM是如何理解“华东区”的?它为什么选择了 JOIN 而不是 EXISTS ?它有没有因为某个字段注释模糊而做出了错误假设?这些问题,如果无法追溯,系统就永远是个黑箱,出了问题,没人能负责。

我们构建了一套完整的“LLM决策溯源”(LLM Decision Provenance)体系,它记录的不是原始日志,而是 结构化的、可查询的决策链

每一轮完整的问答,系统会生成一个唯一的 trace_id ,并记录以下关键节点:

  • Node 1: 原始输入 :用户的问题原文、时间戳、用户ID(脱敏);
  • Node 2: 意图解析 :LLM识别出的原子意图列表,每个意图附带置信度分数;
  • Node 3: 上下文注入 :本次查询所加载的语义桥接层元数据片段(如 branches.region_code 的值列表、 returns.return_reason 的分类树);
  • Node 4: SQL生成 :LLM生成的原始SQL、经“SQL净化器”重写后的最终SQL、以及重写前后的差异对比(Diff);
  • Node 5: 执行摘要 :数据库返回的行数、执行时间、使用的索引名称、是否触发了顺序扫描(Seq Scan);
  • Node 6: 结果渲染 :LLM将原始数据转换为自然语言结论的过程(如“上海分公司退货率上升20%,主要驱动因素是‘物流问题’类别中的‘包装破损’子类,占比达65%”)。

所有这些节点,都以JSON格式,写入一个专用的 llm_traces 表。这个表本身也被纳入了我们的语义桥接层,业务分析师可以直接用自然语言问:“查一下上周所有被重写的SQL”,系统就会返回一个清晰的列表。

一个真实的避坑技巧:我们最初把所有trace数据都写入Elasticsearch,方便全文检索。但很快发现,ES的写入延迟和一致性问题,导致trace记录经常比实际查询慢几秒,失去了“实时诊断”的价值。最后我们果断切回了PostgreSQL,用 INSERT ... ON CONFLICT DO NOTHING 保证幂等,并用物化视图(Materialized View)定期聚合统计,既保证了强一致性,又兼顾了查询性能。

4. 实操过程与核心环节实现:从零搭建一个可运行的Demo

4.1 环境准备:最小可行的“知识系统”骨架

为了让你能立刻上手,我提供一个可以在本地Mac或Linux上15分钟内搭起来的最小可行Demo。它不追求功能完整,但完整呈现了本文所述的核心思想:语义桥接、MCP式上下文注入、A2A事件触发。

所需工具

  • 数据库:PostgreSQL 15+(推荐用Docker,一行命令搞定: docker run -d --name pg-ai -e POSTGRES_PASSWORD=ai123 -p 5432:5432 -v $(pwd)/pgdata:/var/lib/postgresql/data postgres:15
  • LLM:Ollama + Qwen2:1.5b(轻量级,适合本地跑) ollama run qwen2:1.5b
  • Python:3.10+,安装必要库: pip install psycopg2-binary redis python-dotenv

第一步:初始化演示数据库 我们创建一个极简的“销售数据”库,只包含3张表,但足以演示核心逻辑:

-- 创建数据库
CREATE DATABASE ai_knowledge_demo;

-- 切换到新库
\c ai_knowledge_demo

-- 销售员表
CREATE TABLE sales_reps (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  region VARCHAR(50) NOT NULL,
  hire_date DATE NOT NULL
);

-- 客户表
CREATE TABLE customers (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  region VARCHAR(50) NOT NULL,
  sales_rep_id INTEGER REFERENCES sales_reps(id)
);

-- 订单表
CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  customer_id INTEGER REFERENCES customers(id),
  amount NUMERIC(10,2) NOT NULL,
  order_date DATE NOT NULL,
  status VARCHAR(20) DEFAULT 'pending'
);

-- 插入一些演示数据
INSERT INTO sales_reps (name, region, hire_date) VALUES 
('张三', '华东', '2023-01-15'),
('李四', '华北', '2023-03-22');

INSERT INTO customers (name, region, sales_rep_id) VALUES 
('上海科技有限公司', '华东', 1),
('北京创新研究院', '华北', 2);

INSERT INTO orders (customer_id, amount, order_date, status) VALUES 
(1, 15000.00, '2024-06-01', 'completed'),
(1, 22000.00, '2024-06-15', 'completed'),
(2, 8500.00, '2024-06-10', 'completed');

第二步:构建语义桥接层元数据 我们不搞复杂的配置文件,就用一个简单的Python字典 semantic_layer.py 来模拟:

# semantic_layer.py
SEMANTIC_LAYER = {
    "entities": {
        "sales_rep": {
            "table": "sales_reps",
            "attributes": {
                "name": {"type": "string", "description": "销售员姓名"},
                "region": {"type": "string", "description": "所属大区", "values": ["华东", "华北", "华南"]},
                "hire_date": {"type": "date", "description": "入职日期"}
            }
        },
        "customer": {
            "table": "customers",
            "attributes": {
                "name": {"type": "string", "description": "客户名称"},
                "region": {"type": "string", "description": "客户所在大区", "values": ["华东", "华北", "华南"]},
                "sales_rep_name": {"type": "string", "description": "所属销售员姓名", "source": "sales_reps.name"}
            }
        },
        "order": {
            "table": "orders",
            "attributes": {
                "amount": {"type": "numeric", "description": "订单金额"},
                "order_date": {"type": "date", "description": "订单日期"},
                "status": {"type": "string", "description": "订单状态", "values": ["pending", "completed", "cancelled"]}
            }
        }
    },
    "relationships": {
        "customer_to_sales_rep": {
            "from": "customer",
            "to": "sales_rep",
            "type": "many-to-one",
            "join_condition": "customers.sales_rep_id = sales_reps.id"
        }
    }
}

第三步:实现MCP式上下文注入器 创建 context_injector.py ,它接收用户问题,返回精炼的上下文片段:

# context_injector.py
import re
from semantic_layer import SEMANTIC_LAYER

def inject_context(user_query: str) -> dict:
    """
    基于用户问题,提取最相关的语义上下文
    """
    context = {"entities": {}, "relationships": {}}
    
    # 简单的关键词匹配(实际项目中会用更复杂的NLU)
    if "华东" in user_query or "上海" in user_query:
        context["entities"]["sales_rep"] = {
            "region": SEMANTIC_LAYER["entities"]["sales_rep"]["attributes"]["region"]["values"]
        }
        context["entities"]["customer"] = {
            "region": SEMANTIC_LAYER["entities"]["customer"]["attributes"]["region"]["values"]
        }
    
    if "订单" in user_query or "amount" in user_query.lower():
        context["entities"]["order"] = {
            "status_values": SEMANTIC_LAYER["entities"]["order"]["attributes"]["status"]["values"],
            "date_hint": "order_date字段用于时间筛选"
        }
    
    # 如果问题涉及“谁”,则注入销售员信息
    if "谁" in user_query and ("销售" in user_query or "rep" in user_query.lower()):
        context["entities"]["sales_rep"] = {
            "name": "销售员姓名",
            "region": "销售员所属大区"
        }
    
    return context

# 测试
if __name__ == "__main__":
    test_query = "华东区销售额最高的销售员是谁?"
    print(inject_context(test_query))

运行它,你会得到:

{
  "entities": {
    "sales_rep": {"region": ["华东", "华北", "华南"], "name": "销售员姓名"},
    "customer": {"region": ["华东", "华北", "华南"]},
    "order": {"status_values": ["pending", "completed", "cancelled"], "date_hint": "order_date字段用于时间筛选"}
  }
}

这就是MCP的核心: 用最少的、最相关的业务信息,为LLM提供决策依据

4.2 核心环节:LLM提示工程与SQL生成

现在,我们把上下文注入器和LLM连接起来。创建 ai_query_engine.py

# ai_query_engine.py
import os
import psycopg2
from ollama import Client
from context_injector import inject_context
from semantic_layer import SEMANTIC_LAYER

# 初始化Ollama客户端
ollama_client = Client(host='http://localhost:11434')

def generate_sql(user_query: str, context: dict) -> str:
    """
    使用Ollama生成SQL,Prompt中已嵌入上下文
    """
    # 构建系统提示词(System Prompt)
    system_prompt = f"""
你是一个专业的PostgreSQL数据库分析师,专门为销售业务部门服务。
你的任务是,根据用户的问题,生成一条精确、高效、且符合业务语义的SQL查询。
请严格遵守以下规则:
1. 只生成一条SELECT语句,不要有任何解释、不要有```sql```代码块标记。
2. 必须使用以下表结构:
   - sales_reps表:包含id, name, region, hire_date字段。
   - customers表:包含id, name, region, sales_rep_id字段。
   - orders表:包含id, customer_id, amount, order_date, status字段。
3. 你已知的业务上下文:
   - '华东'、'华北'、'华南'是region字段的有效值。
   - 'completed'是orders.status的常用值。
   -
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐