背景
“也祝日志”是我独立完成并发布的一款微信小程序,目前已经可以在微信中搜索使用。
我最初想解决的问题并不只是“找一个地方记日记”。普通笔记能够保存内容,却很难在记录积累之后回答这些问题:最近一段时间我在关注什么?以前遇到过类似的问题吗?当时是怎么处理的?零散记录之间是否存在持续出现的主题?
因此,我把它设计成一个安静的个人记忆空间:用户先记录真实经历,系统再对记录进行整理、检索和回顾,并允许用户与过去的记录进行对话。项目最终形成了微信小程序、FastAPI、SQLite、LLM、Embedding 与 Dify 组成的完整应用链路。
这篇文章不只展示页面,也会说明当前版本真正实现了什么、关键技术是怎样落地的,以及它仍然有哪些边界。
产品功能
当前小程序主要包含五个底部导航入口:
时间线:按照年份、月份和记录节点浏览内容,支持折叠、编辑和删除。



回顾:查看稳定的基础统计,也可以主动生成并保存 AI 回顾。



记录:以较轻的书写界面添加标题、正文和标签。

问问:基于个人记录进行多轮问答,回答可以附带相关记录并跳转回时间线。



我的:管理头像、导出 CSV、查看 AI 使用状态和配置自己的 API Key。

此外还有一个独立的“关注”页面,用于聚合 Hacker News、GitHub 热门仓库搜索结果和 RSS/Atom 信息源,经 Dify 工作流整理后展示中文标题、摘要、分类和主题。
整体架构
项目采用前后端分离结构:
1 | 微信小程序 |
前端使用微信小程序原生 WXML、WXSS 和 JavaScript,没有引入大型 UI 框架。后端使用 FastAPI 提供接口,SQLite 负责持久化,定时任务通过 systemd timer 运行。线上服务由 Uvicorn 承载应用进程,并通过 HTTPS 域名供小程序访问。
从一条记录到可检索记忆
1. 保存原始记录
用户提交标题、正文、分类和标签后,小程序调用 POST /api/records。后端首先校验正文,再读取该用户的 AI 配置并调用 analyze_record。
如果用户已经配置聊天 API Key,模型会根据正文生成结构化的标题、摘要、分类和标签;如果没有配置,或远程调用失败,则使用本地关键词规则和文本截断生成兜底结果。用户主动填写的标题和分类优先保留,用户标签与 AI 标签会合并、去重并限制数量。
这一步的意义是让原始正文多出一组高密度信息:
1 | 正文:今天部署小程序时接口一直报错,最后发现是服务器地址配置错了 |
最终内容写入 records 表,成为关键词、标签、时间和语义检索共同使用的基础数据。
2. 构建 Embedding 文本
项目没有只向量化正文,而是通过 build_embedding_text 将多个字段拼接为一段带语义提示的文本:
1 | 标题:小程序接口调试 |
这样做可以同时利用标题和摘要的主题信息、正文的细节以及标签的显式语义。
3. 生成并保存向量
embed_texts 优先调用服务器统一配置的 Embedding API,当前默认兼容 Voyage 的 /embeddings 接口。成功后,向量和模型名称写入 record_embeddings 表,并通过 record_id 与原记录建立一对一关系。
如果 Embedding API 未配置、网络异常或返回格式不合法,系统不会阻止用户保存记录,而是降级为本地 local-hash-v1:先对中英文词元做特征哈希,再生成并归一化 192 维向量。
这个本地方案主要用于保证可用性,它不是本地神经网络 Embedding 模型,语义能力也不能与 Voyage 等专业模型等同。
混合 RAG:关键词、时间、标签与向量一起工作
用户在“问问”中发送问题后,后端会读取该用户最多 500 条普通记录,并关联已经保存的向量。缺失向量的旧记录会在问答时补齐,然后进入混合检索流程。
1. 规则式意图识别
_classify 根据当前问题和上一轮用户消息,将请求分成四类:
summary:总结、回顾、最近在关注什么。lookup:寻找或询问某条记录。emotion:焦虑、困难、报错、卡住等需要先回应感受的表达。chat:一般对话。
这里使用的是可解释的关键词规则,不是独立的 LLM 意图分类器。它的优点是响应快、没有额外调用成本;不足是对复杂表达和歧义的判断能力有限。
2. 关键词与标签评分
_terms 从问题中提取有效词项并过滤停用词,_score 再对不同字段赋予不同权重:
- 标签命中的权重最高;
- 标题高于摘要,摘要高于正文;
- 分类匹配会增加分数;
- 时间范围命中会增加分数;
- 越新的记录还会获得少量近期加权。
这样可以避免把所有文本字段都当成同等重要。例如问题中明确出现某个标签时,该标签比正文里偶然出现同一个词更有意义。
3. 时间窗口
_time_window 识别“今天”“昨天”“最近一周”“最近一个月”等表达,并转换为明确的起止时间。对于“最近在关注什么”这类总结请求,检索还会按一周、一个月和最近一条记录逐级选择可用范围,而不是在没有本周记录时直接返回空结果。
4. 向量相似度
系统使用 _cos 在 Python 中计算问题向量和记录向量的余弦相似度。相似度达到阈值的记录进入语义候选,高相似度结果会显示“语义:含义接近”的命中原因。
当前项目没有部署向量数据库。Embedding 以 JSON 文本保存在 SQLite 中,查询时读取到 Python 逐条计算,因此更适合个人记录这种中小规模场景。它不是面向百万级文档设计的检索架构。
5. 融合排序
_retrieve 将关键词、标签、时间和近期评分与向量相似度合并,然后按照意图控制上下文数量:
- 总结类最多选取 12 条记录作为上下文;
- 普通检索和聊天最多选取 8 条;
- 情绪类最多选取 3 条,避免把大量记录机械地展示给用户;
- 页面通常最多显示 5 条相关记录。
最终送入模型的并不是整个数据库,而是一组经过筛选的记录摘要。这正是 RAG 在本项目中的核心作用:先检索个人记忆,再让模型基于有限且相关的事实回答。
多轮问答与回答生成
问答历史保存在 ask_messages 表中。每次请求会读取当前用户最近 12 条持久化消息,模型提示中使用最近 10 条有效对话,并加入最多 8 条相关记录的日期、标题、摘要和标签。
answer_question 会根据意图切换回答指令:
- 总结类强调提炼主题和变化,不逐条复述;
- 检索类围绕最相关记录直接回答;
- 情绪类先接住情绪,再自然回应;
- 普通聊天则保持对话感。
Prompt 还要求避免固定句式,不要每次都以“我看到记录里”开头。远程模型调用失败时,系统会返回本地模板回答,保证接口仍有结果,但这种降级回答的自然度和共情能力明显弱于真实模型。
回答完成后,用户问题、AI 回答以及相关记录都会持久化。前端点击相关记录时,可以跳回时间线中的对应节点。
BYOK 与平台赠送额度
AI 应用上线后,一个现实问题是调用成本。完全由开发者承担模型费用不利于长期运行,但要求新用户一开始就理解 Base URL、模型名和 API Key,也会提高使用门槛。
当前项目采用两级路由:
- 用户配置了自己的 API Key 时,聊天和 AI 回顾优先使用用户 BYOK;
- 用户没有配置时,可以使用平台赠送额度:聊天 20 次、AI 回顾 3 次;
- 远程调用失败时,已经预扣的平台额度会退回;
- Embedding 暂时统一使用服务器配置,不消耗个人聊天额度;
- Frontier 使用服务器的 Dify Workflow Key,与个人聊天额度相互独立。
用户 API Key 不保存在小程序本地,而是提交到服务端,使用 cryptography.fernet.Fernet 加密后写入数据库。查询配置状态时只返回掩码,不返回明文。Base URL 必须是允许列表中的公开 HTTPS 地址,并拒绝私网、回环地址和带用户名密码的 URL,用于降低服务端请求伪造风险。
需要说明的是,当前聊天供应商虽然允许选择 Base URL 和模型名,但请求格式仍然要求兼容 OpenAI 风格的 /chat/completions,并不是任意厂商协议都能直接接入。
AI 回顾与时间线快照
回顾页分成两层:
- 默认打开时读取稳定的基础回顾或已有快照,不立即触发慢速模型调用;
- 用户点击“生成 AI 回顾”后,才真正调用模型并保存结果。
generate_review_insights 从最近 7 天和最近 30 天的记录中生成周总结、月总结、关注标签、洞察和下一步建议。如果没有可用 AI,系统仍会基于标签频次生成基础回顾,但会明确标记 ai_used=False。
每周日 20:00,systemd timer 会运行周回顾任务。满足条件且仍有 BYOK 或平台回顾额度的用户会生成周回顾快照;已存在的同周期 AI 快照不会重复生成。周回顾还会同步成只读的 weekly_review 时间线节点,使自动总结能够回到长期记录中。
目前定时任务只生成周回顾,没有自动生成月度时间线节点。
Frontier:从外部信息到可读热点
“关注”模块与个人 RAG 是两条相对独立的链路:
1 | Hacker News / GitHub Search API / RSS |
原始内容统一保存标题、来源、链接、发布时间和描述;各来源特有指标以 JSON 保存,例如 Hacker News 的分数、评论数和排名,或者 GitHub 的 Star、Fork 和语言。这样 RSS 没有互动指标时也可以使用空对象,不需要为每个来源重新设计表结构。
Dify 工作流只负责将候选内容整理为结构化中文结果:展示标题、摘要、分类、主题和重要性,不负责生成来源热度数据。服务端还实现了内容哈希、幂等更新、失败重试上限、单次最多 20 条以及不同来源配额,避免重复消耗工作流额度。
采集任务每天 06:00 和 18:00 自动运行。GitHub 部分的真实实现是调用 GitHub Search API,筛选近期创建、公开、非归档、非 Fork 且达到最低 Star 数的仓库,再按 Star 降序获取;它不是抓取 GitHub 官方 Trending 页面。RSS/Atom 则是通用解析器,可以通过配置接入 arXiv 等信息源。
当前 Frontier 还有清晰的产品边界:
- “稍后看”只保存在页面状态中,尚未持久化到用户账户;
- “查看原文”当前复制链接到剪贴板,而不是直接在小程序内打开任意外链;
- “记下想法”会预填记录标题,但来源链接和外部正文不会自动写入个人记录;
- 因此外部关注内容目前不会自动进入个人 RAG 知识库,只有用户真正写下并保存的内容才会被检索。
登录、数据隔离与隐私
小程序启动时调用 wx.login 获取临时 code,FastAPI 再请求微信 jscode2session 换取 openid。服务端生成随机登录令牌,只把令牌哈希保存在 user_sessions 表中;前端后续通过 Bearer Token 访问接口。
记录、聊天历史、回顾快照、头像、AI 配置和额度均按用户关联查询。头像和 CSV 导出也只处理当前登录用户的数据。AI Key 留在服务端加密保存,不会下发完整明文。
模型问答与回顾会把必要的个人记录片段发送给用户选择的模型服务或平台模型,因此隐私说明中仍需要明确数据处理目的,而不能因为 Key 加密就宣称记录完全不会离开服务器。
部署与自动化
后端部署在 Ubuntu 云服务器,通过 systemd 管理 FastAPI 进程和定时任务,小程序使用已配置的 HTTPS 合法域名访问服务。当前仓库包含 Frontier 早晚刷新和周回顾的 service/timer 配置。
项目的后端测试覆盖了:
- BYOK 优先级、平台额度扣减与失败退款;
- Embedding 使用平台配置的策略;
- Hacker News、GitHub 和 RSS 的标准化、幂等写入与单源失败隔离;
- Dify 结构化输出、内容哈希、来源配额、批次上限和重试;
- 头像大小校验与用户隔离;
- 周回顾额度、缓存与时间线同步。
在本文写作时重新运行测试:
1 | Ran 36 tests |
这组测试主要覆盖新增的 AI 与 Frontier 服务逻辑,并不代表登录、记录 CRUD 和完整问答链路已经拥有同等程度的端到端测试。
项目中我认为最重要的取舍
1. 先做可解释的混合检索,而不是直接引入向量数据库
个人记录规模有限,SQLite 加 Python 余弦扫描足以支撑当前版本。关键词、标签、时间和向量的组合也比只使用向量更容易解释命中原因。代价是记录规模继续增长后,需要迁移到带向量索引的数据库,并补充重排与离线评测。
2. 所有外部 AI 调用都要有失败路径
记录分析、Embedding、问答和回顾都可能受网络、额度和供应商状态影响。项目通过规则摘要、本地哈希向量、模板回答和稳定基础回顾保证核心记录功能不被外部 API 完全阻断,同时通过日志区分真实 AI 结果与降级结果。
3. 在体验和成本之间加入 BYOK 过渡层
直接要求所有用户配置 API Key 会劝退新用户,完全由平台付费又无法长期维持。少量赠送额度让用户先体验核心能力,BYOK 则承担长期使用成本,失败退款和使用日志保证额度状态可追踪。
当前不足与后续方向
基于现有代码,下一阶段最值得继续完善的是:
- 为问答链路建立固定查询集,分别评估 Recall@K、MRR、时间问题命中率和最终回答事实一致性;
- 统一历史 Embedding 的模型版本,在切换模型时支持批量重建,避免不同维度向量无法比较;
- 当个人记录规模继续增长时,引入向量索引和 reranker,而不是继续在 Python 中全量扫描;
- 为登录、记录 CRUD、数据隔离和
/api/ask增加完整接口级测试; - 将 Frontier 的收藏状态持久化,并在用户明确保存后建立外部内容与个人记录的来源关系;
- 完善账号注销、隐私授权说明、数据删除和生产环境 CORS 策略。
代码地图
项目中与核心能力对应的主要文件如下:
1 | miniprogram/ |
结尾
“也祝日志”最初只是一个记录工具,但随着功能迭代,它逐渐变成了一个完整的 AI 应用实践:从用户登录、数据隔离和记录 CRUD,到 Embedding、混合 RAG、多轮问答、BYOK、定时回顾,再到外部信息采集和 Dify 工作流。
对我而言,这个项目最有价值的部分不是接入了多少模型,而是把模型放进一个真实、长期运行的产品后,继续处理上下文、检索、隐私、成本、失败降级、数据持久化和部署运维这些问题。
它仍有许多可以继续完善的地方,但已经完成了从想法、开发、真机调试、服务器部署到微信发布的完整闭环。
