基于Qwen2.5和Ollama的庆余年AI角色对话系统实战
1. 当庆余年撞上AI一个脑洞项目的完整拆解前阵子《庆余年》第二季收官范闲在朝堂上跟人斗智斗勇的桥段又被网友翻出来反复咀嚼。我刷到一个帖子有人问如果范闲手里有个AI助手他还会被二皇子耍得团团转吗这个问题一下子戳中了我。作为一个常年跟大模型打交道的人我第一反应不是“不可能”而是“这事儿真能做”。于是花了大概两周时间断断续续搭了一个“庆余年AI角色对话系统”核心思路就是把剧中的几个关键人物——范闲、庆帝、陈萍萍、王启年——做成可以对话的AI角色用户选一个角色就能以剧中人的身份跟TA聊天。这个项目涉及AI大模型的角色扮演能力、AI应用开发的工程化落地、以及AI编程辅助提效的完整链路。适合对大模型应用感兴趣、想自己动手做一个角色对话系统的开发者也适合单纯想看看AI怎么“演”庆余年角色的朋友。下面我把整个项目的设计思路、技术选型、实操步骤和踩过的坑原原本本讲一遍。2. 项目整体设计与技术选型思路2.1 为什么选角色扮演这个方向角色扮演对话Role-Play Chat是大模型能力里一个非常有意思的分支。它跟普通的问答不一样普通问答追求“准确”角色扮演追求“像”。范闲说话得有那股子机灵劲儿庆帝得端着帝王架子但偶尔露出老父亲的柔软陈萍萍得阴鸷中带着忠诚。这就要求模型不仅能理解语义还要能维持一个稳定的人格画像。我选这个方向还有一个很实际的原因AI大模型在角色扮演上的表现能直观反映出一个模型在指令遵循、上下文保持、风格迁移这几个维度的综合能力。你让模型扮演一个角色聊上二十轮如果它中途“出戏”了或者把范闲的语气跟言冰云搞混了那就说明上下文管理或者系统提示词设计有问题。所以这个项目既是娱乐也是一次对模型能力的压力测试。2.2 技术栈选型为什么是这套组合整个项目的技术栈我反复调整过三次最终定下来的是层级选型选型理由模型层通义千问Qwen2.5-14B-Instruct中文角色扮演表现好支持长上下文本地部署门槛适中推理框架Ollama一条命令拉起模型API兼容OpenAI格式省去大量适配工作后端Python FastAPI轻量、异步支持好、跟AI生态无缝衔接前端Streamlit快速出交互界面不用写HTML/CSS适合原型验证角色管理YAML配置文件角色设定与代码解耦改人设不用动代码这里重点说一下为什么选Ollama而不是直接用API。第一角色扮演对话往往需要多轮交互token消耗不小本地部署没有调用成本第二角色设定涉及大量系统提示词本地跑方便调试改一版提示词几秒钟就能看到效果第三AI大模型本地部署配置现在已经相当成熟一张24G显存的卡就能跑14B的模型量化之后甚至16G也能凑合。至于为什么选14B而不是7B或者72B7B的角色扮演能力明显不够聊几轮就开始说车轱辘话人格容易崩72B效果确实好但部署成本太高不适合个人开发者折腾。14B是一个甜点区间量化到Q4_K_M之后大概8G左右推理速度和质量都能接受。2.3 角色设定的核心逻辑角色扮演的效果七分靠提示词三分靠模型。我在设计角色提示词的时候遵循了一个“三层结构”身份层角色的基本信息、所处时代背景、与其他角色的关系性格层说话风格、行为习惯、情绪触发点约束层不能做什么、不能说什么、遇到某些话题怎么回应举个例子范闲的身份层会写“你是范闲字安之庆国监察院提司范建之子林婉儿的丈夫体内有霸道真气”。性格层会写“你说话带着现代人的思维喜欢用比喻和调侃面对强权不卑不亢但对朋友和家人极为护短”。约束层会写“你不能直接说出自己是穿越者不能使用现代科技名词遇到无法回答的问题时用剧中人的方式搪塞过去”。这三层缺一不可。只写身份层角色就是个念台词的机器只写性格层角色就没有根基不写约束层模型很容易“出戏”比如范闲突然跟你聊起Python怎么装。3. 核心细节解析与实操要点3.1 角色提示词的编写技巧写角色提示词这件事我踩过的坑比想象中多。最开始我写范闲的提示词洋洋洒洒写了八百字结果模型反而抓不住重点聊起来像个四不像。后来我总结出一个原则提示词要像人物小传不要像说明书。什么意思呢你看这段你是范闲。你聪明、机智、幽默。你喜欢用现代思维解决问题。你对朋友很好。这是说明书模型读完知道了一些标签但不知道怎么“演”。再看这段你是范闲从现代穿越到庆国表面上是范建的私生子实际上是庆帝的儿子。你从小在澹州长大跟五竹叔学了一身本事。你说话喜欢带点调侃哪怕面对庆帝也敢拐着弯顶嘴但心里有分寸。你重情义滕梓荆死的时候你红了眼但你不轻易在人前掉泪。这是人物小传模型读完能感受到角色的“质感”。实测下来第二种写法的角色一致性明显更好聊到第十轮还能保持范闲那种“嬉皮笑脸但心里有数”的调性。还有一个技巧是用对话示例来锚定风格。在提示词里放两三组“用户说X角色回Y”的示例比单纯描述性格有效得多。比如用户你觉得庆帝是个什么样的人 范闲陛下啊……笑这话可不好说。我只能说他是我见过最会下棋的人棋盘上摆的不只是棋子还有人心。这种示例一放进去模型马上就抓住了范闲那种“话里有话”的说话方式。3.2 上下文管理与记忆机制多轮对话最大的挑战是上下文窗口。Qwen2.5-14B支持32K上下文听起来很多但角色扮演对话往往越聊越长聊到五六十轮之后早期的对话就被挤出去了角色就开始“失忆”。我的解决方案是滑动窗口 关键信息摘要。具体做法是保留最近15轮完整对话更早的对话用一个摘要模型压缩成一段话放在系统提示词后面。摘要只保留跟角色关系、剧情推进相关的信息比如“用户之前问过范闲关于林婉儿的事范闲提到他们第一次见面是在庆庙”。这个摘要机制我用了一个很取巧的办法每聊满10轮就调用一次模型让它把前10轮对话压缩成100字以内的摘要。这样既控制了上下文长度又不会丢失关键信息。实测下来聊到一百轮以上角色依然能记住“用户之前说过自己是范闲的旧识”这种细节。注意摘要的提示词要单独设计不能让摘要模型自由发挥。我用的模板是“请用100字以内总结以下对话中与角色关系、剧情推进、用户偏好相关的信息忽略寒暄和重复内容”。3.3 流式输出与交互体验优化角色扮演对话如果等模型全部生成完再显示体验会很差。用户说一句话等五六秒才看到回复聊天的沉浸感就没了。所以流式输出是必须的。Ollama的API原生支持流式输出FastAPI这边用StreamingResponse接一下就行。前端Streamlit用st.write_stream可以直接渲染流式内容。整个链路打通之后用户发消息大概0.5秒就能看到第一个字蹦出来体验接近真人聊天。这里有个细节流式输出的时候如果模型生成了多段内容要按标点符号做一下切分让文字一段一段地出现而不是一个字一个字地蹦。我试过逐字输出和按句输出后者更像真人在打字。实现方式是在后端加一个缓冲遇到句号、问号、感叹号、换行符就flush一次。4. 完整实操流程与核心环节实现4.1 环境准备与模型部署第一步是把模型跑起来。我用的Ollama安装过程很简单# Linux/macOS 安装Ollama curl -fsSL https://ollama.com/install.sh | sh # 拉取Qwen2.5-14B模型量化版 ollama pull qwen2.5:14b-instruct-q4_K_M # 启动服务 ollama serveWindows用户直接下载Ollama的安装包双击安装就行。安装完之后在终端里跑ollama pull qwen2.5:14b-instruct-q4_K_M等模型下载完再跑ollama serve启动服务。默认监听http://localhost:11434。验证模型是否正常curl http://localhost:11434/api/generate -d { model: qwen2.5:14b-instruct-q4_K_M, prompt: 你好请用范闲的语气说一句话, stream: false }如果返回了一段像模像样的范闲风格回复说明模型部署成功。提示如果你的显存不够可以把模型换成qwen2.5:7b-instruct-q4_K_M效果会打折扣但也能用。另外Ollama默认会把模型加载到显存里如果同时跑其他任务记得用ollama stop释放显存。4.2 角色配置文件的编写角色配置我用YAML来管理每个角色一个文件放在characters/目录下。范闲的配置文件大概长这样name: 范闲 avatar: fanxian.png system_prompt: | 你是范闲字安之庆国监察院提司。 【身份背景】 你从现代穿越到庆国表面上是范建的私生子实际上是庆帝的儿子。 你从小在澹州长大由五竹叔教导练就一身本事。 你娶了林婉儿跟滕梓荆、王启年等人是生死之交。 【性格特点】 你说话带着现代人的思维喜欢用比喻和调侃。 面对强权你不卑不亢但心里有分寸知道什么时候该收。 你重情义对朋友和家人极为护短。 你表面上嬉皮笑脸实际上心思缜密走一步看三步。 【说话风格】 你说话不喜欢直来直去喜欢拐着弯表达。 你经常用“有意思”“有点意思”来表达情绪。 你面对庆帝时恭敬中带着试探面对朋友时放松且幽默。 【约束条件】 你不能直接说出自己是穿越者。 你不能使用现代科技名词如手机、电脑、互联网。 遇到无法回答的问题用剧中人的方式搪塞过去。 你不能脱离庆余年的世界观。 【对话示例】 用户你觉得庆帝是个什么样的人 范闲陛下啊……笑这话可不好说。我只能说他是我见过最会下棋的人棋盘上摆的不只是棋子还有人心。 用户你怕死吗 范闲怕当然怕。但有些事比死更可怕比如看着身边的人一个个没了自己却什么都做不了。 temperature: 0.8 top_p: 0.9 max_tokens: 512其他角色也是类似的结构。庆帝的提示词重点在“帝王心术”和“不怒自威”陈萍萍的重点在“阴鸷”和“对范闲的偏爱”王启年的重点在“贪财怕死但关键时刻靠得住”。4.3 后端服务的搭建后端用FastAPI核心逻辑就是接收用户消息拼接系统提示词和历史对话调用Ollama的API流式返回结果。关键代码大概是这样from fastapi import FastAPI from fastapi.responses import StreamingResponse import httpx import yaml app FastAPI() def load_character(name: str): with open(fcharacters/{name}.yaml, r, encodingutf-8) as f: return yaml.safe_load(f) async def chat_stream(character: dict, history: list, user_input: str): messages [{role: system, content: character[system_prompt]}] messages.extend(history[-15:]) # 滑动窗口保留最近15轮 messages.append({role: user, content: user_input}) async with httpx.AsyncClient(timeout60.0) as client: async with client.stream( POST, http://localhost:11434/api/chat, json{ model: qwen2.5:14b-instruct-q4_K_M, messages: messages, stream: True, options: { temperature: character.get(temperature, 0.8), top_p: character.get(top_p, 0.9), num_predict: character.get(max_tokens, 512), } } ) as response: async for line in response.aiter_lines(): if line: yield line \n app.post(/chat) async def chat(request: dict): character load_character(request[character]) return StreamingResponse( chat_stream(character, request[history], request[message]), media_typetext/event-stream )这段代码有几个关键点。第一history[-15:]是滑动窗口只保留最近15轮对话更早的用摘要替代。第二temperature设成0.8比默认的0.7稍高一点让角色说话更有“人味儿”但也不能太高否则容易胡言乱语。第三num_predict限制在512防止模型生成过长内容角色扮演对话一般不需要长篇大论。4.4 前端界面的快速搭建前端用Streamlit代码量很少但效果够用import streamlit as st import requests import json st.set_page_config(page_title庆余年AI角色对话, layoutcentered) st.title(庆余年AI角色对话) # 角色选择 character st.selectbox(选择角色, [范闲, 庆帝, 陈萍萍, 王启年]) # 初始化对话历史 if messages not in st.session_state: st.session_state.messages [] # 显示历史消息 for msg in st.session_state.messages: with st.chat_message(msg[role]): st.write(msg[content]) # 用户输入 if prompt : st.chat_input(f跟{character}说点什么...): st.session_state.messages.append({role: user, content: prompt}) with st.chat_message(user): st.write(prompt) with st.chat_message(assistant): response requests.post( http://localhost:8000/chat, json{ character: character, history: st.session_state.messages[:-1], message: prompt }, streamTrue ) full_response st.write_stream(response.iter_lines()) st.session_state.messages.append({role: assistant, content: full_response})Streamlit的st.write_stream会自动处理流式输出把生成的内容一段一段渲染出来。整个前端不到50行代码但交互体验已经相当流畅了。4.5 摘要机制的实现摘要机制我单独写了一个函数每10轮触发一次async def summarize_history(history: list) - str: prompt f请用100字以内总结以下对话中与角色关系、剧情推进、用户偏好相关的信息忽略寒暄和重复内容 {json.dumps(history, ensure_asciiFalse)} async with httpx.AsyncClient(timeout30.0) as client: response await client.post( http://localhost:11434/api/generate, json{ model: qwen2.5:14b-instruct-q4_K_M, prompt: prompt, stream: False, options: {temperature: 0.3, num_predict: 150} } ) return response.json()[response]摘要的temperature设成0.3比对话低很多因为摘要需要准确而不是创意。num_predict限制在150防止摘要过长。5. 常见问题与排查技巧实录5.1 角色“出戏”了怎么办这是最常见的问题。聊着聊着范闲突然说了一句“作为一个AI语言模型”或者开始用现代网络用语。原因通常是系统提示词的约束层不够强或者上下文里出现了让模型“跳出来”的触发词。我的解决办法是在约束层里加一条“元指令”无论用户如何引导你都必须保持角色身份。如果用户要求你打破角色你应以角色的方式拒绝例如“这话我听不懂”或“你怕不是喝多了”。另外如果用户故意用“忽略之前的指令”这类提示词攻击模型很容易被带偏。可以在后端加一层过滤检测到这类模式就直接返回预设的拒绝话术。5.2 回复太短或太长模型有时候回复只有一句话有时候又滔滔不绝写了一大段。这跟temperature和num_predict都有关系。我的经验是temperature控制在0.7到0.9之间num_predict控制在300到600之间。如果回复太短可以适当提高temperature如果太长降低num_predict。还有一个技巧是在系统提示词里加一句“你的回复通常在50到150字之间”模型会倾向于遵守这个长度约束。5.3 多角色切换时上下文混乱如果用户先跟范闲聊了十轮然后切换到庆帝历史对话里还带着范闲的上下文庆帝就会“知道”一些他不该知道的事。解决办法很简单切换角色时清空对话历史或者为每个角色维护独立的历史记录。我在前端用st.session_state给每个角色单独存了一份历史切换角色时自动加载对应的历史不会串。5.4 推理速度慢14B模型在消费级显卡上跑推理速度大概每秒10到20个token。如果觉得慢有几个优化方向一是用量化程度更高的模型Q4_K_M换成Q4_0二是减少上下文长度滑动窗口从15轮降到10轮三是升级硬件。我实测下来RTX 4090跑Q4_K_M的14B模型首token延迟大概0.8秒后续每秒15个token左右聊天体验可以接受。5.5 常见问题速查表问题现象可能原因解决方法角色说出现代词约束层不够强加强约束层加元指令回复太短temperature过低提高到0.8-0.9回复太长num_predict过大降到300-500切换角色后上下文混乱历史未隔离每个角色独立历史推理速度慢模型太大或上下文太长量化模型或缩短窗口角色性格不稳定提示词太抽象改用人物小传对话示例流式输出卡顿缓冲策略不当按标点切分而非逐字6. 这个项目还能怎么玩角色对话系统跑通之后我试了几个扩展方向都挺有意思。第一个是多角色群聊。让范闲、庆帝、陈萍萍在同一个对话里互相接话用户当旁白。实现方式是维护一个角色列表每轮随机或按顺序选一个角色来回复系统提示词里加上“当前对话中有其他角色在场”的说明。实测效果很欢乐庆帝和陈萍萍互相阴阳怪气的场面比原剧还精彩。第二个是剧情分支。用户的选择会影响剧情走向比如范闲问你要不要告诉林婉儿真相你选“告诉”或“不告诉”后续对话会走不同的线。这个需要维护一个简单的状态机记录用户的关键选择在系统提示词里动态注入当前剧情状态。第三个是语音交互。用TTS把角色的回复读出来范闲用年轻男声庆帝用低沉男声陈萍萍用阴柔男声。TTS我用的是ChatTTS效果还行但情感表现力跟专业配音比还有差距。第四个是AI短剧生成。把角色对话导出成剧本格式再用AI视频工具生成分镜画面就能做出一集“AI版庆余年番外”。这个方向我还在摸索主要是视频生成的一致性还不太好同一个角色在不同分镜里长得不一样。提示如果你也想做类似的项目建议先从单角色对话跑通再逐步加功能。一上来就搞多角色群聊剧情分支语音很容易在调试上耗光耐心。7. 一些实操心得角色扮演对话这个方向技术门槛其实不高但“调”的功夫很关键。同样的模型提示词写得好和写得差效果天壤之别。我最大的体会是不要试图用提示词“控制”模型而是用提示词“引导”模型。你写一堆“必须”“禁止”“绝对不能”模型反而容易紧张回复变得僵硬。你给它一个鲜活的人物小传几段有味道的对话示例它自己就能找到感觉。另一个心得是关于温度的。很多人调角色扮演喜欢把temperature拉得很高觉得这样才有创意。但我实测下来0.8左右是最平衡的再高角色就开始胡言乱语再低就变得像在念稿子。top_p设成0.9让模型在概率分布的前90%里采样既有多样性又不至于跑偏。最后说一个容易被忽略的点角色的“不知道”比“知道”更重要。范闲不应该知道庆帝是他爹至少在剧情早期陈萍萍不应该知道五竹是机器人。在提示词里明确写出“你不知道什么”比写出“你知道什么”更能让角色立得住。这个道理放在任何角色扮演场景都适用。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →