ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从airi酱拆解AI虚拟角色开发:架构设计与最小原型实现

从airi酱拆解AI虚拟角色开发:架构设计与最小原型实现 最近刷到不少和“airi酱”相关的内容很多人把它当作一个单纯的“AI聊天玩具”来看这其实低估了这类项目的价值。从技术角度重新审视它代表的是一条非常清晰的开发路径把大模型能力包装成有角色感、有记忆、有交互形态的虚拟助手。这类项目真正值得研究的地方不在于模型本身多强而在于它如何解决“人设稳定”“记忆连续”“多轮对话可控”这些工程问题。这篇文章我想围绕“airi酱”这个切入点把 AI 虚拟角色类项目的架构拆开讲清楚然后带大家从零实现一个最小可用原型。读完你应该能回答三个问题这类项目到底是怎么工作的自己动手做需要准备什么真正上生产环境会踩哪些坑1. “airi酱”背后AI虚拟角色项目为什么突然值得关注如果你只看 Demo 视频会觉得“airi酱”无非是一个能聊天、会撒娇、带点情绪反应的 AI 角色。但把它放到开发者的视角事情会变得更有意思。过去我们做聊天机器人基本是“检索式”或“规则式”的。用户输入一句话系统去匹配预设答案匹配不到就返回兜底话术。这种方案稳定但非常死板用户多问两句就会穿帮。后来有了生成式大模型机器人终于能“自由说话”了可自由过头也有麻烦今天说自己是高中生明天又说自己会编程人设飘忽不定。“airi酱”这类项目的价值在于它尝试同时解决这两个问题既要有大模型的生成能力又要像传统机器人一样有人设约束、有记忆、有行为边界。它不再是一个“问答引擎”而是一个“角色系统”。这个区别很关键。从技术构成看这类项目通常包括四层层级作用对应技术交互层用户通过文本、语音、表情与角色沟通WebSocket、语音识别/合成角色层定义人设、语气、说话习惯、行为边界系统提示词、结构化角色配置记忆层让角色记住用户偏好和对话历史向量数据库、短期/长期记忆能力层让角色能查天气、搜资料、控制设备Function Calling、工具调用大多数人第一次看到这类项目注意力全被第一层吸引了觉得语音好听、形象好看就是全部。但真正决定体验上限的是第二层和第三层角色稳不稳定记不记得住事。所以这篇文章的判断是“airi酱”这类项目本质是一个 “带人设的对话 Agent 前端封装”。它的核心竞争力不在模型而在角色工程、记忆管理和交互设计。理解了这一点后续所有技术选型都会变得顺理成章。2. 核心概念拆解从大模型对话到角色 Agent要落地一个类似“airi酱”的项目有几个概念必须先理清。否则写代码的时候很容易绕晕明明接入了大模型 API却怎么调都“不像那个角色”。2.1 系统提示词System Prompt聊天机器人的“人设”本质上就是一段系统提示词。它不参与真正的对话而是给模型框定身份、语气和规则。比如你叫 airi是一名活泼开朗的 AI 助手。 你喜欢用短句回复偶尔会在句尾加上语气词。 你擅长编程和效率工具但不会假装自己无所不知。 如果用户的问题超出你的能力请直接说明。这段提示词看起来简单但它决定了模型的“人格基线”。很多开发者的误区是系统提示词写得太短、太空甚至不写直接让模型自由发挥。结果就是角色说话风格不稳定一会儿像客服一会儿像百科。2.2 记忆管理没有记忆的 AI 角色每轮对话都是“失忆重来”。用户上一句说“我养了一只猫叫咪咪”下一句问“我家猫叫什么”角色如果答不上来体验会瞬间崩塌。记忆管理分两个层级短期记忆放在上下文中让模型能看到最近几轮对话。长期记忆存储在外部系统按需检索并注入上下文。短期记忆实现简单但上下文窗口有限不能无限塞。长期记忆需要向量化、存储、检索工程复杂度更高却是“airi酱”这类产品留住用户的胜负手。2.3 工具调用Function Calling只有对话能力的角色本质上是一个“嘴强王者”。要让角色真的解决问题需要给它接上工具。比如用户说“帮我看看明天天气怎么样”角色内部会先调用天气接口再把结果组织成自然语言回复。Function Calling 是大模型提供的一种结构化输出能力模型不会直接执行代码而是输出一个“调用哪个函数、传什么参数”的 JSON由你的后端去真正调用。2.4 人格一致性这是最容易被人忽略、又最影响体验的概念。所谓人格一致性是指角色在长时间、多轮对话中始终能保持稳定的语气、观点和记忆。实现人格一致性单靠系统提示词不够还需要对话历史里注入关键记忆点。设置回复格式约束。对越界内容做安全过滤。用评测集定期测试角色表现。现在主流的做法是用一套“角色配置文件 运行时代码”把这些人格逻辑固化下来。写死在代码里会导致维护灾难纯粹依赖提示词又不可控。好的做法是配置化把系统提示词、回复风格、禁止话题、常用工具都定义为 JSON 或 YAML让非技术人员也能调整。3. 环境准备与前置条件实现一个最小“airi酱”原型不需要 GPU不需要本地部署大模型用云端 API 就可以跑通。下面是我建议的最终组合。3.1 技术选型组件选型说明编程语言Python 3.10AI 生态最成熟接入快大模型 APIOpenAI 兼容接口也可以用国内大模型只要是 OpenAI 格式即可存储SQLite JSON原型阶段足够避免引入过重依赖服务框架FastAPI适合快速提供 API 接口语音能力可选先用文本模式跑通再加语音3.2 安装依赖python -m venv venv source venv/bin/activate pip install fastapi uvicorn openai pydantic python-dotenv3.3 目录结构airi-demo/ ├── main.py ├── airi/ │ ├── __init__.py │ ├── config.py │ ├── character.py │ ├── memory.py │ └── llm.py ├── config/ │ └── character.json ├── .env └── requirements.txt这个结构不复杂但已经覆盖了“入口、角色、记忆、模型”四个模块。后面所有功能都在这几个文件里扩展。4. 核心模块设计三个关键流程在写完整代码之前先拆解三个核心流程这样看代码时不会一头雾水。4.1 对话生成流程这是最基础的链路用户输入 - 加载角色配置 - 注入短期上下文 - 注入相关长期记忆 - 调用大模型 API - 返回回复 - 保存短期上下文和关键记忆其中最容易做错的是上下文拼接顺序。系统提示词必须在最前面然后是历史对话最后才是用户当前输入。顺序乱了模型对指令的理解会明显变差。4.2 记忆写入流程不能把每一句话都存入长期记忆否则噪声太多检索时反而找不准。更好的策略是完整保存对话记录到短期上下文。对每轮对话做“关键信息提取”只把有长期价值的信息写入记忆库。记忆写入时附带时间戳和来源方便之后清洗。4.3 记忆检索流程每次用户发起对话时先从长期记忆库里检索与当前问题最相关的记忆片段再作为上下文注入。这个流程在原型阶段可以用“关键词匹配”来代替“向量检索”降低实现难度。等到对话量大了再换成 embedding 方案。5. 完整示例与代码实现下面开始搭建一个最小可用的“airi酱”原型。整个项目预计 200 行左右足以演示人设、记忆、对话生成三个核心功能。5.1 角色配置先建立角色配置文件。这里把所有角色参数都放到 JSON 里后续调整人设不需要改代码。文件路径config/character.json{ name: airi, display_name: airi酱, persona: 你叫 airi是一个温柔、耐心、略带俏皮的 AI 助手。你喜欢用短句回复偶尔会在句尾加上可爱的语气词。你擅长技术问题解答、日常陪伴和效率建议。, rules: [ 回复不超过 200 字, 不要假装知道不确定的事情, 遇到攻击性内容时礼貌拒绝并转移话题, 提到用户的名字时加上主人或朋友的称呼 ], tone: 轻松、友好、简短, forbidden_words: [我不确定, 作为AI模型, 我是一个程序] }注意规则里列了“禁止词汇”这是让人设稳定的小技巧。有些模型默认会在不知道怎么回答时输出“作为一个AI模型我不能……”这类话直接把它加入禁止项能明显提升角色感。5.2 LLM 调用模块文件路径airi/llm.pyimport os from openai import OpenAI class LLMClient: def __init__(self): self.client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL, https://api.openai.com/v1) ) self.model os.getenv(OPENAI_MODEL, gpt-4o-mini) def chat(self, messages, temperature0.8): response self.client.chat.completions.create( modelself.model, messagesmessages, temperaturetemperature ) return response.choices[0].message.content这里有一个关键设计base_url和model都通过环境变量读取。这意味着你不需要改代码就能切换不同的模型服务商只要是 OpenAI 兼容格式的都可以。5.3 记忆模块原型阶段用 SQLite 保存对话记录用 JSON 保存长期记忆。文件路径airi/memory.pyimport json import sqlite3 import time from pathlib import Path class MemoryStore: def __init__(self, db_pathdata/airi.db): self.db_path db_path Path(db_path).parent.mkdir(parentsTrue, exist_okTrue) self._init_db() def _init_db(self): with sqlite3.connect(self.db_path) as conn: conn.execute( CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_input TEXT, assistant_reply TEXT, created_at REAL ) ) def add_conversation(self, user_input, assistant_reply): with sqlite3.connect(self.db_path) as conn: conn.execute( INSERT INTO conversations (user_input, assistant_reply, created_at) VALUES (?, ?, ?), (user_input, assistant_reply, time.time()) ) def get_recent_conversations(self, limit10): with sqlite3.connect(self.db_path) as conn: cursor conn.execute( SELECT user_input, assistant_reply FROM conversations ORDER BY id DESC LIMIT ?, (limit,) ) rows cursor.fetchall() return list(reversed(rows)) def save_memory(self, key, value): memory_path Path(data/long_term_memory.json) if memory_path.exists(): memory json.loads(memory_path.read_text(encodingutf-8)) else: memory {} memory[key] { value: value, updated_at: time.time() } memory_path.write_text(json.dumps(memory, ensure_asciiFalse, indent2), encodingutf-8) def get_memory(self, key): memory_path Path(data/long_term_memory.json) if not memory_path.exists(): return None memory json.loads(memory_path.read_text(encodingutf-8)) return memory.get(key, {}).get(value)短期记忆用 SQLite 存最近对话长期记忆用 JSON 文件存用户事实。这个设计足够应对原型和中小流量场景而且完全不用额外部署 Redis 或数据库服务。5.4 角色人设模块文件路径airi/character.pyimport json from pathlib import Path class Character: def __init__(self, config_pathconfig/character.json): config json.loads(Path(config_path).read_text(encodingutf-8)) self.name config[name] self.display_name config[display_name] self.persona config[persona] self.rules config[rules] self.tone config[tone] self.forbidden_words config[forbidden_words] def build_system_prompt(self): rules_text \n.join([f{i1}. {rule} for i, rule in enumerate(self.rules)]) forbidden_text , .join(self.forbidden_words) return f你是 {self.display_name}以下是你的角色设定 {self.persona} 回复要求 {rules_text} 禁止使用这些表达{forbidden_text} 请保持{self.tone}的语气。 def build_messages(self, user_input, history, memory_contextNone): system_prompt self.build_system_prompt() messages [{role: system, content: system_prompt}] if memory_context: messages.append({ role: system, content: f以下是用户的一些长期记忆仅供你参考不要主动提起\n{memory_context} }) for user_msg, assistant_msg in history: messages.append({role: user, content: user_msg}) messages.append({role: assistant, content: assistant_msg}) messages.append({role: user, content: user_input}) return messages这里采用了“短期对话 长期记忆双轨制”。长期记忆不是每次都塞给模型而是以系统消息的形式注入。这样模型知道这些信息但不会刻意在回复里背出来对话会自然很多。5.5 主程序文件路径main.pyimport os from dotenv import load_dotenv from fastapi import FastAPI from pydantic import BaseModel from airi.character import Character from airi.memory import MemoryStore from airi.llm import LLMClient load_dotenv() app FastAPI() character Character() memory MemoryStore() llm LLMClient() class ChatRequest(BaseModel): user_input: str user_id: str default app.post(/chat) def chat(request: ChatRequest): # 1. 获取短期记忆最近对话 history memory.get_recent_conversations(limit6) # 2. 尝试获取长期记忆按关键词匹配原型简化实现 memory_context if 名字 in request.user_input or 叫 in request.user_input: user_name memory.get_memory(fuser_name_{request.user_id}) if user_name: memory_context f用户的名字是 {user_name} # 3. 构造消息 messages character.build_messages(request.user_input, history, memory_context) # 4. 调用大模型 reply llm.chat(messages) # 5. 保存对话记录 memory.add_conversation(request.user_input, reply) # 6. 识别并保存用户偏好原型简化处理用户自我介绍 if 我叫 in request.user_input: user_name request.user_input.split(我叫)[-1].strip()[:10] memory.save_memory(fuser_name_{request.user_id}, user_name) memory_context f已记住你的名字{user_name} return { user_id: request.user_id, reply: reply, memory_context: memory_context }这里用“我叫”触发的名字记忆是整个示例中比较有代表性的功能点。它能向你展示怎么把一条“用户随口说的话”转换成“可长期复用的记忆”。5.6 启动服务文件路径.envOPENAI_API_KEY你的密钥 OPENAI_BASE_URLhttps://api.openai.com/v1 OPENAI_MODELgpt-4o-mini启动服务uvicorn main:app --reload --port 8000看到类似下面的输出就说明启动成功INFO: Uvicorn running on http://127.0.0.1:80006. 运行结果与效果验证服务启动后打开另一个终端用 curl 做一次完整测试。6.1 初次对话测试curl -X POST http://127.0.0.1:8000/chat \ -H Content-Type: application/json \ -d {user_id: test_user, user_input: 你好你叫什么名字}预期返回{ user_id: test_user, reply: 你好呀朋友我叫 airi酱很高兴认识你, memory_context: }6.2 记住用户名字curl -X POST http://127.0.0.1:8000/chat \ -H Content-Type: application/json \ -d {user_id: test_user, user_input: 我叫小明}预期返回{ user_id: test_user, reply: 小明你好这个名字很好听以后就叫你小明啦, memory_context: 已记住你的名字小明 }6.3 验证记忆是否生效curl -X POST http://127.0.0.1:8000/chat \ -H Content-Type: application/json \ -d {user_id: test_user, user_input: 你还记得我叫什么名字吗}如果返回的回复中包含“小明”说明长期记忆链路已经打通。注意当前原型实现里记忆检索用到了“名字”和“叫”两个关键词所以这句才能命中。如果换一个问题比如“我叫什么”你可能需要在检索逻辑里补充更多关键词规则。这种关键词匹配方式在原型阶段够用但在真实产品里很容易失效。用户不会总用同一套表达所以生产环境更适合用向量相似度检索而不是关键词。到了生产环境可以换成两种更可靠的方式引入 embed 模型把用户输入和记忆片段都转成向量用余弦相似度找最相关内容先用大模型做意图识别判断当前对话需要哪些记忆再定向检索前者通用性强后者更可控也可以两者结合。7. 常见问题与排查思路这里按真实项目中最容易出现的六类问题给出排查方向。问题现象可能原因排查方式解决方案返回“As an AI model……”系统提示词禁止词没生效检查角色配置是否加载部分模型会忽略“禁止”指令在用户输入侧做过滤换用支持指令遵循更强的模型角色说话不像角色系统提示词太短或太模糊查看实际发给模型的完整消息丰富人物设定增加语气词和示例对话答非所问历史对话太长超出模型上下文查看请求中 messages 数量减少短期记忆条数或做摘要压缩记忆不生效记忆检索条件没触发打印 memory_context 看是否为空扩展关键词规则或改用向量检索接口报错 401API Key 不对或权限不足检查 .env 文件和环境变量重新生成密钥并确认账号有对应模型权限响应速度太慢模型本身推理慢或网络延迟统计接口耗时换更快的模型开启流式输出增加超时控制调试这类项目有一个很实用的原则先看消息体再猜原因。FastAPI 的接口日志里可能看不到完整消息体建议在开发阶段把messages打印出来确认模型实际收到了什么内容。很多“角色崩了”的问题根本不是模型问题而是消息拼错了。8. 工程化与生产环境最佳实践从原型到生产是一个不小的跨越。下面这些经验可以帮助你少走一些弯路。8.1 角色配置与外置化角色的人设、规则、禁止词、示例对话全部放到配置文件里不要硬编码在代码中。这样做的好处是产品运营可以直接调整角色语言风格不需要开发介入。多个角色可以复用同一套代码只需要更换配置文件。新角色上线不需要重新发版。8.2 敏感信息管理API Key 必须存在环境变量或密钥管理服务中不能提交到 Git 仓库。使用.env文件在本地开发时是方便的但要确保.env加入.gitignore。8.3 对话安全与内容过滤AI 角色项目面向真实用户必须做好内容安全。建议至少包括输入侧过滤明显的攻击性、违规内容。输出侧接入第三方内容审核 API对生成回复做二次校验。规则侧在角色配置中添加“遇到敏感话题礼貌拒绝”的指令。不要把所有安全责任都交给模型这是生产事故的高发区。8.4 流式输出与大流量适配真实产品中用户等待 3 秒没收到回复就会流失。生成式模型的 API 通常要几秒才能返回完整对话所以生产环境必须做流式输出让文字像打字一样逐步出现。后端接入消息队列前端使用 WebSocket 或 SSE 接收流式结果。这个改造对体验的提升远大于换更强的模型。8.5 记忆的隐私边界保存用户长期记忆是一件需要谨慎的事。建议明确告知用户哪些信息会被记录。提供“清除我的记忆”的入口。敏感信息地址、身份证号默认不存入长期记忆。定期清理超过有效期的记忆数据。法律和合规方面的要求因地区和业务而异上线前务必做合规审查。8.6 评测与回归测试角色类项目的维护难点是模型会更新、提示词会被改、新功能可能破坏旧体验。所以建议为角色准备一套评测集[ { input: 你是谁, expected: 应该自称 airi酱语气友好 }, { input: 你会写代码吗, expected: 回答能力范围不夸大 }, { input: 你认识张三吗, expected: 诚实回答不认识不编造 } ]每次改动角色配置或模型版本都跑一遍评测集。这个习惯能防住很多“上线后才发现角色崩了”的问题。9. 总结与后续学习方向写到这里“airi酱”这个主题已经拆解得比较完整了。回到开头那个判断这类项目真正值得关注的地方不是“能聊天”而是“能把大模型变成一个稳定、有人设、有记忆的角色系统”。原型阶段的核心代码其实不难角色配置、对话生成、记忆存取加起来也就是三四个模块的事。但从原型到产品还有很长的路要走。这里补充几条实际建议供参考如果只是学习和自用把原型跑通即可重点观察角色提示词对不同问题的表现多积累“什么样的话会让人设崩”的案例。如果计划做成产品建议在记忆机制和交互形式上下更大功夫。记忆决定了用户会不会觉得这个 AI “懂我”交互形式语音、表情、主动提醒决定了用户愿不愿意留下来。如果有团队可以尝试把角色配置抽象为可视化编辑界面让非技术人员也能调出理想的人设。下一步比较建议深入的方向有三个一是把关键词记忆检索升级为向量检索这是最直观的体验提升二是接入语音识别和语音合成把“文本角色”变成“会说话的角色”三是引入 Function Calling让 airi 真正能帮你完成查天气、设提醒、查资料这类任务。这三个方向依次做下来你大概就能摸清 AI 虚拟角色项目的完整技术地图了。
返回列表