部署与监控
约 966 字大约 3 分钟
布欧-Lewyon
2026-05-15
首页 › Spring AI › 实战项目:AI 智能客服系统
Docker 打包
# Dockerfile
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]# 构建
./mvnw package -DskipTests
docker build -t cs-ai-service:latest .
# 运行
docker compose up -dActuator 监控配置
# application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus,logfile
endpoint:
health:
show-details: always
metrics:
tags:
application: cs-ai-service
export:
prometheus:
enabled: true@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "cs-ai-service"
);
}
}Token 用量统计
@Aspect
@Component
public class AIMetricsAspect {
private final MeterRegistry meterRegistry;
@AfterReturning(pointcut = "execution(* org.springframework.ai.chat.client.ChatClient.call(..))",
returning = "response")
public void recordTokenUsage(Object response) {
if (response instanceof ChatResponse chatResponse) {
Usage usage = chatResponse.getMetadata().getUsage();
if (usage != null) {
meterRegistry.counter("ai.tokens.prompt",
"model", chatResponse.getMetadata().getModel())
.increment(usage.getPromptTokens());
meterRegistry.counter("ai.tokens.generation",
"model", chatResponse.getMetadata().getModel())
.increment(usage.getGenerationTokens());
meterRegistry.counter("ai.requests.total").increment();
}
}
}
}应用层监控
@RestController
@RequestMapping("/api/admin/monitor")
public class MonitorController {
private final MeterRegistry meterRegistry;
@GetMapping
public Map<String, Object> monitor() {
return Map.of(
"aiRequests", getCounterValue("ai.requests.total"),
"promptTokens", getCounterValue("ai.tokens.prompt"),
"generationTokens", getCounterValue("ai.tokens.generation"),
"knowledgeDocs", knowledgeDocCount(),
"activeSessions", activeSessionCount()
);
}
@GetMapping("/metrics/prometheus")
public String prometheus() {
// Prometheus 格式输出
return ScrapeUtil.scrape(meterRegistry);
}
}多环境配置
# application-dev.yml(开发环境)
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3.2:1b # 轻量模型加速开发
# application-prod.yml(生产环境)
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.3
vectorstore:
pgvector:
index-type: HNSW# 切换环境
java -jar app.jar --spring.profiles.active=dev
java -jar app.jar --spring.profiles.active=prod管理员界面
<!-- templates/admin/dashboard.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>智能客服管理后台</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-50">
<div class="container mx-auto p-6">
<h1 class="text-2xl font-bold mb-6">智能客服管理后台</h1>
<!-- 统计卡片 -->
<div class="grid grid-cols-4 gap-4 mb-8">
<div class="bg-white p-4 rounded shadow">
<div class="text-gray-500 text-sm">今日对话数</div>
<div class="text-2xl font-bold" th:text="${todaySessions}">0</div>
</div>
<div class="bg-white p-4 rounded shadow">
<div class="text-gray-500 text-sm">知识库文档</div>
<div class="text-2xl font-bold" th:text="${knowledgeCount}">0</div>
</div>
<div class="bg-white p-4 rounded shadow">
<div class="text-gray-500 text-sm">总 Token 消耗</div>
<div class="text-2xl font-bold" th:text="${totalTokens}">0</div>
</div>
<div class="bg-white p-4 rounded shadow">
<div class="text-gray-500 text-sm">当前模型</div>
<div class="text-lg font-bold" th:text="${currentModel}">-</div>
</div>
</div>
<!-- 知识库管理 -->
<div class="bg-white p-6 rounded shadow mb-8">
<h2 class="text-xl font-semibold mb-4">知识库管理</h2>
<form th:action="@{/api/admin/knowledge/upload}" method="post"
enctype="multipart/form-data" class="mb-4">
<input type="file" name="file" accept=".pdf,.txt,.md" class="mr-2">
<select name="category" class="mr-2">
<option value="faq">FAQ</option>
<option value="product">产品</option>
<option value="policy">政策</option>
</select>
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">
上传
</button>
</form>
<table class="w-full">
<thead>
<tr class="bg-gray-100">
<th class="p-2 text-left">文件名</th>
<th class="p-2 text-left">分类</th>
<th class="p-2 text-left">状态</th>
<th class="p-2 text-left">切块数</th>
<th class="p-2">操作</th>
</tr>
</thead>
<tbody>
<tr th:each="doc : ${documents}">
<td class="p-2" th:text="${doc.filename}"></td>
<td class="p-2" th:text="${doc.category}"></td>
<td class="p-2" th:text="${doc.status}"></td>
<td class="p-2" th:text="${doc.chunkCount}"></td>
<td class="p-2 text-center">
<a th:href="@{/api/admin/knowledge/{id}/reindex(id=${doc.id})}"
class="text-blue-500 mr-2">重新索引</a>
<a th:href="@{/api/admin/knowledge/{id}/delete(id=${doc.id})}"
class="text-red-500" onclick="return confirm('确定删除?')">删除</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>部署检查清单
| 检查项 | 说明 |
|--------|------|
| PostgreSQL 运行 | `docker ps | grep pgvector` |
| Ollama 模型就绪 | `ollama list` 确认 llama3.2:1b 和 nomic-embed-text |
| 环境变量设置 | `OPENAI_API_KEY`(生产环境) |
| 数据库表自动创建 | `spring.jpa.hibernate.ddl-auto=update` |
| Actuator 端点开放 | `management.endpoints.web.exposure.include=health` |
| 日志级别调整 | 生产 `error`,开发 `debug` |
| CORS 配置 | 如果是前后端分离,需配置跨域 |小结
- Docker Compose 一键启动 PostgreSQL + MySQL + 应用。
- Actuator + Prometheus 监控运行状态。
- Token 用量 AOP 切面统计,区分 prompt 和 generation。
- 管理后台提供知识库管理、统计面板和对话记录。
- 多环境配置(dev:Ollama,prod:OpenAI)切换成本极低。
- 完成此项目 = 掌握了 Spring AI 全部核心能力。
上一节:对话与 Tool 模块
