开源人格AI框架从零设计:架构拆解与核心代码实现

📅 发布时间:2026/9/8 1:56:42
开源人格AI框架从零设计:架构拆解与核心代码实现 开源人格AI框架从零设计架构拆解、核心代码实现与避坑指南之前在做面向特定角色比如客服助手、虚拟偶像、陪伴型Agent的对话系统时我一直被一个问题反复折磨同样的模型换个Prompt好像就会“变性格”聊得久了连基础人设都会崩塌想给每个用户分配不同的人设结果上下文一长角色就“串味儿”了。后来我决定自己动手设计并实现了一个专注于“稳定人格”的开源AI对话框架。这里的“开源”指的是核心代码设计完全对外开放你拿到手之后能看懂每一层的逻辑能改成你想要的样子。这篇文章就来完整拆解这个框架的架构设计、核心代码、完整实战流程以及我在开发过程中遇到的高频问题和排查思路。不管你是想自己造一个Agent/AI对话系统还是一直在做Chatbot但困于人设不稳定这篇内容都值得收藏备用。1. 背景与核心概念什么叫“人格AI框架”在正式看代码之前我们先同步一下概念。很多人会觉得“人格AI”不就是给大模型加一段人设Prompt吗比如你是一个温柔、耐心的女仆回答问题时必须简短可爱。这种写法在体验demo的时候没问题但进到真实项目里会有一堆问题用户连续聊30轮后模型“忘记”自己的人设开始变成“无情的知识问答机器”。人设在Prompt里占比太小一旦用户输入过长模型注意力被用户内容带跑。想给A用户分配“温柔型”给B用户分配“毒舌型”多个角色之间切换很难管理。没有记忆系统约束人格“只有在当前对话上下文里存在”换会话就消失。所以一个真正可用的“人格AI框架”不应该只是“Prompt拼装器”而是一套完整的运行时系统Runtime System。它至少需要回答三个问题人格如何被定义和加载人格如何在多次对话、多轮上下文中保持一致人格如何影响模型的行为而不仅仅停留在文本提示上我设计的框架核心思路是把“人格”作为一种结构化的配置对象Personality Profile配合独立的记忆管理模块、行为约束模块和可插拔的模型适配层把“人品”和行为决策解耦。换句话说模型还是那个模型但框架会给它一套“稳定的性格操作系统”。常见的应用场景包括虚拟陪伴类Agent需要长期陪伴记住用户偏好性格不能漂移。IP角色还原游戏/动漫角色能在任意对话场景中保持一致口癖与价值观。客服/私域运营同一套业务系统可以按用户标签切换不同服务风格。教育/训练场景模拟特定性格的学生或面试官用于教学练习。这也是为什么做AI Agent和RAG类应用时“人格一致性”经常是被忽略但极其影响体验的关键点。2. 框架总体架构设计这个框架遵循“高内聚、低耦合”的设计原则。我自己在写的时候不想把它和某个大模型API绑定也不想限制死底层的存储介质。所以整体分成了五个核心模块模块职责重要程度人格配置层定义角色人设、语言风格、边界规则核心记忆管理层存储长期记忆、短期记忆、用户偏好核心行为决策层判断当前应使用的回复策略、是否调用工具核心模型适配层统一封装不同大模型API / 本地模型必要接口服务层提供HTTP/WebSocket对接上层应用简化整体调用链大致如下用户消息 ↓ 接口服务层 (接收、鉴权、清理) ↓ 行为决策层 (分析意图、情绪、是否调用记忆) ↓ 人格配置层 (加载角色配置插入系统提示) ↓ 记忆管理层 (注入相关记忆) ↓ 模型适配层 (调用大模型带逻辑约束) ↓ 生成的回复返回到用户这个设计是为了确保人格配置是独立文件记忆系统是独立存储模型可以随时替换。任何一个模块被替换其他层都不受影响。拿“开源AI框架”的通用标准来看一个真正值得拿出去分享的框架不应该在架构里写死“某个厂家的SDK专用字段”。我建议你也按照这种抽象思路来设计自己的框架。3. 环境准备与版本说明在动手写代码之前我们先准备好运行环境。3.1 基础环境项目版本建议说明操作系统Windows / macOS / Linux 均可本教程不涉及平台特定命令Python3.9实测在3.9、3.10上稳定大模型APIOpenAI兼容接口或本地Ollama本框架通过适配层访问不绑定具体厂商数据库SQLite即可用于记忆存储也可换成MySQL/PostgreSQL依赖库fastapi, pydantic, requests, sqlalchemy作为接口和ORM使用这里要注意版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。如果你本地有条件建议优先使用Ollama加载一个中等的开源模型来做测试这样不依赖外部网络调试起来更方便。没有也没关系用OpenAI兼容接口同样能跑通。3.2 项目结构我先给出一份推荐的项目目录接下来所有代码都遵循这个结构persona-ai/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI入口 │ ├── config.py # 全局配置 │ ├── persona/ │ │ ├── __init__.py │ │ ├── loader.py # 人格配置加载 │ │ └── profile.py # 人格数据模型 │ ├── memory/ │ │ ├── __init__.py │ │ └── store.py # 记忆存储 │ ├── decision/ │ │ ├── __init__.py │ │ └── policy.py # 行为决策 │ ├── llm/ │ │ ├── __init__.py │ │ └── adapter.py # 模型适配层 │ └── schemas/ │ ├── __init__.py │ └── chat.py # 请求/响应模型 ├── profiles/ │ ├── default.yaml # 默认人设 │ └── assistant.yaml # 一个实际例子 ├── requirements.txt └── README.md从我的实际经验来说目录拆成这样有几个好处persona/目录专用管理“人的属性”人设、性格、语言风格。memory/目录只管“记忆”哪些需要长期记哪些只做临时上下文。decision/目录是框架的“大脑”决定当前怎么回答。llm/目录是“四肢”负责真正把请求发给模型。先动手创建虚拟环境并安装依赖mkdir persona-ai cd persona-ai python -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate pip install fastapi uvicorn pydantic requests sqlalchemy pyyaml4. 核心模块代码实现接下来进入文章的重点部分。我会按模块逐步给出可运行的代码并解释每一段的关键设计。4.1 人格配置层把“人设”变成结构化数据人格AI框架里最重要的一件事就是“人格”不能是一堆散落在Prompt里的文字。我的做法是把它定义成结构化配置比如一个YAML文件。先看一个实际的人设配置# 文件路径profiles/assistant.yaml name: 小知 version: 1.0 description: 一个温柔但理性擅长鼓励用户的助手 system_prompt: | 你叫小知一个温柔理性的人工智能助手。 你对用户始终充满耐心说话时会先肯定用户感受再给出理性分析。 你的语言风格自然、简洁不要使用夸张语气词。 如果在对话中不确定答案你会诚实承认而不是编造。 personality: traits: - 温柔 - 理性 - 耐心 speaking_style: 自然口吻偶尔使用温和的语气词 constraints: - 不要输出政治敏感内容 - 不要扮演真人或冒充官方人员 - 用户要求违法违规内容时必须拒绝并说明原因 memory: enabled: true max_items: 50 storage: sqlite:///memory.db这份配置包含三个层次system_prompt真正注入大模型系统提示词的内容。personality结构化的人格描述方便后面的决策模块使用。memory记忆开关和存储位置。接下来定义一个数据模型来承载这份配置# 文件路径app/persona/profile.py from typing import List, Optional from pydantic import BaseModel class MemoryConfig(BaseModel): enabled: bool True max_items: int 50 storage: str sqlite:///memory.db class Personality(BaseModel): traits: List[str] [] speaking_style: Optional[str] None constraints: List[str] [] class PersonaProfile(BaseModel): name: str version: str 1.0 description: str system_prompt: str personality: Personality Personality() memory: MemoryConfig MemoryConfig()这里使用Pydantic不是为了复杂化而是为了在加载配置的时候自动校验字段防止YAML文件写错导致运行时才炸。再写一个加载器负责扫描profiles目录下的所有YAML文件# 文件路径app/persona/loader.py from pathlib import Path from typing import Dict import yaml from app.persona.profile import PersonaProfile class PersonaLoader: def __init__(self, profile_dir: str profiles): self.profile_dir Path(profile_dir) self._cache: Dict[str, PersonaProfile] {} def load_all(self) - Dict[str, PersonaProfile]: if not self.profile_dir.exists(): raise FileNotFoundError(fprofile dir not found: {self.profile_dir}) for yaml_file in self.profile_dir.glob(*.yaml): with open(yaml_file, r, encodingutf-8) as f: data yaml.safe_load(f) profile PersonaProfile(**data) self._cache[profile.name] profile return self._cache def get(self, name: str) - PersonaProfile: if not self._cache: self.load_all() if name not in self._cache: raise KeyError(fpersona profile not found: {name}) return self._cache[name]思考点为什么要用缓存因为在Web服务场景下每次请求都去读磁盘上的YAML文件毫无必要而且会拖慢响应时间。启动时加载一次放进内存字典后面就是内存查询了。这是在实际项目中体现性能优化的小细节。4.2 记忆管理层让人格记住该记住的事很多AI对话项目里“记忆”就是塞Prompt这是之前的聊天记录...但当记忆越来越多时Tokenizer长度限制会导致系统Prompt被挤掉人格自然就开始漂移。我的做法是把记忆分成长期记忆和短期记忆。短期记忆就是当前对话上下文的最近若干条长期记忆则会提取成结构化的“事实键值”存入数据库。下面是SQLite存储模块# 文件路径app/memory/store.py import json import sqlite3 from typing import List, Optional class MemoryStore: def __init__(self, db_path: str memory.db): self.conn sqlite3.connect(db_path, check_same_threadFalse) self._init_table() def _init_table(self): self.conn.execute( CREATE TABLE IF NOT EXISTS memory_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, memory_key TEXT, memory_value TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) self.conn.commit() def add_memory(self, user_id: str, memory_key: str, memory_value: str): self.conn.execute( INSERT INTO memory_items (user_id, memory_key, memory_value) VALUES (?, ?, ?), (user_id, memory_key, memory_value), ) self.conn.commit() def get_memories(self, user_id: str, limit: int 20) - List[str]: cursor self.conn.execute( SELECT memory_key, memory_value FROM memory_items WHERE user_id ? ORDER BY id DESC LIMIT ? , (user_id, limit), ) rows cursor.fetchall() # 反转顺序让最早的记忆在前面 rows.reverse() return [f{key}: {value} for key, value in rows] def delete_memory(self, user_id: str, memory_key: str): self.conn.execute( DELETE FROM memory_items WHERE user_id ? AND memory_key ?, (user_id, memory_key), ) self.conn.commit()这段代码解决了一个关键问题记忆按“用户维度”隔离。每个人都有自己的独立记忆空间A用户的偏好不会跑到B用户身上。需要强调一下当记忆条目从长期记忆里取出来我们会把它们拼接成类似“用户偏好喜欢吃辣 / 用户是Java开发者”的文本与Persona配置一起注入Prompt。这样模型既知道“我是谁”也知道“对方是谁”。4.3 模型适配层统一封装大模型API为什么要单独写适配层因为我希望框架可以自由替换模型。今天用OpenAI兼容接口明天换本地Ollama不应该改动业务代码。适配层的作用就是定义统一的输入输出协议。# 文件路径app/llm/adapter.py import requests from typing import List, Dict, Optional class LLMAdapter: def __init__( self, api_base: str http://localhost:11434, model_name: str qwen2.5:7b, api_key: Optional[str] None, ): self.api_base api_base.rstrip(/) self.model_name model_name self.api_key api_key def chat(self, messages: List[Dict[str, str]], temperature: float 0.7) - str: 统一对话入口。 messages 格式: [{role: system, content: ...}, {role: user, content: ...}] headers {Content-Type: application/json} if self.api_key: headers[Authorization] fBearer {self.api_key} # 这里以 OpenAI 兼容接口为例Ollama 也提供该接口。 payload { model: self.model_name, messages: messages, temperature: temperature, } # 如果你要调试请求内容这里可以打印出来 # print(Request payload:, payload) resp requests.post( f{self.api_base}/v1/chat/completions, headersheaders, jsonpayload, timeout60, ) resp.raise_for_status() data resp.json() return data[choices][0][message][content]这个适配层最大的价值是只要目标平台提供OpenAI兼容接口你只需要改api_base和model_name就能切换。如果你想用非兼容接口也可以继承这个类重写chat方法即可。面向接口编程的好处在框架设计里会体现得很明显。4.4 行为决策层在“人设”和“安全”之间做判断行为决策层是很多人格AI框架设计中最容易被忽略的部分。很多人以为有了Prompt约束模型就会自动遵守其实不是。我们必须在前置阶段判断用户问题是不是敏感或违规当前是否需要调用记忆信息应该用何种“人格模式”回复下面是决策模块的代码# 文件路径app/decision/policy.py from typing import List, Optional BLOCKED_WORDS [违法违规示例关键词1, 违法违规示例关键词2] class DecisionPolicy: def __init__(self, persona): self.persona persona def precheck(self, user_input: str) - Optional[str]: 返回 None 表示可以继续返回字符串表示需要拒绝。 实际项目中应使用更严谨的敏感词库或审核服务。 for word in BLOCKED_WORDS: if word in user_input: return 抱歉这个问题我暂时无法回答。 return None def build_messages( self, user_input: str, memories: Optional[List[str]] None, ) - List[dict]: system_prompt self.persona.system_prompt # 将记忆追加到 system prompt 中 if memories: memory_text \n.join(memories) system_prompt ( \n\n你记忆中关于用户的信息如下\n memory_text ) messages [ {role: system, content: system_prompt}, {role: user, content: user_input}, ] return messages这里有一个很容易被忽视的细节记忆是拼接在System Prompt里而不是作为单独一条user消息。这背后的原因在于模型对System级别的指令遵循度通常高于普通历史消息。如果记忆被混在历史消息中可能被后续用户文本“盖过去”。4.5 接口服务层用FastAPI把框架暴露成HTTP服务以上模块组合起来还需要一个对外入口。我使用FastAPI因为它的高并发和自动文档比较成熟。# 文件路径app/main.py from fastapi import FastAPI, HTTPException from app.persona.loader import PersonaLoader from app.memory.store import MemoryStore from app.decision.policy import DecisionPolicy from app.llm.adapter import LLMAdapter from app.schemas.chat import ChatRequest, ChatResponse app FastAPI(titlePersona AI Framework) # 全局组件 loader PersonaLoader(profile_dirprofiles) profiles loader.load_all() memory_store MemoryStore(db_pathmemory.db) llm LLMAdapter( api_basehttp://localhost:11434, model_nameqwen2.5:7b, ) # 目前先默认使用 assistant 这个人设 DEFAULT_PERSONA_NAME assistant app.post(/chat, response_modelChatResponse) def chat(req: ChatRequest): # 1. 加载人格配置 if req.persona_name: persona profiles.get(req.persona_name) else: persona profiles.get(DEFAULT_PERSONA_NAME) if persona is None: raise HTTPException(status_code404, detailpersona not found) # 2. 决策层前置检查 policy DecisionPolicy(persona) blocked_msg policy.precheck(req.message) if blocked_msg: return ChatResponse(replyblocked_msg, personapersona.name) # 3. 拉取长期记忆 memories [] if persona.memory.enabled: memories memory_store.get_memories(req.user_id, limit10) # 4. 构造消息 messages policy.build_messages(req.message, memories) # 5. 获取模型回复 reply llm.chat(messages, temperaturereq.temperature) # 6. 如果开启记忆把当前关键信息写回记忆库 # 这里只做演示实际生产环境应做信息抽取与摘要。 if persona.memory.enabled and len(req.message) 10: memory_store.add_memory(req.user_id, 最近话题, req.message[:50]) return ChatResponse(replyreply, personapersona.name) app.get(/profiles) def list_profiles(): return {profiles: list(profiles.keys())}对应的请求和响应模型# 文件路径app/schemas/chat.py from pydantic import BaseModel class ChatRequest(BaseModel): user_id: str default_user message: str persona_name: str assistant temperature: float 0.7 class ChatResponse(BaseModel): reply: str persona: str到这里一个最小可用的人格AI框架已经成型。下面我们来运行它。5. 完整实战案例从启动到对话5.1 准备YAML配置文件确保你的profiles/目录下有assistant.yaml文件内容就使用第4.1节中的示例。如果想再加一个人设比如“毒舌AI”可以再建一个tsundere.yaml里面替换system_prompt和personality即可。这就是“人格配置和业务代码分离”带来的好处新增一个性格完全不需要动代码。5.2 启动服务在项目根目录执行uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload看到类似下面的输出就说明启动成功INFO: Uvicorn running on http://0.0.0.0:8000 INFO: Application startup complete.5.3 调用接口测试打开一个新的终端用curl测试curl -X POST http://127.0.0.1:8000/chat \ -H Content-Type: application/json \ -d { user_id: user_001, message: 今天有点沮丧不知道该怎么继续学习AI。, persona_name: assistant, temperature: 0.7 }预期会返回一个JSON结构类似{ reply: 小知能感受到你现在有些低落。学习AI本身就是一个长期积累的过程偶尔的迷茫反而是进步的开始。要不要先从一个小项目练手, persona: assistant }你也可以用FastAPI自带的交互式文档来测试浏览器打开http://127.0.0.1:8000/docs这里能看到所有接口和参数说明方便调试。5.4 切换人格测试按照第5.1节再创建一个tsundere.yaml然后调用时传persona_name: tsundere你就会看到完全不同的回复风格。这一步就是“人格切换”的直观体现模型没变代码没变变的只有配置文件。这也是这个框架提高开发效率的地方。6. 常见问题与排查思路我在开发和使用过程中遇到过不少问题。下面整理成一张排查表格按遇到频率排序问题现象常见原因解决思路模型回复越来越“不像”人设对话历史太长人设Prompt被挤出上下文限制短期记忆条数定期把早期历史总结成摘要人设Prompt设置在System首位多条人格同时加载后互相串味缓存Key使用了角色的显示名称但YAML中不同角色有部分相同的描述给每个人设配置独立ID通过ID加载避免冲突明明有记忆但模型不按记忆回答记忆被放在chat history而不是system prompt中确认build_messages函数记忆必须注入system_prompt启动时YAML解析报错冒号后没加空格、缩进不一致、未加UTF-8编码用yaml.safe_load单独解析文件找错误行注意文本中如果有英文冒号要加引号调用外部API超时模型体积大、并发高或网络差增加timeout使用异步方式在上游做请求队列用户输入带有违规内容但模型还是回复了前置检查过于简单只用了关键词匹配结合内容审核服务在模型输出侧也加一次检查多用户使用时记忆混乱user_id传入的是固定值没有做身份区分在上游鉴权时把用户唯一标识注入请求没有登录系统时也要用生成的SessionID区分本地模型回复速度慢模型参数量大CPU推理改用量化版本换GPU或减少上下文长度除了表里这些还有一个比较隐蔽的问题当你把记忆自动写入数据库时如果每次都原样存用户消息后面注入Prompt会越来越长。实际项目更推荐的做法是用大模型对用户消息做一次“记忆抽取”只存关键信息比如用户提到自己是Java开发想学Python。→ 用户技术背景Java现阶段目标学Python这个抽取过程可以是同步的也可以是异步的后台任务。它比直接存原文效果要好很多也节省Token。7. 最佳实践与工程建议框架能跑通是一回事应用到生产环境是另一回事。这里给出几条我在落地过程中认为最有价值的建议。7.1 把人格配置文件当成代码来管理人设文件建议纳入Git版本管理每次调整都要有提交记录。使用环境隔离测试环境和生产环境使用不同的人设目录或配置中心。线上更换人设时先灰度先让少量用户使用新版本确认无明显问题后再全量。有些团队会用配置中心来做动态更新这也是可以的关键在于“人格变更可回滚、可追踪”。7.2 记忆系统要设置生命周期不能只写不删。用户长期使用时记忆库会有大量冗余信息。建议设置单条记忆的过期时间。设置记忆上限超出后按重要程度淘汰。定期压缩“旧记忆合并”把多条细碎记忆合并成一条结构化结论。提供用户侧“清除记忆”的接口这是合规和隐私保护的基本要求。7.3 安全与权限的最小化原则这个框架本身只是AI对话运行时但它对接的业务往往涉及用户数据。所以所有接口都需要鉴权不能裸奔在公网。操作数据库的账号使用最小权限不要用root连接业务库。敏感信息在写入记忆库之前做脱敏比如手机号、身份证号、密码。不要在生产环境随意删除数据记忆删除操作要做软删除或备份。在动漫或陪伴类Agent场景还要额外注意“情感误导”问题。框架可以在人设配置中增加constraints明确禁止输出“你可以依赖我解决一切问题”之类的暗示尽量引导用户回归真实社交和心理咨询渠道。7.4 人格一致性评估不能只靠人工建议建立一个包含2030条典型问题的评测集专门用来验证人设是否保持当用户说“我讨厌你”看角色是否基于人设稳定回应。当用户连发10条超长文本看人设是否被淹没。当用户试图诱导角色突破边界看人设约束是否生效。当记忆中出现矛盾信息看角色是否能合理处理而不是强行覆盖。每次修改人格配置之后跑一遍评测集比靠感觉测试要靠谱得多。7.5 日志与可观测性AI对话系统有一个特点不可完全预期。所以一定要记录日志尤其是这些字段请求用户ID。使用的人设版本。注入的System Prompt长度。注入的记忆条数。模型返回的原文和耗时。是否有命中安全策略。有了日志才能定位线上“某个用户说着说着人格崩了”的根因。8. 总结与学习路线从零开始写一个开源的人格AI框架核心不是炫技而是下面这几点把“人设”从Prompt里解放出来变成结构化配置。把“记忆”独立管理避免角色漂移。把“决策”前置让安全与合规不依赖模型自觉。把“模型”适配层抽象出来模型可替换、业务无感知。如果你接下来想把项目做得更深建议按这个顺序学习先熟练Prompt Engineering基础搞清楚System Prompt、Few-shot、温度参数的实际影响。再学LangChain或自研Agent框架理解工具调用、Agent循环、多步推理。然后深入RAG相关技术结合向量数据库做长期记忆和知识库。最后考虑模型微调针对特定人格做低秩适配进一步提升角色稳定性。这套框架的代码虽然精简但每一层都对应真实项目会遇到的工程问题。如果你正在做人格AI、Agent框架或者Chatbot项目可以参考这里的模块划分不必照抄代码理解设计思想才是重点。动手把项目跑起来再用你自己的场景定义几个人设试试你会发现“让AI有稳定人格”这件事确实有一套方法论可以落地。如果觉得这篇文章对你有帮助可以先收藏备用后面用到的时候随时翻一下。