1. 项目概述与定位
1.1 这个项目的核心是什么
我在本地搭了一套基于RAG(检索增强生成)的感情智能助手。说白了,就是把大语言模型完全跑在自己电脑上,让它可以读取你的私人资料——日记、聊天记录、收藏的文章、读书笔记——然后基于这些内容和你对话,并且能理解你说话时的情绪状态,做出带有温度的回应。
这和普通的聊天机器人有本质区别,普通聊天机器人是"人多力量大"式的通用回答,你问它什么,它从训练数据里泛泛地答。而这个助手是"私人定制"式的,它知道你看过什么书、写过什么日记、关注过哪些事。你对它说"我最近好累",它不会给你灌一篇标准化的心灵鸡汤,而是会想起你上个月日记里写的那次项目冲刺,想起你收藏的那篇关于倦怠管理的文章,然后基于这些东西来回你。
项目解决的核心问题有两个。第一是隐私,感情类数据是最敏感的,日记、聊天记录这些东西让云端模型去读,很多人心理上就过不去这一关,本地部署能从根本上切断数据外流的路径。第二是情感理解和上下文结合,纯靠大模型的通用能力做情感陪伴往往流于表面,而RAG能把私人的、碎片化的历史信息变成助手的"记忆",让它的回应有的放矢。
适合做这个项目的人有两类。一类是对RAG技术本身感兴趣的开发者,想搞明白检索增强是怎么落地的,这个项目难度适中,技术栈完整,是个很好的练手对象。另一类是对AI情感陪伴有真实需求的人,比如想给自己做一个长期记忆的树洞,或者想给孩子做一个能和学校知识、家庭记录联动的智能伙伴,这个项目可以做到完全离线、完全可控。
1.2 这个项目不是"聪明小玩具"
在动手之前,我得先把预期管理做好。这套系统不会像ChatGPT那样什么都懂、张口就来,因为它的知识范围被刻意限制在了你喂给它的私人资料里。但恰恰是这个"限制",造就了它最大的价值:确定性。你问它日报里写过的某个决定,它不会瞎编,因为它会先去检索引擎里找到相关段落,再基于那段内容回答。如果检索不到,它会老老实实说"这个我没有记录",而不是编一个答案糊弄你。
感情智能的部分,也不是那种浮夸的"我检测到您今天情绪低落,为您播放轻音乐"。我做的更克制一些:它会识别你当前的情绪倾向(正面、负面、焦虑、疲惫等),在检索和生成时考虑这个信号,调整回应的策略。你情绪低落时它少一点俏皮话,多倾听多共情;你明显开心的时候它再开玩笑。这种动态调整,比一个固定人格的聊天机器人真实得多。
另外提醒一下,别把这个项目当成什么科研工作,它就是一个务实落地的东西。技术栈选型也全部以"本地能跑、配置不苛刻、效果够用"为原则,所以我会尽量避免那种动辄要求24GB显存的企业级方案。
2. 技术选型与架构设计
2.1 模型选型:本地跑得动的才是好模型
整个系统的核心引擎是本地大模型,我选了Ollama作为模型运行时。原因很直接:Ollama对普通用户友好到极致,安装完敲两行命令就能拉起模型,而且它对内存和CPU的利用优化做得不错,笔记本也能跑得动中小尺寸的模型。
模型本身我建议从Qwen2.5和Llama 3.2这两个系列里选。注意看参数规模,不要贪大:
| 模型 | 参数规模 | 量化方式 | 内存需求 | 中文情感理解 | 推荐场景 |
|---|---|---|---|---|---|
| Qwen2.5-7B-Instruct | 7B | Q4_K_M | 约6GB | 优秀,中文语感好 | 首选,通用对话能力强 |
| Qwen2.5-3B-Instruct | 3B | Q4_K_M | 约3GB | 良好 | CPU也能跑,低配机器选它 |
| Llama 3.2-3B-Instruct | 3B | Q4_K_M | 约3GB | 一般,英文更好 | 英文资料多的场景 |
| Llama 3.2-1B-Instruct | 1B | Q8_0 | 约1.5GB | 较弱 | 纯测试验证流程用 |
我实测下来,7B的Qwen2.5在情感理解上明显甩开同尺寸的Llama,尤其在处理中文的含蓄表达时,比如"没事"这种话,Qwen能读出潜在的低落情绪,Llama经常当真觉得没事。如果你手里有16GB内存的电脑,建议直接上7B Q4量化版;如果只有8GB内存,老实点用3B,把上下文窗口开大一点,效果也能接受。
2.2 向量库选型:轻量优先
RAG需要向量数据库来存资料切片。市面上方案很多,Milvus、Weaviate、Qdrant这些都是好工具,但对本地单机项目来说太重了,像是开卡车去菜市场买菜。我选的是ChromaDB,理由很务实:
- 纯Python实现,pip装完就能用,不用额外起服务
- 默认本地持久化,数据落盘就是一个文件夹,备份迁移很方便
- API设计简洁,检索、写入、过滤一两条代码搞定
- 单机千万级向量以内性能足够,这个项目根本到不了那个量级
如果说你的资料量特别大,超过了几十万个切片,再考虑换Qdrant社区版,它有更好的过滤和索引机制。但绝大多数个人助手项目,ChromaDB都是最优解。
2.3 整体架构的工作流程
整个系统跑起来之后,一次对话的处理链路是这样的:
- 用户输入一句话,先经过情绪识别模块
- 情绪识别结果和用户原话一起进入检索器
- 检索器把用户原话做向量化,从向量库里召回最相关的Top-K个文本切片
- 把切片、用户的原话、情绪标签、对话历史拼成一个大提示词
- 交给本地大模型生成回复
这里有个关键点:情绪识别是先用一个轻量规则模型做的,不是直接丢给大模型。因为每次对话都经过大模型做情绪识别的话,耗时会增加两三秒,而且会让整体成本翻倍。规则模型做初步情感分类,把值得关注的情况标出来,大模型只在生成时感知情绪,这样既快又稳。
2.4 为什么不用纯Prompt硬怼
有人可能会想,直接把资料全部塞进上下文,不搞RAG,不是更简单吗?我试过,这条路走不通。7B模型的上下文窗口一般是8K到32K token,折算成中文大约是一万到四万字,你根本没法把一整年的日记塞进去。塞进去之后还有一个更严重的问题,大模型对长上下文的注意力是分散的,中间段内容经常被忽略,生成的回答反而因为信息过载变差。
RAG的本质是"外挂硬盘",先做精确检索,再去读需要的那一小段。既不受上下文窗口限制,又能保证回答是基于检索到的具体内容生成的。这也是RAG在本地部署场景下依然是主流方案的根本原因。
3. 环境准备与模型部署
3.1 硬件配置与量化概念
先说硬件底线。我的主力测试机是Intel i5-12400 + 32GB内存 + GTX 1660(6GB显存),跑Qwen2.5-7B的Q4量化版,生成速度大概是每秒15到20个token,体感就是打字速度略慢一点,但够用。如果你没有独立显卡,用纯CPU跑7B Q4也不是不行,但速度会掉到每秒3到5个token,急死人,建议用3B模型。
这里解释一下量化。大模型的参数通常是FP16(半精度浮点数)存储的,7B模型的FP16权重大约14GB,普通家用机扛不住。量化就是把权重压缩成4bit或8bit,牺牲一点精度换取体积和速度。Q4_K_M是4bit量化里的一个不错方案,质量损失很小,但模型文件直接从14GB降到4.4GB左右。本地部署,量化不是可选项,是必需品。
3.2 Ollama的安装与模型拉取
Ollama官网下载对应系统的安装包,装完就完事了。Windows和macOS都有原生安装包,Linux用一条curl命令。装完之后验证一下:
ollama --version然后拉取模型。我先拉对话模型:
ollama pull qwen2.5:7b-instruct-q4_K_M这里有个细节,Ollama的模型名是带标签的,不指定标签默认拉取该系列的最新版本。如果内存不够,换成3B:
ollama pull qwen2.5:3b-instruct-q4_K_M拉完以后可以用命令行先聊两句试试:
ollama run qwen2.5:7b-instruct-q4_K_M3.3 Embedding模型的选择
RAG的检索环节需要embedding模型,它的作用是把文本变成一串向量数字,让语义上相近的文本在向量空间里靠得近。这个模型不需要有对话能力,但必须对中文语义理解足够好。
我推荐用Ollama里的nomic-embed-text,或者bge-m3。对比一下:
- 输入侧embedding的选型直接决定了检索质量的上限
- 支持8000+ token的输入长度,对长文档切片很友好
- 中文效果比同尺寸的英文embedding模型好一个档次
拉取命令:
ollama pull bge-m3如果你不喜欢Ollama管理embedding,也可以直接用HuggingFace上的sentence-transformers库,但Ollama的方式更省事,反正都是本地跑的。
3.4 启动模型的注意事项
Ollama默认会常驻后台服务,端口是11434。启动模型的时候可以调整两个参数:OLLAMA_CONTEXT_LENGTH控制上下文窗口长度,OLLAMA_NUM_PARALLEL控制并发请求数。对个人助手来说,上下文窗口可以放到8192以上,并发数1就够了,因为你就一个人用。
注意:Ollama默认不设置context length时,有的模型只会用2048的窗口,这在RAG场景下会截断太多内容。建议在启动Ollama服务前设置环境变量
OLLAMA_CONTEXT_LENGTH=8192,或者在后端代码调用时显式传入num_ctx参数。我一开始没注意这个,结果检索回来的原文被截断得七零八落,答非所问。
4. 知识库构建:从原始资料到向量索引
4.1 文档处理与文本拆分
知识库的原材料就是你喂给助手的资料:日记txt、微信聊天导出、读书笔记Markdown、收藏的网页。第一步是解析成纯文本,然后把长文本切成适合作检索的块。
文本拆分是整个RAG流程里最容易被低估的环节。块太大,检索回来的内容太泛,大模型读不过来;块太小,语义被切断,检索召回率低。我实测下来,中文场景下每块200到300字比较合适。具体实现用递归字符分割,按段落、句子、标点逐层切:
from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=250, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] ) chunks = text_splitter.split_text(raw_text)chunk_overlap这个参数值得说道一下,它让相邻两个块之间保留50字的交叠部分,这样跨块的上下文信息不会被拦腰截断。我最早没设overlap,结果出现了大量句子前半截在上一块、后半截在下一块的情况,检索哪怕命中了一块也是残缺信息,生成的质量大打折扣。
4.2 向量化与写入向量库
拿到文本块之后,需要用embedding模型把它们变成向量,然后写到ChromaDB里。完整流程:
import chromadb from chromadb.utils import embedding_functions # 使用Ollama的embedding模型 ollama_ef = embedding_functions.OllamaEmbeddingFunction( url="http://localhost:11434/api/embeddings", model_name="bge-m3" ) client = chromadb.PersistentClient(path="./memory_store") collection = client.get_or_create_collection( name="personal_memory", embedding_function=ollama_ef, metadata={"hnsw:space": "cosine"} ) # 写入 collection.add( ids=[f"chunk_{i}" for i in range(len(chunks))], documents=chunks, metadatas=[{"source": source_name, "date": date_str} for source_name in ...] )这里面有几个值得注意的决策点。第一,hnsw:space用cosine还是ip,取决于你的embedding模型,bge系列模型官方推荐cosine,效果最稳。第二,元数据一定加上source和date,后面按时间过滤、按来源追溯都会用到。第三,ChromaDB的embedding_function参数可以不传,直接传已经算好的向量,但我建议让它内部调用Ollama,省事且统一。
4.3 检索策略与效果调优
检索的时候,最基础的方法是直接取相似度最高的几个结果。但直接这样做效果一般,原因在于用户问题往往是口语化的,和知识库里的书面表达存在措辞差异。我用的优化策略是"查询改写+多路召回+倒排权重"。
查询改写:在交给embedding前,先用一个轻量prompt把用户的说法翻译成更标准的检索式。比如用户说"我上个月写的那个关于假期计划的日记",改写成"假期计划 日记 上个月",能显著提升召回效果。
多路召回:先用embedding向量检索取前20个结果,再用关键词重叠度(比如BM25或简单的Jaccard相似度)取前10个结果,然后去重合并,重排序后取前5个作为上下文。
这个"重排序"的细节很多人会忽略。向量相似度高的块不一定是最相关的,因为在语义空间里,"最近"有时候是"话题接近"而不是"事实相关"。我的做法是把检索回来的结果拼接一个问题,让本地小模型打分,但这个方案对7B模型来说耗时太长。折中方案是把向量相似度和关键词重合度做个加权平均:
final_score = 0.7 * vector_score + 0.3 * keyword_score实测这招对"细节回忆型"问题的提升非常明显。
4.4 多格式文档的入库技巧
除了纯文本,你很可能有PDF、Word、图片里的文字。我踩过的坑是pdfminer这类纯文本解析工具对扫描版PDF完全没辙。后来用了MinerU来做文档解析,它能输出结构化的Markdown,对排版、表格、图片中的文字都有很好的处理效果。MinerU本身是开源的,支持本地部署,正好契合我们的隐私要求。
处理图片里的文字,可以用OCR工具,比如PaddleOCR,它对中文印刷体和手写体的识别率都不错。日记如果是以图片形式存的(比如手机拍照),先OCR再入库。
注意:OCR出来的文本错误率不低,直接入库会污染检索结果。我的做法是OCR之后做一遍轻量校对脚本,至少把明显的错误字符(比如把"我"识别成"找")替换归类。这个步骤耗费的精力不少,但是值得。
5. 情感理解模块与人格设定
5.1 情感识别不用大模型
情感识别模块我选择了基于情感词典和句式的规则引擎。别觉得这方法土,它快、稳、可解释。每一条用户输入,先做情感打分:
- 正负情感词(比如"开心"、"烦"、"焦虑"、"舒服")对应不同权重词库
- 程度副词("非常"、"有点"、"太")作为加权系数
- 句式判断(反问句和感叹句的情感表达强度更高)
举个例子:"我真的好累,什么都提不起兴趣,但生活还得继续。"这个句子里的"累"、"提不起兴趣"是负向词,但"生活还得继续"表达了一种隐性的积极坚持,整体情绪标签可以打上"疲惫但坚韧"。这种混合情感识别用规则很不好搞,后来我又调了一次:先粗分类成正向、负向、中性三类,再对负向做细分(厌倦、焦虑、悲伤、愤怒、孤独)。
5.2 Prompt工程:让大模型"感知"情绪状态
情感标签不是直接裹进用户原话里的,而是作为一个额外的上下文块传进最后的提示词。我的Prompt结构大致是:
系统提示: 你是"小暖",一个温柔但不油腻的私人情感陪伴助手。 你的知识来源是用户的私人记忆库,请基于检索到的内容作答。 当前检测到用户的情绪倾向:【疲惫且焦虑】。 当用户情绪低落时,你的回应应当以倾听和共情为主, 减少说教和提问,避免轻飘飘的鼓励。 检索到的记忆片段: {context} 对话历史: {history} 用户当前消息:{user_input}这里有个关键经验:不要把"情绪标签"写成一个命令式的硬指令(比如"用户很伤心,你必须安慰他"),而是作为"状态描述"交给模型。因为语言模型对"状态感知"模式的遵从度远高于"硬性指令"模式,前者是共情,后者是执行任务,生成出来的语气差异非常大。我对比过同一个输入在两种写法下的输出,状态描述模式明显更自然。
5.3 记忆封装的层次结构
RAG提供的是长时记忆(知识库),对话本身还有短期记忆(多轮上下文)。我设计了一套简单的记忆层次:
- 短期记忆:最近10轮对话原文,直接拼在prompt里
- 长期记忆:RAG检索得到的相关文本块
- 核心人格:系统提示词中固定的角色描述
这三个层次的优先级和权重需要反复调试。短期记忆太长了会挤压RAG的检索结果空间,我后来做了个简单策略——如果最近5轮对话里已经提到了某个话题,那么RAG检索到的相关文本块可以少放一些,因为短期记忆已经覆盖了一部分。这个策略节省了不少上下文空间。
5.4 多角色扩展
这套情感助手的架构其实是支持多角色的,我开发的时候就顺手做了个配置。比如你想让它以"知心姐姐"、"老同学"、"心理教练"等不同身份出现,只需要切换系统提示词和检索时的过滤条件。从同一个知识库里,你可以用不同的"人格视角"去提取信息。比如"知心姐姐"模式检索时更看重日记里的情绪词汇,"老同学"模式更看重共同经历的时间点。这个扩展思路如果是做产品的话很有价值,一套数据支撑多种人格。
6. 完整流程的代码实现
6.1 主流程:从检索到生成
先把核心的对话处理函数写出来,这个函数是系统的中枢,每次用户输入都会走到这里。
import ollama def chat_with_assistant(user_input, history): # 第1步:情感识别 emotion = detect_emotion(user_input) # 第2步:查询改写 refined_query = rewrite_query(user_input) # 第3步:向量检索 vector_results = collection.query( query_texts=[refined_query], n_results=20 ) # 第4步:关键词召回 keyword_results = keyword_search(refined_query) # 第5步:合并、去重、重排序 final_chunks = merge_and_rerank(vector_results, keyword_results, top_n=5) # 第6步:拼装Prompt context = "\n\n".join(final_chunks) messages = [ {"role": "system", "content": build_system_prompt(emotion)}, {"role": "user", "content": f"记忆片段:\n{context}\n\n我的消息:{user_input}"} ] # 第7步:调用本地大模型生成 response = ollama.chat( model="qwen2.5:7b-instruct-q4_K_M", messages=messages, options={"temperature": 0.7, "num_ctx": 8192} ) return response["message"]["content"]这个流程顺序是固定的,但每一步都有值得优化的空间,后面我会展开讲。注意我把对话历史也拼进了messages里,但实际测试发现,如果历史太长,生成速度会明显变慢。目前实测8轮对话以内是流畅的,超过之后需要用滑动窗口只保留最近几轮。
6.2 查询改写与关键词检索的实现
查询改写不一定要用独立的模型,我直接用同一个Ollama模型,但加上一个专门的改写指令。因为改写本身不需要太多生成,开销可以接受:
def rewrite_query(user_input): prompt = f"请把用户的问题改写成更适合检索的关键词组合,要求简洁。\n用户问题:{user_input}\n改写结果:" resp = ollama.generate(model="qwen2.5:3b", prompt=prompt, options={"temperature": 0}) return resp["response"].strip()关键词检索我用了一个简单但有效的方案:jieba分词之后,统计每个词的TF-IDF权重,再将词汇与向量库里的文本做重合度匹配。对,这其实就是BM25的思想,但自己写的好处是可以控制细节、方便调试:
def keyword_search(query, top_k=10): query_tokens = set(jieba.lcut(query)) scores = [] # 从ChromaDB里取所有文档,计算重叠度 all_docs = collection.get() for i, doc in enumerate(all_docs["documents"]): doc_tokens = set(jieba.lcut(doc)) overlap = len(query_tokens & doc_tokens) / max(1, len(query_tokens)) scores.append((all_docs["ids"][i], doc, overlap)) scores.sort(key=lambda x: x[2], reverse=True) return scores[:top_k]这个方案的优点是零额外服务依赖,缺点是数据量大了之后全量扫描很慢。好在个人知识库一般几千个切片,全量扫描也就几十毫秒,完全能接受。
6.3 前端交互与入口封装
后端流程跑通了,还要有一个能交互的界面。我试过几套方案:
- 命令行CLI:最省事,适合自己用
- Gradio Web界面:给不太懂命令行的人用,支持聊天框+历史记录
- 微信公众号接入:给几个朋友用,但配置麻烦
最终我选了Gradio,因为它是纯Python的,而且界面足够好看。核心代码:
import gradio as gr with gr.Blocks(title="本地情感助手") as demo: chatbot = gr.Chatbot(height=450) msg = gr.Textbox(label="说点什么吧", placeholder="跟我说说你的近况...") clear = gr.Button("清空对话") def respond(user_msg, chat_history): if not user_msg.strip(): return "", chat_history reply = chat_with_assistant(user_msg, chat_history) chat_history.append((user_msg, reply)) return "", chat_history msg.submit(respond, [msg, chatbot], [msg, chatbot]) clear.click(lambda: None, None, chatbot, queue=False) demo.launch(server_name="127.0.0.1", server_port=7860)Gradio默认启动在7860端口,绑定127.0.0.1就只允许本机访问,数据不出电脑。如果想从局域网的其他设备访问,改一下server_name就行,但这样就要注意局域网安全了。
6.4 后台服务与定时任务
情感助手最好是常驻的,我写了一个service.py统一管理所有组件:Ollama进程、ChromaDB连接、Gradio界面。然后注册成系统服务,开机自启。这样不用每次手动敲命令。
Windows下用计划任务,macOS/Linux下用systemd。核心就是一条启动命令:
python service.py写日志的环节也别跳过。正常对话日志、检索召回日志、错误日志分开写,后面做问题排查时全靠这些日志。我吃过一次亏,回复内容质量差,但没留日志,根本分不清是检索没召回还是模型生成出了问题。
7. 常见问题与排查实录
7.1 问题速查表
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| 回答完全脱离知识库 | 检索到的相关块context为空 | 检查embedding模型是否正确加载,检查向量库里是否有数据 |
| 回答沾边但不具体 | chunk太大或overlap不足 | 调整chunk_size到200~300,overlap到50以上 |
| 回答像AI套话 | 系统提示词设定不够具体 | 强化人格设定,加入情绪感知描述 |
| 回答卡顿严重 | 上下文窗口过长或量化程度不够 | 限制历史轮数,使用更小的模型 |
| 检索总是漏掉关键信息 | 查询改写没生效 | 检查rewrite_query的输出,看关键词是否保留了核心实体词 |
| 启动报错连不上ChromaDB | 路径冲突或数据损坏 | 换一个PersistentClient路径,旧的vector store另存备份 |
| 中文乱码 | 文件编码问题 | 入库前统一转成UTF-8,清洗不可见字符 |
7.2 效果调优实验记录
我做了一组对比实验,记录几个让我印象深刻的结论。
第一个是chunk_size从500改成250之后,回答的相关性评分明显上升。500字的块意味着一个块里可能包含了三个话题,检索命中时会夹带大量无关内容。改成250字之后,每个块更聚焦,召回的内容也更精准,代价是检索结果的碎片感更强,两块信息之间的连接有时会断掉。最终用overlap 50缓解了这个断裂问题。
第二个是temperature参数的调整。情感陪伴场景下,temperature调得太低(比如0.1),回答会非常稳妥但缺乏人情味,像是AI在背话术;调得太高(比如1.0),回答又容易跑偏,甚至开始教育用户。我最后稳定在0.7到0.8之间。这个区间里模型能写出一些意外的表达,但又不会神游太虚。
第三个是对话历史的轮数。我发现超过10轮之后,模型的注意力会被历史对话稀释,越早的对话被遗忘得越干净。而且,对话历史越长,单次响应时间就越久。后来做了一个综合策略:超过8轮后,只保留最后6轮,其余压缩成一段摘要,用另一个轻量模型把前面对话的内容总结成3句话放进上下文。这样既保留了长程记忆,又不至于挤占注意力。
7.3 数据隐私的自我保护
本地部署最大的价值就是隐私,但这里有个容易被忽略的隐患:如果Gradio的服务端口绑定了所有网卡接口(0.0.0.0),同一局域网内的其他设备是可以访问到的。我在实际使用时只绑定127.0.0.1,并且加了简单的token认证。虽然Gradio本身没有内置用户系统,但可以在前端加一个密码校验。别小看这个,日记这种数据一旦被别人看到,后果是灾难性的。
另一个隐私隐患是日志文件。调试是好事,但日志里最好不要记录完整的对话原文,只记录情绪标签、检索返回的文档ID、生成耗时这些元信息就足够了。万一日志文件被翻到,也不至于泄露私人内容。
7.4 Ollama的调优参数
Ollama有一些隐藏参数值得调一下。
# 设置并发数为1,减少内存争抢 OLLAMA_NUM_PARALLEL=1 # 关闭keep_alive,模型常驻内存,加快响应速度 OLLAMA_KEEP_ALIVE=30m # 显式指定上下文长度,避免默认2048的坑 OLLAMA_CONTEXT_LENGTH=8192特别是OLLAMA_CONTEXT_LENGTH,这个参数的影响我前面反复提过。如果不设置,很多模型默认只使用2048的上下文窗口,检索回来的内容稍微长一点,后段就会被截断,回复引用的信息就残缺了。设置到8192之后,即使chunk接起来比较长,模型也能完整读完。
7.5 针对低配机器的资源优化
如果你的机器只有8GB内存,我推荐一个降级方案。模型换成Qwen2.5-3B Q4,embedding用更轻量的all-MiniLM-L6-v2(英文)或者bge-small-zh(中文)。这两个embedding模型资源占用极小,在CPU上都能跑到不错的性能。然后关闭对话历史摘要模块,直接限制历史轮数在4轮以内。这样整体内存占用可以压到5GB以内,跑起来还是流畅的。
另外建议用ollama ps监控模型的内存占用,如果发现模型被频繁卸载重载(表现为响应时间忽高忽低),大概率是内存不足触发OLLAMA的swap策略,这时候加OLLAMA_KEEP_ALIVE的值,或者干脆换更小的模型。
8. 扩展想法与后续计划
做好了基础版之后,有几个方向我觉得值得继续深挖。
第一个方向是让RAG助手具备"时间感知"。目前的情感助手只能被动检索历史数据,如果加入日记里记录的时间轴,它就能主动说出"去年的这个时候你好像也在焦虑实习的事情"。这种时间上下文能让对话的共情感上一个台阶。
第二个方向是增加多模态能力。目前RAG知识库只能处理文本,但它也能处理图片和音频。热词里提到的"rag知识库能存储图片嘛",答案是"可以,但要走对路子"。图片里的信息需要先经过图像理解模型转成文字描述,再进入向量库,而不是直接把图片塞进去。本地跑一个轻量的视觉模型也可以做到,这个我在后续的版本里计划加进去。
第三个方向是做一个更聪明的记忆遗忘机制。现在知识库是"收进去什么就永远在",但人的记忆是会淡忘的。如果给每条记忆加上时间衰减权重,旧信息在检索时权重降低,新信息权重提高,这个小助手会更像人——既记得你的过去,又不至于被陈年旧事缠绕。
第四个方向是重构情绪识别模块。规则引擎的准确率在现代大模型面前确实不够看,但我选择它是因为速度和可解释性。后续可以在规则引擎预筛、大模型复核的方式上做混合识别,既保证速度,又提升准确率。
最后想跟读者朋友们说的是,这个项目最大的乐趣不在于把技术栈跑通,而在于调整它、塑造它的过程。当我第一次让这个助手基于我过去写的日记回应我关于"累"的抱怨,而它没有给出标准化的鸡汤,而是说出了一句"你上次说累的时候,后来请了三天假去爬山,回来状态好多了"——那一刻我是真的感受到了技术之外的某种东西。希望你们也在这个项目里找到同样的体验。