全栈开发知识库

07 · 项目三:赛博小镇从 0 到 1

目标:跑通并理解 code/chapter15/Helloagents-AI-Town —— 一个像素风办公室小镇游戏:用 WASD 走动,按 E 和三位 AI NPC(张三/Python工程师、李四/产品经理、王五/UI设计师)对话。NPC 有性格、有记忆、有好感度:你聊过的内容它下次还记得,你说话客气或刻薄,它对你的态度会真实变化。

技术要点:Godot 游戏引擎入门 + NPC 人设提示词 + MemoryManager 记忆系统(短期+长期)+ 好感度状态机 + 游戏与后端的 HTTP 通信。

预计 3~5 天。

0. 架构总览:游戏也只是一个"前端"

Godot游戏(GDScript)                        FastAPI后端(:8000)
┌────────────────────┐                    ┌─────────────────────────┐
│ player.gd  玩家移动 │   POST /chat       │ main.py     API路由      │
│ npc.gd     NPC交互  │ ─────────────────→ │ agents.py   NPC管理器     │
│ dialogue_ui.gd 对话框│ ←───────────────── │  ├ SimpleAgent ×3        │
│ api_client.gd ★通信 │   JSON响应         │  ├ MemoryManager ×3 记忆  │
└────────────────────┘   GET /npcs/status  │  └ RelationshipManager 好感度│
                                           │ state_manager.py 自主行为 │
                                           └─────────────────────────┘

和前两个项目结构完全同构:后端还是 FastAPI + SimpleAgent,只是"前端"从浏览器换成了游戏引擎。学完本项目你会彻底明白:任何能发 HTTP 请求的东西,都能当 Agent 的前端。

1. 里程碑一:后端跑起来(半天)

cd hello-agents/code/chapter15/Helloagents-AI-Town/backend
uv venv && source .venv/bin/activate
uv pip install -r requirements.txt
cp .env.example .env

.env 最小配置(Qdrant/Neo4j 等向量、图数据库配置先全部留着不动,记忆系统默认用本地 SQLite——一种"整个数据库就是一个文件"的轻量数据库,Python 自带支持、无需安装,先跑通再说):

LLM_MODEL_ID=Qwen/Qwen2.5-72B-Instruct
LLM_API_KEY=你的key
LLM_BASE_URL=https://api-inference.modelscope.cn/v1/

首次启动前的重要一步:记忆系统的语义检索依赖一个本地向量模型(sentence-transformers/all-MiniLM-L6-v2,约 90MB),第一次运行会自动从 HuggingFace 下载。国内网络直连经常超时,先设置镜像再启动:

bash export HF_ENDPOINT=https://hf-mirror.com # Windows PowerShell: $env:HF_ENDPOINT="https://hf-mirror.com"

建议把这行加进 ~/.zshrc / ~/.bashrc 一劳永逸。下载卡住的排错见第 09 篇项目三部分。

启动:

python main.py

预期输出:

🎮 赛博小镇后端服务启动中...
✅ LLM初始化成功
✅ 张三(Python工程师) Agent创建成功 (记忆系统已启用)
  💾 张三的记忆系统已初始化 (存储路径: .../memory_data/张三)
...
✅ 所有服务已启动!

Checkpoint 1: 打开 http://localhost:8000/docs,用 POST /chat 发 {"npc_name": "张三", "message": "你好,你是做什么的?"},收到符合人设的回复。再连发一句"我刚才问了你什么?",它能答上来——记忆生效了。

2. 里程碑二:读懂后端三大系统(1~2 天)

2.1 NPC 即 Agent:人设完全由提示词塑造

agents.py 顶部的 NPC_ROLES 字典定义了三个角色的全部人设(职位/性格/专长/说话风格/爱好/位置/当前活动),create_system_prompt() 把它们拼成系统提示词,核心段落:

你是Datawhale办公室的Python工程师张三。
【角色设定】性格: 技术宅,喜欢讨论算法和框架 …
【行为准则】
1. 保持角色一致性,用第一人称"我"回答
2. 回复简洁自然,控制在30-50字以内
…
【重要】
- 不要说"我是AI"或"我是语言模型"
- 要像真实的办公室同事一样自然对话

想加第四个 NPC?往 NPC_ROLES 加一个条目就行,Agent、记忆、好感度全部自动创建——这就是数据驱动设计的好处。(游戏侧还要摆一个 NPC 节点,见里程碑四。)

2.2 chat():一次对话的六步流水线

NPCAgentManager.chat() 是全项目最核心的函数,六步背下来(下面是骨架示意,... 处省略了参数,不能直接运行,记结构就行):

def chat(self, npc_name, message, player_id="player"):
    # ① 查好感度 → 生成"对话风格指令"
    affinity = self.relationship_manager.get_affinity(npc_name, player_id)
    affinity_context = f"你与玩家的关系: {level} (好感度: {affinity}/100)\n【对话风格】{modifier}"

    # ② 按当前消息检索相关记忆(工作记忆+情景记忆,最多5条,重要性>=0.3)
    relevant_memories = memory_manager.retrieve_memories(query=message, ...)

    # ③ 组装增强消息: 好感度上下文 + 记忆上下文 + 当前对话
    enhanced_message = affinity_context + memory_context + f"玩家: {message}"

    # ④ 调用Agent生成回复
    response = agent.run(enhanced_message)

    # ⑤ 用LLM分析这轮对话的情感 → 更新好感度
    self.relationship_manager.analyze_and_update_affinity(...)

    # ⑥ 把玩家消息和NPC回复都存进记忆
    memory_manager.add_memory(content=f"玩家说: {message}", memory_type="working", importance=0.5, ...)
    memory_manager.add_memory(content=f"我说: {response}", memory_type="working", importance=0.6, ...)

注意送进模型的不是裸消息,而是三段式的"增强消息":

【当前关系】你与玩家的关系: 亲密 (好感度: 65/100)
【对话风格】热情友好,主动分享,像朋友一样交流

【之前的对话记忆】
[14:32] 玩家说: 我喜欢喝咖啡
[14:33] 我说: 我也是!休息区的手冲不错

【当前对话】
玩家: 推荐个饮料?

NPC 的"人味"= 人设提示词 + 关系状态 + 相关记忆,三者在每轮对话前动态拼装。 这是整个项目最值得带走的一张图。

2.3 记忆系统:短期与长期

每个 NPC 一个 MemoryManager(配置详见第 03 篇 3.5 节),两层结构:

  • 工作记忆(working):最近 10 条对话、2000 token 上限,像人的"刚聊过的印象"。满了之后,重要的会被整合进情景记忆,不重要的被挤掉。
  • 情景记忆(episodic):最多 100 条,持久化在 memory_data/张三/memory.db(SQLite),带重要性分数和时间衰减(decay_factor=0.95)——越久远的事越难被想起来,除非它足够重要。

对话时用 retrieve_memories(query=当前消息) 做语义检索:聊"咖啡"就召回咖啡相关的记忆,而不是机械地取最近 5 条。

调试接口(边玩边看记忆怎么长):

GET    /npcs/张三/memories        # 查看记忆
DELETE /npcs/张三/memories        # 清空记忆(重新开始)

2.4 好感度系统:数值驱动态度

relationship_manager.py 维护每个 NPC 对玩家的好感度(0~100,初始 50),两个方向的作用:

读:好感度 → 档位 → 说话风格指令(拼进增强消息):

好感度 档位 风格指令(示意)
80+ 挚友 亲昵随意,开玩笑,主动关心
60~80 亲密 热情友好,主动分享
40~60 熟悉 礼貌友善,正常交流,保持专业
20~40 冷淡 简短回应,保持距离
<20 疏远 敷衍,不愿多说

写:每轮对话后,analyze_and_update_affinity() 让 LLM 给这轮对话的情感倾向打分(friendly/neutral/rude…),换算成好感度增减。用 LLM 当"情感裁判"——这个技巧(LLM-as-a-Judge)在评估、审核场景到处都在用。

调试接口:GET /affinities 看全部好感度;PUT /npcs/张三/affinity?affinity=90 直接改数值,立刻体验"挚友模式"的说话方式。

Checkpoint 2: 用 /docs 连续对张三说三句刻薄话,观察 GET /npcs/张三/affinity 数值下降、回复变冷淡;清空记忆后再问"我们聊过什么",它应该不记得了。

2.5 state_manager.py:NPC 的"自主行为"

游戏里 NPC 头顶会冒泡自言自语("这个bug真难修…")。实现:state_manager 每 30 秒(NPC_UPDATE_INTERVAL)调用一次 batch_generator 批量为三个 NPC 生成当前状态台词,游戏端轮询 GET /npcs/status 拿去显示。批量生成(一次 LLM 调用生成三个人的台词)是为了省 token 和延迟——值得注意的小优化。

3. 里程碑三:游戏跑起来(半天)

  1. 安装 Godot 4.2+(见第 02 篇第 8 节)
  2. Godot → 导入 → 选择 Helloagents-AI-Town/helloagents-ai-town/project.godot
  3. 确认后端已启动,按 F5 运行
  4. WASD 移动,走近 NPC 按 E 对话,Enter 发送,ESC 关闭

Checkpoint 3: 游戏内与张三对话成功;关掉游戏重开,它还记得上一次的对话内容(记忆持久化在后端,与游戏进程无关)。

4. 里程碑四:读懂游戏侧(1 天,GDScript 速成)

GDScript 语法极像 Python(缩进、动态类型),Python 学完的你直接能读。scripts/ 下五个文件:

  • config.gd —— 后端地址配置(http://localhost:8000),连不上先查这里
  • api_client.gd —— ★ 唯一和后端通信的文件。Godot 的 HTTPRequest 节点发 JSON,回调里解析响应。把它读懂,游戏侧就通了
  • player.gd —— 读 WASD 输入 → 改速度 → 播放走路动画;碰撞体积防止穿墙
  • npc.gd —— 检测玩家靠近(Area2D 信号)→ 显示"按E对话"提示 → 按下 E 发信号打开对话框
  • dialogue_ui.gd —— 对话框 UI:输入框、历史记录、调 api_client 发消息、收到回复后追加显示

Godot 两个核心概念,读脚本前先建立:

  1. 场景树:游戏画面是节点树(玩家节点、NPC 节点、UI 节点),.tscn 文件就是一棵子树。scenes/main.tscn 是主场景,里面摆了地图、玩家、三个 NPC。
  2. 信号(signal):节点间通信靠"信号"(等于事件/回调)。NPC 被交互 → 发信号 → 对话框 UI 接住 → 打开。

改造练习(至少做第 1 个):

  1. 加第四个 NPC:后端 NPC_ROLES 加"赵六/测试工程师";游戏里复制一个 NPC 节点(scenes/npc.tscn 实例),改它的 npc_name 属性和贴图。完整走一遍"数据驱动 + 场景实例化"。
  2. 显示好感度:对话框里加一行小字显示当前好感度(api_client 调 GET /npcs/{name}/affinity)。
  3. 记忆面板:按 M 键弹出当前 NPC 的记忆列表(调 GET /npcs/{name}/memories),直观看到"它记住了什么"。

5. 本项目的可迁移经验

  • 游戏引擎 = 又一种前端:HTTP + JSON 的后端可以对接任何客户端
  • 动态上下文拼装:人设(静态)+ 关系状态(数值)+ 相关记忆(检索)→ 每轮重组提示词,是"有状态 AI 角色"的通用配方
  • 两层记忆 + 语义检索 + 重要性/时间衰减:对话式产品的记忆标准架构
  • LLM-as-a-Judge:用模型给对话打分驱动数值系统(好感度),而不是写死关键词规则
  • 批量生成:多个角色的周期性内容合并成一次 LLM 调用

自测清单

  1. 画出"按 E 对话"到"看到回复"的完整调用链(游戏脚本 → 接口 → 后端函数)。
  2. chat() 的六个步骤,不看文档默写出来。
  3. 工作记忆和情景记忆的区别?memory.db 存的是哪种?
  4. 好感度是怎么"读"进对话、又是怎么"写"回去的?
  5. 为什么 NPC 头顶的自言自语要批量生成、周期刷新,而不是每次实时调 LLM?
  6. 已完成"加第四个 NPC"练习。

下一篇:08 · Docker 与部署实战

本页由 Markdown 自动渲染 · 共 628 篇笔记