158 lines
6.7 KiB
Python
158 lines
6.7 KiB
Python
"""
|
||
NPC AI的类。
|
||
这里指的不是LLM或者Machine Learning,而是NPC的决策机制
|
||
分为两类:规则AI和LLM AI
|
||
"""
|
||
from __future__ import annotations
|
||
from abc import ABC, abstractmethod
|
||
from typing import TYPE_CHECKING
|
||
import random
|
||
|
||
from src.classes.world import World
|
||
from src.classes.region import Region
|
||
from src.classes.root import get_essence_types_for_root
|
||
from src.classes.event import Event, NULL_EVENT
|
||
from src.utils.llm import get_ai_prompt_and_call_llm_async
|
||
from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_PAIR, ACTION_NAME_PARAMS_PAIRS
|
||
from src.utils.config import CONFIG
|
||
from src.classes.actions import ACTION_INFOS_STR
|
||
|
||
if TYPE_CHECKING:
|
||
from src.classes.avatar import Avatar
|
||
|
||
class AI(ABC):
|
||
"""
|
||
抽象AI:统一采用批量接口。
|
||
原先的 GroupAI(多个角色的AI)语义被保留并上移到此基类。
|
||
子类需实现 _decide(world, avatars) 返回每个 Avatar 的 (action_name, action_params, thinking)。
|
||
"""
|
||
|
||
@abstractmethod
|
||
async def _decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple]:
|
||
pass
|
||
|
||
async def decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple[ACTION_NAME_PARAMS_PAIRS, str, str, Event]]:
|
||
"""
|
||
决定做什么,同时生成对应的事件。
|
||
一个ai支持批量生成多个avatar的动作。
|
||
这对LLM AI节省时间和token非常有意义。
|
||
"""
|
||
results = {}
|
||
max_decide_num = CONFIG.ai.max_decide_num
|
||
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]))
|
||
|
||
for avatar, result in list(results.items()):
|
||
# 兼容:RuleAI 返回单动作,LLMAI 返回动作链
|
||
if result and isinstance(result[0], list):
|
||
action_name_params_pairs, avatar_thinking, objective = result # type: ignore
|
||
else:
|
||
action_name, action_params, avatar_thinking, objective = result # type: ignore
|
||
action_name_params_pairs = [(action_name, action_params)]
|
||
# 不在决策阶段生成开始事件,提交阶段统一触发
|
||
results[avatar] = (action_name_params_pairs, avatar_thinking, objective, NULL_EVENT)
|
||
|
||
return results
|
||
|
||
class RuleAI(AI):
|
||
"""
|
||
规则AI(批量接口,内部逐个决策)
|
||
"""
|
||
|
||
def __decide(self, world: World, avatar: "Avatar", regions: list[Region]) -> tuple[ACTION_NAME, ACTION_PARAMS, str, str]:
|
||
"""
|
||
单个 Avatar 的决策逻辑。
|
||
先做一个简单的:
|
||
1. 找到自己灵根对应的最好的区域
|
||
2. 检测自己是否在最好的区域
|
||
3. 如果不在,则移动到最好的区域
|
||
4. 如果已经到达最好的区域,则进行修炼
|
||
5. 如果需要突破境界了,则突破境界
|
||
"""
|
||
if random.random() < 0.1:
|
||
return ("Play", {}, "", "放松一下,缓解修行压力")
|
||
|
||
best_region = self.get_best_region_for_avatar(avatar, regions)
|
||
|
||
if avatar.is_in_region(best_region):
|
||
if avatar.cultivation_progress.can_break_through():
|
||
return ("Breakthrough", {}, "", "尽快突破到更高境界")
|
||
else:
|
||
return ("Cultivate", {}, "", "稳步提升修为")
|
||
else:
|
||
return ("MoveToRegion", {"region": best_region.name}, "", f"前往{best_region.name}修行")
|
||
|
||
async def _decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple[ACTION_NAME, ACTION_PARAMS, str, str]]:
|
||
"""
|
||
决策逻辑:批量接口的实现上,逐个 Avatar 调用 __decide 进行独立决策,
|
||
以保持规则AI的可控性与可测试性。
|
||
"""
|
||
results: dict[Avatar, tuple[ACTION_NAME, ACTION_PARAMS, str, str]] = {}
|
||
regions: list[Region] = list(world.map.regions.values())
|
||
|
||
for avatar in avatars_to_decide:
|
||
results[avatar] = self.__decide(world, avatar, regions)
|
||
|
||
return results
|
||
|
||
def get_best_region_for_avatar(self, avatar: "Avatar", regions: list[Region]) -> Region:
|
||
"""
|
||
根据avatar的灵根找到最适合的区域
|
||
"""
|
||
essence_types = get_essence_types_for_root(avatar.root)
|
||
def best_density(region: Region) -> int:
|
||
return max((region.essence.get_density(et) for et in essence_types), default=0)
|
||
region_with_best_essence = max(regions, key=best_density)
|
||
return region_with_best_essence
|
||
|
||
class LLMAI(AI):
|
||
"""
|
||
LLM AI
|
||
一些思考:
|
||
AI动作应该分两类:
|
||
1. 长期动作,比如要持续很长一段时间的行为
|
||
2. 突发应对动作,比如突然有人要攻击NPC,这个时候的反应
|
||
"""
|
||
|
||
async def _decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple[ACTION_NAME_PARAMS_PAIRS, str, str]]:
|
||
"""
|
||
异步决策逻辑:通过LLM决定执行什么动作和参数
|
||
"""
|
||
global_info = world.get_info()
|
||
# 在提示中包含处于角色观测范围内的其他角色
|
||
avatar_infos = {}
|
||
for avatar in avatars_to_decide:
|
||
observed = world.get_observable_avatars(avatar)
|
||
avatar_infos[avatar.name] = avatar.get_prompt_info(observed)
|
||
general_action_infos = ACTION_INFOS_STR
|
||
info = {
|
||
"avatar_infos": avatar_infos,
|
||
"global_info": global_info,
|
||
"general_action_infos": general_action_infos,
|
||
}
|
||
res = await get_ai_prompt_and_call_llm_async(info)
|
||
results: dict[Avatar, tuple[ACTION_NAME_PARAMS_PAIRS, str, str]] = {}
|
||
for avatar in avatars_to_decide:
|
||
r = res[avatar.name]
|
||
# 仅接受 action_name_params_pairs,不再支持单个 action_name/action_params
|
||
raw_pairs = r["action_name_params_pairs"]
|
||
pairs: ACTION_NAME_PARAMS_PAIRS = []
|
||
for p in raw_pairs:
|
||
if isinstance(p, list) and len(p) == 2:
|
||
pairs.append((p[0], p[1]))
|
||
elif isinstance(p, dict) and "action_name" in p and "action_params" in p:
|
||
pairs.append((p["action_name"], p["action_params"]))
|
||
else:
|
||
# 跳过无法解析的项
|
||
continue
|
||
# 至少有一个
|
||
if not pairs:
|
||
raise ValueError(f"LLM未返回有效的action_name_params_pairs: {r}")
|
||
|
||
avatar_thinking = r.get("avatar_thinking", r.get("thinking", ""))
|
||
objective = r.get("objective", "")
|
||
results[avatar] = (pairs, avatar_thinking, objective)
|
||
return results
|
||
|
||
llm_ai = LLMAI()
|
||
rule_ai = RuleAI() |