AI回答采集:基于PostgreSQL JSONB的原始数据存储与可追溯性方案

文章简介:AI回答采集后,如何存储原始数据以保证可追溯性?本文使用PostgreSQL JSONB设计存储方案,包含raw_answers主表、answer_versions版本表和answer_metrics指标表,并给出Python+SQLAlchemy示例代码。适合后端开发者阅读,最终给出可复用的表设计和存储流程。

问题背景

AI回答采集系统中,原始数据存储常被简化:只保存最终指标(如回答长度、关键词出现次数),但一旦需要回溯异常回答或重新计算指标,就会陷入无源可查的困境。本文设计一套可追溯的存储方案,确保每条回答的来源、版本和上下文都能被精确还原。

为什么选择JSONB

选择PostgreSQL的JSONB类型存储原始回答,而不是拆成多个字段或使用关系表,主要基于以下考虑:

  • 结构灵活:AI回答内容本身是嵌套JSON(如包含思考链、工具调用),JSONB天然支持,无需预定义字段。
  • 可扩展:后续增加元数据(如token用量、延迟)只需在JSON中添加键值,无需改表结构。
  • 查询方便:JSONB支持GIN索引,可对回答内容进行部分检索,例如查找包含特定关键词的回答。

如果回答超过10MB,建议使用外部存储(如S3),在answer_content中存储对象键。本文以常规场景(回答<1MB)为例。

数据库表设计

主表:raw_answers

每条回答对应一条记录,通过request_id+platform+model_name保证唯一性。

sql 复制代码
CREATE TABLE raw_answers (
    id BIGSERIAL PRIMARY KEY,
    request_id VARCHAR(64) NOT NULL,          -- 唯一请求ID
    query_text TEXT NOT NULL,                  -- 原始问题
    platform VARCHAR(32) NOT NULL,             -- 平台标识,如 'openai', 'wenxin'
    model_name VARCHAR(64) NOT NULL,           -- 模型名称,如 'gpt-4', 'ernie-bot'
    model_version VARCHAR(32),                 -- 模型版本号,如 '2024-01-25'
    answer_content JSONB NOT NULL,             -- 完整回答内容(JSON格式)
    metadata JSONB,                            -- 元数据,如 token用量、延迟、重试次数
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    is_deleted BOOLEAN DEFAULT FALSE,          -- 软删除标记
    UNIQUE(request_id, platform, model_name)
);

CREATE INDEX idx_raw_answers_request_id ON raw_answers(request_id);
CREATE INDEX idx_raw_answers_created_at ON raw_answers(created_at);
CREATE INDEX idx_raw_answers_platform_model ON raw_answers(platform, model_name);

为什么用request_id+platform+model_name作为唯一约束? 同一请求可能同时调用多个模型,每个模型返回不同回答,需要独立存储;而同一模型的重试请求应覆盖旧回答,但旧版本需保留。

版本追踪表:answer_versions

当回答因重试或手动修正被覆盖时,旧版本移入此表。

sql 复制代码
CREATE TABLE answer_versions (
    id BIGSERIAL PRIMARY KEY,
    raw_answer_id BIGINT NOT NULL REFERENCES raw_answers(id),
    answer_content JSONB NOT NULL,
    metadata JSONB,
    version INT NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    change_reason VARCHAR(128)                -- 变更原因,如 'retry', 'manual_correction'
);

CREATE INDEX idx_answer_versions_raw_id ON answer_versions(raw_answer_id);

版本管理策略:只保留最近N个版本(如5个),通过定时任务清理旧版本。变更原因字段务必填写,否则后续审计困难。

指标结果表:answer_metrics

从原始回答计算出的指标(如长度、关键词计数)存储在此表,与原始数据分离,便于重算。

sql 复制代码
CREATE TABLE answer_metrics (
    id BIGSERIAL PRIMARY KEY,
    raw_answer_id BIGINT NOT NULL REFERENCES raw_answers(id),
    metric_name VARCHAR(64) NOT NULL,          -- 指标名称,如 'length', 'keyword_count'
    metric_value NUMERIC NOT NULL,
    computed_at TIMESTAMP NOT NULL DEFAULT NOW(),
    computation_version VARCHAR(32)            -- 计算脚本版本
);

CREATE INDEX idx_answer_metrics_raw_id ON answer_metrics(raw_answer_id);
CREATE INDEX idx_answer_metrics_name ON answer_metrics(metric_name);

当计算逻辑变更时,通过computation_version字段区分,先删除旧版本指标,再重新计算。

核心实现

存储流程

  1. 采集服务收到回答后,生成唯一request_id(UUID)。
  2. 将原始回答(JSON)和元数据(token用量、延迟等)组装成一条记录。
  3. 插入raw_answers表:如果request_id+platform+model_name已存在,则先将旧版本插入answer_versions,再更新raw_answers。
  4. 异步计算指标,写入answer_metrics。

示例代码(Python + SQLAlchemy)

python 复制代码
from sqlalchemy import create_engine, Column, String, JSON, DateTime, Boolean, BigInteger, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from datetime import datetime
import uuid

Base = declarative_base()

class RawAnswer(Base):
    __tablename__ = 'raw_answers'
    id = Column(BigInteger, primary_key=True)
    request_id = Column(String(64), nullable=False)
    query_text = Column(Text, nullable=False)
    platform = Column(String(32), nullable=False)
    model_name = Column(String(64), nullable=False)
    model_version = Column(String(32))
    answer_content = Column(JSON, nullable=False)
    metadata = Column(JSON)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    is_deleted = Column(Boolean, default=False)

def save_raw_answer(session, query_text, platform, model_name, answer_content, metadata=None, model_version=None):
    request_id = str(uuid.uuid4())
    existing = session.query(RawAnswer).filter_by(
        request_id=request_id,
        platform=platform,
        model_name=model_name
    ).first()
    if existing:
        # 保存旧版本
        old_version = AnswerVersion(
            raw_answer_id=existing.id,
            answer_content=existing.answer_content,
            metadata=existing.metadata,
            version=1,
            change_reason='update'
        )
        session.add(old_version)
        # 更新现有记录
        existing.answer_content = answer_content
        existing.metadata = metadata
        existing.model_version = model_version
        existing.updated_at = datetime.utcnow()
    else:
        new_record = RawAnswer(
            request_id=request_id,
            query_text=query_text,
            platform=platform,
            model_name=model_name,
            model_version=model_version,
            answer_content=answer_content,
            metadata=metadata
        )
        session.add(new_record)
    session.commit()
    return request_id

代码说明

  • 该代码是核心片段,完整实现需包含AnswerVersion模型和数据库连接配置。
  • 实际使用时,应处理并发冲突(如使用SELECT FOR UPDATE)。

验证结果

以下验证步骤基于PostgreSQL 15,在psql客户端执行。假设已创建表并插入一条记录。

  1. 插入记录后,查询raw_answers表:
sql 复制代码
SELECT id, request_id, platform, model_name, created_at 
FROM raw_answers 
WHERE query_text LIKE '%如何实现可追溯性%';

正常情况下应当返回一条记录,包含自动生成的ID和创建时间。如果返回空,请检查query_text是否匹配。

  1. 更新同一request_id的记录后,查询answer_versions表:
sql 复制代码
SELECT * FROM answer_versions WHERE raw_answer_id = 1;

正常情况下应当返回一条记录,包含旧版本的answer_content和change_reason='update'。如果返回空,说明更新时未触发版本保存,请检查代码中的existing判断逻辑。

注意:以上验证未在实际生产环境执行,仅为设计验证。实际部署时建议编写自动化测试。

常见问题与避坑

1. 回答内容过大

如果回答超过PostgreSQL的JSONB限制(约1GB),建议:

  • 使用外部存储(如S3),在answer_content中存储对象键。
  • 或者分块存储,用多个字段组合。

2. 版本管理策略

  • 只保留最近N个版本,通过定时任务清理。
  • 变更原因字段务必填写,否则后续审计困难。

3. 指标重算

当计算逻辑变更时,需要重算历史指标。此时通过answer_metrics的computation_version字段区分,先删除旧版本指标,再重新计算。

总结

本文解决了AI回答采集后原始数据存储的可追溯性问题。根因是只保存指标而丢弃原始回答。最终方案使用PostgreSQL JSONB存储原始回答,通过request_id+platform+model_name保证唯一性,版本表支持回滚,指标表与原始数据分离便于重算。该方案适用于日均百万级记录的中等规模系统。对于更高吞吐场景,可考虑分区表或列式存储。当前限制:未处理并发写入冲突,版本表清理策略需根据实际数据量调整。

相关推荐
WeilinerL1 小时前
CodeGraph:让AI理解代码仓库的神器
前端·人工智能
躺柒1 小时前
读半导体简史08大型机
linux·服务器·c语言·数据库·unix·ibm
DFT计算杂谈2 小时前
交错磁研究进展材料物性与交叉应用
数据库·人工智能·python·opencv·算法
CCYe、2 小时前
企业 AI 网关采购需求清单:一份集团级 RFP 的能力映射
人工智能
xfan_me2 小时前
汽车维保记录精准版 API 快速接入指南
人工智能·汽车
IvorySQL2 小时前
PG 日报|查询优化器重大修复,支持更多复杂 SQL 优化
数据库·人工智能·postgresql·开源
AI视觉网奇2 小时前
大模型多轮对话技巧
人工智能
qq_454245032 小时前
engine prompt
人工智能·prompt
秋枫要学习2 小时前
半价 Claude 上线,OpenAI 却先崩了
人工智能·开源