AI 机器人项目设计详解
对应项目:
这篇文档专门解决一个高频面试问题:
面试官问你“这个 AI 项目是怎么设计的”,你怎么讲,才能显得你不是只会调模型接口,而是真的有系统设计能力?
这篇稿子会重点展开:
- 项目定位和业务目标
- 为什么不能只做一个简单聊天接口
- 整体架构怎么拆
- 普通聊天、联网搜索、RAG 客服三条链路怎么跑
- Advisor 为什么是项目核心设计
- 数据层、文件层、流式层为什么这么设计
- 面试时怎么讲得更像自己真做过
一、这个项目本质上是什么
这个项目本质上不是一个“调用大模型 API 的 Demo”,而是:
一个基于 Spring Boot 3 + Spring AI 构建的 AI 对话平台,支持普通聊天、联网搜索增强问答,以及基于私有知识库的智能客服。
也就是说,这个项目不是只做一个 /chat 接口,而是围绕真实产品需求,把下面这些能力都做进去了:
- 会话管理
- 历史消息持久化
- SSE 流式输出
- 长上下文记忆
- 联网搜索
- 网页正文抓取与清洗
- Markdown 知识库管理
- 分片上传与合并
- 异步向量化
- RAG 检索问答
所以这个项目真正考验的是:
- AI 应用工程化能力
- 模块拆分能力
- 对话链路设计能力
- 数据和异步流程设计能力
二、面试时开场第一句话怎么说
建议你这样讲:
这个项目是一个基于
Spring Boot 3 + Spring AI + PostgreSQL/pgvector搭建的 AI 对话平台,后端主要支持三类能力:普通多轮聊天、联网搜索增强问答,以及基于私有 Markdown 知识库的智能客服。设计上我把记忆注入、联网搜索、RAG 检索、流式消息落库这些能力拆成了独立的 Advisor 和业务模块,这样系统既能满足聊天体验,也有比较完整的工程化能力。
这句话的好处是:
- 一上来就把项目定位拉高
- 直接点出“不是简单调用模型”
- 把核心设计点 Advisor 提前抛出来
三、为什么这个项目不能写成一个简单 Controller
很多 AI 项目一开始都会写成这样:
- Controller 收到用户输入
- 直接拼 prompt
- 调模型
- 把结果返回
这种写法短期能跑,但很快就会遇到问题:
- 多轮对话记忆怎么做
- 会话和消息怎么存
- 联网搜索逻辑放哪
- 知识库检索怎么接进去
- 流式输出和落库怎么兼顾
- 文件上传与向量化怎么管理
- 以后想新增能力时怎么扩展
所以这个项目没有走“单 Controller 直调模型”的路线,而是采用了:
- Controller 负责入口
- Service 负责业务
- Advisor 负责模型调用链增强
- PostgreSQL 负责业务数据和向量数据
- Event/Listener 负责异步向量化
这才是一个真正能扩展的 AI 应用后端架构。
四、整体架构怎么拆
这个项目后端可以拆成 6 层:
- Controller 接口层
- Service 业务层
- Advisor 增强链
- 数据访问层
- 文件与事件处理层
- 外部能力集成层
五、各层分别负责什么
1. Controller 接口层
核心文件:
这一层主要负责:
- 接收前端请求
- 参数校验
- 区分不同对话场景
- 组装 ChatModel/ChatClient
- 选择要挂哪些 Advisor
- 返回 REST 或 SSE 响应
也就是说,Controller 层负责“搭路由和组装调用链”,而不是承载复杂业务。
2. Service 业务层
主要负责:
- 对话新建、删除、重命名、分页查询
- 消息分页查询
- 文件检查、上传、合并、删除、分页查询
- 搜索服务
- 网页内容抓取服务
这层让业务逻辑从 Controller 中剥离出来。
3. Advisor 增强链
这是整个项目最核心的设计层。
核心类有:
- CustomChatMemoryAdvisor.java
- NetworkSearchAdvisor.java
- CustomerServiceAdvisor.java
- CustomStreamLoggerAndMessage2DBAdvisor.java
它们负责在模型调用前后做增强:
- 注入历史消息
- 注入联网搜索上下文
- 注入 RAG 检索上下文
- 流式收集模型输出并落库
4. 数据访问层
主要依赖:
- MyBatis Plus
- PostgreSQL
- pgvector
负责:
- 存储会话
- 存储消息
- 存储文件元数据
- 存储分片信息
- 存储知识库向量
5. 文件与事件处理层
负责:
- 文件分片上传
- 文件合并
- 状态管理
- 上传成功后发布事件
- 异步向量化
6. 外部能力集成层
主要是:
- OpenAI 兼容模型接口
- SearXNG 搜索引擎
- OkHttp 抓取网页
- Jsoup 清洗 HTML
六、为什么 Advisor 是整个项目最核心的设计
这是面试最值得展开的地方。
如果不用 Advisor,会发生什么?
- 记忆注入写在 Controller
- 联网搜索写在 Controller
- RAG 检索写在 Controller
- 流式落库也写在 Controller
最后就会变成:
- 一个很长的方法
- 一堆 if-else
- 不同场景互相耦合
所以这个项目把“模型调用增强”统一抽成了 Advisor。
本质上,Advisor 的作用就是:
在模型调用链的前后插入可复用的增强逻辑。
这样每个 Advisor 只负责一件事:
- 记忆 Advisor 只管“补历史消息”
- 联网 Advisor 只管“做搜索并补上下文”
- RAG Advisor 只管“检索知识库并补上下文”
- 流式落库 Advisor 只管“收集输出并落库”
这种设计的好处非常明显:
- 逻辑职责清晰
- 不同场景下可自由组合
- 新增能力时不容易破坏老链路
- 代码更符合扩展开放原则
这能非常强地体现你的架构思维。
七、普通聊天链路是怎么设计的
普通聊天对应的核心入口是:
整体链路
-
前端调用
/chat/completion -
后端解析:
- 用户消息
chatId- 模型名称
- 温度
- 是否开启联网搜索
-
构建
OpenAiChatModel -
构建
ChatClient -
根据是否联网决定挂哪个 Advisor
-
一律挂上流式落库 Advisor
-
模型开始流式输出
-
结果通过 SSE 推送给前端
-
流结束后完整问答入库
为什么这样设计
这样设计的目的,是把“普通聊天”和“增强聊天”统一到一套主流程里。
不管是:
- 纯聊天
- 联网搜索聊天
本质上都是:
- 创建模型调用
- 挂不同增强器
- 流式输出
这让主链路保持一致,扩展性很好。
八、为什么普通聊天要做“记忆注入”
如果每次请求只把当前一句话发给模型,那就不是真正的多轮聊天。
所以项目里做了:
CustomChatMemoryAdvisor
它负责:
- 根据
chatId查询历史消息 - 按时间顺序组装消息列表
- 转成模型能理解的 message 结构
- 把历史消息和当前问题一起送给模型
为什么不直接让前端传全部上下文
直接让前端每次传全量上下文,问题很多:
- 前端负担大
- 历史太长时请求体变大
- 容易出现前后端上下文不一致
- 后端不好统一控制上下文长度
而把消息存在数据库,再由后端统一拉最近 N 条消息,有几个明显优点:
- 后端可统一裁剪上下文
- 前端接口更简洁
- 历史消息可审计
- 对话可以分页查看
这也是一个很典型的“产品级设计”。
九、为什么流式输出还要做“完整落库”
项目里做了一个非常重要的设计:
CustomStreamLoggerAndMessage2DBAdvisor
它不是只负责把模型 token 流式推给前端,而是做了两件事:
- 实时向前端推送
- 流结束后,把完整问答落库
为什么不边流边写库
边流边写库的缺点很明显:
- 一条完整回答会被拆成很多碎片
- 中途断流时数据很脏
- 前端历史展示不方便
所以项目里选择的是:
- 先用
StringBuilder或类似方式累积完整内容 - 等模型输出结束后,再把用户消息和完整助手消息一起写库
这样设计的好处
- 数据库存的是完整消息,而不是碎片
- 审计和回放更方便
- 历史对话展示更自然
- 流式体验和持久化一致性兼顾
这个点非常适合在面试里展示你对“流式接口”和“落库一致性”的理解。
十、联网搜索链路为什么不是“搜一下就喂给模型”
联网搜索核心不在于“能搜索”,而在于“搜索结果怎么转成高质量上下文”。
项目里对应的核心模块是:
SearXNGServiceImplSearchResultContentFetcherServiceImplNetworkSearchAdvisor
完整链路
- 用户开启联网搜索
NetworkSearchAdvisor拦截用户问题- 调 SearXNG 搜索关键词
- 拿到一批搜索结果 URL
- 用 OkHttp + CompletableFuture 并发抓取网页
- 用 Jsoup 从 HTML 中提取正文
- 过滤无效内容,控制长度
- 把清洗后的内容拼成
context - 把
question + context塞进增强 Prompt - 交给模型回答
为什么不能直接把搜索结果标题简介塞给模型
因为:
- 搜索摘要往往不够详细
- 很多关键信息在正文里
- 标题和摘要容易误导模型
为什么不能直接把整页 HTML 塞给模型
因为:
- 噪音太多
- HTML 标签浪费 token
- 广告、导航、脚本内容会污染上下文
所以正确的工程做法就是:
- 先搜索
- 再抓正文
- 再清洗
- 最后增强 Prompt
这就是这个项目里联网搜索的设计亮点。
十一、为什么用 SearXNG
很多面试官会问:
为什么不直接用某个搜索引擎 API?
你可以这样回答。
1. 它是元搜索引擎
可以聚合多个搜索来源,对外统一成一个接口。
2. 自部署,控制力更强
比直接依赖第三方商业搜索 API 更灵活。
3. 后端对接成本低
业务层只需对接一个搜索服务。
4. 方便后续切换和扩展
以后想换搜索源,对业务代码影响小。
这体现的是“中间层抽象”的意识。
十二、为什么抓网页正文要用并发
如果串行抓 10 个网页,每个网页几百毫秒到几秒,整体响应就会很慢。
所以项目里用了:
- OkHttp
CompletableFuture- 双线程池
来并发抓取内容。
为什么这很重要?
- 联网搜索场景下,用户对延迟非常敏感
- 搜索结果条数越多,串行方式越不可接受
- 并发抓取可以显著降低整体等待时间
这说明你不是只关心“功能能不能跑”,而是考虑到了性能体验。
十三、RAG 客服链路是怎么设计的
RAG 客服对应核心模块:
- AiCustomerServiceController.java
CustomerServiceAdvisorVectorStore
核心链路
- 用户发送客服问题
- Controller 创建模型调用
- 挂载
CustomerServiceAdvisor - Advisor 根据问题去 pgvector 做相似度检索
- 拿到 topK 相关文档
- 把文档内容拼接成上下文
- 用模板把上下文和问题组合成增强 Prompt
- 交给模型生成答案
- 流式返回给前端
为什么要把客服和普通聊天分开
因为客服场景强调的是:
- 基于知识库回答
- 减少幻觉
- 输出要稳定
- 超出范围时要收口
而普通聊天更强调:
- 通用性
- 灵活回答
- 多轮交互体验
这两种场景的目标不同,Prompt 和上下文来源也不同,所以设计上要分开。
十四、为什么要把 RAG 做成单独的 Advisor
这也是一个很重要的设计点。
如果把 RAG 写死在 Controller 里,后面会遇到问题:
- Controller 逻辑越来越重
- 提示词和检索逻辑耦合
- 想切换检索策略不方便
而做成单独 Advisor 后,好处很明显:
- 检索逻辑独立
- Prompt 模板独立
- 可替换性更强
- 更方便后续扩展 metadata 过滤、不同 topK 策略、不同检索方式
这本质上是在做:
检索层和生成层之间的桥接抽象。
这个表述很适合面试。
十五、为什么 PostgreSQL 同时承担业务存储和向量存储
这个项目里 PostgreSQL 承担了两类角色:
- 业务库
- 向量库底座
业务库存什么
- 会话
- 消息
- 文件元数据
- 文件分片信息
向量库存什么
- Markdown 切分后的文档向量
这样设计的好处
1. 架构更简单
一套数据库就能支撑业务表和向量检索。
2. 运维成本低
不需要额外引入专门向量数据库。
3. 对中等规模项目足够实用
当前这种场景下,pgvector 已经可以满足需求。
4. 业务数据和知识库数据管理更统一
例如删除文件时,可以很方便地同时清理元数据和向量。
这体现的是“在当前规模下做合理技术选型”的能力。
十六、知识库文件为什么要做“分片上传 + 合并 + 异步向量化”
很多人做知识库问答时只做了一步:
- 上传文件
- 直接同步向量化
这在 Demo 里可以,但在真实系统里问题很多:
- 大文件上传不稳定
- 断点续传难做
- 向量化耗时长,会阻塞请求
- 失败后难恢复
所以这个项目做了更完整的设计。
完整链路
-
前端先调文件检查接口
-
服务端根据 MD5 判断:
- 是否已存在
- 是否支持秒传
- 哪些分片已上传
-
前端分片上传文件
-
服务端记录每个分片信息
-
分片齐全后执行合并
-
合并完成后更新文件状态
-
发布“上传完成”事件
-
监听器异步解析 Markdown 并向量化
-
更新最终状态
这样设计的价值
- 大文件上传更稳
- 可支持断点续传
- 合并和向量化解耦
- 主请求响应更快
- 状态更清晰,便于排查问题
这就是为什么这个项目看起来更像“系统设计”,而不是“AI 能力拼装”。
十七、为什么异步向量化很关键
向量化通常比较耗时,原因包括:
- 文件解析
- 文本切分
- embedding 计算
- 向量写入
- 去重判断
如果把这些都放在上传请求里同步执行,会带来几个问题:
- 接口耗时很长
- 用户体验差
- 容易超时
- 出错时主流程体验不好
所以项目里用:
- 事件
- 监听器
- 异步线程池
来做向量化。
这是一种非常典型的工程优化思路:
主流程负责接收和确认,重计算流程异步化处理。
十八、为什么要做文件状态机
一个文件从上传到最终可用,不是一步完成的。
它至少会经历:
- 上传中
- 待向量化
- 向量化中
- 完成
- 失败
如果没有状态机,会发生什么?
- 前端不知道当前文件能不能用
- 出错了不好排查
- 无法区分“还在处理中”还是“已经失败”
所以文件状态管理实际上是知识库工程化里很重要的一部分。
这也很适合面试里讲:
我们没有把文件上传看成一个简单表单动作,而是把它设计成带状态流转的生命周期管理,这样前端展示、异常恢复和问题排查都会更清晰。
十九、Prompt 为什么也算架构的一部分
在传统后端里,大家讲架构时更关注代码和数据库。
但在 AI 项目里,Prompt 其实也是系统设计的一部分。
因为它决定了:
- 模型的角色
- 回答边界
- 输出格式
- 是否允许幻觉
- 超出知识库时怎么处理
这个项目里至少有两类 Prompt 设计:
1. 联网搜索场景 Prompt
强调:
- 基于上下文回答
- 综合多个来源
- 尽量带来源信息
- 不要胡乱扩展
2. 智能客服场景 Prompt
强调:
- 角色一致
- 严格基于知识库
- 超出范围要明确收口
- 输出口径稳定
所以你在面试时可以说:
这个项目里我不是把 Prompt 当成随手写的一段文本,而是把它当成模型行为控制的一部分,不同业务场景用了不同模板,再结合 Advisor 注入上下文,来控制回答质量和稳定性。
二十、SSE 为什么适合这个项目
这个项目用了 SSE 做流式输出。
为什么选 SSE,而不是一上来就用 WebSocket?
因为这里的核心需求是:
- 用户发一次请求
- 后端单向持续返回模型生成结果
这非常适合 SSE。
SSE 的优点
- 协议简单
- 基于 HTTP,接入成本低
- 前端容易消费
- 对“模型逐 token 输出”场景很友好
为什么不是 WebSocket
WebSocket 更适合:
- 高频双向交互
- 实时协作
- 长期连接状态管理
而这个项目当前主要是单向流式回答,所以 SSE 是更轻量的选择。
这体现的是技术选型的合理性。
二十一、AI 项目的完整三条链路怎么讲
面试时最好能一口气把三条链路讲清楚。
链路 1:普通聊天
- 前端发
/chat/completion - 后端构建模型调用
- 根据
chatId拉历史消息 - 记忆 Advisor 注入上下文
- 模型流式生成回答
- 前端通过 SSE 接收内容
- 流式落库 Advisor 收集完整消息并入库
链路 2:联网搜索聊天
- 前端开启联网搜索参数
- 后端挂载
NetworkSearchAdvisor - 搜索问题关键词
- 并发抓取网页正文
- 清洗后构造上下文
- 把增强 Prompt 交给模型
- 流式返回并落库
链路 3:RAG 智能客服
- 用户进入客服场景提问
- 后端挂载
CustomerServiceAdvisor - 通过 pgvector 检索相关文档
- 构造上下文和增强 Prompt
- 模型流式回答
- 前端显示结果
如果你能顺畅讲出这三条链路,面试官基本会认可你对这个项目的掌控程度。
二十二、这个项目的亮点怎么讲
建议你重点讲 6 个亮点。
亮点 1:不是单一聊天接口,而是多场景 AI 平台
包含普通聊天、联网问答、RAG 客服三类能力。
亮点 2:Advisor 链式增强设计
记忆、联网、RAG、落库模块化,扩展性很好。
亮点 3:PostgreSQL 同时支撑业务数据和向量检索
架构简单、成本可控、适合当前规模。
亮点 4:流式输出和完整落库兼顾
兼顾用户体验和数据一致性。
亮点 5:知识库管理做了完整生命周期设计
包括文件检查、分片上传、合并、异步向量化和状态机。
亮点 6:联网搜索不是简单摘要拼接,而是正文抓取和清洗
质量更高,也更像真实产品能力。
二十三、这个项目最容易被追问的难点
难点 1:如何让普通聊天真正具备“记忆”
难点在于:
- 历史消息如何持久化
- 怎么控制上下文长度
- 怎么在每次请求时回放历史
你的解法:
- 会话和消息落 PostgreSQL
- 通过 Advisor 按
chatId拉最近 N 条消息注入上下文
难点 2:流式输出和落库如何兼容
难点在于:
- 流式输出是碎片化的
- 持久化更需要完整消息
你的解法:
- 先流式返回
- 同时累积完整内容
- 结束后统一落库
难点 3:RAG 不只是检索,还要处理工程链路
难点在于:
- 文件上传
- 文件切分
- 向量化耗时
- 去重
- 状态维护
你的解法:
- 文件服务 + 事件驱动 + 异步向量化 + 状态机
难点 4:联网搜索结果如何变成高质量上下文
难点在于:
- 搜索结果摘要信息不足
- 原始 HTML 噪音很大
你的解法:
- 搜链接
- 抓正文
- Jsoup 清洗
- 控制上下文长度
二十四、这个项目有哪些可以继续优化的地方
这个问题面试官也可能问。
你可以这样回答。
1. 记忆管理还能更精细
现在是按最近 N 条消息注入,后续可以改成:
- 按 token 长度裁剪
- 按摘要 + 最近消息混合注入
2. RAG 检索还能更强
可以继续优化:
- chunk 粒度
- topK 策略
- 元数据过滤
- rerank
3. 联网搜索可以加更强的结果排序
现在主要是搜索 + 正文抓取,后续可以做:
- 可信站点优先
- 多源交叉验证
- 摘要去重
4. 对话和知识库的观测能力还能提升
比如增加:
- 检索命中率统计
- Prompt 版本追踪
- token 消耗监控
- 用户提问分类分析
这样会显得你不仅关注当前实现,也有后续产品化思路。
二十五、面试时的 1 分钟版本
这个项目是一个基于 Spring Boot 3 和 Spring AI 的 AI 对话平台,主要支持普通多轮聊天、联网搜索增强问答,以及基于私有 Markdown 知识库的智能客服。设计上我把记忆注入、联网搜索、RAG 检索、流式落库这些能力拆成了独立的 Advisor,这样不同场景可以按需组合,而不会把所有逻辑堆到 Controller 里。数据层上我用 PostgreSQL 同时存会话、消息和知识库向量;知识库部分还做了分片上传、文件合并和异步向量化,所以整个系统既满足了聊天体验,也具备比较完整的工程化能力。
二十六、面试时的 3 分钟展开版
这个项目本质上不是一个简单的 AI 聊天 Demo,而是一个比较完整的 AI 应用后端。它主要有三类能力:普通多轮聊天、联网搜索增强问答,以及基于私有知识库的智能客服。
设计上我重点考虑的是,模型调用前后会有很多增强逻辑,比如普通聊天要带历史记忆,联网场景要先检索并抓取网页正文,客服场景要先做向量检索并构造知识库上下文,如果这些都堆在 Controller 里,代码会非常难维护。所以我基于 Spring AI 的 Advisor 机制,把这些能力拆成了独立模块,比如记忆 Advisor、联网搜索 Advisor、RAG Advisor,还有一个专门负责流式输出收集和消息落库的 Advisor。
数据层方面,我用 PostgreSQL 存会话、消息、文件元数据,同时通过 pgvector 存知识库向量。普通聊天时,根据chatId从数据库拉历史消息做上下文注入;联网搜索时,先调用 SearXNG 获取结果,再用 OkHttp 和 CompletableFuture 并发抓取网页内容,用 Jsoup 提取正文后拼进 Prompt;智能客服则通过 pgvector 检索最相关知识片段,再用固定模板约束模型基于知识库回答。
另外知识库部分我没有做成简单的单文件上传,而是支持文件检查、分片上传、合并和异步向量化,这样更符合真实系统场景。所以这个项目最核心的设计点,是把 AI 能力做成了模块化、链路化和工程化,而不只是单点调用模型。
二十七、面试官继续深挖时怎么接
他问“你这个项目最核心的设计是什么”
你就答:
- Advisor 链式增强机制
因为它把复杂的模型调用逻辑解耦了。
他问“你这个项目最难的地方是什么”
你就答:
- 流式输出和落库兼容
- RAG 工程链路设计
- 联网搜索结果清洗和上下文控制
他问“你真正做了什么”
你就答:
- 不是只写了 Controller
- 而是参与了链路拆分、存储设计、Advisor 设计、RAG 设计、联网搜索设计、文件生命周期设计
他问“这个项目和普通 AI Demo 的区别是什么”
你就答:
- 有状态
- 有存储
- 有链路增强
- 有文件生命周期
- 有流式与落库设计
- 有真实工程能力
二十八、最后一句总结怎么说
这个项目最能体现我的,不只是我会接大模型,而是我能把 AI 能力做成一个有分层、有存储、有增强链、有异步流程和有产品化思路的工程系统。