PostgreSQL + SQLAlchemy 快速搭一个能跑的 Agent 后端数据层

目录

[一、先定目标:一个 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 层怎么串起来)

九、为什么要学事务

一个简单示例

十、哪些字段应该加索引

messages

tasks

tool_calls

memories

document_chunks

[十一、Agent 项目里 JSONB 该怎么用](#十一、Agent 项目里 JSONB 该怎么用)

[适合放进 JSONB 的内容](#适合放进 JSONB 的内容)

[不适合全塞 JSONB 的内容](#不适合全塞 JSONB 的内容)

[十二、怎么把数据库接到 Python Web 项目里](#十二、怎么把数据库接到 Python Web 项目里)

十三、建议你尽快补上的工具:Alembic

十四、给你一套非常务实的落地顺序

[第 1 步](#第 1 步)

[第 2 步](#第 2 步)

[第 3 步](#第 3 步)

[第 4 步](#第 4 步)

十五、你现在最该做的最小练习

任务目标

功能要求

十六、给你的下一步建议


接着上篇《Agent 开发者如何快速上手 SQL:从表设计到 Python 交互的一篇实战入门》,这次不讲"数据库是什么"这种开胃菜了,直接给你一套适合 Agent 项目的数据库设计实战模板。目标很明确:

让你能用 PostgreSQL + SQLAlchemy 快速搭一个能跑的 Agent 后端数据层。

你可以把这一篇理解成:

  • 前一篇:建立 SQL 基本认知

  • 这一篇:把认知落到 Agent 项目结构上


一、先定目标:一个 Agent 项目到底要存什么

一个稍微正经一点的 Agent 应用,通常至少会碰到这几类数据:

  1. 用户数据

  2. 会话数据

  3. 消息历史

  4. 任务执行状态

  5. 工具调用日志

  6. 知识库文档

  7. 文档切片

  8. 检索日志

  9. 长期/短期记忆

也就是说,数据库不是"顺手存个聊天记录"那么简单。

它本质上是 Agent 的状态底座

如果没有这层底座,Agent 看起来像在工作,实际上像一个边跑边失忆的电子生物。


二、适合 Agent 项目的数据库设计原则

在正式建表前,先把方向拧正,不然后面容易越写越抽象。

1. 把"会话"和"消息"拆开

很多新手喜欢直接来一张 chat_history 表,把所有内容都塞进去。

能跑,但后面一扩展就开始散架。

应该拆成:

  • conversations:描述一次对话会话

  • messages:描述会话中的每条消息

这样你后续才能支持:

  • 会话标题

  • 会话归档

  • 会话级别设置

  • 消息级检索

  • 多轮上下文管理


2. 把"任务执行"和"消息内容"拆开

用户说一句话,不代表 Agent 只做一件事。

例如用户说:

帮我分析最近的销售异常,并输出报告

系统内部可能会拆成:

  • 获取最近销售数据

  • 调用分析工具

  • 生成摘要

  • 生成最终报告

这时候你就需要一个 tasks 表来记录执行过程,而不是把所有东西糊在 assistant 回复里。


3. 把"工具调用日志"单独存

Agent 最容易出问题的地方,就是工具调用。

比如:

  • 输入参数不对

  • 工具返回错误

  • 工具执行超时

  • 工具结果异常

如果没有 tool_callstool_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 项目里,事务很重要。

比如你要做这几件事:

  1. 写入用户消息

  2. 创建任务

  3. 记录工具调用

  4. 更新任务状态

如果执行到第 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

当你从"练习项目"走向"真实项目"时,表结构肯定会改。

比如:

  • messagesmodel_name

  • tasksretry_count

  • memoriesexpires_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 项目中的作用就不再是"听懂了",而是"会用了"。


十六、给你的下一步建议

你现在最适合继续补的,不是再看一堆抽象概念,而是把下面两块接上:

  1. PostgreSQL + SQLAlchemy + Alembic 的完整项目初始化模板

  2. FastAPI + 数据库 + Agent Service 的一套后端骨架

这样你就能从"懂数据库"直接迈到"能把 Agent 项目接上数据库并跑起来"。

相关推荐
Z1eaf_complete2 小时前
SQL注入如何写入Webshell
数据库·sql
瀚高PG实验室2 小时前
瀚高安全版 V4.5.10卸载后残留了db_ha的agent进程导致6666端口被占用
linux·数据库·安全·瀚高数据库
Monly212 小时前
大模型:LangChain调用大语言模型
人工智能·语言模型·langchain
reesn2 小时前
嵌入模型分类问答
人工智能·分类·数据挖掘
七牛云行业应用2 小时前
别瞎折腾了!4 步排查法,手把手教你搞定 OpenClaw Skills 各种安装报错
后端·openai·agent
软件聚导航2 小时前
OpenClaw实战应用场景-之有道龙虾
ai·agent·openclaw
对号东2 小时前
2026现象级开源AI智能体|OpenClaw:让AI从“聊天”变“干活”,本地部署零门槛
人工智能·开源
与数据交流的路上2 小时前
oceanbase-长事务排查
java·数据库·oceanbase