资讯动态

基于AI与向量数据库的TTRPG战役记忆引擎实战开发指南

发布时间:2026/8/24 5:40:03 来源:尧图企业网站定制
你好我是专注于技术实战分享的博主。在桌面角色扮演游戏TTRPG的漫长战役中无论是作为主持人GM还是玩家最头疼的问题之一就是“记忆”上周那个关键NPC叫什么三年前埋下的伏笔线索是什么这个魔法物品的详细规则是什么传统的手写笔记或零散的文档难以高效检索和关联导致剧情连贯性下降游戏体验打折。今天我将为你深入解析一个名为Table Canon的开源项目。它是一个专为TTRPG设计的“战役记忆引擎”核心是利用AI技术将散乱的游戏笔记、对话、设定自动整理、关联并构建成一个可搜索、可追溯的“战役知识库”。本文将从一个开发者的视角完整拆解其技术架构、实现原理并手把手教你如何基于类似思路构建自己的AI辅助工具。无论你是对AI应用开发感兴趣的工程师还是想提升游戏体验的TTRPG爱好者都能从中获得实用的代码和清晰的思路。1. 背景与核心概念为什么TTRPG需要“记忆引擎”在深入代码之前我们首先要理解问题域。TTRPG如《龙与地下城》、《克苏鲁的呼唤》是一种高度依赖叙事和自由度的游戏。一场“战役”可能持续数月甚至数年产生海量的非结构化信息叙事信息剧情发展、角色对话、地点描述。规则信息角色属性、物品数据、法术效果。元信息玩家决策、GM的未公开设定“幕后秘密”。传统的信息管理方式如文本文件、Wiki、笔记软件存在几个痛点检索困难记得大概内容但找不到原文。关联性弱事件A、人物B、地点C之间的内在联系难以直观展现。信息孤岛对话记录在聊天软件角色卡在PDF地图在图片里无法统一查询。记忆负担GM需要记住大量细节压力巨大。Table Canon的核心理念就是充当GM的“第二大脑”。它不是一个替代GM的AI主持人而是一个增强GM能力的辅助工具。其核心工作流程可以概括为采集 - 理解 - 关联 - 检索。采集从各种来源游戏聊天记录、语音转文字、手动输入的笔记收集原始文本。理解利用大语言模型LLM理解文本的语义识别出实体人物、地点、组织和事件。关联自动建立实体与实体、实体与事件之间的关系形成知识图谱。检索提供自然语言查询接口例如“找出所有与‘盗贼公会’相关的对话和NPC”引擎能返回高度相关且上下文完整的片段。接下来我们将从技术选型开始一步步构建这个系统的核心模块。2. 环境准备与版本说明我们将使用 Python 作为主要开发语言这是当前AI生态最活跃的语言。以下是构建一个简化版“记忆引擎”所需的环境操作系统Windows 10/11, macOS, 或 Linux (Ubuntu 20.04)。本文示例在 Ubuntu 22.04 上开发。Python 版本3.9 或 3.10。建议使用pyenv或conda管理多版本。关键库与工具LangChain用于构建LLM应用的框架简化与模型交互、构建链式流程。版本0.1.x。OpenAI API或本地LLM用于文本理解和生成。我们将使用 OpenAI GPT-3.5/4 的API作为示例。也可用ollama运行本地模型如Llama 3。向量数据库用于存储文本的向量嵌入Embedding并实现语义搜索。选用ChromaDB轻量且易集成。FastAPI用于构建提供查询接口的Web服务。SQLite或PostgreSQL用于存储结构化的元数据如实体关系。项目初始化 首先创建项目目录并初始化虚拟环境。# 创建项目目录 mkdir table_canon_demo cd table_canon_demo # 创建虚拟环境以venv为例 python3 -m venv venv # 激活虚拟环境 # Linux/macOS source venv/bin/activate # Windows # venv\Scripts\activate # 安装核心依赖 pip install langchain langchain-openai chromadb fastapi uvicorn sqlalchemy pydantic目录结构预览table_canon_demo/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 应用入口 │ ├── core/ │ │ ├── __init__.py │ │ ├── memory_engine.py # 记忆引擎核心类 │ │ └── models.py # 数据模型 (Pydantic) │ ├── services/ │ │ ├── __init__.py │ │ ├── ingestion.py # 数据采集与处理服务 │ │ └── query.py # 查询服务 │ └── db/ │ ├── __init__.py │ └── session.py # 数据库会话 ├── data/ # 存放原始文本数据 ├── storage/ # ChromaDB 向量存储目录 ├── requirements.txt └── README.md3. 核心原理与技术拆解3.1 文本向量化与语义搜索这是实现“理解”和“检索”的基础。我们无法让计算机直接理解文本但可以将其转化为数学向量一组数字。语义相近的文本其向量在空间中的距离也更近。嵌入模型Embedding Model如text-embedding-3-small它将一段文本转换为一个固定长度的向量例如1536维。向量数据库存储所有文本片段及其对应的向量。当用户查询时先将查询语句也转化为向量然后在数据库中快速找出与之最相似的向量即最相关的文本片段。这个过程称为“近似最近邻搜索”。3.2 大语言模型LLM的角色LLM在系统中扮演两个关键角色信息提取器从大段文本中结构化地提取出实体、关系、摘要。例如输入一段游戏对话LLM可以输出JSON格式的结果{entities”: [{name”: “艾莉丝”, “type”: “NPC”}], “summary”: “玩家向艾莉丝打听了关于古墓的消息”}。答案合成器当检索到多个相关文本片段后LLM可以阅读这些片段并合成一个连贯、精炼的答案来响应用户的查询。3.3 知识图谱的构建简单的语义搜索能找相关段落但无法揭示深层次关系。知识图谱通过“实体-关系-实体”的三元组来形式化知识。实体游戏中的具体对象如人物:艾莉丝、地点:银月城、组织:盗贼公会。关系连接实体的谓语如属于、位于、提及、拥有。 系统可以定期用LLM分析新加入的文本抽取出三元组存入图数据库如Neo4j或关系数据库的特殊表中从而实现“这个NPC在哪次会话中被提到过”这类复杂查询。4. 完整实战构建简化版战役记忆引擎让我们开始编码实现一个具备核心功能的简化版本。4.1 数据模型定义首先在app/core/models.py中定义核心的数据结构。# app/core/models.py from pydantic import BaseModel, Field from typing import List, Optional, Dict, Any from datetime import datetime from enum import Enum class DocumentSource(str, Enum): SESSION_LOG session_log # 游戏会话记录 PLAYER_NOTE player_note # 玩家笔记 GM_NOTE gm_note # GM私有笔记 RULEBOOK rulebook # 规则书片段 class TextChunk(BaseModel): 文本块模型是向量化存储的基本单位 id: Optional[str] None content: str Field(..., description文本内容) source: DocumentSource session_id: Optional[str] Field(None, description关联的游戏会话ID) timestamp: Optional[datetime] None metadata: Dict[str, Any] Field(default_factorydict) # 存放额外信息如页码、说话者 class Entity(BaseModel): 实体模型 id: Optional[str] None name: str type: str # 如NPC, Location, Item, Organization description: Optional[str] None class Relationship(BaseModel): 关系模型 source_entity_id: str target_entity_id: str relation_type: str # 如located_in, member_of, owns description: Optional[str] None source_text_chunk_id: Optional[str] None # 该关系出自哪个文本块4.2 实现记忆引擎核心类在app/core/memory_engine.py中我们创建引擎的核心类。# app/core/memory_engine.py import os from typing import List, Optional from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain_chroma import Chroma from langchain.schema import Document as LangchainDocument from langchain.text_splitter import RecursiveCharacterTextSplitter from app.core.models import TextChunk, DocumentSource import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class CampaignMemoryEngine: def __init__(self, persist_directory: str ./storage/chroma_db, openai_api_key: Optional[str] None): 初始化记忆引擎。 Args: persist_directory: ChromaDB持久化目录 openai_api_key: OpenAI API密钥如为None则从环境变量读取 self.persist_directory persist_directory # 初始化嵌入模型 - 用于将文本转换为向量 self.embeddings OpenAIEmbeddings( modeltext-embedding-3-small, openai_api_keyopenai_api_key or os.getenv(OPENAI_API_KEY) ) # 初始化向量数据库客户端 self.vectorstore Chroma( persist_directorypersist_directory, embedding_functionself.embeddings, ) # 初始化文本分割器用于将长文本切分成块 self.text_splitter RecursiveCharacterTextSplitter( chunk_size1000, # 每个块约1000字符 chunk_overlap200, # 块之间重叠200字符保持上下文 separators[\n\n, \n, 。, , , , , , ] ) # 初始化LLM用于信息提取和答案合成 self.llm ChatOpenAI( modelgpt-3.5-turbo, temperature0.1, # 低温度保证输出稳定 openai_api_keyopenai_api_key or os.getenv(OPENAI_API_KEY) ) logger.info(fCampaignMemoryEngine 初始化完成向量库路径: {persist_directory}) def ingest_text(self, raw_text: str, source: DocumentSource, metadata: Optional[dict] None) - List[str]: 摄入原始文本进行分割、向量化并存储。 Returns: 生成的文本块ID列表 if metadata is None: metadata {} # 1. 分割文本 texts self.text_splitter.split_text(raw_text) logger.info(f文本分割完成共得到 {len(texts)} 个块。) # 2. 准备LangChain Document格式用于存入向量库 docs [] chunk_ids [] for i, text in enumerate(texts): # 为每个块创建唯一的ID和元数据 chunk_id f{source.value}_{hash(text) 0xFFFFFFFF} chunk_meta { chunk_id: chunk_id, source: source.value, chunk_index: i, **metadata # 合并传入的元数据 } doc LangchainDocument(page_contenttext, metadatachunk_meta) docs.append(doc) chunk_ids.append(chunk_id) # 3. 添加到向量数据库 self.vectorstore.add_documents(docs) logger.info(f成功将 {len(docs)} 个文本块存入向量数据库。) return chunk_ids def search_similar(self, query: str, k: int 5) - List[dict]: 语义搜索根据查询语句返回最相关的文本块。 Args: query: 自然语言查询 k: 返回最相关的k个结果 Returns: 包含文本内容和元数据的字典列表 # 使用向量数据库进行相似性搜索 docs_with_score self.vectorstore.similarity_search_with_score(query, kk) results [] for doc, score in docs_with_score: results.append({ content: doc.page_content, metadata: doc.metadata, relevance_score: float(score) # 分数越低表示越相似距离越近 }) logger.info(f搜索查询: {query}返回 {len(results)} 个结果。) return results def query_with_llm(self, query: str, k: int 5) - str: 高级查询结合语义搜索和LLM生成一个连贯的答案。 1. 搜索相关文本片段。 2. 将片段作为上下文提供给LLM。 3. 让LLM基于上下文回答问题。 # 1. 搜索相关上下文 context_chunks self.search_similar(query, kk) if not context_chunks: return 抱歉在我的记忆库中没有找到相关信息。 # 2. 构建LLM提示词 context_text \n\n---\n\n.join([f[来源: {chunk[metadata].get(source, 未知)}]\n{chunk[content]} for chunk in context_chunks]) prompt f你是一个专业的TTRPG战役记忆助手。请基于以下提供的战役记录片段回答玩家或主持人的问题。 如果信息不足请基于已有信息进行合理推断并说明这是推断。 如果信息矛盾请指出矛盾点。 请用清晰、有条理的方式回答。 相关战役记录 {context_text} 问题{query} 请根据以上记录回答问题 # 3. 调用LLM生成答案 from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser prompt_template ChatPromptTemplate.from_messages([ (system, 你是一个专业的TTRPG战役记忆助手。), (human, {prompt}) ]) chain prompt_template | self.llm | StrOutputParser() answer chain.invoke({prompt: prompt}) return answer4.3 构建Web查询接口使用FastAPI创建一个简单的Web服务提供查询功能。在app/main.py中# app/main.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List, Optional import uvicorn from app.core.memory_engine import CampaignMemoryEngine, DocumentSource from app.core.models import TextChunk app FastAPI(titleTable Canon Demo API, descriptionTTRPG战役记忆引擎演示API) # 全局初始化记忆引擎 memory_engine CampaignMemoryEngine() class IngestRequest(BaseModel): text: str source: DocumentSource metadata: Optional[dict] None class QueryRequest(BaseModel): question: str use_llm: bool True # 是否使用LLM合成答案若为False则只返回相关片段 top_k: int 5 app.post(/ingest/, summary摄入战役文本) async def ingest_text(request: IngestRequest): 将一段文本如会话记录、笔记存入记忆引擎。 try: chunk_ids memory_engine.ingest_text(request.text, request.source, request.metadata) return {message: 文本摄入成功, chunk_ids: chunk_ids} except Exception as e: raise HTTPException(status_code500, detailf文本摄入失败: {str(e)}) app.post(/query/, summary查询战役记忆) async def query_memory(request: QueryRequest): 向记忆引擎提问。 try: if request.use_llm: # 使用LLM合成答案的高级查询 answer memory_engine.query_with_llm(request.question, krequest.top_k) return {answer: answer, query_type: llm_synthesized} else: # 仅返回相似性搜索结果 results memory_engine.search_similar(request.question, krequest.top_k) return {results: results, query_type: semantic_search} except Exception as e: raise HTTPException(status_code500, detailf查询失败: {str(e)}) app.get(/health) async def health_check(): return {status: healthy} if __name__ __main__: uvicorn.run(app, host0.0.0.0, port8000)4.4 运行与验证设置环境变量在终端中设置你的OpenAI API密钥。export OPENAI_API_KEY你的-api-key-here # Windows (PowerShell): $env:OPENAI_API_KEY你的-api-key-here启动服务cd table_canon_demo python -m app.main服务将在http://localhost:8000启动。摄入数据使用curl或 Postman 测试/ingest/接口。curl -X POST \ http://localhost:8000/ingest/ \ -H Content-Type: application/json \ -d { text: 在今天的冒险中队伍来到了被遗忘的银月城。他们在酒馆遇到了一个名叫‘灰袍’的矮人铁匠他透露城外的古墓最近有异常动静并警告说里面可能有亡灵生物。游荡者艾莉丝偷偷记下了古墓的大致位置。, source: session_log, metadata: {session_date: 2023-10-27, gm: Mike} }进行查询测试/query/接口。# 简单语义搜索 curl -X POST \ http://localhost:8000/query/ \ -H Content-Type: application/json \ -d { question: 银月城有什么信息, use_llm: false } # 使用LLM的高级问答 curl -X POST \ http://localhost:8000/query/ \ -H Content-Type: application/json \ -d { question: 总结一下关于古墓的线索。, use_llm: true }对于第二个查询你将得到一个由LLM生成的、基于上下文的连贯回答而不是简单的文本片段列表。4.5 结果说明通过以上步骤我们成功构建了一个具备最核心功能的TTRPG战役记忆引擎。它能够存储将非结构化的游戏文本分割、向量化后存储。检索根据语义相似度快速找到相关文本段落。问答结合LLM对检索到的信息进行总结、推理以自然语言回答复杂问题。这已经解决了“找不到”和“记不住”的核心痛点。你可以通过不断摄入游戏日志来丰富这个记忆库。5. 常见问题与排查思路在开发和运行此类AI应用时你可能会遇到以下问题问题现象可能原因排查与解决思路启动服务时报错OpenAI API key not provided1. 环境变量未设置。2. 代码中未正确读取。1. 检查OPENAI_API_KEY环境变量是否已设置并生效echo $OPENAI_API_KEY。2. 在代码初始化时尝试直接传入openai_api_key参数进行测试。调用/ingest/或/query/接口速度很慢1. 网络问题导致调用OpenAI API延迟高。2. 文本过长分割或嵌入耗时。3. ChromaDB首次运行或数据量大。1. 检查网络连接考虑使用OpenAI的代理或确保区域正确。2. 调整text_splitter的chunk_size避免单个块过大。3. 对于生产环境考虑异步处理摄入任务或使用更高效的向量数据库如Pinecone、Qdrant。查询结果不相关1. 嵌入模型不适合该类型文本。2. 文本分割不合理破坏了上下文。3. 查询语句过于模糊或复杂。1. 尝试不同的嵌入模型如text-embedding-3-large。2. 调整分割器的chunk_size和separators对于对话文本可以尝试按说话人分割。3. 引导用户提出更具体的问题或在查询前对用户问题进行简单的重写或扩展。LLM生成的答案存在“幻觉”LLM基于不完整的上下文进行了过度推断。1. 增加检索返回的上下文数量 (top_k)。2. 在提示词Prompt中加强指令要求“严格基于提供的信息回答不要编造”。3. 在返回答案的同时附上引用的源文本片段让用户自行判断。内存或磁盘占用过高1. 摄入的文本数据量极大。2. ChromaDB索引文件增长。1. 定期清理或归档旧的、不常用的会话数据。2. 考虑使用支持标量量化的向量数据库或将向量数据库部署在独立服务器。6. 最佳实践与工程建议要将这个演示项目转化为一个健壮、可用的工具需要考虑以下工程化实践数据安全与隐私本地化部署对于涉及未公开战役设定的敏感信息强烈建议使用本地部署的LLM如通过ollama运行Llama 3、Mistral和嵌入模型如sentence-transformers库中的模型避免数据上传至第三方API。访问控制为Web服务添加API密钥认证或用户登录系统防止未授权访问。数据加密对存储在磁盘上的向量数据库和元数据库进行加密。性能优化异步处理使用asyncio和FastAPI的异步端点来处理耗时的LLM调用和向量搜索避免阻塞请求。缓存机制对常见的查询结果进行缓存减少对LLM和向量数据库的重复调用。批处理摄入支持上传整个日志文件在后台异步进行分割、向量化和存储。功能增强实体与关系抽取定期运行后台任务使用LLM从新摄入的文本中提取实体和关系构建真正的知识图谱。可以使用LangChain的LLMChain或OpenAI Functions来输出结构化数据。多模态支持结合OCR技术摄入游戏地图的扫描图片并让LLM描述图片内容将其转化为文本信息存入引擎。版本管理与回溯为战役记忆库添加版本概念允许GM回溯到某个特定游戏日期的状态。提示词工程系统提示词定制为LLM设计更专业的系统角色例如“你是一位严谨的战役档案管理员只陈述事实不做创造性叙述”。少样本学习在提示词中提供几个正确提取信息或回答问题的例子引导LLM遵循特定格式和风格。元数据过滤在搜索时允许用户按来源如“只搜索GM笔记”、时间范围等元数据进行过滤提升查询精度。可观测性与监控日志记录详细记录每一次摄入和查询包括耗时、Token使用量、查询词和返回结果数量用于分析和优化。成本监控如果使用付费API需要监控Token消耗设置预算警报。通过遵循这些实践你可以构建一个不仅功能强大而且稳定、安全、可维护的TTRPG AI辅助工具真正成为你游戏桌上不可或缺的“记忆外挂”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价