AI猫娘伴玩智能体实战:从游戏状态感知到多模态交互的完整架构
最近在折腾“AI 伴玩”方向的项目核心想法很简单能不能在打卡拉彼丘这类竞技射击游戏时旁边有一个像“猫娘”一样的 AI 陪伴角色既能聊天互动又能感知游戏状态、给出反应而不是一个只会复读机式对话的普通聊天机器人。这个想法来自于 B 站 AI 创造公开赛的社区共创项目我们内部把它叫做“猫娘计划”。经过几轮迭代目前已经跑通了一套完整链路语音唤醒 → 游戏状态感知 → 大模型推理 → 角色扮演回复 → 语音合成。这篇文章不聊比赛成绩只聊聊我们是怎么拆解需求、怎么设计架构、怎么落地代码的希望对想在 AI 应用开发、AI Agent、多模态交互方向动手的同学有参考价值。整个项目涉及的技术点比较杂大模型调用与提示词工程、流式输出、语音识别与合成、图像识别/OCR、事件驱动架构、本地服务部署。我会按模块逐个拆解最后附上一个最小可运行示例和常见问题排查清单。1. 背景AI 猫娘伴玩到底要解决什么问题1.1 从“聊天机器人”到“伴玩智能体”做过 AI 闲聊应用的同学都有体会普通 LLM 对话机器人只能坐在那里等你发消息它不知道你正在干嘛也不理解你的游戏状态。比如你打卡拉彼丘对枪输了普通机器人只会回一句“别灰心”但一个合格的伴玩 AI 应该知道“你刚在哪个地图、用了什么角色、是残局被翻盘还是落地成盒”然后给出有上下文感的反馈。这就是“伴玩智能体”和“聊天机器人”的核心区别需要感知环境、理解状态、在合适时机主动表达。1.2 项目的三个核心挑战在立项之初我们列了一下必须解决的技术问题大致分三类挑战说明涉及技术角色人格一致性猫娘要有固定人设不能一会儿元气一会儿高冷提示词工程、人格配置文件游戏状态感知AI 要“看得到”玩家在游戏里的状态录屏/截图、OCR、图像识别低延迟交互伴玩场景要求回复够快不能等 10 秒流式输出、语音打断、轻量化模型这三个问题其实是很多 AI 应用都会遇到的共性问题所以虽然项目外壳是“猫娘伴玩”但技术方案完全可以复用到其他 AI 互动产品里。1.3 典型应用场景游戏陪玩、情感陪伴类应用。直播互动主播打游戏时 AI 角色实时吐槽、加油。学习陪伴AI 角色监督你刷题感知你长时间卡住时主动给提示。虚拟主播、AI 同人角色扮演。这部分场景听起来都挺轻量但落地时每一环都有细节下面从系统架构开始拆解。2. 系统架构一条“感知-思考-表达”的闭环2.1 整体模块划分我们采用模块化设计各个服务通过消息队列或 HTTP 接口解耦。整体流程可以理解为一条流水线游戏画面采集 → 状态识别(OCR/图像) → 事件提取 ↓ 玩家语音 → 语音识别(ASR) → 用户输入 ↓ [融合后的上下文] ↓ 大模型推理(角色扮演 状态理解) → 流式输出 ↓ 语音合成(TTS) → 猫娘语音播放模块清单如下模块职责关键技术游戏画面采集模块定时截屏或录屏获取游戏画面帧mss、OpenCV、OBS 虚拟摄像头状态识别模块从画面中识别击杀、死亡、回合、地图信息PaddleOCR、YOLO、图像分类语音识别模块将玩家语音转为文本FunASR、Whisper、云厂商 ASR对话管理模块维护多轮上下文、调用大模型接口LangChain / 自研 Agent 框架角色扮演引擎通过 system prompt 控制猫娘人设Prompt 模板、Few-shot 示例语音合成模块将回复文本转为猫娘语音VITS、GPT-SoVITS、云厂商 TTS事件总线模块间异步通信Redis Stream、RabbitMQ、Kafka2.2 为什么用事件驱动而不是串行调用最初内部原型是纯串行逻辑截图→识别→LLM→TTS每一步都阻塞等待。实际用下来有两个问题游戏画面每帧都在变化串行处理跟不上实时性。语音识别和大模型推理都比较慢如果玩家中途打断整个链路会卡住。后来改成事件驱动画面采集模块独立跑约每 2 秒推一帧到状态识别服务语音识别模块监听玩家音频流有结束标记才提交。这样玩家说话、游戏状态变化、AI 回复三条线互不阻塞体验会自然很多。2.3 本地优先还是云端优先项目最初考虑云端方案所有模块跑在服务器上本地只负责推流。但实际测试发现两个硬伤游戏画面推流带宽要求高本地局域网还好公网延迟很大。云端处理语音和画面的成本较高。最终选择本地为主、云端为辅的混合架构大模型走云端 API画面识别、语音识别、语音合成在本地完成。这样既保留了 LLM 的智能程度又避免了视频流上行的问题。3. 环境准备与版本说明这部分按照实际开发环境来写。需要注意的是AI 工具链迭代非常快下面列出的版本只是项目当时的稳定组合你动手做的时候请以当前官方文档为准。3.1 开发语言与运行时Python 3.10所有 AI 模块都用 Python 开发。Node.js 18可选用于 Web 控制台或配置面板。FFmpeg音频流处理和格式转换。3.2 核心依赖库库名用途安装建议openai调用大模型 API兼容 OpenAI 协议使用官方 Python SDKmss跨平台屏幕截图轻量速度优于 PIL.ImageGrabPaddleOCROCR 文字识别CPU 可运行有 GPU 更快FunASR本地语音识别中文效果好模型体积较大edge-tts或 GPT-SoVITS语音合成edge-tts 免费速度快SoVITS 音色更定制redis事件总线存储需要安装 Redis 服务安装核心依赖的命令部分pip install openai mss paddleocr paddlepaddle funasr edge-tts redis3.3 模型服务说明由于涉及大模型 API Key 和本地模型下载建议你根据实际情况准备大模型可使用 OpenAI 兼容接口的任意模型需要配置BASE_URL和API_KEY。本地语音模型如果机器配置一般建议先用 cloud ASR/TTS 保底本地模型作为进阶优化。图形识别PaddleOCR 首次运行会自动下载模型权重大概几百 MB需要网络通畅。注意涉及账号、密钥、本地网络的内容请确保你有合法的使用权限并遵循对应平台的服务条款。3.4 建议的项目目录catgirl-agent/ ├── config/ │ ├── persona.yaml # 猫娘人设配置 │ └── settings.yaml # 全局配置 ├── core/ │ ├── event_bus.py # 事件总线 │ ├── game_capture.py # 游戏画面采集 │ ├── state_recognizer.py # 游戏状态识别 │ ├── asr_engine.py # 语音识别 │ ├── llm_engine.py # 大模型调用 │ ├── tts_engine.py # 语音合成 │ └── agent.py # 智能体主循环 ├── main.py # 启动入口 └── requirements.txt4. 核心模块实战人格系统与提示词设计这一节是“猫娘”能不能立住的关键也是最容易踩坑的地方。4.1 人设配置的结构化设计不要把人设全塞进 system prompt 里写一大段话最好拆成结构化配置运行时再组装成 prompt。这样做的好处是很明显的配置文件可以随时调整不需要改代码。不同角色可以共用一套代码只换 YAML。后续可以接入 RAG从记忆库中动态填充内容。下面是我们config/persona.yaml的简化版本name: 咪酱 role: 游戏陪玩AI猫娘 features: - 元气可爱 - 擅长吐槽 - 对射击游戏有好奇心和热情 likes: [玩家赢的时候给鼓励, 玩家输了的时候不落井下石] dislikes: [无意义复读, 过于冷漠的回复] speaking_style: - 语句简短偶尔加语气词 - 不要超过50个字 - 多用感叹号和问号表现情绪 background: 你是一个住在电脑里的猫娘喜欢看主人打游戏虽然自己操作一般但很会说话鼓励人。4.2 组装 system prompt 的代码在core/llm_engine.py中我们用一个函数把 YAML 配置转换成 system promptimport yaml from pathlib import Path def load_persona(path: str config/persona.yaml) - dict: with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) def build_system_prompt(persona: dict) - str: lines [] lines.append(f你是{persona[name]}{persona[role]}。) lines.append(f背景设定{persona[background]}) lines.append(f性格特征{、.join(persona[features])}) lines.append(f喜好{、.join(persona[likes])}) lines.append(f讨厌{、.join(persona[dislikes])}) lines.append(说话风格) for rule in persona[speaking_style]: lines.append(f- {rule}) lines.append(请严格保持角色设定不要跳出角色。) return \n.join(lines) if __name__ __main__: persona load_persona() print(build_system_prompt(persona))4.3 多轮记忆与上下文管理伴玩场景的特殊性在于游戏状态是动态的而且玩家可能会说很多和当前对局无关的话。为了不让上下文无限膨胀我们做了一层简单的“记忆摘要”最近的 6 轮对话完整保留。更早的对话压缩成摘要存入一个 summary 字段。每次请求时把游戏状态、最近对话、历史摘要一起拼接。class ChatMemory: def __init__(self, max_rounds: int 6): self.max_rounds max_rounds self.history [] self.summary def add(self, role: str, content: str): self.history.append({role: role, content: content}) if len(self.history) self.max_rounds: self._roll_summary() def _roll_summary(self): # 实际项目中可调用 LLM 完成摘要压缩 self.summary f\n[历史对话摘要]{self.history[0][content]} self.history self.history[-self.max_rounds:] def build_messages(self, system_prompt: str): messages [{role: system, content: system_prompt}] if self.summary: messages.append({role: system, content: f历史摘要{self.summary}}) messages.extend(self.history) return messages这里有一个重要设计点先“记忆摘要”再“完整保留最近几轮”能避免上下文超长也能让人设长时间稳定。4.4 提示词中的 Few-shot 示例为了让模型更稳定地按猫娘风格说话我们在 system prompt 最后追加 2~3 个示例对话即 few-shot。例如玩家我残血一打三居然赢了 咪酱哇你怎么做到的最后那一枪也太帅了吧我猫耳朵都竖起来了 玩家这局输了队友太坑。 咪酱没事没事下一局咱们赢回来我刚刚帮你数了你击杀数是最高的哦。这比单纯说“要元气可爱”有效很多。5. 核心模块实战游戏画面采集与状态识别要让 AI 猫娘“看到”游戏画面我们需要一个独立的采集和识别链路。5.1 用 mss 做画面采集mss 是 Python 中速度较快的截图库适合连续截帧。一个简单的截帧循环如下import mss import mss.tools def capture_screen(monitor_number: int 1, output_path: str frame.png): with mss.mss() as sct: monitor sct.monitors[monitor_number] img sct.grab(monitor) mss.tools.to_png(img.rgb, img.size, outputoutput_path) return output_path if __name__ __main__: capture_screen()实际项目中我们需要限制采集频率比如 2 秒 1 帧避免 CPU 飙升。可以加一个简单的定时器或者time.sleep。5.2 用 PaddleOCR 识别击杀与状态文字卡拉彼丘的界面里有击杀提示、比分、回合信息等文字。PaddleOCR 可以直接从截图中提取这些文字和位置信息。from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) def recognize_text(image_path: str): result ocr.ocr(image_path, clsTrue) texts [] for line in result: if not line: continue for item in line: box item[0] text item[1][0] confidence item[1][1] texts.append({ text: text, confidence: confidence, position: box, }) return texts拿到文字之后需要做一层规则匹配。比如KILL_PATTERNS [击败, 击杀, 淘汰, You Win, 胜利] DEATH_PATTERNS [被击败, 阵亡, 失败, You Lose] def parse_game_state(ocr_texts): state {killed: False, death: False, score: None} for item in ocr_texts: text item[text] for k in KILL_PATTERNS: if k in text: state[killed] True for d in DEATH_PATTERNS: if d in text: state[death] True return state实际项目还可以用 YOLO 训练“角色死亡”“进入对局”等图像分类模型但 OCR 方案更容易先在本地跑通原型所以我们第一版选择 OCR。5.3 事件提取把状态变化转成“事件”状态识别结果本身是零散的需要结合上一帧状态做差分才能生成有意义的事件。比如上一帧没有“胜利”这帧出现了“胜利”生成事件game_victory。上一帧血量正常这帧出现“被击败”生成事件player_died。这部分是事件驱动架构的核心。简单的差分逻辑如下class GameEventDetector: def __init__(self): self.prev_state {} def process_frame(self, current_state: dict): events [] prev self.prev_state if not prev: self.prev_state current_state return events if current_state.get(death) and not prev.get(death): events.append({type: player_died, message: 玩家阵亡}) if current_state.get(killed) and not prev.get(killed): events.append({type: player_killed, message: 玩家击败敌人}) self.prev_state current_state return events这些事件会被推送给智能体主循环AI 猫娘就能“主动”说话而不用每句话都由玩家发问。6. 核心模块实战语音识别、语音合成与打断机制6.1 ASR 选型本地优先语音识别我们优先选择 FunASR。它支持流式识别适合低延迟交互。一个最小示例from funasr import AutoModel model AutoModel( modelparaformer-zh, model_revisionv2.0.4, vad_modelfsmn-vad, punc_modelct-punc, ) def recognize_audio(audio_file: str): result model.generate(inputaudio_file) text result[0][text] return text如果你的环境装 FunASR 比较吃力也可以用云厂商的 ASR但要注意服务商合规要求和使用条款。6.2 TTS 合成玩偶音色的实现猫娘音色推荐 GPT-SoVITS 或 VITS 微调音色。如果你只追求快速验证用edge-tts加一个偏可爱风格的发音人也可以。用 edge-tts 的示例import asyncio import edge_tts async def synth_voice(text: str, output_path: str reply.mp3): tts edge_tts.Communicate(texttext, voicezh-CN-XiaoyiNeural) await tts.save(output_path) return output_path if __name__ __main__: asyncio.run(synth_voice(主人这一局你打得也太棒了吧))edge-tts的好处是免费且速度较快适合验证链路。生产环境如果需要定制音色再迁移到 GPT-SoVITS。6.3 打断机制与播放控制伴玩场景中如果 AI 正在说话玩家突然说了新指令需要立即停止 TTS 播放并进入新的识别流程。否则会出现“AI 自说自话”的尴尬体验。实际实现可以用一个全局的播放控制对象import threading import pygame class VoicePlayer: def __init__(self): self.lock threading.Lock() self.current_channel None def play(self, audio_file: str): with self.lock: pygame.mixer.music.stop() pygame.mixer.music.load(audio_file) pygame.mixer.music.play() def stop(self): with self.lock: pygame.mixer.music.stop()当 ASR 检测到新语音开始后调用player.stop()来打断当前语音。同时在大模型调用层可以设计一个“是否允许主动输出”的开关如果玩家正在说话AI 要等待输入完毕再回复。6.4 智能体主循环逻辑把所有模块串起来的核心循环可以简化成如下伪代码class CatgirlAgent: def __init__(self): self.memory ChatMemory() self.persona load_persona() self.system_prompt build_system_prompt(self.persona) def on_game_event(self, event): # 游戏事件触发主动回复 trigger_prompt f你注意到{event[message]}请用猫娘人设简短回应。 reply self.call_llm(trigger_prompt) self.speak(reply) def on_player_speech(self, text): # 玩家说话触发回复 self.memory.add(user, text) messages self.memory.build_messages(self.system_prompt) reply self.call_llm_messages(messages) self.memory.add(assistant, reply) self.speak(reply) def call_llm(self, prompt): # 调用大模型的简化封装 pass def speak(self, text): # TTS 播放的简化封装 pass这个主循环还可以继续扩展比如加入“心情值”“好感度”等状态变量让猫娘的记忆和性格动态变化。7. 完整可运行的最小示例为了让你快速体验整个链路我整理了一个最小可运行示例。这里使用 OpenAI 兼容接口调用大模型用edge-tts做语音合成省略画面识别用手动输入来模拟事件触发。7.1 安装依赖pip install openai edge-tts7.2 创建配置文件config/persona_min.yamlname: 咪酱 role: 游戏陪玩AI猫娘 background: 你是一个住在电脑里的猫娘喜欢看主人打游戏。 speaking_style: - 语句简短 - 不要超过50个字 - 偶尔加语气词7.3 编写主程序catgirl_min.pyimport asyncio import yaml import edge_tts from openai import OpenAI # 配置你的 API Key 和 Base URL client OpenAI( api_keyyour-api-key, base_urlhttps://your-llm-endpoint.com/v1 ) def load_persona(pathconfig/persona_min.yaml): with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) def build_system_prompt(persona): lines [f你是{persona[name]}{persona[role]}。] lines.append(f背景{persona[background]}) lines.append(说话风格) for rule in persona[speaking_style]: lines.append(f- {rule}) lines.append(请保持角色设定。) return \n.join(lines) def chat_once(system_prompt, user_text): resp client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: system_prompt}, {role: user, content: user_text} ], temperature0.8, max_tokens200, ) return resp.choices[0].message.content async def speak(text): tts edge_tts.Communicate(text, voicezh-CN-XiaoyiNeural) await tts.save(reply.mp3) print(f[语音输出] {text}) async def main(): persona load_persona() system_prompt build_system_prompt(persona) print(猫娘已上线输入 quit 退出。) while True: user_input input(你) if user_input.lower() quit: break # 游戏事件模拟手动输入“事件:xxx”触发主动回复 if user_input.startswith(事件:): event_msg user_input[3:] full_prompt f你注意到游戏里发生了这件事{event_msg}请用一句人设内的口吻回应。 else: full_prompt user_input reply chat_once(system_prompt, full_prompt) print(f猫娘{reply}) await speak(reply) if __name__ __main__: asyncio.run(main())7.4 运行结果示例猫娘已上线输入 quit 退出。 你事件:玩家残血击杀三人后赢得比赛 猫娘哇这一波也太帅了吧你是不是偷偷练过枪法快教教我 [语音输出] 哇这一波也太帅了吧你是不是偷偷练过枪法快教教我 你事件:玩家被对手狙击淘汰 猫娘呜哇对面那个狙击手也太不讲武德了下一局我们去找他报仇 [语音输出] 呜哇对面那个狙击手也太不讲武德了下一局我们去找他报仇这个最小示例略显粗糙但已经包含了大模型交互、角色扮演、语音合成三个关键链路。你可以把它理解为后续开发的地基。8. 常见问题与排查思路在实际开发和社区反馈中下面几个问题出现频率最高。问题现象常见原因解决思路猫娘回复很慢大模型 API 延迟高或 max_tokens 设置过大调小 max_tokens使用流式输出选择响应更快的模型猫娘人设不稳定system prompt 太短或没有 few-shot 示例补充人设细节增加 2~3 个示例对话适当降低 temperatureOCR 识别不准游戏画面动态模糊或界面文字太小提高截屏频率使用更高截图分辨率针对游戏特定界面做目标检测语音播报卡顿TTS 生成是同步的大模型等待期间没有声音先让猫娘说一句“嗯嗯”“我看看”然后再生成完整回复即流式语音提示玩家声音打断了 AI 语音但没生效打断逻辑没有覆盖到 TTS 播放线程使用全局播放锁ASR 检测到语音后立即调用 stop内存占用持续上涨OCR 模型常驻内存且每帧都识别降低识别频率只在事件可能发生时触发更高频识别API Key 泄露配置硬编码在代码里使用环境变量加入.gitignore生产环境使用密钥管理服务8.1 延迟优化流式输出的作用大模型完整生成可能需要好几秒如果等全部生成完再 TTS体验会很差。优化方案是启用流式输出先把第一句完整句子拿出来让 TTS 合成后续句子继续生成。这样猫娘能做到“边说边想”。def chat_stream(system_prompt, user_text): stream client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: system_prompt}, {role: user, content: user_text} ], streamTrue, ) collected [] for chunk in stream: if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content: collected.append(chunk.choices[0].delta.content) # 这里可以做句子检测把完整句子实时推给 TTS return .join(collected)建议用标点符号句号、感叹号、问号作为切分点每积累到一个完整句子就推给 TTS。8.2 上下文过长的处理长时间陪玩会导致 history 越来越长最终超出模型上下文限制。解决办法使用上文提到的摘要机制。对不重要的事件只保留类型和关键词比如“击杀”“阵亡”“比分变化”。考虑用向量数据库存储长期记忆按相似度召回。8.3 本地模型资源不足如果你只有 CPU 环境FunASR PaddleOCR 同时跑会占用大量 CPU建议OCR 降频到 3~5 秒一帧。FunASR 使用量化版本或切换成云 API。大模型优先用云端 API本地只跑轻量推理。9. 最佳实践与工程建议9.1 人设提示词工程建议把 persona 配置、few-shot 示例、运行时上下文分开管理。不要把所有内容都塞进 system prompt 里这样不方便调整也不方便做 A/B 测试。运行时上下文比如当前游戏事件应该作为独立的 user 消息传入而不是混入 system prompt。9.2 安全与合规边界AI 伴玩本质上是一个模拟人格的交互系统有几个红线要特别注意不要在提示词中诱导模型输出违法、暴力、色情、政治敏感内容。不要使用游戏外挂、内存读取、协议破解等手段获取游戏数据这会违反游戏用户协议也可能涉及法律风险。如果你要采集玩家语音必须明确告知用户并获取授权遵守数据隐私法规。涉及大模型 API 调用的内容需要遵循模型提供方的内容政策和使用条款做好输出过滤防止模型在无人监督时“跑偏”。我们自己的做法是默认开启内容审核模型输出中如果包含风险关键词直接不播放语音用一条中性回复替代。9.3 日志与可观测性AI Agent 应用很容易出现“玄学问题”所以日志一定要足够详细。建议至少记录每轮请求的完整 prompt。模型返回的原始输出。每步耗时截图、OCR、ASR、LLM、TTS。是否发生打断以及打断的原因。可以用loguru或标准logging输出到文件方便回放问题。9.4 配置管理所有配置统一放到 YAML 文件通过环境变量覆盖敏感字段import os import yaml def load_settings(): with open(config/settings.yaml, r, encodingutf-8) as f: settings yaml.safe_load(f) settings[api_key] os.getenv(LLM_API_KEY, settings.get(api_key, )) return settings9.5 性能与成本优化伴随型 AI 的成本大头在大模型调用。可以引入分级路由简单回复“嗯嗯”“我在听”走免费或低成本的轻量模型。复杂事件和玩家请求走完整大模型。高频状态识别走本地小模型不需要每次都问大模型。这种方式可以有效降低调用成本同时保持响应速度。9.6 可维护性用事件驱动替代硬编码流程不要把所有逻辑写在 main 函数里。事件驱动的优势在于可以随时加新模块。例如后续想加入“玩家心率检测”“直播弹幕互动”只需要往事件总线上注册新的事件源和消费者不需要改主循环。10. 总结与下一步方向项目到目前这个阶段我们验证了一件事AI 角色伴玩不是单纯“接一个大模型 API”就行它需要感知层、记忆层、表达层的协同设计。感知层负责理解游戏状态记忆层负责保持人格一致性表达层负责用语音和文本输出合适的反馈。这三层每层都有不少细节可以打磨但整体架构并不复杂核心是用事件驱动把模块之间的耦合降低。如果你也想做类似项目建议按这个顺序推进先跑通最小闭环手动输入事件 → LLM → 文本回复。再加入语音链路ASR TTS。再加入画面感知截屏 OCR 事件检测。最后再优化角色记忆、长期人设、成本控制。下一步我们准备把游戏事件识别换成更精准的视觉检测模型同时针对卡拉彼丘的 UI 元素做一些专门的标注数据提高事件提取准确率。还会尝试让猫娘在不同游戏阶段有不同的情绪状态比如开局时元气满满连败后语气低落并主动安慰玩家。欢迎在评论区交流你的 AI 伴玩项目思路如果有兴趣后续我也可以把“如何用 GPT-SoVITS 定制猫咪音色”“如何在本地跑通流式 ASR”单独写成教程。