PostgreSQL码农集散地

进阶课 15 数据库集成DeepSeek

PolarDB 进阶课程15 集成DeepSeek

穷鬼玩PolarDB RAC一写多读集群系列已经写了几篇:

  • 《在Docker容器中用loop设备模拟共享存储》
  • 《如何搭建PolarDB容灾(Standby)节点》
  • 《共享存储在线扩容》
  • 《计算节点 Switchover》
  • 《在线备份》
  • 《在线归档》
  • 《实时归档》
  • 《时间点恢复(PITR)》
  • 《读写分离》
  • 《主机全毁, 只剩共享存储的PolarDB还有救吗?》
  • 《激活容灾(Standby)节点》
  • 《将“共享存储实例”转换为“本地存储实例”》
  • 《将“本地存储实例”转换为“共享存储实例”》
  • 《升级vector插件》
  • 《使用图数据库插件AGE》

本篇文章介绍一下如何在PolarDB数据库中接入私有化大模型服务? 实验环境依赖 《在Docker容器中用loop设备模拟共享存储》 , 如果没有环境, 请自行参考以上文章搭建环境.

同时还涉及几个插件如下, 安装插件时, 需要在PolarDB集群的所有机器上都进行安装, 顺序建议先PolarDB Standby, 然后是所有的RO节点, 然后是RW节点.

  • https://github.com/pramsey/pgsql-http
  • https://github.com/pramsey/pgsql-openai

数据库和大模型结合, 可以实现很多场景的应用, 例如

  • 社交场景、电商用户评价的情感词实时计算(好评、中评、差评).
  • 将私有化文本存储在数据库中, 并计算其向量值建立向量索引, 在基于问题生成文本时使用向量索引快速检索与问题相关的私有文本片段, 实现RAG
  • 将会话历史存储于数据库中, 并计算其向量值建立向量索引, 在调用大语言模型与用户进行交互时, 从历史会话中搜索相关内容, 作为上下文信息辅助文本生成, 实现大模型外脑记忆
  • 通过对当前数据库实例元数据的理解, 帮助开发人员按需求自动生成SQL, 简化开发者的工作
  • 通过对当前数据库实例统计信息的理解, 帮助开发人员优化数据库, 例如发现不合适的全表扫描, 推荐使用索引

但是, 有些情况你可能无法使用云端大语言模型服务(例如 openai), 例如如下情况

  • 大语言模型云服务在远程, 你的业务可能与之网络不通, 无法调用
  • 敏感数据可能泄露给openai服务, 所以不允许使用openai服务
  • openai可能没有针对你的数据进行训练, 不适合你的场景, 你需要一个微调过的模型

此时, 你可能需要自建私有openai服务, 比较廉价(亲民)的方案是使用 apple ARM芯片的Mac + ollam.

下面将使用例子简单分享一下, 使用openai+http插件让PolarDB PostgreSQL快捷使用openai/自建openai服务 , 从而可实现:

  • 远程openai服务 -> 数据库内 文本 - to - embedding vector
  • 远程openai服务 -> 数据库内 文本实时分析 (例如 情感词分类)
  • 本地/自建 llm 服务 -> 数据库内 文本 - to - embedding vector
  • 本地/自建 llm 服务 -> 数据库内 文本实时分析 (例如 情感词分类)
  • 本地/自建 llm 服务 -> 通过对当前数据库实例元数据的理解, 帮助开发人员按需求自动生成SQL, 简化开发者的工作
  • 本地/自建 llm 服务 -> 通过对当前数据库实例统计信息的理解, 帮助开发人员优化数据库, 例如发现不合适的全表扫描, 推荐使用索引

使用到如下组件

  • apple ARM芯片的Mac  (要求内存大于使用的模型的大小, 例如16G内存建议跑14b及以下模型)
  • ollama on Mac
  • large language model on Mac  (推理模型 及 embedding模型)
  • PolarDB postgresql in Docker
  • openai 插件 for PolarDB postgresql
  • http 插件 for PolarDB postgresql

DEMO

b站视频链接

Youtube视频链接

1、ollama 部署很简单, 参考下文

  • 《AI大模型+全文检索+tf-idf+向量数据库+我的文章 系列之1 - 低配macbook成功跑AI大模型LLama3:8b, 感谢ollama》
  • 《OLLAMA 环境变量/高级参数配置 : 监听 , 内存释放窗口 , 常用设置举例》
    • 建议设置 launchctl setenv OLLAMA_KEEP_ALIVE "-1" ; launchctl setenv OLLAMA_HOST "0.0.0.0"

拉取几个模型试试

$ ollama pull deepseek-r1:7b   

$ ollama pull mxbai-embed-large:latest  

查看当前已有的模型

$ ollama list  
NAME                        ID              SIZE      MODIFIED       
deepseek-r1:7b              0a8c26691023    4.7 GB    15 minutes ago    
deepseek-r1:14b             ea35dfe18182    9.0 GB    2 weeks ago       
mxbai-embed-large:latest    468836162de7    669 MB    2 months ago   

2、设置环境变量, 让ollama监听0.0.0.0地址, 位于容器内的PolarDB数据库可以访问到宿主机上或远程的ollama服务

Open your terminal.  

Set the environment variable using launchctl:  
  launchctl setenv OLLAMA_HOST "0.0.0.0"

Restart the Ollama application to apply the changes.
  ollama serve

或者直接使用命令行启动ollama服务, 并设置环境变量:

  OLLAMA_HOST=0.0.0.0:11434 OLLAMA_KEEP_ALIVE=-1 ollama serve   

确认ollama 11434端口监听正常:

U-4G77XXWF-1921:~ digoal$ nc -v 127.0.0.1 11434  
Connection to 127.0.0.1 port 11434 [tcp/*] succeeded!  
^C  
U-4G77XXWF-1921:~ digoal$ nc -v xxx.xxx.xxx.xxx 11434  
Connection to xxx.xxx.xxx.xxx port 11434 [tcp/*] succeeded!  
^C  

3、启动PolarDB 数据库, 如果你还没有部署, 请参看:

  • 《在Docker容器中用loop设备模拟共享存储》

4、进入容器编译插件.

编译插件时, 需要在PolarDB集群的所有机器上都进行编译, 顺序建议先PolarDB Standby, 然后是所有的RO节点, 然后是RW节点.

创建插件则仅需在RW节点进行.

如果涉及到postgresql.conf配置文件的设置, 则同样需要在PolarDB集群的所有机器上都进行编译, 顺序建议先PolarDB Standby, 然后是所有的RO节点, 然后是RW节点.

http插件

cd /data  
git clone --depth 1 https://github.com/pramsey/pgsql-http  
cd /data/pgsql-http  
USE_PGXS=1 make install  

openai插件

cd /data  
git clone --depth 1 https://github.com/pramsey/pgsql-openai  
cd /data/pgsql-openai  
USE_PGXS=1 make install  

5、安装http和openai插件

psql  

create extension http ;  
create extension openai ;  
postgres=# \dx  
                                              List of installed extensions  
        Name         | Version |   Schema   |                                Description                                   
---------------------+---------+------------+----------------------------------------------------------------------------  
 http                | 1.6     | public     | HTTP client for PostgreSQL, allows web page retrieval inside the database.  
 openai              | 1.0     | public     | OpenAI client.  
 plpgsql             | 1.0     | pg_catalog | PL/pgSQL procedural language  
 polar_feature_utils | 1.0     | pg_catalog | PolarDB feature utilization  
(4 rows)  

openai有3个接口函数, 分别用于查询当前支持哪些模型, 基于提示词生成文本, 基于文本生成向量

  • openai.models() returns setof record. Returns a listing of the AI models available via the API.
  • openai.prompt(context text, query text, [model text]) returns text. Set up context and then pass in a starting prompt for the model.
  • openai.vector(query text, [model text]) returns text. For use with embedding models ( Ollama, OpenAI ) only, returns a JSON-formatted float array suitable for direct input into pgvector. Using non-embedding models to generate embeddings will result in extremely poor search results.

openai函数接口详情

postgres=# \df openai.*  
                                                                          List of functions
 Schema |  Name  |                                Result data type                                 |                   Argument data types                    | Type  
--------+--------+---------------------------------------------------------------------------------+----------------------------------------------------------+------  
 openai | models | TABLE(id text, object text, created timestamp without time zone, owned_by text) |                                                          | func  
 openai | prompt | text                                                                            | context text, prompt text, model text DEFAULT NULL::text | func  
 openai | vector | text                                                                            | input text, model text DEFAULT NULL::text                | func  
(3 rows)  

注意http插件可以设置http请求超时, 默认可能是5秒, 如果大模型的硬件较差, 响应可能会超时报错, 你可以使用如下函数进行设置

SELECT http_set_curlopt('CURLOPT_TIMEOUT', '100');  -- 设置为100秒
-- 退出会话后需要重新设置.  

试一试这几个openai接口函数

首先需要设置几个会话变量(注意 提示词模型选哪个? 文本转向量的模型选哪个? 取决于你下载了哪些模型, 同时还取决于你的机器配置(GPU和内存)以及你期望的响应速度、生成效果等.), 让数据库知道去哪里调用大语言模型. 或者你也可以把会话变量设置到PolarDB的postgresql.conf参数文件、user 配置、database 配置中.

psql  

-- 设置当前会话  
SET openai.api_uri = 'http://192.168.64.1:11434/v1/';  -- 这个地址可以替换成你环境中的ollama监听的地址  
SET openai.api_key = 'none';  
SET openai.prompt_model = 'deepseek-r1:7b';  
SET openai.embedding_model = 'mxbai-embed-large';  -- 虽然prompt_model也可以用来做embedding, 就是太大了, 太耗费算力.  所以换小模型做.  

or  

-- 设置到某个database到配置中, 下次连接这个数据库时会自动配置这些项  
alter database postgres set openai.api_uri = 'http://192.168.64.1:11434/v1/';  
alter database postgres SET openai.api_key = 'none';  
alter database postgres SET openai.prompt_model = 'deepseek-r1:7b';  
alter database postgres SET openai.embedding_model = 'mxbai-embed-large';  

postgres=# select * from pg_db_role_setting ;  
 setdatabase | setrole |                                                                  setconfig  
-------------+---------+----------------------------------------------------------------------------------------------------------------------------------------------  
       5     |       0 | {openai.api_uri=http://192.168.64.1:11434/v1/,openai.api_key=none,openai.prompt_model=deepseek-r1:7b,openai.embedding_model=mxbai-embed-large}  
(1 row)  

然后就可以调用openai的接口函数了

-- 查询当前支持哪些模型  
postgres=# SELECT * FROM openai.models();  
            id            | object |       created       | owned_by 
--------------------------+--------+---------------------+----------
 deepseek-r1:7b           | model  | 2025-02-07 02:53:02 | library
 deepseek-r1:14b          | model  | 2025-01-21 03:49:44 | library
 mxbai-embed-large:latest | model  | 2024-11-21 06:02:27 | library
(3 rows)

-- 将文本转换为向量  
SELECT openai.vector('A lovely orange pumpkin pie recipe.');  

[-0.021690376, 0.003092674, -0.00023757249, 0.0059805233,  
 0.0024175171, 0.013349159, 0.006348481, 0.016774092,  
 0.0051014116, 0.026803626, 0.015113969, 0.0031985058,  
  ...  
 -0.020051343, 0.006571748, 0.008234819, 0.010086719,  
 -0.0071006618, -0.020877795, -0.022467814, 0.010012546,  
 0.0008801813, -0.0006236545, 0.016922941, -0.011781357]  

-- 基于提示词生成文本  
SELECT openai.prompt(  
'You are an advanced sentiment analysis model. Read the given  
   feedback text carefully and classify it as one of the  
   following sentiments only: "positive", "neutral", or  
   "negative". Respond with exactly one of these words  
   and no others, using lowercase and no punctuation'
,  
'I enjoyed the setting and the service and the bisque was  
   great.'
 );  

  prompt  
----------  
 positive  

目前ollama api不支持跳过deepseek-r1 <think>部分的输出, 可以在数据中处理一下

  • https://github.com/deepseek-ai/DeepSeek-R1/issues/23
select btrim(split_part(  
  openai.prompt(  
'You are an advanced sentiment analysis model. Read the given  
   feedback text carefully and classify it as one of the  
   following sentiments only: "positive", "neutral", or  
   "negative". Respond with exactly one of these words  
   and no others, using lowercase and no punctuation'
,  
'I enjoyed the setting and the service and the bisque was  
   great.'
 )  
, '</think>', 2)
, "char"(10));   

6、设计一个表, 存储客户反馈, 以及对应的情感词

CREATE TABLE feedback (  
    feedback text, -- freeform comments from the customer  
    sentiment text -- positive/neutral/negative from the LLM  
    );  

创建一个触发器函数, 在插入以上表时自动触发, 调用大语言模型, 生成feedback字段内容对应的情感词(正面、中性、反面)

--  
-- Step 1: Create the trigger function
--  
CREATE OR REPLACE FUNCTION analyze_sentiment() RETURNS TRIGGER AS $$  
DECLARE  
    response TEXT;  
BEGIN  
    perform http_set_curlopt('CURLOPT_TIMEOUT', '600');  -- 设置 curl 调用超时时间为600秒.  
    -- Use openai.prompt to classify the sentiment as positive, neutral, or negative  
    response := openai.prompt(  
'You are an advanced sentiment analysis model. Read the given feedback text carefully and classify it as one of the following sentiments only: "positive", "neutral", or "negative". Respond with exactly one of these words and no others, using lowercase and no punctuation',  
        NEW.feedback  
    );  

    -- Set the sentiment field based on the model's response  
    -- NEW.sentiment := response;
    if pg_catalog.position(response, '
</think>') > 0 then 
      NEW.sentiment := btrim(split_part( response , '
</think>', 2) , "char"(10)) ;
    end if;

    RETURN NEW;  
END;  
$$ LANGUAGE '
plpgsql';  

--  
-- Step 2: Create the trigger to execute the function before each INSERT or UPDATE  
--  
CREATE TRIGGER set_sentiment  
    BEFORE INSERT OR UPDATE ON feedback  
    FOR EACH ROW  
    EXECUTE FUNCTION analyze_sentiment();  

插入数据, 并查询调用大模型计算情感词的效果如下

INSERT INTO feedback (feedback)  
    VALUES  
        ('The food was not well cooked and the service was slow.'),  
        ('I loved the bisque but the flan was a little too mushy.'),  
        ('This was a wonderful dining experience, and I would come again,  
          even though there was a spider in the bathroom.'
);  

postgres=# select * from feedback ;  
                            feedback                             | sentiment  
-----------------------------------------------------------------+-----------  
 The food was not well cooked and the service was slow.          | negative  
 I loved the bisque but the flan was a little too mushy.         | negative  
 This was a wonderful dining experience, and I would come again,+| positive  
           even though there was a spider in the bathroom.       |  
(3 rows)  

postgres=# INSERT INTO feedback (feedback) values ('我讨厌吸烟, 危害健康');  
INSERT 0 1  
postgres=# INSERT INTO feedback (feedback) values ('少量喝酒, 对身体可能有好处');  
INSERT 0 1  
postgres=# INSERT INTO feedback (feedback) values ('今天天气很不错, 非常适合跑步');  
INSERT 0 1  
postgres=# INSERT INTO feedback (feedback) values ('西湖的游客很多, 特别是国庆节的时候, 断桥都要被人踩断了');  
INSERT 0 1  
postgres=# select * from feedback ;  
                            feedback                             | sentiment  
-----------------------------------------------------------------+-----------  
 The food was not well cooked and the service was slow.          | negative  
 I loved the bisque but the flan was a little too mushy.         | negative  
 This was a wonderful dining experience, and I would come again,+| positive  
           even though there was a spider in the bathroom.       |  
 我讨厌吸烟, 危害健康                                            | negative  
 少量喝酒, 对身体可能有好处                                      | positive  
 今天天气很不错, 非常适合跑步                                    | positive  
 西湖的游客很多, 特别是国庆节的时候, 断桥都要被人踩断了          | negative  
(7 rows)  

在实际使用中, 可能在应用收到客户请求后塞到消息队列, 从消息队列获取并调用大模型计算情感词可能更好.  因为调用模型计算是较耗时的过程, 长期占用数据库链接、长事务有很多副作用.

7、生成SQL

SELECT openai.prompt(  
'无所不能的AI啊,我在使用PolarDB for PostgreSQL数据库时遇到一个问题, 请你帮忙写一下SQL来满足我的要求.',  
'如何能从表里面获知某个用户关注的用户以及他关注的用户又关注了哪些用户, 如此一直返回到第三层, 请问这个SQL应该怎么写呢? 下面是存储数据的表结构: create table tbl (id int primary key, userid int, ping_userid int);') ;  

 返回的效果取决于模型的能力, 下面看看我们用的deepseek-r1:7b r1蒸馏的qwen 7b小模型回答怎么样? 


为了从表tbl中找出某个用户及其关注的人以及被关注者的关注者,直到第三层,可以使用嵌套的子查询。以下是满足要求的SQL代码:

SELECTid, userid, ping_userid
FROM tbl 
WHERE
id = 1OR
userid IN ( 
SELECT ping_userid FROM tbl WHEREid = 1
) OR
userid IN (
SELECT ping_userid FROM tbl WHERE
    userid IN ( 
SELECT ping_userid FROM tbl WHEREid = 1
    )
);

步骤解析:

  1. 第一层(id=1):
    直接查找id为1的所有关注者,即userid。

  2. 第二层:
    查找那些关注了第一层中用户的人,即通过子查询获取这些用户的ping_userid作为userid。

  3. 第三层:
    再次查找那些关注了第二层中的用户的人,同样使用子查询获取他们的ping_userid作为userid。

这样,SQL会返回三层的结果:id=1的用户、被关注者以及被关注者的关注对象。如果需要调整层数或用户ID,只需修改相应的位置即可。


 下面是deepseek官网r1版本对这个问题的回答, 完全碾压r1蒸馏的7b小模型, 同时也碾压了chatGPT: 

要查询用户关注的层级关系直到第三层,可以使用PostgreSQL的递归CTE(公用表表达式)。以下是满足需求的SQL语句:

WITHRECURSIVE follow_chain AS (
SELECT
        userid AS start_user,
        ping_userid AS followed_user,
1ASlevel,
ARRAY[userid] AS visited_users  -- 防止循环
FROM tbl
WHERE userid = 目标用户ID-- 替换为你的用户ID
UNIONALL
SELECT
        fc.start_user,
        t.ping_userid,
        fc.level + 1,
        fc.visited_users || t.userid  -- 将当前用户加入路径
FROM follow_chain fc
JOIN tbl t ON fc.followed_user = t.userid
WHERE
        fc.level < 3-- 限制到第三层
ANDNOT t.userid = ANY(fc.visited_users)  -- 避免循环
)
SELECT
    start_user AS 初始用户,
    followed_user AS 被关注用户,
levelAS 关注层级
FROM follow_chain;

参数说明:

  • 目标用户ID:替换为你要查询的用户ID。
  • visited_users:用于记录访问路径,防止出现循环关注(如A→B→A)。

结果说明:

  • 初始用户:查询的起始用户。
  • 被关注用户:直接或间接关注的用户。
  • 关注层级:关注关系的层级(1=直接关注,2=间接第二层,3=间接第三层)。

示例:

假设用户1000的关注链如下:

1000 → 2000 (L1)
2000 → 3000 (L2)
3000 → 4000 (L3)

查询结果将包含:

初始用户 | 被关注用户 | 关注层级
---------------------------------
1000    | 2000       | 1
1000    | 3000       | 2
1000    | 4000       | 3

注意事项:

  • 如果某一层没有更多关注关系(如第三层用户未关注他人),该分支的递归将自动终止。
  • 通过visited_users字段避免循环(如A→B→A→B...)。

update: 20250213, chatgpt 采用了递归, 但是没有考虑回环图结构(如 a-b-a , a-b-c-a) .

chatGPT被deepseek-r1碾压:

要查询某个用户(假设其 userid 为 X)关注的用户,以及这些被关注用户的关注者,直到第三层,您可以使用递归查询(即公用表表达式,CTE)来实现。

假设您的表结构如下:

CREATETABLE tbl (
idSERIAL PRIMARY KEY,
    userid INT,
    ping_userid INT
);

其中,userid 表示关注者,ping_userid 表示被关注者。

以下是一个示例 SQL 查询,使用递归查询获取从用户 X 开始的关注关系,直到第三层:

WITHRECURSIVE FollowHierarchy AS (
-- 第1层:用户 X 关注的用户
SELECT
        t1.userid AS level1_userid,
        t1.ping_userid AS level2_userid,
NULL::INTAS level3_userid
FROM tbl t1
WHERE t1.userid = X

UNIONALL

-- 第2层:第1层用户关注的用户
SELECT
        t2.userid AS level1_userid,
        t2.ping_userid AS level2_userid,
NULL::INTAS level3_userid
FROM tbl t2
INNERJOIN FollowHierarchy fh ON t2.userid = fh.level2_userid

UNIONALL

-- 第3层:第2层用户关注的用户
SELECT
        t3.userid AS level1_userid,
        t3.ping_userid AS level2_userid,
        t3.ping_userid AS level3_userid
FROM tbl t3
INNERJOIN FollowHierarchy fh ON t3.userid = fh.level2_userid
)
SELECT * FROM FollowHierarchy;

在上述查询中:

  1. 第1层:从 tbl 表中获取用户 X 关注的用户。

  2. 第2层:递归地获取第1层用户关注的用户。

  3. 第3层:递归地获取第2层用户关注的用户。

请将 X 替换为您感兴趣的具体用户 ID。

此查询将返回一个包含三列的结果集:

  • level1_userid:用户 X。

  • level2_userid:用户 X 关注的用户。

  • level3_userid:用户 X 关注的用户的关注者。

请注意,递归查询可能会对性能产生影响,特别是在数据量较大的情况下。 建议在实际应用中根据数据量和性能需求进行优化。

参考

《穷鬼玩PolarDB RAC一写多读集群系列 | 在Docker容器中用loop设备模拟共享存储》

《穷鬼玩PolarDB RAC一写多读集群系列 | 如何搭建PolarDB容灾(Standby)节点》

《穷鬼玩PolarDB RAC一写多读集群系列 | 共享存储在线扩容》

《穷鬼玩PolarDB RAC一写多读集群系列 | 计算节点 Switchover》

《穷鬼玩PolarDB RAC一写多读集群系列 | 在线备份》

《穷鬼玩PolarDB RAC一写多读集群系列 | 在线归档》

《穷鬼玩PolarDB RAC一写多读集群系列 | 实时归档》

《穷鬼玩PolarDB RAC一写多读集群系列 | 时间点恢复(PITR)》

《穷鬼玩PolarDB RAC一写多读集群系列 | 读写分离》

《穷鬼玩PolarDB RAC一写多读集群系列 | 主机全毁, 只剩共享存储的PolarDB还有救吗?》

《穷鬼玩PolarDB RAC一写多读集群系列 | 激活容灾(Standby)节点》

《穷鬼玩PolarDB RAC一写多读集群系列 | 将“共享存储实例”转换为“本地存储实例”》

《穷鬼玩PolarDB RAC一写多读集群系列 | 将“本地存储实例”转换为“共享存储实例”》

《穷鬼玩PolarDB RAC一写多读集群系列 | 升级vector插件》

《穷鬼玩PolarDB RAC一写多读集群系列 | 使用图数据库插件AGE》

《openai+http插件让PostgreSQL快捷使用openai/自建openai服务》

https://github.com/ollama/ollama/tree/main/docs

《AI大模型+全文检索+tf-idf+向量数据库+我的文章 系列之1 - 低配macbook成功跑AI大模型LLama3:8b, 感谢ollama》

《OLLAMA 环境变量/高级参数配置 : 监听 , 内存释放窗口 , 常用设置举例》

https://api-docs.deepseek.com/zh-cn/