目录
[一、先定目标:一个 Agent 项目到底要存什么](#一、先定目标:一个 Agent 项目到底要存什么)
[二、适合 Agent 项目的数据库设计原则](#二、适合 Agent 项目的数据库设计原则)
[1. 把"会话"和"消息"拆开](#1. 把“会话”和“消息”拆开)
[2. 把"任务执行"和"消息内容"拆开](#2. 把“任务执行”和“消息内容”拆开)
[3. 把"工具调用日志"单独存](#3. 把“工具调用日志”单独存)
[4. RAG 的文档和切片要分表](#4. RAG 的文档和切片要分表)
[5. Memory 也要结构化](#5. Memory 也要结构化)
[三、一套适合 Agent 的核心表结构](#三、一套适合 Agent 的核心表结构)
[1. users:用户表](#1. users:用户表)
[2. conversations:会话表](#2. conversations:会话表)
[3. messages:消息表](#3. messages:消息表)
[4. tasks:任务表](#4. tasks:任务表)
[5. tool_calls:工具调用表](#5. tool_calls:工具调用表)
[6. documents:文档表](#6. documents:文档表)
[7. document_chunks:文档切片表](#7. document_chunks:文档切片表)
[8. retrieval_logs:检索日志表](#8. retrieval_logs:检索日志表)
[9. memories:记忆表](#9. memories:记忆表)
[第七步:如涉及 RAG](#第七步:如涉及 RAG)
[第九步:写入 Agent 回复](#第九步:写入 Agent 回复)
[六、用 SQLAlchemy 写一套项目级代码骨架](#六、用 SQLAlchemy 写一套项目级代码骨架)
[1. 目录结构建议](#1. 目录结构建议)
[2. session.py:数据库连接](#2. session.py:数据库连接)
[3. base.py:声明式基类](#3. base.py:声明式基类)
[4. models.py:核心模型定义](#4. models.py:核心模型定义)
[5. 初始化建表](#5. 初始化建表)
[七、Repository 层怎么写](#七、Repository 层怎么写)
[1. conversation_repo.py](#1. conversation_repo.py)
[2. message_repo.py](#2. message_repo.py)
[3. task_repo.py](#3. task_repo.py)
[八、Service 层怎么串起来](#八、Service 层怎么串起来)
[十一、Agent 项目里 JSONB 该怎么用](#十一、Agent 项目里 JSONB 该怎么用)
[适合放进 JSONB 的内容](#适合放进 JSONB 的内容)
[不适合全塞 JSONB 的内容](#不适合全塞 JSONB 的内容)
[十二、怎么把数据库接到 Python Web 项目里](#十二、怎么把数据库接到 Python Web 项目里)
[第 1 步](#第 1 步)
[第 2 步](#第 2 步)
[第 3 步](#第 3 步)
[第 4 步](#第 4 步)
接着上篇《Agent 开发者如何快速上手 SQL:从表设计到 Python 交互的一篇实战入门》,这次不讲"数据库是什么"这种开胃菜了,直接给你一套适合 Agent 项目的数据库设计实战模板。目标很明确:
让你能用 PostgreSQL + SQLAlchemy 快速搭一个能跑的 Agent 后端数据层。
你可以把这一篇理解成:
-
前一篇:建立 SQL 基本认知
-
这一篇:把认知落到 Agent 项目结构上
一、先定目标:一个 Agent 项目到底要存什么
一个稍微正经一点的 Agent 应用,通常至少会碰到这几类数据:
-
用户数据
-
会话数据
-
消息历史
-
任务执行状态
-
工具调用日志
-
知识库文档
-
文档切片
-
检索日志
-
长期/短期记忆
也就是说,数据库不是"顺手存个聊天记录"那么简单。
它本质上是 Agent 的状态底座。
如果没有这层底座,Agent 看起来像在工作,实际上像一个边跑边失忆的电子生物。
二、适合 Agent 项目的数据库设计原则
在正式建表前,先把方向拧正,不然后面容易越写越抽象。
1. 把"会话"和"消息"拆开
很多新手喜欢直接来一张 chat_history 表,把所有内容都塞进去。
能跑,但后面一扩展就开始散架。
应该拆成:
-
conversations:描述一次对话会话 -
messages:描述会话中的每条消息
这样你后续才能支持:
-
会话标题
-
会话归档
-
会话级别设置
-
消息级检索
-
多轮上下文管理
2. 把"任务执行"和"消息内容"拆开
用户说一句话,不代表 Agent 只做一件事。
例如用户说:
帮我分析最近的销售异常,并输出报告
系统内部可能会拆成:
-
获取最近销售数据
-
调用分析工具
-
生成摘要
-
生成最终报告
这时候你就需要一个 tasks 表来记录执行过程,而不是把所有东西糊在 assistant 回复里。
3. 把"工具调用日志"单独存
Agent 最容易出问题的地方,就是工具调用。
比如:
-
输入参数不对
-
工具返回错误
-
工具执行超时
-
工具结果异常
如果没有 tool_calls 或 tool_logs 表,你排障时就会像在黑屋里抓会发电的泥鳅。
4. RAG 的文档和切片要分表
不要一张表既存文档,又存 chunk。
应该分成:
-
documents -
document_chunks
因为文档和切片是两个层级的数据对象。
5. Memory 也要结构化
Memory 不是只有"存一点聊天摘要"这么简单。
至少应该区分:
-
记忆内容
-
来源会话
-
来源消息
-
记忆类型
-
更新时间
-
是否有效
否则你后面做长期记忆更新、冲突消解、记忆召回时,会变成一锅热腾腾的数据浆糊。
三、一套适合 Agent 的核心表结构
下面我给你一套够用、清晰、容易扩展的表设计。
1. users:用户表
sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
用途:
-
存用户基本信息
-
给 conversations、memories 等表提供归属关系
2. conversations:会话表
sql
CREATE TABLE conversations (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255),
status VARCHAR(50) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
用途:
-
一次对话会话的容器
-
可以挂很多 message、task、memory 关系
关系:
- 一个 user 对应多个 conversation
3. messages:消息表
sql
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL,
content TEXT NOT NULL,
message_type VARCHAR(50) NOT NULL DEFAULT 'text',
token_count INT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
role 一般建议统一为:
-
system -
user -
assistant -
tool
用途:
-
存多轮消息
-
支持对话回放
-
支持消息级上下文拼接
关系:
- 一个 conversation 对应多个 message
4. tasks:任务表
sql
CREATE TABLE tasks (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
message_id BIGINT REFERENCES messages(id) ON DELETE SET NULL,
task_type VARCHAR(100) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
input_payload JSONB,
result_payload JSONB,
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
用途:
-
记录 Agent 内部任务
-
记录执行状态
-
支持失败重试、任务审计、任务回放
建议 status 统一枚举:
-
pending -
running -
completed -
failed
5. tool_calls:工具调用表
sql
CREATE TABLE tool_calls (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
tool_name VARCHAR(100) NOT NULL,
tool_input JSONB,
tool_output JSONB,
status VARCHAR(50) NOT NULL DEFAULT 'success',
latency_ms INT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
用途:
-
记录每次工具调用
-
支持问题排查
-
支持性能分析
-
支持"Agent 为什么这么回答"的追溯
6. documents:文档表
sql
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
source VARCHAR(255),
source_type VARCHAR(50),
document_status VARCHAR(50) DEFAULT 'active',
metadata JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
用途:
-
存知识库中的原始文档元数据
-
可挂来源、作者、行业、发布时间等信息
7. document_chunks:文档切片表
sql
CREATE TABLE document_chunks (
id BIGSERIAL PRIMARY KEY,
document_id BIGINT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
chunk_index INT NOT NULL,
chunk_text TEXT NOT NULL,
embedding_id VARCHAR(255),
metadata JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
用途:
-
存切片后的文本
-
供 RAG 检索使用
关系:
- 一个 document 对应多个 chunk
8. retrieval_logs:检索日志表
sql
CREATE TABLE retrieval_logs (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT REFERENCES conversations(id) ON DELETE SET NULL,
task_id BIGINT REFERENCES tasks(id) ON DELETE SET NULL,
query_text TEXT NOT NULL,
top_k INT NOT NULL,
result_snapshot JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
用途:
-
记录一次检索行为
-
记录召回结果快照
-
方便做效果分析和调试
9. memories:记忆表
sql
CREATE TABLE memories (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
conversation_id BIGINT REFERENCES conversations(id) ON DELETE SET NULL,
message_id BIGINT REFERENCES messages(id) ON DELETE SET NULL,
memory_type VARCHAR(50) NOT NULL,
content TEXT NOT NULL,
importance_score FLOAT DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
memory_type 可以先简单分为:
-
profile:用户画像 -
preference:用户偏好 -
fact:稳定事实 -
task_context:任务上下文 -
summary:摘要记忆
四、这些表之间的关系到底是什么
你可以把它想成下面这个结构:
users
└── conversations
├── messages
├── tasks
│ └── tool_calls
└── retrieval_logs
documents
└── document_chunks
users
└── memories
更具体一点:
-
一个用户有多个会话
-
一个会话有多条消息
-
一个会话可能触发多个任务
-
一个任务可能调用多个工具
-
一个文档有多个切片
-
一个用户有多条记忆
这套关系已经能撑起大多数 Agent MVP 和很多中型项目了。
五、一个真实运行例子:数据库里的调用链路是什么样的
假设用户输入:
帮我分析近 7 天订单异常,并总结成报告
数据库里大概会发生这些事:
第一步:找到用户
在 users 表里找到当前用户。
第二步:创建会话
在 conversations 表中创建一条会话记录。
第三步:写入用户消息
在 messages 表里插入:
-
role =
user -
content = 用户输入内容
第四步:创建任务
在 tasks 表里创建分析任务:
-
task_type =
sales_analysis -
status =
pending
第五步:任务运行
系统开始执行任务,更新:
status = running
第六步:调用工具
如果 Agent 调用了数据库查询工具、报表工具、检索工具,就在 tool_calls 表里记录:
-
tool_name
-
tool_input
-
tool_output
-
latency_ms
第七步:如涉及 RAG
系统在 retrieval_logs 中记录:
-
query_text
-
top_k
-
result_snapshot
第八步:任务完成
在 tasks 表里写入:
-
result_payload
-
status = completed
第九步:写入 Agent 回复
在 messages 表插入 assistant 回复。
第十步:抽取记忆
如果这次对话产生了值得长期保留的信息,就写入 memories。
你会发现,数据库不是附属品,它就是整个系统"发生了什么"的证据链。
六、用 SQLAlchemy 写一套项目级代码骨架
下面我给你一个可直接改造为项目代码的骨架。
1. 目录结构建议
agent_app/
├── app/
│ ├── db/
│ │ ├── base.py
│ │ ├── session.py
│ │ └── models.py
│ ├── repositories/
│ │ ├── conversation_repo.py
│ │ ├── message_repo.py
│ │ ├── task_repo.py
│ │ └── memory_repo.py
│ ├── services/
│ │ └── agent_service.py
│ └── main.py
└── requirements.txt
这个结构的好处是:
模型、数据访问、业务逻辑分层。
别一股脑把所有数据库代码都写进一个 main.py,那样后面会长成一棵悲伤的意大利面树。
2. session.py:数据库连接
python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql+psycopg://postgres:postgres@127.0.0.1:5432/agent_app"
engine = create_engine(
DATABASE_URL,
echo=False,
pool_pre_ping=True
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
pool_pre_ping=True 很实用,能减少连接失效带来的问题。
3. base.py:声明式基类
python
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
4. models.py:核心模型定义
python
from datetime import datetime
from sqlalchemy import String, Text, ForeignKey, DateTime, Integer, Float, Boolean
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(100), nullable=False)
email: Mapped[str | None] = mapped_column(String(255), unique=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
conversations: Mapped[list["Conversation"]] = relationship(back_populates="user")
memories: Mapped[list["Memory"]] = relationship(back_populates="user")
class Conversation(Base):
__tablename__ = "conversations"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
title: Mapped[str | None] = mapped_column(String(255))
status: Mapped[str] = mapped_column(String(50), default="active")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
user: Mapped["User"] = relationship(back_populates="conversations")
messages: Mapped[list["Message"]] = relationship(back_populates="conversation", cascade="all, delete-orphan")
tasks: Mapped[list["Task"]] = relationship(back_populates="conversation", cascade="all, delete-orphan")
class Message(Base):
__tablename__ = "messages"
id: Mapped[int] = mapped_column(primary_key=True)
conversation_id: Mapped[int] = mapped_column(ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False)
role: Mapped[str] = mapped_column(String(50), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
message_type: Mapped[str] = mapped_column(String(50), default="text")
token_count: Mapped[int | None] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
conversation: Mapped["Conversation"] = relationship(back_populates="messages")
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
conversation_id: Mapped[int] = mapped_column(ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False)
message_id: Mapped[int | None] = mapped_column(ForeignKey("messages.id", ondelete="SET NULL"))
task_type: Mapped[str] = mapped_column(String(100), nullable=False)
status: Mapped[str] = mapped_column(String(50), default="pending")
input_payload: Mapped[dict | None] = mapped_column(JSONB)
result_payload: Mapped[dict | None] = mapped_column(JSONB)
error_message: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
conversation: Mapped["Conversation"] = relationship(back_populates="tasks")
tool_calls: Mapped[list["ToolCall"]] = relationship(back_populates="task", cascade="all, delete-orphan")
class ToolCall(Base):
__tablename__ = "tool_calls"
id: Mapped[int] = mapped_column(primary_key=True)
task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False)
tool_name: Mapped[str] = mapped_column(String(100), nullable=False)
tool_input: Mapped[dict | None] = mapped_column(JSONB)
tool_output: Mapped[dict | None] = mapped_column(JSONB)
status: Mapped[str] = mapped_column(String(50), default="success")
latency_ms: Mapped[int | None] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
task: Mapped["Task"] = relationship(back_populates="tool_calls")
class Document(Base):
__tablename__ = "documents"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
source: Mapped[str | None] = mapped_column(String(255))
source_type: Mapped[str | None] = mapped_column(String(50))
document_status: Mapped[str] = mapped_column(String(50), default="active")
metadata_json: Mapped[dict | None] = mapped_column("metadata", JSONB)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
chunks: Mapped[list["DocumentChunk"]] = relationship(back_populates="document", cascade="all, delete-orphan")
class DocumentChunk(Base):
__tablename__ = "document_chunks"
id: Mapped[int] = mapped_column(primary_key=True)
document_id: Mapped[int] = mapped_column(ForeignKey("documents.id", ondelete="CASCADE"), nullable=False)
chunk_index: Mapped[int] = mapped_column(Integer, nullable=False)
chunk_text: Mapped[str] = mapped_column(Text, nullable=False)
embedding_id: Mapped[str | None] = mapped_column(String(255))
metadata_json: Mapped[dict | None] = mapped_column("metadata", JSONB)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
document: Mapped["Document"] = relationship(back_populates="chunks")
class RetrievalLog(Base):
__tablename__ = "retrieval_logs"
id: Mapped[int] = mapped_column(primary_key=True)
conversation_id: Mapped[int | None] = mapped_column(ForeignKey("conversations.id", ondelete="SET NULL"))
task_id: Mapped[int | None] = mapped_column(ForeignKey("tasks.id", ondelete="SET NULL"))
query_text: Mapped[str] = mapped_column(Text, nullable=False)
top_k: Mapped[int] = mapped_column(Integer, nullable=False)
result_snapshot: Mapped[dict | None] = mapped_column(JSONB)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class Memory(Base):
__tablename__ = "memories"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
conversation_id: Mapped[int | None] = mapped_column(ForeignKey("conversations.id", ondelete="SET NULL"))
message_id: Mapped[int | None] = mapped_column(ForeignKey("messages.id", ondelete="SET NULL"))
memory_type: Mapped[str] = mapped_column(String(50), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
importance_score: Mapped[float] = mapped_column(Float, default=0.0)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
user: Mapped["User"] = relationship(back_populates="memories")
这套模型已经能覆盖:
-
基础聊天
-
任务执行
-
工具日志
-
RAG 文档
-
长期记忆
5. 初始化建表
python
from app.db.base import Base
from app.db.session import engine
from app.db import models # 确保模型被导入
Base.metadata.create_all(bind=engine)
生产环境不建议长期用 create_all() 管理表结构,后面应该切 Alembic。
但在你学习和起步阶段,它很方便。
七、Repository 层怎么写
Repository 层的作用是:
把数据库操作从业务逻辑里拆出来。
1. conversation_repo.py
python
from sqlalchemy.orm import Session
from app.db.models import Conversation
def create_conversation(db: Session, user_id: int, title: str | None = None) -> Conversation:
conversation = Conversation(user_id=user_id, title=title)
db.add(conversation)
db.commit()
db.refresh(conversation)
return conversation
def get_conversation(db: Session, conversation_id: int) -> Conversation | None:
return db.query(Conversation).filter(Conversation.id == conversation_id).first()
2. message_repo.py
python
from sqlalchemy.orm import Session
from app.db.models import Message
def add_message(db: Session, conversation_id: int, role: str, content: str, token_count: int | None = None) -> Message:
message = Message(
conversation_id=conversation_id,
role=role,
content=content,
token_count=token_count
)
db.add(message)
db.commit()
db.refresh(message)
return message
def list_messages(db: Session, conversation_id: int) -> list[Message]:
return (
db.query(Message)
.filter(Message.conversation_id == conversation_id)
.order_by(Message.created_at.asc())
.all()
)
3. task_repo.py
python
from sqlalchemy.orm import Session
from app.db.models import Task, ToolCall
def create_task(db: Session, conversation_id: int, message_id: int | None, task_type: str, input_payload: dict | None = None) -> Task:
task = Task(
conversation_id=conversation_id,
message_id=message_id,
task_type=task_type,
status="pending",
input_payload=input_payload
)
db.add(task)
db.commit()
db.refresh(task)
return task
def update_task_status(db: Session, task_id: int, status: str, result_payload: dict | None = None, error_message: str | None = None) -> Task | None:
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
return None
task.status = status
task.result_payload = result_payload
task.error_message = error_message
db.commit()
db.refresh(task)
return task
def add_tool_call(db: Session, task_id: int, tool_name: str, tool_input: dict | None, tool_output: dict | None, status: str = "success", latency_ms: int | None = None) -> ToolCall:
tool_call = ToolCall(
task_id=task_id,
tool_name=tool_name,
tool_input=tool_input,
tool_output=tool_output,
status=status,
latency_ms=latency_ms
)
db.add(tool_call)
db.commit()
db.refresh(tool_call)
return tool_call
八、Service 层怎么串起来
这一层负责真正的业务流程。
比如用户发来一句话时,你的后端怎么落库。
python
from sqlalchemy.orm import Session
from app.repositories.conversation_repo import create_conversation
from app.repositories.message_repo import add_message
from app.repositories.task_repo import create_task, update_task_status, add_tool_call
def handle_user_message(db: Session, user_id: int, user_text: str):
# 1. 创建会话
conversation = create_conversation(db, user_id=user_id, title="新会话")
# 2. 写入用户消息
user_msg = add_message(db, conversation.id, "user", user_text)
# 3. 创建任务
task = create_task(
db,
conversation_id=conversation.id,
message_id=user_msg.id,
task_type="general_agent_task",
input_payload={"query": user_text}
)
# 4. 更新任务状态为 running
update_task_status(db, task.id, "running")
# 5. 模拟工具调用
tool_result = {"summary": "这是一个模拟分析结果"}
add_tool_call(
db,
task_id=task.id,
tool_name="mock_analyzer",
tool_input={"text": user_text},
tool_output=tool_result,
status="success",
latency_ms=120
)
# 6. 任务完成
update_task_status(
db,
task.id,
"completed",
result_payload=tool_result
)
# 7. 写入 assistant 回复
assistant_msg = add_message(
db,
conversation.id,
"assistant",
f"分析完成:{tool_result['summary']}"
)
return {
"conversation_id": conversation.id,
"user_message_id": user_msg.id,
"task_id": task.id,
"assistant_message_id": assistant_msg.id
}
你可以看到这里的逻辑已经很像真实 Agent 运行链路了。
九、为什么要学事务
Agent 项目里,事务很重要。
比如你要做这几件事:
-
写入用户消息
-
创建任务
-
记录工具调用
-
更新任务状态
如果执行到第 3 步炸了,但前两步已经写进数据库,系统状态就会不一致。
这时候就需要事务。
一个简单示例
python
from sqlalchemy.orm import Session
from app.db.models import Message, Task
def create_message_and_task(db: Session, conversation_id: int, content: str):
try:
message = Message(
conversation_id=conversation_id,
role="user",
content=content
)
db.add(message)
db.flush() # 先拿到 message.id,但还不 commit
task = Task(
conversation_id=conversation_id,
message_id=message.id,
task_type="analyze",
status="pending",
input_payload={"content": content}
)
db.add(task)
db.commit()
db.refresh(message)
db.refresh(task)
return message, task
except Exception:
db.rollback()
raise
这里的关键点是:
-
flush():先把对象送进数据库,拿到主键 id -
commit():最后统一提交 -
rollback():一旦出错就回滚
这能保证你的数据不会"只写一半"。
十、哪些字段应该加索引
索引不是越多越好,但核心查询字段要加。
对 Agent 项目来说,常见索引位点有:
messages
-
conversation_id -
created_at
tasks
-
conversation_id -
status -
created_at
tool_calls
-
task_id -
tool_name
memories
-
user_id -
memory_type -
is_active
document_chunks
-
document_id -
chunk_index
例如:
CREATE INDEX idx_messages_conversation_id ON messages(conversation_id);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_memories_user_id ON memories(user_id);
你后面数据量一大,没有索引,查询会慢得像数据库在思考宇宙意义。
十一、Agent 项目里 JSONB 该怎么用
PostgreSQL 的 JSONB 非常适合 Agent 项目。
但别滥用。
适合放进 JSONB 的内容
-
tool_input
-
tool_output
-
task 的输入输出 payload
-
文档的灵活元数据
-
retrieval result snapshot
不适合全塞 JSONB 的内容
-
用户 id
-
conversation_id
-
role
-
status
-
created_at
这些是高频过滤字段,应该老老实实做成结构化列。
一句话总结就是:
固定且经常查的字段,用普通列。
灵活多变、不固定结构的字段,用 JSONB。
十二、怎么把数据库接到 Python Web 项目里
如果你后面用 FastAPI,这套数据库层很容易接进去。
例如:
python
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from app.db.session import SessionLocal
from app.services.agent_service import handle_user_message
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/chat")
def chat(user_id: int, text: str, db: Session = Depends(get_db)):
return handle_user_message(db, user_id, text)
这就是典型的:
-
API 收请求
-
Service 处理业务
-
Repository 写数据库
结构很清爽。
十三、建议你尽快补上的工具:Alembic
当你从"练习项目"走向"真实项目"时,表结构肯定会改。
比如:
-
给
messages加model_name -
给
tasks加retry_count -
给
memories加expires_at
这时候就别再手动改数据库了,容易把环境改成一场行为艺术。
建议上 Alembic。
安装:
bash
pip install alembic
初始化:
alembic init migrations
生成迁移:
alembic revision --autogenerate -m "init tables"
执行迁移:
alembic upgrade head
这就是你前面问到的那串命令背后的工程意义:
不是"运行一下",而是"把数据库结构推进到最新版本"。
十四、给你一套非常务实的落地顺序
如果你现在正在做 Agent 项目,我建议你按这个顺序推进:
第 1 步
先把这 5 张表建起来:
-
users -
conversations -
messages -
tasks -
tool_calls
因为这是 Agent 最核心的运行闭环。
第 2 步
把 Python 的这几个能力打通:
-
创建用户
-
创建会话
-
插入消息
-
创建任务
-
记录工具调用
-
查询会话历史
第 3 步
再补 RAG 相关表:
-
documents -
document_chunks -
retrieval_logs
第 4 步
最后补 Memory:
memories
这时候你的项目就从"能聊天"进化成"有状态、有追踪、有知识层"的 Agent 应用了。
十五、你现在最该做的最小练习
不要只看,直接敲。最有效。
你可以马上做这个 mini project:
任务目标
做一个最小 Agent 数据层 demo
功能要求
-
创建一个用户
-
创建一个会话
-
插入一条用户消息
-
创建一个任务
-
记录一次工具调用
-
插入一条 assistant 回复
-
查询并打印整个会话历史
这套练习做完,你对数据库在 Agent 项目中的作用就不再是"听懂了",而是"会用了"。
十六、给你的下一步建议
你现在最适合继续补的,不是再看一堆抽象概念,而是把下面两块接上:
-
PostgreSQL + SQLAlchemy + Alembic 的完整项目初始化模板
-
FastAPI + 数据库 + Agent Service 的一套后端骨架
这样你就能从"懂数据库"直接迈到"能把 Agent 项目接上数据库并跑起来"。