从静态页面到企业知识库 AI SaaS:产品流程、RAG 与可靠性架构设计

这篇文章记录的不是已经完成上线的系统,而是从当前 HTML 原型推导出的产品和架构设计。

漂浮在未来实验室中的人物,象征 AI 知识库架构探索

今天的 HTML 练习只有一个静态页面:上面有知识文档列表、上传按钮、处理状态、问题输入框、回答和引用来源。页面本身没有连接数据库,也没有真正上传文件或调用模型。

但当我开始追问“按钮点击以后发生什么”“处理状态由谁负责”“引用从哪里来”,一个静态页面很快展开成了完整的企业知识库架构。

这篇文章记录的不是已经完成上线的系统,而是从当前 HTML 原型推导出的产品和架构设计。

先定义产品,而不是先堆技术栈

企业知识库服务的目标用户可以是需要查询内部资料的工程师、运营人员和知识库管理员。对普通使用者来说,核心目标不是“体验向量数据库”,而是快速获得可信、可追溯的答案;对管理员来说,核心目标是让资料能够成功进入知识库,失败时知道原因并能够恢复。

一个最小用户故事是:

作为知识库管理员,我希望看到文档处理失败的原因并能重新处理,这样我不需要联系开发人员就能恢复常见的摄取失败。

静态页面因此不能只显示“成功”状态,还需要展示:

已就绪
处理中
处理失败:文件编码无法识别
重新处理

正常流程是:

上传文档
→ 等待处理
→ 文档就绪
→ 提出问题
→ 阅读流式回答
→ 查看引用

失败流程是:

上传文档
→ 处理失败
→ 查看失败原因
→ 重新处理
→ 文档就绪

这里体现了一个基础产品原则:状态反馈不是装饰。没有“处理中”,用户会认为点击上传没有响应;没有明确失败原因和恢复动作,用户只能反复尝试或联系开发人员。

MVP 是最小闭环,不是最低质量

MVP 是 Minimum Viable Product,中文通常译为“最小可行产品”。它不是随便拼出的残缺版本,而是用最小功能范围建立一个真实可用的业务闭环。

企业知识库 MVP 的闭环可以定义为:

登录
→ 上传文档
→ 异步处理
→ 提问
→ 获得带引用回答
→ 失败时能够恢复

高级管理后台、多模型路由、复杂分析大屏和全局文件去重可以推迟,但权限隔离、数据持久化、文件校验、失败反馈和引用验证不能因为“MVP”而省略。可以缩小功能范围,不能删除决定可信性和安全性的底线。

这也意味着当前 35 天项目更适合采用“Next.js 前端 + FastAPI 模块化单体 + 独立 Worker”,而不是一开始拆成大量微服务。模块化单体便于单人开发、测试和部署,Worker 又能隔离耗时的文档处理任务。

DOM 负责展示,PostgreSQL 保存权威状态

我最初认为页面中的 <span> 可以作为文档状态来源:

<span>处理中</span>

但 DOM 只是当前浏览器标签页里的展示结构。刷新后它会重新创建,用户可以通过开发者工具修改它,其他设备和其他用户也看不到这份本地状态。它不能承担持久化、并发控制或审计职责。

更合理的职责分工是:

组件职责
DOM向当前用户展示状态
FastAPI鉴权、校验和业务入口
Redis传递异步任务
Worker执行解析、切块和 Embedding
PostgreSQL保存权威、持久化业务状态

页面可以先显示“正在提交”改善反馈,但最终必须根据服务端返回的数据库状态校准 UI。

一次 HTTP 请求只能返回一次

文档解析、切块和 Embedding 可能持续几秒甚至几分钟。FastAPI 不应该一直占用原始上传请求等待 Worker 完成,而应在接收并创建任务后立即返回:

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "document_id": "doc_123",
  "status": "queued"
}

202 Accepted 表示请求已经接受,但后台工作尚未完成。

这里有一个容易说错的地方:服务端不能先对同一个 HTTP 请求返回“任务已接受”,等 Worker 完成后再返回第二次结果。一次普通 HTTP 请求只有一次响应。后续状态必须通过新的通信获得,例如:

  • 前端每隔两三秒轮询状态接口;
  • 服务端通过 SSE 推送状态事件;
  • 用户刷新页面后重新从 API 查询。

文档状态变化频率不高,MVP 使用轮询通常最简单。AI 回答会持续产生 Token,更适合 SSE 或流式 fetch。架构不应该因为“问答已经用了 SSE”就强迫所有低频状态都复用同一种机制。

完整摄取链路可以设计为:

Browser
→ FastAPI 校验文件、权限和幂等键
→ 原文件写入对象存储
→ PostgreSQL 创建 queued 文档版本
→ Redis 投递摄取任务
→ Worker 读取文件
→ 状态更新为 processing
→ 解析、切块、生成 Embedding
→ chunks 和向量写入 PostgreSQL/pgvector
→ 完整性校验
→ 原子切换当前版本
→ 状态更新为 ready

任何一步失败,都应记录稳定的错误类型和可展示原因,再把状态更新为 failed

原文件、结构化状态和向量应该放在哪里

PDF、Word 等原始二进制文件更适合保存在 S3 或 MinIO 这样的对象存储中。它们支持大对象读写、生命周期管理和版本策略,也避免 PostgreSQL 备份被大量二进制内容拖累。

PostgreSQL 则保存结构化数据:

documents
document_versions
ingestion_jobs
chunks
conversations
messages
answer_runs
citations

每个文档版本保存对象路径、SHA-256、处理状态和创建时间;每个 chunk 保存文本、页码、章节、文档版本以及 embedding。

浏览器可以提前计算文件哈希,用于提示重复上传,但浏览器属于用户可控制环境,后端或 Worker 必须重新读取实际存储的文件并计算可信 SHA-256。影响权限、计费、完整性和业务状态的结果,都必须由可信后端验证。

在多租户环境中,MVP 最稳妥的是租户内去重:

UNIQUE(tenant_id, sha256)

后端即使在物理存储层发现另一个租户拥有相同文件,也不能对当前用户提示“该文件已存在于另一家公司”。否则,攻击者可以通过上传已知文件并观察响应,推测其他企业是否拥有敏感资料。

文档更新不能先破坏旧版本

文档更新时,如果先删除旧 chunks 和向量,再开始生成新版本,一旦解析或模型调用失败,原本可用的知识会立即消失。

更可靠的方式是先完整构建新版本:

创建 v2,状态 processing
→ 解析并生成 v2 的全部 chunks
→ 生成并写入 v2 的全部向量
→ 检查数量、维度和错误
→ 数据库事务切换 active_version_id:v1 → v2
→ v2 标记 ready
→ v1 延迟退役和清理

如果 v2 失败,v1 继续对外提供问答,v2 标记为 failed 并允许重新处理。这个设计主要保证可用性和一致性:查询不会同时混入新旧 chunks,也不会把只处理了一半的版本当成完整知识。

RAG 有两条链路:入库不是检索

RAG 是 Retrieval-Augmented Generation,检索增强生成。我一开始把“文档切块、向量匹配、文档 ready”写在同一条流程里,实际混淆了离线入库和在线查询。

文档入库负责建立可检索索引:

原始文件
→ 解析文本和结构
→ 切分 chunks
→ 为 chunks 生成 embeddings
→ 保存文本、元数据和向量
→ 文档版本 ready

用户问答才会使用索引:

用户问题
→ FastAPI 鉴权
→ 使用兼容的 Embedding 模型将问题向量化
→ 在租户、权限和 active_version 范围内检索
→ 融合向量与关键词候选
→ reranker 精排
→ 将高质量 chunks 放入 Prompt
→ 大模型生成回答
→ 流式发送给前端
→ 根据 chunk 元数据构造引用

问题向量和 chunk 向量必须处于兼容的向量空间,因此升级 Embedding 模型时不能只切换查询模型而忽略已有索引。

混合检索负责召回候选:向量检索寻找语义相关内容,关键词检索补充专有名词、编号和精确短语。reranker 再对“问题与候选 chunk 的相关性”做更精细排序。

权限必须在检索前生效

企业知识库不能先从全库检索 Top K,再把无权限结果过滤掉。这样不仅效率较低,更重要的是无权限文本可能已经进入 reranker、模型请求或日志,形成数据泄露。

正确顺序是:

解析用户身份、tenant_id 和用户组
→ 在 SQL/向量查询中加入租户、ACL 和 active_version 条件
→ 只对有权限的 chunks 排序
→ reranker
→ Prompt
→ 模型

引用也不能只依赖大模型生成的文字。后端应根据命中 chunk 的元数据构造引用,例如:

chunk_id
document_id
document_version_id
page_number
section_title
tenant_id
permission_scope

用户点击引用时,文档接口仍要再次鉴权。不能因为回答阶段已经检索过一次,就把永久公开的对象存储地址直接交给浏览器。

真流式不是把完整答案切成小段播放

“先生成完整答案,再把字符串切成几段逐字显示”只能制造视觉动画,无法降低首 Token 等待时间。

真正的流式链路是:

模型生成一批 Token
→ Provider 返回增量数据
→ FastAPI 立即转发
→ SSE 或流式 fetch 发送给浏览器
→ 前端增量更新当前回答

如果连接在回答一半时中断,页面不能把部分内容伪装成完整回答。前端应保留已显示内容,标记“回答中断”,并提供重新生成。

后端也不应只保存“问题状态”。一个用户问题可能对应多次回答尝试:

user_message
└── answer_run #1:interrupted
└── answer_run #2:completed

answer_run 可以保存 queued → retrieving → generating → completed/failed/interrupted 状态、部分内容、检索 chunk ID、模型版本和错误类型。重新生成创建新的 run,而不是覆盖旧记录。

重复请求需要幂等键,不需要语义相似度

用户可能连续点击两次“重新生成”,网络库也可能自动重试同一个请求。通过问题文本的 embedding 相似度去重并不可靠:相似问题可能是两次有意操作,相同问题也可能是一次明确的重新生成。

更准确的方法是幂等键:

POST /messages/msg_123/answer-runs
Idempotency-Key: 8b77341d-...

一次用户操作生成一个唯一 key;同一次操作的网络重试继续使用这个 key;用户有意再次生成时使用新 key。数据库通过唯一约束阻止并发重复创建:

UNIQUE(tenant_id, user_id, idempotency_key)

前端禁用正在提交的按钮有助于交互,但不能代替服务端幂等,因为它阻止不了网络重试和并发请求。

指标、Trace 和日志回答不同问题

用户说“点击发送后页面像卡住了”,最直接的体验指标不是完整答案总耗时,而是首 Token 延迟。

P95 是第 95 百分位数。若首 Token P95 为 2.5 秒,表示 95% 的请求能在 2.5 秒内看到第一个 Token,仍有 5% 更慢。平均值可能掩盖少数非常慢的请求,因此通常同时观察:

  • P50:典型用户体验;
  • P95:较慢用户和服务目标;
  • P99:极端长尾问题。

企业知识库 MVP 至少可以关注:

文档处理成功率
文档处理 P95 时延
问答首 Token P95
完整回答 P95
引用可用率
从上传到首次获得带引用回答的成功率

当 P95 变差时,只记录一个总耗时还不够。一次请求应通过同一个 trace_id 拆成权限检查、检索、reranker 和模型首 Token 等阶段。

可观测性的三个概念分别回答:

  • Metrics:系统是否出现了问题;
  • Traces:问题发生在哪个组件;
  • Logs:该组件当时发生了什么。

更多日志不等于更安全。日志可以记录租户 ID、trace ID、耗时、状态码和错误类型,但不应默认记录完整 Prompt、私有文档正文、密码、密钥或访问令牌。

这个项目最终应该有多大

按当前 35 天计划,最终目标是一个可部署、可演示、具备生产意识的中型 SaaS MVP,而不是成熟的大型企业平台。

合理架构是:

Browser
→ Next.js
→ FastAPI
   ├── PostgreSQL/pgvector
   ├── Redis → Worker
   ├── S3/MinIO
   └── LLM/Embedding Provider

第一方代码大约会落在数万行,而不是几十万行。几十万行依赖、构建产物或未经审查的 AI 生成代码没有作品价值。更重要的验收是:关键模块能够解释,核心链路有测试,权限和失败边界清楚,系统能够部署、观察和恢复。

SSO/SAML、复杂组织权限、计费、多区域容灾和大型运营平台可以成为后续演进方向,不应在 MVP 阶段挤压核心闭环。

我的 AI SaaS 六维审查清单

1. 产品

  • 目标用户是谁,核心任务是什么;
  • 正常流程是否闭环;
  • 失败原因是否可理解,用户是否能够恢复;
  • 当前功能是否真的属于 MVP。

2. 状态

  • 哪些状态只用于展示;
  • 哪个数据源是权威来源;
  • 状态机是否包含处理中、完成、失败、中断和重试;
  • 刷新或断线后是否能够恢复。

3. 数据

  • 原文件、结构化元数据、chunks 和向量分别存在哪里;
  • 文档版本如何切换;
  • 入库与查询是否分离;
  • 引用是否能追溯到确定版本和位置。

4. 安全

  • 鉴权和租户过滤在哪一层生效;
  • 无权限内容是否可能进入模型、日志或缓存;
  • 引用下载是否再次鉴权;
  • 浏览器提交的哈希、身份和业务状态是否由后端验证。

5. 失败与可靠性

  • 长任务是否异步执行;
  • 重试是否幂等;
  • 新版本失败时旧版本是否仍然可用;
  • 流式回答中断后是否被错误标记为完成。

6. 指标

  • 是否衡量核心产品闭环;
  • 是否同时观察成功率、P95 延迟、引用和成本;
  • 是否能通过 trace 定位慢阶段;
  • 日志是否足够排障且不泄露敏感内容。

总结

今天从一张静态 HTML 页面得到的最大收获,是学会追问页面背后的状态和责任边界。

<span>处理中</span> 只是展示,PostgreSQL 才保存权威状态;上传按钮只是入口,Worker 才执行耗时摄取;对象存储保留原文件,pgvector 保存可检索 chunks;RAG 入库负责建立索引,在线问答负责使用索引;大模型组织回答,引用必须来自检索元数据;流式输出改善首 Token 体验,幂等和状态机负责处理失败与重复请求。

架构设计并不是把更多技术名词画进图里,而是回答六个问题:用户要完成什么、状态由谁负责、数据放在哪里、失败怎样恢复、权限在哪里生效、结果如何衡量。

页面还没有实现这些后端能力,但它已经提供了一个清晰的产品入口。接下来的学习会逐步把这份设计落实为 CSS 布局、JavaScript 交互、React 状态、Next.js 页面、FastAPI API、异步 Worker 和可评估的 RAG 服务。

没有回应

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注