目录

一、Deepseek API接入流程与配置

二、改用 .env 文件管理 API Key 的做法

三、LangGraph状态图集成

四、后续计划


一、Deepseek API接入流程与配置

  1. 在DeepSeek官网(DeepSeek 开放平台)注册账号并申请API Key。进入控制台后,点击“创建API Key”,系统会生成一串以“sk-”开头的密钥,这个密钥需要妥善保存,因为后续页面不会再完整显示。
  2. story_engine.py 中新增的 LLMClient 类
    # backend/story_engine.py
    import requests
    
    class LLMClient:
        def __init__(self, api_key: str = None, base_url: str = None, model: str = None):
            self.api_key = api_key or os.getenv("DEEPSEEK_API_KEY", "sk-")#填入自己的api key
            self.base_url = base_url or os.getenv("LLM_BASE_URL", "https://api.deepseek.com/v1")
            self.model = model or os.getenv("LLM_MODEL", "deepseek-chat")
            self.headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
    
        def generate(self, messages: List[Dict[str, str]], temperature: float = 0.8) -> str:
            url = f"{self.base_url}/chat/completions"
            payload = {"model": self.model, "messages": messages, "temperature": temperature, "max_tokens": 800}
            try:
                resp = requests.post(url, headers=self.headers, json=payload, timeout=30)
                resp.raise_for_status()
                return resp.json()["choices"][0]["message"]["content"]
            except Exception as e:
                print(f"LLM 调用失败: {e}")
                return "(大模型暂时无法响应,请稍后再试。)"

    这段代码定义了一个 LLMClient 类,用于与 DeepSeek 大模型 API 进行交互。类的 __init__ 方法接收可选的 API Key、服务地址和模型名称,如果没有传入则从环境变量读取,并提供了一个硬编码的默认 Key(仅作临时测试用)。它构造了请求头,包含认证信息。generate 方法是核心功能:接收一个消息列表(通常包含 system 和 user 角色)和 temperature 参数,向 DeepSeek 的 /chat/completions 端点发送 POST 请求,等待模型返回生成的文本。如果请求成功,从响应的 JSON 中提取出 choices[0].message.content 作为结果返回;如果发生任何异常(网络错误、超时、API 报错等),则捕获异常并打印错误信息,同时返回一个友好的降级提示字符串,保证程序不会因 API 调用失败而崩溃。

  3. StoryEngine 类初始化时接收 llm_client 参数并存储

    class StoryEngine:
        def __init__(self, llm_client: LLMClient = None):
            self.story_templates = self.load_story_templates()
            self.llm = llm_client or LLMClient()   # 新增
            self.memory = MemorySaver()
            self.graph = self.build_graph()
  4. intent_parser 节点改为使用 LLM
    def intent_parser(self, state: StoryState) -> StoryState:
        user_input = state["messages"][-1]["content"]
        system_prompt = """你是一个游戏意图识别器。用户输入一段自然语言,你需要判断用户的意图(intent)和目标(target)。
    意图必须是以下之一:attack, talk, explore, use, move, check。
    目标是从输入中提取的关键对象(如敌人名、物品名、方向等),如果没有明确目标则为空字符串。
    输出格式:{"intent": "attack", "target": "哥布林"} 只输出 JSON,不要有其他内容。"""
        user_prompt = f"用户输入:{user_input}"
        try:
            resp = self.llm.generate([{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}], temperature=0.2)
            data = json.loads(resp.strip())
            state["intent"] = data.get("intent", "explore")
            state["intent_target"] = data.get("target", "")
        except:
            # 降级为关键词匹配
            state = self._fallback_intent_parser(state)
        return state

    这段代码的作用是将玩家输入的自然语言转换为结构化的游戏指令。首先从状态中提取用户最新的一条消息作为输入,然后构造一个系统提示词,要求大模型从“attack、talk、explore、use、move、check”六种意图中选择一种,并提取出目标对象(如敌人名、物品名),输出格式为严格的 JSON。接着调用 self.llm.generate 发送给大模型,temperature 设为 0.2 以保证输出的确定性。如果成功解析 JSON,就将意图和目标存入状态中;如果解析失败(例如大模型返回了非 JSON 内容或网络异常),则捕获异常并降级到关键词匹配的 _fallback_intent_parser 方法,确保游戏在任何情况下都能识别玩家意图。最后返回更新后的状态,供后续的逻辑裁判节点使用。

  5. narrative_director 节点改为使用 LLM 生成叙事(包含上下文、历史压缩、摘要更新)
    def narrative_director(self, state: StoryState) -> StoryState:
        template = self.story_templates.get(state["story_id"], self.story_templates["fantasy_001"])
        
        # 构建完整的历史对话(如果太长,使用摘要)
        full_history = state.get("messages", [])
        history_text = self._compress_history(full_history, state.get("summary", ""))
        
        # 构造系统提示词
        system_prompt = f"""你是《{template['title']}》的叙事导演。
    【世界观】{template['background']}
    【当前场景】{state.get('current_description', template['scene_description'])}
    【主线任务】{state['task_progress'].get('main', '')}
    
    叙事要求:
    - 使用第二人称“你”,语言生动细腻。
    - 包含“五要素”:心理活动、动作细节、环境变化、对话、状态提示。
    - 严格基于以下给出的【行动结果】和【角色状态】来描写,不要自己编造数值变化。
    - 保持奇幻或科幻风格一致。
    - 回复长度150~300字。"""
    
        user_prompt = f"""【用户行动】{state['messages'][-1]['content']}
    【意图】{state['intent']} → 目标:{state['intent_target']}
    【行动结果】{state.get('action_result_desc', '')} (判定:{state['last_action_result']})
    
    【角色状态】
    - 生命: {state['attributes'].get('hp', 100)}/{state['attributes'].get('mp', 100) if 'mp' in state['attributes'] else state['attributes'].get('energy', 100)}
    - 力量:{state['attributes'].get('strength',0)} 智力:{state['attributes'].get('intelligence',0)} 金币:{state['attributes'].get('money',0)}
    - 背包: {', '.join(state['inventory']) if state['inventory'] else '空'}
    - NPC好感: {state['relationships']}
    
    【剧情回顾】
    {history_text}
    
    请续写一段剧情。"""
    
        llm_messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}
        ]
        narrative = self.llm.generate(llm_messages, temperature=0.8)
        
        # 附加状态条(规则生成,非LLM)
        hp = state['attributes'].get('hp', 100)
        hp_emoji = "alive" if hp > 50 else "dead"
        status_bar = f"\n\n【状态】{hp_emoji}生命:{hp} | 力量:{state['attributes'].get('strength',0)} | 智力:{state['attributes'].get('intelligence',0)} | 金币:{state['attributes'].get('money',0)}"
        full_narrative = narrative + status_bar
        
        # 更新状态
        state["messages"].append({"role": "assistant", "content": full_narrative, "timestamp": datetime.now().isoformat()})
        state["current_description"] = full_narrative
        if "action_result_desc" in state:
            del state["action_result_desc"]
        
        # 定期更新摘要(每5轮)
        if state["turn_count"] % 5 == 0:
            state["summary"] = self._update_summary(state)
        
        return state

    这段代码用于生成沉浸式的剧情描述。它首先获取故事模板和当前状态,将历史对话压缩或摘要以避免超长。然后构造系统提示词(设定叙事风格、世界观、当前场景和任务)和用户提示词(包含玩家行动、意图、逻辑裁判的结果、角色属性和剧情回顾),一起发送给大模型生成 150~300 字的叙事文本。生成后,再附加一条由规则生成的状态条(生命值、力量、智力、金币)。最后将完整的回复存入消息历史,更新当前场景描述,并每隔 5 回合调用大模型生成一次剧情摘要,用于后续的上下文压缩。

  6. 新增辅助方法 _compress_history 和 _update_summary
    def _compress_history(self, messages: List[Dict], existing_summary: str = "") -> str:
        full_text = "\n".join([f"{m['role']}: {m['content']}" for m in messages])
        if len(full_text) < 4000:
            return full_text
        recent = messages[-10:]
        recent_text = "\n".join([f"{m['role']}: {m['content']}" for m in recent])
        if existing_summary:
            return f"【历史摘要】{existing_summary}\n\n【最近剧情】{recent_text}"
        else:
            return recent_text
    
    def _update_summary(self, state: StoryState) -> str:
        history = "\n".join([f"{m['role']}: {m['content']}" for m in state["messages"]])
        prompt = f"请用200字以内总结以下游戏剧情的关键发展、获得的重要物品、遇到的主要NPC和做出的重要选择:\n{history}"
        try:
            summary = self.llm.generate([{"role": "user", "content": prompt}], temperature=0.5)
            return summary
        except:
            return ""

    _compress_history 方法首先将所有消息拼接成完整文本,如果总长度小于 4000 字符就直接返回;否则只保留最近 10 条消息,并判断是否存在已有的摘要,若有则将摘要与最近剧情拼接返回,从而避免超出模型上下文窗口。_update_summary 方法在每 5 个回合被调用,它把全部对话历史发送给大模型,要求用 200 字以内总结关键剧情发展、获得的重要物品、遇到的主要 NPC 和重要选择,生成的摘要会存入状态供后续压缩使用。如果调用失败则返回空字符串,不影响游戏继续运行。

  7. app.py 中初始化 LLM 客户端并传入故事引擎

    # backend/app.py
    import os
    from story_engine import StoryEngine, LLMClient   # 导入新增的 LLMClient
    
    # 硬编码 API Key(当前做法)
    DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "sk-") #填入自己的api key
    llm_client = LLMClient(api_key=DEEPSEEK_API_KEY)
    engine = StoryEngine(llm_client=llm_client)

二、改用 .env 文件管理 API Key 的做法

考虑到我们目前只是临时接入大模型 API 进行效果验证,尚未获得老师提供的正式密钥,而项目的最终架构是基于 LangGraph 的多智能体协同框架,不同节点(如意图解析、叙事导演、历史摘要等)可能会调用不同的大模型甚至不同厂商的服务。如果继续将 API Key 硬编码在代码中,不仅会导致密钥泄露风险,未来也难以灵活切换模型或管理多个密钥。因此,我们决定改用 .env 文件来集中管理所有环境变量(包括 API Key、服务地址、模型名称等),并通过 python-dotenv 加载。这种方式既符合开发规范,也为我们后续扩展多模型协同打下了基础。

  1. 在项目根目录创建 .env 文件(注意等号前后不要有空格)

    DEEPSEEK_API_KEY=sk-xxx  #填写自己的密钥
    LLM_BASE_URL=https://api.deepseek.com/v1  
    LLM_MODEL=deepseek                    
  2. 修改 app.py 和 story_engine.py 中的加载方式
    在 app.py 中添加:
    import os
    from dotenv import load_dotenv
    from .story_engine import StoryEngine   
    
    load_dotenv()
    DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
    if not DEEPSEEK_API_KEY:
        raise ValueError("请在 .env 文件中设置 DEEPSEEK_API_KEY")
    
    def serialize_state(state: Dict) -> Dict:
        # 返回更多字段,包括 generation_source, generation_error, generation_model, generation_usage
    
    def to_legacy_ending_key(ending_key: str) -> str:
        # 映射结局类型
    
    

    注意其中的 story_engine 需要使用相对导入,若使用直接导入将出现以下错误:

    在 story_engine.py 中添加:

    import os
    from dotenv import load_dotenv
    from typing import TypedDict, List, Dict, Any, Optional
    from langgraph.graph import StateGraph, END
    try:
        from langgraph.checkpoint.memory import MemorySaver
    except ImportError:
        from langgraph.checkpoint import MemorySaver
    import requests  
     
    class LLMClient:
        def __init__(self, api_key: str = None, base_url: str = None, model: str = None):
            # 初始化LLM客户端
            
        
        def generate_narrative(self, prompt: str, max_tokens: int = 500) -> Optional[str]:
            # 直接调用DeepSeek HTTP接口生成故事叙述
     
        
    class StoryEngine:
            def intent_parser(self, state: Dict) -> Dict:
            # 将用户输入转换为结构化指令
     
            def narrative_director(self, state: Dict) -> Dict:
            # 调用LLM生成沉浸式剧情描述

    需要注意的是,仅调用大模型没办法得到我们想要的回答,还需要将故事本身的背景和用户的交互传入,具体的修改代码如下:

    prompt = f"""你是一个沉浸式角色扮演游戏的故事叙述者。请根据以下信息生成一段引人入胜的故事描述(2-3句话,不超过100个字):

    修改为:

    # 收集最近对话上下文,帮助模型保持剧情连续性
            history_lines = []
            for msg in state.get("messages", [])[-6:]:
                role = "玩家" if msg.get("role") == "user" else "系统"
                content = str(msg.get("content", "")).replace("\n", " ").strip()
                if content:
                    history_lines.append(f"{role}: {content[:120]}")
            recent_history = "\n".join(history_lines) if history_lines else "无"
            player_input = ""
            if state.get("messages"):
                last_msg = state["messages"][-1]
                if last_msg.get("role") == "user":
                    player_input = str(last_msg.get("content", "")).strip()
            
            prompt = f"""你是一个沉浸式角色扮演游戏的故事叙述者。请根据以下信息生成下一段剧情。

    通过修改 narrative_director 方法中调用大模型的提示词(设定不同的输出要求,提供更详细的提示词等等),或者添加方法来限制大模型的输出,可以得到不同的回答,如下图所示:

  3. 检查用量信息可以看到是否真正调用大模型,如下图所示:
  4. 如果要同时支持另一个大模型,也就是添加另一个大模型的 api key,需要在 .env 中添加新模型的变量,具体的形式与添加 deepseek api 时相同(例如,添加千问的 api 可以写Qwen_API_KEY=sk-xxx等);然后修改 LLMClient 类,增加 provider 参数,或创建新的客户端类(如 QwenClient)。接着在 StoryEngine 中初始化多种客户端(self.llm_deepseek = LLMClient(provider="deepseek") self.llm_qewn = QwenClient()),在具体的节点中选择使用哪个客户端。例如,intent_parser 想用 deepseek,narrative_director 想用 qwen,则在对应方法中调用 self.llm_deepseek.generate() 或 self.llm_qwen.generate()。

三、LangGraph状态图集成

当前的 story_engine.py 已经写了三个独立的节点函数(意图解析、逻辑裁判、叙事导演),并且在 process_action 中手动按顺序调用它们。这种方式虽然能运行,但存在几个问题:首先,如果将来想在中间加入条件分支(比如生命值过低时直接跳转到结局),就需要修改 process_action 的代码,添加一堆 if 判断,节点之间的耦合会越来越紧。其次,每个玩家的游戏状态都存储在内存字典中,一旦服务重启就会丢失,无法实现真正的“存档/读档”。最后,节点之间只能线性执行,无法支持循环或并行。
因此,我们使用 LangGraph 来解决这些问题。它允许将每个节点定义为一个独立的函数,然后用一张“图”来描述节点之间的连接关系(顺序、条件跳转、循环等)。图编译后,只需要调用 graph.invoke(state, config),它会自动按照你定义的规则执行节点,并维护状态。更重要的是,LangGraph 内置了 MemorySaver 检查点机制,只要传入一个 thread_id,它就能自动保存和恢复这个会话的状态——相当于免费获得了存档功能。所以,所谓的“LangGraph 状态图集成”,就是把现有的三个节点函数“注册”到一张图上,然后用 invoke 来驱动整个流程。

具体的构建步骤如下:

  1. 导入 LangGraph 相关模块,在 story_engine.py 文件的顶部添加:
    from typing import TypedDict, Dict, Any, Optional, List
    from langgraph.graph import StateGraph, END
    from langgraph.checkpoint.memory import MemorySaver
  2. 定义 StoryState 类:
    class StoryState(TypedDict):
        story_id: str
        scene_id: str
        messages: List[Dict[str, str]]
        current_description: str
        attributes: Dict[str, int]
        inventory: List[str]
        relationships: Dict[str, int]
        task_progress: Dict[str, str]
        intent: str
        intent_target: str
        last_action_result: str
        turn_count: int
        ending_triggered: Optional[str]
        user_id: Optional[str]
        last_generation_source: Optional[str]
        last_generation_error: Optional[str]
        last_generation_usage: Optional[Dict]
        last_generation_model: Optional[str]
        action_result_desc: Optional[str]
  3. 在 StoryEngine.__init__ 中初始化 MemorySaver 并构建图:
    self.memory = MemorySaver()
    self.graph = self.build_graph()
  4. 编写 build_graph 方法:
    def build_graph(self):
        workflow = StateGraph(StoryState) 
        # 添加三个节点
        workflow.add_node("intent_parser", self.intent_parser)
        workflow.add_node("logic_referee", self.logic_referee)
        workflow.add_node("narrative_director", self.narrative_director)
        # 设置入口
        workflow.set_entry_point("intent_parser")
        # 添加顺序边
        workflow.add_edge("intent_parser", "logic_referee")
        workflow.add_edge("logic_referee", "narrative_director")
        workflow.add_edge("narrative_director", END)
        # 编译图,并传入 checkpointer 以支持状态持久化
        return workflow.compile(checkpointer=self.memory)
  5. 修改 process_action 方法,使用 graph.invoke,将原来的手动循环代码替换为:
    def process_action(self, state: Dict, user_input: str, thread_id: str = None) -> Dict:
        # 添加用户消息
        state["messages"].append({
            "role": "user",
            "content": user_input,
            "timestamp": datetime.now().isoformat()
        })
        
        # 如果没有提供 thread_id,则生成一个
        if not thread_id:
            thread_id = f"{state.get('user_id', 'anonymous')}_{state.get('story_id', 'default')}"
        
        config = {"configurable": {"thread_id": thread_id}}
        
        # graph.invoke 需要传入配置,并且返回更新后的状态
        result = self.graph.invoke(state, config=config)
        return result
  6. 修改 app.py 中调用 process_action 的方式:
    new_state = engine.process_action(state, req.user_input, thread_id=req.session_id)
  7. 运行代码,可以看出大模型输出的内容与角色属性建立连接:

四、后续计划

完成了大模型 API 接入与 LangGraph 状态图集成后,当前的故事引擎已经具备了智能叙事和工作流管理的基础能力。但整体功能还比较基础,比如角色的差异化体验、长期的养成与成长反馈几乎还是空白。接下来,我们将进入功能细化阶段,我会重点实现角色成长模块,包括角色创建、属性系统、天赋与出身选择、多周目数据继承以及配套的成就体系。这些内容需要完成完整的前后端开发与数据库设计,让玩家真正拥有属于自己的游戏历程。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐