
在桌面角色扮演游戏TTRPG如《龙与地下城》中游戏主持人GM需要管理一个由玩家、非玩家角色、地点、任务和无数互动细节构成的庞大世界。随着战役推进数周甚至数月的游戏内容会形成海量的会话记录、角色对话和剧情线索。如何让 AI 助手例如基于大语言模型的 GM 副手准确记住并理解这些分散的、非结构化的信息而不是在每次提问时都依赖有限的上下文窗口进行“失忆式”回答是提升 AI 在 TTRPG 中实用性的核心挑战。Table Canon 正是为了解决这一问题而设计的“战役记忆引擎”。它不是一个游戏客户端也不是一个 AI 聊天前端而是一个专为 TTRPG 场景优化的记忆系统后端。其核心思想是模仿人类 GM 的长期记忆方式并非记住每一句原话而是提取关键实体人物、地点、组织和事实关系、事件、状态并将其组织成一张不断演化的知识图谱。当 AI 需要回答关于战役历史的问题时Table Canon 能够从这张图谱中检索出最相关的上下文片段动态地注入给大语言模型从而使 AI 的回答具备连贯性、准确性和深度显著减少“AI 幻觉”现象——即 AI 编造不存在的人物或事件。本文面向有一定技术背景的 TTRPG 爱好者、AI 应用开发者或是希望将大语言模型与复杂、长上下文领域结合的技术人员。我们将从零开始理解 Table Canon 的设计理念搭建其运行环境并通过一个简化的示例战役演示如何将杂乱的聊天记录转化为结构化的记忆并最终让一个 AI 助手基于这些记忆进行智能应答。你将了解到如何构建一个属于自己的、能够“记住”整个奇幻世界的小型 AI 大脑。1. 理解战役记忆引擎的核心机制从对话到知识图谱在深入代码之前必须厘清 Table Canon 要解决的根本问题以及其技术路径。传统上让 AI 记住长对话有两种朴素思路一是将全部历史记录作为上下文输入这很快会触及模型令牌Token数上限成本高昂且效率低下二是进行简单的文本摘要但摘要会丢失大量细节且难以支持对特定实体的深度查询。Table Canon 采用了更接近人类认知的“提取-索引-检索”三层架构。1.1 信息提取从非结构化文本中捕获实体与关系原始的游戏日志或聊天记录是非结构化的自然语言。第一步是使用大语言模型如 GPT-4、Claude 或本地模型作为“信息提取器”。我们不是让模型去理解整个故事而是让它执行一项更具体的任务识别文本中出现的命名实体Entity和陈述性事实Statement。实体包括人物PC玩家角色、NPC非玩家角色、地点城镇、 dungeon、组织公会、教派、物品传奇武器、魔法道具等。每个实体都有类型和名称。关系与事实描述实体之间的互动或状态。例如“艾莉丝 拜访了 银月城” 或 “国王 怀疑 暗影公会”。一个复杂事件可能由多个事实构成。这个过程通常通过精心设计的提示词Prompt引导大语言模型以结构化格式如 JSON输出。这步的输出是将流水账式的对话转化为了离散的、机器可读的知识单元。1.2 知识索引构建可查询的向量记忆库提取出的实体和事实是独立的。为了高效检索我们需要将它们“存储”起来。Table Canon 的核心是使用向量数据库如 Chroma、Weaviate、Pinecone。向量化每个实体或事实的文本描述例如“艾莉丝半精灵游侠来自北境森林”通过嵌入模型Embedding Model转换为一个高维向量。这个向量在数学空间中的位置编码了该段文本的语义信息。索引存储这些向量连同它们的原始文本和元数据如来源会话、时间戳被存入向量数据库。由此非结构化的文本知识被组织成了一个语义层面的“地图”。1.3 语义检索根据问题寻找相关记忆当用户或GM向AI助手提出一个问题时例如“艾莉丝和银月城的城主是什么关系”系统不会去直接“翻看”所有原始日志。查询向量化首先将这个问题本身也转化为一个向量。相似度搜索在向量数据库中进行相似度搜索通常使用余弦相似度寻找与问题向量最接近的那些实体或事实向量。这意味着即使问题中没有直接提到“拜访”这个词但因为语义相近存储了“艾莉丝拜访银月城”事实的向量也可能被检索出来。上下文组装检索出的Top K个相关记忆片段原始文本将与当前问题、以及可能的一些全局设定如世界观一起组装成一个新的、信息丰富的提示词提交给大语言模型生成最终答案。通过这个机制AI的“工作记忆”不再是完整的对话历史而是由当前问题动态激活的、最相关的那部分长期记忆。这极大地扩展了AI可处理的信息范围并提高了回答的准确性。2. 环境准备与项目结构搭建我们将基于一个假设的 Python 项目来模拟实现 Table Canon 的核心流程。虽然原项目可能采用其他技术栈但原理相通。这里我们使用 LangChain一个流行的LLM应用框架来简化与大语言模型和向量数据库的交互。2.1 开发环境与依赖确保你的开发环境已安装 Python 3.8。建议使用虚拟环境。# 创建并激活虚拟环境可选 python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows # 安装核心依赖 pip install langchain langchain-community langchain-openai pip install chromadb # 轻量级向量数据库用于演示 pip install tiktoken # 用于Token计数 pip install python-dotenv # 管理环境变量关键依赖说明langchain: 提供LLM应用开发的抽象层和链式调用。langchain-openai: 用于接入OpenAI的模型如GPT-3.5/4。如果你使用本地模型如通过Ollama则需要安装对应的集成包如langchain-ollama。chromadb: 一个开源的嵌入式向量数据库无需单独服务适合本地开发和演示。python-dotenv: 用于从.env文件加载API密钥等敏感信息。2.2 项目结构规划一个清晰的项目结构有助于管理代码、配置和数据。table_canon_demo/ ├── .env # 存储敏感配置如OPENAI_API_KEY ├── requirements.txt # 项目依赖列表 ├── config.py # 配置文件 ├── main.py # 主程序入口 ├── core/ # 核心逻辑模块 │ ├── __init__.py │ ├── extractor.py # 信息提取器 │ ├── memory_store.py # 记忆存储与检索 │ └── agent.py # AI助手代理 ├── data/ # 数据目录 │ ├── raw_logs/ # 存放原始战役日志.txt文件 │ └── chroma_db/ # Chroma向量数据库持久化目录 └── utils/ └── __init__.py2.3 配置文件与环境变量创建.env文件来管理密钥切勿提交至版本控制系统# .env OPENAI_API_KEYsk-your-openai-api-key-here # 如果使用其他模型例如 Anthropic Claude # ANTHROPIC_API_KEYyour-claude-key创建config.py来定义全局配置# config.py import os from dotenv import load_dotenv load_dotenv() # 加载 .env 文件中的变量 class Config: # LLM 配置 LLM_PROVIDER openai # 可选openai, anthropic, ollama等 OPENAI_API_KEY os.getenv(OPENAI_API_KEY) OPENAI_MODEL gpt-3.5-turbo # 用于信息提取和问答对精度要求高可换gpt-4 EMBEDDING_MODEL text-embedding-3-small # OpenAI的嵌入模型 # 向量数据库配置 VECTOR_STORE_TYPE chroma PERSIST_DIRECTORY ./data/chroma_db # 向量数据库持久化路径 # 记忆引擎参数 EXTRACTION_PROMPT_TEMPLATE 你是一个资深的TTRPG战役记录分析专家。请从以下游戏会话片段中提取所有重要的**实体**和**事实**。 实体包括玩家角色(PC)、非玩家角色(NPC)、地点、组织、重要物品。 事实描述了实体之间的关系或发生的事件。 请以JSON格式输出包含两个字段 1. entities: 一个列表每个元素是一个对象包含 name名称和 type类型如PC、NPC、Location等。 2. statements: 一个列表每个元素是一个对象包含 subject主语实体名、predicate谓语如“拜访”、“拥有”、“怀疑”、object宾语实体名和 description简短的事实描述。 会话片段 {session_text} 输出JSON RETRIEVAL_TOP_K 5 # 每次检索返回的最相关记忆数量 config Config()3. 实现核心模块提取、存储与检索我们将按照数据流依次实现三个核心模块。3.1 信息提取器 (Extractor)extractor.py负责调用大语言模型从原始文本中提取结构化信息。# core/extractor.py import json import logging from langchain_openai import ChatOpenAI from langchain.schema import HumanMessage, SystemMessage from config import config logger logging.getLogger(__name__) class InformationExtractor: def __init__(self): # 初始化LLM用于信息提取 self.llm ChatOpenAI( modelconfig.OPENAI_MODEL, api_keyconfig.OPENAI_API_KEY, temperature0.1 # 低温度保证输出稳定性 ) self.prompt_template config.EXTRACTION_PROMPT_TEMPLATE def extract_from_session(self, session_text: str): 从单段会话文本中提取实体和事实 if not session_text.strip(): return {entities: [], statements: []} # 构造提示词 prompt self.prompt_template.format(session_textsession_text) messages [ SystemMessage(content你是一个精确的JSON输出机器。只返回有效的JSON不要有任何额外解释。), HumanMessage(contentprompt) ] try: response self.llm.invoke(messages) # 解析LLM返回的JSON字符串 result json.loads(response.content) # 简单验证结构 if not isinstance(result, dict) or entities not in result or statements not in result: logger.warning(fLLM返回了非标准JSON结构: {response.content[:200]}) return {entities: [], statements: []} return result except json.JSONDecodeError as e: logger.error(f解析LLM返回的JSON失败: {e}. 原始响应: {response.content[:500]}) return {entities: [], statements: []} except Exception as e: logger.error(f信息提取过程发生未知错误: {e}) return {entities: [], statements: []} def batch_extract(self, session_list): 批量提取适用于处理多段日志 all_entities [] all_statements [] for text in session_list: result self.extract_from_session(text) all_entities.extend(result.get(entities, [])) all_statements.extend(result.get(statements, [])) # 简单的去重基于名称和类型或描述 unique_entities self._deduplicate_entities(all_entities) unique_statements self._deduplicate_statements(all_statements) return {entities: unique_entities, statements: unique_statements} def _deduplicate_entities(self, entity_list): seen set() unique [] for e in entity_list: key (e.get(name, ).lower(), e.get(type, )) if key not in seen: seen.add(key) unique.append(e) return unique def _deduplicate_statements(self, statement_list): seen set() unique [] for s in statement_list: # 基于主语、谓语、宾语的核心三元组去重 key (s.get(subject, ).lower(), s.get(predicate, ).lower(), s.get(object, ).lower()) if key not in seen: seen.add(key) unique.append(s) return unique关键点解释温度参数temperature设置为较低的0.1是为了让模型在信息提取这种需要高准确度的任务上输出更确定、更一致的结果减少随机性。SystemMessage用于设定模型角色强调“只返回JSON”这能有效约束模型输出格式便于后续解析。错误处理LLM的输出可能不稳定必须用try-except包裹并对解析失败的情况提供降级处理返回空结构避免整个流程中断。去重同一实体或事实可能在多段日志中被反复提及批量处理后的简单去重是必要的可以避免向量数据库中存在大量重复记忆。3.2 记忆存储与检索 (MemoryStore)memory_store.py负责管理向量数据库处理记忆的存储和检索。# core/memory_store.py import os from typing import List, Dict, Any from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain.schema import Document from config import config class MemoryStore: def __init__(self): # 初始化嵌入模型 self.embeddings OpenAIEmbeddings( modelconfig.EMBEDDING_MODEL, api_keyconfig.OPENAI_API_KEY ) # 初始化或加载现有的Chroma向量库 self.persist_directory config.PERSIST_DIRECTORY self.vector_store self._init_vector_store() def _init_vector_store(self): 初始化向量存储如果存在持久化数据则加载 os.makedirs(self.persist_directory, exist_okTrue) # Chroma会自动处理持久化 return Chroma( persist_directoryself.persist_directory, embedding_functionself.embeddings, collection_namecampaign_memory ) def add_memories(self, extracted_data: Dict[str, List]): 将提取的实体和事实作为记忆添加到向量库 documents [] metadatas [] # 1. 将实体转化为文档 for entity in extracted_data.get(entities, []): content f实体{entity[name]}类型{entity[type]}. # 可以添加更多描述性信息如果后续有更丰富的实体属性 doc Document(page_contentcontent) metadata { type: entity, name: entity[name], entity_type: entity[type], source: extraction } documents.append(doc) metadatas.append(metadata) # 2. 将事实陈述转化为文档 for stmt in extracted_data.get(statements, []): content f事实{stmt[description]} doc Document(page_contentcontent) metadata { type: statement, subject: stmt[subject], predicate: stmt[predicate], object: stmt[object], description: stmt[description], source: extraction } documents.append(doc) metadatas.append(metadata) if documents: # 批量添加到向量库 self.vector_store.add_documents(documentsdocuments, metadatasmetadatas) # 显式持久化到磁盘 self.vector_store.persist() print(f成功添加 {len(documents)} 条记忆到知识库。) else: print(没有可添加的记忆。) def search_memories(self, query: str, k: int None) - List[Dict[str, Any]]: 根据查询语义搜索相关记忆 if k is None: k config.RETRIEVAL_TOP_K # 执行相似度搜索 docs_and_scores self.vector_store.similarity_search_with_score(query, kk) results [] for doc, score in docs_and_scores: results.append({ content: doc.page_content, metadata: doc.metadata, relevance_score: score # 注意Chroma返回的是距离分数越小越相似 }) return results def get_stats(self): 获取记忆库的简单统计信息 # 注意Chroma的collection计数可能需要通过内部属性获取这里用try-except try: collection self.vector_store._collection count collection.count() return {total_memories: count} except: return {total_memories: 无法获取}关键点解释文档化无论是实体还是事实都被封装成 LangChain 的Document对象其page_content是用于向量化的文本metadata存储原始的结构化信息便于后续过滤或展示。元数据Metadata在元数据中存储类型entity/statement、名称、关系等使得在检索后不仅能得到相关文本还能知道这条记忆的“身份”这对于后续的答案生成逻辑非常重要。持久化Chroma将向量索引存储在本地目录程序重启后可以重新加载记忆不会丢失。相似度搜索similarity_search_with_score返回文档及其与查询的相似度分数通常是距离。分数越低表示在向量空间中越接近语义越相关。3.3 AI助手代理 (Agent)agent.py是面向用户的接口它协调提取器、记忆库和LLM完成“提问-检索-回答”的闭环。# core/agent.py from typing import List from langchain_openai import ChatOpenAI from langchain.schema import HumanMessage, SystemMessage from .extractor import InformationExtractor from .memory_store import MemoryStore from config import config class CampaignAgent: def __init__(self): self.extractor InformationExtractor() self.memory_store MemoryStore() # 初始化一个专门用于生成答案的LLM可以与提取器使用不同模型 self.answer_llm ChatOpenAI( modelconfig.OPENAI_MODEL, api_keyconfig.OPENAI_API_KEY, temperature0.7 # 回答问题时温度可以稍高更有创造性 ) def ingest_session_log(self, session_text: str): 消化一段会话日志提取记忆并存储 print(正在从会话日志中提取记忆...) extracted_data self.extractor.extract_from_session(session_text) print(f提取到 {len(extracted_data[entities])} 个实体{len(extracted_data[statements])} 个事实。) self.memory_store.add_memories(extracted_data) def query(self, question: str) - str: 根据记忆回答关于战役的问题 print(f正在检索与问题相关的记忆{question}) # 1. 从记忆库中检索相关上下文 relevant_memories self.memory_store.search_memories(question) if not relevant_memories: context 知识库中没有找到与问题直接相关的记忆。 else: # 2. 组装上下文 memory_texts [] for mem in relevant_memories[:5]: # 限制上下文长度 mem_text f- {mem[content]} (来源{mem[metadata].get(type, N/A)}) memory_texts.append(mem_text) context \n.join(memory_texts) print(f检索到 {len(relevant_memories)} 条相关记忆。) # 3. 构造给LLM的提示词 system_prompt 你是一个专业的TTRPG战役助手熟知当前战役的所有历史。请严格基于提供的“相关记忆”来回答问题。如果记忆中没有足够信息请如实说明你不知道不要编造信息即避免幻觉。 相关记忆 {context} user_prompt f玩家或GM的问题{question} messages [ SystemMessage(contentsystem_prompt.format(contextcontext)), HumanMessage(contentuser_prompt) ] # 4. 调用LLM生成答案 print(正在生成答案...) response self.answer_llm.invoke(messages) return response.content def get_memory_stats(self): 获取记忆库状态 return self.memory_store.get_stats()关键点解释双LLM模式实践中信息提取和问答可以使用同一个LLM但参数如temperature可以不同。提取需要高精确度低temperature而问答可以有一定创造性稍高temperature。对于资源敏感的场景甚至可以用大模型做提取用小模型做问答。上下文组装将检索到的记忆片段格式化后放入系统提示词System Prompt中。明确指示模型“严格基于相关记忆回答”这是对抗“幻觉”的关键指令。检索结果限制即使检索到很多条也只取最相关的几条如Top 5放入上下文以控制令牌消耗和避免信息过载。4. 运行验证从日志到智能问答现在我们将所有模块串联起来用一个完整的例子验证流程。4.1 准备示例战役日志在data/raw_logs/session_1.txt中放入一段虚构的TTRPG会话记录**游戏日期** 风息月15日 **参与玩家** 艾莉丝半精灵游侠、波尔特人类法师 **地点** 银月城旅店“沉睡巨人” **剧情摘要** 艾莉丝和波尔特在“沉睡巨人”旅店遇到了神秘的半身人情报贩子“快嘴”芬恩。芬恩告诉他们银月城的城主“高塔”霍雷肖最近行为诡异频繁与来自“黯影商会”的使者密会。芬恩怀疑城主可能被商会胁迫或控制了。作为报酬芬恩希望玩家们能去城主城堡的下水道入口附近找到一个他丢失的镶银烟斗那是一个重要信物。 艾莉丝同意了这个任务但波尔特对芬恩的动机表示怀疑。4.2 编写主程序并测试创建main.py作为演示入口# main.py import os from core.agent import CampaignAgent def main(): # 初始化战役助手 agent CampaignAgent() print( Table Canon 战役记忆引擎演示 ) # 演示1消化一段会话日志 print(\n1. 正在消化会话日志...) log_path ./data/raw_logs/session_1.txt if os.path.exists(log_path): with open(log_path, r, encodingutf-8) as f: session_text f.read() agent.ingest_session_log(session_text) else: print(f日志文件不存在: {log_path}请先创建。) # 使用硬编码文本演示 session_text 艾莉丝和波尔特在银月城遇到了芬恩。芬恩说城主霍雷肖与黯影商会有联系。 agent.ingest_session_log(session_text) # 查看记忆库状态 stats agent.get_memory_stats() print(f当前记忆库总数: {stats}) # 演示2基于记忆进行问答 print(\n2. 开始问答测试。输入 quit 退出。) while True: question input(\n请输入你的问题: ).strip() if question.lower() in [quit, exit, q]: break if not question: continue answer agent.query(question) print(f\n[助手回答]\n{answer}\n{-*40}) if __name__ __main__: main()运行程序python main.py4.3 预期交互过程与输出程序运行后你会看到类似以下的输出 Table Canon 战役记忆引擎演示 1. 正在消化会话日志... 正在从会话日志中提取记忆... 提取到 6 个实体4 个事实。 成功添加 10 条记忆到知识库。 当前记忆库总数: {total_memories: 10} 2. 开始问答测试。输入 quit 退出。 请输入你的问题: 艾莉丝是谁 正在检索与问题相关的记忆艾莉丝是谁 检索到 3 条相关记忆。 正在生成答案... [助手回答] 根据相关记忆艾莉丝是一名半精灵游侠。她与波尔特一起在银月城的“沉睡巨人”旅店活动并遇到了情报贩子芬恩。她同意了芬恩提出的寻找烟斗的任务。 ---------------------------------------- 请输入你的问题: 城主霍雷肖怎么了 正在检索与问题相关的记忆城主霍雷肖怎么了 检索到 2 条相关记忆。 正在生成答案... [助手回答] 根据记忆银月城的城主“高塔”霍雷肖最近行为诡异频繁与来自“黯影商会”的使者进行密会。情报贩子芬恩怀疑他可能被黯影商会胁迫或控制了。 ---------------------------------------- 请输入你的问题: 芬恩想要什么 正在检索与问题相关的记忆芬恩想要什么 检索到 2 条相关记忆。 正在生成答案... [助手回答] 芬恩希望艾莉丝和波尔特能去城主城堡的下水道入口附近帮他找到一个丢失的镶银烟斗。这个烟斗被描述为一个重要的信物。 ----------------------------------------验证成功的关键标志提取阶段程序能正确识别出实体艾莉丝、波尔特、芬恩、霍雷肖、银月城、黯影商会等和事实“行为诡异”、“密会”、“同意任务”等。存储阶段控制台打印了添加记忆的数量并且后续问答能基于这些记忆进行。检索与问答阶段AI 的回答严格基于提取的记忆没有凭空捏造信息例如如果你问一个日志中不存在的人物“国王”AI 应回答不知道。回答内容连贯将分散的记忆点整合成了通顺的段落。5. 常见问题排查与优化策略在实际搭建和运行过程中你可能会遇到以下典型问题。5.1 信息提取不准确或遗漏现象LLM 提取的实体和事实数量过少或类型错误如把地点识别为人名。可能原因与解决方案问题现象可能原因检查与解决思路提取结果为空或很少1. 提示词Prompt不够清晰。2. 会话文本太短或信息密度低。3. LLM 温度参数过高输出不稳定。1.优化提示词在提示词中提供更具体的实体类型示例和事实格式示例。例如明确列出“PC, NPC, Location, Organization, Item”等类型并给出一个完整的JSON输出样例。2.调整文本确保输入文本包含足够的具体描述。对于稀疏文本可以尝试将多段相关日志合并后一起提取。3.降低温度将提取器的temperature降至 0.1 或 0确保确定性。实体类型识别错误1. 类型定义模糊。2. 名称本身有歧义如“十字路口”既是地点也可能是组织名。1.细化类型定义在提示词中为每种类型提供简短说明例如“Location: 城市、村庄、建筑、自然地貌等”。2.利用上下文在提取时可以要求LLM不仅给出类型还附带一个简短的“理由”字段用于后续人工审核或规则校正。JSON 解析失败1. LLM 输出格式不符合JSON规范多了前言后语。2. JSON 中存在未转义的特殊字符。1.强化格式指令在 System Prompt 中严格强调“只输出JSON不要有任何其他文本”。2.使用解析库的容错模式如json.loads()失败后可以尝试用正则表达式提取{}内的内容。3.使用 LangChain 的输出解析器如PydanticOutputParser可以更稳定地获取结构化数据。5.2 检索结果不相关现象提出的问题明明在日志中提到过但检索到的记忆片段风马牛不相及。可能原因与解决方案问题现象可能原因检查与解决思路检索结果完全无关1. 嵌入模型不适合领域。2. 记忆文本的向量化质量差过于简短或模糊。1.更换嵌入模型OpenAI的text-embedding-3-small通用性较好。对于特定领域如奇幻文学可以尝试在领域文本上微调过的开源嵌入模型如BGE、SentenceTransformers系列。2.优化记忆文本不要只存储“实体名称类型NPC”可以拼接一些上下文如“实体芬恩类型NPC。描述一个在银月城活动的神秘半身人情报贩子外号‘快嘴’。”检索结果部分相关但漏掉关键1. 检索数量k设置太小。2. 查询语句与记忆文本表述差异大。1.调整k值逐步增加RETRIEVAL_TOP_K例如从5到10观察召回率变化。注意权衡召回率和上下文长度。2.查询重写Query Rewriting在检索前先用一个轻量级模型或规则对用户问题进行扩展或改写。例如将“城主怎么了”重写为“银月城城主霍雷肖最近发生了什么情况”。重复记忆干扰检索同一事实被多次提取并存储导致相似度前几名都是同一内容。1.加强提取阶段的去重如代码所示基于核心三元组进行去重。2.在检索后做去重对检索结果根据元数据或内容相似度进行后处理合并或剔除高度重复的条目。5.3 AI 回答出现“幻觉”或偏离记忆现象即使检索到了正确记忆AI 生成的答案仍然包含未提及的细节或错误信息。可能原因与解决方案问题现象可能原因检查与解决思路答案编造细节1. 系统提示词约束力不足。2. 问答LLM的温度设置过高。1.强化系统指令在 System Prompt 中使用更严厉的措辞如“你必须且只能使用提供的记忆来回答问题。如果记忆中没有相关信息请直接说‘根据现有记忆我无法回答这个问题。’严禁推断或编造。”2.降低问答温度将answer_llm的temperature调低如从0.7调到0.3。3.提供引用要求模型在答案中引用它所依据的记忆编号或片段这既能提高可信度也便于人工核查。答案忽略部分记忆上下文中的记忆片段过多或过长模型未能注意到所有信息。1.优化上下文组装对检索到的记忆进行重要性排序或摘要。例如只保留与问题关键词重叠度最高的片段。2.使用更强大的模型如果使用 GPT-3.5-turbo对于复杂查询和长上下文其遵循指令和关注所有细节的能力可能弱于 GPT-4。在关键场景考虑升级模型。答案格式不符合要求模型在答案前后添加了多余的解释。在 Human Prompt 中明确指定输出格式例如“请用一句简洁的话回答。”或“请以列表形式给出要点。”5.4 性能与成本优化对于长期运行的战役记忆库会不断增长需要关注效率和成本。批量处理日志不要每次游戏后都实时处理。可以积累多次会话记录后使用batch_extract方法一次性处理减少LLM调用次数。分层记忆存储将记忆分为“核心设定”如世界背景、主要角色关系和“会话事实”。核心设定可以手动录入或一次性提取会话事实则增量添加。检索时可以优先检索与会话事实相关的记忆必要时再结合核心设定。使用本地模型对于信息提取和问答可以尝试使用量化后的开源大模型如 Llama 3、Qwen 等通过 Ollama 或 vLLM 本地部署以消除API调用成本。但需在精度和速度上做好权衡。向量数据库优化对于超过数万条的记忆可以考虑使用支持更高效索引如 HNSW的专业向量数据库如 Weaviate, Qdrant并建立基于元数据如时间、实体类型的过滤索引加速检索。6. 生产环境最佳实践与扩展方向将 Table Canon 从演示原型发展为可用于真实战役的可靠工具还需要考虑以下方面。6.1 工程化与可靠性配置管理将所有配置模型类型、API密钥、路径、参数外置到配置文件或环境变量便于不同环境开发、测试、生产切换。日志记录为每个核心模块添加详细的日志记录如使用 Pythonlogging模块记录信息提取的原始输入输出、检索查询和结果、LLM调用耗时等便于监控和调试。错误处理与重试LLM API 调用可能因网络或速率限制失败。需要实现带退避策略的重试机制并对持久化操作向量数据库写入进行异常捕获。数据备份定期备份chroma_db目录下的数据文件。考虑将记忆的原始文本和提取的结构化数据也备份到关系型数据库或文件系统中作为向量数据库的容灾备份。6.2 记忆的维护与演化记忆更新与修正玩家可能纠正之前的错误或GM可能修订设定。系统需要支持对已有记忆的更新或软删除。一种实现方式是为每条记忆添加版本号或“是否有效”标志并在检索时过滤。记忆融合当从不同会话中提取到关于同一实体的矛盾信息时例如一个NPC的阵营描述前后不一致需要设计冲突解决策略例如以最新信息为准或标记为“存疑”供GM审核。记忆抽象与总结除了原子事实可以定期如每完成一个故事章节让LLM对一段时间内的记忆进行总结生成更高层次的“情节摘要”或“人物弧光”并将其也作为一条可检索的记忆存储起来用于回答宏观问题。6.3 扩展功能设想主动记忆提醒在游戏过程中当对话触及到某个关键记忆点时例如玩家再次提到“黯影商会”系统可以主动向GM推送相关的背景记忆辅助叙事。关系图谱可视化将存储的实体和事实导出为图数据如 Neo4j并提供可视化界面让GM能直观地查看人物关系网和事件链条。多模态记忆除了文本日志系统是否可以接入语音记录通过ASR转文本或游戏地图图片通过多模态模型提取描述来丰富记忆玩家视角记忆为每个玩家角色建立独立的“记忆视图”只包含该角色所知的信息用于生成符合角色认知的对话或日记。构建一个成熟的战役记忆引擎是一个持续迭代的过程。从最基础的“提取-存储-检索”闭环开始逐步加入上述高级特性你将能够打造出一个真正理解你的游戏世界、并成为GM得力助手的AI系统。核心始终是让技术服务于叙事而不是让叙事适应技术。