
引言为什么我要把 AIFriends 的架构和流程单独写成一份复习文档如果你最近在准备 AI 应用方向的面试或者正在从零搭一个带社交属性的 AI 产品那 AIFriends 这个项目应该是个不错的参考样本。它不算特别复杂但麻雀虽小五脏俱全——涵盖了用户体系、AI Agent 交互、消息推送、积分经济系统、管理后台这些典型模块而且业务闭环是完整的。说实话我最初接到这个项目的技术梳理任务时第一反应是“这不就是个套壳聊天应用吗”。但真正把架构图和业务流程逐层拆开之后我才发现里面的设计取舍比预想中多得多。比如 AI 人格的会话上下文怎么管理、异步任务队列怎么设计、积分扣费怎么避免并发超扣、管理后台的审核流怎么和用户侧状态机联动——这些都是实际业务里绕不开的细节也是面试官最喜欢深挖的点。这份文档的定位是“复习用的”所以我会刻意把每个模块的架构决策、流程节点、数据流转逻辑都讲透而不是只罗列一堆技术名词。不管你是想照着这个思路做自己的 AI 社交产品还是单纯想理解 agent 类项目的通用架构模式这篇内容应该都能帮你省下不少瞎琢磨的时间。1. 项目整体设计与架构思路拆解1.1 项目定位与核心需求解析AIFriends 本质上是一个“AI 虚拟好友社交平台”。用户注册之后可以创建或者选择不同的 AI 好友角色和这些角色进行多轮对话。这些 AI 好友不是简单的问答机器人而是有固定人设、记忆能力、情感反馈的 agent 形态产品。从业务需求倒推技术需求核心要解决四件事多角色 AI 人格的管理每个 AI 好友拥有独立的 prompt 模板、知识库、语气风格甚至记忆片段对话过程的稳定性多轮对话不能丢上下文消息要能实时送达网络抖动不能直接吞消息商业化闭环免费用户每天有对话次数限制付费用户解锁无限畅聊需要一套积分/会员系统内容安全AI 生成内容不能失控需要前置审核和后置举报机制这个定位意味着架构不能只考虑“能跑”还要考虑“能管”。很多 AI 应用死在两个地方一个是上下文管理乱导致体验崩塌另一个是商业化路径不清晰导致产品没法续命。AIFriends 的设计从一开始就把这两条线拉了进来。1.2 整体架构分层与模块划分整个系统采用前后端分离 微服务化的架构风格但并没有一上来就拆十几二十个服务——那对小团队和快速迭代来说反而是灾难。它按照业务域拆成了六个核心服务服务模块职责范围关键技术点用户服务注册、登录、个人资料、好友关系JWT 鉴权、Redis 会话缓存AI Agent 服务角色人格管理、对话生成、上下文管理LLM 接入层、Prompt 模板引擎、向量记忆库消息服务实时消息收发、消息持久化、已读回执WebSocket 长连接、消息队列削峰订单/积分服务积分充值、对话扣费、会员套餐事务性扣费、幂等性保障审核服务用户输入侧敏感词、AI 输出侧合规过滤异步审核队列、人工审核工作台管理后台服务角色管理、用户管理、数据看板、审核处理RBAC 权限模型、操作日志审计每个服务独立部署、独立数据库用 HTTP/REST 作为服务间同步调用的主要协议异步场景走消息队列。没有引入太重的 Service Mesh也没有强行上分布式事务而是尽量把跨服务的数据一致性控制在“最终一致”的范围内。提示这个架构的聪明之处在于边界划分基本沿着“业务对象”走而不是沿着“技术层次”走——按用户、对话、消息、钱、内容安全来分每一个服务都能独立演进这是新手做架构时最容易忽略的一点。1.3 为什么选这个架构方案而不是单体应用有人可能会问一个 AI 聊天产品单体应用不香吗前期开发效率更高部署也更简单。这个问题的答案在于“业务预期的变化方向”。AI 对话类产品有三个天然特点第一LLM 调用的延迟和成本是波动的需要独立的服务来做限流、降级、重试策略第二对话上下文可能包含大量的个性化记忆数据存储层需要能够灵活扩容第三消息实时通道和业务 API 的负载特征完全不同——WebSocket 连接是长驻型的而业务 API 是突发型的放在同一个进程里互相拖累就是必然的。所以在 AIFriends 的架构里消息服务被单独拆了出来这是一个非常正确的决策。长连接服务不会被业务接口的突发流量打垮业务服务也不会因为消息广播的逻辑 bug 而全部宕机。你要做 AI 应用消息通道和业务逻辑分离这条底线最好从一开始就守住。2. 核心业务流程拆解与设计逻辑2.1 用户从注册到首次对话的完整链路一个用户从进入产品到产生第一段对话背后走的是这样一条链路用户通过手机号或第三方账号注册用户服务创建账号并返回 JWT Token前端携带 Token 调用 AI Agent 服务拉取可选择的 AI 好友列表用户选择某个 AI 好友点击“开始聊天”前端发起 WebSocket 连接消息服务完成连接鉴权和会话绑定用户发送第一条消息消息服务先落库再转发给 AI Agent 服务AI Agent 服务加载该好友的人格 prompt、历史记忆、当前会话上下文调用 LLM 生成回复回复内容先送审核服务做合规检查通过后由消息服务推送给用户这个流程看着不复杂但有两个细节值得展开。第一个细节是“先落库再转发”。很多初版实现图省事直接走内存转发消息一旦服务重启就丢。AIFriends 的做法是用户消息到达消息服务之后立刻写数据库状态标记为“已发送”等 AI 回复生成后再把这一轮会话的状态更新为“已完成”。这样即使中间任何一环挂了消息也不会丢用户刷新页面还能看到历史记录。第二个细节是 AI 回复的审核时机。这里是在“生成后、推送前”审核不是生成前拦截。原因很简单生成前的 prompt 审核只能拦住输入侧的问题但 LLM 的输出不可完全预测所以输出侧必须有一道独立检查。虽然这样会增加用户等待时间但安全合规这条线不能省。2.2 AI 对话流程中的上下文管理与记忆机制AI 好友和普通聊天机器人的最大区别在于“记忆”。AIFriends 的 agent 设计里记忆分为三个层次短期会话记忆存储在当前 session 内记录最近的对话轮次保存在 Redis过期时间 30 分钟长期用户记忆记录用户的基本喜好、性格标签、说过的重要信息写入向量数据库角色设定记忆AI 好友自身的人设背景、说话风格、知识边界作为系统级 prompt每次用户发消息时AI Agent 服务会执行一个“记忆组装”流程从 Redis 取出短期会话记录从向量库检索与当前话题相关的长期记忆片段然后把角色设定、短期上下文、相关记忆拼装成最终的 prompt 发送给 LLM。这个设计解决了一个核心矛盾LLM 的上下文窗口是有限的你不能把用户全部历史对话都塞进去必须做有选择性的提取。向量检索在这里起到的作用就是“只挑和当前话题有关的记忆”既控制 token 数量又让回复看起来是“记得你”的。注意记忆检索是 AI 社交产品体验的分水岭。很多团队前期为了省事只往 prompt 里塞最近 N 轮对话结果 AI 聊了三天就把用户的生日、喜欢的音乐类型全忘了用户立刻就会觉得“这是个假 AI”。记忆机制不是锦上添花是产品能不能留住用户的关键。2.3 积分扣费与会员体系的流程设计商业模式部分AIFriends 走的是“免费次数 积分充值 会员订阅”三者结合的路子。积分扣费流程是这里面最容易出并发问题的地方。每次用户发送一条消息会先经过积分服务的预扣费操作。这个预扣费不是直接扣余额而是先冻结对应积分等 AI 回复成功推送给用户后再转正式扣费如果 AI 生成失败则解冻退回。这样做的好处是避免“用户发了消息但 AI 没回复钱却已经扣了”的客诉。在技术实现上扣费用的是 Redis 的 Lua 脚本做原子操作先检查余额再扣减保证并发场景下不会超扣。数据库层面记录每一笔积分流水方便后续对账和客服查询。会员体系则简单一些会员用户在有效期内不限制对话次数但会有每日最大消息数的风控上限防止接口被脚本刷爆。会员状态存在用户服务的缓存里AI Agent 服务每次收到对话请求时会校验会员资格。这个积分流程设计里最值得学习的一点是“预冻结”的思路。真实业务里凡是涉及“扣钱 异步结果”的场景都应该考虑这个模式。直接先扣款再退款也不是不行但用户体验和客服压力完全不是一个量级。3. 关键模块的技术实现与实操要点3.1 AI Agent 服务中 Prompt 模板的角色管理实现AI 好友的人格差异完全靠 Prompt 工程来实现。AIFriends 里每个 AI 角色对应一个 Prompt 模板模板不是死字符串而是支持变量的结构化配置。举一个实际的模板片段你是{character_name}年龄{age}性格{personality}。 你正在和用户进行一场{relationship_type}的对话。 【背景记忆】 {memory_snippets} 【近期对话】 {chat_history} 【用户画像】 {user_profile} 请用{style_guide}的风格回复回复长度控制在{max_tokens}字以内。这里的变量分别从角色配置表、记忆检索结果、会话上下文、用户画像服务中动态填充。模板本身存放在数据库里而不是硬编码在代码里这样运营人员可以在管理后台直接调整某个 AI 角色的人设不需要重新发版。技术实现上有一个容易踩坑的点模板变量的注入顺序会影响 LLM 的输出质量。AIFriends 的实测经验是“角色设定放最前面用户画像放最后”效果最好因为 LLM 对 prompt 开头和结尾的信息注意力更强把最核心的人设约束放在开头把对当前回复影响最大的用户信息放在结尾回复质量的稳定性会有明显提升。3.2 消息服务的推送机制与接口设计消息服务需要同时支持 WebSocket 长连接和 HTTP 回调两种消息下发方式。WebSocket 用于实时推送HTTP 回调主要用于第三方渠道或者前端断线重连后的消息补偿拉取。实际的接口设计大概是这样的// 发送消息请求 POST /api/v1/chat/message { session_id: uuid, session_type: ai_friend, content: 今天心情不太好, message_type: text } // 响应 { message_id: uuid, status: pending, estimated_reply_time_ms: 3500 }这里用 message_id 做全链路的追踪标识。消息从客户端发出到 AI 回复返回中间经过消息服务、Agent 服务、审核服务所有的状态变更都通过这个 message_id 关联。前端可以轮询或者通过 WebSocket 推送收到状态更新当状态变为“completed”时渲染 AI 回复。关于 WebSocket 连接管理AIFriends 用了 Redis Pub/Sub 做多实例消息广播。单台实例只维护自己节点上的客户端连接但服务端要推送某条消息时通过 Redis 频道广播所有实例收到后只推送给本地持有对应 session 的连接。这个方案比自研一套消息路由协议简单得多实测在几千并发连接下完全够用。3.3 审核服务的异步处理链路审核服务在 AIFriends 里是独立部署的没有嵌在 Agent 服务的同步调用链里。设计成异步的原因很简单LLM 回复已经要花 2-5 秒了如果审核再同步加 500 毫秒用户体感会明显变差。异步审核的流程是AI Agent 服务生成回复后把回复内容投递到审核消息队列审核服务消费队列先做机器敏感词过滤再调用内容安全 API 做语义级检测如果自动审核通过直接把消息推送给用户如果疑似违规进入人工审核队列人工审核完成后审核结果通过回调接口告知消息服务决定放行还是拦截这个链路里有个取舍问题用户体验和合规风险怎么平衡。AIFriends 的做法是“低风险秒放行高风险进人工”。机器审核认为没有问题的消息直接推送概率极低的高危内容即使要等人工审核也绝对不能放出来。还有一个细节是审核服务的降级策略。如果内容安全 API 调用超时不能无限阻塞消息推送可以设置一个超时阈值比如 800ms超时后先标记为“待复核”放行消息后续在后台异步补充审核。属于业务向安全做适当妥协的经典做法不能因为审核系统的问题把整个对话功能拖死。4. 实操过程与核心环节实现复盘4.1 从零搭建环境与依赖服务如果你要复现这套架构本地开发环境建议这样搭建。基础设施部分Docker Compose 是起步的正确选择。AIFriends 依赖的中间件主要包括PostgreSQL业务数据、Redis缓存和 Pub/Sub、RabbitMQ异步任务队列、Milvus 或 Chroma向量记忆库、MinIO对象存储用于存用户头像和对话附件。按依赖顺序启动容器# 1. 启动基础设施 docker compose up -d postgres redis rabbitmq minio # 2. 启动向量数据库无 GPU 需求CPU 模式即可 docker compose up -d chroma # 3. 初始化数据库表结构 cd services/user-service alembic upgrade head cd services/message-service alembic upgrade head # 4. 启动各个微服务 cd services/ai-agent python app.py --port 8001 cd services/message python app.py --port 8002 cd services/order python app.py --port 8003一个容易踩坑的点是数据库初始化顺序。user-service 和 message-service 之间有外键关联吗从架构上看没有——各服务库都是独立的所以不存在严格的建表顺序依赖。但如果你的实现里跨库引用了记得先起的服务要容忍关联表暂不存在的异常或者用事件机制等依赖服务就绪。4.2 核心服务间的接口约定与联调记录服务间通信的接口约定是整个项目里最需要提前锁死的东西。我在实际联调中吃过亏两个服务各自开发到联调阶段发现字段命名不一致、状态码语义不一致返工成本极高。AIFriends 的约定大致如下服务间 API 统一走 /api/v1/ 前缀内部调用带 internal-token 头与用户侧 JWT 区分用户 ID 和会话 ID 统一用雪花算法生成不用数据库自增 ID避免跨服务暴露业务量状态码统一使用 0 表示成功非 0 为业务错误码HTTP 层面只区分 2xx 和 5xx关键链路发送消息、扣费必须打印链路 TraceID日志格式统一为 JSON联调时最耗时间的是 WebSocket 消息时序问题。我在本地模拟了弱网环境做测试发现偶发的消息延迟会打乱前端渲染顺序——用户发了消息 A 和 BAI 先回复了 B 再回复 A对话顺序就乱了。解决办法是在消息体里加一个 client_msg_seq 字段前端本地维护递增序号渲染时先按 seq 排序再展示。这个字段在初版设计里完全没考虑属于联调时踩坑后补的。4.3 LLM 接入层设计与 Key 池管理AI Agent 服务的核心是 LLM 接入层这块做得不好再好的 prompt 也白搭。AIFriends 没有直接在各处硬编码 LLM API 调用而是抽象出了一个统一的 LLM Gateway。这个 Gateway 做了三件事多厂商模型路由不同 AI 角色可以配置不同的模型比如知识型角色用更强的模型闲聊型角色用更快更便宜的模型Gateway 根据角色配置做路由Key 池化管理多个 API Key 轮询使用某个 Key 触发限流时自动切换下一个并标记该 Key 冷却超时重试与降级单次 LLM 调用最长等待 15 秒超时后自动重试一次切换 Key仍失败则返回友好话术给用户并把这条消息标记为“ai 不可用”Key 池管理是很多团队会忽视的模块。LLM 供应商的限流策略和成本控制直接关系到项目能跑多久Key 被限流了没有自动切换机制整个对话功能就是瘫痪的。这个模块用最简单的轮询 冷却期就够了不必引入太复杂的负载均衡策略。4.4 部署架构与配置管理的实战建议部署方面AIFriends 用 Docker Compose 做单机编排生产环境建议升级到 Kubernetes。但要注意的是微服务架构里服务拆得越多部署和排障成本越高。我的建议是如果你的团队只有两三个人前期用 Docker Compose 部署在单台 4C16G 的服务器上撑住几千 DAU 完全没问题。等用户量起来之后再迁 K8s利用 namespace 和 deployment 的滚动更新能力来降低发版风险。配置管理上强烈建议用环境变量 配置中心的方式不要硬编码在代码里或者写死在配置文件中。至少要把数据库连接串、Redis 地址、LLM API Key 这些敏感配置单独抽离生产环境用 K8s Secret 或在配置中心加密存储。5. 常见问题与排查技巧实录5.1 消息丢失与重复推送问题这类问题在我实际运行中遇到得最多尤其是消息丢失。现象用户发了一条消息客户端状态一直停在“发送中”数据库里没有这条记录。排查路径先看 Nginx 和网关的访问日志确认请求是否到达消息服务确认消息服务日志里有没有对应的消息事件查数据库对应表看是否有记录但状态异常如果服务日志都没有基本可以断定是前端没发出来或者网关丢包优先查前端逻辑处理方案客户端新增失败重试机制超时 5 秒自动重发并带上 client_msg_seq 去重服务端在消息表增加唯一索引session_id client_msg_seq从根本上杜绝重复入库。重复推送的问题更隐蔽。用户发送一条消息AI 回复生成后 WebSocket 推送了一次前端断线重连后 HTTP 补偿拉取又拿到了一次导致对话界面出现两条相同的回复。解决办法是在前端维护已渲染消息的 ID 集合渲染前先查重。5.2 LLM 响应超时与假死问题AI Agent 服务调用外部 LLM 时最常见的故障就是响应超时。这里的超时场景和常规数据库超时不一样外部 LLM 服务可能因为自身负载高而响应极慢或者长时间没有返回任何流式数据。排查建议给 LLM 调用设置多级超时连接超时3 秒、首 token 等待超时10 秒、整体响应超时30 秒对同时发起的并发请求数做信号量控制超出排队等待防止 LLM API 被瞬时打满流式模式下如果超过 30 秒没有任何 token 返回直接中断该次生成返回一个兜底文案给用户还有一个容易被忽略的点LLM 调用线程池的大小设置不合理会导致服务整体线程阻塞。AIFriends 踩过这个坑最初设置的线程池太小某个时刻用户量上来所有线程都阻塞在 LLM 调用上健康检查接口都没有空闲线程响应了。解决方法是把线程池换成 IO 密集型的 ThreadPool并且给健康检查接口单独留出线程配额。5.3 积分扣费不一致的排查记录积分扣费不一致主要体现在三种情况用户发送消息后余额扣了但 AI 回复没有生成用户并发发送多条消息只成功扣了部分积分对账发现积分流水和订单记录金额不匹配第一种情况在预冻结模式下很少出现但如果出现大概率是冻结转扣费的流程没走完。排查时看订单表的状态如果订单停在“frozen”状态超过 30 分钟手动补一个任务把它转成“failed”并解冻。第二种情况一般是 Redis Lua 脚本在并发下执行了正确性校验但 application 层把返回结果做了错误的空值处理。这种问题需要看具体代码但排查思路是在测试环境用并发工具比如 jmeter 或 go-wrk模拟 50 并发发消息看 Redis 的扣减记录和数据库的流水是否一致。5.4 审核服务堆积导致消息延迟推送当内容安全 API 调用波动时审核队列会积压导致 AI 回复迟迟推不到用户端。处理优先级从高到低检查内容安全 API 的调用成功率如果 API 本身降级了自动切换备用供应商调整审核服务的消费并发数加大消费线程池把积压消化掉动态调整超时阈值把自动放行的判断标准适当放宽比如超时 1 秒就标记待复核放行人工审核队列单独限流防止操作员处理不过来之后积压到爆这个问题的核心是“审核不能阻塞主流程”。AI 对话产品对实时性的要求很高一条消息 10 秒内不回复用户基本就流失了。所以审核链路一定要和业务主链路解耦宁可放进来再处理也不能让用户一直等。5.5 新增 AI 角色的配置冷启动运营在管理后台新建一个 AI 角色用户端立刻就能看到并开始聊天但实际体验可能会很差——因为这个角色还没有积累任何记忆也缺少对话样本。这就需要一个冷启动策略。AIFriends 的做法是给新角色填充“种子对话数据”在角色上线前运营配置 20-30 组预设问答对写入角色的记忆库作为初始记忆。这样用户第一次和新角色聊天时角色就已经“知道”一些关于自己的背景信息不会出现一问三不知的情况。技术上实现很简单就是往向量库写入一批初始文档同时更新角色配置表里的 greeting_message 和 初始人设关键词。这里有一个典型坑新角色上线后由于没有历史对话数据向量检索可能返回空结果Prompt 模板里的 memory_snippets 变量为空导致 LLM 生成的回复极其干瘪。所以模板渲染时要兼容空变量的情况为空时直接省略该段提示而不是输出一行空字段。5.6 问题排查速查表症状可能原因快速排查方向用户消息发送失败WebSocket 连接断开检查实例连接数、Redis Pub/Sub 频道是否存在消息收到但 AI 未回复Agent 服务调用 LLM 超时查看 LLM Gateway 的日志和耗时指标AI 回复了但用户没收到审核服务拦截或推送失败查审核队列状态和消息服务推送日志积分被扣但 AI 没回复预冻结后未正常转扣费查订单状态手动补偿解冻用户反馈 AI 记忆混乱向量检索质量低或上下文拼接错误查记忆检索排名情况和 Prompt 组装结果管理后台角色修改未生效角色配置缓存未刷新检查 Redis 缓存 key 的过期策略和手动刷新接口6. 踩坑记录与架构演进复盘6.1 初版单体应用到微服务拆分的迁移逻辑AIFriends 最初的核心对话功能其实是一个单体应用代码量到两万行左右时就明显吃力了。最典型的问题是消息推送的 WebSocket 连接和业务 API 共享进程一旦某个业务接口出现慢查询GC 停顿时间变长WebSocket 心跳就会断客户端表现为“掉线”。迁移到微服务之后最直接的收益不是性能提升了多少而是故障隔离做得更好了。消息服务宕机用户还能正常登录浏览Agent 服务依赖的 LLM 供应商故障其他服务完全不受影响。这种隔离能力对在线业务来说比单纯的性能优化重要得多。另外拆分之后每个服务的数据结构可以做针对性优化。用户服务用关系型存储用户画像Agent 服务用向量库支撑记忆检索订单服务用事务性数据库保证资金安全。数据模型跟着业务属性走而不是被一个“大而全”的库绑架。6.2 数据一致性与多服务事务的取舍微服务架构里最烦人的问题是跨服务的数据一致性。AIFriends 没有引入分布式事务框架而是用“本地事务 消息事件”的模式保证最终一致。举例来说用户充值的流程订单服务在本地库创建订单状态为“待支付”支付回调成功后订单服务更新订单状态并发送“支付成功”事件到消息队列积分服务消费事件给用户增加积分并写入积分流水如果步骤 2 之后、步骤 3 之前积分服务恰好宕机用户支付了但积分没到账。解决方式是引入一个对账补偿任务——定时扫描“支付成功但积分未增加”的订单重新发送事件。这种模式的代码量不大但能覆盖绝大多数故障场景比引入 Seata 这类分布式事务中间件的成本低得多。6.3 我对 AI 社交类项目架构扩展性的心得这类项目的扩展路线大致可以分三个阶段。第一个阶段是把核心对话链路跑通重点在于 prompt 质量和记忆机制第二个阶段是完善商业化和安全体系积分、会员、审核、管理后台一个都不能少第三个阶段才是规模化引入更多的 AI 角色类型、多人聊天室、UGC 社区等。如果一开始就按第三个阶段的标准做架构设计大概率会过度设计。AIFriends 的做法是我比较认同的按业务自然增长逐步演进但每个阶段都预留好扩展点——比如 Prompt 模板从一开始就可以配置化向量记忆库一开始就独立存储消息服务从一开始就支持多实例广播。我个人在实际项目中的体会是AI 产品的架构难点从来不在“怎么调用大模型”而在于怎么把大模型的输出安全、稳定、商业化地融入到你现有的业务体系里。Prompt 写得好可以让一个角色讨人喜欢但架构设计得好才能让一百万个角色同时活着且不出乱子。