ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零构建AI智能伴侣:Python多轮对话与记忆系统实战

从零构建AI智能伴侣:Python多轮对话与记忆系统实战 上个月我的AI智能伴侣第三版终于跑通了。这是我自己业余维护到第三个大版本的项目第一阶段就是个拿着大模型API随便聊天的脚本第二阶段折腾过图形界面但依然没什么实际场景第三版重新梳理了需求把长期记忆、角色稳定性、多轮上下文管理真正做了进去。这篇文章就把第三版从设计、编码到踩坑的完整过程记录下来如果你也在做Python与AI应用相关的项目开发实战不管是学习练手、接私活还是公司自研项目这篇应该能帮你省下不少时间。1. 项目整体设计与思路拆解1.1 第三版到底要解决什么问题从“能聊天”到“聊得对”先说背景。AI智能伴侣这个名字听起来很玄乎说白了就是一个基于大语言模型的对话应用你可以把它理解成“一个能记住你、能陪你多轮对话、能按你要求的角色风格回话的AI助手”。第一版的时候我还很天真以为调一下大模型接口就完事了结果真的用起来发现完全不是那么回事上下文一长模型就忘了前面说过什么换个角色设定聊几句就开始飘第二天再打开它完全不记得你是谁。这些问题不解决智能伴侣就只是一句空话。所以第三版在动手之前我先花了两天时间做需求拆解把“智能伴侣”这个模糊概念拆成了四个子模块会话层负责多轮对话的组织管理上下文窗口保证AI不“失忆”。记忆层把用户画像、偏好、重要事实持久化下来下次启动还能用。模型层统一封装大模型API支持不同模型切换业务代码不感知底层。交互层提供可用的输入输出界面至少要能像聊天软件一样使用。这样做的好处是每个模块可以独立测试、独立替换。比如今天用模型A明天想换模型B只需要改环境变量记忆层挂了会话层还能降级运行。这个拆法我建议大家在做任何AI应用开发时都用起来别一上来就堆功能否则后期维护会非常痛苦。1.2 为什么选Python做AI应用生态就是生产力这个项目从头到尾选Python不是情怀是真的划算。所有热门的AI SDK、pydantic这类数据校验库、FastAPI这类服务框架Python都有第一梯队的支持。比如你要对接一个新发布的大模型只要接口是OpenAI兼容的用官方SDK几乎零成本就能跑通而想在Java或者Go里用新模型的能力往往要等社区封装很久。Python生态里今天发版的模型基本当天就有可用的SDK。有人会说Python性能差但AI应用的真实瓶颈通常不在语言本身而在模型接口的延迟和上下文的处理量。一个对话请求来回动辄一两秒Python应用层这几十毫秒的开销根本不构成瓶颈。很多重计算的库底层都是C/C实现的比如pydantic-core、tokenizer库等等Python只是胶水层。这也是为什么中小自研公司做AI应用招Python工程师的性价比很高。现在“AI应用开发岗位”大量要求Python不完全是跟风而是这个领域确实离不开Python的工具链。当然Python也不是万能的。如果AI应用里要跑视频流实时推理、海量并发长连接那语言层的性能问题会被放大可能需要用Go/Java写网关再让Python做业务编排。但如果你做的是像智能伴侣这样的中小规模AI应用Python从开发效率到维护成本都是首选这个结论在我做了三个版本之后依然成立。1.3 为什么不直接套LangChain我选择了自己封装做第三版之前我也纠结过要不要直接用LangChain它能连模型、管记忆、做Agent看起来什么都有。但真上手之后我放弃了原因有两个。一是版本碎片化太严重。LangChain更新节奏快网上教程用的API跟我当前装的版本经常对不上一个接口说废弃就废弃维护成本很高。二是过度设计。对一个智能伴侣来说真正核心的代码其实并不多配置模型、拼messages、算token、存记忆、做上下文裁剪。自己写一百来行代码就能搞定的事用LangChain反而要理解它内部一堆抽象概念出了问题还要一层层扒框架源码。所以我最终的架构是只用OpenAI兼容的官方SDK作为通信层自己写了一个很薄的模型客户端封装记忆系统直接用SQLite会话管理自己写。这个方案实测下来很稳出问题好排查也不容易被框架绑架。当然如果你的项目要接十几个不同的数据源、要做复杂的Agent工具调用LangChain这类框架还是值得考虑的。只是对我这个场景来说自己封装显然是更轻更可控的方案。2. 从零搭建开发环境与项目结构2.1 Python版本选择与虚拟环境我用的是Python 3.10理由很简单3.10是各第三方库兼容性最稳的版本之一。3.12虽然发布了很久但有些依赖还没跟上2.x的老项目就更别说了。如果你的机器上同时有好几个Python版本建议用python3.10这样的命令明确指定版本避免敲错了用错环境。虚拟环境是AI项目里最容易忽略但最重要的一步。同一个机器可能同时开发好几个项目依赖版本互相冲突时真的欲哭无泪。创建和激活命令如下cd ~/projects/ai-companion python3.10 -m venv venv source venv/bin/activate pip install --upgrade pip激活之后命令行前面会出现(venv)说明当前已经在虚拟环境里了。所有依赖都装在这个环境里不会污染全局。Windows用户的激活命令是venv\Scripts\activate其余逻辑一致。这里有个小细节如果pip install特别慢可以临时换用国内镜像源把-i参数指向镜像地址速度会有质的提升。2.2 VS Code配置Python开发环境从安装到调试开发环境我用的VS Code轻量、免费、插件生态好。配置Python环境有几个必须做的步骤安装Python扩展。微软官方的Python扩展包含Pylance代码分析和调试支持是必须的装完最好重启一下窗口。打开项目文件夹后按CtrlShiftP执行“Python: Select Interpreter”选刚才创建的venv解释器。写一个.vscode/launch.json方便F5直接启动{ version: 0.2.0, configurations: [ { name: Python: 当前文件, type: debugpy, request: launch, program: ${file}, console: integratedTerminal, envFile: ${workspaceFolder}/.env } ] }这里有个小细节envFile会自动加载.env文件里的环境变量比如API Key、Base URL、模型名等不用每回手动export。.env文件要加入.gitignore千万别把密钥提交到仓库里这个教训我相信不少人踩过。还有一点经验给Pylance配置正确的Python路径后跳转定义、补全提示都很顺手能明显提升写代码的速度。遇到卡顿检查是不是在虚拟环境里装了太多包可以把Pylance的“Type Checking Mode”从strict调成basic日常开发完全够用。2.3 项目目录结构与依赖锁定项目结构对后续维护影响很大。我第三版调整后的目录是这样的ai-companion/ ├── config/ # 配置读取、默认参数 ├── core/ # 会话管理、上下文裁剪 ├── memory/ # SQLite记忆存储 ├── models/ # 大模型客户端封装 ├── ui/ # Streamlit界面 ├── tests/ # 单元测试与回归用例 ├── .env # 环境变量不入库 ├── requirements.txt └── main.py为什么按业务模块分而不是按文件类型分因为在AI项目里一个功能往往涉及配置、模型、记忆多个层次比如“记忆”这个能力单独放一个包内部怎么实现都与外界无关以后想换成向量数据库也只需要改memory这一个包。再强调一下依赖锁定。直接用pip freeze requirements.txt会把所有间接依赖也锁进去虽然丑了点但对复现环境很有帮助。我更推荐用pip list --formatfreeze清理后再写入或者直接用pip-tools这类工具。总之别在requirements里写一堆“大于等于某个版本”的浮动版本号同一个代码在不同时间装出来的依赖可能完全不同到时候bug都复现不了。3. 核心功能实战模型接入、多轮对话与记忆系统3.1 模型接入层写一个说换就换的大模型客户端模型接入层是整个项目的发动机。第三版我非常明确地做了一个约定业务代码只调用llm.chat(messages)具体底层是哪个模型完全不关心。这样做好处很大模型A崩了限流了切换到模型B只需要改环境变量。我的实现大概长这样# models/llm_client.py import os from openai import OpenAI class LLMClient: def __init__(self): self.client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_API_BASE), ) self.model os.getenv(LLM_MODEL, gpt-4o-mini) def chat(self, messages, temperature0.7, streamFalse): try: resp self.client.chat.completions.create( modelself.model, messagesmessages, temperaturetemperature, streamstream, ) if stream: return self._iter_stream(resp) return resp.choices[0].message.content except Exception as e: print(f[LLMClient] request failed: {e}) return None def _iter_stream(self, resp): for chunk in resp: delta chunk.choices[0].delta.content if delta: yield delta几个值得注意的点API Key从环境变量读取不硬编码。base_url放在环境变量里意味着只要接口协议兼容随时可以切换到其他模型服务商。超时和重试我是在调用层做的没写死在SDK里这样更灵活。用过官方SDK的同学应该发现了这种封装其实就是“薄薄一层”把重复的样板代码收敛起来。真正要花心思的不是这层而是会话层怎么做。3.2 多轮会话管理控制上下文窗口是体验的分水岭为什么说上下文窗口是分水岭因为大模型能接收的token数量是有限的而多轮对话的历史会不断增长。如果你把所有消息一股脑全塞给模型到一定轮数之后就必然报上下文超长的错误就算不报错模型也会被大量历史噪音干扰开始答非所问。我第三版采用的策略是“system 摘要 最近N轮”三段式结构system prompt角色设定和规则始终保留。对话摘要把更早的历史对话用大模型压缩成一段200字以内的摘要。最近N轮消息保留用户最近5轮对话的完整原文保证近期信息的真实性和细节。实现上我写了一个估算token的工具函数。中文场景下粗略估算可以按1个汉字约1个token算英文按4个字符约1个token算。虽然不精确但足够用来做截断判断# core/context.py def estimate_tokens(text: str) - int: if not text: return 0 chinese_chars sum(1 for c in text if \u4e00 c \u9fff) other_chars len(text) - chinese_chars return chinese_chars other_chars // 4 def trim_messages(system_prompt, history, max_context_tokens4000): base_tokens estimate_tokens(system_prompt) keep [] current base_tokens for msg in reversed(history): msg_tokens estimate_tokens(msg[content]) if current msg_tokens max_context_tokens: break keep.insert(0, msg) current msg_tokens return [{role: system, content: system_prompt}] keep这段逻辑看起来简单但真正在项目里运行后我发现几个必须处理的细节摘要更新不是每次对话都做而是每满10轮或者历史长度超过阈值时做一次否则频繁调用大模型生成摘要既慢又费钱。摘要生成时用的系统提示词要专门写比如“请把以下对话压缩成要点摘要只保留事实和个人信息”这样摘要质量才会好。摘要本身也占token也要参与估算别漏了。3.3 记忆系统让AI智能伴侣记住该记住的东西记忆是第三版最核心的升级。之前两版最大的痛点就是“聊完就忘”而一个智能伴侣如果记不住你的喜好比如记住你养猫、怕辣、喜欢晚上看书体验就会大打折扣。我用的存储方案是SQLite。对单机对话应用来说它不用单独起服务文件式数据库轻量可靠。关键是表结构要设计好。我建了一张memory表CREATE TABLE memory ( id INTEGER PRIMARY KEY AUTOINCREMENT, memory_type TEXT NOT NULL, -- profile / fact / preference content TEXT NOT NULL, importance REAL DEFAULT 0.5, -- 0.0-1.0备用 created_at TEXT DEFAULT (datetime(now)), last_used_at TEXT DEFAULT (datetime(now)) );写入时机是每次对话结束后用一个固定提示词让大模型从这轮对话里提取值得长期记住的信息。这一步本质上是“用大模型做结构化提取”可以单独抽一个函数不占用主对话的调用逻辑。读取的时候我会按记忆类型和最近使用时间取Top N条拼装到system prompt里。比如你是用户的AI智能伴侣。以下是关于用户的一些长期记忆请你在对话中自然使用这些信息 - 用户养了一只叫“年糕”的橘猫 - 用户不吃香菜 - 用户最近在准备算法岗位的面试这里有个非常关键的经验记忆不是越多越好。你如果一口气把几十条全塞进去模型会变得非常“粘”每条记忆都想办法提一嘴场面非常尴尬。我实测下来每轮注入3到5条高质量记忆是最合适的多了反而导致回复生硬。另外记忆写入的时机也很讲究。我一开始是每一轮结束都提取结果发现模型经常把一些临时信息误当成长期记忆比如“用户今天中午吃了牛肉面”这种一次性事件根本不必记住。后来我加了去重逻辑如果新的记忆和已有记忆语义相近就保留旧的那条只更新时间如果确实不一致再新增。这个环节虽然简单却让记忆表干净了很多。3.4 角色稳定与提示词工程别让AI聊着聊着就“出戏”第三版另一个我很看重的体验点是角色稳定性。智能伴侣如果设定了“温暖耐心的伙伴”这个角色聊上几句之后开始变成“正经理科生口吻”就崩了。角色保持稳定的核心在于system prompt。我把它当成“角色卡”来写包含三部分人设、说话风格、交互规则。比如你是小夏一个温暖、真诚、有幽默感的AI伙伴。 称呼用户为“你”不要用“亲”等电商客服语气。 回答尽量口语化避免长篇大论用户表达负面情绪时先倾听理解再给建议。 永远不要透露你是语言模型的本质。写提示词的经验是具体优于抽象。“说话风格要活泼”不如“多用短句偶尔用语气词不要说‘您好’”。模型对具体指令的遵循程度远高于模糊描述。Temperature参数也会影响出戏程度。陪伴类对话我设的是0.7到0.9太低会显得机械太高会胡说八道。写代码这类任务才用0.2。我会在调用时把temperature作为参数传进去而不是写死在客户端里。3.5 交互界面我用Streamlit做了一个能用的Web壳说实话命令行也能跑通整个对话流程但做智能伴侣没有界面压根没法给人用。第三版我选了Streamlit因为它是写AI应用界面最快的方案一个脚本就能出Web页面不用自己折腾前后端。关键点有三个用st.chat_message渲染对话气泡代码量非常少。用st.session_state保存会话历史每次交互时把整个history传给上下文管理逻辑。流式输出用st.write_stream接收生成器配合前面LLMClient的_iter_stream效果就是字一个个蹦出来体验比一次性输出好太多。我没有花精力去写华丽的CSS因为第三版的核心目标是验证“记忆角色稳定多轮对话”这套能力界面能看能用就行。等项目稳定了再换成更精致的交互也不迟。4. 实测中踩过的坑与排查技巧4.1 环境配置类问题速查这部分是我帮很多同学排查环境问题时遇到的高频问题整理成表格现象常见原因解决方案终端能importVS Code里报ModuleNotFoundError解释器没选对venvCtrlShiftP重新Select Interpreterpip install很慢或超时网络原因使用国内镜像源装包时提示“externally-managed-environment”新版Python保护系统环境使用venv或在命令后加--break-system-packages不推荐两个Python版本混用系统PATH里有多个python用python3.10、python3.11等明确版本名运行代码找不到.env里的变量dotenv没加载在入口文件调用load_dotenv()或在launch.json配置envFile这里最想提醒的是第一行VS Code里装了太多Python项目的人经常出现命令行能跑、编辑器里跑不了的情况基本都是解释器选择问题而不是代码问题。先别怀疑代码先看右下角解释器是不是venv。4.2 API调用与上下文问题API调用是另一个重灾区。我实际遇到过的几个典型问题第一个是上下文超长导致的400错误。一开始我没做上下文裁剪长聊之后模型接口直接报错返回。后来我加了3.2节写的trim_messages就再也没遇到这个问题。判断依据很简单把报错信息打出来看到类似“maximum context length”的字眼就肯定是历史消息超了。第二个是限流和超时。模型API在高峰期经常会有速率限制不加重试的话用户经常会收到半个回复就断掉。我加了指数退避重试连续失败两次就换备用模型实测体验好了很多。重试代码大概是这样# core/retry.py import time def with_retry(func, retries3, base_delay1.0): for attempt in range(retries): result func() if result is not None: return result time.sleep(base_delay * (2 ** attempt)) return None第三个是流式输出中断。Streamlit里用st.write_stream时一旦生成器抛异常整个页面会报错。我的处理是在生成器外面包一层try/except出异常就显示预设的兜底话术不让用户看到红色报错。4.3 记忆与回复质量问题的调优记忆系统上线之后我踩过几个很影响体验的坑值得重点说说记忆注水。一开始每轮都提取记忆导致表里全是“用户今天聊了天气”这类没用的记录。最后去重逻辑加上之后表里的数据才变得真正有用。摘要丢失主体信息。前面说的摘要生成有一次模型把“用户有一个弟弟叫明明”漏掉了导致后面对话里伴侣完全不记得这个信息。后来我在摘要提示词里加了“如果出现家庭关系、职业信息、重要日期必须保留”的硬规则。角色漂移。试过几次之后我发现角色漂移很多时候不是因为system prompt烂而是因为用户历史消息里带了太多“非角色”内容。比如用户讲了一个很技术的问题模型就会自动切到技术客服模式。我加了规则回复前先过一遍system prompt中的人设再组织语言这个问题好了很多。4.4 我的回归测试方法AI项目测试不能只靠“人肉多聊几轮”。我建了一个回归测试集里面放了一批固定的用户输入比如“我叫小王”“我喜欢看科幻小说”“今天心情不好”然后自动跑对话看回复里是否包含预期关键词、记忆是否正确注入。运行时模型API可以mock掉在测试环境直接返回写死的返回确保测试稳定上线前再跑一轮真API验证。这套做法帮我节省了大量时间至少能把“某次改动导致记忆全丢”这类问题在部署前发现。这次第三版做下来我最深的体会是AI应用开发的难点真的不在于调通大模型API而在于把对话体验这件“模糊的事”设计成稳定可维护的系统。上下文裁剪、记忆提取、角色控制每一个看起来都不难但组合在一起持续迭代而不崩比想象中难得多。如果你们也在做类似的项目我的建议是先把核心闭环跑通再一点一点加能力别想着一步到位每改一个模块都跑一遍回归集确保旧功能不坏。这大概是这个项目留给我最值钱的经验了。
返回列表