山东大学软件工程2023级创新项目实训 | 二、初步技术学习与StoryEcho项目框架搭建
时间: 2026年3月下旬 - 4月上旬
在项目选题正式确定为“StoryEcho:基于大模型的沉浸式互动叙事平台”并完成详细任务书撰写后,我们 FateWeaver 团队进入了第二个关键阶段——技术预研与项目框架雏形搭建。这段时间的核心目标是学习项目所需的技术栈,并产出一个可运行、包含核心交互逻辑的最小可行性产品框架,为后续成员并行开发奠定基础。
目录
3.4 前端核心功能 (app.js + index.html)
一、技术选型确认与团队学习
在正式编码之前,我们花了一周左右的时间进行技术调研和学习。以下是大模型根据我们项目书内容推荐的技术框架。

虽然团队对 Web 开发有一定基础,但要构建一个涉及 AI Agent 协作、复杂状态管理、长程上下文处理 的互动叙事系统,仍存在明显的知识盲区。以下是我们在这一阶段重点学习和了解的技术内容。
1.1 前端技术栈学习
经过讨论,我们决定采用 Vue 3 作为前端框架,主要基于以下考量:
-
组件化开发:互动叙事平台需要大量的弹窗组件(角色创建、结局画廊、任务日志等),Vue 的单文件组件模式能够帮助我们更好地组织代码结构。
-
响应式数据绑定:游戏状态(如角色属性、背包物品、NPC好感度)需要实时反映在界面上,Vue 的响应式系统可以让 UI 自动跟随数据变化,减少手动 DOM 操作的复杂度。
-
Composition API:相较于 Options API,Composition API 能够更灵活地组织逻辑代码,特别是当单个组件逻辑变得复杂时(如主游戏界面同时管理叙事窗口、侧边栏状态、快捷操作等多个模块)。
我们还学习了如何在不使用构建工具的情况下,通过 CDN 方式快速引入 Vue 和周边库(如 Axios)。这种方式降低了初期环境配置的复杂度,让我们能够更快地验证想法。
1.2 后端技术栈学习
后端方面,我们选择了 FastAPI 框架,主要学习内容包括:
-
异步请求处理:由于 AI 生成剧情可能需要数秒的响应时间,异步框架能够避免请求阻塞,提升并发处理能力。
-
自动 API 文档生成:FastAPI 内置的 Swagger UI 可以自动生成接口文档(
/docs端点),这对团队协作调试非常有帮助。 -
CORS 跨域配置:由于前后端分离开发,前端运行在
localhost:8080,后端运行在localhost:8000,需要正确配置 CORS 中间件才能让浏览器允许跨域请求。
1.3 LangGraph 与 AI Agent 架构学习
这是本项目的核心技术难点。传统上,如果直接让一个大模型同时负责“理解用户意图、计算数值变化、生成文学描述”,容易出现以下问题:
-
指令漂移:模型可能在生成剧情时忽略了之前设定的人物状态(如明明没血了还写勇猛战斗)。
-
数学不准确:大模型不擅长精确的数值计算(如 HP 扣减、概率判定)。
-
上下文遗忘:长程交互中容易忘记早期埋下的伏笔。
通过查阅相关资料,我们了解到 LangGraph 是一个用于构建多智能体协作流程的框架。它的核心思想是:
-
将复杂任务拆分为多个独立的“节点”,每个节点负责一个单一职责(如意图识别、逻辑判定、文本生成)。
-
通过“有向无环图”定义节点之间的流转顺序。
-
使用“状态对象”在节点间传递数据。
我们设想未来可以将 StoryEcho 的叙事流程建模为以下 Agent 节点:
-
意图解析 Agent:接收用户自由文本输入,识别用户的真实意图(想攻击、想对话、想探索等),输出结构化指令。
-
逻辑裁判 Agent:根据结构化指令和当前角色属性,进行数值演算(如攻击是否命中、造成多少伤害、好感度如何变化)。
-
叙事导演 Agent:基于逻辑裁判的结果,生成符合当前情境的沉浸式剧情描述。
这种架构的优势在于:将“逻辑”与“文学”解耦。逻辑裁判专注于精确的数学计算和规则判定,叙事导演专注于根据结果生成优美连贯的文字。两者通过状态对象协作,既能保证游戏规则的严谨性,又能发挥大模型的创造力。但是由于项目成本和技术难度,最终落实可行性不确定。
1.4 状态管理思想学习
我们还学习了如何设计一个结构化的“游戏状态对象”。在互动叙事中,需要追踪的信息非常多样:
-
角色属性(HP、MP、力量、智力等)
-
背包物品列表
-
NPC 好感度
-
任务进度
-
对话历史
我们学习了将这些信息统一封装在一个 gameState 对象中的设计模式。这样,无论是前端展示还是后端逻辑处理,都可以通过读写同一个状态对象来完成,避免了数据分散导致的不一致问题。
二、框架雏形搭建:从反复调试到确立雏形
完成初步技术学习后,我们进入了框架搭建阶段。这一过程并非一帆风顺,而是经历了多次尝试和调整。
2.1 线下共同讨论与各自尝试
在4月初的一周里,我们采取了“先各自探索、后集中整合”的工作模式。每位成员都在自己的电脑上尝试搭建一套可运行的前后端连接环境,并借助 DeepSeek、ChatGPT 等大模型来辅助解决遇到的问题。

大家遇到的问题主要集中在:
-
环境配置问题:Python 依赖包的版本兼容性(特别是
uvicorn和fastapi的版本匹配)、Vue CDN 资源加载失败等。 -
跨域请求失败:前端请求后端接口时浏览器报 CORS 错误,需要正确配置 FastAPI 的
CORSMiddleware。 -
前后端数据格式不一致:后端返回的 JSON 结构与前端期望的字段名不匹配,导致页面渲染空白。
-
启动脚本设计:需要一个统一的启动脚本
run.py,能够一键启动后端服务并提示前端访问方式。
在各自调试的过程中,我们不断通过大模型获取帮助。例如,当遇到 CORS 问题时,我们将错误信息粘贴给 DeepSeek,它会给出配置代码示例;当遇到无法运行,环境不适配时,大模型给出对应解决方案。

2.2 确定最终可运行框架
经过多次线下碰头讨论和代码比对,我们最终选定了团队中可以运行、代码结构最清晰的一套版本作为项目框架。这个框架具备以下特点:
-
一键启动:运行
python run.py即可启动后端服务,控制台会清晰打印访问地址和 API 文档地址。 -
前后端解耦:前端纯静态文件,可通过 Live Server 或 Python HTTP Server 独立运行,便于调试。
-
Mock 数据支持:即使不接入大模型 API,系统也能基于内置的简化规则返回响应,方便前端开发独立进行。
-
完整的状态流转:从故事选择到角色创建(Mock 版本),再到主游戏循环,整个流程已经打通。
三、现有项目雏形功能解析
目前的代码框架已经成功支撑起了两个核心界面:故事大厅 和 主叙事对话界面。以下结合代码详细介绍已实现的主要功能。


3.1 统一启动入口 (run.py)
为了方便团队成员和后续演示,我们编写了统一的启动脚本。该脚本负责启动 FastAPI 后端服务,并在控制台输出清晰的访问指引。
#!/usr/bin/env python
import subprocess
import sys
import os
def main():
print("=" * 50)
print("StoryEcho 沉浸式互动叙事平台")
print("=" * 50)
# 启动后端
print("\n🚀 启动后端服务...")
backend_process = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "backend.app:app", "--host", "0.0.0.0", "--port", "8000", "--reload"],
cwd=os.path.dirname(os.path.abspath(__file__))
)
print("✅ 后端服务已启动: http://localhost:8000")
print("📖 API文档: http://localhost:8000/docs")
# 提示前端启动方式
print("\n🌐 前端访问方式:")
print(" 1. 使用Live Server打开 frontend/index.html")
print(" 2. 或使用Python HTTP服务器: cd frontend && python -m http.server 8080")
print(" 3. 访问: http://localhost:8080")
print("\n按 Ctrl+C 停止服务...")
try:
backend_process.wait()
except KeyboardInterrupt:
print("\n正在停止服务...")
backend_process.terminate()
backend_process.wait()
print("服务已停止")
if __name__ == "__main__":
main()
3.2 后端 API 服务 (app.py)
后端基于 FastAPI 提供了以下核心接口:
(1)获取故事列表 (GET /api/stories)
@app.get("/api/stories")
async def get_stories():
stories = []
for sid, template in engine.story_templates.items():
stories.append({
"story_id": sid,
"title": template["title"],
"genre": template["genre"],
"background": template["background"],
"difficulty": "中等"
})
return {"stories": stories}
该接口从 StoryEngine 的故事模板库中读取所有可用剧本,返回给前端展示。目前内置了“迷雾森林”和“赛博2077”两个故事。
(2)开始游戏 (POST /api/story/start)
@app.post("/api/story/start")
async def start_story(req: StartRequest):
session_id = str(uuid.uuid4())
state = engine.start_story(req.story_id, req.user_id)
active_sessions[session_id] = state
return {
"session_id": session_id,
"state": {
"messages": state["messages"],
"attributes": state["attributes"],
"inventory": state["inventory"],
"task_progress": state["task_progress"],
"turn_count": state["turn_count"]
}
}
该接口接收故事 ID 和用户 ID,创建一个新的游戏会话,返回唯一的 session_id 和初始游戏状态。
(3)处理用户行动 (POST /api/story/action)
@app.post("/api/story/action")
async def process_action(req: ActionRequest):
if req.session_id not in active_sessions:
raise HTTPException(status_code=404, detail="会话不存在")
state = active_sessions[req.session_id]
new_state = engine.process_action(state, req.user_input)
active_sessions[req.session_id] = new_state
result = {
"state": {
"messages": new_state["messages"],
"attributes": new_state["attributes"],
"inventory": new_state["inventory"],
"task_progress": new_state["task_progress"],
"turn_count": new_state["turn_count"]
},
"session_id": req.session_id
}
if new_state.get("ending_triggered"):
result["ending"] = new_state["ending_triggered"]
return result
这是核心交互接口。接收用户输入文本和会话 ID,调用 StoryEngine.process_action() 处理逻辑,返回更新后的游戏状态。注意,目前 StoryEngine 是基于规则匹配的简化版本,下一阶段将被替换为调用大模型 API 的 LLMStoryEngine。
3.3 故事引擎 (story_engine.py)
目前的故事引擎是一个基于关键词匹配的简化实现,用于模拟游戏逻辑,方便前端独立开发和调试。
def process_action(self, state: Dict, user_input: str) -> Dict:
# 添加用户消息
state["messages"].append({"role": "user", "content": user_input})
state["turn_count"] += 1
# 解析意图
text = user_input.lower()
response = ""
# 根据不同意图生成响应
if "探索" in text or "周围" in text:
response = self.handle_explore(state)
elif "背包" in text or "物品" in text:
response = self.handle_inventory(state)
elif "攻击" in text or "战斗" in text:
response = self.handle_attack(state)
elif "治疗" in text or "药水" in text:
response = self.handle_heal(state)
elif "状态" in text or "属性" in text:
response = self.handle_status(state)
else:
response = self.handle_general(state, text)
# 添加状态显示
hp = state["attributes"]["hp"]
hp_emoji = "❤️" if hp > 70 else "💛" if hp > 30 else "💔"
status = f"\n\n【状态】{hp_emoji} HP:{hp}/100 | 💪力量:{state['attributes']['strength']} | 🧠智力:{state['attributes']['intelligence']}"
full_response = response + status
# 添加AI回复
state["messages"].append({"role": "assistant", "content": full_response})
state["current_description"] = full_response
# 检查结局
if state["attributes"]["hp"] <= 0:
state["ending_triggered"] = "bad_ending"
elif state["turn_count"] >= 15:
state["ending_triggered"] = "good_ending"
return state
各处理函数实现了基本的游戏逻辑:
def handle_explore(self, state: Dict) -> str:
discoveries = [
("你发现了一些野果!生命值恢复了10点。", {"hp": 10}),
("你在地上捡到了20枚金币。", {"money": 20}),
("你找到了一瓶治疗药水!", {}),
("你发现了一条新的小径,但没有什么收获。", {})
]
desc, changes = random.choice(discoveries)
for key, val in changes.items():
if key in state["attributes"]:
state["attributes"][key] = min(100, state["attributes"][key] + val)
if "治疗药水" in desc and "治疗药水" not in state["inventory"]:
state["inventory"].append("治疗药水")
return f"🔍 {desc}"
3.4 前端核心功能 (app.js + index.html)
前端采用 Vue 3 的 Composition API 组织代码,核心响应式数据如下:
const gameStarted = ref(false); // 是否已开始游戏
const stories = ref([]); // 故事列表
const selectedStory = ref(null); // 选中的故事
const gameState = ref({ // 游戏状态
scene_id: '',
current_description: '',
messages: [],
attributes: {},
inventory: [],
relationships: {},
task_progress: {},
turn_count: 0
});
const userInput = ref(''); // 用户输入
const isLoading = ref(false); // 加载状态
const sessionId = ref(''); // 会话ID
(1)故事选择与游戏启动
const selectStory = (story) => {
selectedStory.value = story;
showCharacterCreation.value = true;
};
const startGameWithCharacter = async (characterData) => {
isLoading.value = true;
try {
const response = await axios.post(`${API_BASE}/api/story/start`, {
story_id: selectedStory.value.story_id,
user_id: 'user_' + Date.now(),
character: characterData
});
sessionId.value = response.data.session_id;
gameState.value = response.data.state;
gameStarted.value = true;
} catch (error) {
console.error('开始游戏失败:', error);
// 降级到 Mock 模式
startMockGame(selectedStory.value, characterData);
} finally {
isLoading.value = false;
}
};
(2)发送用户行动
const sendAction = async () => {
if (!userInput.value.trim() || isLoading.value) return;
const action = userInput.value;
userInput.value = '';
isLoading.value = true;
// 将用户消息添加到界面
gameState.value.messages.push({ role: 'user', content: action });
try {
const response = await axios.post(`${API_BASE}/api/story/action`, {
user_input: action,
session_id: sessionId.value
});
gameState.value = response.data.state;
if (response.data.ending) {
ending.value = true;
endingType.value = response.data.ending;
}
} catch (error) {
console.error('发送行动失败:', error);
mockResponse(action);
} finally {
isLoading.value = false;
}
};
(3)快捷操作
const quickAction = (action) => {
userInput.value = action;
sendAction();
};
前端界面通过 v-for 渲染消息列表,通过 v-model 绑定输入框,整体交互流程已经完整打通。
四、总结与下一阶段计划
经过这段时间的努力,我们完成了从技术学习到框架搭建的关键一步。目前的成果如下:
-
技术学习:团队成员对 Vue 3 Composition API、FastAPI 异步开发、LangGraph 多智能体架构思想有了基本了解,为后续深入开发打下了基础。
-
框架雏形:产出了一个可运行的基线版本,包含故事选择、主游戏循环、状态追踪等核心功能。
-
代码规范:确立了前后端数据交互的 JSON 格式规范,为后续并行开发提供了统一标准。
下一阶段的核心目标是:接入真实的 AI 大脑,验证对话流。
具体计划包括:
-
接入 API Key:在
app.py中编写新的LLMStoryEngine类,替换目前基于关键词匹配和if-else规则的模拟回复逻辑。该类将调用大模型 API(如 DeepSeek 或 OpenAI),并将模型返回的非结构化文本解析为符合我们gameState结构的 JSON 数据。 -
验证对话流:测试在接入大模型后,目前的 JSON 状态结构是否能被 AI 稳定生成和解析。重点关注
attributes中的数值变化(如 HP 减少、金币增加)和inventory的物品增删逻辑是否准确,确保“逻辑裁判”的精确性不被大模型的“文学生成”所干扰。
通过亲手搭建这个框架,我们深刻体会到“逻辑与叙事分离”架构的必要性。目前的框架为后续接入大模型、实现真正的 AI 驱动互动叙事体验做好了充分准备。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)