AI世界构建实战:让Galgame角色拥有记忆与自主行为

AI世界构建实战:让Galgame角色拥有记忆与自主行为 你是不是也有过这种感觉玩 Galgame 的时候最上头的不是选项本身而是“如果这个角色真的有想法他不按剧本走会怎样”。过去这个念头只能靠脑补现在不一样了。把大模型接进游戏世界让每个角色拥有自己的记忆、目标、行动力和对话能力一个“会自己运转的 AI 世界”是可以真实跑起来的。这篇文章会分享我实现这类 AI 世界项目的完整思路从架构设计、Agent 建模、记忆系统、世界循环调度到如何让玩家像 Galgame 一样介入这个世界以及实际运行中踩过哪些坑。内容偏工程实践代码部分以 Python 示例为主足够你自己复刻一个最小版本。1. 这篇文章真正要解决的问题先给一个明确判断做一个“会自己运转的 AI 世界”难点不在大模型 API 调用而在工程化。你只要调过几分钟对话接口就知道让单个 LLM 角色聊天一点都不难。难的是让多个 AI Agent 生活在同一个世界里各自有目标、有记忆、会规划、会行动并且这一切可以持续运转几个小时甚至几天不发生状态错乱、角色失忆、逻辑循环。Galgame 的核心体验是“代入感”玩家希望通过选择和对话影响角色命运走进不同的故事线。传统 Galgame 用分支剧情实现这一点代价是文本量爆炸而且玩家的自由度始终被脚本限制。AI 世界的思路完全不同它不写死分支而是建立一个角色模拟系统让 NPC 的行为天然具备多样性。玩家不再从预设选项里挑一个而是真正介入角色的生活。本文适合以下读者阅读想让 AI Agent 拥有持续记忆和自主行为而不是一问一答的开发者。对 AI 游戏、互动叙事、虚拟世界感兴趣想了解背后技术栈的人。有 AI 应用开发经验想尝试“多个 Agent 协同 世界状态维护”工程的工程师。读完这篇文章你会得到一个可运行的 AI 世界项目框架理解它和普通 AI Chatbot 的本质区别也能避开我在实际开发中踩过的主要坑。2. AI 世界与传统 NPC 或普通 Chatbot 的本质区别很多人第一次听到“AI 世界”第一反应是“不就是一个带人设的聊天机器人吗”这是最容易产生的误解。先做一组对比对比维度传统 Game NPC普通 ChatbotAI 世界 Agent行为来源脚本和状态机单轮对话模型自主规划 环境感知 对话记忆能力无或极短单会话上下文持久化记忆 反思目标系统无只响应玩家无自身目标和每日计划环境影响有限无感知其他角色和环境变化玩家介入走分支选项直接对话对话 观察 影响决策AI 世界和传统 NPC 的本质区别在于“自主性”和“惯性”。“自主性”是指角色不需要玩家发消息也会自己行动。早上起来TA 会根据自己的性格、近期目标、和其他角色的关系决定今天要做什么。这需要一套 Agent 调度循环感知环境 → 更新记忆 → 规划行动 → 执行行动 → 记录结果。“惯性”是指世界不会因为玩家下线就停止。你在 Galgame 里存档退出故事就冻结了。但在 AI 世界里角色们会继续生活、聊天、产生新记忆等你下次打开游戏时世界已经前进了一大截。这种体验非常奇妙但也对系统的稳定性提出了很高要求。还有一个关键点是“一致性”。Galgame 的角色之所以让人喜欢是因为每个角色有稳定的人格。AI 世界最容易出问题的地方恰恰是模型生成时的不确定性导致角色言行前后矛盾。上一秒还很温柔下一秒就变得暴躁这就破坏了代入感。所以AI 世界项目除了实现 Agent 行为循环还必须设计人格约束系统。一句话总结AI 世界不是 Chatbot 的集合而是一个由多个自主 Agent 构成、有记忆、有时间、有环境反馈的持续模拟系统。3. 系统架构设计一个最小可运行的 AI 世界3.1 架构总览整个系统我拆成了五层每一层各司其职世界状态层保存环境信息、角色位置、当前时间。Agent 层角色的人格、记忆、目标和行为逻辑。调度层驱动世界时间前进决定哪个角色在什么时刻行动。模型接入层统一封装大模型 API处理提示词组装和响应解析。交互层玩家查看世界状态、与角色对话、介入角色行为。这个分层的好处是每一层都可以独立替换。比如你接入了新的 LLM只需要改模型接入层你想把文字渲染改成可视化界面只动交互层。3.2 核心流程世界循环AI 世界的核心是一个“心跳循环”。我的设计是这样的世界时间推进一个最小单位比如 10 分钟。对每个 Agent 计算当前状态是否空闲、是否有行动。让 Agent 决定下一步行动继续当前任务、发起对话、移动、或思考。执行行动并更新世界状态。将行动结果写入 Agent 记忆。相比实时让每个 Agent 调用 LLM 决策这个设计更可控也更省成本。实际项目中每次“世界心跳”所有角色全部参与是不划算的因为大部分时刻角色只是在做不重要的事。后续可以增加“重点 Agent”机制只对玩家附近、或正在发生关键事件的角色做细致推理其余角色用规则或更小模型代替。4. 环境准备与技术选型4.1 技术栈清单为了降低复刻难度我没有使用重量级框架而是选了以下技术栈Python 3.9主语言生态丰富。FastAPI提供 HTTP 接口方便玩家与角色交互。SQLite用于持久化世界状态和角色属性。向量数据库可以选择轻量的chromadb也可以用 SQLite 向量扩展保存语义向量做记忆检索。LLM API兼容 OpenAI 格式的模型服务即可本地部署可基于 vLLM 或 Ollama 封装。Pydantic用于定义 Agent 状态和各层数据结构。这里是合理的技术选型。在实际项目中如果团队已有基础设施也可以替换为其他数据库和模型服务架构不受影响。4.2 项目目录结构一个清晰的目录结构能省下很多 debug 时间。我推荐这样组织ai_world/ ├── main.py # 入口启动世界服务和交互接口 ├── config.yaml # 世界配置、模型配置、Agent 数量 ├── core/ │ ├── world.py # 世界状态管理和时间推进 │ ├── scheduler.py # 行动调度器 │ ├── memory.py # 记忆存储与检索 │ └── llm.py # 模型接入封装 ├── agents/ │ ├── base.py # Agent 基类 │ ├── traits.py # 人格/性格定义 │ └── actions.py # 行动动作定义 ├── data/ │ ├── world_state.json # 世界初始状态 │ └── agents.json # 角色初始配置 └── play.py # Galgame 模式的玩家交互脚本先不要急着写很多类把最小流程跑通再逐步扩展。5. 核心代码实现下面进入本文的核心部分。我会带你实现一个最小可运行的 AI 世界包括 Agent 建模、记忆系统、世界循环、玩家介入四个模块。5.1 Agent 建模Agent 是一个有性格、有记忆、有目标的对象。我用 Pydantic 定义数据结构保证状态可序列化和持久化。# 文件路径agents/base.py from typing import List, Optional from datetime import datetime from pydantic import BaseModel, Field class MemoryItem(BaseModel): 一条记忆 content: str timestamp: datetime importance: int Field(default3, ge1, le5) class AgentState(BaseModel): Agent 的完整状态 agent_id: str name: str personality: str 温柔且好奇 background: str 小镇图书馆管理员 goals: List[str] [] position: str library status: str idle # idle / acting / talking / sleeping energy: int Field(default100, ge0, le100) short_term_memory: List[MemoryItem] [] long_term_memory: List[MemoryItem] []关键点personality和background决定角色回应风格对话时放进系统提示词里。status表示角色当前状态调度器会避免让正在对话的角色同时干别的事。short_term_memory和long_term_memory分开存储方便管理。goals是角色自驱行为的来源。5.2 记忆系统让角色“记得”而不是“失忆”没有记忆的 Agent 只是临时演员。记忆系统的核心有两个能力存入和检索。我设计了一个极简记忆类支持将记忆向量化并保存到向量数据库再按“与当前情景的相关性”检索。为了让示例可运行这里用一个简化版本但保留了真实项目中的接口形态。具体代码如下# 文件路径core/memory.py from typing import List from datetime import datetime import chromadb from chromadb.utils import embedding_functions class MemoryStore: def __init__(self, collection_name: str world_memory): self.client chromadb.PersistentClient(path./data/chroma) self.collection self.client.get_or_create_collection( namecollection_name, embedding_functionembedding_functions.DefaultEmbeddingFunction() ) def add_memory(self, agent_id: str, content: str, importance: int 3): 存入一条记忆 self.collection.add( documents[content], ids[f{agent_id}_{datetime.now().timestamp()}], metadatas[{agent_id: agent_id, importance: importance}] ) def query_memory(self, agent_id: str, query: str, top_k: int 5) - List[str]: 检索与当前情况最相关的记忆 result self.collection.query( query_texts[query], n_resultstop_k, where{agent_id: agent_id} ) return result[documents][0] if result[documents] else []这里真正值得注意的一点是检索时一定要用where过滤agent_id。否则角色 A 可能“记得”角色 B 的私密记忆导致世界观崩坏。成本虽然只有一行过滤条件却直接决定沉浸感的下限。5.3 世界循环调度器让世界自己转起来世界循环是项目的大脑。它负责推进时间让每个 Agent 按顺序行动。设计逻辑是推进世界时间。选出需要行动的 Agent状态为 idle、energy 足够的角色。调用 LLM 让 Agent 做决策。把行动落到世界状态中。将结果写入记忆。下面是一个不依赖具体 LLM SDK 的实现示例模型接口由core/llm.py封装# 文件路径core/scheduler.py import asyncio from typing import List from agents.base import AgentState, MemoryItem from core.memory import MemoryStore from core.world import WorldState class WorldScheduler: def __init__(self, agents: List[AgentState], world: WorldState, memory: MemoryStore, llm_client): self.agents agents self.world world self.memory memory self.llm llm_client async def tick(self): 推进一个世界时间片 self.world.advance_time(minutes10) for agent in self.agents: if agent.status ! idle or agent.energy 20: continue # 1. 构建决策所需的上下文 context self._build_decision_context(agent) # 2. 调用 LLM 决定下一步行动 action await self.llm.decide_action(context) # 3. 执行行动并更新世界状态 await self._execute_action(agent, action) # 4. 记录记忆 self.memory.add_memory(agent.agent_id, f{agent.name} 决定{action}) agent.status idle async def run_forever(self): 持续运行世界循环 while True: await self.tick() await asyncio.sleep(1) def _build_decision_context(self, agent: AgentState) - str: 组装决策上下文人格 当前目标 近期记忆 环境 recent_memories self.memory.query_memory(agent.agent_id, agent.goals[0] if agent.goals else 日常行动) return f 你是{agent.name}{agent.personality}。背景{agent.background} 当前时间{self.world.current_time} 当前位置{agent.position} 当前能量{agent.energy} 目标{agent.goals} 近期记忆 {chr(10).join(recent_memories)} 请决定下一步行动例如前往图书馆、和某人交谈、休息、整理书籍等只需输出行动简述。 在执行_execute_action时需要针对不同行动类型做分支处理。比如“移动”会更新agent.position“交谈”会生成对话并写入世界事件流“休息”会提高agent.energy。5.4 Galgame 模式玩家如何介入世界“当 Galgame 玩”的核心是玩家可以查看角色状态、与角色对话并且自己的行为会影响世界。玩家对话不是普通 API 调用而是要带着角色的完整记忆和当前情境。我在 FastAPI 中实现一个交互接口# 文件路径main.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from agents.base import AgentState from core.memory import MemoryStore from core.llm import get_llm_client app FastAPI() memory MemoryStore() llm get_llm_client() agents: dict[str, AgentState] {} class TalkRequest(BaseModel): agent_id: str player_message: str app.post(/talk) async def talk_to_agent(req: TalkRequest): 玩家与指定 Agent 对话 agent agents.get(req.agent_id) if not agent: raise HTTPException(status_code404, detailAgent 不存在) # 检索该角色的相关记忆 memories memory.query_memory(agent.agent_id, req.player_message, top_k5) system_prompt f 你是{agent.name}{agent.personality}。背景{agent.background} 你在{agent.position}当前时间是{agent.status}。 你正在进行一场对话对方是一名访客。 你的近期相关记忆 {chr(10).join(memories)} 请以角色身份自然地回应玩家的消息不要提及你是 AI。 player_reply await llm.chat(system_prompt, req.player_message) # 这场对话需要被角色记住 memory.add_memory(agent.agent_id, f{agent.name}与访客交谈{req.player_message} - {player_reply}) return {agent: agent.name, reply: player_reply}注意和普通 Chatbot 最大的不同是这个接口每次都会把该角色的“长期记忆检索结果”注入提示词。这意味着角色在一周后仍然记得你和 TA 聊过什么并且会自然地引用之前聊的内容形成真正的关系推进感。6. 运行结果与效果验证6.1 启动世界配置好模型接入后用以下命令启动服务python main.py然后启动世界循环# 文件路径play.py import asyncio from core.scheduler import WorldScheduler from core.memory import MemoryStore from core.llm import get_llm_client from core.world import WorldState from agents.base import AgentState async def main(): world WorldState(initial_time08:00) memory MemoryStore() llm get_llm_client() agents [ AgentState(agent_ida1, name绫音, personality温柔但有点傲娇, goals[整理图书馆, 和青梅竹马聊天]), AgentState(agent_ida2, name悠真, personality开朗热心, goals[练习吉他, 遇到新朋友]), ] scheduler WorldScheduler(agents, world, memory, llm) await scheduler.run_forever() asyncio.run(main())6.2 预期输出发生关键事件时交互层会打印类似日志[世界时间 08:10] 绫音 决定整理图书馆书架 [世界时间 08:20] 悠真 决定去公园散步也许会遇到熟人 [世界时间 08:40] 绫音 和 悠真 在图书馆门口相遇绫音说“你今天来得真早。”日志中能看到角色开始“自发行动”而不是等待玩家输入这就是世界“活”起来的直接证据。6.3 怎么判断运行成功判断标准有四个角色之间会发生自然对话而不是每个人只自说自话。角色行动与人格一致傲娇角色不会主动说出过于直白的甜言蜜语。角色会调用记忆如果第一天 TA 遇见了某位访客第二天会想起来。世界推进后状态一致角色移动后位置信息被正确记录重新登录不会重置。如果上面任何一条不满足都说明系统还没达到“可玩”状态。7. 常见问题与排查方法这部分是把项目真正跑起来时最可能遇到的坑我按问题现象整理了排查表问题现象可能原因排查方式解决方案Agent 行为完全随机像精神分裂人格 prompt 没有生效或每次请求都截断了人格描述检查实际发送的 prompt 是否包含完整人格设定将人格描述固定写入 system prompt并设置较高温度参数区分场景角色之间不会互动调度器没有“发现附近角色”的机制查看行动日志确认角色是否处于同一位置增加“相邻检测”Agent 执行移动后检查同位置其他 Agent生成触发对话的事件记忆检索出无关内容向量检索只按语义相似度召回缺少时间衰减和重要性过滤打印检索出的记忆片段检查相关性增加 importance 字段排序结合时间衰减函数加权后再送入 LLM世界运行几小时后状态冲突或丢失世界状态没有持久化进程重启后丢失检查持久化逻辑是否已写入磁盘每次世界心跳后将 Agent 状态和世界状态写入 SQLite成本过高每个心跳都调用 LLM每个 Agent 每个时间片都推理请求量爆炸观察 API 调用量和响应耗时引入“优先级调度”只让正在进行关键行动的 Agent 调用 LLM其余角色用简单规则或预置动作LLM 响应格式不稳定角色行动决策结果是自然语言解析困难查看 LLM 返回的原始内容使用 JSON 输出模式要求模型输出{action: 移动, target: library}格式并做异常重试8. 最佳实践与工程建议8.1 架构与数据设计一是在真实的 AI 世界项目中建议把“生成决策”和“生成对话”拆成两次独立调用而不是合并成一次。前者对速度和成本敏感可以控制推理参数后者对质量和风格敏感使用不同的模型和温度。合并调用虽然省一次请求但会让决策质量下降而且后续无法单独优化。二是记忆系统是 AI 世界的胜负手。不要满足于简单的向量存储要按重要程度、时间衰减、角色关系三个维度加权检索。比如相处很久的老朋友说过的话权重应该高于路人偶然说过的一句话。三是世界状态必须持久化。开发中很容易因为断点重启导致角色记忆和世界状态对不上这特别影响体验。我建议每次世界心跳后对 Agent 状态、记忆索引、世界时间做一次原子写入。8.2 成本控制AI 世界项目的 API 成本比 Chatbot 高很多因为角色在后台也会持续消耗 Token。比较好的策略是分级调度高优先级玩家正在注视的角色、正在对话的角色走最强模型。中优先级进行关键行动的角色走标准模型。低优先级只是背景路过的角色走规则引擎或者很小的模型。这个设计能让运营成本下降 50% 以上同时玩家感知不到质量差异。8.3 安全与边界涉及 AI 角色模拟时要留意内容边界。角色可能做出不符合预期的行为需要在调度层加白名单动作系统只允许 Agent 执行预先定义的行动类型# 文件路径agents/actions.py ALLOWED_ACTIONS {移动, 对话, 休息, 思考, 工作, 阅读, 记录}如果 LLM 返回的行动不在白名单内自动降级为“思考”。这个约束既保证世界逻辑可维护也是内容安全的重要防线。对于涉及身份、权限或数据写入的接口刚才的 FastAPI 示例只是一个最小可用版本。在生产环境必须补充认证鉴权玩家只能操作自己的存档游戏世界不能访问或修改其他人的世界状态。8.4 可扩展性方向前端渲染文字日志改成 2D 地图显示角色移动或接 Galgame 立绘系统。多人共玩多个玩家共享同一个世界产生完全不同的社交玩法。角色关系图谱建立角色之间的关系值互动推进关系成长关系反过来影响对话风格。长期演化给世界添加经济系统、季节系统、随机事件让角色面临更多变化。9. 总结从构思到跑通这个“AI 世界”我最大的体感是AI 应用开发的真正门槛已经不再是模型能力而是“如何把模型放进一个会持续运转的系统中”。让角色记住昨天发生的事、按人格行动、不互相穿帮、不让玩家出戏每一步都是精细的工程问题。如果这篇文章对你有帮助建议收藏备用。你不需要一开始就做一个完整的世界可以先从两个角色、一个场景、一天时间模拟开始再逐步加入记忆、关系、事件系统。AI Agent 的世界一旦转起来你会看到很多超越脚本预设的意外剧情那才是这个方向最迷人的地方。