# ASR 实时关键词事件监控服务技术方案
> 基于当前已确认需求整理
> 技术栈:Java 21、Spring Boot 4.1.0、Spring MVC、Spring Data JPA、AC 自动机、React + Vite + shadcn/ui
---
## 1. 项目目标
建设一个独立的 ASR 实时事件监控服务。
坐席前端每收到一条新的 ASR Final,就调用后端实时检测接口。后端根据当前已生效的关键词规则进行匹配,并在同一通话内首次发现新的业务事件时返回提醒。
首期主要解决:
- 潜在转接方向识别
- 实时关键词检测
- 同一通话内去重提醒
- 规则配置页面
- 规则保存后立即生效
- Demo 页面验证当前生效规则
后续可扩展:
- 高频事件匹配
- 风险事件提醒
- 服务规范提醒
- 正则匹配
- 语义相似匹配
- Dify 二次确认
- Redis 多实例会话状态
- SSE 实时推送
---
## 2. 首期范围
### 2.1 首期实现内容
- Java 21
- Spring Boot 4.1.0
- Spring MVC REST 接口
- 前端主动提交 ASR Final
- 只处理 Final,不处理 Partial
- 只匹配市民侧文本
- AC 自动机关键词匹配
- 最近五条市民 ASR Final 拼接
- 同一通话、同一事件只提醒一次
- 规则通过页面编辑
- 规则保存到数据库
- 保存成功后重建 AC 自动机并立即生效
- 提供单页面配置与测试
- JVM 内存保存通话状态
- 提供通话主动关闭接口
- TTL 自动清理过期会话
- 单实例部署
### 2.2 首期不实现内容
- Redis
- Kafka、RabbitMQ、RocketMQ
- Elasticsearch
- 向量数据库
- Embedding 语义匹配
- Dify 实时确认
- WebSocket 或 SSE
- 通话状态查询接口
- 独立 `eventId`
- 多实例部署
- 复杂草稿、审核和发布流程
---
## 3. 最终确定的接口边界
### 3.1 实时检测接口
实时检测业务接口:
```http
POST /api/v1/asr-events
```
前端每收到一条新的 ASR Final,就调用该接口。
请求不再传 `eventId`,使用:
```text
callId + seq
```
唯一标识一条 ASR Final。
### 3.2 不提供通话状态查询接口
不提供:
```http
GET /api/v1/calls/{callId}/state
```
每次实时检测响应同时返回:
- 本次新增提醒:`newAlerts`
- 当前通话累计结果:`currentResults`
### 3.3 提供通话关闭接口
通话结束时,前端应主动关闭会话,立即释放内存中的通话状态:
```http
POST /api/v1/calls/{callId}/close
```
同时保留 TTL 定时清理,作为兜底机制,处理前端未调用 close、进程异常退出等遗漏场景。
约束条件:
- 每通电话的 `callId` 必须唯一
- 正常结束路径优先调用 close
- 未关闭或异常遗留的会话由 TTL 自动清理
### 3.4 保留每通电话独立锁
Spring MVC 会并发处理请求,同一通话的两条 ASR Final 可能同时进入后端。
每个 `callId` 使用独立锁,保护:
```text
幂等检查
→ 更新上下文
→ 关键词匹配
→ 判断是否首次提醒
→ 更新会话状态
→ 组装响应
```
不同通话使用不同锁,不会互相阻塞。
---
## 4. 总体架构
```mermaid
flowchart LR
A[ASR 系统] --> B[坐席前端]
B -->|ASR Final| C[POST /api/v1/asr-events]
subgraph M[ASR 实时事件监控服务]
C --> D[请求校验]
D --> E[callId + seq 幂等]
E --> F[更新最近 Final 上下文]
F --> G[文本标准化]
G --> H[AC 自动机匹配]
H --> I[按事件聚合关键词]
I --> J[会话级新增事件差分]
J --> K[返回 newAlerts + currentResults]
end
L[JVM 内存会话状态] <--> E
L <--> F
L <--> J
N[shadcn 配置页面] --> O[规则管理 API]
O --> P[JPA 数据库]
O --> Q[构建新 AC 自动机]
Q --> R[AtomicReference 原子切换]
R --> H
K --> B
```
---
## 5. 技术栈
| 类别 | 选型 |
|---|---|
| JDK | Java 21 |
| 后端框架 | Spring Boot 4.1.0 |
| Web | Spring MVC |
| 参数校验 | Jakarta Validation |
| 数据持久化 | Spring Data JPA |
| 开发数据库 | H2 文件模式 |
| 正式数据库 | PostgreSQL,可平滑替换 |
| 关键词匹配 | `org.ahocorasick:ahocorasick` |
| 会话状态 | `ConcurrentHashMap` |
| 并发控制 | 每通话 `ReentrantLock` |
| 规则热切换 | `AtomicReference` |
| 定时清理 | `@Scheduled` |
| 健康检查 | Spring Boot Actuator |
| 前端 | React + TypeScript + Vite |
| UI | Tailwind CSS + shadcn/ui |
| 测试 | JUnit 5、MockMvc、Spring Boot Test Starter |
---
## 6. POM 调整
当前 POM 已包含:
- `spring-boot-starter-webmvc`
- `spring-boot-starter-validation`
- `spring-boot-starter-actuator`
- 对应测试 Starter
建议增加:
```xml
org.ahocorasick
ahocorasick
0.6.3
org.springframework.boot
spring-boot-starter-data-jpa
com.h2database
h2
runtime
org.springframework.boot
spring-boot-configuration-processor
true
```
开发阶段使用 H2 文件数据库,规则配置不会因服务重启丢失。
正式部署时可将 H2 切换为 PostgreSQL,JPA 业务代码无需明显修改。
---
## 7. 项目目录
```text
demo/
├── pom.xml
│
├── frontend/
│ ├── package.json
│ ├── vite.config.ts
│ ├── components.json
│ └── src/
│ ├── App.tsx
│ ├── api/
│ │ └── rule-api.ts
│ ├── components/
│ │ ├── RuleEditor.tsx
│ │ ├── RuleDialog.tsx
│ │ └── MatchTester.tsx
│ └── components/ui/
│
└── src/
├── main/
│ ├── java/com/example/demo/
│ │ ├── DemoApplication.java
│ │ │
│ │ ├── api/
│ │ │ ├── AsrEventController.java
│ │ │ ├── CallController.java
│ │ │ └── RuleAdminController.java
│ │ │
│ │ ├── application/
│ │ │ ├── AsrEventMonitorService.java
│ │ │ ├── CallSessionService.java
│ │ │ └── RuleManagementService.java
│ │ │
│ │ ├── domain/
│ │ │ ├── AsrFinalEventRequest.java
│ │ │ ├── MonitorResponse.java
│ │ │ ├── CloseCallResponse.java
│ │ │ ├── MatchResult.java
│ │ │ ├── AlertResult.java
│ │ │ ├── RuleDocument.java
│ │ │ ├── RuleSet.java
│ │ │ ├── EventRule.java
│ │ │ ├── CallSession.java
│ │ │ └── EventState.java
│ │ │
│ │ ├── matcher/
│ │ │ ├── TextMatcher.java
│ │ │ ├── AcKeywordMatcher.java
│ │ │ ├── AcAutomatonFactory.java
│ │ │ ├── AcAutomatonSnapshot.java
│ │ │ └── KeywordPayload.java
│ │ │
│ │ ├── repository/
│ │ │ ├── SessionStore.java
│ │ │ ├── InMemorySessionStore.java
│ │ │ ├── RuleVersionEntity.java
│ │ │ └── RuleVersionRepository.java
│ │ │
│ │ ├── config/
│ │ │ └── MonitorProperties.java
│ │ │
│ │ ├── job/
│ │ │ └── SessionCleanupJob.java
│ │ │
│ │ └── support/
│ │ ├── TextNormalizer.java
│ │ ├── EventKey.java
│ │ └── ApiExceptionHandler.java
│ │
│ └── resources/
│ ├── application.yml
│ └── static/
│
└── test/
```
---
## 8. ASR 实时检测接口
### 8.1 请求
```http
POST /api/v1/asr-events
Content-Type: application/json
```
```json
{
"callId": "call-1001",
"seq": 28,
"speaker": "citizen",
"text": "我这个顺丰快递一直没有收到",
"final": true
}
```
### 8.2 DTO
```java
public record AsrFinalEventRequest(
@NotBlank
@Size(max = 128)
String callId,
@PositiveOrZero
long seq,
@NotBlank
@Pattern(regexp = "citizen|agent")
String speaker,
@NotBlank
@Size(max = 4000)
String text,
@JsonProperty("final")
boolean isFinal
) {
}
```
### 8.3 响应
```json
{
"accepted": true,
"duplicate": false,
"callId": "call-1001",
"acceptedSeq": 28,
"revision": 1,
"activeRuleVersion": 5,
"newAlerts": [
{
"eventType": "transfer",
"eventId": "sf-express",
"eventName": "顺丰速递绿色渠道服务热线",
"status": "matched",
"matchedKeywords": [
"顺丰快递",
"顺丰"
],
"evidence": "我这个顺丰快递一直没有收到",
"firstMatchedSeq": 28
}
],
"currentResults": [
{
"eventType": "transfer",
"eventId": "sf-express",
"eventName": "顺丰速递绿色渠道服务热线",
"status": "matched",
"matchedKeywords": [
"顺丰快递",
"顺丰"
],
"evidence": "我这个顺丰快递一直没有收到",
"firstMatchedSeq": 28
}
]
}
```
字段说明:
| 字段 | 说明 |
|---|---|
| `accepted` | 请求是否被处理 |
| `duplicate` | 是否为重复 Final |
| `newAlerts` | 本次新增、需要前端提醒的事件 |
| `currentResults` | 当前通话累计识别到的全部事件 |
| `activeRuleVersion` | 当前实际参与匹配的规则版本 |
---
## 9. 幂等算法
删除独立 `eventId` 后,使用:
```text
callId + seq
```
标识 ASR Final。
每个通话保存最近已处理的序号:
```java
private final Set processedSeqs =
new LinkedHashSet<>();
```
```java
public boolean markProcessed(long seq, int maxSize) {
if (processedSeqs.contains(seq)) {
return false;
}
processedSeqs.add(seq);
while (processedSeqs.size() > maxSize) {
Iterator iterator = processedSeqs.iterator();
iterator.next();
iterator.remove();
}
return true;
}
```
使用有界集合而不是只判断:
```java
seq <= lastSeq
```
可以允许少量乱序请求继续参与匹配,降低漏判风险。
前提:
- `callId` 每通电话唯一
- `seq` 在同一通电话内唯一
如果坐席和市民分别独立编号,应改成:
```text
callId + speaker + seq
```
---
## 10. 文本标准化算法
```java
public final class TextNormalizer {
private static final Pattern REMOVABLE =
Pattern.compile(
"[\\s,。!?、;:,.!?;:\"'()()\\[\\]【】]+"
);
private TextNormalizer() {
}
public static String normalize(String text) {
if (text == null || text.isBlank()) {
return "";
}
String normalized = Normalizer.normalize(
text,
Normalizer.Form.NFKC
).toLowerCase(Locale.ROOT);
return REMOVABLE
.matcher(normalized)
.replaceAll("");
}
}
```
示例:
```text
EMS → ems
E M S → ems
顺丰,快递 → 顺丰快递
```
---
## 11. 最近 Final 上下文
匹配文本使用最近五条市民 ASR Final。
目的:降低 ASR 切段造成的漏判。
例如:
```text
Final 27:我是在拼
Final 28:多多上买的
```
拼接后:
```text
我是在拼。多多上买的
```
标准化后:
```text
我是在拼多多上买的
```
从而命中“拼多多”。
会话中按 seq 保存:
```java
private final NavigableMap recentCitizenFinals =
new TreeMap<>();
```
```java
public void appendCitizenFinal(
long seq,
String text,
int windowSize
) {
recentCitizenFinals.put(seq, text);
while (recentCitizenFinals.size() > windowSize) {
recentCitizenFinals.pollFirstEntry();
}
}
```
---
## 12. AC 自动机匹配
### 12.1 Payload
```java
public record RuleTarget(
String ruleSetId,
String ruleId,
String ruleName
) {
}
public record KeywordPayload(
String normalizedKeyword,
String displayKeyword,
List targets
) {
}
```
### 12.2 构建流程
```text
读取完整规则
→ 校验
→ 标准化关键词
→ 构建新的 PayloadTrie
→ 包装为不可变快照
→ AtomicReference 原子替换
```
实时 ASR 请求只扫描内存中的自动机,不访问数据库。
### 12.3 Matcher
```java
@Component
public class AcKeywordMatcher {
private final AtomicReference
currentSnapshot = new AtomicReference<>();
public List match(String text) {
AcAutomatonSnapshot snapshot =
currentSnapshot.get();
if (snapshot == null || text == null || text.isBlank()) {
return List.of();
}
String normalizedText =
TextNormalizer.normalize(text);
Collection> emits =
snapshot.trie().parseText(normalizedText);
return aggregate(emits, text);
}
public void replaceSnapshot(
AcAutomatonSnapshot newSnapshot
) {
currentSnapshot.set(newSnapshot);
}
}
```
### 12.4 重叠关键词处理
规则可能同时包含:
```text
顺丰快递
顺丰
```
不使用 `ignoreOverlaps`。
AC 自动机返回全部命中,应用层按事件聚合:
```json
{
"eventId": "sf-express",
"matchedKeywords": [
"顺丰快递",
"顺丰"
]
}
```
同一方向只返回一个事件结果。
---
## 13. 会话级新增提醒算法
每通电话维护:
```java
private final Set alertedEventKeys =
new HashSet<>();
```
统一事件键:
```text
eventKey = ruleSetId + ":" + ruleId
```
例如:
```text
transfer:sf-express
```
新增事件差分:
```text
newEvents = currentMatchedEvents - alertedEvents
```
Java:
```java
List newMatches = matches.stream()
.filter(match ->
!session.hasAlerted(match.eventKey())
)
.toList();
```
更新状态:
```java
for (MatchResult match : matches) {
session.mergeMatch(match, seq);
}
for (MatchResult match : newMatches) {
session.markAlerted(match.eventKey());
}
```
效果:
```text
第一次出现“顺丰”
→ 提醒一次
再次出现“顺丰”
→ 不重复提醒
后续出现“拼多多”
→ 新增提醒拼多多
```
---
## 14. 每通电话独立锁
```java
private final ConcurrentHashMap
callLocks = new ConcurrentHashMap<>();
```
```java
public ReentrantLock getLock(String callId) {
return callLocks.computeIfAbsent(
callId,
ignored -> new ReentrantLock()
);
}
```
处理时:
```java
ReentrantLock lock =
sessionStore.getLock(request.callId());
lock.lock();
try {
return processUnderLock(request);
} finally {
lock.unlock();
}
```
锁保护:
```text
幂等检查
→ 上下文更新
→ 匹配
→ 新事件判断
→ 状态更新
→ 响应构建
```
不同通话使用不同锁,可以并行处理。
---
## 15. 会话关闭与 TTL 清理
会话释放采用 **主动关闭 + TTL 兜底** 双路径:
```text
通话结束
→ 优先:POST /api/v1/calls/{callId}/close(立即释放)
→ 兜底:TTL 定时任务清理遗漏会话
```
### 15.1 通话关闭接口
```http
POST /api/v1/calls/{callId}/close
```
响应示例:
```json
{
"callId": "call-1001",
"closed": true,
"existed": true
}
```
| 字段 | 说明 |
|---|---|
| `callId` | 被关闭的通话 ID |
| `closed` | 本次调用是否完成关闭处理(接口幂等,恒为 `true`) |
| `existed` | 关闭前会话是否存在;重复关闭或不存在时为 `false` |
处理流程:
```text
获取 callId 独立锁
→ 删除 CallSession
→ 删除对应 ReentrantLock
→ 返回 closed=true
```
语义约定:
- 接口幂等:同一 `callId` 多次 close 不报错
- 通话结束后应尽快调用,避免会话长期占用内存
- 由于 `callId` 必须全局唯一,关闭后不应再复用同一 `callId` 提交 ASR Final
服务层示意:
```java
public CloseCallResponse close(String callId) {
ReentrantLock lock = sessionStore.getLock(callId);
lock.lock();
try {
boolean existed = sessionStore.remove(callId);
sessionStore.removeLock(callId);
return new CloseCallResponse(callId, true, existed);
} finally {
lock.unlock();
}
}
```
### 15.2 TTL 兜底清理
即使提供 close,仍保留 TTL,用于清理以下遗留会话:
- 前端未调用 close
- 页面异常关闭、网络故障导致 close 未送达
- 进程重启前未及时释放的会话(单实例内存方案下重启后本就为空,TTL 主要覆盖运行期遗漏)
```yaml
monitor:
recent-final-window-size: 5
max-processed-seqs: 500
session-ttl: PT2H
session-cleanup-interval: PT10M
```
```java
@Scheduled(
fixedDelayString =
"${monitor.session-cleanup-interval:PT10M}"
)
public void cleanupExpiredSessions() {
Instant threshold =
Instant.now(clock)
.minus(properties.sessionTtl());
sessionStore.removeExpired(threshold);
}
```
TTL 清理时同样删除:
- `CallSession`
- 对应的 `ReentrantLock`
close 与 TTL 互不冲突:已 close 的会话不存在,TTL 跳过;未 close 的会话到期后由 TTL 删除。
---
## 16. 规则数据模型
首期使用一张版本表保存完整规则 JSON。
```sql
CREATE TABLE rule_version (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
version_no BIGINT NOT NULL,
active BOOLEAN NOT NULL,
content_json CLOB NOT NULL,
rule_count INT NOT NULL,
keyword_count INT NOT NULL,
created_at TIMESTAMP NOT NULL
);
```
规则文档:
```json
{
"ruleSets": [
{
"id": "transfer",
"name": "潜在转接",
"enabled": true,
"matcher": "ac-keyword",
"rules": [
{
"id": "sf-express",
"name": "顺丰速递绿色渠道服务热线",
"enabled": true,
"keywords": [
"顺丰",
"顺丰快递",
"顺丰速运"
]
}
]
}
]
}
```
每次保存并生效生成新版本:
```text
V1 active=false
V2 active=false
V3 active=true
```
---
## 17. 规则管理接口
### 17.1 获取当前生效规则
```http
GET /api/v1/admin/rules
```
响应:
```json
{
"version": 5,
"ruleCount": 14,
"keywordCount": 39,
"ruleSets": []
}
```
### 17.2 保存并生效
```http
PUT /api/v1/admin/rules
```
请求:
```json
{
"baseVersion": 5,
"ruleSets": []
}
```
使用 `baseVersion` 做乐观并发控制。
版本冲突:
```http
409 Conflict
```
```json
{
"code": "RULE_VERSION_CONFLICT",
"message": "规则已被其他页面修改,请刷新后重试",
"currentVersion": 6
}
```
### 17.3 测试当前生效规则
```http
POST /api/v1/admin/rules/test
```
请求:
```json
{
"text": "我这个顺丰快递一直没有收到"
}
```
响应:
```json
{
"activeVersion": 6,
"normalizedText": "我这个顺丰快递一直没有收到",
"matches": [
{
"eventType": "transfer",
"eventId": "sf-express",
"eventName": "顺丰速递绿色渠道服务热线",
"matchedKeywords": [
"顺丰快递",
"顺丰"
]
}
]
}
```
测试接口是无状态接口:
- 不保存通话上下文
- 不执行同一通话去重
- 每次都返回当前文本的直接匹配结果
- 验证的是后端当前真正生效的内存 AC 自动机
---
## 18. 规则保存后生效流程
```text
页面提交完整规则和 baseVersion
↓
检查 baseVersion
↓
校验规则结构和关键词冲突
↓
预构建新的 AC 自动机
↓
数据库保存新规则版本
↓
事务提交成功
↓
AtomicReference 原子切换
↓
后续 ASR 请求立即使用新规则
```
核心原则:
> 新自动机先构建成功,数据库事务提交成功,之后才切换运行时规则。
事务内:
```java
@Transactional
public SaveRuleResponse saveAndActivate(
SaveRuleRequest request
) {
RuleDocument document =
ruleValidator.validateAndNormalize(request);
AcAutomatonSnapshot snapshot =
automatonFactory.build(document);
repository.deactivateCurrent();
RuleVersionEntity entity =
repository.save(
RuleVersionEntity.active(
document,
snapshot.ruleCount(),
snapshot.keywordCount()
)
);
eventPublisher.publishEvent(
new RuleActivatedEvent(
entity.getVersionNo(),
snapshot
)
);
return SaveRuleResponse.from(entity, snapshot);
}
```
事务提交后切换:
```java
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT
)
public void activate(RuleActivatedEvent event) {
keywordMatcher.replaceSnapshot(
event.snapshot()
.withVersion(event.version())
);
}
```
失败策略:
- 校验失败:不保存、不切换
- AC 自动机构建失败:不保存、不切换
- 数据库提交失败:不切换
- 旧规则继续正常工作
---
## 19. 服务启动后的规则恢复
服务启动时:
```text
查询数据库中 active=true 的最新规则
→ 解析完整规则 JSON
→ 校验
→ 构建 AC 自动机
→ 设置为当前运行快照
```
如果数据库没有规则:
```text
读取内置初始配置
→ 创建 V1
→ 设置 active=true
→ 构建并加载自动机
```
---
## 20. 规则校验要求
保存前至少校验:
1. 规则集 ID 不重复
2. 事件 ID 不重复
3. 事件名称不能为空
4. 启用事件至少有一个关键词
5. 标准化后关键词不能为空
6. 同一事件内关键词不能重复
7. 同一关键词不能指向多个转接方向
8. Matcher 类型必须存在
9. `baseVersion` 必须等于当前版本
对于“闪购”“邮政”“百度”等较宽泛关键词,可以作为 warning 返回,但首期允许保存。
---
## 21. 单页面前端
前端采用:
```text
React
TypeScript
Vite
Tailwind CSS
shadcn/ui
```
页面不使用路由,只使用两个 Tab:
```text
┌─────────────────────────────────────────────┐
│ ASR 事件监控配置 当前生效版本 V6 │
├─────────────────────────────────────────────┤
│ [规则配置] [匹配验证] │
├─────────────────────────────────────────────┤
│ │
│ 当前 Tab 内容 │
│ │
└─────────────────────────────────────────────┘
```
---
## 22. 规则配置页面
页面功能:
- 展示规则列表
- 新增规则
- 编辑规则
- 删除规则
- 启用或停用规则
- 增加或删除关键词
- 保存并生效
- 显示当前生效版本
- 显示规则数和关键词数
推荐 shadcn/ui 组件:
| 功能 | 组件 |
|---|---|
| 页面切换 | `Tabs` |
| 规则展示 | `Card` 或 `Table` |
| 关键词 | `Badge` |
| 启用状态 | `Switch` |
| 新增与编辑 | `Dialog` |
| 删除确认 | `AlertDialog` |
| 按钮 | `Button` |
| 文本输入 | `Input` |
| 操作提示 | `Sonner` |
页面示意:
```text
潜在转接规则 共 14 条
┌───────────────────────────────────────────┐
│ 顺丰速递绿色渠道服务热线 [启用] │
│ ID:sf-express │
│ │
│ [顺丰 ×] [顺丰快递 ×] [顺丰速运 ×] │
│ [编辑] [删除] │
└───────────────────────────────────────────┘
[新增规则] [保存并生效]
```
---
## 23. Demo 匹配验证页面
Demo 页面用于验证当前后端真正生效的 AC 自动机。
```text
匹配验证 生效版本 V6
测试文本
┌───────────────────────────────────────────┐
│ 我这个顺丰快递一直没有收到 │
└───────────────────────────────────────────┘
[清空] [开始检测]
检测结果
✓ 命中潜在转接方向
顺丰速递绿色渠道服务热线
事件 ID:sf-express
命中关键词:[顺丰快递] [顺丰]
```
页面调用:
```http
POST /api/v1/admin/rules/test
```
保存并生效后,可自动切换到 Demo Tab 并验证新关键词。
---
## 24. 前后端集成
开发阶段:
```text
Vite:http://localhost:5173
Spring Boot:http://localhost:8080
```
`vite.config.ts`:
```ts
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
},
},
},
})
```
生产构建:
```bash
cd frontend
pnpm build
```
将 `frontend/dist` 内容复制到:
```text
src/main/resources/static/
```
最终前端和后端一起打包进同一个 Spring Boot JAR。
访问:
```text
http://localhost:8080/
```
即可打开规则配置和 Demo 页面。
---
## 25. application.yml 示例
```yaml
spring:
application:
name: asr-event-monitor
datasource:
url: jdbc:h2:file:./data/rules;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
jpa:
hibernate:
ddl-auto: update
open-in-view: false
server:
port: 8080
monitor:
recent-final-window-size: 5
max-processed-seqs: 500
session-ttl: PT2H
session-cleanup-interval: PT10M
management:
endpoints:
web:
exposure:
include:
- health
- info
- metrics
```
---
## 26. 性能与部署
当前规模:
```text
坐席数:2
每坐席约 0~2 条 ASR Final/秒
总请求量:约 0~4 QPS
```
推荐资源:
```text
CPU:1 核
内存:512 MB~1 GB
实例数:1
```
启动:
```bash
mvn clean package
java \
-Xms256m \
-Xmx512m \
-jar target/demo-0.0.1-SNAPSHOT.jar
```
由于通话状态保存在 JVM 内存中,首期只部署一个实例。
---
## 27. 测试与验收
至少覆盖以下场景:
1. 首次出现“顺丰”产生提醒
2. 同一通话重复出现“顺丰”不重复提醒
3. 同一通话后续出现“拼多多”产生新增提醒
4. 同一个 `callId + seq` 重复提交返回 `duplicate=true`
5. 少量乱序请求仍可参与匹配
6. 坐席侧文本不参与匹配
7. Partial 文本不参与匹配
8. `EMS`、`ems`、`E M S` 均可命中
9. 相邻 Final 拼接后可命中被切分的品牌
10. 不同通话状态相互隔离
11. close 能立即释放通话会话,重复 close 幂等
12. TTL 能清理未 close 的过期会话
13. 保存新规则后 Demo 接口立即使用新版本
14. 保存失败时旧规则继续有效
15. 两个管理页面同时编辑时能够检测版本冲突
16. Actuator 健康检查正常
---
## 28. 后续演进
### 28.1 高频事件
增加规则集:
```json
{
"id": "high-frequency-event",
"name": "高频事件",
"enabled": true,
"matcher": "ac-keyword",
"rules": []
}
```
### 28.2 Dify 二次确认
状态扩展:
```text
matched
→ confirming
→ confirmed / rejected
```
Dify 调用异步执行,不阻塞 ASR 接口。
### 28.3 语义匹配
新增:
```java
public class SemanticMatcher implements TextMatcher {
}
```
保持统一 `MatchResult` 输出。
### 28.4 Redis 与多实例
需要扩容时:
```text
InMemorySessionStore
→ RedisSessionStore
```
Controller、规则管理、Matcher 和前端协议保持不变。
---
## 29. 最终方案摘要
实时检测链路:
```text
前端收到 ASR Final
↓
POST /api/v1/asr-events
↓
callId + seq 幂等
↓
最近五条市民 Final 拼接
↓
文本标准化
↓
内存 AC 自动机匹配
↓
按业务事件聚合关键词
↓
与本通话已提醒事件集合做差
↓
返回 newAlerts + currentResults
```
规则生效链路:
```text
shadcn 单页面编辑完整规则
↓
PUT /api/v1/admin/rules
↓
校验规则
↓
预构建新 AC 自动机
↓
数据库保存新版本
↓
事务提交
↓
AtomicReference 原子切换
↓
Demo 页面立即验证
```
首期接口:
```text
POST /api/v1/asr-events
POST /api/v1/calls/{callId}/close
GET /api/v1/admin/rules
PUT /api/v1/admin/rules
POST /api/v1/admin/rules/test
```
会话清理:
```text
通话结束 → close 立即释放
遗漏会话 → TTL 定时兜底清理
```
最终采用:
```text
Spring Boot 4.1.0
Java 21
Spring MVC
Spring Data JPA
H2 文件数据库
AC 自动机
JVM 内存会话状态
React + Vite + shadcn/ui
单实例部署
```