diff --git a/README.md b/README.md index 4e6267a..9e4964d 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ - ✅ LLM接口集成 - ✅ 角色AI系统(规则AI + LLM AI) - ✅ 协程化决策机制,异步运行 -- [ ] 长期规划和目标导向行为) +- [ ] 长期规划和目标导向行为 - [ ] 突发动作响应系统(对外界刺激的即时反应) - [ ] LLM驱动的NPC对话、思考、互动、事件总结 - [ ] 智能决策系统 diff --git a/src/classes/ai.py b/src/classes/ai.py index 5c670ee..1eb402b 100644 --- a/src/classes/ai.py +++ b/src/classes/ai.py @@ -22,9 +22,23 @@ class AI(ABC): """ AI的基类 """ + pass + +class SingleAI(AI): + """ + 单个角色的AI + """ def __init__(self, avatar: Avatar): self.avatar = avatar + @abstractmethod + async def _decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, str]: + """ + 决策逻辑:决定执行什么动作和参数 + 由子类实现具体的决策逻辑 + """ + pass + async def decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, str, Event]: """ 决定做什么,同时生成对应的事件 @@ -38,15 +52,37 @@ class AI(ABC): return action_name, action_params, avatar_thinking, event - @abstractmethod - async def _decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, str]: +class GroupAI(AI): + """ + 多个角色的AI + """ + def __init__(self): + self.avatars = [] + + async def _decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple[ACTION_NAME, ACTION_PARAMS, str]]: """ 决策逻辑:决定执行什么动作和参数 由子类实现具体的决策逻辑 """ pass -class RuleAI(AI): + async def decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple[ACTION_NAME, ACTION_PARAMS, str, Event]]: + """ + 决定做什么,同时生成对应的事件 + """ + # 先决定动作和参数 + results = await self._decide(world, avatars_to_decide) + + for avatar, result in results.items(): + action_name, action_params, avatar_thinking = result + action = avatar.create_action(action_name) + event = action.get_event(**action_params) + results[avatar] = (action_name, action_params, avatar_thinking, event) + + # 获取动作对象并生成事件 + return results + +class RuleAI(SingleAI): """ 规则AI """ @@ -80,7 +116,7 @@ class RuleAI(AI): region_with_best_essence = max(regions, key=lambda region: region.essence.get_density(essence_type)) return region_with_best_essence -class LLMAI(AI): +class LLMAI(GroupAI): """ LLM AI 一些思考: @@ -90,20 +126,23 @@ class LLMAI(AI): 不能每个单步step都调用一次LLM来决定下一步做什么。这样子一方面动作一直乱变,另一方面也太费token了。 decide的作用是,拉取既有的动作链(如果没有了就call_llm),再根据动作链决定动作,以及动作之间的衔接。 """ - async def _decide(self, world: World) -> tuple[ACTION_NAME, ACTION_PARAMS, str]: + async def _decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple[ACTION_NAME, ACTION_PARAMS, str]]: """ 异步决策逻辑:通过LLM决定执行什么动作和参数 """ - action_space_str = self.avatar.get_action_space_str() - avatar_infos_str = str(self.avatar) - regions_str = "\n".join([str(region) for region in world.map.regions.values()]) - avatar_persona = self.avatar.persona.prompt - dict_info = { - "action_space": action_space_str, - "avatar_infos": avatar_infos_str, - "regions": regions_str, - "avatar_persona": avatar_persona + global_info = world.get_prompt() + avatar_infos = {avatar.id: avatar.get_prompt() for avatar in avatars_to_decide} + info = { + "avatar_infos": avatar_infos, + "global_info": global_info } - res = await get_ai_prompt_and_call_llm_async(dict_info) - action_name, action_params, avatar_thinking = res["action_name"], res["action_params"], res["avatar_thinking"] - return action_name, action_params, avatar_thinking \ No newline at end of file + res = await get_ai_prompt_and_call_llm_async(info) + results = {} + for avatar in avatars_to_decide: + action_name, action_params, avatar_thinking = res[avatar.id]["action_name"], res[avatar.id]["action_params"], res[avatar.id]["avatar_thinking"] + results[avatar] = (action_name, action_params, avatar_thinking) + return results + +llm_ai = LLMAI() +# rule_ai = RuleAI() +rule_ai = None \ No newline at end of file diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 24cccd7..b5fe31d 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -14,7 +14,6 @@ from src.classes.age import Age from src.classes.event import NULL_EVENT from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_PAIR -from src.classes.ai import AI, RuleAI, LLMAI from src.classes.persona import Persona, personas_by_id from src.utils.id_generator import get_avatar_id from src.utils.config import CONFIG @@ -53,28 +52,29 @@ class Avatar: root: Root = field(default_factory=lambda: random.choice(list(Root))) persona: Persona = field(default_factory=lambda: random.choice(list(personas_by_id.values()))) - ai: AI = None cur_action_pair: Optional[ACTION_PAIR] = None history_action_pairs: list[ACTION_PAIR] = field(default_factory=list) thinking: str = "" def __post_init__(self): """ - 在Avatar创建后自动初始化tile和AI + 在Avatar创建后自动初始化tile """ self.tile = self.world.map.get_tile(self.pos_x, self.pos_y) - if CONFIG.ai.mode == "llm": - self.ai = LLMAI(self) - else: - self.ai = RuleAI(self) - def __str__(self) -> str: + def __hash__(self) -> int: + return hash(self.id) + + def get_info(self) -> str: """ 获取avatar的详细信息 尽量多打一些,因为会用来给LLM进行决策 """ return f"Avatar(id={self.id}, 性别={self.gender}, 年龄={self.age}, name={self.name}, 区域={self.tile.region.name}, 灵根={self.root.value}, 境界={self.cultivation_progress})" + def __str__(self) -> str: + return self.get_info() + def create_action(self, action_name: ACTION_NAME) -> Action: """ 根据动作名称创建新的action实例 @@ -95,21 +95,17 @@ class Avatar: raise ValueError(f"未找到名为 '{action_name}' 的动作类") + def load_decide_result(self, action_name: ACTION_NAME, action_args: ACTION_PARAMS, avatar_thinking: str): + action = self.create_action(action_name) + self.thinking = avatar_thinking + self.cur_action_pair = (action, action_args) async def act(self): """ 角色执行动作。 - 实际上分为两步:决定做什么(decide)和实际去做(do) + 注意这里只负责执行,不负责决定做什么动作。 事件只在决定动作时产生,执行过程不产生事件 """ - event = NULL_EVENT - - if self.cur_action_pair is None: - # 决定动作时生成事件 - action_name, action_args, avatar_thinking, event = await self.ai.decide(self.world) - action = self.create_action(action_name) - self.thinking = avatar_thinking - self.cur_action_pair = (action, action_args) # 纯粹执行动作,不产生事件 action, action_params = self.cur_action_pair @@ -119,7 +115,7 @@ class Avatar: # 将完成的动作对添加到历史记录中 self._add_to_history(self.cur_action_pair) - return event + return def _add_to_history(self, action_pair: ACTION_PAIR) -> None: """ @@ -208,6 +204,15 @@ class Avatar: action_space = [{"action": action.__class__.__name__, "params": action.PARAMS, "comment": action.COMMENT} for action in doable_actions] return action_space + def get_prompt(self) -> str: + """ + 获取角色提示词 + """ + info = self.get_info() + persona = self.persona.prompt + action_space = self.get_action_space_str() + return f"{info}\n其个性为:{persona}\n决策时需参考这个角色的个性。\n该角色的动作空间及其参数为:{action_space}" + def get_new_avatar_from_ordinary(world: World, current_month_stamp: MonthStamp, name: str, age: Age): """ 从凡人中来的新修士 diff --git a/src/classes/world.py b/src/classes/world.py index c7d6dae..4f0bf5a 100644 --- a/src/classes/world.py +++ b/src/classes/world.py @@ -6,4 +6,8 @@ from src.classes.calendar import Year, Month, MonthStamp @dataclass class World(): map: Map - month_stamp: MonthStamp \ No newline at end of file + month_stamp: MonthStamp + + def get_prompt(self) -> str: + regions_str = "\n".join([str(region) for region in self.map.regions.values()]) + return f"世界地图上存在的区域为:{regions_str}" \ No newline at end of file diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 0f58320..1c92fbf 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -5,6 +5,7 @@ from src.classes.avatar import Avatar, get_new_avatar_from_ordinary, Gender from src.classes.age import Age from src.classes.world import World from src.classes.event import Event, is_null_event +from src.classes.ai import llm_ai, rule_ai from src.utils.names import get_random_name from src.utils.config import CONFIG @@ -25,11 +26,23 @@ class Simulator: events = [] # list of Event death_avatar_ids = [] # list of str + # 决定动作行为 + avatars_to_decide = [avatar for avatar in list(self.avatars.values()) if avatar.cur_action_pair is None] + if CONFIG.ai.mode == "llm": + ai = llm_ai + else: + ai = rule_ai + if avatars_to_decide: + decide_results = await ai.decide(self.world, avatars_to_decide) + for avatar, result in decide_results.items(): + action_name, action_args, avatar_thinking, event = result + avatar.load_decide_result(action_name, action_args, avatar_thinking) + if not is_null_event(event): + events.append(event) + # 结算角色行为 for avatar_id, avatar in self.avatars.items(): - event = await avatar.act() - if not is_null_event(event): - events.append(event) + await avatar.act() if avatar.death_by_old_age(): death_avatar_ids.append(avatar_id) event = Event(self.world.month_stamp, f"{avatar.name} 老死了,时年{avatar.age.get_age()}岁") diff --git a/src/utils/llm.py b/src/utils/llm.py index 7c958ca..be13f2d 100644 --- a/src/utils/llm.py +++ b/src/utils/llm.py @@ -80,8 +80,9 @@ async def get_prompt_and_call_llm_async(template_path: Path, infos: dict) -> str template = read_txt(template_path) prompt = get_prompt(template, infos) res = await call_llm_async(prompt) - # print(f"res = {res}") json_res = parse_llm_response(res) + # print(f"prompt = {prompt}") + # print(f"json_res = {json_res}") return json_res def get_ai_prompt_and_call_llm(infos: dict) -> dict: diff --git a/static/config.yml b/static/config.yml index 8f70a8a..51e75cd 100644 --- a/static/config.yml +++ b/static/config.yml @@ -7,8 +7,8 @@ paths: templates: static/templates/ ai: - mode: "rule" # "rule" or "llm" + mode: "llm" # "rule" or "llm" game: - init_npc_num: 2 + init_npc_num: 3 npc_birth_rate_per_month: 0.001 \ No newline at end of file diff --git a/static/templates/ai.txt b/static/templates/ai.txt index 7098926..ee774ac 100644 --- a/static/templates/ai.txt +++ b/static/templates/ai.txt @@ -1,18 +1,16 @@ 你是一个决策者,这是一个修仙的仙侠世界,你负责来决定一些NPC的下一步行为。 世界地图上存在的区域为: -{regions} -你需要进行决策的NPC的基本信息为: +{global_info} +你需要进行决策的NPC的dict[AvatarId, info]为 {avatar_infos} -其个性为:{avatar_persona} -决策时需参考这个角色的个性。 -该角色的动作空间及其参数为: -{action_space} -注意,只返回json格式的动作 -返回格式: +注意,只返回json格式的结果。 +分Avatar进行返回,格式为: {{ - "thinking": ..., // 简单思考应该怎么决策 - "action_name": ..., - "action_params": ..., - "avatar_thinking": ..., // 从角色角度,以第一人称视角,描述心态,符合世界观 + AvatarId: {{ + "thinking": ..., // 简单思考应该怎么决策 + "action_name": ..., + "action_params": ..., + "avatar_thinking": ..., // 从角色角度,以第一人称视角,描述心态,符合世界观 + }} }} \ No newline at end of file