Support immediate in-memory release via POST /api/v1/calls/{callId}/close while keeping scheduled TTL as a fallback for unclosed sessions.
Co-authored-by: Cursor <cursoragent@cursor.com>
30 KiB
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 实时检测接口
实时检测业务接口:
POST /api/v1/asr-events
前端每收到一条新的 ASR Final,就调用该接口。
请求不再传 eventId,使用:
callId + seq
唯一标识一条 ASR Final。
3.2 不提供通话状态查询接口
不提供:
GET /api/v1/calls/{callId}/state
每次实时检测响应同时返回:
- 本次新增提醒:
newAlerts - 当前通话累计结果:
currentResults
3.3 提供通话关闭接口
通话结束时,前端应主动关闭会话,立即释放内存中的通话状态:
POST /api/v1/calls/{callId}/close
同时保留 TTL 定时清理,作为兜底机制,处理前端未调用 close、进程异常退出等遗漏场景。
约束条件:
- 每通电话的
callId必须唯一 - 正常结束路径优先调用 close
- 未关闭或异常遗留的会话由 TTL 自动清理
3.4 保留每通电话独立锁
Spring MVC 会并发处理请求,同一通话的两条 ASR Final 可能同时进入后端。
每个 callId 使用独立锁,保护:
幂等检查
→ 更新上下文
→ 关键词匹配
→ 判断是否首次提醒
→ 更新会话状态
→ 组装响应
不同通话使用不同锁,不会互相阻塞。
4. 总体架构
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-webmvcspring-boot-starter-validationspring-boot-starter-actuator- 对应测试 Starter
建议增加:
<dependency>
<groupId>org.ahocorasick</groupId>
<artifactId>ahocorasick</artifactId>
<version>0.6.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
开发阶段使用 H2 文件数据库,规则配置不会因服务重启丢失。
正式部署时可将 H2 切换为 PostgreSQL,JPA 业务代码无需明显修改。
7. 项目目录
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 请求
POST /api/v1/asr-events
Content-Type: application/json
{
"callId": "call-1001",
"seq": 28,
"speaker": "citizen",
"text": "我这个顺丰快递一直没有收到",
"final": true
}
8.2 DTO
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 响应
{
"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 后,使用:
callId + seq
标识 ASR Final。
每个通话保存最近已处理的序号:
private final Set<Long> processedSeqs =
new LinkedHashSet<>();
public boolean markProcessed(long seq, int maxSize) {
if (processedSeqs.contains(seq)) {
return false;
}
processedSeqs.add(seq);
while (processedSeqs.size() > maxSize) {
Iterator<Long> iterator = processedSeqs.iterator();
iterator.next();
iterator.remove();
}
return true;
}
使用有界集合而不是只判断:
seq <= lastSeq
可以允许少量乱序请求继续参与匹配,降低漏判风险。
前提:
callId每通电话唯一seq在同一通电话内唯一
如果坐席和市民分别独立编号,应改成:
callId + speaker + seq
10. 文本标准化算法
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("");
}
}
示例:
EMS → ems
E M S → ems
顺丰,快递 → 顺丰快递
11. 最近 Final 上下文
匹配文本使用最近两条市民 ASR Final。
目的:降低 ASR 切段造成的漏判。
例如:
Final 27:我是在拼
Final 28:多多上买的
拼接后:
我是在拼。多多上买的
标准化后:
我是在拼多多上买的
从而命中“拼多多”。
会话中按 seq 保存:
private final NavigableMap<Long, String> recentCitizenFinals =
new TreeMap<>();
public void appendCitizenFinal(
long seq,
String text,
int windowSize
) {
recentCitizenFinals.put(seq, text);
while (recentCitizenFinals.size() > windowSize) {
recentCitizenFinals.pollFirstEntry();
}
}
12. AC 自动机匹配
12.1 Payload
public record RuleTarget(
String ruleSetId,
String ruleId,
String ruleName
) {
}
public record KeywordPayload(
String normalizedKeyword,
String displayKeyword,
List<RuleTarget> targets
) {
}
12.2 构建流程
读取完整规则
→ 校验
→ 标准化关键词
→ 构建新的 PayloadTrie
→ 包装为不可变快照
→ AtomicReference 原子替换
实时 ASR 请求只扫描内存中的自动机,不访问数据库。
12.3 Matcher
@Component
public class AcKeywordMatcher {
private final AtomicReference<AcAutomatonSnapshot>
currentSnapshot = new AtomicReference<>();
public List<MatchResult> match(String text) {
AcAutomatonSnapshot snapshot =
currentSnapshot.get();
if (snapshot == null || text == null || text.isBlank()) {
return List.of();
}
String normalizedText =
TextNormalizer.normalize(text);
Collection<PayloadEmit<KeywordPayload>> emits =
snapshot.trie().parseText(normalizedText);
return aggregate(emits, text);
}
public void replaceSnapshot(
AcAutomatonSnapshot newSnapshot
) {
currentSnapshot.set(newSnapshot);
}
}
12.4 重叠关键词处理
规则可能同时包含:
顺丰快递
顺丰
不使用 ignoreOverlaps。
AC 自动机返回全部命中,应用层按事件聚合:
{
"eventId": "sf-express",
"matchedKeywords": [
"顺丰快递",
"顺丰"
]
}
同一方向只返回一个事件结果。
13. 会话级新增提醒算法
每通电话维护:
private final Set<String> alertedEventKeys =
new HashSet<>();
统一事件键:
eventKey = ruleSetId + ":" + ruleId
例如:
transfer:sf-express
新增事件差分:
newEvents = currentMatchedEvents - alertedEvents
Java:
List<MatchResult> newMatches = matches.stream()
.filter(match ->
!session.hasAlerted(match.eventKey())
)
.toList();
更新状态:
for (MatchResult match : matches) {
session.mergeMatch(match, seq);
}
for (MatchResult match : newMatches) {
session.markAlerted(match.eventKey());
}
效果:
第一次出现“顺丰”
→ 提醒一次
再次出现“顺丰”
→ 不重复提醒
后续出现“拼多多”
→ 新增提醒拼多多
14. 每通电话独立锁
private final ConcurrentHashMap<String, ReentrantLock>
callLocks = new ConcurrentHashMap<>();
public ReentrantLock getLock(String callId) {
return callLocks.computeIfAbsent(
callId,
ignored -> new ReentrantLock()
);
}
处理时:
ReentrantLock lock =
sessionStore.getLock(request.callId());
lock.lock();
try {
return processUnderLock(request);
} finally {
lock.unlock();
}
锁保护:
幂等检查
→ 上下文更新
→ 匹配
→ 新事件判断
→ 状态更新
→ 响应构建
不同通话使用不同锁,可以并行处理。
15. 会话关闭与 TTL 清理
会话释放采用 主动关闭 + TTL 兜底 双路径:
通话结束
→ 优先:POST /api/v1/calls/{callId}/close(立即释放)
→ 兜底:TTL 定时任务清理遗漏会话
15.1 通话关闭接口
POST /api/v1/calls/{callId}/close
响应示例:
{
"callId": "call-1001",
"closed": true,
"existed": true
}
| 字段 | 说明 |
|---|---|
callId |
被关闭的通话 ID |
closed |
本次调用是否完成关闭处理(接口幂等,恒为 true) |
existed |
关闭前会话是否存在;重复关闭或不存在时为 false |
处理流程:
获取 callId 独立锁
→ 删除 CallSession
→ 删除对应 ReentrantLock
→ 返回 closed=true
语义约定:
- 接口幂等:同一
callId多次 close 不报错 - 通话结束后应尽快调用,避免会话长期占用内存
- 由于
callId必须全局唯一,关闭后不应再复用同一callId提交 ASR Final
服务层示意:
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 主要覆盖运行期遗漏)
monitor:
recent-final-window-size: 2
max-processed-seqs: 500
session-ttl: PT2H
session-cleanup-interval: PT10M
@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。
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
);
规则文档:
{
"ruleSets": [
{
"id": "transfer",
"name": "潜在转接",
"enabled": true,
"matcher": "ac-keyword",
"rules": [
{
"id": "sf-express",
"name": "顺丰速递绿色渠道服务热线",
"enabled": true,
"keywords": [
"顺丰",
"顺丰快递",
"顺丰速运"
]
}
]
}
]
}
每次保存并生效生成新版本:
V1 active=false
V2 active=false
V3 active=true
17. 规则管理接口
17.1 获取当前生效规则
GET /api/v1/admin/rules
响应:
{
"version": 5,
"ruleCount": 14,
"keywordCount": 39,
"ruleSets": []
}
17.2 保存并生效
PUT /api/v1/admin/rules
请求:
{
"baseVersion": 5,
"ruleSets": []
}
使用 baseVersion 做乐观并发控制。
版本冲突:
409 Conflict
{
"code": "RULE_VERSION_CONFLICT",
"message": "规则已被其他页面修改,请刷新后重试",
"currentVersion": 6
}
17.3 测试当前生效规则
POST /api/v1/admin/rules/test
请求:
{
"text": "我这个顺丰快递一直没有收到"
}
响应:
{
"activeVersion": 6,
"normalizedText": "我这个顺丰快递一直没有收到",
"matches": [
{
"eventType": "transfer",
"eventId": "sf-express",
"eventName": "顺丰速递绿色渠道服务热线",
"matchedKeywords": [
"顺丰快递",
"顺丰"
]
}
]
}
测试接口是无状态接口:
- 不保存通话上下文
- 不执行同一通话去重
- 每次都返回当前文本的直接匹配结果
- 验证的是后端当前真正生效的内存 AC 自动机
18. 规则保存后生效流程
页面提交完整规则和 baseVersion
↓
检查 baseVersion
↓
校验规则结构和关键词冲突
↓
预构建新的 AC 自动机
↓
数据库保存新规则版本
↓
事务提交成功
↓
AtomicReference 原子切换
↓
后续 ASR 请求立即使用新规则
核心原则:
新自动机先构建成功,数据库事务提交成功,之后才切换运行时规则。
事务内:
@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);
}
事务提交后切换:
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT
)
public void activate(RuleActivatedEvent event) {
keywordMatcher.replaceSnapshot(
event.snapshot()
.withVersion(event.version())
);
}
失败策略:
- 校验失败:不保存、不切换
- AC 自动机构建失败:不保存、不切换
- 数据库提交失败:不切换
- 旧规则继续正常工作
19. 服务启动后的规则恢复
服务启动时:
查询数据库中 active=true 的最新规则
→ 解析完整规则 JSON
→ 校验
→ 构建 AC 自动机
→ 设置为当前运行快照
如果数据库没有规则:
读取内置初始配置
→ 创建 V1
→ 设置 active=true
→ 构建并加载自动机
20. 规则校验要求
保存前至少校验:
- 规则集 ID 不重复
- 事件 ID 不重复
- 事件名称不能为空
- 启用事件至少有一个关键词
- 标准化后关键词不能为空
- 同一事件内关键词不能重复
- 同一关键词不能指向多个转接方向
- Matcher 类型必须存在
baseVersion必须等于当前版本
对于“闪购”“邮政”“百度”等较宽泛关键词,可以作为 warning 返回,但首期允许保存。
21. 单页面前端
前端采用:
React
TypeScript
Vite
Tailwind CSS
shadcn/ui
页面不使用路由,只使用两个 Tab:
┌─────────────────────────────────────────────┐
│ ASR 事件监控配置 当前生效版本 V6 │
├─────────────────────────────────────────────┤
│ [规则配置] [匹配验证] │
├─────────────────────────────────────────────┤
│ │
│ 当前 Tab 内容 │
│ │
└─────────────────────────────────────────────┘
22. 规则配置页面
页面功能:
- 展示规则列表
- 新增规则
- 编辑规则
- 删除规则
- 启用或停用规则
- 增加或删除关键词
- 保存并生效
- 显示当前生效版本
- 显示规则数和关键词数
推荐 shadcn/ui 组件:
| 功能 | 组件 |
|---|---|
| 页面切换 | Tabs |
| 规则展示 | Card 或 Table |
| 关键词 | Badge |
| 启用状态 | Switch |
| 新增与编辑 | Dialog |
| 删除确认 | AlertDialog |
| 按钮 | Button |
| 文本输入 | Input |
| 操作提示 | Sonner |
页面示意:
潜在转接规则 共 14 条
┌───────────────────────────────────────────┐
│ 顺丰速递绿色渠道服务热线 [启用] │
│ ID:sf-express │
│ │
│ [顺丰 ×] [顺丰快递 ×] [顺丰速运 ×] │
│ [编辑] [删除] │
└───────────────────────────────────────────┘
[新增规则] [保存并生效]
23. Demo 匹配验证页面
Demo 页面用于验证当前后端真正生效的 AC 自动机。
匹配验证 生效版本 V6
测试文本
┌───────────────────────────────────────────┐
│ 我这个顺丰快递一直没有收到 │
└───────────────────────────────────────────┘
[清空] [开始检测]
检测结果
✓ 命中潜在转接方向
顺丰速递绿色渠道服务热线
事件 ID:sf-express
命中关键词:[顺丰快递] [顺丰]
页面调用:
POST /api/v1/admin/rules/test
保存并生效后,可自动切换到 Demo Tab 并验证新关键词。
24. 前后端集成
开发阶段:
Vite:http://localhost:5173
Spring Boot:http://localhost:8080
vite.config.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,
},
},
},
})
生产构建:
cd frontend
pnpm build
将 frontend/dist 内容复制到:
src/main/resources/static/
最终前端和后端一起打包进同一个 Spring Boot JAR。
访问:
http://localhost:8080/
即可打开规则配置和 Demo 页面。
25. application.yml 示例
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: 2
max-processed-seqs: 500
session-ttl: PT2H
session-cleanup-interval: PT10M
management:
endpoints:
web:
exposure:
include:
- health
- info
- metrics
26. 性能与部署
当前规模:
坐席数:2
每坐席约 0~2 条 ASR Final/秒
总请求量:约 0~4 QPS
推荐资源:
CPU:1 核
内存:512 MB~1 GB
实例数:1
启动:
mvn clean package
java \
-Xms256m \
-Xmx512m \
-jar target/demo-0.0.1-SNAPSHOT.jar
由于通话状态保存在 JVM 内存中,首期只部署一个实例。
27. 测试与验收
至少覆盖以下场景:
- 首次出现“顺丰”产生提醒
- 同一通话重复出现“顺丰”不重复提醒
- 同一通话后续出现“拼多多”产生新增提醒
- 同一个
callId + seq重复提交返回duplicate=true - 少量乱序请求仍可参与匹配
- 坐席侧文本不参与匹配
- Partial 文本不参与匹配
EMS、ems、E M S均可命中- 相邻 Final 拼接后可命中被切分的品牌
- 不同通话状态相互隔离
- close 能立即释放通话会话,重复 close 幂等
- TTL 能清理未 close 的过期会话
- 保存新规则后 Demo 接口立即使用新版本
- 保存失败时旧规则继续有效
- 两个管理页面同时编辑时能够检测版本冲突
- Actuator 健康检查正常
28. 后续演进
28.1 高频事件
增加规则集:
{
"id": "high-frequency-event",
"name": "高频事件",
"enabled": true,
"matcher": "ac-keyword",
"rules": []
}
28.2 Dify 二次确认
状态扩展:
matched
→ confirming
→ confirmed / rejected
Dify 调用异步执行,不阻塞 ASR 接口。
28.3 语义匹配
新增:
public class SemanticMatcher implements TextMatcher {
}
保持统一 MatchResult 输出。
28.4 Redis 与多实例
需要扩容时:
InMemorySessionStore
→ RedisSessionStore
Controller、规则管理、Matcher 和前端协议保持不变。
29. 最终方案摘要
实时检测链路:
前端收到 ASR Final
↓
POST /api/v1/asr-events
↓
callId + seq 幂等
↓
最近两条市民 Final 拼接
↓
文本标准化
↓
内存 AC 自动机匹配
↓
按业务事件聚合关键词
↓
与本通话已提醒事件集合做差
↓
返回 newAlerts + currentResults
规则生效链路:
shadcn 单页面编辑完整规则
↓
PUT /api/v1/admin/rules
↓
校验规则
↓
预构建新 AC 自动机
↓
数据库保存新版本
↓
事务提交
↓
AtomicReference 原子切换
↓
Demo 页面立即验证
首期接口:
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
会话清理:
通话结束 → close 立即释放
遗漏会话 → TTL 定时兜底清理
最终采用:
Spring Boot 4.1.0
Java 21
Spring MVC
Spring Data JPA
H2 文件数据库
AC 自动机
JVM 内存会话状态
React + Vite + shadcn/ui
单实例部署