From 9634ace682e280202b9cd73c1e911a39dc25959b Mon Sep 17 00:00:00 2001 From: bridge Date: Sat, 22 Nov 2025 18:49:15 +0800 Subject: [PATCH] fix bug --- src/classes/ai.py | 11 ++++++++- src/sim/simulator.py | 32 +++++++++++++++---------- src/utils/ai_batch.py | 53 +++++++++++++++++++++++++++++++++++++++++ static/config.yml | 2 +- web/src/stores/world.ts | 28 ++++++++++++++++++---- 5 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 src/utils/ai_batch.py diff --git a/src/classes/ai.py b/src/classes/ai.py index 7023373..331820b 100644 --- a/src/classes/ai.py +++ b/src/classes/ai.py @@ -5,6 +5,7 @@ NPC AI 的类。 from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING +import asyncio from src.classes.world import World from src.classes.event import Event, NULL_EVENT @@ -35,8 +36,16 @@ class AI(ABC): """ results = {} max_decide_num = CONFIG.ai.max_decide_num + + # 使用 asyncio.gather 并行执行多个批次的决策 + tasks = [] for i in range(0, len(avatars_to_decide), max_decide_num): - results.update(await self._decide(world, avatars_to_decide[i:i+max_decide_num])) + tasks.append(self._decide(world, avatars_to_decide[i:i+max_decide_num])) + + if tasks: + batch_results_list = await asyncio.gather(*tasks) + for batch_result in batch_results_list: + results.update(batch_result) for avatar, result in list(results.items()): action_name_params_pairs, avatar_thinking, short_term_objective = result # type: ignore diff --git a/src/sim/simulator.py b/src/sim/simulator.py index edd5068..21db1ab 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -1,4 +1,5 @@ import random +import asyncio from src.classes.calendar import Month, Year, MonthStamp from src.classes.avatar import Avatar, Gender @@ -118,9 +119,14 @@ class Simulator: events = [] for avatar in self.world.avatar_manager.avatars.values(): avatar.update_time_effect() - for avatar in list(self.world.avatar_manager.avatars.values()): - fortune_events = await try_trigger_fortune(avatar) - events.extend(fortune_events) + + # 使用 gather 并行触发奇遇 + tasks = [try_trigger_fortune(avatar) for avatar in self.world.avatar_manager.avatars.values()] + results = await asyncio.gather(*tasks) + for res in results: + if res: + events.extend(res) + return events async def _phase_nickname_generation(self): @@ -129,11 +135,11 @@ class Simulator: """ from src.classes.nickname import process_avatar_nickname - events = [] - for avatar in list(self.world.avatar_manager.avatars.values()): - event = await process_avatar_nickname(avatar) - if event: - events.append(event) + # 并发执行 + tasks = [process_avatar_nickname(avatar) for avatar in self.world.avatar_manager.avatars.values()] + results = await asyncio.gather(*tasks) + + events = [e for e in results if e] return events async def _phase_long_term_objective_thinking(self): @@ -141,11 +147,11 @@ class Simulator: 长期目标思考阶段 检查角色是否需要生成/更新长期目标 """ - events = [] - for avatar in list(self.world.avatar_manager.avatars.values()): - event = await process_avatar_long_term_objective(avatar) - if event: - events.append(event) + # 并发执行 + tasks = [process_avatar_long_term_objective(avatar) for avatar in self.world.avatar_manager.avatars.values()] + results = await asyncio.gather(*tasks) + + events = [e for e in results if e] return events def _phase_update_celestial_phenomenon(self): diff --git a/src/utils/ai_batch.py b/src/utils/ai_batch.py new file mode 100644 index 0000000..e8b72f1 --- /dev/null +++ b/src/utils/ai_batch.py @@ -0,0 +1,53 @@ +""" +通用 AI 任务批处理器。 +用于将串行的异步任务收集起来并行执行,优化 LLM 密集型场景的性能。 +""" +import asyncio +from typing import Coroutine, Any, List + +class AITaskBatch: + """ + AI 任务批处理器。 + + 使用示例: + ```python + async with AITaskBatch() as batch: + for item in items: + batch.add(process_item(item)) + # with 块结束时,所有任务已并发执行完毕 + ``` + """ + def __init__(self): + self.tasks: List[Coroutine[Any, Any, Any]] = [] + + def add(self, coro: Coroutine[Any, Any, Any]) -> None: + """ + 添加一个协程任务到池中(不立即执行)。 + 注意:传入的协程应该自行处理结果(如修改对象状态),或者通过外部变量收集结果。 + """ + self.tasks.append(coro) + + async def run(self) -> List[Any]: + """ + 并行执行池中所有任务,并等待全部完成。 + 返回所有任务的结果列表(顺序与添加顺序一致)。 + """ + if not self.tasks: + return [] + + # 使用 gather 并发执行 + results = await asyncio.gather(*self.tasks) + + # 清空任务队列 + self.tasks = [] + return list(results) + + async def __aenter__(self) -> "AITaskBatch": + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + # 如果 with 块内部发生异常,不执行任务,直接抛出 + if exc_type: + return + await self.run() + diff --git a/static/config.yml b/static/config.yml index a03698f..7029926 100644 --- a/static/config.yml +++ b/static/config.yml @@ -14,7 +14,7 @@ paths: saves: assets/saves/ ai: - max_decide_num: 4 + max_decide_num: 3 max_parse_retries: 3 game: diff --git a/web/src/stores/world.ts b/web/src/stores/world.ts index 537fa58..8945f9a 100644 --- a/web/src/stores/world.ts +++ b/web/src/stores/world.ts @@ -65,7 +65,8 @@ export const useWorldStore = defineStore('world', () => { if (!rawEvents || rawEvents.length === 0) return; // 转换 DTO -> Domain - const newEvents: GameEvent[] = rawEvents.map(e => ({ + // 增加临时索引 _seq 记录原始逻辑顺序,用于同时间戳事件的排序 + const newEvents: GameEvent[] = rawEvents.map((e, index) => ({ id: e.id, text: e.text, content: e.content, @@ -74,13 +75,32 @@ export const useWorldStore = defineStore('world', () => { timestamp: (e.year ?? year.value) * 12 + (e.month ?? month.value), relatedAvatarIds: e.related_avatar_ids || [], isMajor: e.is_major, - isStory: e.is_story - })); + isStory: e.is_story, + _seq: index + } as GameEvent & { _seq: number })); // 排序并保留最新的 N 条 const MAX_EVENTS = 300; const combined = [...newEvents, ...events.value]; - combined.sort((a, b) => b.timestamp - a.timestamp); // 降序 + + combined.sort((a, b) => { + // 1. 先按时间戳降序(最新的月在上面) + const ta = a.timestamp; + const tb = b.timestamp; + if (tb !== ta) { + return tb - ta; + } + + // 2. 时间相同时,按原始逻辑顺序降序(后发生的在上面) + // 旧事件通常没有 _seq (undefined),视为最旧 (-1) + const seqA = (a as any)._seq ?? -1; + const seqB = (b as any)._seq ?? -1; + + // 如果都是旧事件,保持相对顺序 (Stable) + if (seqA === -1 && seqB === -1) return 0; + + return seqB - seqA; + }); events.value = combined.slice(0, MAX_EVENTS); }