群面模拟中的AI智能体发言决策引擎设计:异步收集、五维权重与阈值设计
一、引言
在群面模拟系统中,AI候选人需要像真实面试者一样"抢话"发言——既不能过于沉默,也不能喧宾夺主。如何让多个AI智能体在讨论中自然地轮流发言、有节奏地推进讨论?本文从源码层面拆解AgentEngine的核心设计:异步循环收集、五维权重评分和发言阈值机制。
二、整体架构概览
2.1 线程模型
系统配置了两个专用的线程池:
@Bean(name = "agentExecutor")
public Executor agentExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程
executor.setMaxPoolSize(50); // 最大线程
executor.setQueueCapacity(200); // 队列容量
executor.setThreadNamePrefix("agent-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
@Bean(name = "speechExecutor")
public Executor speechExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("speech-");
return executor;
}
- agentExecutor:负责运行每个房间的Agent主循环(决策循环)
- speechExecutor:负责执行具体智能体的发言(LLM调用 + WebSocket广播)
这种分离的核心思想是:决策循环是轻量级的纯计算,发言是重量级的LLM调用,互不阻塞。
三、异步循环收集机制
3.1 每房间一个独立的事件循环
@Async("agentExecutor")
public void runAgentLoop(String roomId, Agent.AgentContext context) {
while (true) {
// 1. 检查房间状态
String status = roomOps.getRoomStatus(roomId);
if (status == null || !"ACTIVE".equals(status)) break;
// 2. 检查阶段是否该自动推进
if (loopPhase == Phase.ICEBREAK && hasAllCandidatesSpokenInIcebreak(roomId)) {
roomOps.changePhase(roomId, Phase.STATEMENT.name());
Thread.sleep(300);
continue;
}
// 3. 收集所有智能体的发言请求
List<SpeakRequest> requests = collectSpeakRequests(roomId, freshContext);
pendingRequests.put(roomId, requests);
// 4. 如果有人正在发言,跳过本轮
if (speakingInProgress.get(roomId)) {
Thread.sleep(300);
continue;
}
// 5. 决策下一个发言人
SpeakRequest winner = decideNextSpeaker(roomId, requests);
// 6. 异步执行发言
if (winner != null) {
executeAgentSpeech(roomId, winner, freshContext);
}
// 7. 节流等待
Thread.sleep(500);
}
}
关键设计点:
- @Async("agentExecutor")让每个房间的循环运行在独立的线程中
- 轮询模式:每500ms一个周期,而不是事件驱动——这避免了复杂的并发同步
- speakingInProgress 锁:用ConcurrentHashMap做轻量级互斥,确保同一时间只有一个智能体在发言
- lastCompletedSpeaker:记录上一个发言者,避免连续发言
3.2 收集发言分数的流程
private List<SpeakRequest> collectSpeakRequests(String roomId, Agent.AgentContext context) {
for (Agent agent : agents) {
// 破冰阶段每人只说一次
if (currentPhase == Phase.ICEBREAK && hasSpokenInCurrentPhase(...)) {
continue;
}
// 构建上下文(当前阶段、已用时间、沉默时长、最近事件)
SpeakContext speakContext = new SpeakContext(
currentPhase,
elapsedTime,
silenceDuration,
recentEvents,
Map.of()
);
// 调用智能体的评分函数
double score = agent.calculateSpeakScore(speakContext);
// 过滤:超过阈值才加入候选池
if (score >= threshold) {
requests.add(request);
}
}
// 破冰阶段保底:如果没人达标,选最高分
if (currentPhase == Phase.ICEBREAK && requests.isEmpty()) {
requests.add(allScores.get(0));
}
return requests;
}
为什么是轮询而不是推送?
1. 解耦:AgentEngine不需要维护与每个Agent的连接
2. 一致性:在单次循环中收集所有分数,避免因并发导致的时间窗口不一致
3. 容错:某个Agent计算出错不影响其他Agent的评分收集
四、五维权重评分设计
4.1 评分公式
score = W1_ROLE(0.25) × roleScore
+ W2_CONTENT(0.25) × contentRelevance
+ W3_EMOTION(0.15) × emotionActivation
+ W4_TIMING(0.20) × timingWindow
+ W5_PHASE(0.15) × phaseWeight
五维权重之和 = **1.0**,其中角色和内容权重最高。
4.2 维度一:角色匹配度(Role Score)- 权重0.25
不同角色在不同阶段有不同的发言倾向:
| 角色 | 破冰 | 陈述 | 辩论 | 收敛 | 总结 |
|------|------|------|------|------|------|
| **Leader** | 0.7 | 0.8 | **0.9** | **0.9** | 0.8 |
| **Coordinator** | **0.8** | 0.6 | 0.5 | **0.9** | 0.7 |
| **Viewpoint** | 0.3 | **0.9** | **0.9** | 0.5 | 0.6 |
| **Executor** | 0.5 | 0.5 | 0.6 | **0.7** | 0.5 |
| **Supporter** | 0.6 | 0.4 | 0.3 | **0.6** | 0.4 |
设计意图:
- **Leader**在辩论和收敛阶段最有发言动力(引导方向、总结决策)
- **Coordinator**在破冰和收敛阶段最活跃(破冰暖场、调和分歧)
- **Viewpoint**在陈述和辩论阶段最高(输出观点、深度分析)
- **Executor**在收敛阶段关注方案可行性
- **Supporter**整体分数偏低,体现"补位"角色特性
4.3 维度二:内容相关性(Content Relevance)- 权重0.25
private List<SpeakRequest> collectSpeakRequests(String roomId, Agent.AgentContext context) {
for (Agent agent : agents) {
// 破冰阶段每人只说一次
if (currentPhase == Phase.ICEBREAK && hasSpokenInCurrentPhase(...)) {
continue;
}
// 构建上下文(当前阶段、已用时间、沉默时长、最近事件)
SpeakContext speakContext = new SpeakContext(
currentPhase,
elapsedTime,
silenceDuration,
recentEvents,
Map.of()
);
// 调用智能体的评分函数
double score = agent.calculateSpeakScore(speakContext);
// 过滤:超过阈值才加入候选池
if (score >= threshold) {
requests.add(request);
}
}
// 破冰阶段保底:如果没人达标,选最高分
if (currentPhase == Phase.ICEBREAK && requests.isEmpty()) {
requests.add(allScores.get(0));
}
return requests;
}
当前实现使用模拟值,实际可演化为:
- 基于向量相似度计算当前话题与Agent知识领域的相关性
- 基于NLP分析最近的讨论主题是否匹配Agent的角色定位
4.4 维度三:情绪激活度(Emotion Activation)- 权重0.15
protected double calculateEmotionActivation(SpeakContext context) {
Double attackLevel = state.getEmotionVector().get("attackLevel");
return attackLevel != null ? attackLevel : 0.3;
}
基于MBTI人格的情绪向量:
| 情绪维度 | 含义 | 计算方式 |
|---------|------|---------|
| tension | 紧张度 | E→0.4, I→0.5 |
| confidence | 自信度 | N→0.7, S→0.5 |
| **attackLevel** | 攻击性 | E+T→0.6, 其他→0.3 |
| stability | 情绪稳定度 | J→0.7, P→0.5 |
例如:ENTJ(攻击性0.6)在辩论阶段会比INFP(攻击性0.3)更主动发言。
4.5 维度四:时机窗口(Timing Window)- 权重0.20
protected double calculateTimingWindow(SpeakContext context) {
long timeSinceLastSpeak = System.currentTimeMillis() - state.getLastSpeakTime();
if (timeSinceLastSpeak < 3000) {
return 0.1; // 刚说完,冷却中
} else if (timeSinceLastSpeak < 5000) {
return 0.5; // 短暂冷却期
} else if (context.silenceDuration() > 400) {
return 0.9; // 冷场了,抢话机会
}
return 0.6; // 正常状态
}
设计逻辑:
1. 刚发言完3秒内→ 分数极低(0.1),配合冷却期防止连续发言
2. 3-5秒缓冲期→ 中等分数(0.5),可以再次发言但不优先
3. 沉默超过400ms→ 高分(0.9),防止讨论冷场
4. 正常状态→ 基准分(0.6)
4.6 维度五:阶段权重(Phase Weight)- 权重0.15
protected double calculatePhaseWeight(Phase phase) {
return switch (phase) {
case ICEBREAK -> 0.3; // 破冰阶段,发言动力低(每人只说一次)
case STATEMENT -> 0.8; // 陈述阶段,每个人都想输出观点
case DEBATE -> 0.7; // 辩论阶段,高参与度
case CONVERGENCE -> 0.5; // 收敛阶段,趋于稳定
case SUMMARY -> 0.9; // 总结阶段,都想做总结者
};
}
- 破冰(0.3)最低——每人只说一次自我介绍
- 总结(0.9)最高——每个人都想做那个画句号的人
- 陈述(0.8)和辩论(0.7)次高——核心讨论环节
五、发言阈值与仲裁机制
5.1 双层阈值设计
private static final double SPEAK_THRESHOLD = 0.65; // 常规阈值
private static final double ICEBREAK_SPEAK_THRESHOLD = 0.50; // 破冰阈值
为什么是0.65?
假设一个普通场景:角色分0.6 + 内容分0.5 + 情绪分0.3 + 时机分0.6 + 阶段分0.8,加权计算:
score = 0.25×0.6 + 0.25×0.5 + 0.15×0.3 + 0.20×0.6 + 0.15×0.8
= 0.15 + 0.125 + 0.045 + 0.12 + 0.12
= 0.56
要达到0.65,需要在多个维度都处于活跃状态,例如:
Leader辩论阶段:0.25×0.9 + 0.25×0.7 + 0.15×0.6 + 0.20×0.9 + 0.15×0.7
= 0.225 + 0.175 + 0.090 + 0.180 + 0.105
= 0.775 ← 远超阈值,很可能会发言
设计目标:
- 0.65作为"软门槛":智能体需要"有一定的表达欲"才能进入候选池
- 破冰阶段降低到0.50:因为破冰的自我介绍不需要太强的表达动机
- 结合冷却期惩罚(×0.3):刚发言完的智能体分数会被压到0.2以下,自然进入静默
5.2 发言仲裁器
private SpeakRequest decideNextSpeaker(String roomId, List<SpeakRequest> requests) {
if (requests == null || requests.isEmpty()) return null;
requests.sort((a, b) -> Double.compare(b.getSpeakScore(), a.getSpeakScore()));
// 检查发言时间比例,确保无人超过35%
for (SpeakRequest request : requests) {
if (currentSpeaker 中已有此人) continue; // 正在发言,跳过
if (是上一个发言者) continue; // 避免连说
double ratio = getAgentSpeakingTime(events, request.getAgentId()) / totalSpeakingTime;
if (ratio < MAX_SPEAK_RATIO) { // MAX_SPEAK_RATIO = 0.35
return request;
}
}
// 备选:找非重复发言者
return selectNonRepeatingWinner(requests, lastSpeakerId);
}
仲裁规则优先级:
1. **非重复优先**:避免同一人连续发言
2. **35%时间比例上限**:防止某一智能体垄断讨论
3. **分数排序**:同等条件下选最高分
5.3 冷却期机制
// Agent发言后立即进入冷却
state.setInCooldown(true);
new Thread(() -> {
Thread.sleep(3000); // 3秒冷却
state.setInCooldown(false);
}).start();
// 冷却期分数打三折
if (state != null && state.isInCooldown()) {
score *= 0.3;
}
这意味着冷却期内的智能体分数被压到0.2以下,基本不可能再次发言,强制轮流。
六、完整的发言决策流程
┌─────────────────────────────────────────────────┐
│ AgentEngine.runAgentLoop() │
│ 每500ms一个周期 │
├─────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ 1. 检查房间状态 │────>│ 2. 收集所有Agent │ │
│ │ 是否为ACTIVE │ │ 的发言分数 │ │
│ └────────────────┘ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 3. 每个Agent计算五维加权分数 │ │
│ │ role × 0.25 + content × 0.25 │ │
│ │ + emotion × 0.15 + timing × 0.20 │ │
│ │ + phase × 0.15 │ │
│ └────────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 4. 阈值过滤 (score ≥ 0.65) │ │
│ │ 破冰阶段: ≥ 0.50 │ │
│ │ 冷却期惩罚: ×0.3 │ │
│ └────────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 5. 仲裁器决策下一个发言人 │ │
│ │ - 按分数排序 │ │
│ │ - 跳过当前发言者/上一位发言者 │ │
│ │ - 检查时间比例 ≤ 35% │ │
│ └────────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 6. 异步执行发言(speechExecutor) │ │
│ │ - 调用DeepSeek API生成内容 │ │
│ │ - WebSocket广播给房间所有成员 │ │
│ │ - 持久化Event到数据库 │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
七、设计总结与思考
7.1 为什么这样设计?
| 设计目标 | 实现方式 |
|---------|---------|
| **公平性** | 35%时间上限 + 冷却期 + 非重复优先 |
| **自然感** | 角色阶段匹配 + 冷场抢话 + 随机扰动 |
| **多样性** | 16种MBTI × 5种角色,不同人格发言风格各异 |
| **可扩展** | 五维权重可调,阈值可配,Agent可热插拔 |
7.2 可优化方向
1. **动态阈值**:根据房间人数、阶段进展自动调整阈值
2. **情感衰减**:长期未发言的Agent应获得情感补偿分
3. **内容相关性**:引入NLP语义匹配替代随机模拟
4. **打断检测**:当前已支持用户打断,未来可加入Agent间打断机制
7.3 与真实群面的映射
| 真实群面 | 系统实现 |
|---------|---------|
| 面试者性格各异 | MBTI 16型人格映射到5维向量 |
| 有人喜欢抢话 | 攻击性(attackLevel)高 → 情绪激活度高 |
| 有人喜欢控场 | Leader角色在辩论收敛阶段分数高 |
| 冷场时需要有人救场 | silenceDuration > 400ms → 时机分0.9 |
| 不能一个人说太多 | 35%时间比例上限 + 冷却期 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)