1. 项目概述:一个AI教育平台的“隐形”后端架构

做后端开发这些年,我越来越认同一个观点:好的后端工程是“隐形”的。当用户流畅地使用一个应用时,他们不会去想数据库的表是怎么设计的,请求是怎么被限流的,或者记忆功能是怎么存储和检索的。他们只会觉得“这东西好用”。只有当系统出问题的时候,后端才会被“看见”——而作为一个搞砸过不少次的人,我对这种“被看见”的滋味再熟悉不过了。

最近我主导了EduRag这个AI教育平台的后端基础设施搭建。这是一个基于RAG(检索增强生成)技术,让学生能针对上传的PDF教材进行问答和讨论的平台。我的工作涵盖了从Supabase数据库Schema设计、FastAPI接口层、与Hindsight记忆服务的集成,到速率限制、WebSocket聊天室和安全中间件栈的全部内容。这部分系统从来不会出现在产品演示里,但却是整个产品能稳定运行的基石。今天,我想抛开那些光鲜的AI模型和交互设计,聊聊这些通常没人讨论、却至关重要的基础设施决策,尤其是围绕向量数据库、异步任务和系统可靠性展开的那些“坑”与收获。

2. 核心架构与数据层设计

2.1 全托管数据库策略:为什么选择Supabase PostgreSQL

项目早期我们就做了一个关键决定: 完全摒弃本地数据库,所有数据持久化都交给Supabase的托管PostgreSQL 。这个决策极大地简化了部署和运维复杂度——我们不需要操心数据库的安装、备份、升级和高可用。但硬币的另一面是,我们必须一次性把数据Schema设计得足够健壮,因为在生产环境进行表结构迁移的痛苦指数非常高。

我们整个平台的核心数据模型由八张表构成,它们共同支撑了用户、内容、交互和记忆四大模块:

  • users :存储账户核心信息,包括机构ID、姓名、经过bcrypt哈希的密码、角色枚举类型和头像偏好。这里的 role 枚举(如 student teacher admin )是整个基于角色的访问控制(RBAC)系统的基石,后续所有鉴权逻辑都源于此。
  • search_history :记录每个用户发起的每一次RAG查询。包含查询文本、时间戳和返回的结果数量。这张表是后续“热门话题”分析和个性化推荐的数据源头。
  • 内容三件套( pdfs pdf_chunks rag_embeddings :这是RAG功能的核心。
    • pdfs 表存放PDF文件的上传元数据(如文件名、大小、上传者)和最重要的 indexing_status (索引状态)。
    • pdf_chunks 表存储从PDF中提取出来的文本片段,并记录每个片段在原文中的位置信息(如页码、起始行)。
    • rag_embeddings 表则与 pdf_chunks 一一对应,存储每个文本片段的向量嵌入(我们使用Google Gemini的嵌入模型生成)。这里我们直接将向量以 float[] (浮点数数组)的形式存入PostgreSQL。

关键决策点:在数据库内计算向量相似度。 最初我们考虑过将向量拉取到Python应用层,再用NumPy或专门的向量库计算余弦相似度。但考虑到一次查询可能需要比对成千上万个向量,网络I/O和序列化/反序列化的开销会非常大。最终,我们利用PostgreSQL的数组操作和立方体(cube)扩展,直接在SQL中完成向量相似度计算。一句 ORDER BY embedding <=> query_embedding LIMIT K 就能高效完成最近邻搜索。这避免了在海量数据中移动数据的性能瓶颈,也是我们选择PostgreSQL而非更简单的键值存储的重要原因之一。

2.2 行级安全:将数据权限防线推进到数据库

如果说Schema设计决定了数据的形状,那么 行级安全(Row Level Security, RLS) 则决定了数据访问的边界。我们在Supabase中为每一张表都启用了RLS,并编写了精细的策略(Policies)。

这意味着,权限检查不再仅仅依赖于我们手写的应用层业务逻辑。即使某处API路由的代码因为疏忽忘了检查用户权限,试图执行 SELECT * FROM pdfs ,数据库也会根据当前连接的用户ID(通过Supabase的 auth.uid() 获取),自动过滤掉不属于该用户的记录。一个学生永远无法通过任何方式(包括直接连接数据库)查看到其他同学的搜索历史或上传的PDF。

实操心得:RLS的代价与收益。 启用和调试RLS策略确实花了将近一天的时间,它增加了Schema设计的复杂性。但在我看来,这笔投资回报率极高。在开发后期,它就至少阻止了两次因代码逻辑不严谨而可能导致的数据泄露风险。它相当于在应用逻辑这堵“墙”后面,又加装了一道数据库级别的“铁门”,实现了深度防御。对于教育这种涉及敏感数据的领域,这种级别的安全冗余是必要的。

3. 核心服务集成与异步任务管理

3.1 记忆服务集成:让AI拥有“记忆力”

记忆功能是EduRag区别于普通问答机器人的关键。我们集成了Hindsight作为外部记忆服务,我为此构建了四个核心端点:

  1. /memory/status :健康检查。
  2. /memory/retain (保留) :在每次成功的RAG搜索后自动调用。它接收一个结构化的“事实”字符串(例如“学生查询了生物化学中的酶抑制机制”),并将其存储到该用户专属的Hindsight记忆库中。每个用户通过我们的用户ID标识其独立的记忆库。
  3. /memory/recall (回忆) :在RAG检索流程中被调用。前端查询传入后,除了搜索向量数据库,还会调用此端点。Hindsight会返回与当前查询语义相关的历史记忆“事实”,这些事实会被注入到最终发给大语言模型的提示词中,使回答更具连续性和个性化。
  4. /memory/reflect (反思) :一个由管理员触发的操作,请求Hindsight对某个用户的整个记忆库生成一个AI总结。教师可以通过管理面板查看某个学生学习了哪些主题、频率如何、在何处参与度下降。这个仅用约20行后端代码实现的功能,后来成了最受教师欢迎的管理功能之一。

避坑指南:外部服务调用的超时与降级。 我为 recall 端点设置了6秒的超时( HINDSIGHT_TIMEOUT )。记忆功能是增强体验的,而不是核心路径的阻塞点。如果 recall 因网络或服务问题超时,查询会 自动降级 ——继续执行常规的RAG流程,只是缺少了记忆上下文,而不是向用户返回一个错误。这确保了核心功能的可用性。

3.2 那个耗费一整天的PDF索引Bug

这是我印象最深的一个故障,根本原因在于对异步任务生命周期的错误理解。

现象 :教师反馈,有些PDF在后台显示“已索引”,但学生搜索时却返回无结果。数据库 pdfs 表的 indexed_at 时间戳已更新,文件也在存储桶里,但 rag_embeddings 表里就是没有对应的向量数据。

排查过程 :我一开始在索引流程的每一步加日志,手动跑了三遍,一切正常。向量生成和存储都没问题。无法复现故障。后来才意识到,问题只在 并发 时出现。当两位老师同时上传并触发索引时,其中一份的向量写入会神秘丢失。

根本原因 :为了不阻塞API响应,我使用了 asyncio.create_task() 来将耗时的向量生成和存储操作扔到后台执行。路由处理函数立即返回200成功。然而,我没有 await 这个任务,也没有任何机制跟踪它的完成状态。当主请求上下文结束后,这个后台任务有时会被垃圾回收机制中断,导致其中的Supabase写入操作半途而废。任务引用丢失了,失败也是静默的。

解决方案

  1. 对于小文件 :我将嵌入任务改为在路由处理函数中 同步 执行,接受由此增加的延迟,换取确定性。
  2. 对于大文件 :端点返回 202 Accepted 和一个任务ID。前端(教师仪表板)轮询一个状态端点来获取索引进度。
  3. 显式化失败 :在 pdfs 表中增加了 failed_at error_message 字段。任何索引失败都会记录在此,让问题从“隐身”变为“可见”。

核心教训:区分“尽力而为”和“必须完成”的任务。 asyncio.create_task() 适合用于真正的“发后即忘”型任务,比如清理临时缓存、发送非关键的通知。而对于涉及数据持久化、状态变更的核心业务操作,必须有完整的生命周期管理和错误处理机制。要么同步执行并妥善处理超时,要么引入可靠的任务队列(如Celery、RQ)。

4. 保障系统稳定与安全的“无聊”配置

4.1 速率限制:晚做不如早做

我是在项目后期才加入速率限制的,现在想来有些后悔。我们使用 SlowAPI 这个库,它与FastAPI中间件链集成,实现了基于令牌桶算法的IP级限流。

如何确定“60次/分钟”这个阈值? 这不是拍脑袋决定的。我们模拟了一个典型场景:一间有30个学生的实验室,在30分钟的课程内同时使用平台进行查询。峰值请求大约在每分钟30次左右。将限制设为60次/分钟,既为正常的课堂活动留出了充足余量,又能有效阻止恶意的脚本攻击或意外循环调用。我们通过压力测试发现,低于这个值,正常课堂流量偶尔会被限制;高于这个值,在遭受攻击性负载测试时,后端的Gemini API调用成本会变得不可控。

4.2 安全头与CORS:半小时能搞定的事,别拖两周

SecurityHeaders 中间件负责为每个响应添加 Content-Security-Policy HSTS X-Frame-Options 等头部。这些头部能防御一系列应用层逻辑无法单独抵御的客户端攻击(如点击劫持、某些类型的XSS)。实现它只花了大约30分钟,但我却因为“不紧急”而推迟了两周。这应该是在项目第一天就完成的事情。

CORS配置的教训 :我们的前端部署在Vercel上,这意味着原点(Origin)在开发环境( localhost:3000 )、预览环境(随机的Vercel子域名)和生产环境(固定域名)下是不同的。为了让CORS在所有环境正常工作,我不得不在FastAPI中间件中明确配置允许的来源列表,并制定清晰的规则:预览环境的来源在 staging 阶段被允许,但在生产配置中被严格排除。事先规划好多环境策略,能省去不少调试时间。

5. 实时交互与功能降级设计

5.1 WebSocket聊天室:连接管理与自动过期

平台包含一个学生实时聊天室。我使用FastAPI的WebSocket支持,编写了一个 ConnectionManager 类来管理活跃连接(用一个以用户ID为键的字典存储)。身份验证在握手时通过URL查询参数传递的JWT完成,因为WebSocket连接建立后无法再设置HTTP头。

技术难点:消息的自动过期 。我们不希望聊天记录成为永久数据,因此设计了消息在存活一定时间(TTL)后自动删除的机制。我实现了一个每60秒运行一次的后台任务,删除Supabase中过期的消息。这里我再次使用了 asyncio.create_task() ——是的,就是导致索引Bug的那个模式——但在这里是合适的。因为消息过期是一个真正的“尽力而为”的操作。即使某次清理循环错过了,也不会造成数据不一致或功能损坏,只是消息留存得久了一点。这再次印证了那个原则: 关键数据操作必须可靠,“锦上添花”的操作可以异步化

5.2 功能开关:HINDSIGHT_ENABLED的启示

开发过程中有一个下午,Hindsight API服务不可用大约两小时。我发现,仅仅因为每次搜索后自动调用的 retain 端点抛出未处理的异常,就导致整个RAG搜索接口返回500错误。记忆功能本应是增强项,却拖垮了核心功能。

我当即引入了 HINDSIGHT_ENABLED 这个环境变量作为功能开关。当设置为 false 时,所有对记忆服务的调用都会被静默跳过。系统进入“无状态”模式:RAG搜索照常工作,推荐功能回退到全局热门话题,记忆相关的UI显示友好的空状态。没有错误,没有功能降级,只是暂时少了些个性化特性。

设计原则:外部依赖必须可隔离。 任何引入外部依赖的功能,都不应该以牺牲核心系统的可靠性为代价。必须为运维人员提供一个干净的“开关”,使其能在依赖服务出现问题时,快速切断故障点,保障主体服务可用。如果你的功能无法被干净地关闭,那么你就将系统的可靠性绑在了第三方服务的稳定性上。

6. 隐私保护与数据脱敏

在将数据发送到外部记忆服务(Hindsight)之前,我们强制进行了一次 PII(个人身份信息)脱敏 处理。 retain 路径上集成了一個函数,通过正则表达式扫描即将存储的“事实”字符串,剥离其中的邮箱地址、电话号码等敏感信息。

这个函数不是可选的,它被硬编码在写入路径中。将包含学生PII的数据存储到外部服务,是一个绝不能靠文档约定或开发者自觉来保障的风险点。 隐私保护必须通过代码路径来强制实施,而不是依靠可能没人会仔细阅读的文档。 这是我们在处理教育数据时的一条铁律。

7. 总结与可复用的经验清单

回顾整个EduRag后端基础设施的构建过程,以下这些经验我认为具有普适性,值得传递给其他开发者:

  1. 数据库权限要“深” :像Supabase的行级安全(RLS)这样的特性,虽然增加了一些设计复杂度,但能提供应用层无法比拟的数据安全保证。对于多租户或涉及敏感数据的应用,值得投入。
  2. 异步任务需“慎”用 :严格区分任务的紧要程度。对于需要保证完成的核心操作(如订单创建、数据索引),避免使用 asyncio.create_task() 这类“发后即忘”的模式。采用同步执行(配合超时)或引入可靠的消息队列。
  3. 失败要“可见” :对于任何代表异步操作状态的数据库表(如 pdfs ),增加 failed_at error_message 这样的字段。将静默失败转化为显式失败,是快速定位和修复问题的前提。
  4. 安全与限流要“早” :速率限制和安全头部这类基础设施,实现成本低,但事后补充的心理压力和风险更高。应该在项目初期就作为基础组件引入。
  5. 外部依赖要“可切” :为集成的外部服务设计功能开关。确保在第三方服务不可用时,你的核心功能可以优雅降级,而不是完全崩溃。
  6. 隐私处理要“硬编码” :涉及用户隐私的数据流出(如发送到外部API),脱敏逻辑应该作为代码路径中的强制步骤,而不是可配置或可选的选项。

后端的工作很少成为演示的焦点,没人会在产品发布会上讲解RLS策略或令牌桶算法。但这没关系。我们的职责就是让那些能上演示的功能正确、可靠地运行,并确保当问题出现时——问题总会出现的——故障是可见的、可控的、且可恢复的。EduRag的记忆基础设施是我最满意的部分,并非因为它技术多复杂,而是因为它直接改变了产品能为学生带来的价值。能产生可见用户价值的基础设施,就是最好的基础设施。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐