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:
@@ -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 拼接
|
||||
↓
|
||||
文本标准化
|
||||
↓
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -12,7 +12,7 @@ public record MonitorProperties(
|
||||
) {
|
||||
public MonitorProperties {
|
||||
if (recentFinalWindowSize <= 0) {
|
||||
recentFinalWindowSize = 2;
|
||||
recentFinalWindowSize = 5;
|
||||
}
|
||||
if (maxProcessedSeqs <= 0) {
|
||||
maxProcessedSeqs = 500;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user