7、对话记忆(基于PostgreSQL)
But,Redis是本质上还是基于内存的,就算Redis有持久化逻辑,Redis重启之后数据还是会丢失的。最佳实践就是直接把对话存储到像MySQL类的数据库中,那么在这个项目中呢,我们就暂时不用MySQL去实现对话的持久化存储了,在这里我们使用PostgreSQL。
关于PostgreSQL:
-
PostgreSQL(通常简称为 Postgres)是一个功能强大、开源、对象-关系型数据库管理系统(ORDBMS)。它以稳定性、可扩展性、标准兼容性和丰富的功能著称,被广泛用于企业级应用、Web 后端、数据分析、GIS 系统等领域。
核心特点
1. 完全开源 & 免费
- 使用 PostgreSQL License(类似 MIT),允许自由使用、修改、分发,甚至用于商业产品,无版权费用。
2. 高度兼容 SQL 标准
- 支持 SQL:2016 大部分特性,包括窗口函数、CTE(公共表表达式)、JSON、全文搜索等。
- 被认为是最符合 SQL 标准的开源数据库之一。
3. 强大的数据类型支持
-
基础类型:整数、浮点、字符串、日期时间等
-
高级类型:
- JSON / JSONB(二进制优化的 JSON,支持索引和查询)
- 数组(如
TEXT[]) - 范围类型(如
int4range,tsrange) - 几何类型(点、线、多边形)
- 网络地址类型(
inet,cidr,macaddr) - 自定义类型(通过
CREATE TYPE)
4. 扩展性强
-
支持自定义函数(用 SQL、PL/pgSQL、Python、Perl、C 等编写)
-
支持
扩展插件
,例如:
PostGIS:地理空间数据处理(GIS)pg_trgm:模糊文本匹配uuid-ossp:生成 UUIDTimescaleDB:时序数据(基于 Postgres)
5. ACID 事务 & 并发控制
- 完整支持 ACID(原子性、一致性、隔离性、持久性)
- 使用 MVCC(多版本并发控制),读不阻塞写,写不阻塞读,高并发性能好。
6. 安全性高
- 细粒度的权限控制(表、列、行级安全)
- 支持 SSL/TLS 加密连接
- 多种认证方式(密码、LDAP、Kerberos、证书等)
- 通过
pg_hba.conf精确控制谁可以从哪里连接
7. 高可用与复制
- 支持流复制(Streaming Replication)
- 支持逻辑复制(Logical Replication,用于跨版本或选择性同步)
- 可搭配 Patroni、repmgr、pgpool-II 等实现自动故障转移
8. 活跃的社区与生态
- 拥有全球活跃的开发者和用户社区
- 云厂商广泛支持(AWS RDS/Aurora、Google Cloud SQL、Azure Database for PostgreSQL 等)
🗃️ 典型应用场景
场景 说明 Web 应用后端 Django、Ruby on Rails、Node.js 等框架首选数据库 地理信息系统(GIS) 通过 PostGIS 插件支持空间查询(如“附近 5 公里的商店”) 数据分析与 BI 支持窗口函数、聚合、物化视图,可替代部分数据仓库 微服务架构 每个服务可拥有独立数据库,保证解耦 时序数据 配合 TimescaleDB 插件高效存储监控、IoT 数据
而且PostgreSQL 可以高效存储和查询向量数据(是通过pgvector插件实现的),而且PostgreSQL的流行度已经超过了MySQL
这里我直接使用docker的方式直接运行一个PostgreSQL的容器,或者说你有云服务器的话,并且已经安装了宝塔面板的话,直接在软件商店就能安装PostgreSQL。
docker run --name pgvector-db \
-e POSTGRES_PASSWORD=E628L1..3K220 \
-e POSTGRES_USER=postgres \
-e POSTGRES_DB=postgres \
-p 5433:5432 \
-v ~/pgvector-data:/var/lib/postgresql/data \
-d \
pgvector/pgvector:pg16
这里的数据库名和密码以及用户名是在docker命令就设置好的
对于LangChain来说,也内置的基于SQL的对话历史解决方案,使用langchain_community.chat_message_histories包中的SQLChatMessageHistory可是实现基于SQL的对话历史持久化存储。
我们在该项目的根目录下新建一个message_history文件夹,并新建py文件sql_message_history.py
导入相关的包并编写如下代码:
# message_history/sql_message_history.py
from message_history.async_pg_history import AsyncPostgresChatMessageHistory
def get_session_history(session_id: str):
return AsyncPostgresChatMessageHistory(
session_id=session_id,
table_name="langchain_chat_messages",
)
这里我们使用SQLAlchemy,这是一个流行的 Python ORM 和数据库工具库)中用于异步数据库操作的模块。
注意,这里的AsyncPostgresChatMessageHistory是我自定义的一个异步类,那为什么要自定义呢?
目前 langchain_community.chat_message_histories.SQLChatMessageHistory 并不原生支持 SQLAlchemy 的异步引擎(如 AsyncEngine) —— 它的 async_mode=True 本质上是“在异步上下文中运行同步数据库操作”,并非真正的异步数据库驱动。
解决方案:自定义一个真正的异步 BaseChatMessageHistory
我们可以基于 SQLAlchemy Core + asyncpg,自己实现一个支持 PostgreSQL 异步读写 的消息历史类,完全兼容 LangChain 的异步接口(如 .aget_messages(), .aadd_messages())。
这段代码大家如果看不懂的话,直接复制粘贴就好(别忘了安装依赖sqlalchemy asyncpg):
# message_history/async_pg_history.py
import json
from typing import List, Optional
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import BaseMessage, messages_from_dict, messages_to_dict
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy import text
import config
# 创建全局异步引擎(推荐单例)
engine = create_async_engine(
config.DATABASE_URL,
echo=False, # 生产环境设为 False
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
)
# 初始化表结构(可单独运行一次)
async def init_chat_history_table(table_name: str = "langchain_chat_messages"):
async with engine.begin() as conn:
await conn.execute(text(f"""
CREATE TABLE IF NOT EXISTS {table_name} (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
message JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
"""))
await conn.execute(text(f"""
CREATE INDEX IF NOT EXISTS idx_session_id_{table_name}
ON {table_name} (session_id);
"""))
class AsyncPostgresChatMessageHistory(BaseChatMessageHistory):
def __init__(self, session_id: str, table_name: str = "langchain_chat_messages"):
if not session_id:
raise ValueError("session_id must be provided")
self.session_id = session_id
self.table_name = table_name
async def aget_messages(self) -> List[BaseMessage]:
async with AsyncSession(engine) as session:
result = await session.execute(
text(f"""
SELECT message
FROM {self.table_name}
WHERE session_id = :session_id
ORDER BY id ASC
"""),
{"session_id": self.session_id}
)
rows = result.fetchall()
# SQLAlchemy + asyncpg 自动将 JSONB 反序列化为 dict
message_dicts = [row[0] for row in rows] # row[0] is already a dict
return messages_from_dict(message_dicts)
async def aadd_messages(self, messages: List[BaseMessage]) -> None:
if not messages:
return
async with AsyncSession(engine) as session:
message_dicts = messages_to_dict(messages)
for msg in message_dicts:
# 🔧 关键修复:将 dict 序列化为 JSON 字符串
await session.execute(
text(f"""
INSERT INTO {self.table_name} (session_id, message)
VALUES (:session_id, :message)
"""),
{
"session_id": self.session_id,
"message": json.dumps(msg) # 传 str 而非 dict
}
)
await session.commit()
async def aclear(self) -> None:
"""异步清空当前 session 的所有消息"""
async with AsyncSession(engine) as session:
await session.execute(
text(f"DELETE FROM {self.table_name} WHERE session_id = :session_id"),
{"session_id": self.session_id}
)
await session.commit()
# 同步方法:提示用户使用异步版本
def get_messages(self) -> List[BaseMessage]:
raise NotImplementedError("Use 'aget_messages()' in async context.")
def add_messages(self, messages: List[BaseMessage]) -> None:
raise NotImplementedError("Use 'aadd_messages()' in async context.")
def clear(self) -> None:
raise NotImplementedError("Use 'aclear()' in async context.")
同时在配置文件中加上DATABASE_URL = "postgresql+asyncpg://user:password@localhost:5432/mydb"
8、异步数据库操作
那什么是异步数据库操作呢?
1、同步vs异步
- 同步:程序执行到数据库操作的时候会阻塞,直到数据库返回结果之后才继续执行下一行代码,但是在高并发场景下,或者是比较耗费时间的任务,这种等待会浪费资源
- 异步:程序发起数据库请求之后不会等待,可以去做别的事情,等待数据库准备好结果之后在回调或者`await`获取结果
2. 异步数据库的意义
- 提高 I/O 密集型应用(如 Web API)的并发性能。
- 配合异步 Web 框架(如 FastAPI、Starlette、Quart)使用,避免阻塞事件循环。
- 适用于高并发、低延迟的场景。
现在修改我们的ai_client.py代码
主要修改如下:
获取对话历史函数,调用我们自定义的类,这时候直接传入一个sessionid就好了
def get_session_history(session_id: str):
return AsyncPostgresChatMessageHistory(session_id=session_id)
调用方法改为异步的
async for chunk in rag_with_history.astream(...):
print(chunk, end="", flush=True)
完整代码如下(包含测试用例):
# ai_client.py
import os
import asyncio
from message_history.async_pg_history import AsyncPostgresChatMessageHistory
from langchain_community.chat_models import ChatTongyi
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough
from langchain_core.runnables.history import RunnableWithMessageHistory
from operator import itemgetter
import config
# 注入 DashScope API Key
if config.DASHSCOPE_API_KEY:
os.environ["DASHSCOPE_API_KEY"] = config.DASHSCOPE_API_KEY
# 初始化 LLM
llm = ChatTongyi(
model=config.MODEL,
temperature=config.TEMPERATURE,
)
# ========== 加载文档 ==========
all_docs = []
try:
txt_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.txt",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
md_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.md",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
pdf_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
all_docs = (txt_docs or []) + (md_docs or []) + (pdf_docs or [])
except Exception as e:
print(f"文档加载失败: {e}")
all_docs = []
# 分块
text_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=500, chunk_overlap=80)
texts = text_splitter.split_documents(all_docs) if all_docs else []
# 初始化 Embeddings
embeddings = DashScopeEmbeddings(model="text-embedding-v2")
# 初始化向量数据库(Chroma)
if texts:
vectorstore = Chroma.from_documents(
documents=texts,
embedding=embeddings,
persist_directory=config.EMBEDDINGS_DIR
)
vectorstore.persist()
else:
try:
vectorstore = Chroma(
persist_directory=config.EMBEDDINGS_DIR,
embedding_function=embeddings
)
except Exception as e:
print("无文档且无法加载向量库,将使用纯 LLM 模式(无检索)")
vectorstore = None
# 初始化检索器
if vectorstore:
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
else:
retriever = lambda query: []
# 格式化检索结果
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# ========== 构建 Prompt ==========
prompt = ChatPromptTemplate.from_messages([
("system",
"你是一名资深 Java 开发专家,精通 JDK 8-17、Spring 生态、并发、JVM 调优等。\n"
"请基于以下检索到的上下文回答用户的问题。\n"
"如果上下文不相关,请仅基于你的知识回答,不要编造。\n\n"
"上下文:\n{context}"
),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}")
])
# ========== 构建 RAG 链 ==========
rag_chain = (
RunnablePassthrough.assign(context=itemgetter("input") | retriever | format_docs)
| prompt
| llm
| StrOutputParser()
)
# 获取对话历史 ,
def get_session_history(session_id: str):
return AsyncPostgresChatMessageHistory(session_id=session_id)
# ========== 带记忆的完整链 ==========
rag_with_history = RunnableWithMessageHistory(
rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history"
)
# ========== 异步对话测试函数 ==========
async def run_conversation():
SESSION_ID = "user_session_1"
print("欢迎使用 Java 专家问答系统(PostgreSQL 多轮对话)\n")
# 第一轮
question1 = "怎么自定义线程池,不用给我代码,简单说不超过200个字"
print(f"用户: {question1}")
print("AI: ", end="", flush=True)
async for chunk in rag_with_history.astream(
{"input": question1},
config={"configurable": {"session_id": SESSION_ID}}
):
print(chunk, end="", flush=True)
print("\n")
# 第二轮
question2 = "那核心线程数一般设多少?"
print(f"\n用户: {question2}")
print(" AI: ", end="", flush=True)
async for chunk in rag_with_history.astream(
{"input": question2},
config={"configurable": {"session_id": SESSION_ID}}
):
print(chunk, end="", flush=True)
print("\n")
# 第三轮
question3 = "如果任务很多,队列会满吗?"
print(f"\n 用户: {question3}")
print(" AI: ", end="", flush=True)
async for chunk in rag_with_history.astream(
{"input": question3},
config={"configurable": {"session_id": SESSION_ID}}
):
print(chunk, end="", flush=True)
print("\n")
print("\n多轮对话测试完成!对话已存入 PostgreSQL。")
# ========== 主入口 ==========
if __name__ == "__main__":
# 初始化数据库表(可选)
from message_history.async_pg_history import init_table
asyncio.run(init_table())
# 运行对话
asyncio.run(run_conversation())
ps:这时候需要通过Navicat登录到PostgreSQL去创建表,如果遇到权限问题就使用postgres用户
执行以下建表语句
DROP TABLE IF EXISTS langchain_chat_messages;
CREATE TABLE langchain_chat_messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
message JSONB NOT NULL, -- ← JSONB
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_session_id ON langchain_chat_messages (session_id);
运行代码:
控制台输出如下
然后再看以下PostgreSQL的对话记录,可以看到已经成功存入了PostgreSQL
ps:什么是jsonb格式的数据
JSONB 是 PostgreSQL 特有的数据类型,用于高效地存储和查询 JSON(JavaScript Object Notation)数据。它的名字中 “B” 代表 Binary(二进制) ,意味着它在内部以二进制格式存储 JSON 数据,而不是原始的文本字符串。
JSONB vs JSON(PostgreSQL 中的两种 JSON 类型)
| 特性 | JSON | JSONB |
|---|---|---|
| 存储格式 | 原始 JSON 文本(保留空格、键顺序、重复键) | 二进制解析后的结构(压缩、去重、键无序) |
| 写入速度 | 快(直接存字符串) | 稍慢(需解析为二进制) |
| 读取/查询速度 | 慢(每次都要解析) | 快(已解析,支持索引) |
| 支持索引 | ❌ 不支持 GIN 索引优化查询 | ✅ 支持 GIN 索引,可高效查询内部字段 |
| 重复键处理 | 保留所有键 | 只保留最后一个值 |
| 键顺序 | 保留 | 不保留(视为无序对象) |
| 存储空间 | 较大 | 更紧凑(无冗余空格等) |
绝大多数场景推荐使用
JSONB,尤其是需要查询、过滤或索引 JSON 内容时(比如聊天记录、配置、日志等)。
在 Python + SQLAlchemy + asyncpg 中使用的时候,可以传 dict(SQLAlchemy 自动序列化为 JSON 字符串再转为 JSONB)数据类型,然后存入到PostgreSQL
至此,基于PostgreSQL的持久化多轮对话实现完毕!我们已经实现了多轮会话,RAG,等功能,已经可以解决我们的基本的对话,提问需求了,但是好像还缺点什么。严格的来说目前这个还算不上智能体,那什么是真正的智能体(ReAct)呢?