摘要
前面的文章分别介绍了大模型调用、RAG、Agent、工具调用、权限控制、评估和平台化设计。但如果把这些能力真正组合成一个可运行的项目,仍然需要解决一系列工程问题:业务边界如何定义,模型服务如何抽象,用户和会话如何存储,流式对话如何实现,知识库和 Agent 如何接入,权限和安全如何落地,运行指标如何采集。
本系列将围绕一个“企业智能客服与知识库助手”项目,从零设计并逐步实现一个生产级 AI 后端。本篇是项目总览,重点不在某一段具体代码,而是建立完整的系统模型:项目要解决什么问题,哪些功能属于第一阶段,服务如何拆分,数据如何流转,技术选型如何判断,以及后续文章将如何一步步完成系统。
本文会从需求分析开始,设计用户、会话、消息、知识库、模型、工具和任务等核心对象,给出单体模块化架构和后续演进方向,并讨论如何在第一版中控制范围,避免把项目做成一个无法交付的“大而全 AI 平台”。
读完本文后,你应该能够:
- 明确一个生产级 AI 后端项目的业务目标和范围;
- 识别聊天应用与传统 CRUD 系统的差异;
- 设计模型服务、业务服务、知识库和 Agent 的边界;
- 规划用户、会话、消息等核心数据模型;
- 设计同步问答和流式对话的基本架构;
- 选择适合第一阶段的技术栈;
- 制定从 MVP 到生产版本的迭代路线;
- 为后续实现文章建立统一上下文。
一、背景与问题
1. 为什么需要一个完整的 AI 后端项目
调用大模型 API 本身并不复杂:
接收问题
-> 调用模型
-> 返回答案
但企业真正需要的通常不是一个孤立的聊天页面,而是一套可以接入业务系统的后端:
- 用户可以注册、登录和管理自己的会话;
- 会话中的消息需要保存和检索;
- 模型可以基于企业知识库回答问题;
- 某些问题需要调用订单、工单或库存系统;
- 长文本回答需要流式返回;
- 管理员需要查看调用量、Token 和费用;
- 不同角色只能访问授权的知识库;
- 模型不可用时需要重试、降级或转人工;
- 所有关键操作都应当可追踪和审计。
因此,AI 后端的难点不只是模型能力,而是如何把模型嵌入一个可靠的业务系统。
2. 项目场景:企业智能客服与知识库助手
本系列选择“企业智能客服与知识库助手”作为示例项目。
用户可以通过 Web 或其他客户端提问:
用户:如何申请远程办公?
系统:
-> 判断这是知识库问题
-> 检索企业制度
-> 生成基于资料的回答
-> 返回引用来源
如果用户继续问:
我的订单 1001 为什么还没有发货?
系统需要:
识别订单查询意图
-> 验证用户身份
-> 查询用户有权限访问的订单
-> 结合订单结果生成解释
-> 返回答案
如果用户要求退款:
识别高风险操作
-> 查询订单和退款条件
-> 生成操作预览
-> 请求人工或用户确认
-> 执行退款
这个场景可以覆盖:
- 基础对话;
- 上下文管理;
- RAG;
- 工具调用;
- 权限控制;
- 人工审批;
- 流式输出;
- 任务和审计。
3. AI 应用和传统业务系统的区别
传统业务接口通常是确定性的:
请求参数
-> 业务规则
-> 数据库操作
-> 确定的响应
AI 应用增加了一个不完全确定的模型层:
用户输入
-> 意图理解
-> 模型规划
-> 检索或工具调用
-> 模型生成
-> 业务校验
-> 返回结果
差异主要体现在:
| 输入 | 结构化参数为主 | 自然语言、文件和多模态数据 |
| 输出 | 固定 DTO | 文本、JSON、工具调用或流 |
| 执行 | 规则确定 | 模型存在不确定性 |
| 成本 | 主要是机器资源 | 还包括 Token 和模型调用 |
| 调试 | 看请求和数据库 | 需要看 Prompt、模型和轨迹 |
| 错误 | 通常可分类 | 可能是幻觉、误判、拒答或工具错误 |
| 测试 | 精确断言 | 规则、语义和人工评估结合 |
| 安全 | 接口和数据权限 | 还要防提示词注入和越权工具调用 |
这意味着 AI 后端必须在模型能力和确定性业务规则之间建立边界。
4. 项目第一阶段不解决什么
为了确保项目可以交付,第一阶段明确排除以下内容:
- 不做通用多租户 AI 平台;
- 不支持所有模型供应商的全部特性;
- 不实现复杂的多智能体自治协作;
- 不允许模型直接执行任意 Shell 命令;
- 不把支付、退款等高风险动作自动化到无审批;
- 不一开始就拆成大量微服务;
- 不追求覆盖所有文件格式;
- 不把评估系统做成独立商业平台。
第一阶段聚焦:
一个业务场景
+ 一个模块化后端
+ 一个主要模型供应商
+ 一个知识库
+ 少量受控工具
+ 可用的流式对话
+ 基本的权限、日志和监控
范围控制不是降低质量,而是先建立一条完整、可验证的闭环。
5. 目标用户
项目包含三类用户:
普通员工
可以:
- 发起对话;
- 查看自己的会话;
- 上传允许的资料;
- 查询企业知识;
- 申请人工服务。
客服或业务人员
可以:
- 查看授权范围内的客户问题;
- 查询业务数据;
- 接管人工会话;
- 查看知识库检索来源;
- 标记错误回答。
管理员
可以:
- 管理用户和角色;
- 管理知识库;
- 配置模型和 Prompt;
- 查看运行指标;
- 查看审计记录;
- 管理工具权限。
后续可以加入租户、部门和更细粒度的数据权限。
二、核心概念
1. 系统功能边界
第一版系统可以划分为以下模块:
| 用户与认证 | 注册、登录、Token、角色 |
| 会话管理 | 创建会话、查询历史、归档 |
| 消息管理 | 用户消息、助手消息、状态 |
| 模型服务 | 同步调用、流式调用、错误处理 |
| Prompt 管理 | 模板、变量、版本 |
| 知识库 | 文件上传、解析、切片、检索 |
| 对话编排 | 判断是否需要检索或工具 |
| 工具服务 | 订单查询、工单创建等受控工具 |
| 任务管理 | 异步任务、重试、进度 |
| 监控审计 | 日志、Token、延迟、费用和操作记录 |
这些模块不一定一开始都是独立服务,可以先在一个 Spring Boot 或其他后端应用中按模块组织。
2. 用户、会话与消息
这是对话系统的基础模型。
User
-> Conversation
-> Message
-> MessagePart
用户代表身份,会话代表一个连续的对话上下文,消息代表一次用户或系统交互。
消息角色至少包括:
| user | 用户输入 |
| assistant | 模型或人工客服回复 |
| system | 系统事件或状态信息 |
| tool | 工具执行结果 |
消息还需要有状态:
created
-> processing
-> completed
-> failed
-> cancelled
3. 模型服务
模型服务负责把业务请求转换为供应商请求:
业务请求
-> 选择模型
-> 组装消息
-> 注入配置
-> 调用供应商
-> 解析响应
-> 记录用量
-> 返回统一结果
业务模块不应该关心:
- 供应商的 URL;
- API Key;
- 供应商响应字段;
- 具体流式事件格式;
- Token 统计字段差异。
模型服务需要对外提供稳定接口:
public interface ModelService {
ModelResult complete(ModelRequest request);
Flux<ModelChunk> stream(ModelRequest request);
}
4. Prompt 模板
Prompt 不是散落在代码中的长字符串,而应该成为可管理的配置对象:
{
"template_id": "customer-support",
"version": "v3",
"system_prompt": "你是企业客服助手…",
"variables": [
"user_name",
"knowledge_context",
"conversation_summary"
],
"status": "published"
}
Prompt 模板需要支持:
- 变量替换;
- 版本管理;
- 测试;
- 灰度;
- 回滚;
- 权限控制。
5. 知识库
知识库用于让模型基于企业资料回答问题:
文件
-> 解析
-> 清洗
-> 切片
-> 向量化
-> 存储
-> 召回
-> 重排序
-> 注入 Prompt
知识库对象包括:
| KnowledgeBase | 一个知识集合 |
| Document | 上传的原始文件 |
| Chunk | 文档切片 |
| Embedding | 切片向量 |
| Retrieval | 一次检索请求 |
| Citation | 返回给用户的引用 |
知识库回答不能只返回结论,还应尽量返回来源,以便用户核验。
6. Agent 编排
对话服务至少需要判断:
这是普通聊天吗?
-> 直接调用模型
需要企业资料吗?
-> 检索知识库
需要业务数据吗?
-> 调用受控工具
需要修改业务状态吗?
-> 进入审批或人工确认
可以先用规则和固定工作流实现,再逐步引入更灵活的 Agent。
7. 工具
第一版工具尽量少且明确:
search_knowledge_base
read_order_summary
create_support_ticket
工具需要定义:
- 名称和描述;
- 输入 Schema;
- 权限;
- 风险等级;
- 超时;
- 幂等性;
- 输出字段;
- 审计要求。
不直接提供:
execute_sql
execute_shell
read_any_file
工具应该表达业务意图,而不是暴露底层系统能力。
8. 流式对话
流式对话的用户体验是:
发送问题
-> 快速返回 conversation_id 和 message_id
-> 持续接收 token
-> 接收完成事件
-> 消息状态变为 completed
后端要同时处理:
- 消息持久化;
- SSE 或 WebSocket;
- 模型片段转发;
- 客户端断开;
- 中断和取消;
- 最终完整内容保存。
9. 运行和审计
每次模型或工具调用都应关联:
tenant_id
user_id
conversation_id
message_id
task_id
trace_id
model
prompt_version
tool_version
这样可以回答:
- 某个用户的请求经过了哪些步骤;
- 某次回答使用了哪个模型;
- 哪个知识库版本参与了检索;
- 为什么产生了这笔费用;
- 哪个工具修改了业务数据。
三、工作原理
1. 系统总体架构
第一版可以采用模块化单体:
客户端
|
v
API Gateway
|
v
AI Backend
+———————————————+
| Auth | Conversation | Message | Chat |
| Model | Prompt | RAG | Agent | Tool |
| Task | Audit | Metrics |
+———————————————+
| | | |
v v v v
MySQL Redis Vector DB MQ/Worker
| | | |
+———-+———-+———-+
|
Model Provider
模块化单体的优点:
- 本地开发简单;
- 事务边界清楚;
- 部署成本低;
- 适合快速迭代;
- 仍然可以建立清晰模块边界。
2. 一次普通对话的流程
1. 用户提交消息
2. 认证模块确认身份
3. 会话模块校验会话归属
4. 消息模块保存用户消息
5. Chat Service 判断场景
6. Context Service 加载历史上下文
7. Prompt Service 组装 Prompt
8. Model Service 调用模型
9. 保存助手消息
10. 返回结果和用量
如果是流式请求,第 8~9 步会变成:
建立流连接
-> 接收模型片段
-> 转发给客户端
-> 累积完整内容
-> 流结束后保存助手消息
3. 一次知识库问答流程
用户问题
-> 判断需要知识库
-> 生成检索查询
-> 检索文档切片
-> 权限过滤
-> 重排序
-> 构造引用上下文
-> 调用模型
-> 返回答案和来源
权限过滤必须发生在把资料交给模型之前。不能先召回所有文档,再依赖模型自己判断哪些资料可以使用。
4. 一次工具调用流程
用户问题
-> Agent 识别需要工具
-> 生成工具调用
-> 后端校验工具权限
-> 校验参数和资源归属
-> 执行业务工具
-> 脱敏工具结果
-> 返回模型继续生成
-> 保存完整 Trace
如果工具是写操作:
识别高风险动作
-> 生成操作预览
-> 等待用户或人工审批
-> 获取一次性审批凭证
-> 执行写操作
-> 记录审计
5. 同步请求与异步任务
适合同步处理:
- 简短问答;
- 小型分类;
- 单次知识库检索;
- 低风险工具查询。
适合异步处理:
- 长文档解析;
- 批量向量化;
- 长报告生成;
- 多步骤 Agent;
- 人工审批流程;
- 大批量工单处理。
异步任务接口:
POST /api/tasks
返回:
{
"task_id": "task_1001",
"status": "queued"
}
查询:
GET /api/tasks/task_1001
6. 核心数据流
对话数据流:
Client
-> API
-> Conversation Service
-> Message Repository
-> Chat Orchestrator
-> Model Service
-> Message Repository
-> Client
知识库数据流:
Upload
-> Document Service
-> Parser
-> Chunker
-> Embedding Service
-> Vector Store
任务数据流:
Task API
-> Task DB
-> Message Queue
-> Worker
-> Step Executor
-> Model / Tool
-> Task DB
7. 技术选型
第一版可以使用:
| 后端框架 | Spring Boot |
| API | REST、SSE |
| 数据库 | MySQL 或 PostgreSQL |
| ORM | JPA、MyBatis 或 jOOQ |
| 缓存 | Redis |
| 消息队列 | RabbitMQ、Kafka 或 Redis Streams |
| 向量检索 | pgvector、Milvus 或 Elasticsearch |
| 模型调用 | WebClient、SDK 或 Spring AI |
| 文件存储 | S3 兼容对象存储 |
| 认证 | JWT、OAuth2 或企业统一认证 |
| 监控 | Micrometer、Prometheus、OpenTelemetry |
| 部署 | Docker Compose,后续可迁移 Kubernetes |
选择原则:
- 团队熟悉度优先;
- 先满足业务,不为技术名词增加复杂度;
- 关键组件支持替换;
- 能够本地启动和测试;
- 有明确的运维能力。
8. 目录结构
建议从业务模块和基础设施分层:
ai-backend/
├── src/main/java/com/example/aibackend/
│ ├── AiBackendApplication.java
│ ├── common/
│ │ ├── error/
│ │ ├── security/
│ │ └── web/
│ ├── auth/
│ │ ├── controller/
│ │ ├── service/
│ │ └── domain/
│ ├── conversation/
│ │ ├── controller/
│ │ ├── service/
│ │ ├── repository/
│ │ └── domain/
│ ├── model/
│ │ ├── client/
│ │ ├── prompt/
│ │ └── usage/
│ ├── knowledge/
│ │ ├── document/
│ │ ├── chunk/
│ │ ├── retrieval/
│ │ └── embedding/
│ ├── agent/
│ │ ├── orchestration/
│ │ ├── tool/
│ │ └── workflow/
│ ├── task/
│ │ ├── service/
│ │ ├── worker/
│ │ └── repository/
│ └── observability/
│ ├── audit/
│ ├── metrics/
│ └── tracing/
├── src/main/resources/
│ ├── application.yml
│ └── db/migration/
├── src/test/
├── Dockerfile
└── compose.yaml
9. 第一版接口设计
认证接口:
POST /api/auth/login
POST /api/auth/refresh
GET /api/auth/me
会话接口:
POST /api/conversations
GET /api/conversations
GET /api/conversations/{conversationId}
DELETE /api/conversations/{conversationId}
消息接口:
POST /api/conversations/{conversationId}/messages
GET /api/conversations/{conversationId}/messages
GET /api/conversations/{conversationId}/stream
知识库接口:
POST /api/knowledge-bases
POST /api/knowledge-bases/{id}/documents
GET /api/knowledge-bases/{id}/documents
DELETE /api/documents/{id}
任务接口:
POST /api/tasks
GET /api/tasks/{taskId}
POST /api/tasks/{taskId}/cancel
POST /api/tasks/{taskId}/resume
管理接口:
GET /api/admin/usage
GET /api/admin/traces
GET /api/admin/audits
接口设计先围绕业务用例,不要一开始暴露所有内部对象。
10. 版本化策略
配置、模型和数据都需要考虑版本:
API v1
Agent v1
Prompt v1
Knowledge Base v1
Tool v1
每个运行实例保存版本快照:
{
"agent_version": "support-v1",
"prompt_version": "support-prompt-v3",
"model_version": "model-a",
"knowledge_version": "kb-2026-09-18",
"tool_version": "tools-v1"
}
这样以后才能回放和比较不同版本的表现。
四、实战示例
本节实现项目的最小可行闭环:用户创建会话、发送消息、后端调用模型、保存消息并返回答案。
1. 初始化 Spring Boot 项目
推荐依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
实际版本应根据项目当前 Spring Boot 版本锁定,不能在生产项目中随意漂移。
2. 配置数据库和模型服务
spring:
datasource:
url: ${DATABASE_URL:jdbc:mysql://localhost:3306/ai_backend}
username: ${DATABASE_USERNAME:app}
password: ${DATABASE_PASSWORD:change_me}
data:
redis:
url: ${REDIS_URL:redis://localhost:6379}
app:
model:
base-url: ${MODEL_BASE_URL:https://api.example.com/v1}
api-key: ${MODEL_API_KEY:}
model-name: ${MODEL_NAME:default–model}
connect-timeout: 3s
read-timeout: 60s
security:
jwt-secret: ${JWT_SECRET:change_me}
conversation:
max-history-messages: 20
max-input-length: 10000
生产环境中,默认值只能用于本地开发。启动时应该检查生产环境是否仍然使用默认密钥。
3. 定义用户和会话实体
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 120)
private String email;
@Column(nullable = false)
private String passwordHash;
@Column(nullable = false, length = 30)
private String role;
@Column(nullable = false)
private boolean enabled = true;
}
会话实体:
@Entity
@Table(name = "conversations")
public class Conversation {
@Id
private String id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private User owner;
@Column(nullable = false, length = 200)
private String title;
@Column(nullable = false, length = 30)
private String status = "ACTIVE";
@Column(nullable = false)
private Instant createdAt;
@Column(nullable = false)
private Instant updatedAt;
}
会话查询必须校验当前用户是所有者或拥有相应管理权限。
4. 定义消息实体
@Entity
@Table(name = "messages")
public class Message {
@Id
private String id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Conversation conversation;
@Column(nullable = false, length = 20)
private String role;
@Lob
@Column(nullable = false)
private String content;
@Column(nullable = false, length = 30)
private String status;
private Integer inputTokens;
private Integer outputTokens;
private String modelName;
private String traceId;
private Instant createdAt;
}
生产环境还可以增加:
- parent_message_id;
- tool_call_json;
- citations_json;
- error_code;
- metadata_json;
- deleted_at。
5. 定义聊天接口
public record SendMessageRequest(
@NotBlank
@Size(max = 10000)
String content
) {
}
public record SendMessageResponse(
String conversationId,
String messageId,
String answer,
String status
) {
}
Controller:
@RestController
@RequestMapping("/api/conversations")
public class ConversationController {
private final ConversationService conversationService;
@PostMapping("/{conversationId}/messages")
public SendMessageResponse send(
@PathVariable String conversationId,
@Valid @RequestBody SendMessageRequest request,
CurrentUser currentUser
) {
return conversationService.sendMessage(
conversationId,
currentUser.id(),
request
);
}
}
6. 实现对话服务
@Service
public class ConversationService {
private final ConversationRepository conversationRepository;
private final MessageRepository messageRepository;
private final ContextService contextService;
private final ChatOrchestrator chatOrchestrator;
@Transactional
public SendMessageResponse sendMessage(
String conversationId,
Long userId,
SendMessageRequest request
) {
Conversation conversation =
conversationRepository.findOwned(
conversationId,
userId
).orElseThrow(() ->
new NotFoundException("会话不存在"));
Message userMessage = MessageFactory.userMessage(
conversation,
request.content()
);
messageRepository.save(userMessage);
ConversationContext context =
contextService.build(conversation);
ModelAnswer answer = chatOrchestrator.answer(
conversation,
context,
request.content()
);
Message assistantMessage = MessageFactory.assistantMessage(
conversation,
answer
);
messageRepository.save(assistantMessage);
return new SendMessageResponse(
conversationId,
assistantMessage.getId(),
answer.text(),
"completed"
);
}
}
真实项目中,模型调用不建议长时间占用数据库事务。可以采用:
短事务保存用户消息
-> 事务外调用模型
-> 短事务保存助手消息
这样不会让数据库事务一直等待外部模型。
7. 设计上下文服务
@Service
public class ContextService {
private final MessageRepository messageRepository;
private final ConversationSummaryService summaryService;
public ConversationContext build(
Conversation conversation
) {
List<Message> recent = messageRepository
.findRecent(
conversation.getId(),
20
);
String summary = summaryService
.getSummary(conversation.getId())
.orElse("");
return ConversationContext.of(
summary,
recent
);
}
}
上下文服务后续可以增加:
- Token 预算;
- 消息摘要;
- 用户长期记忆;
- 相关历史消息检索;
- 知识库上下文;
- 工具结果裁剪。
8. 实现模型服务接口
public record ModelRequest(
String systemPrompt,
List<ModelMessage> messages,
double temperature,
int maxOutputTokens
) {
}
public record ModelResult(
String text,
String modelName,
Integer inputTokens,
Integer outputTokens,
String providerRequestId
) {
}
public interface ModelService {
ModelResult complete(ModelRequest request);
}
Chat Orchestrator:
@Service
public class ChatOrchestrator {
private final PromptService promptService;
private final ModelService modelService;
public ModelAnswer answer(
Conversation conversation,
ConversationContext context,
String userInput
) {
ModelRequest request = promptService.build(
conversation,
context,
userInput
);
ModelResult result = modelService.complete(request);
return new ModelAnswer(
result.text(),
result.modelName(),
result.inputTokens(),
result.outputTokens(),
result.providerRequestId()
);
}
}
9. 加入最小知识库能力
第一版可以先实现检索接口,而不立即完成复杂的文件处理:
public interface KnowledgeService {
List<KnowledgeChunk> search(
Long userId,
String query,
int topK
);
}
编排器判断是否需要检索:
public ModelAnswer answer(
Conversation conversation,
ConversationContext context,
String userInput
) {
List<KnowledgeChunk> chunks = List.of();
if (intentClassifier.requiresKnowledge(userInput)) {
chunks = knowledgeService.search(
conversation.getOwner().getId(),
userInput,
5
);
}
ModelRequest request = promptService.build(
conversation,
context,
userInput,
chunks
);
return toAnswer(modelService.complete(request));
}
知识库召回结果需要附带来源:
public record KnowledgeChunk(
String documentId,
String title,
String content,
double score,
String sourceUrl
) {
}
10. 实现流式响应
流式接口:
@GetMapping(
value = "/{conversationId}/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE
)
public Flux<ServerSentEvent<ChatStreamEvent>> stream(
@PathVariable String conversationId,
@RequestParam String message,
CurrentUser currentUser
) {
return conversationStreamService.stream(
conversationId,
currentUser.id(),
message
);
}
事件结构:
public record ChatStreamEvent(
String type,
String messageId,
String delta,
String status,
String errorCode
) {
}
事件类型:
message.started
message.delta
message.citation
message.completed
message.failed
不要只返回裸文本片段。结构化事件便于前端处理状态、引用和错误。
11. 加入调用记录
@Entity
@Table(name = "model_calls")
public class ModelCall {
@Id
private String id;
private String traceId;
private String conversationId;
private String messageId;
private String provider;
private String modelName;
private String promptVersion;
private Integer inputTokens;
private Integer outputTokens;
private Long latencyMs;
private String status;
private String errorCode;
private BigDecimal estimatedCost;
private Instant createdAt;
}
调用记录可用于:
- 成本统计;
- 延迟分析;
- 供应商故障排查;
- 用户用量限制;
- 模型版本对比;
- 质量评估关联。
12. 最小测试闭环
第一版至少建立三层测试:
服务层测试
测试会话归属、消息保存和模型调用编排。
模型客户端集成测试
使用 Mock Server 验证 HTTP 请求和响应解析。
API 测试
验证认证、参数校验、错误响应和流式事件。
测试不应该真实调用生产模型。可以使用固定响应、WireMock 或录制的模型响应。
五、常见问题与实践建议
1. 项目一开始就做成“万能 Agent”
万能 Agent 通常意味着:
- 工具过多;
- Prompt 过长;
- 权限边界不清;
- 失败难以定位;
- 测试集难以建立。
第一版应该围绕少量明确场景:
知识库问答
+ 订单查询
+ 人工工单
先保证流程闭环,再扩展能力。
2. 把模型调用放在 Controller 中
Controller 中直接写模型调用,会导致:
- 无法复用;
- 难以测试;
- 错误处理分散;
- 流式和同步重复;
- 供应商耦合;
- 业务逻辑混乱。
Controller 应只负责 HTTP 适配,把模型调用交给应用服务和模型客户端。
3. 让模型决定所有业务状态
模型可以判断意图和生成建议,但不应该直接决定:
- 用户是否有权限;
- 订单是否可以退款;
- 是否完成扣款;
- 是否已经发布;
- 是否批准高风险操作。
这些状态必须由确定性业务代码和数据库事务控制。
4. 把完整历史消息无限传给模型
这会带来:
- 成本增长;
- 延迟增加;
- 上下文超限;
- 旧信息干扰;
- 敏感数据暴露。
应使用窗口、摘要、重要事实和相关检索控制上下文。
5. 不保存模型调用的版本信息
如果只保存最终回答,后续无法知道:
- 使用了哪个模型;
- 使用了哪个 Prompt;
- 使用了哪个知识库版本;
- 使用了哪些工具;
- 调用失败是否重试。
每次运行都应该保存版本快照和 Trace ID。
6. 让流式消息只存在内存中
如果流式输出过程中服务重启,内存中的内容会丢失。建议:
- 用户消息先保存;
- 助手消息创建为 processing;
- 流式片段按策略持久化或缓存;
- 完成后写入完整内容;
- 异常时保存失败状态和部分结果。
是否保存每个 Token,要根据规模和审计要求决定。大多数系统保存聚合后的片段即可。
7. 把知识库权限交给模型
模型不应该判断“这份文档是否可以给当前用户”。权限过滤必须在检索层或数据访问层完成。
正确流程:
当前用户权限
-> 过滤可检索文档
-> 向量召回
-> 返回授权范围内的片段
-> 交给模型
8. 第一版就引入微服务
除非已经有明确的团队和部署需求,否则优先模块化单体。AI 应用早期变化快,模块化单体更利于:
- 快速调整业务流程;
- 统一事务;
- 本地调试;
- 减少网络故障;
- 降低部署成本。
9. 只测试模型文本,不测试工具轨迹
如果 Agent 调用了错误工具但最终碰巧回答正确,仍然存在安全和成本问题。
测试还应检查:
- 工具是否正确;
- 参数是否正确;
- 是否越权;
- 是否重复调用;
- 是否在失败后正确降级。
10. 没有失败和降级设计
模型服务、向量库、Redis、对象存储和业务系统都可能失败。
每个依赖都应明确:
- 超时;
- 重试;
- 熔断;
- 降级;
- 用户提示;
- 是否允许任务恢复。
11. 忽略数据生命周期
对话、文件、向量、模型输出和审计记录都需要定义:
- 保存多久;
- 谁可以查看;
- 如何脱敏;
- 如何删除;
- 是否支持用户导出;
- 是否需要合规留存。
没有生命周期设计,数据会无限增长并增加合规风险。
12. 过度追求抽象
第一版不需要同时支持十个模型供应商、五种向量数据库和所有 Agent 框架。建议先定义最小稳定接口:
ModelService
KnowledgeService
ToolExecutor
ConversationService
TaskService
当确实出现替换需求时,再增加适配器和路由。
六、进阶思考
1. 从模块化单体演进到服务化
项目可以按以下路径演进:
模块化单体
-> 独立 Worker
-> 独立模型网关
-> 独立知识库服务
-> 独立任务编排服务
-> 多租户 AI 平台
拆分依据应是:
- 独立扩容需求;
- 故障隔离需求;
- 团队边界;
- 数据权限;
- 发布节奏;
- 技术栈差异。
2. 数据库和消息一致性
发送消息和投递异步任务时,可能出现:
数据库消息保存成功
-> 任务入队失败
-> 用户消息没有得到回答
可以使用 Outbox Pattern:
同一数据库事务:
保存用户消息
写入 outbox_event
后台发布器:
读取 outbox_event
投递任务
标记事件完成
这样可以降低数据库写入和消息投递之间的不一致。
3. 知识库的增量更新
企业文档会变化。知识库需要支持:
- 文档版本;
- 增量解析;
- 旧切片失效;
- 新向量写入;
- 索引状态;
- 失败重试;
- 更新进度;
- 权限同步。
文档更新流程:
上传新版本
-> 解析
-> 生成切片
-> 生成向量
-> 写入新索引
-> 验证召回
-> 切换知识库版本
-> 清理旧版本
不要在文档上传后直接删除旧版本,否则新版本处理失败时无法回退。
4. 模型路由和备用模型
模型服务可以根据场景选择:
简单问答 -> 低成本模型
长文档 -> 长上下文模型
高风险任务 -> 稳定模型
主模型故障 -> 备用模型
敏感内容 -> 本地模型
路由决策需要考虑:
- 能力;
- 成本;
- 延迟;
- 数据敏感等级;
- 当前健康状态;
- 供应商额度。
不同模型的输出质量可能不同,切换后应运行专项评估。
5. 评估体系
项目应建立最小评估集:
高频问题
+ 复杂问题
+ 无答案问题
+ 权限问题
+ 工具调用问题
+ 恶意输入
+ 服务失败
每次修改以下内容都要回归:
- Prompt;
- 模型;
- 检索策略;
- 工具描述;
- 上下文窗口;
- 输出 Schema;
- 工作流。
评估指标:
| answer_correctness | 答案正确性 |
| groundedness | 是否基于资料 |
| citation_accuracy | 引用是否准确 |
| tool_accuracy | 工具选择和参数 |
| safety_pass_rate | 安全用例通过率 |
| latency | 延迟 |
| cost | 成本 |
| handoff_rate | 人工转接率 |
6. 安全模型
AI 后端需要同时保护:
用户身份
-> 会话和消息
-> 知识库文档
-> 工具调用
-> 模型上下文
-> 输出结果
重点防护:
- Prompt 注入;
- 越权检索;
- 跨用户会话访问;
- 敏感数据进入模型;
- 工具参数注入;
- 高风险操作绕过审批;
- 过度保存原始内容;
- 日志泄露密钥。
7. 业务工具的权限边界
工具权限可以分为:
L0:纯计算
L1:公开读取
L2:授权读取或草稿写入
L3:业务状态变更
L4:高风险外部操作
不同等级使用不同策略:
| L0 | 自动执行 |
| L1 | 自动执行并审计 |
| L2 | 用户权限校验 |
| L3 | 用户确认或业务审批 |
| L4 | 强审批、二次认证和审计 |
8. 可观测性和成本中心
系统需要把模型调用纳入统一可观测链路:
HTTP Request
-> Conversation
-> Agent Run
-> Retrieval
-> Model Call
-> Tool Call
-> Final Response
每个节点记录:
- trace_id;
- tenant_id;
- user_id;
- latency;
- status;
- token;
- cost;
- error;
- version。
成本中心可以按租户、团队、用户、Agent、模型和业务场景统计。
9. 高可用和容量规划
模型服务的容量规划不能只看 QPS,还要看:
- 平均输入 Token;
- 平均输出 Token;
- 并发流式连接;
- 工具调用次数;
- RAG 检索耗时;
- Worker 并发;
- 数据库连接;
- 队列积压。
典型容量链路:
用户并发
-> SSE 连接数
-> 模型并发
-> Token 吞吐
-> 数据库写入
-> 队列和 Worker
流式接口尤其要关注长连接数量和连接断开后的资源释放。
10. 生产发布策略
建议采用:
开发环境
-> 集成测试
-> 离线评估
-> 测试环境
-> 小流量灰度
-> 观察指标
-> 全量发布
发布内容包括:
- 应用代码;
- 数据库迁移;
- Prompt 版本;
- Agent 配置;
- 工具版本;
- 知识库版本;
- 模型路由。
这些内容要能独立回滚或明确兼容关系。
11. 下一阶段的项目拆分
后续系列可以按以下顺序实现:
第 1 篇:项目和架构设计
第 2 篇:需求分析与 AI 后端边界
第 3 篇:系统架构和服务拆分
第 4 篇:用户、会话和消息表
第 5 篇:模型服务抽象
第 6 篇:流式接口
第 7 篇:知识库文件上传与解析
第 8 篇:向量化、召回和重排序
第 9 篇:Agent 工具注册和调用
第 10 篇:权限、安全和敏感信息
第 11 篇:监控、Token 和费用
第 12 篇:Docker、Nginx 和 Kubernetes
第 13 篇:项目复盘
每篇文章都应该在前一篇设计的基础上增加一个可以验证的能力,而不是只写概念。
结论
从零设计一个生产级 AI 后端,第一步不是直接接入模型,而是先明确业务目标、系统边界和交付路径。
本系列选择企业智能客服与知识库助手作为项目主线,第一版采用模块化单体架构,围绕以下核心能力展开:
用户和认证
-> 会话和消息
-> 模型服务
-> Prompt 和上下文
-> 知识库
-> Agent 和工具
-> 流式输出
-> 异步任务
-> 权限和审批
-> 日志、监控和审计
项目设计中最重要的原则是:
- 先完成一个端到端闭环;
- 业务规则和模型生成明确分离;
- 模型服务通过统一接口抽象;
- 工具按最小权限开放;
- 长任务必须支持持久化和恢复;
- 流式输出要保存最终一致的消息;
- 知识库检索必须结合权限;
- 模型输出必须经过校验;
- 每次运行都要记录版本和 Trace;
- 复杂度由真实需求驱动。
下一篇将进入需求分析阶段,具体讨论 AI 应用和传统业务系统的差异,梳理用户角色、核心用例、非功能需求、数据安全要求和第一版 MVP 的验收标准。


