摘要
完成需求分析后,AI 后端项目需要进入架构设计阶段。架构设计要回答的核心问题不是"用哪个模型",而是系统如何拆分、各模块职责如何划分、数据如何流动、哪些能力可以独立扩展,以及出现故障时如何隔离影响。
以"企业智能客服与知识库助手"为例,一个生产级 AI 后端通常不能把用户、会话、模型调用、文件解析、向量检索、Agent 工具和监控统计全部塞进一个服务里。短期看这样开发最快,长期看会导致模型切换困难、知识库任务拖慢在线对话、业务权限和 AI 能力耦合、成本统计混乱、故障难以定位。
本文围绕模型服务、业务服务和知识库三个核心部分,系统拆解生产级 AI 后端的架构边界。文章会给出推荐模块划分、数据流、接口边界、数据库职责、异步任务、可观测性和演进路线,为后续的数据表设计、模型服务抽象、流式接口、知识库和 Agent 工具实现打基础。
一、背景与问题
1. 为什么不能从 Controller 直接调用模型
很多 AI 项目的第一个版本会写成这样:
text
Controller
├─ 接收用户问题
├─ 查询会话历史
├─ 读取知识库文件
├─ 调用向量数据库
├─ 拼 Prompt
├─ 调用模型 API
├─ 保存回答
└─ 返回结果
这在 Demo 阶段可以快速验证,但进入真实业务后会出现一堆问题:
- Controller 过重,业务逻辑难以测试;
- 模型供应商切换会影响大量代码;
- 知识库文件解析和在线对话互相影响;
- Token、费用、延迟统计分散在各处;
- 对话权限和知识库权限混在一起;
- RAG、Agent、工具调用难以扩展;
- 模型调用失败时缺少统一降级策略;
- 难以区分业务错误、检索错误和模型错误。
生产级系统必须先做边界拆分。
2. AI 后端比传统后端多了哪些架构对象
传统业务后端常见模块包括:
text
用户服务
订单服务
商品服务
支付服务
消息服务
权限服务
AI 后端还需要引入新的核心对象:
| 对象 | 作用 |
|---|---|
| 模型服务 | 统一封装大模型调用、流式输出、重试、降级和成本统计 |
| 会话服务 | 管理用户会话、消息、上下文和状态 |
| 知识库服务 | 管理文档、切片、向量化、召回和重排序 |
| Prompt 服务 | 管理系统提示词、模板、变量和版本 |
| Agent 服务 | 负责任务规划、工具选择和多步执行 |
| 工具服务 | 封装业务查询、工单创建、订单查询等可调用能力 |
| 评估服务 | 评估回答质量、召回质量和幻觉风险 |
| 观测服务 | 记录 Token、延迟、错误率、费用和调用链 |
如果这些能力没有清晰边界,系统会很快变成"AI 调用大杂烩"。
3. 本项目的架构目标
以企业智能客服与知识库助手为主线,架构目标可以定义为:
text
支持多用户、多会话
支持流式 AI 对话
支持企业知识库问答
支持模型供应商切换
支持文档异步解析和向量化
支持权限隔离和数据安全
支持调用监控和成本统计
支持后续扩展 Agent 工具
这些目标决定了架构拆分方式。
二、核心概念
1. 业务服务
业务服务负责面向用户和业务场景,处理确定性的业务逻辑。
在本项目中,它主要包括:
- 用户和租户;
- 会话和消息;
- 客服问答入口;
- 权限校验;
- 用户操作记录;
- 对话状态管理;
- 前端 API。
业务服务不应该直接依赖某个模型供应商的 SDK。它应该调用模型服务提供的统一接口。
2. 模型服务
模型服务负责封装所有模型调用细节:
- OpenAI、通义千问、本地模型等供应商适配;
- 同步对话和流式对话;
- Embedding 调用;
- 模型路由;
- 超时、重试、降级;
- Token 统计;
- 成本估算;
- 错误标准化;
- 请求日志和链路追踪。
业务服务只关心:
text
给定消息和参数,返回模型结果
不关心底层是哪个模型。
3. 知识库服务
知识库服务负责把企业文档变成可检索、可引用的知识。
主要职责包括:
- 文件上传记录;
- 文档解析;
- 文本切片;
- Embedding;
- 向量入库;
- 关键词索引;
- 召回;
- 重排序;
- 引用来源返回;
- 文档权限控制。
知识库服务既有在线接口,也有异步任务。文件解析和向量化不能阻塞用户对话主链路。
4. Agent 和工具服务
第一阶段可以先不做复杂 Agent,但架构上要预留位置。
Agent 服务负责:
- 判断用户意图;
- 拆解任务步骤;
- 选择工具;
- 调用工具;
- 汇总结果;
- 控制权限和执行边界。
工具服务负责封装业务能力,例如:
- 查询订单;
- 查询物流;
- 创建工单;
- 查询会员等级;
- 查询售后政策;
- 转人工。
Agent 不能直接访问数据库,而应该通过受控工具调用业务能力。
三、工作原理
1. 推荐的总体架构
第一阶段可以采用"模块化单体 + 清晰边界"的架构,而不是一开始就拆成很多微服务。
text
前端 Web / 管理后台
│
▼
API 网关 / 统一鉴权
│
▼
AI 后端应用
├─ 用户与权限模块
├─ 会话与消息模块
├─ AI 编排模块
├─ 模型服务模块
├─ 知识库模块
├─ Agent 工具模块
├─ 监控与费用模块
└─ 管理后台模块
│
├──────────────► 模型供应商 / 本地模型
├──────────────► 向量数据库
├──────────────► 关系型数据库
├──────────────► 对象存储
└──────────────► 消息队列
模块化单体的优点:
- 开发效率高;
- 部署简单;
- 事务和调试方便;
- 适合早期快速演进;
- 后续可以按边界拆服务。
关键是代码层面必须先拆清楚边界,不要因为是单体就把所有逻辑混在一起。
2. 在线对话链路
企业知识库问答的在线链路如下:
text
用户发送问题
↓
业务服务鉴权
↓
保存用户消息
↓
AI 编排模块加载会话上下文
↓
知识库服务召回相关片段
↓
Prompt 服务组装提示词
↓
模型服务发起流式调用
↓
后端持续推送模型片段
↓
保存助手回答
↓
记录 Token、延迟和引用来源
在这个链路里,职责要分清:
| 步骤 | 所属模块 |
|---|---|
| 鉴权 | 用户与权限模块 |
| 保存消息 | 会话与消息模块 |
| 加载历史 | 会话与消息模块 |
| 知识召回 | 知识库模块 |
| Prompt 组装 | AI 编排 / Prompt 模块 |
| 模型调用 | 模型服务模块 |
| 流式推送 | API 层 / AI 编排模块 |
| 统计费用 | 监控与费用模块 |
3. 文件入库链路
知识库文件上传后,不应该同步完成所有处理。
推荐链路:
text
管理员上传文件
↓
保存文件元数据
↓
文件进入对象存储
↓
投递解析任务
↓
文档解析
↓
文本清洗
↓
切片
↓
Embedding
↓
向量入库
↓
更新知识库状态
如果把解析、切片和向量化放在上传接口里,用户会长时间等待,接口也容易超时。
4. 模型服务的内部结构
模型服务建议再拆成几层:
text
ModelService
├─ ChatModelClient
├─ EmbeddingModelClient
├─ ModelRouter
├─ ModelProviderAdapter
├─ RetryAndFallbackPolicy
├─ TokenUsageRecorder
└─ ModelCallLogRepository
每一层的职责:
| 组件 | 职责 |
|---|---|
ModelService |
对业务提供统一入口 |
ModelRouter |
根据场景选择模型 |
ProviderAdapter |
适配不同供应商协议 |
RetryAndFallbackPolicy |
控制重试和降级 |
TokenUsageRecorder |
记录 Token 和费用 |
ModelCallLogRepository |
保存调用日志 |
这样后续从 OpenAI 切到通义千问、本地模型或企业模型网关时,不需要改业务模块。
5. 知识库服务的内部结构
知识库服务可以拆成:
text
KnowledgeBaseService
├─ DocumentService
├─ FileStorageService
├─ DocumentParser
├─ ChunkService
├─ EmbeddingTaskService
├─ RetrievalService
├─ RerankService
└─ CitationService
关键数据对象包括:
- 知识库;
- 文档;
- 文档版本;
- 文档切片;
- 向量记录;
- 解析任务;
- 召回记录;
- 引用来源。
知识库不是简单的"文件表 + 向量表",还要支持状态、版本、权限和任务失败重试。
四、实战示例
1. 包结构设计
在模块化单体中,可以先使用清晰的包结构:
text
com.example.ai
├─ user
│ ├─ api
│ ├─ domain
│ ├─ service
│ └─ repository
├─ conversation
│ ├─ api
│ ├─ domain
│ ├─ service
│ └─ repository
├─ model
│ ├─ api
│ ├─ application
│ ├─ domain
│ ├─ provider
│ └─ repository
├─ knowledge
│ ├─ api
│ ├─ application
│ ├─ domain
│ ├─ parser
│ ├─ retrieval
│ └─ repository
├─ agent
│ ├─ application
│ ├─ tool
│ └─ domain
├─ observability
│ ├─ metrics
│ ├─ log
│ └─ cost
└─ common
├─ error
├─ security
└─ config
注意不要按技术分成:
text
controller
service
repository
entity
这种分法在项目变大后会让业务边界变得模糊。
2. 定义模型服务接口
业务层不要直接依赖供应商 SDK,可以先定义模型服务接口:
java
public interface ModelService {
ChatResult chat(ChatRequest request);
Flux<ChatStreamChunk> streamChat(ChatRequest request);
EmbeddingResult embed(EmbeddingRequest request);
}
请求对象:
java
public record ChatRequest(
String tenantId,
String userId,
String conversationId,
String scene,
List<ChatMessage> messages,
ModelOptions options
) {
}
流式片段:
java
public record ChatStreamChunk(
String requestId,
String type,
String content,
TokenUsage usage
) {
}
业务服务只调用这个接口,不关心底层模型。
3. 定义知识库检索接口
java
public interface KnowledgeRetrievalService {
RetrievalResult retrieve(RetrievalRequest request);
}
请求对象:
java
public record RetrievalRequest(
String tenantId,
String userId,
List<String> knowledgeBaseIds,
String query,
int topK
) {
}
返回对象:
java
public record RetrievalResult(
List<RetrievedChunk> chunks
) {
}
public record RetrievedChunk(
String documentId,
String chunkId,
String title,
String content,
double score
) {
}
这里要把租户、用户和知识库 ID 传入检索服务,避免越权召回。
4. AI 编排服务
AI 编排模块负责把会话、知识库和模型服务串起来:
java
@Service
public class AiConversationOrchestrator {
private final ConversationService conversationService;
private final KnowledgeRetrievalService retrievalService;
private final PromptBuilder promptBuilder;
private final ModelService modelService;
public Flux<ChatStreamChunk> streamAnswer(AskQuestionCommand command) {
conversationService.checkPermission(command.userId(), command.conversationId());
conversationService.saveUserMessage(
command.conversationId(),
command.userId(),
command.question()
);
List<ChatMessage> history = conversationService.loadRecentMessages(
command.conversationId(),
10
);
RetrievalResult retrieval = retrievalService.retrieve(
new RetrievalRequest(
command.tenantId(),
command.userId(),
command.knowledgeBaseIds(),
command.question(),
5
)
);
List<ChatMessage> promptMessages = promptBuilder.buildForKnowledgeQa(
command.question(),
history,
retrieval.chunks()
);
ChatRequest request = new ChatRequest(
command.tenantId(),
command.userId(),
command.conversationId(),
"knowledge_qa",
promptMessages,
ModelOptions.defaultOptions()
);
return modelService.streamChat(request)
.doOnComplete(() -> {
// TODO 保存最终 assistant 消息、引用来源和费用统计
});
}
}
这段代码体现了一个重要原则:
编排服务负责串联流程,但具体能力由各模块提供。
5. 数据库职责划分
关系型数据库适合保存:
- 用户;
- 租户;
- 会话;
- 消息;
- 文件元数据;
- 文档状态;
- 切片元数据;
- 模型调用日志;
- Token 和费用统计。
对象存储适合保存:
- 原始文件;
- 解析后的中间文件;
- 导出的报告。
向量数据库适合保存:
- 文档切片向量;
- 语义检索索引。
不要把所有数据都塞进向量数据库。向量库负责检索,不负责完整业务状态。
6. 异步任务设计
知识库处理适合使用任务表或消息队列:
text
document_parse_task
├─ id
├─ document_id
├─ status
├─ retry_count
├─ error_message
├─ created_at
└─ updated_at
任务状态:
text
PENDING
PROCESSING
SUCCESS
FAILED
RETRYING
这样可以支持:
- 失败重试;
- 后台进度查询;
- 管理员重新处理;
- 大文件限流;
- 多 worker 并发消费。
五、常见问题与实践建议
1. 一开始要不要拆微服务
不建议一开始就拆很多微服务。
更推荐:
text
第一阶段:模块化单体
第二阶段:拆出知识库处理 worker
第三阶段:拆出模型服务或模型网关
第四阶段:根据流量和团队边界拆更多服务
架构演进要按痛点驱动,而不是为了"看起来先进"。
2. 模型服务和业务服务为什么要分开
模型服务变化很频繁:
- 模型供应商会换;
- 模型版本会升级;
- 价格会变化;
- 限流策略会变化;
- 参数会调整;
- 需要新增降级模型。
业务服务应该稳定表达业务流程,不应该被模型供应商牵着走。
3. 知识库服务为什么要独立边界
知识库处理有明显的离线任务特征:
- 文件可能很大;
- 解析耗时;
- 向量化耗费模型资源;
- 失败率比普通业务接口高;
- 需要重试和进度管理;
- 不同格式文件处理方式不同。
如果知识库逻辑和在线对话混在一起,线上问答接口会被离线任务拖慢。
4. Prompt 放在哪里
Prompt 不应该散落在代码各处。建议至少做到:
- 按场景管理;
- 有版本号;
- 支持灰度;
- 记录每次调用使用的 Prompt 版本;
- 重要 Prompt 变更需要评审;
- 支持回滚。
Prompt 是 AI 系统的业务逻辑之一,不能当成普通字符串随意修改。
5. 如何避免知识库越权
知识库检索前必须做权限过滤。
常见方式:
text
用户问题
↓
根据用户和租户计算可访问知识库
↓
只在可访问知识库内检索
↓
返回引用来源
不要先召回全部内容,再在回答阶段要求模型"不要泄露"。权限必须由程序控制。
6. 架构图不能只画组件,还要画数据流
很多架构图只画:
text
前端 -> 后端 -> 模型
这不够。AI 后端架构图必须画清楚:
- 用户问题如何进入系统;
- 会话历史从哪里来;
- 知识片段如何召回;
- Prompt 如何构造;
- 模型调用如何记录;
- 生成结果如何保存;
- 引用来源如何返回;
- 异步任务如何更新状态。
没有数据流的架构图很难指导开发。
六、进阶思考
1. 多模型路由
生产系统通常不会只用一个模型。
可以按场景路由:
| 场景 | 模型策略 |
|---|---|
| 普通客服问答 | 成本较低、速度较快的模型 |
| 复杂投诉分析 | 能力更强的模型 |
| 文档摘要 | 长上下文模型 |
| 敏感内容判断 | 专门的安全模型 |
| Embedding | 独立 Embedding 模型 |
模型路由应由模型服务负责,业务模块只传入场景和需求。
2. 观测指标设计
AI 后端要重点记录:
- 请求量;
- 成功率;
- 首 token 延迟;
- 总耗时;
- 输入 Token;
- 输出 Token;
- 总费用;
- 召回数量;
- 召回命中率;
- 模型错误;
- 用户反馈。
没有这些指标,就无法判断系统是否真的可用。
3. 安全边界
AI 后端至少有四层安全边界:
text
用户权限
↓
知识库权限
↓
工具调用权限
↓
模型输出安全
越靠前的安全控制越可靠。不能把所有安全责任都交给模型。
4. 从模块化单体到服务拆分
当系统增长后,可以按以下顺序拆分:
text
知识库处理 Worker
↓
模型服务 / 模型网关
↓
向量检索服务
↓
Agent 执行服务
↓
评估与监控服务
拆分条件包括:
- 资源需求明显不同;
- 部署频率不同;
- 故障需要隔离;
- 团队职责独立;
- 数据边界清晰;
- 性能瓶颈明确。
不要因为模块名字不同就拆服务。服务拆分要解决真实问题。
结论
生产级 AI 后端的架构设计,关键在于边界清晰。业务服务负责用户、会话、权限和业务流程;模型服务负责模型调用、供应商适配、重试降级和成本统计;知识库服务负责文档解析、切片、向量化、召回和引用来源;Agent 和工具服务则为后续复杂任务执行预留扩展空间。
第一阶段推荐采用模块化单体,通过包结构、接口和数据边界先把职责拆清楚。等知识库任务、模型调用流量或团队协作出现明确瓶颈后,再逐步拆出独立服务。
架构设计不是画一张漂亮的图,而是把系统的变化点隔离开:模型会变、知识库会变、业务流程会变、成本和安全要求也会变。只有边界清楚,后续的数据表设计、模型抽象、流式接口、知识库处理和 Agent 工具接入才能稳定推进。
下一篇可以继续进入"用户、会话与消息表应该如何设计",把本篇中的会话和消息边界落到具体的数据模型上。