
在跑团TTRPG时你是否曾为记不住复杂的剧情线、NPC关系和玩家决策而烦恼或者作为游戏主持人GM是否苦于在漫长的战役中保持故事的一致性和连贯性传统的手写笔记或零散的文档常常在关键时刻“掉链子”让精心设计的冒险体验大打折扣。本文将深入探讨如何利用现代AI技术构建一个专为TTRPG设计的“战役记忆引擎”——Table Canon。我们将从核心概念出发一步步拆解其技术架构并提供一个可运行的实战项目手把手教你搭建一个能够理解、记忆并推理游戏世界信息的智能助手。无论你是对AI应用开发感兴趣的开发者还是希望用技术提升跑团体验的资深GM都能从本文中获得一套完整的解决方案。1. 背景与核心概念什么是战役记忆引擎在深入代码之前我们首先要厘清几个关键概念TTRPG、战役记忆以及AI在其中扮演的角色。TTRPG桌上角色扮演游戏是一种由玩家通过语言描述和规则判定来共同推进故事的游戏如《龙与地下城》DD。游戏的核心是“战役”Campaign即一个由多个冒险章节构成的长期故事。GM需要管理庞大的世界观、错综的人物关系和动态的剧情分支。传统信息管理的痛点信息碎片化笔记散落在纸质笔记本、多个数字文档甚至聊天记录中。检索困难在游戏进行中快速查找某个NPC三周前说过的话几乎不可能。一致性挑战随着战役推进很容易遗忘早期的设定或玩家选择导致剧情矛盾。创意枯竭GM需要不断创造新内容缺乏一个能激发灵感的“知识库”。AI战役记忆引擎Campaign Memory Engine正是为了解决这些问题而生。它不是一个简单的笔记应用而是一个具备以下能力的智能系统结构化记忆将游戏中的自然语言描述如“精灵王子艾兰对盗贼怀有戒心”转化为结构化的知识实体艾兰关系不信任对象盗贼。语义理解与检索允许GM用自然语言提问如“上次谁在酒馆提到了古墓”引擎能理解问题意图并返回相关片段。上下文关联与推理基于已有记忆对当前游戏情境提供建议或预警例如“玩家正在调查的商人他的兄弟正是他们上周救过的民兵队长这可能会影响交易态度”。叙事辅助生成根据已有设定和剧情走向辅助生成NPC对话、地点描述或新的剧情钩子。本质上它是一个专属于你当前战役的、持续学习的“第二大脑”或“副GM”将AI大模型的通用语言能力与特定战役的私有知识库相结合。2. 技术选型与环境准备构建这样一个系统我们需要一套组合技术栈。本文将基于一个Python技术栈进行演示该栈平衡了能力、开发效率和社区支持。2.1 核心组件与技术栈后端框架FastAPI。轻量级、异步高性能非常适合构建AI应用的API能轻松处理聊天、检索等请求。向量数据库ChromaDB。轻量、易嵌入、开发者友好是存储和检索文本嵌入向量即AI理解的语义表示的理想选择。大语言模型LLM与嵌入模型Ollama本地模型。为了数据隐私和可控性我们选择在本地运行开源模型。Ollama是一个强大的本地LLM运行和管理工具。嵌入模型用于将文本转换为向量推荐nomic-embed-text或all-minilm它们在小规模数据上表现良好且速度快。对话/推理模型用于对话和推理推荐llama3.2:3b或qwen2.5:7b它们在保持较小参数量的同时提供了不错的推理能力。开发语言Python 3.10。其他工具langchain库用于编排AI链uv或pip用于包管理。2.2 环境搭建步骤请确保你的开发环境已安装Python 3.10或更高版本。步骤一创建项目目录并初始化环境# 创建项目目录 mkdir table_canon_engine cd table_canon_engine # 创建虚拟环境推荐使用uv或venv python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 创建核心文件 touch main.py requirements.txt .env mkdir app touch app/__init__.py app/models.py app/routers.py app/vector_store.py步骤二安装Ollama前往 Ollama官网 下载并安装对应操作系统的版本。安装完成后打开终端运行# 拉取我们需要的模型请确保网络通畅模型较大 ollama pull nomic-embed-text # 嵌入模型 ollama pull llama3.2:3b # 对话模型可根据硬件选择更大模型如llama3.2:7b步骤三安装Python依赖编辑requirements.txt文件添加以下内容fastapi0.104.1 uvicorn[standard]0.24.0 langchain0.1.0 langchain-community0.0.10 chromadb0.4.22 ollama0.1.9 python-dotenv1.0.0 pydantic2.5.0 pydantic-settings2.1.0然后安装pip install -r requirements.txt3. 核心架构与原理拆解我们的Table Canon引擎主要包含三个核心模块理解它们是如何协作的至关重要。3.1 知识摄取与向量化模块这是系统的“记忆形成”阶段。当GM输入一段游戏日志如“玩家在幽暗森林击败了巨魔并发现了一把刻有古老精灵符文的匕首”时文本分割使用langchain的文本分割器将长文本按语义切分成较小的片段如按句子或段落以便更精细地检索。向量化每个文本片段通过Ollama运行的嵌入模型转换为一个高维向量一组数字。这个向量在数学空间中的位置代表了该文本的“语义”。语义相似的文本其向量在空间中的位置也接近。存储将{文本片段 对应向量 元数据如发生时间、地点、相关角色}存入ChromaDB向量数据库。3.2 语义检索模块这是系统的“记忆回想”阶段。当GM提问时问题向量化将问题如“我们之前在森林里找到过什么武器”同样转换为向量。相似度搜索在ChromaDB中计算问题向量与所有存储向量之间的“余弦相似度”。相似度最高的前k个文本片段就是与问题最相关的“记忆”。上下文组装将这些检索到的文本片段连同问题本身一起组装成一个提示Prompt发送给对话模型。3.3 对话与推理模块这是系统的“智能应答”阶段。组装好的提示被送入Ollama运行的对话模型如Llama 3.2。模型的指令通常是你是一个TTRPG战役记忆助手。请严格基于以下提供的战役上下文来回答问题。如果上下文不足以回答问题请说明你不知道。 战役上下文 {检索到的相关文本片段1} {检索到的相关文本片段2} ... 问题{用户的问题} 回答模型基于给定的上下文进行推理和生成确保回答不“幻觉”出不存在的信息牢牢扎根于战役事实。4. 完整实战构建Table Canon引擎现在让我们将理论付诸实践一步步构建出可运行的代码。4.1 项目结构与配置首先设置环境变量。创建.env文件# .env OLLAMA_BASE_URLhttp://localhost:11434 EMBEDDING_MODELnomic-embed-text LLM_MODELllama3.2:3b VECTOR_DB_PATH./chroma_db然后创建配置和基础模型。编辑app/models.py# app/models.py from pydantic import BaseModel from typing import List, Optional from datetime import datetime class MemoryFragment(BaseModel): 战役记忆片段的数据模型 id: Optional[str] None content: str # 文本内容 metadata: dict # 元数据如 {“timestamp”: “2023-10-27”, “location”: “幽暗森林”, “characters”: [“艾兰”, “盗贼”]} embedding: Optional[List[float]] None # 向量表示 class QueryRequest(BaseModel): 用户查询请求模型 question: str top_k: int 3 # 返回最相关的记忆片段数量 class QueryResponse(BaseModel): 查询响应模型 answer: str relevant_memories: List[MemoryFragment] # 用于回答的相关记忆 class AddMemoryRequest(BaseModel): 添加记忆请求模型 content: str metadata: dict {}4.2 实现向量存储层编辑app/vector_store.py这是与ChromaDB交互的核心。# app/vector_store.py import chromadb from chromadb.config import Settings from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma from app.models import MemoryFragment import os from dotenv import load_dotenv load_dotenv() class VectorStoreManager: 向量数据库管理类封装所有存储和检索操作 def __init__(self): self.embedding_model OllamaEmbeddings( modelos.getenv(EMBEDDING_MODEL, nomic-embed-text), base_urlos.getenv(OLLAMA_BASE_URL, http://localhost:11434) ) self.persist_directory os.getenv(VECTOR_DB_PATH, ./chroma_db) # 初始化Chroma客户端和LangChain向量库 self.client chromadb.PersistentClient( pathself.persist_directory, settingsSettings(anonymized_telemetryFalse) ) # 创建一个名为“campaign_memories”的集合如果不存在 self.collection self.client.get_or_create_collection(namecampaign_memories) # LangChain的向量库包装便于使用其检索接口 self.vectorstore Chroma( clientself.client, collection_namecampaign_memories, embedding_functionself.embedding_model.embed_query, ) def add_memory(self, memory: MemoryFragment) - str: 添加一个记忆片段到向量数据库 # 生成嵌入向量 embedding self.embedding_model.embed_query(memory.content) memory.embedding embedding # 使用一个简单的内容哈希作为ID确保幂等性 import hashlib memory_id hashlib.md5(memory.content.encode()).hexdigest()[:12] # 添加到Chroma集合 self.collection.add( documents[memory.content], metadatas[memory.metadata], embeddings[embedding], ids[memory_id] ) return memory_id def search_similar(self, query: str, top_k: int 3) - List[MemoryFragment]: 语义搜索最相关的记忆片段 # 使用LangChain的相似度搜索 docs_and_scores self.vectorstore.similarity_search_with_score(query, ktop_k) results [] for doc, score in docs_and_scores: results.append(MemoryFragment( iddoc.metadata.get(id, ), contentdoc.page_content, metadatadoc.metadata, # score 是相似度分数这里可以记录 )) return results def get_all_memories(self, limit: int 100) - List[MemoryFragment]: 获取所有记忆用于调试或管理 results self.collection.get(limitlimit) memories [] for i in range(len(results[ids])): memories.append(MemoryFragment( idresults[ids][i], contentresults[documents][i], metadataresults[metadatas][i], )) return memories4.3 实现AI对话链我们需要一个模块来协调检索和生成。创建app/chain.py# app/chain.py from langchain_community.llms import Ollama from langchain.prompts import PromptTemplate from langchain.chains import RetrievalQA from app.vector_store import VectorStoreManager from app.models import QueryRequest, QueryResponse import os from dotenv import load_dotenv load_dotenv() class CampaignQAChain: 战役问答链核心的检索-生成流程 def __init__(self): self.vector_store VectorStoreManager() self.llm Ollama( modelos.getenv(LLM_MODEL, llama3.2:3b), base_urlos.getenv(OLLAMA_BASE_URL, http://localhost:11434), temperature0.1, # 低温度使输出更确定、更基于事实 ) # 定义提示词模板指导模型基于上下文回答 self.prompt_template PromptTemplate( input_variables[context, question], template你是一个专业的TTRPG战役记忆助手。你的任务是严格基于提供的战役上下文来回答问题。 如果上下文信息不足以回答请直接说“根据现有记录无法确定”。不要编造信息。 相关战役上下文 {context} 问题{question} 请基于以上上下文给出回答 ) def query(self, request: QueryRequest) - QueryResponse: 处理用户查询检索 - 组装上下文 - 生成回答 # 1. 语义检索 relevant_memories self.vector_store.search_similar(request.question, top_krequest.top_k) if not relevant_memories: return QueryResponse( answer目前知识库中没有找到相关信息。, relevant_memories[] ) # 2. 组装上下文 context_text \n---\n.join([mem.content for mem in relevant_memories]) # 3. 格式化提示词并调用LLM prompt self.prompt_template.format(contextcontext_text, questionrequest.question) answer self.llm.invoke(prompt) return QueryResponse( answeranswer.strip(), relevant_memoriesrelevant_memories )4.4 构建FastAPI后端服务现在我们将所有模块连接起来通过API暴露功能。编辑app/routers.py# app/routers.py from fastapi import APIRouter, HTTPException from app.models import AddMemoryRequest, QueryRequest, QueryResponse, MemoryFragment from app.vector_store import VectorStoreManager from app.chain import CampaignQAChain from typing import List router APIRouter(prefix/api/v1, tags[campaign-memory]) vector_store VectorStoreManager() qa_chain CampaignQAChain() router.post(/memories, response_modeldict) async def add_memory(request: AddMemoryRequest): 添加新的战役记忆片段 try: memory MemoryFragment(contentrequest.content, metadatarequest.metadata) memory_id vector_store.add_memory(memory) return {message: Memory added successfully, id: memory_id} except Exception as e: raise HTTPException(status_code500, detailfFailed to add memory: {str(e)}) router.post(/query, response_modelQueryResponse) async def query_memory(request: QueryRequest): 向战役记忆引擎提问 try: return qa_chain.query(request) except Exception as e: raise HTTPException(status_code500, detailfQuery failed: {str(e)}) router.get(/memories, response_modelList[MemoryFragment]) async def list_memories(limit: int 50): 列出所有记忆片段用于调试和管理 try: return vector_store.get_all_memories(limitlimit) except Exception as e: raise HTTPException(status_code500, detailfFailed to list memories: {str(e)})最后创建主程序入口main.py# main.py from fastapi import FastAPI from app.routers import router import uvicorn from dotenv import load_dotenv load_dotenv() app FastAPI( titleTable Canon - AI Campaign Memory Engine API, description一个为TTRPG战役提供智能记忆和问答的AI引擎, version0.1.0 ) # 注册路由 app.include_router(router) app.get(/) async def root(): return {message: Welcome to Table Canon AI Campaign Memory Engine} if __name__ __main__: uvicorn.run(main:app, host0.0.0.0, port8000, reloadTrue)4.5 运行与验证步骤一启动Ollama服务确保Ollama在后台运行。通常安装后会自动启动服务。可以在终端检查ollama list如果看到你拉取的模型列表说明服务正常。步骤二启动FastAPI后端在你的项目根目录下运行python main.py你应该看到类似输出INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRLC to quit)步骤三使用API进行测试我们可以使用curl命令或任何API测试工具如Postman、Hoppscotch来测试。添加记忆模拟GM记录一次游戏会话。curl -X POST http://localhost:8000/api/v1/memories \ -H Content-Type: application/json \ -d { content: 在‘锈蚀酒馆’玩家们从老板汤姆那里听说北边的废弃矿洞最近传来奇怪的呜咽声。他们决定明天一早去调查。, metadata: { session: 第3次跑团, location: 锈蚀酒馆, npc: [汤姆老板], timestamp: 2023-10-27 } }再添加几条curl -X POST http://localhost:8000/api/v1/memories \ -H Content-Type: application/json \ -d { content: 矿洞深处队伍遭遇了被黑暗能量腐蚀的巨型蝙蝠。游侠莉娜用银箭矢射中了它的翅膀弱点为大家创造了机会。, metadata: { session: 第3次跑团, location: 废弃矿洞, characters: [莉娜], enemy: 腐蚀巨蝠, timestamp: 2023-10-27 } } curl -X POST http://localhost:8000/api/v1/memories \ -H Content-Type: application/json \ -d { content: 在巨蝠巢穴后方发现了一个上锁的精灵风格宝箱。盗贼卡洛斯尝试开锁但失败了锁似乎被魔法封印。, metadata: { session: 第3次跑团, location: 废弃矿洞-深处, characters: [卡洛斯], item: 精灵宝箱, timestamp: 2023-10-27 } }进行智能查询现在GM可以像和助手对话一样提问。curl -X POST http://localhost:8000/api/v1/query \ -H Content-Type: application/json \ -d { question: 我们之前在矿洞里遇到了什么怪物是谁击败了它, top_k: 3 }预期响应具体措辞可能因模型略有不同{ answer: 根据记录你们在废弃矿洞深处遭遇了一只被黑暗能量腐蚀的巨型蝙蝠。游侠莉娜用银箭矢射中了它的翅膀弱点为队伍创造了击败它的机会。, relevant_memories: [ { id: ..., content: 矿洞深处队伍遭遇了被黑暗能量腐蚀的巨型蝙蝠。游侠莉娜用银箭矢射中了它的翅膀弱点为大家创造了机会。, metadata: {...} }, ... // 其他相关记忆 ] }再问一个更关联的问题curl -X POST http://localhost:8000/api/v1/query \ -H Content-Type: application/json \ -d { question: 关于那个宝箱我们有什么线索, top_k: 2 }引擎应该能准确回忆起宝箱及其魔法锁的细节。步骤四查看所有记忆curl -X GET http://localhost:8000/api/v1/memories至此一个具备核心记忆、检索和问答功能的AI战役记忆引擎就成功运行起来了GM可以通过简单的API调用来记录和查询无需再手动翻找零散的笔记。5. 常见问题与排查思路在开发和运行过程中你可能会遇到以下问题问题现象常见原因解决思路启动main.py时报错ConnectionError连接到Ollama1. Ollama服务未运行。2.OLLAMA_BASE_URL环境变量错误。1. 在终端运行ollama serve启动服务。2. 检查.env文件或环境变量确保OLLAMA_BASE_URLhttp://localhost:11434。添加或查询记忆时速度很慢1. 首次运行嵌入模型需要加载。2. 硬件性能不足特别是运行7B以上模型。3. ChromaDB索引未优化。1. 首次使用耐心等待后续调用会缓存。2. 考虑使用更小的模型如llama3.2:3b或确保有足够RAM/VRAM。3. 对于大量数据考虑在VectorStoreManager初始化时配置collection的索引参数。查询返回的结果不相关答非所问1. 嵌入模型不适合该类型文本。2. 文本分割不合理丢失了上下文。3.top_k参数太小。1. 尝试更换嵌入模型如all-minilm。2. 调整langchain文本分割器的chunk_size和chunk_overlap参数。3. 适当增加top_k值如5-10并在QueryResponse中检查返回的relevant_memories质量。LLM的回答出现“幻觉”编造信息1. 提示词Prompt约束力不够。2. 检索到的上下文太少或完全不相关。3. 模型温度temperature参数过高。1. 强化app/chain.py中提示词模板的约束明确要求“基于上下文”。2. 先确保检索模块返回的结果是相关的见上一条。3. 降低Ollama初始化时的temperature如设为0.1。ChromaDB 报错或数据丢失1. 存储路径权限问题。2. 不同版本ChromaDB不兼容。1. 检查VECTOR_DB_PATH指向的目录是否有读写权限。2. 锁定chromadb的版本号避免升级导致数据格式变化。生产环境建议定期备份向量数据库目录。6. 最佳实践与工程建议将原型转化为一个稳定、可用的工具还需要考虑以下工程化实践6.1 记忆的元数据标准化为记忆片段设计丰富且结构化的元数据能极大提升检索和后期分析的精度。建议字段session_id场次、game_date游戏内日期、real_date真实日期、location地点、characters涉及角色列表、type类型对话/战斗/探索/物品、importance重要性评分。实现方式在AddMemoryRequest模型中定义更严格的metadata结构或提供前端表单让GM填写。6.2 记忆的更新、修正与版本管理战役事实可能随着剧情修正。简单的“添加”不够。更新策略为MemoryFragment添加version字段和parent_id字段。当需要修正时不是删除旧记录而是添加一条新记录并标记其parent_id为旧记录的ID同时将旧记录的is_current设为False。检索策略在search_similar时默认只检索is_currentTrue的记录。查询历史时可以专门检索某个version。6.3 前端界面开发API虽然强大但GM需要一个友好的界面。可以考虑技术栈使用Streamlit或Gradio快速构建一个简单的Web界面包含记忆录入框、聊天式问答界面和记忆图谱可视化。核心功能记忆看板按时间线或地点展示所有记忆卡片。智能问答框像聊天软件一样直接提问。实体关系图自动抽取记忆中的实体人物、地点、组织并绘制关系图提供全局视角。6.4 性能与扩展性优化批量导入实现一个/api/v1/memories/batch接口支持导入历史战役日志纯文本文件并自动分割、向量化、存储。异步处理对于添加记忆尤其是批量导入这种可能耗时的操作应使用FastAPI的BackgroundTasks或消息队列如Celery异步处理避免阻塞API响应。缓存对于频繁查询的通用问题如“当前队伍成员有哪些”可以将答案缓存一段时间使用redis或memcached。6.5 安全与隐私考量数据加密所有记忆数据是战役的核心资产。考虑在存储到ChromaDB前对content字段进行应用层加密或在数据库层面使用加密存储。API认证为FastAPI添加简单的API Key认证如使用HTTPBearer防止服务被未经授权访问。完全离线本方案的最大优势是模型Ollama和数据库ChromaDB均可本地运行确保所有敏感的游戏剧情数据不出本地环境。通过遵循这些最佳实践你可以将一个演示原型逐步打磨成一个能够在真实、长期的TTRPG战役中可靠服役的“副GM”工具。它不仅解决了信息管理的问题更能通过AI的语义理解能力发掘出那些连GM自己都可能忽略的故事线索和联系真正提升跑团的叙事深度和沉浸感。