refactor llm

This commit is contained in:
bridge
2025-11-19 01:23:55 +08:00
parent c4bc8daddc
commit e7d6ce7879
37 changed files with 499 additions and 315 deletions

View File

@@ -168,7 +168,7 @@ class ActualActionMixin():
...
@abstractmethod
def finish(self, **params) -> list[Event]:
async def finish(self, **params) -> list[Event]:
return []

View File

@@ -71,7 +71,7 @@ class Battle(InstantAction):
# InstantAction 已实现 step 完成
def finish(self, avatar_name: str) -> list[Event]:
async def finish(self, avatar_name: str) -> list[Event]:
res = self._last_result
if not (isinstance(res, tuple) and len(res) == 4):
return []
@@ -87,10 +87,10 @@ class Battle(InstantAction):
pass
result_event = Event(self.world.month_stamp, result_text, related_avatars=rel_ids, is_major=True)
# 生成战斗小故事(同步调用,与其他动作保持一致)
# 生成战斗小故事
target = self._get_target(avatar_name)
start_text = self._start_event_content if hasattr(self, '_start_event_content') else result_event.content
story = StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT)
story = await StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT)
story_event = Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True)
return [result_event, story_event]

View File

@@ -118,7 +118,7 @@ class Breakthrough(TimedAction):
# TimedAction 已统一 step 逻辑
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
if not self._last_result:
return []
result_ok = self._last_result[0] == "success"
@@ -139,7 +139,7 @@ class Breakthrough(TimedAction):
# 故事参与者:本体 +(可选)相关角色
prompt = TribulationSelector.get_story_prompt(str(calamity))
story = StoryTeller.tell_story(core_text, ("突破成功" if result_ok else "突破失败"), self.avatar, self._calamity_other, prompt=prompt)
story = await StoryTeller.tell_story(core_text, ("突破成功" if result_ok else "突破失败"), self.avatar, self._calamity_other, prompt=prompt)
events.append(Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True))
return events

View File

@@ -85,7 +85,7 @@ class Catch(TimedAction):
region = self.avatar.tile.region
return Event(self.world.month_stamp, f"{self.avatar.name}{region.name} 尝试御兽", related_avatars=[self.avatar.id])
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
res = self._caught_result
if not (isinstance(res, tuple) and len(res) == 3):
return []

View File

@@ -71,7 +71,7 @@ class Cultivate(TimedAction):
# TimedAction 已统一 step 逻辑
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
return []

View File

@@ -32,7 +32,7 @@ class DevourMortals(TimedAction):
def start(self) -> Event:
return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇开始吞噬凡人", related_avatars=[self.avatar.id])
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
return []

View File

@@ -75,7 +75,7 @@ class Escape(InstantAction):
# InstantAction 已实现 step 完成
def finish(self, avatar_name: str) -> list[Event]:
async def finish(self, avatar_name: str) -> list[Event]:
return []

View File

@@ -65,7 +65,7 @@ class Harvest(TimedAction):
# TimedAction 已统一 step 逻辑
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
return []

View File

@@ -43,7 +43,7 @@ class HelpMortals(TimedAction):
# TimedAction 已统一 step 逻辑
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
return []

View File

@@ -65,7 +65,7 @@ class Hunt(TimedAction):
# TimedAction 已统一 step 逻辑
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
return []

View File

@@ -67,7 +67,7 @@ class MoveAwayFromAvatar(TimedAction):
# TimedAction 已统一 step 逻辑
def finish(self, avatar_name: str) -> list[Event]:
async def finish(self, avatar_name: str) -> list[Event]:
return []

View File

@@ -47,7 +47,7 @@ class MoveAwayFromRegion(InstantAction):
# InstantAction 已实现 step 完成
def finish(self, region: str) -> list[Event]:
async def finish(self, region: str) -> list[Event]:
return []

View File

@@ -59,7 +59,7 @@ class MoveToAvatar(DefineAction, ActualActionMixin):
done = self.avatar.tile == target.tile
return ActionResult(status=(ActionStatus.COMPLETED if done else ActionStatus.RUNNING), events=[])
def finish(self, avatar_name: str) -> list[Event]:
async def finish(self, avatar_name: str) -> list[Event]:
return []

View File

@@ -52,7 +52,7 @@ class MoveToRegion(DefineAction, ActualActionMixin):
done = self.avatar.is_in_region(r) or ((self.avatar.pos_x, self.avatar.pos_y) in getattr(r, "cors", ()))
return ActionResult(status=(ActionStatus.COMPLETED if done else ActionStatus.RUNNING), events=[])
def finish(self, region: Region | str) -> list[Event]:
async def finish(self, region: Region | str) -> list[Event]:
return []

View File

@@ -63,7 +63,7 @@ class NurtureWeapon(TimedAction):
related_avatars=[self.avatar.id]
)
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
weapon_name = self.avatar.weapon.name if self.avatar.weapon else "兵器"
proficiency = self.avatar.weapon_proficiency
# 注意升华事件已经在_execute中添加这里只添加完成事件

View File

@@ -31,7 +31,7 @@ class Play(TimedAction):
# TimedAction 已统一 step 逻辑
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
return []

View File

@@ -48,7 +48,7 @@ class PlunderMortals(TimedAction):
# TimedAction 已统一 step 逻辑
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
return []

View File

@@ -59,7 +59,7 @@ class SelfHeal(TimedAction):
# TimedAction 已统一 step 逻辑
def finish(self) -> list[Event]:
async def finish(self) -> list[Event]:
healed_total = int(getattr(self, "_healed_total", 0))
# 统一用一次事件简要反馈
return [Event(self.world.month_stamp, f"{self.avatar.name} 疗伤完成HP已回满本次恢复{healed_total}当前HP {self.avatar.hp}", related_avatars=[self.avatar.id])]

View File

@@ -79,7 +79,7 @@ class SellItems(InstantAction):
# InstantAction 已实现 step 完成
def finish(self, item_name: str) -> list[Event]:
async def finish(self, item_name: str) -> list[Event]:
return []

View File

@@ -75,6 +75,6 @@ class SwitchWeapon(InstantAction):
related_avatars=[self.avatar.id]
)
def finish(self, weapon_type_name: str) -> list[Event]:
async def finish(self, weapon_type_name: str) -> list[Event]:
return []

View File

@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
from src.classes.world import World
from src.classes.event import Event, NULL_EVENT
from src.utils.llm import get_ai_prompt_and_call_llm_async
from src.utils.llm import call_ai_action
from src.classes.typings import ACTION_NAME_PARAMS_PAIRS
from src.utils.config import CONFIG
from src.classes.actions import ACTION_INFOS_STR
@@ -70,7 +70,7 @@ class LLMAI(AI):
"global_info": global_info,
"general_action_infos": general_action_infos,
}
res = await get_ai_prompt_and_call_llm_async(info)
res = await call_ai_action(info)
results: dict[Avatar, tuple[ACTION_NAME_PARAMS_PAIRS, str, str]] = {}
for avatar in avatars_to_decide:
r = res[avatar.name]

View File

@@ -365,7 +365,7 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
result: ActionResult = action.step(**params_for_step)
if result.status == ActionStatus.COMPLETED:
params_for_finish = filter_kwargs_for_callable(action.finish, params)
finish_events = action.finish(**params_for_finish)
finish_events = await action.finish(**params_for_finish)
# 仅当当前动作仍然是刚才执行的那个实例时才清空
# 若在 step() 内部通过"抢占"机制切换了动作(如 Escape 失败立即切到 Battle不要清空新动作
if self.current_action is action_instance_before:

View File

@@ -478,7 +478,7 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
base_event = Event(month_at_finish, event_text, related_avatars=related_avatars, is_major=True)
# 生成故事事件
story = await StoryTeller.tell_story_async(event_text, res_text, *actors_for_story, prompt=story_prompt)
story = await StoryTeller.tell_story(event_text, res_text, *actors_for_story, prompt=story_prompt)
story_event = Event(month_at_finish, story, related_avatars=related_avatars, is_story=True)
# 返回基础事件和故事事件

View File

@@ -12,7 +12,7 @@ if TYPE_CHECKING:
from src.classes.event import Event
from src.utils.config import CONFIG
from src.utils.llm import get_prompt_and_call_llm_async
from src.utils.llm import call_llm_with_template, LLMMode
from src.run.log import get_logger
logger = get_logger().logger
@@ -91,7 +91,7 @@ async def generate_long_term_objective(avatar: "Avatar") -> Optional[LongTermObj
}
# 调用LLM并自动解析JSON使用fast模型
response_data = await get_prompt_and_call_llm_async(template_path, infos, mode="fast")
response_data = await call_llm_with_template(template_path, infos, LLMMode.FAST)
content = response_data.get("long_term_objective", "").strip()

View File

@@ -96,7 +96,7 @@ class DualCultivation(MutualAction):
initiator.cultivation_progress.add_exp(exp_gain)
self._dual_exp_gain = exp_gain
def finish(self, target_avatar: "Avatar|str") -> list[Event]:
async def finish(self, target_avatar: "Avatar|str") -> list[Event]:
target = self._get_target_avatar(target_avatar)
events: list[Event] = []
success = self._dual_cultivation_success
@@ -111,7 +111,7 @@ class DualCultivation(MutualAction):
# 生成恋爱/双修小故事
start_text = self._start_event_content or result_event.content
story = StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT)
story = await StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT)
story_event = Event(self.world.month_stamp, story, related_avatars=[self.avatar.id, target.id], is_story=True)
events.append(story_event)
else:

View File

@@ -79,7 +79,7 @@ class GiftSpiritStone(MutualAction):
# 目标获得灵石
target.magic_stone += self.GIFT_AMOUNT
def finish(self, target_avatar: "Avatar|str") -> list[Event]:
async def finish(self, target_avatar: "Avatar|str") -> list[Event]:
target = self._get_target_avatar(target_avatar)
events: list[Event] = []
success = self._gift_success
@@ -98,7 +98,7 @@ class GiftSpiritStone(MutualAction):
# 生成赠送小故事
from src.classes.story_teller import StoryTeller
start_text = self._start_event_content or result_event.content
story = StoryTeller.tell_story(
story = await StoryTeller.tell_story(
start_text,
result_text,
self.avatar,

View File

@@ -90,7 +90,7 @@ class Impart(MutualAction):
target.cultivation_progress.add_exp(exp_gain)
self._impart_exp_gain = exp_gain
def finish(self, target_avatar: "Avatar|str") -> list[Event]:
async def finish(self, target_avatar: "Avatar|str") -> list[Event]:
target = self._get_target_avatar(target_avatar)
events: list[Event] = []
success = self._impart_success
@@ -110,7 +110,7 @@ class Impart(MutualAction):
# 生成师徒传道小故事
from src.classes.story_teller import StoryTeller
start_text = self._start_event_content or result_event.content
story = StoryTeller.tell_story(
story = await StoryTeller.tell_story(
start_text,
result_text,
self.avatar,

View File

@@ -7,7 +7,7 @@ import asyncio
from src.classes.action.action import DefineAction, ActualActionMixin, LLMAction
from src.classes.tile import get_avatar_distance
from src.classes.event import Event
from src.utils.llm import get_prompt_and_call_llm, get_prompt_and_call_llm_async
from src.utils.llm import call_llm_with_template, LLMMode
from src.utils.config import CONFIG
from src.classes.relation import relation_display_names, Relation
from src.classes.relations import get_possible_new_relations
@@ -81,17 +81,10 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
"feedback_actions": feedback_actions,
}
def _call_llm_feedback(self, infos: dict) -> dict:
"""
兼容保留:同步调用(不在事件循环内使用)。
"""
async def _call_llm_feedback(self, infos: dict) -> dict:
"""异步调用 LLM 获取反馈"""
template_path = self._get_template_path()
res = get_prompt_and_call_llm(template_path, infos, mode="fast")
return res
async def _call_llm_feedback_async(self, infos: dict) -> dict:
template_path = self._get_template_path()
return await get_prompt_and_call_llm_async(template_path, infos, mode="fast")
return await call_llm_with_template(template_path, infos, LLMMode.FAST)
def _set_target_immediate_action(self, target_avatar: "Avatar", action_name: str, action_params: dict) -> None:
"""
@@ -132,16 +125,14 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
return self.find_avatar_by_name(target_avatar)
return target_avatar
def _execute(self, target_avatar: "Avatar|str") -> None:
"""
保留同步实现(不在事件循环内使用)。
"""
async def _execute(self, target_avatar: "Avatar|str") -> None:
"""异步执行互动动作"""
target_avatar = self._get_target_avatar(target_avatar)
if target_avatar is None:
return
infos = self._build_prompt_infos(target_avatar)
res = self._call_llm_feedback(infos)
res = await self._call_llm_feedback(infos)
r = res.get(infos["avatar_name_2"], {})
thinking = r.get("thinking", "")
feedback = r.get("feedback", "")
@@ -209,12 +200,8 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
# 若无任务,创建异步任务
if self._feedback_task is None and self._feedback_cached is None:
infos = self._build_prompt_infos(target)
try:
loop = asyncio.get_running_loop()
self._feedback_task = loop.create_task(self._call_llm_feedback_async(infos))
except RuntimeError:
# 无运行中的事件循环时,退化为同步调用(如离线批处理)
self._feedback_cached = self._call_llm_feedback(infos)
loop = asyncio.get_running_loop()
self._feedback_task = loop.create_task(self._call_llm_feedback(infos))
# 若任务已完成,消费结果
if self._feedback_task is not None and self._feedback_task.done():
@@ -238,7 +225,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
return ActionResult(status=ActionStatus.RUNNING, events=[])
def finish(self, target_avatar: "Avatar|str") -> list[Event]:
async def finish(self, target_avatar: "Avatar|str") -> list[Event]:
"""
完成互动动作,事件已在 step 中处理,无需额外事件
"""

View File

@@ -10,7 +10,7 @@ if TYPE_CHECKING:
from src.classes.event import Event
from src.utils.config import CONFIG
from src.utils.llm import get_prompt_and_call_llm_async
from src.utils.llm import call_llm_with_template, LLMMode
from src.run.log import get_logger
logger = get_logger().logger
@@ -73,7 +73,7 @@ async def generate_nickname(avatar: "Avatar") -> Optional[str]:
}
# 调用LLM并自动解析JSON
response_data = await get_prompt_and_call_llm_async(template_path, infos, mode="fast")
response_data = await call_llm_with_template(template_path, infos, LLMMode.FAST)
nickname = response_data.get("nickname", "").strip()
thinking = response_data.get("thinking", "")

View File

@@ -3,8 +3,11 @@ from __future__ import annotations
from typing import Dict, TYPE_CHECKING
import random
if TYPE_CHECKING:
from src.classes.avatar import Avatar
from src.utils.config import CONFIG
from src.utils.llm import get_prompt_and_call_llm, get_prompt_and_call_llm_async
from src.utils.llm import call_llm_with_template, LLMMode
story_styles = [
"平淡叙述:语句克制、少修饰、像旁观者记录。",
@@ -67,32 +70,7 @@ class StoryTeller:
return f"{event}{res}{style}"
@staticmethod
def tell_story(event: str, res: str, *actors: "Avatar", prompt: str = "") -> str:
"""
生成小故事(同步版本)。
基于 `static/templates/story.txt` 模板,失败时返回降级文案。
Args:
event: 事件描述
res: 结果描述
*actors: 参与的角色1-2个
prompt: 可选的故事提示词
"""
avatar_infos = StoryTeller._build_avatar_infos(*actors)
infos = StoryTeller._build_template_data(event, res, avatar_infos, prompt)
try:
data = get_prompt_and_call_llm(StoryTeller.TEMPLATE_PATH, infos, mode="fast")
story = data.get("story", "").strip()
if story:
return story
except Exception:
pass
return StoryTeller._make_fallback_story(event, res, infos["style"])
@staticmethod
async def tell_story_async(event: str, res: str, *actors: "Avatar", prompt: str = "") -> str:
async def tell_story(event: str, res: str, *actors: "Avatar", prompt: str = "") -> str:
"""
生成小故事(异步版本)。
基于 `static/templates/story.txt` 模板,失败时返回降级文案。
@@ -107,8 +85,8 @@ class StoryTeller:
infos = StoryTeller._build_template_data(event, res, avatar_infos, prompt)
try:
data = await get_prompt_and_call_llm_async(StoryTeller.TEMPLATE_PATH, infos, mode="fast")
story = str(data.get("story", "")).strip()
data = await call_llm_with_template(StoryTeller.TEMPLATE_PATH, infos, LLMMode.FAST)
story = data.get("story", "").strip()
if story:
return story
except Exception:
@@ -116,4 +94,5 @@ class StoryTeller:
return StoryTeller._make_fallback_story(event, res, infos["style"])
__all__ = ["StoryTeller"]