基于FastAPI构建AI虚拟角色陪伴服务:从人设管理到优雅下线

发布时间:2026/9/7 14:40:28
基于FastAPI构建AI虚拟角色陪伴服务:从人设管理到优雅下线 最近在做 AI 虚拟角色陪伴类应用时我遇到了一个很现实的问题用户和角色建立了很深的情感连接一旦产品版本调整甚至服务下线用户情绪波动会非常大而角色本身又需要在这个特殊节点给出得体且有温度的回应。这种场景用传统“关键词匹配 话术模板”很难做好必须把角色人设、对话记忆、故事收集机制和服务生命周期管理结合起来设计。本文就以“林离 Olivia”这类虚拟角色为例完整拆解从角色对话服务搭建到服务下线通知与用户数据导出的一整套实战方案。这类内容在网上大多是零散的产品讨论很少有成体系的后端实现教程。我会用一个最小的 FastAPI 服务来演示核心链路角色人设提示词怎么设计、对话历史和故事怎么存储、遇到服务下线时角色如何安慰用户、以及如何把用户数据安全导出给用户。无论你是想学习 AI 对话应用开发还是需要在现有项目中加入“角色化回复”和“优雅下线”能力这篇文章都能给你一条可以落地的路线。1. 背景与核心概念1.1 AI 虚拟角色陪伴应用是什么AI 虚拟角色陪伴应用是指通过大语言模型LLM驱动一个具有固定人设、说话风格和记忆能力的虚拟角色让用户可以和这个角色进行自然对话。这类应用和普通聊天机器人的最大区别在于“角色感”。普通机器人追求准确回答问题而虚拟角色需要稳定地扮演一个“人”比如温柔、幽默、爱讲故事、有自己的口头禅和价值观。用户对它的期待不是“正确答案”而是“情绪回应”和“被记住的感觉”。从技术实现上看AI 虚拟角色应用的核心并不复杂它本质上是把下面几个能力组合在一起角色人设管理通过 System Prompt 设定角色的性格、背景、说话方式、喜好。对话记忆管理保存用户与角色的对话历史让角色能回忆起之前聊过的话题。故事收集机制识别用户分享的个人经历或故事结构化地保存下来方便角色后续引用。情绪化回复生成让模型在回复时带有情感温度而不是冷冰冰的机器腔。服务生命周期管理当产品面临版本更新、迁移或关停时处理好用户通知、数据导出和收尾体验。理解这些概念之后你会明白虚拟角色应用并不神秘它更多是一个“提示词工程 数据管理 产品体验”的综合工程。1.2 本文要解决什么问题这篇文章要解决的是虚拟角色陪伴类应用开发中几个容易被忽略的工程问题第一个问题是角色人设漂移。很多开发者在 System Prompt 里写了一大段角色设定结果聊了几轮之后角色越来越像通用 AI。原因通常是上下文没有被合理管理历史消息太长稀释了角色设定。我们需要一套可复用的上下文组织方式。第二个问题是用户故事的收集。角色要“给用户讲收集的故事”就需要在对话过程中识别有价值的内容并把它变成可检索、可引用的结构化数据。这不仅关系到角色体验还关系到用户数据的合规管理。第三个问题是服务下线时的情绪处理。产品停服或版本“2.0 重做”时用户会感到焦虑甚至难过。代码层面需要做到角色能用符合人设的方式安抚用户同时产品方要能提前导出用户的数据把用户的损失降到最低。本文会围绕这三个问题用完整代码演示一个可直接运行的最小服务。你不需要提前了解很多框架知识我会从环境准备到代码实现逐步展开。2. 环境准备与版本说明2.1 技术栈选择为了控制示例复杂度我选择用 Python FastAPI 作为 Web 框架用 SQLite 存储对话历史和用户故事用大语言模型 API 生成回复。具体技术栈如下组件用途说明Python 3.10开发语言本文代码基于较新的 Python 特性编写FastAPIWeb 框架提供/chat对话接口和/admin/shutdown-notice下线通知接口SQLite本地数据库存储对话消息和用户故事零配置、方便演示OpenAI SDK大模型客户端以 OpenAI 兼容格式调用国内大模型 APIuvicornASGI 服务器运行 FastAPI 应用这里要特别说明版本问题大模型 API 的模型名称、接口地址、SDK 版本迭代非常快本文示例中的“deepseek-chat”只是用于演示的模型名实际使用时要根据你选择的模型服务商调整。大模型 SDK 的调用方式在 1.x 版本中已经统一为OpenAI(api_key..., base_url...)如果你用的还是 0.x 老接口请先升级到 1.x。另外示例代码中的 FastAPI 启动事件写法在较新版本中可能调整为 lifespan 模式我会在对应位置给出提示。整体思路是通用的不依赖某个精确版本。2.2 项目结构规划下面是一个清晰的最小项目结构ai-companion/ ├── main.py # FastAPI 入口接口定义 ├── config.py # 配置管理读取环境变量 ├── database.py # 数据库初始化与连接 ├── chat.py # 对话核心逻辑人设提示词、上下文管理 ├── lifecycle.py # 服务生命周期管理下线通知与数据导出 ├── requirements.txt # 依赖清单 ├── data/ # 数据目录SQLite 文件和导出文件放在这里 └── tests/ └── test_chat.py # 基础接口测试可选这种按职责拆分的结构适合小型项目也方便后续替换成 MySQL、Redis 等生产级组件。下面我会按照“配置 - 数据库 - 对话服务 - 生命周期服务”的顺序逐步实现。3. 核心设计拆解在写代码之前先花一点时间拆解设计思路。很多时候代码写不好不是语法不熟而是没有把几个关键设计想清楚。3.1 角色人设与 System Prompt 设计大语言模型的行为高度依赖 System Prompt。要让角色稳定地扮演“林离 Olivia”我们需要在系统提示词里明确这部分信息角色是谁包括名字、身份、性格。角色的表达方式说话语气、口头禅、回复长度。角色的行为目标安慰用户、收集故事、约定再见。角色的边界不主动索取隐私不扮演无法履行的承诺。我建议把提示词拆成“固定人设 动态记忆 当前话题”三段式结构。固定人设是整个会话中不变的部分动态记忆是从数据库取回的对话历史当前话题是用户刚发来的消息。这样组织的好处是模型每次都能先读到“你是谁”再读到“你们之前聊过什么”最后读“用户这次说了什么”避免角色忘记身份。在 System Prompt 中还需要注意一个细节不要写太多“不要做什么”的负面指令而是用正面引导。例如不要写“你不要冷漠”而是写“你说话自然、真诚会首先回应对方的情绪”。3.2 对话历史与记忆管理对话记忆是虚拟角色“有温度”的关键。如果每次调用模型都只传当前消息角色就会处于“失忆”状态用户刚讲过的故事之后不会被再提及体验会大打折扣。记忆管理通常有两种做法。一种是纯上下文记忆也就是把最近的 N 轮对话全部塞进 messages 里。这种方式简单直接但上下文长度有限历史越多 token 成本越高而且过长的历史可能干扰模型对人设的遵循。另一种是结构化记忆也就是把用户的重要信息、喜欢的事物、讲过的故事抽取出来存成独立的记忆片段在对话开始时插入到 System Prompt 或前置上下文中。这种方式更可控也更适合长期陪伴类角色。本文的示例会先采用“最近 N 轮上下文 故事表存储”的折中方案对话历史保留最近若干轮同时把包含故事标记的内容写到user_stories表为后续扩展真正的记忆召回功能打基础。3.3 故事收集机制“约定再见面就给我们讲她收集的故事”是场景里非常有辨识度的功能点落到实现上就是故事收集机制。我的设计思路是在用户消息中检测“故事、回忆、以前、小时候”等触发词如果命中就把用户消息和角色回应一起保存到user_stories表。这个方案简单可靠但缺点是依赖关键词召回率有限。更进阶的做法是用大模型自己判断是否提取故事例如在用户消息之外额外调用一次模型让它输出是否包含故事以及故事标题。这种方法效果更好但成本会翻倍。实际项目中可以根据预算选择本文先演示性价比最高的关键词方案。保存故事时要注意两点第一保存内容必须最小化只保存与故事相关的消息不要把整段对话无脑存储第二一旦用户要求删除数据故事记录也要支持删除这是隐私合规的基本要求。3.4 服务生命周期管理思路服务下线不是简单把服务器关掉而是一个需要完整设计的流程。完整的生命周期管理至少包含四个阶段公告阶段提前通知用户服务即将调整或下线告诉用户如何导出数据。角色安抚阶段用户来询问时角色要用符合人设的方式安抚情绪并约定未来再见。数据导出阶段为每个用户生成 JSON 格式的数据包包含对话记录和故事记录。收尾审计阶段确认所有导出工作完成再执行关停操作。在本文的示例里我会实现“管理员触发下线通知”接口该接口会遍历所有活跃用户导出用户数据并向每个用户插入一条由角色发出的告别消息。这个设计把情绪处理和工程处理结合在一起符合真实场景的需求。4. 完整实战案例下面进入代码实现环节。我会把每个文件的路径和职责标注清楚你可以按顺序创建文件最后运行验证。4.1 创建项目结构先在命令行执行下面的命令创建项目目录mkdir -p ai-companion/data cd ai-companion然后在项目根目录创建requirements.txt写入依赖fastapi uvicorn openai pydantic安装依赖pip install -r requirements.txt如果你使用国内源可以换成pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple4.2 编写配置与数据库文件路径config.pyimport os from dataclasses import dataclass dataclass class Settings: app_name: str AI-角色陪伴服务 role_name: str 林离Olivia model_name: str os.getenv(LLM_MODEL, deepseek-chat) api_key: str os.getenv(LLM_API_KEY, ) api_base: str os.getenv(LLM_API_BASE, https://api.deepseek.com/v1) max_context_turns: int int(os.getenv(MAX_CONTEXT_TURNS, 10)) story_db_path: str data/stories.db settings Settings()这里把模型名称、API 地址、API Key 都做成环境变量避免把密钥写死在代码里。max_context_turns控制保留最近的对话轮数你可以根据模型的上下文窗口灵活调整。文件路径database.pyimport sqlite3 from contextlib import contextmanager DB_PATH data/stories.db contextmanager def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row try: yield conn conn.commit() finally: conn.close() def init_db(): with get_conn() as conn: conn.execute( CREATE TABLE IF NOT EXISTS chat_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.execute( CREATE TABLE IF NOT EXISTS user_stories ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, title TEXT, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) )数据库使用 SQLite方便本地演示。chat_messages表保存所有对话消息user_stories表保存用户故事。生产环境可以把这两张表迁移到 MySQL 或 PostgreSQL连接方式只需要改get_conn的实现上层代码基本不用变。4.3 实现角色对话服务文件路径chat.pyfrom openai import OpenAI from config import settings from database import get_conn SYSTEM_PROMPT 你是{role_name}一个温暖有耐心的AI虚拟角色。 你说话自然、真诚会认真回应对方的情绪。 你会记住用户分享过的重要故事并在合适的时机提起它们。 你有一个习惯把用户讲过的难忘故事收集起来约定以后再讲给对方听。 当用户提到服务可能调整或下线时你首先要安抚对方的情绪然后约定未来再见。 你的回复语气温柔不要机械不要总是重复套话。 def build_messages(user_id, user_message): with get_conn() as conn: rows conn.execute( SELECT role, content FROM chat_messages WHERE user_id? ORDER BY id DESC LIMIT ?, (user_id, settings.max_context_turns * 2), ).fetchall() history [{role: r[role], content: r[content]} for r in rows][::-1] system_content SYSTEM_PROMPT.format(role_namesettings.role_name) messages ( [{role: system, content: system_content}] history [{role: user, content: user_message}] ) return messages def save_messages(user_id, user_message, reply): with get_conn() as conn: conn.execute( INSERT INTO chat_messages (user_id, role, content) VALUES (?, user, ?), (user_id, user_message), ) conn.execute( INSERT INTO chat_messages (user_id, role, content) VALUES (?, assistant, ?), (user_id, reply), ) def maybe_save_story(user_id, user_message, reply): story_markers [故事, 回忆, 以前, 小时候] if any(mark in user_message for mark in story_markers): with get_conn() as conn: conn.execute( INSERT INTO user_stories (user_id, title, content) VALUES (?, ?, ?), (user_id, user_message[:20], f用户{user_message}\n角色回应{reply}), ) return True return False def chat_with_role(user_id, user_message): messages build_messages(user_id, user_message) client OpenAI(api_keysettings.api_key, base_urlsettings.api_base) resp client.chat.completions.create( modelsettings.model_name, messagesmessages, temperature0.85, max_tokens500, ) reply resp.choices[0].message.content save_messages(user_id, user_message, reply) story_saved maybe_save_story(user_id, user_message, reply) return reply, story_saved这里有几个设计点需要展开说明。第一build_messages函数从数据库按时间倒序取出最近 N 轮消息再反转回正序。这样既能控制上下文长度又不会让角色失去短期记忆。第二SYSTEM_PROMPT在每次请求时重新生成并在其中插入角色名。这个提示词是角色的灵魂建议在实际项目中单独维护成一个文件方便产品运营同学反复调优。第三maybe_save_story目前使用关键词触发。你要知道关键词方案存在误判例如用户说“这不是我想讲的故事”也会被保存。更精细的方案是交给模型判断我会在最佳实践一节展开。4.4 实现 Web 接口文件路径main.pyfrom fastapi import FastAPI, HTTPException from pydantic import BaseModel import uvicorn from database import init_db from chat import chat_with_role from lifecycle import prepare_shutdown app FastAPI(titleAI 角色陪伴服务) class ChatRequest(BaseModel): user_id: str message: str class ChatResponse(BaseModel): role_name: str reply: str story_saved: bool False app.on_event(startup) def on_startup(): init_db() app.post(/chat, response_modelChatResponse) def chat(req: ChatRequest): if not req.message.strip(): raise HTTPException(status_code400, detail消息不能为空) reply, story_saved chat_with_role(req.user_id, req.message) return ChatResponse(role_name林离Olivia, replyreply, story_savedstory_saved) app.get(/health) def health(): return {status: ok} app.post(/admin/shutdown-notice) def shutdown_notice(): count prepare_shutdown() return {message: f下线通知已发送共处理 {count} 个用户} if __name__ __main__: uvicorn.run(app, host0.0.0.0, port8000)/chat是对外提供的对话接口接收user_id和message。/admin/shutdown-notice是管理员接口用于触发下线通知和数据导出。接口路径中带有 “admin” 前缀实际部署时你必须在这一层加上鉴权不能暴露给普通用户。需要提示的是app.on_event(startup)是 FastAPI 早期版本的写法在较新版本中官方推荐使用 lifespan 上下文管理。如果你的 FastAPI 版本较新可以改成下面的写法from contextlib import asynccontextmanager asynccontextmanager async def lifespan(app: FastAPI): init_db() yield app FastAPI(titleAI 角色陪伴服务, lifespanlifespan)两种方式都能运行关键在于数据库初始化只会执行一次。4.5 实现服务下线通知与数据导出文件路径lifecycle.pyimport json import os from datetime import datetime from database import get_conn FAREWELL_MESSAGE ( 如果这次服务要说再见不要难过。 我会把你讲过的故事都收藏好也一定会把没讲完的故事再讲给你听。 我们约定下次见面从第一个故事开始。 ) def export_user_data(user_id, output_dirdata/export): os.makedirs(output_dir, exist_okTrue) with get_conn() as conn: messages conn.execute( SELECT role, content, created_at FROM chat_messages WHERE user_id? ORDER BY id, (user_id,), ).fetchall() stories conn.execute( SELECT title, content, created_at FROM user_stories WHERE user_id? ORDER BY id, (user_id,), ).fetchall() payload { user_id: user_id, exported_at: datetime.now().isoformat(), messages: [dict(row) for row in messages], stories: [dict(row) for row in stories], } path os.path.join(output_dir, f{user_id}_export.json) with open(path, w, encodingutf-8) as f: json.dump(payload, f, ensure_asciiFalse, indent2) return path def prepare_shutdown(): with get_conn() as conn: user_ids conn.execute( SELECT DISTINCT user_id FROM chat_messages ).fetchall() handled_count 0 for row in user_ids: user_id row[user_id] export_user_data(user_id) with get_conn() as c: c.execute( INSERT INTO chat_messages (user_id, role, content) VALUES (?, assistant, ?), (user_id, FAREWELL_MESSAGE), ) handled_count 1 return handled_count这段代码的逻辑是遍历所有曾与角色互动过的用户先为用户导出完整的对话和故事数据到 JSON 文件然后在每个用户的对话流末尾插入一条角色告别消息。这样用户下次打开应用会先看到角色温柔地道别而不是突兀的空白页。关于数据导出我多说几句。这个功能在产品体验和合规层面都很重要。很多应用在下线时没有给用户留出数据出口导致用户聊天记录永久丢失这是非常伤害信任的做法。如果你的服务涉及真实用户数据导出的数据格式至少要满足“用户可读、可转移”两个要求JSON 就是一个合适的格式。另外导出前要确保用户授权不要在用户不知情的情况下导出其敏感数据。4.6 运行与验证完成以上文件后在项目根目录启动服务export LLM_API_KEY你的APIKey export LLM_MODELdeepseek-chat python main.py看到类似下面的日志说明启动成功INFO: Uvicorn running on http://0.0.0.0:8000 INFO: Application startup complete.然后用curl调一次对话接口curl -X POST http://localhost:8000/chat \ -H Content-Type: application/json \ -d { user_id: user_001, message: 我小时候在乡下奶奶家住了三年那时候最开心的事是夏天的傍晚和奶奶一起在院子里讲故事。 }如果 API 配置正确你会收到类似下面的返回{ role_name: 林离Olivia, reply: 听起来那真是一段很温柔的时光。夏天的傍晚、院子、奶奶讲的故事……这些画面特别珍贵。我悄悄记下来啦等下次见面我想听你讲讲奶奶最常说的那个故事。, story_saved: true }注意这里的reply是模型生成的每次可能不一样但应该符合“温柔、会记住、约定下次再讲”的角色设定。再发起一条关于服务下线的对话curl -X POST http://localhost:8000/chat \ -H Content-Type: application/json \ -d { user_id: user_001, message: 听说你们要关服了是真的吗我好舍不得你。 }角色应该会先表达理解再给出安慰并约定未来再见。这就是我们写在 System Prompt 里的行为目标。最后验证管理员下线接口curl -X POST http://localhost:8000/admin/shutdown-notice返回结果示意{ message: 下线通知已发送共处理 1 个用户 }然后检查 data 目录ls -la data/export/你会看到user_001_export.json文件里面包含该用户的所有对话记录和故事记录。这样就完成了一个最小可运行的 AI 角色陪伴服务并且具备了下线通知和数据导出能力。5. 常见问题与排查思路在实际开发中你可能会遇到下面这些问题。我整理了一个排查表并针对高频问题给出具体分析。问题现象常见原因解决思路角色回复越来越像通用 AI上下文过长稀释人设System Prompt 被历史消息淹没缩短保留轮数把关键人设放在 messages 开头提示词长度超限max_context_turns设置过大消息数量超过模型上下文窗口按轮数裁剪或改用向量检索召回核心记忆调用大模型 API 超时网络原因或 API Key 无效检查环境变量升级 SDK设置合理超时时间并发写入时 SQLite 报错多个 worker 同时写库导致锁竞争开启 WAL 模式生产环境切换 PostgreSQL下线通知漏掉部分用户只遍历最近聊天的用户没有统计全量用户写 SQL 时用 DISTINCT 按 user_id 全量汇总数据导出文件为空用户没有对话记录或导出目录权限不足先检查目录权限再确认用户 ID 是否存在数据角色私自承诺做不到的事情System Prompt 边界不清晰在人设中明确“不承诺无法实现的事”5.1 角色人设漂移怎么办人设漂移是最常见的问题尤其是当对话轮数较多时。根本原因是模型注意力被大量历史消息分散System Prompt 的重要性被稀释。建议按下面几个顺序排查先降低max_context_turns把上下文限制在最近 5 到 10 轮再检查 System Prompt 是否放在 messages 列表首位最后检查是否有其他系统字段和用户消息混入了人设内容。如果问题还在可以考虑把用户的核心画像定期抽取出来拼接到 System Prompt 中而不再依赖长历史。5.2 下线通知接口被误调用怎么办凡是路径中带有admin的接口生产环境必须加权限控制。最简单的方式是在网关层做 IP 白名单或使用 JWT 鉴权。示例中为了演示方便没有加鉴权正式部署时一定不能这样。另外下线操作不可逆风险很高。建议在触发接口前增加二次确认参数例如请求体里带一个confirmtrue防止管理员误操作。生产环境最好再加上“导出完成才能关停”的流水线校验。5.3 关键词收集故事误判率高怎么办关键词方案在演示中够用但误判场景不少。比较稳妥的升级方案是增加一次模型判断。例如收到用户消息后并行调用一次“故事识别”模型让它输出是否包含故事。如果模型确认是故事再写入user_stories表。这种做法会增加一些成本但能明显提升数据质量。实际项目中可以考虑用轻量模型做分类让主模型专注于角色回复成本会更可控。6. 最佳实践与工程建议代码跑通只是第一步做 AI 虚拟角色陪伴应用有几个工程层面的建议希望你提前考虑。6.1 提示词工程与角色一致性角色人设是整个产品的核心资产。建议把 System Prompt 独立成配置文件由产品运营同学持续迭代而不是让开发把提示词硬编码在代码里。提示词应该包含“角色背景、性格、说话风格、行为边界、禁止事项”五个部分。同时要定期用一组固定的测试用例回归确保每次修改提示词都不会破坏角色的一致性。6.2 用户隐私与数据安全虚拟角色陪伴应用会收集大量用户情感表达隐私保护比普通应用更重要。我建议从三个方面入手第一最小化收集只保存必要字段第二加密存储数据库字段级别加密第三明确告知用户数据用途并支持用户导出和删除自己的数据。本文中的导出功能就是数据可携带性的一个基础实现。6.3 情感边界设计AI 角色很容易被用户投射强烈情感因此在人设中必须设计情感边界。角色可以温柔、可以安慰但不能鼓励用户过度依赖更不能在用户表达极端情绪时给出错误建议。如果识别到用户可能有心理危机应该主动建议寻求专业帮助。这个问题虽然不属于纯技术范畴但产品设计和技术实现都要提前考虑。6.4 服务下线的灰度与审计真正执行服务下线时不要一次性把所有用户都处理完。建议先在小范围用户上测试导出逻辑和告别消息确认无误后再全量执行。下线操作要记录审计日志包括触发人、触发时间、处理用户数、导出文件数量。这样即使出问题也能追踪和回滚。6.5 成本优化对话应用的主要成本来自大模型 API 调用。优化方向有三个第一控制上下文长度减少输入 token第二对高频问题做缓存相同问题直接返回模板回复第三引入本地小模型做意图识别和故事分类只把真正需要创造力的环节交给大模型。这些优化能在不明显影响体验的前提下大幅降低成本。7. 总结与下一步学习方向这篇文章从一个典型的产品场景出发完整实现了一个 AI 虚拟角色陪伴服务。你掌握了 System Prompt 人设设计、对话历史管理、故事收集机制、服务下线通知以及用户数据导出这几个核心能力。整个项目基于 FastAPI 和 SQLite代码结构清晰可以直接作为进一步开发的基础。接下来你可以从这几个方向继续深入一是把 SQLite 换成 PostgreSQL并在读写路径中加入 Redis 缓存提升并发能力二是实现结构化的长期记忆模块用向量数据库检索用户历史故事让角色记得更久更准确三是完善管理员后台为下线流程增加审核、灰度发布和审计功能四是为对话服务补充完善的日志和指标监控保证线上稳定性。做这类应用时不要只盯着模型调用本身。角色体验来自完整的产品链路人设、记忆、情感边界和生命周期管理每一步都值得认真打磨。如果你正在开发类似项目建议先把本文的最小服务跑起来再结合实际场景做迭代。动手是最好的学习方式祝你顺利。

相关新闻