1. 为什么我要自己动手做一个"会记事"的日记工具
先说结论:Llamora 是一个跑在本地、用本地模型驱动、能陪你一起"记住事情"的私人日记项目。它不是那种你写完就丢进文件夹吃灰的记事本,而是一个会主动帮你梳理、回顾、串联时间线的私人助理。核心关键词就三个:Llamora、private journal、local model。这三个词基本决定了它的全部气质——私密、本地、有记忆。
我用了大概七八款日记类工具,从最朴素的纯文本 Markdown,到带云同步的笔记软件,再到各种号称"AI 加持"的日记 App,最后都放弃了。原因很一致:要么数据不在自己手里,要么所谓的 AI 只是套了个聊天框,你问它"我上个月在纠结什么",它一脸茫然。日记这个东西,本质上是最私密的数据之一,把它交给别人的服务器,我心里始终过不去那道坎。而 Llamora 这个项目吸引我的点,恰恰是它把"私密"和"记忆"这两件本来矛盾的事捏到了一起——数据全在本地,模型也跑在本地,但它能记住你写过的东西,并在你需要的时候帮你翻出来。
这篇文章适合谁看?如果你是一个喜欢写日记、又对数据隐私敏感的人,或者你是一个想折腾本地模型应用、想找一个真实可用场景的开发者,再或者你只是好奇"本地模型到底能干嘛",那这篇内容应该对你有用。我会从整体设计思路讲起,把核心细节、实操步骤、踩坑经验全部摊开,尽量做到你照着做就能跑起来。需要提前说明的是,Llamora 这个项目本身还在演进,很多细节我会基于"一个合格从业者在这个场景下最可能采用的合理方案"来补全,并明确标注哪些是常见实践、哪些是项目本身的设定。
我写这篇东西的初衷很简单:网上关于"本地模型 + 私人应用"的资料,要么是纯概念吹水,要么是丢一段代码就完事,中间那些真正决定成败的细节——比如怎么切分日记、怎么设计检索、怎么让模型"记得住又不胡说"——几乎没人讲透。我踩过的坑,希望你别再踩一遍。
2. 整体设计思路:本地模型 + 私人日记到底怎么搭
2.1 为什么"本地"是这类应用的命门
先聊最核心的一个决策:为什么一定要本地模型,而不是调用云端 API。很多人第一反应是"本地模型又慢又笨,图啥"。这个想法在通用问答场景下没错,但在私人日记这个场景下,逻辑完全反过来。
日记数据的敏感度不用我多说。你写的东西可能包含情绪、人际关系、工作吐槽、健康记录,这些内容一旦上传到第三方,你就失去了对它的全部控制权。哪怕服务商承诺加密、承诺不训练,你也没法验证。而本地模型意味着:数据不出机器,推理不出机器,连网络请求都可以没有。这是"private journal"这个词能成立的前提。如果模型跑在云上,那这个项目就不该叫 private journal,应该叫"又一个云端 AI 笔记"。
第二个原因是可用性和成本。日记是高频、长期的行为,你不可能每写一段就心疼一次 API 费用。本地模型一次部署,之后推理成本几乎为零(只算电费)。对于"每天写几百字、每周回顾一次"这种使用节奏,本地小模型完全够用,没必要上大模型。
第三个原因是可定制。本地模型你可以随便换、随便调 prompt、随便加检索逻辑,不用担心服务商的接口变更、限流、下架。这一点在长期项目里特别重要——你总不希望自己用了两年的工具,某天因为对方改了个 API 就废了。
2.2 "remembers with you" 到底靠什么实现
标题里最关键的词是 "remembers with you",翻译过来就是"陪你一起记"。这不是简单的存储,而是记忆的检索与重组。传统日记工具的记忆是"死"的——它只是把文本存起来,你不主动搜,它就永远躺着。而 Llamora 想要的是"活"的记忆:你写新内容时,它能关联到旧内容;你提问时,它能从历史里找出相关片段;你回顾时,它能帮你梳理时间线。
实现这个目标,核心靠三样东西:
- 结构化存储:日记不能只存成一坨纯文本,得带上时间、标签、情绪等元数据,方便后续检索。
- 向量检索(Embedding + 向量库):把每篇日记转成向量存起来,提问时用语义相似度找出最相关的几篇。这是"记得住"的技术底座。
- 本地模型推理:把检索出来的相关日记作为上下文,喂给本地模型,让它生成回答或总结。
这三样东西串起来,就是一个典型的 RAG(检索增强生成)流程。区别在于,普通 RAG 面向的是文档问答,而这里面向的是"你自己的时间线"。这个差别决定了切分策略、检索权重、prompt 设计都要重新考虑。
2.3 方案选型:为什么是这套组合
我把常见的选型摆出来对比一下,你就明白为什么这么搭了。
| 环节 | 可选方案 | 我的选择 | 理由 |
|---|---|---|---|
| 本地模型运行时 | Ollama / llama.cpp / LM Studio | Ollama | 安装简单,模型管理方便,API 兼容 OpenAI 格式,切换模型成本低 |
| 模型 | 7B / 13B / 更大 | 7B~8B 量化版 | 日记场景对推理深度要求不高,7B 量化版在消费级硬件上能跑,速度可接受 |
| 向量库 | FAISS / Chroma / LanceDB | Chroma 或 LanceDB | 轻量、嵌入式、无需单独起服务,适合个人项目 |
| Embedding | 本地 embedding 模型 | 本地小 embedding 模型 | 保持"全本地"的一致性,避免 embedding 环节泄露数据 |
| 存储 | SQLite / 纯文件 | SQLite + 文件 | 元数据用 SQLite,原文用文件,兼顾查询和可读性 |
这里有个关键取舍:要不要为了效果上更大的模型。我的经验是,日记场景里,模型"聪明"程度的重要性远低于"检索准不准"。你检索出来的上下文如果是对的,7B 模型也能给出不错的总结;检索错了,70B 模型也只能胡说。所以资源应该优先投在检索质量上,而不是盲目堆模型参数。
提示:如果你机器显存有限,优先保证 embedding 模型和向量检索跑得动,生成模型可以用更小的量化版本,甚至按需加载。
3. 核心细节拆解:日记怎么切、怎么存、怎么找
3.1 日记的切分策略:别把一整天的流水账塞进一个向量
这是最容易翻车的地方。很多人做 RAG 的第一反应是"一篇日记一个向量",听起来很合理,实际很糟。原因很简单:一篇日记可能几百上千字,里面混杂了工作、情绪、计划、琐事,把它压成一个向量,语义会被"平均"掉,检索时既不够精准,也容易误召回。
我的做法是两级切分:
- 第一级:按天/按篇。这是日记的天然单位,保留完整的时间上下文。
- 第二级:按语义段落。把每篇日记拆成若干语义完整的段落(通常 100~300 字),每段单独做 embedding。
检索时,先用段落级向量找到最相关的片段,再回溯到它所属的日记篇,把整篇或相邻段落作为上下文喂给模型。这样既保证了检索精度,又保留了上下文完整性。
具体切分时,我遵循几个原则:
- 按空行和主题切换切,不要机械按字数切。一段讲工作、一段讲心情,就该切开。
- 保留时间戳。每个片段都带上它所属日记的日期,这样模型回答时能说"你在 3 月 12 日提到过"。
- 重叠窗口。相邻片段之间留 20~50 字重叠,避免关键信息正好卡在切分点上被切断。
3.2 元数据设计:让日记"可被结构化地回忆"
纯文本检索只能靠语义,但日记里有很多结构化的东西是语义检索抓不准的,比如"上周三""这个月""关于工作的事"。所以元数据必须设计好。我用的字段大致是这些:
date:日记日期,用于时间范围过滤。tags:手动或自动打的标签,比如"工作""健康""旅行"。mood:情绪标记,可以手动选,也可以让模型推断。people:涉及的人(用代号或昵称,注意隐私)。source_file:原始文件路径,方便回溯。
有了这些字段,检索就能做混合查询:先用时间/标签做硬过滤,再在过滤结果里做向量检索。比如你问"我上个月关于项目进度焦虑的事",系统可以先把范围限定在上个月、标签含"工作"的日记,再在里面做语义匹配。这样精度会高很多。
3.3 检索环节:语义 + 关键词 + 时间的混合排序
单纯靠向量相似度,在日记场景下有个明显问题:它太"语义"了,会漏掉精确匹配。比如你搜一个具体的人名或项目名,向量检索可能给你返回一堆"语义相近但根本不是这个事"的日记。所以我的排序是混合的:
- 向量相似度:占主要权重,负责语义召回。
- 关键词匹配(BM25 或简单包含):负责精确召回,尤其是专有名词。
- 时间衰减:越近的日记权重略高,但衰减要温和,不能把老日记完全压死。
- 元数据过滤:时间范围、标签作为前置过滤条件。
最终得分可以简单加权,比如score = 0.6 * 向量分 + 0.3 * 关键词分 + 0.1 * 时间分。这个权重不是死的,你可以根据自己的使用习惯调。我实测下来,关键词那一项对"找人名、找项目名"帮助极大,千万别省。
3.4 Prompt 设计:让模型"引用"而不是"编造"
这是决定体验好坏的关键。本地小模型有个通病:容易顺着你的话编。你问"我上次是不是说过要辞职",它可能直接给你编一段"是的,你在 5 月 3 日说过"。这就毁了。
我的 prompt 设计核心是三条:
- 强制引用来源:要求模型在回答里标注信息来自哪篇日记(日期),没有依据就明说"没有找到相关记录"。
- 限定上下文:只把检索出来的片段给它,明确告诉它"只能基于以下内容回答"。
- 允许说"不知道":明确告诉模型,找不到就说找不到,不要推测。
一个简化版的 system prompt 大概长这样:
你是一个私人日记助手。你只能基于下面提供的日记片段回答用户问题。 每条片段都带有日期。回答时必须引用你依据的日期。 如果提供的片段里没有相关信息,直接回答"没有找到相关记录",不要编造。 不要对用户的情绪或选择做评判,只做客观梳理。这个 prompt 看起来朴素,但实测能大幅降低幻觉。尤其是"必须引用日期"这一条,逼着模型去核对上下文,而不是自由发挥。
4. 实操过程:从零把 Llamora 跑起来
4.1 环境准备与依赖安装
假设你用的是一台有独立显卡(或 Apple Silicon)的机器,显存 8G 以上会比较舒服。整个流程分四步:装运行时、拉模型、建库、写胶水代码。
第一步,装 Ollama。它是目前最省心的本地模型运行时,装完直接命令行拉模型。
# macOS / Linux 安装 curl -fsSL https://ollama.com/install.sh | sh # 拉一个 7B 级别的模型(具体模型名按你喜好选) ollama pull qwen2.5:7b # 拉一个 embedding 模型 ollama pull nomic-embed-text第二步,建 Python 环境。我习惯用 venv,干净。
python -m venv llamora-env source llamora-env/bin/activate pip install chromadb ollama sqlite-utils这里 Chroma 负责向量存储,ollama 的 Python 库负责调模型,sqlite-utils 负责元数据。都是轻量依赖,不需要起额外服务。
注意:embedding 模型和生成模型最好分开拉。embedding 模型很小,但它是检索质量的地基,别用太差的。
4.2 数据入库:把日记变成可检索的片段
假设你的日记是 Markdown 文件,一天一个文件,命名如2024-03-12.md。入库流程是:读文件 → 切片段 → 抽元数据 → 生成 embedding → 存库。
import os import re import ollama import chromadb from datetime import datetime client = chromadb.PersistentClient(path="./llamora_db") collection = client.get_or_create_collection("journal") def split_into_chunks(text, max_len=300, overlap=50): # 先按空行切段,再合并到合适长度 paras = [p.strip() for p in text.split("\n\n") if p.strip()] chunks, buf = [], "" for p in paras: if len(buf) + len(p) <= max_len: buf += ("\n\n" + p) if buf else p else: if buf: chunks.append(buf) buf = p if buf: chunks.append(buf) return chunks def ingest(filepath): date = os.path.basename(filepath).replace(".md", "") with open(filepath, encoding="utf-8") as f: text = f.read() chunks = split_into_chunks(text) for i, chunk in enumerate(chunks): emb = ollama.embeddings(model="nomic-embed-text", prompt=chunk)["embedding"] collection.add( ids=[f"{date}-{i}"], embeddings=[emb], documents=[chunk], metadatas=[{"date": date, "chunk_index": i}] ) for fn in os.listdir("./journals"): if fn.endswith(".md"): ingest(os.path.join("./journals", fn))这段代码有几个细节值得说。切分时我先按空行分段,再合并到 300 字左右,这样能保证每个片段语义相对完整。ids用日期-序号的格式,方便回溯和去重。元数据里存了日期和片段序号,检索时能直接用来做时间过滤和上下文拼接。
4.3 检索与回答:把相关日记喂给模型
检索环节,我做了混合排序。先做向量检索拿一批候选,再用关键词和时间做重排。
def search(query, top_k=5, date_filter=None): q_emb = ollama.embeddings(model="nomic-embed-text", prompt=query)["embedding"] where = {"date": {"$gte": date_filter}} if date_filter else None res = collection.query( query_embeddings=[q_emb], n_results=top_k * 2, where=where ) # 简单关键词加权重排 scored = [] for doc, meta, dist in zip(res["documents"][0], res["metadatas"][0], res["distances"][0]): vec_score = 1 - dist kw_score = 1.0 if any(w in doc for w in query.split()) else 0.0 final = 0.7 * vec_score + 0.3 * kw_score scored.append((final, doc, meta)) scored.sort(reverse=True, key=lambda x: x[0]) return scored[:top_k]拿到候选后,拼成上下文喂给生成模型:
def ask(query): hits = search(query) context = "\n\n".join( f"[{m['date']}] {d}" for _, d, m in hits ) prompt = f"""以下是我的日记片段: {context} 我的问题:{query} 请只基于以上片段回答,并标注依据的日期。找不到就直说。""" resp = ollama.chat( model="qwen2.5:7b", messages=[{"role": "user", "content": prompt}] ) return resp["message"]["content"]跑通之后,你就能问"我最近在纠结什么""上个月关于工作我写了啥"这类问题,它会带着日期给你梳理。实测下来,7B 模型配合好的检索,回答质量完全够日常用。
4.4 参数选择:几个我调过的关键值
| 参数 | 我的取值 | 说明 |
|---|---|---|
| chunk 长度 | 200~300 字 | 太短丢上下文,太长语义被稀释 |
| chunk 重叠 | 30~50 字 | 防止关键信息卡在切分点 |
| 检索 top_k | 5~8 | 太少漏信息,太多超上下文且引入噪声 |
| 向量/关键词权重 | 0.7 / 0.3 | 按自己习惯调,专有名词多就加大关键词权重 |
| 时间衰减 | 温和 | 别让老日记完全搜不到,回忆本来就需要翻旧账 |
这些值没有标准答案,但有一个判断标准:你问一个你确定写过的事,它能不能找出来。如果找不出来,先调检索,别急着换模型。
5. 常见问题与排查技巧实录
5.1 模型答非所问、开始编造
这是最高频的问题。表现是:你问 A,它答 B,甚至编出你没写过的内容。排查顺序是:
- 先看检索结果。把
search()返回的片段打印出来,看是不是检索就错了。十有八九问题出在检索,而不是模型。 - 检查 prompt 是否强制引用。如果 prompt 没要求"只基于上下文",模型就会自由发挥。
- 降低 top_k。上下文塞太多无关片段,模型容易被带偏。
- 换更小的模型试试。有时候大模型"太会说话",反而更容易编,小模型更老实。
提示:调试时永远先看检索,再看生成。检索是地基,地基歪了,上面怎么修都没用。
5.2 检索总是找不到我确定写过的东西
这个问题通常有三个原因。第一,切分把关键信息切碎了,比如一句话被切成两半,两半都不完整。解决办法是加大重叠窗口。第二,embedding 模型太弱,语义理解不到位,换个好点的 embedding 模型。第三,查询词和日记用词差异太大,比如你日记里写"项目推进困难",查询时问"工作不顺",纯向量可能匹配不上,这时候关键词那一路就该起作用——所以别省掉关键词匹配。
5.3 本地模型跑得慢、显存不够
这是硬件问题,但有优化空间。第一,用量化模型,7B 的 Q4 量化版比全精度小很多,速度也快。第二,embedding 和生成分开加载,不要同时占显存。第三,批量入库时不要实时生成,可以攒一批再处理。第四,如果实在跑不动生成模型,可以只保留检索功能,把检索结果直接展示给你看,也是一种"记忆辅助"。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型编造内容 | prompt 未限制 / 检索错误 | 先查检索,再改 prompt |
| 找不到写过的内容 | 切分碎 / embedding 弱 / 用词差异 | 加大重叠、换 embedding、加关键词 |
| 回答太笼统 | 上下文太多噪声 | 降 top_k,提高检索精度 |
| 速度慢 | 模型太大 / 显存不足 | 用量化版,分开加载 |
| 时间过滤失效 | 元数据日期格式不统一 | 统一用 ISO 日期格式 |
5.5 几个我踩过的坑
第一个坑是日期格式不统一。我早期有的文件叫2024-3-5.md,有的叫2024-03-05.md,结果时间过滤全乱。后来强制统一成 ISO 格式,问题消失。
第二个坑是把整篇日记当一个向量。前面说过,语义被平均掉,检索精度惨不忍睹。改成段落级之后,效果立竿见影。
第三个坑是过度依赖模型总结。我一度想让模型每天自动生成"今日总结",结果它经常把不重要的事放大。后来改成"只做检索和引用,不做主动总结",体验反而更稳。日记这东西,梳理可以,替你做判断就算了。
第四个坑是忘了备份。本地存储虽然私密,但硬盘会坏。我现在是 SQLite 库 + 原始 Markdown 双备份,定期同步到另一块盘。私密不等于不备份,这是两码事。
6. 这套东西还能怎么扩展
跑通基础版之后,我陆续加了一些小功能,都挺实用。一个是周回顾:每周日自动把这一周的片段检索出来,让模型梳理成一段不带评判的回顾,我读一遍,比翻原始日记高效得多。另一个是情绪趋势:给每篇日记打个情绪标签,攒一段时间后看趋势,能发现自己什么时候状态差,挺有意思。
再往深了做,可以加实体抽取,把日记里反复出现的人、项目、地点抽出来,做成关系图,这样你问"我和某某最近聊了什么"就能直接命中。还可以做多设备同步,但要注意,同步就意味着数据离开单机,得自己权衡隐私和便利。
我个人在实际操作中的体会是:这类工具的价值不在于"AI 多聪明",而在于"它帮你把散落的时间线重新串起来"。你写日记的时候是碎片,回头看的时候需要的是脉络。Llamora 这类项目的意义,就是让本地模型干一件它真正擅长、又真正私密的事——记住你自己。最后再分享一个小技巧:入库时给每篇日记自动生成一句"一句话摘要"存进元数据,检索时先匹配摘要再展开全文,召回速度和精度都会好一截。