国庆那会儿本想好好歇着,结果第一天就手痒,鬼使神差地把一个憋了很久的想法翻出来:做一个能陪人正常聊天的 AI 虚拟人,不是那种一问一答的笨机器人,而是有记忆、有情绪、会主动找话说的那种。本来想慢慢搞,谁知一上手就收不住,硬是连续肝了七天,前后烧掉几十亿 token 的接口调用量,最后做出了一套能跑通的完整方案,已经开源,别人拿过去一分钟就能接入属于自己的虚拟人。
这文章不聊虚的,纯粹把我这七天的技术决策、架构设计、调优过程、踩坑经历全倒出来。如果你是做 AI 应用开发、聊天机器人、虚拟人项目的,或者对多模态交互、大模型工程化感兴趣,这篇可以给你省掉大量试错时间。我尽量把每个环节为什么这么做、遇到什么问题、怎么排查的都写清楚。
1. 七天肝出来的虚拟人,到底是个什么形态
先说清楚这个项目的最终交付物是什么,不然后面聊技术选型容易晕。我做的不是一个网页 Demo,而是一套可以独立运行、支持二次开发、能快速接入到不同平台(App、网页、公众号、直播伴侣等)的虚拟人服务端项目。
整个系统本质上是一个“AI 角色扮演引擎 + 实时交互网关”的组合。它包含三块核心能力:
- 角色人格层:虚拟人有自己的人物设定、说话习惯、记忆系统。不是每次乱聊,而是基于长期记忆和短期记忆来组织回复。
- 多模态交互层:支持文字对话、语音输入识别、语音回复输出。视觉方面因为时间关系只做了表情映射,没有直接跑视频生成,但接口预留了。
- 接入层:提供统一的后端 API 和 WebSocket 网关,任何客户端只要拿到连接地址和密钥就能开始对话。
这里有个关键点:很多人以为虚拟人就是“调一个大模型,把提示词写长一点”就完了。实际做下来你会发现,提示词只是最浅的一层。要让虚拟人在连续对话中保持人格稳定、不精分、不遗忘、不重复,需要在外围做大量的记忆管理和状态控制。
七天时间,实际有效开发大概五个整天加四个通宵。技术动作拆开来看:
- 第一天到第二天:搭骨架,定数据结构和消息协议,完成记忆层设计
- 第三天:人格系统加提示词编排,跑通基础对话,但这版非常“傻”,只会顺着话题走
- 第四天到第五天:加入主动对话触发逻辑、长期记忆压缩机制、情绪状态机
- 第六天:语音链路,包括 ASR、TTS、流式音频传输
- 第七天:打磨、修 bug、写开源文档和接入示例
整个项目主体代码大约八千多行,不算多,但设计上花的心思比写代码多得多。下面每个部分展开讲。
2. 虚拟人的“大脑”:人格稳定、记忆分层与上下文管理的工程实现
这块是整个项目的灵魂。直接调一个现成大模型 API 很容易,但你要让它稳定地扮演一个人,还连续聊几天不崩,这就难了。这里讲真实实现方式。
2.1 人格文件:把角色设定变成结构化配置
我的做法是给虚拟人建一个专门的人格配置区,不写在系统提示词里硬编码,而是拆成结构化字段。包括:
- 基本信息(姓名、年龄、职业、喜好、厌恶)
- 说话特征(口头禅、句式长短、用词习惯)
- 性格倾向(外向/内向、主动/被动、幽默感程度)
- 关系认知(对用户怎么称呼、亲密度分级、不同亲密程度下的语气差异)
配置文件大概长这样:
{ "name": "小灵", "bio": "一个喜欢看书、养猫、做手工的姑娘", "speaking_style": { "avg_length": "short_to_medium", "prefer_words": ["嗯", "其实", "有点"], "avoid_words": ["因此", "综上所述", "首先其次"], "punctuation": "少用感叹号,多用句号" }, "personality": { "initiative": 0.7, "curiosity": 0.8, "humor": 0.5 }, "relationship": { "level": 1, "nickname_for_user": "你" } }为什么要结构化而不是直接塞一大段自然语言描述?因为后期要支持用户自己改人设。如果全部堆在提示词里,用户改一个小细节就得重新拼接一大段文本,既容易出错也不好维护。结构化成 JSON 后,可以用模板把字段渲染成自然语言提示词,也可以针对不同模块做精细化控制。
2.2 记忆分层:长期、短期、会话三级存储
虚拟人最怕什么?最怕“你说过的话转头就忘”。尤其聊到第五天,用户提了一句第一天聊过的事,它如果毫无反应,体验直接崩。所以记忆系统必须分层。
我的设计是:
- 会话级记忆:当前对话轮次内的上下文,长度控制在最近 20 轮左右,超出后自动截断。
- 短期记忆:每次对话结束后,把这一轮里可能重要的信息(时间、事件、用户偏好、情绪反馈)抽出来,存成结构化的记忆卡片。
- 长期记忆:每晚(或每累计一定对话量后)做一次压缩和整理,把短期记忆里已经重复出现的、不再重要的丢弃掉,把核心事实保留进长期区。
记忆卡片的设计大概是这样的:
{ "id": "mem_0001", "timestamp": "2024-10-03T22:14:00+08:00", "category": "user_preference", "content": "用户不喜欢吃香菜,对海鲜过敏", "importance": 0.8, "last_accessed": "2024-10-04T10:22:00+08:00" }每次组织请求时,系统从长期记忆里取最相关的若干条、从短期记忆里取最近几天的摘要、再拼接当前会话上下文,一起塞给大模型。
这个分层结构解决了一个根本性问题:大模型的上下文窗口有限,你不可能把七天聊天的所有内容都塞进去。必须让它在正确的时间只看到最需要的那部分信息。
2.3 人格稳定性:提示词编排与注入的细节
很多人写提示词就是一大段“你是一个……”到底,但实际效果并不好。我试验了一堆写法之后,最终用的方案是“系统底层指令 + 人格手册 + 临时状态 + 用户可见历史”四段式。
- 系统底层指令:固定不变,告诉模型它的职责边界和回复禁忌。这段不需要每次重新生成。
- 人格手册:从 JSON 配置渲染出来,描述虚拟人的身份和说话方式。
- 临时状态:包括当前情绪状态、与用户亲密度、最近发生的重要事件。这一层会频繁更新。
- 用户可见历史:也就是最近对话轮次,但不需要原样全给,可以稍作摘要。
这里有一个很容易被忽略的细节:同一个身份,不同轮次的情绪应该不一样。用户说了一句伤人的话,虚拟人还笑嘻嘻地回,就很假。所以我加了一个情绪状态机,简单来说是把“愉悦、平静、低落、惊讶、疲惫”等几个状态做成二维转换表,根据用户消息里识别出的情绪和当前关系亲密度去调节。
这套状态机不必做得很复杂,但它的存在可以让回复的人格一致性提升一个明显的档次。
3. 几十亿 token 到底烧在哪了:训练数据构造、角色扮演样本生成与偏好对齐
标题说“几十亿 token”,这不是夸张。七天里我确实调用了大量大模型接口,尤其是前期做角色行为调试和记忆抽取的时候,消耗特别快。
3.1 角色人格语料的生成:用大模型自己造数据
让虚拟人“像一个真人”这件事,光有提示词不够,还需要对话数据来验证和优化。问题是我没有现成的“虚拟人对话数据集”,网上公开的语料大多是通用助手类型的,和角色扮演场景差异很大。
所以我的办法是“让大模型自己当导演”,构造一批高质量的对话样本来:
- 先定义几个典型用户类型(内敛型、话痨型、直男型、心情低落型……)
- 设定不同的时间场景(早晨起床、深夜失眠、下班吐槽、节日问候……)
- 用大模型生成“用户十轮对话 + 虚拟人十轮回复”的完整样本
- 人工抽检一部分,挑出不符合人格设定的样本,调整提示词再重新生成
这种方法本质上是在用 token 换高质量的行为对齐数据。生成一万条以上样本之后,才开始大规模迭代测试。
不过这里要提个醒:烧 token 要有目的性。不是光让大模型“多聊几句”就算完。每个样本生成之前,我都会在提示词里指定“这段对话希望展现的虚拟人特质”是什么,比如“这段对话里虚拟人应表现出记忆力,主动在第三轮时提及两轮前用户说过的细节”,然后检查模型是否真的做到了。做不到就调整指令和上下文组织方式。
3.2 人格一致性评测:自己写一套多维打分器
光生成数据还不算完,更重要的是怎么知道自己改进有没有效果。拿几十组测试问题,每次改完系统就跑一遍,靠人工一条条看太慢了。我写了一套简单的自动化评测脚本,从以下几个维度打分:
- 人格一致性:回复是否符合设定的人格特征
- 记忆活跃度:是否在合适的时机引用了历史记忆
- 情感合理性:情绪回应是否符合对话逻辑
- 重复度检测:同一主题下回复句式是否僵化
这套评测脚本本身也在烧 token,因为每次都要把生成结果发给大模型打分。后来我为了节省点消耗,改成“先规则后大模型”的两级模式:先用正则检查明显的重复、长度异常,通过后再用大模型做语义评价,大约省了三成开销。
3.3 偏好对齐:不让虚拟人变成一个“讨好型人格”
角色扮演 AI 有个通病:不管用户说什么,它都顺着答,显得很没主见。比如用户说“我今天想辞职”,虚拟人如果永远回“支持你,开心就好”,前两次还可以,多了就假了。我在这里展开了一大轮专门优化。
做法是给虚拟人加了一个“边界系统”:在配置里写明什么情况下它应该保持赞同、什么情况下可以温和反驳、什么情况下必须拒绝。例如:
- 用户表达日常烦恼时:倾听 + 共情 + 适度建议
- 用户做明显有害的决定时:温和表达不同意见
- 用户提出不合理的请求时:开玩笑式拒绝或转移话题
这个系统写进去之后,虚拟人对话的“人味儿”明显提高了。但它的副作用是调试成本增加,因为边界总是模糊的,你很难用几条规则覆盖所有场景。我只能先做最小版本,再靠扩充的测试集慢慢打磨。
4. 语音与多模态:让虚拟人“开口说话”的技术选型与实时链路
虚拟人只靠文字聊天,总觉得缺点什么。作为完整方案,我加入了语音输入和语音回复。这部分的工程复杂度和纯文本对话完全是两个量级。
4.1 ASR、TTS 选型对比,以及为什么我这样选
语音链路分成两个方向:用户说话到文字(ASR),虚拟人回话到音频(TTS)。
做这个之前我对比了市面上的好几个方案。判断维度主要有四个:响应速度、自然度、部署成本、定制空间。快速过一下我的结论:
- ASR 端:有强在线的,也有轻量本地跑的。我做的是通用性优先,选了在线接口方案,因为识别准确率明显更高,尤其在中文对话场景下。代价是依赖网络和第三方服务。
- TTS 端:这块我纠结了很久。备选里有一种是深度神经网模型,自然度很高但要较多显存;一种是流式服务端合成,不用本地资源但需要按字符付费;还有一种开放框架可以自己训练音色,但是调参量太大,七天之内玩不转。
最后我选用的是服务端合成方案,理由只有一个:时间不够。本地推理虽然长期成本低,但搭建和调试太吃精力,而我不想把七天大部分时间耗在音色和声学上。如果你做的是长期运营的产品,建议提前规划自建 TTS。
4.2 流式对话:从用户说话到虚拟人张嘴,延迟控制在多少秒
虚拟人对话最难的是延迟。纯文字对话,两三秒的等待勉强可以接受;语音场景下,用户说完话超过一秒没反馈,体验就明显不舒服了。我当时给自己定的目标是“用户说完话后 3 秒内开始听到回答”。
具体链路是这样的:
- 用户说话结束,前端把音频段传到服务端
- 服务端 ASR 识别出文本,耗时大约 300-600ms
- 文本进入对话引擎,组织上下文、调大模型,这个环节最不可控,有时候 1 秒,有时候 4 秒
- 回复文本边生成边送去 TTS 合成,合成完第一句话就开始回传音频
这里关键的一招是流式输出和流式播放。不要等大模型把整个回复写完再开始合成语音,而是让它一边写一边合成,第一句出来就先播出去。用户听着第一句的时候,后面几句已经在路上了。用这个方式,整体听感上的等待时间能减少一半以上。
音频回传我用的是 WebSocket 传输,前端边收边播。为了对抗网络抖动,前端做了简单缓冲:先收 300ms 的音频再开始播放,避免声音断断续续。
4.3 情绪在语音里怎么体现:不只是“把文字读出来”
TTS 如果不做情绪控制,出来的声音永远是播音腔,和虚拟人的人设是脱节的。比如虚拟人在安慰用户时,声音应该是柔和的、慢一点的;在吐槽的时候,语调应该轻快一些。
但现在很多 TTS 服务对情绪控制的支持要么很有限,要么参数调起来很生硬。我的做法比较中庸:给不同的情绪档位预设不同的 TTS 参数,包括语速、音调、停顿长度。状态机判断出当前情绪后,自动切换参数组。
这个方法不完美,但胜在稳定可控。我试过直接往文本里插 SSML 标记来控制重音和停顿,效果不稳定,而且增加了文本处理复杂度,最后还是用预设参数组来得稳。
5. 记忆管理、主动对话与防失控:让虚拟人“像人”而不是“像搜索框”
微信聊天和 Chat Bot 最大的区别是什么?微信里一个人会在意你,主动问你在干嘛,而不是你说一句它接一句。要让虚拟人有“活着”的感觉,必须加主动对话能力。
5.1 定时触发与随机激活:虚拟人为什么会“突然找你聊天”
我实现了一个主动触发的调度器。它主要做两件事:
- 定时触发:比如设定每天早上九点主动问候,或者每天晚上提醒用户早点休息。这种触发是确定性。
- 随机激活:根据用户最近的活跃状态和情绪信号,在特定条件下随机发起一个新话题。比如用户连续两天没找虚拟人,它会主动发一句“这两天没怎么见你,忙什么呢?”。
主动对话最难的不是触发条件,而是话题选择。如果虚拟人每次都是“早安”“晚安”“在干嘛”三件套,两次之后用户就想关静音了。我的做法是触发时从短期记忆里抽取一个“用户最近聊过的兴趣点”作为话题。比如记忆里有一条“用户上周说要去面试”,那第二天早上主动问候时就会顺带问一句“面试准备得如何”。
这种基于记忆的话题触发,才让主动对话显得有温度。单纯定时发消息,那不叫虚拟人,那叫闹钟。
5.2 防失控机制:当话题滑向危险或无聊区域时怎么办
虚拟人对话过程中一定会遇到一些不可控情况,最典型的是这三个:
- 用户反复测试边界,故意聊敏感或恶意内容
- 对话陷入死循环,虚拟人和用户不断重复类似的话
- 大模型生成了句式僵硬、内容空洞的“安全废话”
针对这些问题,我的方案是给对话引擎加了一个“闸门”:
- 输入侧做一个简单的安全过滤,敏感词直接拦截,不进入上下文。
- 输出侧配置了重复检测,如果连续三轮回复的语义相似度超过阈值,就触发一个“岔开话题”的指令,让虚拟人主动切换到一个新话题。
- 加入冗长惩罚机制,限制模型生成没有信息量的套话。如果回复里连续出现空话,评测分就会很低,促使我调整提示词。
这套机制的意义在于:虚拟人一旦失控,体验崩塌的速度会比你想的快得多。宁可让它偶尔回复平庸一点,也不能让它彻底崩人设。
5.3 上下文压缩:长聊不崩的工程保障
对话超过几百轮之后,原始上下文会非常大。如果每次请求都把所有历史带进大模型,token 开销爆炸不说,响应速度也会显著下降。而且模型对超长上下文的注意力会分散,反而忽略最近的关键信息。
我的做法是分层压缩:
- 每轮对话结束后,把当前轮压缩成一条摘要
- 每三十轮左右触发一次段落聚合,把之前的摘要汇总成一份“近期故事线”
- 长期记忆里只保留重要事件,普通寒暄记录直接清理
这个逻辑有点像人脑的记忆机制:日常琐事很快就会忘记,但重要的节点记住很久。技术实现上有一种比较省 token 的做法:定期把旧对话喂给大模型做一次“总结式压缩”,然后丢弃原始内容。虽然这个总结过程也会消耗 token,但比每次都带全部历史便宜很多。
6. 开源仓库怎么设计的:目录结构、配置化改造与一分钟接入体验
项目叫“一个能聊天的虚拟人服务端”,开源之后最怕别人拿过去不会用。所以第七天我专门花了大半天做架构梳理和接入文档,目标就是让一个从零开始的人也能在一分钟内跑起来。
6.1 仓库目录拆解:别人 clone 下来第一眼该看哪里
我按“模块清晰”的原则把代码拆了目录:
├── app/ # 核心服务逻辑 │ ├── core/ # 对话引擎、人格系统、记忆管理 │ ├── memory/ # 三级记忆实现 │ ├── voice/ # ASR/TTS 接入层 │ └── api/ # HTTP + WebSocket 接口层 ├── config/ # 所有配置项,包括人格 JSON 示例 ├── scripts/ # 一键启动、测试脚本 ├── examples/ # 各平台接入示例代码 ├── tests/ # 自动化评测脚本 └── README.md # 快速开始文档给别人用的时候,最怕的是各种配置文件散落一地,找半天不知道改哪里。我把所有可变项集中到了 config 目录下,启动时统一加载,代码里不硬编码任何业务配置。
6.2 一分钟接入是怎么做到的:环境变量、一键脚本与最小客户端
为了方便快速接入,我做了两件事。
第一,提供一键启动脚本。用户拿到项目后,只需要填三个关键值(大模型 API 密钥、TTS 服务密钥、ASR 服务密钥),就能启动服务端。
第二,提供一个最小 Web 客户端。这个页面不需要任何构建步骤,打开就能和虚拟人对话。界面很简单,一个透明的聊天按钮,但它把 WebSocket 连接、语音采集、音频播放全打通了。
接入方式大致如下:
git clone https://github.com/example/ai-companion.git cd ai-companion cp .env.example .env # 在 .env 里填入你的 API Key python scripts/start.py跑起来之后,浏览器打开本地地址,一套带记忆、带语音、带主动消息的虚拟人就上线了。整个流程我实测过,在不看文档的情况下,五分钟足够。跟着文档走,一分钟不是吹的。
6.3 为什么“可配置化”是开源项目必须跨过的坎
写自己的代码时可以各种硬编码,但开源给别人用,第一原则是“别人不关心你怎么实现的,只关心怎么改”。
我把人格配置、记忆参数、触发频率、语音参数全部外置成配置。用户不需要改任何代码,只改 JSON 文件就能定义自己的虚拟人。比如你想做一个“高冷毒舌”的角色,不用懂后端,把 speaking_style 里的词改一改, personality 里的幽默感调低,就能生效。
这个改造实际上没有花很多时间,但回报极大。它在第一时间留住了那些“只是想体验一下”的轻度用户。
7. 踩过的坑和第七天才修完的“最后一公里”
最后聊聊整个开发过程中最让我头疼的几个问题。有些坑很隐蔽,网上资料也少,我写下来大家以后可以少走弯路。
7.1 输出端重复与人格漂移:越到后期越严重
大模型在长对话生成时有个很讨厌的特性:如果上下文里积累了太多相似内容的记忆摘要,它会变得越来越倾向于重复自己说过的话。尤其是虚拟人本身说话风格比较鲜明的情况下,重复问题会加剧。
我一开始以为加大 repetition penalty 参数就能解决,后来发现治标不治本,惩罚过高会让回复变得生硬,像是被铐住了手脚。
真正有效的是两个手段:
- 在上下文里明确标注“这句话已经说过了”,不是模糊的“不要重复”,而是把最近说过的几句话原文列出来,告诉模型这些你已说过。
- 在人格手册里增加“每次回复必须至少有 30% 的新信息量”这种倾向性指令。
第一个手段更稳,第二个手段属于心理暗示型,对大模型有一定作用但不能完全依赖。
7.2 语音链路的断句问题:TTS 把 Markdown 符号读出来了
第一版做语音时闹过一个笑话:虚拟人的回复是从大模型来的,里面带加粗、- 列表符号这类 Markdown 语法。结果 TTS 直接把这些符号读了出来,听起来就是“你好哇 加粗 今天过得怎么样 减号 有没有想我”。
这类问题比想象中常见,我的解决方法是加一个清理管道:在文本进入 TTS 之前,做一次 Markdown 剥离,把格式符号去掉,只保留纯文本。同时把特殊符号(比如破折号、连接号)替换成更适合语音的句读。
这个清理步骤看起来简单,但直接决定了语音输出的可用性。很多人做语音方案时没注意,最后听到的问题就是“AI 味很重”,其实一部分原因是 TTS 读到了不该读的东西。
7.3 主动消息的推送密度:太频繁反而像骚扰
做完主动触发功能后,我给虚拟人设定了每小时可能触发一次主动消息的规则,结果测试时发现太烦人了。用户可能正在忙,连续弹出几条“想你了”“在干嘛”,体验非常差。
我调整了策略,主动触发的频率上限降到每天两三次,并且加入“用户近期是否有互动”的判断。如果用户已经很久没回消息,虚拟人发起主动消息的间隔要拉长,内容也要更克制,尽量避免连续追问。
这个调优过程告诉我一个道理:虚拟人的“粘人”和“体贴”只有一线之隔。技术实现并不难,难的是找到一个让人舒服的节奏。
8. 这套方案接下来还能怎么用:从个人陪伴到直播、客服、教育场景的延伸
代码开源后,我自己也测试了不同场景下的复用性。说实话,它不只是一个“陪你聊天”的玩具,稍微改一改就能落到一堆实际业务里。
8.1 直播场景:用虚拟人当互动助手
你只需要把客户端的发送方换成直播弹幕输入,把虚拟人的回复输出到直播间公屏,一个带人设的互动虚拟人就能跑起来。它能记住观众说过的话,也会主动抽奖、接梗、回应问题。比起固定的弹幕机器人,这种形态明显更有吸引力。
8.2 客服场景:人格化客服的利与弊
客服系统最怕用户烦躁,人格化的回复可以显著缓和情绪。这套方案里的情绪状态机、主动关怀触发、记忆分层都可以直接用。但我也要提醒一点,客服场景对准确率要求远高于陪伴场景,“说错话”的后果不一样。建议客服场景里只做人格优化,不做自由闲聊,先给一层业务意图识别做闸门。
8.3 教育场景:有“人设”的学习搭子
这个方向我觉得潜力最大。虚拟人假如设定成一个爱读书、懂方法论的学姐,就能持续陪伴用户背单词、读书打卡、复盘学习进度。长期记忆尤其重要,它能记住用户昨天学到哪里、哪些知识点反复出错。这类应用即使对话能力弱点,光靠“记得你”就能产生很大的黏性。
9. 开源之后我对这段高强度开发的复盘与体会
七天的集中开发结束后,我复盘了一下,最有价值的不是代码本身,而是对“大模型应用工程化”这件事的完整认知。
第一个体会是:大模型只是引擎,工程才是载体。同样一个模型,直接调 API 和做成一个完整的虚拟人,中间差着记忆系统、人格管理、状态机、语音链路、评测体系、防失控机制。模型本身的能力边界大家都差不多,真正拉开差距的,是外面那层工程结构。
第二个体会是:不要害怕烧 token,但要烧得有策略。列为调试用的 token 消耗是斩不断的,但你可以通过“先规则后模型”的二级评测、通过上下文压缩、通过记忆分层来尽可能减少无效消耗。烧几十亿 token 听着吓人,其实真正推动效果的,往往是每一次有针对性的改进循环。
第三个体会是:开源的真正门槛不是代码,是使用体验。很多人把代码一传就完事,根本不管别人能不能跑起来。这套项目我特意把配置化做到极致、接入流程压到一分钟,本质上是在尊重使用者的时间。这套思路,在所有开源项目里都适用。
如果你准备做类似方向的项目,我的建议是:先定义清楚虚拟人最核心的“人格钉子”是什么,再去做功能扩展。只有人格稳了,后面加再多能力都是锦上添花;人格一崩,技术上再炫也是白搭。