基于AI Agent与LLM的交互式叙事系统构建实践
如果你是一名《斗罗大陆》系列的资深读者或者是一位对同人创作、小说设定分析感兴趣的技术爱好者那么你很可能遇到过这样的困境一个绝佳的灵感在脑海中迸发——比如“主角穿越到龙王传说时代系统却突然宕机只能凭借前世记忆和锻造天赋逆天改命”——但当你试图将这个复杂的、充满交互和逻辑的幻想世界构建成可互动的故事或游戏时却感到无从下手。传统的写作工具只能处理线性文本而游戏引擎又过于沉重。我们需要的是一个能够理解复杂设定、管理多线剧情、并能让角色“活”起来的智能叙事框架。这正是AI Agent智能体技术在内容创作领域大放异彩的舞台。本文将以一个高度具体的“斗三同人”设定为蓝本为你完整拆解如何利用现代AI与开发技术构建一个可交互、有记忆、能推理的智能故事世界。这不仅仅是一篇编程教程更是一次关于“如何将天马行空的创意工程化”的深度实践。你将学到的不只是几行代码而是从世界观导入、角色建模、到剧情推演和交互实现的完整方法论。1. 从灵感到系统我们要解决的核心问题让我们先明确目标。基于开头的设定我们想构建的系统需要解决以下几个核心痛点复杂状态管理主角的“先天六级精神力”、“锻造天赋”、“魔王魂技”NPC如唐舞麟、古月娜的状态以及“系统停机”这一特殊事件都需要被精准定义和追踪。角色行为模拟角色不能是提线木偶。唐舞麟遇到危机时会如何反应古月娜在未被“提前守护”的时间线里命运如何演变系统需要基于角色设定进行自主推理。剧情非线性推演故事不应只有一条主线。玩家的每个选择比如选择先提升锻造还是先修炼魂技都可能引发蝴蝶效应导致截然不同的分支剧情。自然语言交互用户应能用自然语言与故事世界互动例如下达指令“去锻造协会找震华拜师”或询问“我现在的精神力能学习什么魂技”。传统的有限状态机FSM或行为树Behavior Tree对于如此庞大且开放的世界观会变得异常臃肿且难以维护。而基于大语言模型LLM的AI Agent框架凭借其强大的自然语言理解、上下文记忆和逻辑推理能力成为了更优雅的解决方案。我们的技术判断是利用LLM作为世界模型的“大脑”用结构化数据如JSON Schema定义实体和状态再以函数调用Function Calling的方式将LLM的决策转化为具体的游戏逻辑是构建此类复杂交互叙事系统的可行路径。下面我们就一步步将其实现。2. 核心概念与架构设计在开始编码前需要理解几个关键概念AI Agent智能体在此上下文中指一个能感知环境故事状态、进行决策剧情推演、并执行动作更新状态、生成文本的自治系统。我们的“故事世界”本身就是一个高级Agent。大语言模型LLM如GPT-4、Claude 3或开源的Llama 3作为系统的推理引擎。它负责理解输入、分析当前状态、并决定下一步发生什么。函数调用Function Calling/Tool Calling让LLM能够触发外部函数的关键机制。例如LLM可以决定“角色学习了新魂技”然后调用update_character_skill这个函数来实际修改数据。向量数据库Vector Database用于存储和检索庞大的背景知识如《斗罗大陆》原著设定、角色资料。当需要查询“昊天宗的历史”时可以快速找到相关段落提供给LLM作为上下文。系统架构图文字描述用户输入 | v [自然语言接口] - 解析用户意图 | v [故事状态管理器] (当前世界状态JSON) | v [LLM推理引擎] - [向量数据库]背景知识 | v [函数调用路由] - update_world_state() | generate_dialogue() | ... | v [响应生成器] - 文本输出 状态更新这个架构的核心是“LLM驱动状态转移”。LLM不直接修改数据而是通过调用我们预先定义好的、安全的函数来操作世界。3. 环境准备与工具选型我们将使用Python作为主要开发语言因为它拥有最丰富的AI生态库。基础环境Python 3.10包管理工具pip 或 poetry核心库安装我们将使用langchain框架来简化Agent的构建流程。# 创建项目目录并进入 mkdir douluo-story-agent cd douluo-story-agent # 创建虚拟环境推荐 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装核心依赖 pip install langchain langchain-openai langchain-community # 安装用于函数调用和状态管理的库 pip install pydantic # 安装向量数据库客户端以Chroma为例 pip install chromadbLLM服务选择你需要一个LLM的API密钥。本文以OpenAI GPT-4为例但你完全可以替换为其他兼容OpenAI API的模型如Azure OpenAI、Ollama本地模型。# 设置环境变量或在代码中直接配置 export OPENAI_API_KEYyour-api-key-here4. 第一步定义故事世界的“数据结构”任何系统的起点都是数据模型。我们使用Pydantic来定义强类型的结构。# file: models.py from enum import Enum from typing import List, Optional, Dict, Any from pydantic import BaseModel, Field class ElementType(str, Enum): 魂力属性 FIRE 火 WATER 水 WIND 风 EARTH 土 LIGHT 光 DARK 暗 SPACE 空间 TIME 时间 NONE 无 class Character(BaseModel): 角色模型 id: str Field(..., description角色唯一标识) name: str Field(..., description角色姓名) age: int Field(..., description年龄) spirit_power_level: int Field(default0, description魂力等级) spirit_rings: List[int] Field(default_factorylist, description魂环年份列表如[100, 500]) element: ElementType Field(defaultElementType.NONE, description武魂元素属性) skills: List[str] Field(default_factorylist, description掌握的魂技名称列表) relationships: Dict[str, str] Field(default_factorydict, description与其他角色的关系键为角色ID值为关系描述如‘好友’、‘师徒’) background: str Field(default, description角色背景故事) current_location: str Field(default未知, description当前所在位置) class WorldState(BaseModel): 世界状态模型 timeline: str Field(default龙王传说初期, description当前时间线) protagonist_id: str Field(..., description主角ID) characters: Dict[str, Character] Field(default_factorydict, description所有角色字典键为角色ID) events: List[str] Field(default_factorylist, description已发生的关键事件记录) system_online: bool Field(defaultTrue, description‘系统’是否运行) # 可以添加更多全局状态如“史莱克学院声望”、“传灵塔关系”等 global_variables: Dict[str, Any] Field(default_factorydict) class Action(BaseModel): 动作模型用于函数调用 name: str Field(..., description动作名称如‘move’, ‘learn_skill’, ‘dialogue’) parameters: Dict[str, Any] Field(default_factorydict, description动作参数)这个模型定义了我们故事世界的骨架。接下来我们初始化我们的主角和世界。5. 第二步初始化世界观与主角根据输入设定创建初始的世界状态。# file: initialize_world.py from models import WorldState, Character, ElementType def create_initial_world() - WorldState: 创建初始的斗罗世界状态 # 1. 创建主角 protagonist Character( idprotagonist_1, name林风, # 假设主角名 age12, spirit_power_level10, # 先天满魂力根据设定调整 spirit_rings[], # 刚开始还没有魂环 elementElementType.NONE, # 可设定为特殊元素如SPACE skills[基础锻造术, 精神力感知六级], background穿越者前世为顶级工匠携带部分记忆。系统在穿越后意外停机。, current_location傲来城初级魂师学院 ) # 2. 创建关键NPC tang_wulin Character( idtang_wulin, name唐舞麟, age12, spirit_power_level11, spirit_rings[400], # 假设初期获得一个四百年魂环 elementElementType.NONE, # 蓝银草这里简化 skills[蓝银缠绕, 控鹤擒龙], current_location傲来城初级魂师学院 ) gu_yue Character( idgu_yue, name古月娜, age12, spirit_power_level29, # 银龙王化身实力隐藏 spirit_rings[1000, 1000], # 隐藏的千年魂环 elementElementType.SPACE, skills[元素掌控, 空间穿梭], current_location传灵塔分部附近 ) xie_xie Character( idxie_xie, name谢邂, age12, spirit_power_level12, spirit_rings[300], elementElementType.WIND, skills[光龙刃, 影龙刃], current_location傲来城初级魂师学院 ) # 3. 建立初始关系 protagonist.relationships { tang_wulin: 同班同学, xie_xie: 同班同学, gu_yue: 关注对象 # 目标是“提前守护” } tang_wulin.relationships {protagonist_1: 同班同学} xie_xie.relationships {protagonist_1: 同班同学} # 4. 构建世界 world_state WorldState( timeline龙王传说元年史莱克学院招生前, protagonist_idprotagonist_1, characters{ protagonist.id: protagonist, tang_wulin.id: tang_wulin, gu_yue.id: gu_yue, xie_xie.id: xie_xie, }, events[ 主角林风穿越至斗罗大陆系统遭遇未知错误停机。, 林风觉醒先天六级精神力展现出惊人的锻造天赋。, 林风、唐舞麟、谢邂进入傲来城初级魂师学院同一班级。 ], system_onlineFalse, # 系统停机是关键设定 global_variables{ zhenhua_met: False, # 是否已遇见神匠震华 na_guard_started: False, # 是否开始守护古月娜 academy_entrance_exam_coming: True } ) return world_state if __name__ __main__: world create_initial_world() print(f世界初始化完成。时间线{world.timeline}) print(f主角{world.characters[world.protagonist_id].name}) print(f系统状态{‘运行中’ if world.system_online else ‘已停机’})运行这个脚本你就拥有了一个结构化的、可编程的“斗罗世界”初始状态。6. 第三步构建世界的“规则引擎”——函数调用这是最核心的部分。我们需要定义一系列函数让LLM能够通过这些函数来改变世界。# file: world_functions.py from models import WorldState, Character from typing import Dict, Any, Tuple import json class WorldFunctions: def __init__(self, world_state: WorldState): self.world_state world_state def move_character(self, character_id: str, destination: str) - Tuple[str, bool]: 移动角色到指定地点 if character_id not in self.world_state.characters: return f错误角色 {character_id} 不存在。, False old_loc self.world_state.characters[character_id].current_location self.world_state.characters[character_id].current_location destination self.world_state.events.append(f{self.world_state.characters[character_id].name} 从 {old_loc} 移动到了 {destination}。) return f{self.world_state.characters[character_id].name} 已到达 {destination}。, True def learn_skill(self, character_id: str, skill_name: str) - Tuple[str, bool]: 角色学习新魂技 if character_id not in self.world_state.characters: return f错误角色 {character_id} 不存在。, False char self.world_state.characters[character_id] if skill_name in char.skills: return f{char.name} 已经掌握了 {skill_name}。, False # 这里可以添加学习条件判断例如魂力等级、前置技能等 char.skills.append(skill_name) self.world_state.events.append(f{char.name} 领悟了新的魂技{skill_name}。) return f{char.name} 成功学会了 {skill_name}, True def update_relationship(self, char_a_id: str, char_b_id: str, relation: str) - Tuple[str, bool]: 更新两个角色之间的关系 if char_a_id not in self.world_state.characters or char_b_id not in self.world_state.characters: return f错误角色不存在。, False self.world_state.characters[char_a_id].relationships[char_b_id] relation self.world_state.characters[char_b_id].relationships[char_a_id] relation # 假设关系是对称的 self.world_state.events.append( f{self.world_state.characters[char_a_id].name} 与 {self.world_state.characters[char_b_id].name} 的关系变为{relation}。 ) return f关系已更新。, True def trigger_event(self, event_description: str) - Tuple[str, bool]: 触发一个全局事件 self.world_state.events.append(event_description) return f事件已记录{event_description}, True def get_world_snapshot(self) - Dict[str, Any]: 获取当前世界的JSON快照用于提供给LLM # 使用Pydantic的dict方法确保序列化 snapshot self.world_state.dict() # 可以过滤掉一些过于详细或内部的信息 return snapshot # 更多函数combat战斗、craft锻造、conversation对话生成等 # def start_combat(self, attacker_id: str, defender_id: str): # ... # def craft_item(self, crafter_id: str, item_blueprint: str): # ... # 将这些函数包装成LangChain Tool的格式 from langchain.tools import StructuredTool from pydantic import BaseModel, Field class MoveInput(BaseModel): character_id: str Field(description要移动的角色ID) destination: str Field(description目的地) class LearnSkillInput(BaseModel): character_id: str Field(description要学习技能的角色ID) skill_name: str Field(description要学习的魂技名称) def create_world_tools(world_functions: WorldFunctions): 创建LangChain可用的Tool列表 move_tool StructuredTool.from_function( funcworld_functions.move_character, namemove_character, description将一个角色移动到新的地点。, args_schemaMoveInput ) learn_skill_tool StructuredTool.from_function( funcworld_functions.learn_skill, namelearn_skill, description让一个角色学习一个新的魂技。, args_schemaLearnSkillInput ) # ... 为其他函数创建Tool return [move_tool, learn_skill_tool] # 返回Tool列表这些函数就是LLM能够操纵世界的“手”。接下来我们让LLM学会使用这些“手”。7. 第四步创建AI Agent——世界的“大脑”现在我们将LLM、世界状态和工具连接起来形成完整的Agent。# file: story_agent.py from langchain.agents import AgentExecutor, create_openai_tools_agent from langchain_openai import ChatOpenAI from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain.memory import ConversationBufferMemory from initialize_world import create_initial_world from world_functions import WorldFunctions, create_world_tools import json class DouluoStoryAgent: def __init__(self, model_namegpt-4-turbo-preview): # 1. 初始化世界 self.world_state create_initial_world() self.world_funcs WorldFunctions(self.world_state) # 2. 初始化LLM self.llm ChatOpenAI(modelmodel_name, temperature0.7) # temperature可调越高创造性越强但可能偏离设定。 # 3. 创建工具 self.tools create_world_tools(self.world_funcs) # 4. 构建系统提示词这是Agent的“人格”和“目标” system_prompt f你是一个《斗罗大陆III龙王传说》同人世界的智能叙事引擎。 当前世界状态摘要 - 时间线{self.world_state.timeline} - 主角{self.world_state.characters[self.world_state.protagonist_id].name}ID: {self.world_state.protagonist_id} - 关键事件{‘; ‘.join(self.world_state.events[-3:])} # 只显示最近3个事件 - 特殊设定穿越者系统已停机主角需依靠自身能力。 你的职责 1. 根据用户的输入可能是指令、问题或情节设想和当前世界状态推理出接下来最合理、最符合设定的情节发展。 2. 你必须通过调用我提供的工具函数来改变世界状态或者直接生成叙述性文本。 3. 严格遵循《斗罗大陆》的基础设定和角色性格。唐舞麟坚韧重情古月娜娜儿初期冷漠但内心善良。 4. 对于涉及战斗、修炼、锻造等专业内容需参考斗罗大陆的魂力、魂环、魂技体系。 5. 保持故事的趣味性和逻辑自洽。 用户可能是“玩家”控制主角或“导演”推动剧情。请根据用户的输入风格灵活应对。 每次响应请先简要说明你的推理然后执行动作或生成叙述。 prompt ChatPromptTemplate.from_messages([ (system, system_prompt), MessagesPlaceholder(variable_namechat_history), (human, {input}), MessagesPlaceholder(variable_nameagent_scratchpad), # 这是给Agent记录工具调用中间步骤的地方 ]) # 5. 创建记忆让Agent记住对话历史 memory ConversationBufferMemory(memory_keychat_history, return_messagesTrue, output_keyoutput) # 6. 创建Agent agent create_openai_tools_agent(self.llm, self.tools, prompt) self.agent_executor AgentExecutor( agentagent, toolsself.tools, memorymemory, verboseTrue, # 设为True可以看到详细的推理过程调试时非常有用 return_intermediate_stepsTrue, handle_parsing_errorsTrue # 处理解析错误 ) def run(self, user_input: str): 运行Agent处理用户输入 print(f\n[用户] {user_input}) try: # 在调用前将当前世界快照以某种方式告知LLM可以通过修改prompt动态注入 # 这里采用简单方式在输入前附加世界状态信息 enriched_input f当前世界状态快照{json.dumps(self.world_funcs.get_world_snapshot(), ensure_asciiFalse)[:1500]}...\n用户指令{user_input} result self.agent_executor.invoke({input: enriched_input}) print(f\n[世界响应] {result[output]}) # 可以在这里打印出世界状态的变化 print(f\n[最新事件] {self.world_state.events[-1] if self.world_state.events else 无}) return result[output] except Exception as e: print(fAgent执行出错{e}) return f系统处理出现了一些问题{e} # 一个简单的对话循环 if __name__ __main__: agent DouluoStoryAgent(model_namegpt-4) # 或 gpt-3.5-turbo print(斗罗大陆交互叙事系统启动系统状态停机) print(你可以像玩游戏一样输入指令例如‘林风去锻造师协会’或者‘我想学习一个防御魂技’。) print(输入 quit 退出。\n) while True: try: user_input input( ) if user_input.lower() in [quit, exit, q]: print(故事暂存期待下次继续。) break if user_input.strip(): agent.run(user_input) except KeyboardInterrupt: print(\n中断。) break现在一个具备基本推理和行动能力的“斗罗世界AI”就构建完成了。你可以运行它并尝试输入指令。8. 运行示例与效果验证让我们启动系统并进行一次简单的交互。运行程序python story_agent.py预期启动输出斗罗大陆交互叙事系统启动系统状态停机 你可以像玩游戏一样输入指令例如‘林风去锻造师协会’或者‘我想学习一个防御魂技’。 输入 quit 退出。用户输入示例1玩家指令 林风想去锻造师协会拜师提升锻造技艺。Agent可能的推理与输出简化版[世界响应] 推理主角林风拥有冠绝同龄的锻造天赋且当前位于傲来城。傲来城有锻造师协会分会这是接触高阶锻造师如未来神匠震华的合理起点。根据设定他应主动前往。 行动调用 move_character 工具将 protagonist_1 移动到“傲来城锻造师协会”。 叙述林风离开了学院心中对锻造的渴望驱使他走向城西那座标志性的锤形建筑——傲来城锻造师协会。这里汇聚了众多锻造师或许是他的机遇所在。 [最新事件] 林风 从 傲来城初级魂师学院 移动到了 傲来城锻造师协会。用户输入示例2导演模式 让唐舞麟在学院后山遇到一只受伤的百年魂兽陷入苦战。Agent可能的推理与输出[世界响应] 推理唐舞麟性格善良且富有责任感遇到受伤魂兽大概率会尝试帮助。这可以作为一个触发事件可能影响其魂环获取或与主角的关系。需要更新他的位置并触发事件。 行动调用 move_character 工具将 tang_wulin 移动到“学院后山”。调用 trigger_event 工具记录事件。 叙述放学后唐舞麟独自前往学院后山修炼却在一片灌木丛中发现了一只腿部受伤、气息萎靡的百年风狼。风狼龇牙低吼但行动不便一场遭遇战似乎不可避免。 [最新事件] 唐舞麟在学院后山遭遇受伤的百年风狼陷入对峙。通过这样的交互故事就在你的引导和AI的推理下动态推进了。你可以随时查询状态例如输入“林风现在有什么技能”。9. 常见问题与排查思路在构建和运行此类AI叙事系统时你可能会遇到以下问题问题现象可能原因排查方式解决方案Agent无法理解指令或调用无关工具。1. 系统提示词system_prompt不够清晰未明确Agent的职责和边界。2. 提供给LLM的世界状态信息过于冗长或杂乱。3. LLM的temperature参数过高导致过度发散。1. 检查verboseTrue时的输出看LLM在收到输入后的“思考”过程。2. 精简世界状态快照只提供最关键信息如主角位置、最近事件。3. 将temperature调低如0.3。1. 细化提示词用更明确的规则约束AI例如“你必须先判断用户意图是移动、查询还是触发事件”。2. 设计一个“状态摘要”函数而非传递全部JSON。3. 使用更强大的模型如GPT-4。函数调用参数错误如角色ID不存在。1. LLM“幻想”出了不存在的角色或地点。2. 工具的描述不够精确。1. 在工具函数内部加强参数校验并返回明确的错误信息。2. 在调用工具前让LLM先通过一个“查询”工具确认实体是否存在。1. 在系统提示词中明确列出所有现有角色ID和地点。2. 为工具的参数schema添加更详细的描述和示例。故事逻辑偏离原著设定战力崩坏。LLM缺乏足够的领域知识或提示词约束力不足。检查事件记录看是否出现了违背核心设定的内容如角色过早获得十万年魂环。1. 引入向量数据库将《斗罗大陆》原著关键设定、人物传记作为知识库嵌入在推理前先检索相关片段注入上下文。2. 在trigger_event等关键函数中加入逻辑校验规则。对话历史过长导致API令牌超限或性能下降。ConversationBufferMemory 无限制保存历史。监控每次请求的token消耗。1. 使用ConversationSummaryMemory或ConversationBufferWindowMemory来限制历史长度。2. 定期将长对话总结后存入“长期记忆”另一个向量库。运行速度慢。1. 模型响应慢如GPT-4。2. 工具调用链过长。使用时间戳记录各阶段耗时。1. 对于简单指令可降级使用GPT-3.5-Turbo。2. 优化工具设计减少不必要的调用。考虑将多个关联操作合并为一个“复合工具”。10. 进阶优化与工程实践要让这个系统从Demo走向可用还需要以下工程化优化1. 知识增强RAG将斗罗大陆的详细设定、魂技大全、地图资料存入向量数据库如Chroma。在每次推理前先根据用户输入和当前状态检索相关背景知识并附加到提示词中。这能极大提升故事的真实性和一致性。# 示例使用Chroma存储和检索知识 from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.document_loaders import TextLoader # 加载设定文本 loader TextLoader(douluo_setting.txt) documents loader.load() text_splitter RecursiveCharacterTextSplitter(chunk_size500, chunk_overlap50) texts text_splitter.split_documents(documents) # 创建向量库 vectorstore Chroma.from_documents(documentstexts, embeddingOpenAIEmbeddings()) # 在Agent推理前检索 retriever vectorstore.as_retriever() relevant_docs retriever.get_relevant_documents(昊天宗绝学)2. 分层状态管理当前世界状态是一个全局对象。在复杂交互中可以将其分为会话状态当前对话的临时上下文。剧情状态已发生的不可逆事件、角色等级、物品归属。世界规则物理法则、魂力成长公式、战斗计算公式。 将状态持久化到数据库如SQLite/PostgreSQL支持存档/读档。3. 更丰富的工具集实现完整的战斗模拟、锻造系统、经济系统等。def simulate_combat(attacker_id: str, defender_id: str, move: str) - Dict: 基于属性模拟战斗回合 # 读取双方角色属性 # 根据魂技、魂环、等级计算伤害 # 更新角色状态血量、魂力 # 返回战斗日志和结果4. 前端交互界面使用Gradio或Streamlit快速构建一个Web界面让交互更直观可以显示角色属性、地图、事件日志等。# 使用Gradio的简单示例 import gradio as gr agent DouluoStoryAgent() def respond(message, history): response agent.run(message) return response gr.ChatInterface(respond, title斗罗大陆交互叙事).launch()11. 总结从设定到可运行世界的完整路径通过以上步骤我们完成了一次将文字设定转化为动态智能系统的实践。其核心路径可以总结为设定结构化将模糊的灵感穿越、系统、天赋转化为精确的数据模型Pydantic Classes。逻辑工具化将可能发生的剧情操作移动、学习、交互封装成可调用的函数Tools。推理Agent化利用大语言模型作为中枢理解意图并选择合适工具执行。知识外部化将庞大的背景设定卸载到向量数据库通过检索增强生成RAG保证一致性。状态持久化管理好世界状态的变化这是故事得以延续的根基。这个项目的价值远不止于一个“斗罗同人模拟器”。它提供了一套将复杂、开放的叙事需求工程化实现的方法论。你可以将这套框架应用于游戏原型设计快速验证玩法和剧情分支。互动小说创作为读者提供深度参与感。角色扮演AI助手构建更智能、更有记忆的虚拟角色。起点可以是一个简单的想法而终点是一个由代码驱动、充满无限可能的鲜活世界。现在你可以基于这个框架去完善你的锻造系统设计“魔王魂技”的独特效果甚至模拟“史莱克七怪”的团队羁绊了。真正的故事现在才刚刚开始。