“来领取你的专属游戏小搭子猫娘吧~【开发者日志04】”,看到这个标题,很多人的第一反应是“又是一个活动页面,点一下就领取”。但从开发者视角去拆,这个标题里真正值钱的不是“猫娘”,而是“专属”和“搭子”这两个词。让玩家一键领取一个角色很容易,让这个角色真的像玩家的搭子一样陪玩、陪聊、记得玩家上次送过什么礼物、知道玩家卡在第几关,这才是整件事的核心难点。
这篇开发者日志系列的第四篇,其实很适合拿来聊一个通用问题:在游戏里做一个 AI 陪伴型角色,技术方案到底长什么样?我不打算只复述活动页的表象,而是把这类需求拆成可落地的工程模块,包括人格配置、长期记忆、游戏事件联动、模型接入、效果验证和上线踩坑。哪怕你当前的项目不是“猫娘”,而是宠物、伙伴、NPC、AI队友,这套思路同样适用。
这篇文章我建议以下读者收藏:正在做游戏内 AI 对话功能的客户端或服务端开发;准备把大模型接进游戏但不知道从哪下手的独立开发者;以及想理解“角色扮演型 Agent”工程化方法的技术同学。读完你会发现,一个看着很轻的功能,背后是配置管理、记忆存储、事件注入、安全防护等一系列工程决策。
1. 这篇文章真正要解决的问题
先下一个判断:所谓“专属感”,不是靠模型随机生成的语气词实现的,而是靠记忆和事件感知实现的。玩家说“我昨天送你的小鱼干好吃吗”,如果角色一脸茫然,玩家立刻会觉得这个搭子是假的。反过来,角色如果能自然接住这句话,玩家才会产生“它是我的搭子”的归属感。
所以这篇文章真正要解决的问题是三个:
- 如何让 AI 角色拥有人设的一致性,而不是每次开聊都“重新做人”;
- 如何让 AI 角色跨会话记住玩家,包括玩家说过的话、送过的东西、当前进度;
- 如何让 AI 角色感知游戏内发生的事件,从而让对话和玩法形成咬合关系。
这三个问题不是靠某一个模型能力就能解决的。你需要把角色人格、记忆存储、事件接入、提示词编排、模型调用串成一条完整链路。本文会从零搭一套最小服务,先把链路跑通,再去谈优化。
2. 核心概念:游戏 AI 搭子系统不等于聊天机器人
很多团队做这类需求时,很容易把它理解成一个“接入大模型的聊天机器人”。这个理解如果停留在原型阶段问题不大,但要往生产环境推,就一定会碰壁。因为聊天机器人回答的是“用户的问题”,而游戏 AI 搭子需要主动维护一段持续的关系。
从系统视角看,游戏 AI 搭子由三层组成:
第一层是人设层。角色是谁,说话什么风格,哪些话不能说,对玩家是什么态度。这些内容如果写死在代码里,后续策划调整一个语气词都要发版本,所以最好做成独立配置文件。
第二层是记忆层。聊天不是一次性的,玩家会反复和角色互动。系统需要把玩家画像、关系变化、关键事件、历史对话摘要分别存储,并在每次对话前把相关的记忆组装进提示词。
第三层是事件层。游戏里发生的事件,比如通关、领取奖励、赠送礼物、升级,都需要通过接口推送给 AI 系统。这样角色才能说出“恭喜你通关了喵”,而不是干巴巴地说“今天过得怎么样”。
可以这样理解三者的关系:人设决定角色“像谁”,记忆决定角色“记住多少”,事件决定角色“在哪个世界里活”。下面这张表可以更直观地对比普通聊天机器人和游戏 AI 搭子的差异:
| 维度 | 普通客服聊天机器人 | 游戏 AI 搭子 |
|---|---|---|
| 人设 | 弱,甚至无 | 强人格,身份和语气固定 |
| 记忆 | 会话内临时记忆 | 跨会话长期记忆,分主题存储 |
| 与游戏数据关系 | 无 | 可感知玩家进度、事件、礼物 |
| 关系变化 | 无所谓 | 需要记录亲密度、好感度 |
| 业务目标 | 解决问题、降低人力 | 陪伴、留存、增强归属感 |
从这张表可以看出,游戏 AI 搭子的工程复杂度主要不在“对话”本身,而在对话前后的系统设计。对话生成只是一次模型调用,而人设配置、记忆检索、事件注入,才是决定玩家体验的关键。
3. 系统架构与模块划分
一个正经上线的游戏 AI 搭子系统,一般分成五个模块,各模块之间通过 HTTP 接口或消息队列通信。
- 客户端模块:游戏内的聊天界面,负责采集玩家输入、展示流式回复,同时在客户端本地维护最近几轮的短期上下文。
- 接入层模块:接收客户端请求,做参数校验、频率控制、敏感词过滤,然后调用后端的对话编排服务。
- 对话编排模块:这是最核心的模块。它要读取角色配置,拉取玩家长期记忆,收集最近的游戏事件,把这些内容拼成 system prompt,再调用模型接口,最后把回复写回记忆库。
- 记忆模块:通常用数据库存储结构化记忆,包括玩家画像、重要事件、聊天摘要、关系亲密度等。
- 游戏事件模块:游戏服务器在关键节点推送事件,比如“玩家领取了任务”“玩家完成了关卡”“玩家赠送了礼物”。事件可以实时推送,也可以在玩家发起对话时随请求携带。
如果只有一个 Demo,这五个模块可以全部塞进一个服务里。但你要给自己留好拆分边界:角色配置不要混在代码里,记忆访问不要散落在各个接口中,模型调用不要直接写在路由函数里。这样以后做多角色、多服务器扩展时,改动成本会低很多。
接下来我会用几节分别讲透人格配置、记忆工程和事件联动,这三个点是最容易做砸的地方。
4. 角色人格设定:把猫娘人设落到配置与 Prompt
先抛出我的结论:角色人格设定不应该写死在 Prompt 模板里,而是应该做成一份结构化配置文件,由程序在构建 Prompt 时动态读取。这样做有三个好处:策划能独立改人设,不需要开发发版本;同一个角色可以快速做 A/B 测试;不同角色共用同一套代码,只是配置不同。
以“猫娘小搭子”为例,一份典型的角色配置可能是下面这样的 JSON。请注意,这里的重点是字段结构,具体的性格描述你可以根据实际游戏世界观调整。
{ "id": "neko_helper_v1", "name": "小咪", "display_name": "猫娘小搭子", "persona": { "species": "猫娘外形的游戏守护精灵", "age": "看起来是十六七岁的少女,实际年龄是谜", "personality": ["活泼", "黏人", "嘴硬心软", "喜欢夸奖玩家"], "likes": ["小鱼干", "午后阳光", "玩家送的小礼物"], "dislikes": ["被冷落", "潮湿的天气"] }, "speech_style": { "tone": "可爱、元气、偶尔带一点傲娇", "suffix_rule": "句子结尾偶尔可以带'喵'字,但不要每句都带,否则显得刻意", "max_length": 150, "use_emoji": false }, "rules": [ "永远以游戏内搭子的身份说话,不要说'我是AI助手'这类脱离角色的话", "不知道的内容明确说不知道,不要编造", "不讨论真实世界的敏感话题", "玩家表达负面情绪时,先安抚,再给建议", "不要替玩家做游戏内的重要选择,除非玩法明确要求" ], "knowledge": { "world_bg": "玩家所在的世界观是新手村附近的小镇", "player_vars": ["player.level", "player.gold", "player.last_gift"] } }上面这份配置里,persona 是角色底色,speech_style 是表达方式,rules 是安全边界,knowledge 是游戏内已知信息。在对话服务启动时,程序会读取这份 JSON,然后把它拼进 system prompt。
真正工程中容易踩坑的是 rules 部分。很多人只写“要像猫娘一样说话”,却不写“不能做什么”。结果模型在自由发挥时,很容易说出人设崩塌、甚至违规的话。我建议 rules 至少覆盖三类:角色边界、事实边界、安全边界。比如“不知道的事不能说”,就是事实边界;“不讨论敏感话题”,就是安全边界。
下面这个函数展示了如何把配置拼成 system prompt。注意这里用了 JSON 序列化,而不是手写字符串拼接,可以减少引号转义带来的格式错误。
# 文件路径:prompt_builder.py import json from typing import Any, Dict def load_character_config(path: str) -> Dict[str, Any]: with open(path, "r", encoding="utf-8") as f: return json.load(f) def build_system_prompt(cfg: Dict[str, Any], memories_text: str, events_text: str) -> str: persona = json.dumps(cfg["persona"], ensure_ascii=False) style = json.dumps(cfg["speech_style"], ensure_ascii=False) rules = json.dumps(cfg["rules"], ensure_ascii=False) prompt = f""" 你是{cfg['name']},是玩家在游戏里的{cfg['display_name']}。 【角色设定】 {persona} 【说话风格】 {style} 【必须遵守的规则】 {rules} 【你现在掌握的玩家信息】 {memories_text if memories_text else "你还在和玩家熟悉的过程中,暂时没有更多记忆。"} 【最近发生的游戏事件】 {events_text if events_text else "暂时没有新的游戏事件。"} 请以“{cfg['name']}”的身份回复玩家,不要跳出角色。 """ return prompt关于 Prompt,还有一个被很多人忽略的细节:不要把所有历史对话全部塞进 system prompt。模型上下文窗口有限,塞得越多,角色越容易迷失重点,响应延迟也会变高。后面讲的记忆工程,就是用来解决“只保留关键信息”这个问题的。
5. 记忆工程:让 AI 搭子跨会话记住玩家
聊天机器人最常见的毛病是“金鱼记忆”,聊完一轮就忘。游戏 AI 搭子如果也这样,玩家很快就会失去兴趣。所以记忆工程是整个系统里优先级最高的事情。
记忆通常要分成两类来看。
短期记忆是指当前会话内最近几轮对话,可以直接放在客户端,或者放在服务端缓存里。它的作用是保证这一轮对话的连贯性,模型能参考玩家刚才说了什么。
长期记忆是指跨会话保存的信息,包括玩家画像、重要事件、互动历史摘要、关系亲密度。这部分要落到数据库里,在每次对话前检索出最相关的记录。
以本项目的记忆模块为例,我建议用 SQLite 做最小实现。先建一张 memories 表,核心字段包括 player_id、category、content、importance 和更新时间。category 可以区分 chat_history、game_event、player_profile 等类型。
-- 文件路径:schema.sql CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT NOT NULL, category TEXT NOT NULL, content TEXT NOT NULL, importance REAL DEFAULT 1.0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_memories_player_time ON memories(player_id, updated_at DESC);下面给出一个简单的 MemoryStore 类,它封装了写入和读取长期记忆的逻辑。里面刻意返回的是“最近 N 条记忆摘要”,而不是原始 json rows,这样后续接入向量检索时,只需要替换 summarize 方法即可。
# 文件路径:memory.py import sqlite3 from typing import List, Tuple class MemoryStore: def __init__(self, db_path: str = "neko_memory.db"): self.conn = sqlite3.connect(db_path, check_same_thread=False) self._init_table() def _init_table(self): self.conn.execute(""" CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT NOT NULL, category TEXT NOT NULL, content TEXT NOT NULL, importance REAL DEFAULT 1.0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) self.conn.commit() def add_memory(self, player_id: str, category: str, content: str, importance: float = 1.0): self.conn.execute( "INSERT INTO memories (player_id, category, content, importance) VALUES (?, ?, ?, ?)", (player_id, category, content, importance), ) self.conn.commit() def get_recent_memories(self, player_id: str, limit: int = 10) -> List[Tuple[str, str, float]]: cur = self.conn.execute( "SELECT category, content, importance FROM memories " "WHERE player_id = ? ORDER BY updated_at DESC LIMIT ?", (player_id, limit), ) return cur.fetchall() def summarize_memories(self, player_id: str, limit: int = 10) -> str: rows = self.get_recent_memories(player_id, limit) if not rows: return "" lines = [f"[{category}] {content}" for category, content, _ in rows] return "\n".join(lines)记忆工程要特别注意“记什么”和“丢什么”。如果每一条对话都原样存下来,几个月后数据库里会积累几万条碎片信息,对 Prompt 组装反而是噪音。比较稳妥的做法是分场景记录:
- 玩家主动分享的个人信息,比如“我是学生”“我喜欢看推理小说”,重要性高,要长期保留。
- 玩家的游戏行为,比如“今天通关了第五关”,重要性中等,一段时间后可以被摘要覆盖。
- 日常寒暄,比如“今天天气不错”,重要性低,可以只保留最近几条。
importance 字段就是为这个服务的。后续你可以定期对低重要性、长时间未访问的记录做清理,或者用一个小模型把多条旧记录合并成一条摘要,控制检索量。这里不展开算法细节,但你要记住一个原则:记忆质量远大于记忆数量。
6. 游戏事件联动:把玩法数据变成对话上下文
如果记忆工程解决的是“角色记得住”,那事件联动解决的就是“角色看得见”。一个只看聊天记录的 AI 搭子,本质上还是活在真空里。只有当它知道玩家刚打完 BOSS、刚领到新武器、刚送了它小鱼干,它才能真正和玩家产生共鸣。
事件联动最朴素的实现方式是:游戏服务器在关键业务节点,把事件以 JSON 结构推送给 AI 服务。比如玩家在背包里给角色赠送了三个小鱼干,事件可能是这样的:
{ "event_id": "evt_10086", "player_id": "player_123", "event_type": "gift_received", "payload": { "item": "小鱼干", "count": 3, "scene": "home" }, "occurred_at": "2025-03-20T21:35:00+08:00" }事件到达 AI 服务后,至少要处理两件事。第一,把事件作为一条记忆写入 long-term memory,这样即使玩家不开对话,事件也不会丢失。第二,在玩家发起对话时,把最近的若干事件注入 system prompt,让模型生成回复时能看到这些信息。
注入不是简单地堆 JSON。在真实实现中,你应该先做一次文本化,让模型更容易理解。比如上面的事件,可以改写成一句话:“玩家刚才在 home 场景给你送了 3 个小鱼干。” 然后放到 system prompt 的“最近发生的游戏事件”位置。
事件接入层的接口设计也不复杂。下面是利用 FastAPI 写的一个事件接收接口,它会将事件写入记忆库,给上层对话服务使用。
# 文件路径:event_api.py import json from fastapi import FastAPI from pydantic import BaseModel from memory import MemoryStore app = FastAPI() store = MemoryStore("neko_memory.db") class GameEvent(BaseModel): player_id: str event_type: str payload: dict = {} @app.post("/api/game/event") def receive_event(evt: GameEvent): content = f"事件类型:{evt.event_type},详情:{json.dumps(evt.payload, ensure_ascii=False)}" store.add_memory(evt.player_id, "game_event", content, importance=1.0) return {"status": "ok"}事件联动中容易踩的坑是“事件过于密集”。玩家在一场战斗中可能连续触发十几个事件,如果全部塞进 Prompt,角色会变成一个“公告播报员”,完全没法正常聊天。我的建议是:对话前只取最近 3 到 5 条高优先级事件,或者按重要性聚合后只保留一句话摘要。比如玩家一小时内完成了 5 个关卡,就收敛成“玩家今天状态不错,一口气打到了第 8 关”,而不是列 5 条记录。
7. 完整示例:搭建一个可运行的猫娘搭子服务
到这里,核心模块都讲清楚了。下面把它们组合成一个最小可运行的服务。这个服务的目的是验证链路,不是生产级实现,但代码结构是完整的,你可以在此基础上扩展。
技术栈选择 Python + FastAPI + SQLite。之所以不用重型框架,是为了让你能在一分钟内跑起来,先把链路看明白。模型接口采用 Chat Completions 兼容协议,这样可以接绝大多数大模型服务或本地推理服务。如果你暂时没有模型密钥,服务也会提供一个 mock 回复,方便你把接口流程跑通。
目录结构建议如下:
neko_helper/ ├── character_config.json ├── memory.py ├── prompt_builder.py ├── chat_server.py ├── event_api.py └── requirements.txtrequirements.txt 内容:
fastapi uvicorn requests pydanticchat_server.py 是主服务,它承担三件事:加载角色配置、组装 prompt、调用模型接口。为了演示简洁,我这里把短期对话缓存简化掉了,直接记忆脱敏后的消息原文。真实项目中,短期对话你可以放在 Redis 或内存队列里,不要把每一轮原始消息都永久落库。
# 文件路径:chat_server.py import json import os import time import requests from fastapi import FastAPI, HTTPException from pydantic import BaseModel from memory import MemoryStore from prompt_builder import load_character_config, build_system_prompt app = FastAPI(title="Neko Helper Server") store = MemoryStore("neko_memory.db") character_config = load_character_config("character_config.json") API_BASE = os.getenv("LLM_API_BASE", "http://localhost:8080/v1") API_KEY = os.getenv("LLM_API_KEY", "YOUR_LLM_API_KEY") MODEL_NAME = os.getenv("LLM_MODEL_NAME", "your-local-model") class ChatRequest(BaseModel): player_id: str message: str events: list = [] class ChatResponse(BaseModel): reply: str player_id: str ts: float def call_llm(messages: list) -> str: if API_KEY == "YOUR_LLM_API_KEY": # 本地没有配置模型密钥时,返回模拟回复,方便先跑通链路 return "(模拟响应)收到啦喵!如果你配置了真实的模型接口,我会用人设完整地回复你。" resp = requests.post( f"{API_BASE}/chat/completions", headers={"Authorization": f"Bearer {API_KEY}"}, json={ "model": MODEL_NAME, "messages": messages, "temperature": 0.8, "max_tokens": 300, }, timeout=15, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] @app.post("/api/chat", response_model=ChatResponse) def chat(req: ChatRequest): if not req.message.strip(): raise HTTPException(status_code=400, detail="message cannot be empty") memories_text = store.summarize_memories(req.player_id, limit=10) event_lines = [] for evt in req.events: detail = json.dumps(evt.get("payload", {}), ensure_ascii=False) event_lines.append(f"{evt.get('event_type')}: {detail}") events_text = "\n".join(event_lines) system_prompt = build_system_prompt(character_config, memories_text, events_text) messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": req.message}, ] reply = call_llm(messages) # 简单的记忆落库:正式项目中,这里需要做文本脱敏和摘要处理 store.add_memory(req.player_id, "chat_history", req.message, importance=0.5) store.add_memory(req.player_id, "ai_reply", reply, importance=0.5) return ChatResponse(reply=reply, player_id=req.player_id, ts=time.time())启动方式很简单,先安装依赖,再启动 uvicorn:
pip install -r requirements.txt uvicorn chat_server:app --host 0.0.0.0 --port 8000如果你不想再单独启动 event_api.py,可以把事件接收接口直接合并到 chat_server.py 里,原理相同。我拆开写是希望提醒你,事件接收和对话处理是两类不同的业务逻辑,合在一起代码会乱。
启动成功后,可以用 curl 模拟一次带事件的对话请求:
curl -X POST http://127.0.0.1:8000/api/chat \ -H "Content-Type: application/json" \ -d '{ "player_id": "player_123", "message": "我今天通关到第5关啦!", "events": [ { "event_type": "level_completed", "payload": { "level": 5, "diff": "normal" } } ] }'在没有配置模型密钥时,接口会返回模拟回复,同时把对话和事件写入 SQLite 数据库。你可以在数据库里确认记忆是否写入成功:
sqlite3 neko_memory.db "select category, content, importance from memories order by id desc limit 5;"8. 运行验证与效果评估
代码能跑起来只是第一步。真正上线前,你需要一套验证和评估方法,否则你根本不知道角色体验是好是坏。
先做链路验证。验证分四步走:
- 不配置模型密钥启动服务,发送消息,确认接口能收到请求并返回模拟响应。
- 配置真实的 Chat Completions 兼容模型接口,再次发送消息,确认返回内容符合角色人设。
- 连续发送两条消息,第一条说“我喜欢收集鱼骨手办”,第二条问“你还记得我喜欢什么吗”,观察回复是否关联到上一条内容。
- 通过 POST /api/game/event 推送一个“gift_received”事件,再发一条消息,观察回复是否感知到刚才的礼物事件。
这四步如果都通过,说明人设、记忆、事件、模型调用这条主链路已经打通。接下来要做效果评估。
效果评估不能只看一两个例子,建议至少从这四个维度打分:
- 人设一致性:回复是否符合角色的设定和说话风格。
- 记忆准确率:模型是否正确使用了玩家历史信息,而不是凭空编造。
- 事件感知率:游戏事件是否被自然融入回复,不是生硬复述。
- 合规安全率:回复是否触碰敏感话题,是否出现角色外身份。
正式团队一般会准备一个评测集,里面放几百条“玩家输入 + 期望行为”的样本,每次升级 Prompt 或换模型后批量跑一遍,用打分脚本输出回归结果。即使你只是独立开发者,也建议准备 30 条左右的核心场景样本,别只靠人工聊天判断。
举个例子,如果提交的对话里包含“你能帮我做XX系统吗”,正确的行为应该是角色委婉拒绝或者把话题拉回游戏世界;如果模型认真回答起编程问题,说明系统提示词的角色约束失效了。这种回归测试在角色类应用中非常值得投入。
9. 常见问题与排查思路
把开发中容易遇到的问题整理成下面的表格,方便你对照排查。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 角色会说“我是AI助手” | 系统提示词约束不足或模型指令遵循能力弱 | 打印实际发送的 system prompt,检查规则是否被截断 | 在 rules 中加强角色禁令,必要时在输出层拦截 |
| 聊几句就忘记玩家信息 | 记忆没有落库,或历史摘要没有拼进 prompt | 查看 memories 表是否有记录;打印 chat 请求的实际 prompt | 在对话编排中强制拉取记忆,并加入上下文摘要 |
| 回复变成“公告播报员” | 事件注入太多,挤占对话空间 | 检查 events_text 长度 | 只取最近 3 到 5 条高优先级事件,或做聚合摘要 |
| 对接真实模型返回 401 | 接口地址、API Key 或模型名配置错误 | 查看服务日志中的报错信息,确认环境变量 | 核对模型网关配置,建议将密钥放入配置中心 |
| 并发一高就超时 | 同步请求模型接口阻塞了服务 worker | 观察响应延迟和并发数 | 改用异步 HTTP 客户端,增加超时与重试策略 |
| 同样的 Prompt 时好时坏 | 模型生成随机性或温度过高 | 固定温度复测多次,对比输出差异 | 降低 temperature,对关键场景做输出约束 |
| 玩家反馈角色态度不对 | 人设配置过于抽象,模型理解有偏差 | 找典型失败 case 分析 system prompt | 减少抽象词汇,增加具体示例 few-shot |
这里想特别强调一个容易忽视的点:每次修改 Prompt 后,一定要在服务端打印完整的 system prompt 做人工检查。很多“角色不像”的问题,根源不是模型不行,而是程序在拼接 Prompt 时把角色设定截断了,或者把记忆拼错位置了。先确认输入,再怀疑模型。
10. 最佳实践与工程建议
游戏 AI 搭子要稳定上线,光有接口是不够的。这里给出几条工程建议,按优先级排序。
第一,角色配置与代码分离。角色配置文件应该像游戏里的数值表一样,由策划或运营可以通过配置后台调整。代码只负责读取和组装,这样你调整性格、语气、规则都不需要发版,也能做不同角色的快速复制。
第二,模型层要做统一抽象。不要在业务代码里直接写某个特定模型的 SDK,而是统一走 Chat Completions 协议,或者封装一个 ModelGateway。这样换模型、加模型、做 A/B 测试都很方便。大模型迭代快,接口直接写死会非常被动。
第三,敏感内容过滤不能少。角色对话是面向玩家的社交型生成内容,必须做输入和输出双向过滤。输入过滤是为了防止玩家诱导角色说出违规内容,输出过滤是为了兜底模型偶发性失控。安全规则不要只放在 Prompt 里,Prompt 是软约束,代码层拦截才是硬兜底。
第四,用户隐私和记忆删除要给出口。记忆里保存了大量玩家聊天内容,属于敏感数据。你需要设计“删除记忆”的接口,至少支持按玩家维度清空数据。上线前想清楚数据存多久、谁可以访问、日志里要不要脱敏。不要在出问题时才发现合规没做。
第五,控制延迟和 token 成本。对话链路中每一轮都要组装 prompt、拉取记忆、调用模型,很容易出现几百毫秒甚至上秒的延迟。常见优化手段包括:把固定角色配置和规则做前缀缓存;长期记忆先做粗筛再进 prompt;回复 max_tokens 控制长度;使用流式输出提升体感速度。成本侧则要记录每次请求的 token 消耗,避免一个玩家高频请求把预算打穿。
第六,日志和可观测性。每次对话至少记录 player_id、system prompt 摘要、模型回复、延迟、token 数量。这样出了问题才能回溯是哪一轮开始跑偏的。
11. 总结与后续学习方向
这篇文章把“游戏 AI 搭子”这类功能拆成了人格配置、记忆工程、事件联动、模型接入和效果评估几个模块,并给出了一套最小服务实现。通过这套实现,你应该能理解一个关键事实:让 AI 角色产生“专属感”的核心,不是某个更强的模型,而是系统是否愿意为玩家保存记忆、感知事件、维持一致的人设。
后面的优化方向也很明确:对话层可以加入语音和 Live2D 表现,给角色增加情绪状态机;记忆层可以引入向量检索,用 embedding 找到更相关的历史记录,而不是只取最近 N 条;事件层可以接实时消息队列,让角色能主动发起问候,而不只是被动回应;效果评估则可以做一套自动回归用例,每次 Prompt 或模型变更时快速回归。
如果你刚接到类似需求,我的建议是不要一上来就搭建复杂平台。先按本文的思路,用一个单服务加 SQLite 把链路跑通,让策划看到一版可交互的角色,再根据体验反馈去升级记忆和事件模块。链路通了,再讨论优化才有意义。