Widen citizen Final window to 5 and document monitor flow in Chinese.

Improve cut-segment recall and clarify AsrEventMonitorService behavior for local maintainers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xin Wang
2026-07-15 21:55:52 +08:00
parent af85f1c4b1
commit 311bef5270
6 changed files with 31 additions and 11 deletions

View File

@@ -44,7 +44,7 @@
- 只处理 Final不处理 Partial
- 只匹配市民侧文本
- AC 自动机关键词匹配
- 最近条市民 ASR Final 拼接
- 最近条市民 ASR Final 拼接
- 同一通话、同一事件只提醒一次
- 规则通过页面编辑
- 规则保存到数据库
@@ -512,7 +512,7 @@ E M S → ems
## 11. 最近 Final 上下文
匹配文本使用最近条市民 ASR Final。
匹配文本使用最近条市民 ASR Final。
目的:降低 ASR 切段造成的漏判。
@@ -837,7 +837,7 @@ public CloseCallResponse close(String callId) {
```yaml
monitor:
recent-final-window-size: 2
recent-final-window-size: 5
max-processed-seqs: 500
session-ttl: PT2H
session-cleanup-interval: PT10M
@@ -1317,7 +1317,7 @@ server:
port: 8080
monitor:
recent-final-window-size: 2
recent-final-window-size: 5
max-processed-seqs: 500
session-ttl: PT2H
session-cleanup-interval: PT10M
@@ -1453,7 +1453,7 @@ POST /api/v1/asr-events
callId + seq 幂等
最近条市民 Final 拼接
最近条市民 Final 拼接
文本标准化

View File

@@ -15,6 +15,19 @@ import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.stereotype.Service;
/**
* ASR Final 实时事件监控服务。
*
* <p>在每个 {@code callId} 的独立锁内按如下流程处理:
* <ol>
* <li>拒绝 Partial{@code final=false}),不改写会话匹配状态</li>
* <li>按 {@code callId + seq} 做幂等</li>
* <li>忽略坐席agent文本请求可接受但不参与关键词匹配</li>
* <li>维护最近若干条市民 Final 窗口,送入 AC 自动机匹配</li>
* <li>与本通话已提醒事件做差,得到 {@code newAlerts}</li>
* <li>同时返回 {@code newAlerts} 与累计 {@code currentResults}</li>
* </ol>
*/
@Service
public class AsrEventMonitorService {
@@ -37,6 +50,9 @@ public class AsrEventMonitorService {
this.clock = clock;
}
/**
* 处理一条 ASR 事件。不同通话可并行;同一 {@code callId} 由独立锁串行化。
*/
public MonitorResponse handle(AsrFinalEventRequest request) {
ReentrantLock lock = sessionStore.getLock(request.callId());
lock.lock();
@@ -53,7 +69,7 @@ public class AsrEventMonitorService {
long activeRuleVersion = keywordMatcher.activeVersion();
// Partial ASR: do not mutate match state or idempotency set
// Partial:仍返回当前累计结果,但不写入幂等集 / 匹配上下文
if (!request.isFinal()) {
return MonitorResponse.rejected(
request.callId(),
@@ -64,6 +80,7 @@ public class AsrEventMonitorService {
);
}
// 同一 callId + seq 已处理过 → 判定为重复(有界历史可容忍少量乱序)
if (!session.markProcessed(request.seq(), properties.maxProcessedSeqs())) {
return MonitorResponse.duplicate(
request.callId(),
@@ -74,7 +91,7 @@ public class AsrEventMonitorService {
);
}
// Agent side Final: accept for idempotency, never match
// 坐席 Final只计入幂等关键词仅匹配市民侧文本
if (!SPEAKER_CITIZEN.equals(request.speaker())) {
return MonitorResponse.accepted(
request.callId(),
@@ -86,6 +103,7 @@ public class AsrEventMonitorService {
);
}
// 保留最近 N 条市民 Final 再拼接,降低 ASR 切段导致的漏判
session.appendCitizenFinal(
request.seq(),
request.text(),
@@ -95,6 +113,7 @@ public class AsrEventMonitorService {
String matchText = session.buildMatchText();
List<MatchResult> matches = keywordMatcher.match(matchText);
// 本通话首次出现的事件进入 newAlerts重复命中不再提醒
List<MatchResult> newMatches = matches.stream()
.filter(match -> !session.hasAlerted(match.eventKey()))
.toList();
@@ -117,6 +136,7 @@ public class AsrEventMonitorService {
);
}
/** 将新增提醒的匹配结果转为接口侧 AlertResult。 */
private static List<AlertResult> toAlerts(CallSession session, List<MatchResult> newMatches) {
List<AlertResult> alerts = new ArrayList<>(newMatches.size());
for (MatchResult match : newMatches) {

View File

@@ -12,7 +12,7 @@ public record MonitorProperties(
) {
public MonitorProperties {
if (recentFinalWindowSize <= 0) {
recentFinalWindowSize = 2;
recentFinalWindowSize = 5;
}
if (maxProcessedSeqs <= 0) {
maxProcessedSeqs = 500;

View File

@@ -16,7 +16,7 @@ server:
port: 8080
monitor:
recent-final-window-size: 2
recent-final-window-size: 5
max-processed-seqs: 500
session-ttl: PT2H
session-cleanup-interval: PT10M

View File

@@ -34,7 +34,7 @@ class AsrEventMonitorServiceTest {
matcher.replaceSnapshot(factory.build(sampleRules(), 5L));
MonitorProperties properties = new MonitorProperties(
2,
5,
500,
Duration.ofHours(2),
Duration.ofMinutes(10)

View File

@@ -20,7 +20,7 @@ class SessionCleanupJobTest {
void cleanupUsesTtlThreshold() {
SessionStore sessionStore = mock(SessionStore.class);
MonitorProperties properties = new MonitorProperties(
2,
5,
500,
Duration.ofHours(2),
Duration.ofMinutes(10)