AI日记软件 - 项目计划文档
1. 项目概述
1.1 项目背景
做一个本地运行的AI日记软件,通过与AI对话的形式进行日记记录,最后自动总结归档。
核心特点:
- 本地运行,保护隐私
- AI主动提问引导
- 多LLM支持 (OpenAI/Claude/Ollama)
- 双存储:SQLite查询 + txt归档
1.2 需求对齐
| 需求项 | 选择 |
|---|---|
| 技术栈 | Python + CLI/TUI |
| LLM来源 | 多源支持 (OpenAI/Claude/Ollama) |
| 存储方案 | SQLite + txt (两者都要) |
| UI形式 | 命令行/TUI (textual) |
| 核心流程 | AI主动提问引导 |
| 附加功能 | MVP优先,后续扩展 |
2. 技术选型
| 层级 | 技术选型 | 说明 |
|---|---|---|
| TUI框架 | textual v8+ |
现代异步TUI,支持CSS布局 |
| LLM调用 | llm 库 |
多Provider统一接口 (OpenAI/Claude/Ollama) |
| 数据存储 | SQLite + txt | SQLite查询 + txt归档 |
| 配置管理 | JSON文件 | API Key及参数配置 |
3. 架构设计
3.1 整体架构
┌─────────────────────────────────────────────────────────┐
│ CLI/TUI 层 │
│ (textual / prompt_toolkit) │
└─────────────────────┬───────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────┐
│ 业务逻辑层 │
│ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │
│ │ 会话管理器 │ │ Prompt引擎 │ │ 归档总结器 │ │
│ │ (Session) │ │ (引导对话) │ │ (summary) │ │
│ └─────────────┘ └─────────────┘ └───────────────┘ │
└─────────────────────┬───────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────┐
│ LLM 抽象层 │
│ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │
│ │ OpenAI适配 │ │ Claude适配 │ │ Ollama适配 │ │
│ │ (可复用llm) │ │ (可复用llm) │ │ (可复用llm) │ │
│ └─────────────┘ └─────────────┘ └───────────────┘ │
└─────────────────────┬───────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────┐
│ 数据存储层 │
│ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │
│ │ SQLite │ │ txt归档 │ │ config.json │ │
│ │ (查询/统计) │ │ (备份/导出) │ │ (API配置) │ │
│ └─────────────┘ └─────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────┘
3.2 数据流设计
┌─────────────────────────────────────────────────────────────┐
│ TUI 界面层 │
│ DiaryApp.on_input_submit() → 显示消息 │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ 会话管理层 │
│ SessionManager.add_message() → 维护对话历史 │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Prompt 引导层 │
│ PromptEngine.build_messages() → 构建设system + history │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ LLM 抽象层 │
│ LLMManager.chat() → llm库统一接口 │
│ (自动路由到 OpenAI / Claude / Ollama) │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ 存储层 │
│ SQLiteStorage (实时) + TxtArchiver (归档) │
└─────────────────────────────────────────────────────────────┘
4. 核心模块详细设计
4.1 LLM 抽象层 (src/llm/)
使用 llm 库作为底层,支持多Provider。
# src/llm/manager.py - LLM管理器
import llm
class LLMManager:
"""统一管理多Provider"""
def __init__(self, config: dict):
self.config = config
self._model = None
def get_model(self):
"""根据配置获取llm模型"""
model_id = self.config.get('model', 'gpt-4')
return llm.get_model(model_id)
async def chat(self, messages: list[dict]) -> str:
"""统一聊天接口"""
model = self.get_model()
response = model.chat(messages)
return response.text()
async def stream_chat(self, messages: list[dict]):
"""流式聊天接口"""
model = self.get_model()
for chunk in model.chat(messages, stream=True):
yield chunk.content
关键点:
llm库已实现多Provider支持,通过llm.get_model('模型名')自动路由- 支持通过环境变量或
llm keys set管理API Key - Ollama通过
llm-ollama插件支持
4.2 会话管理层 (src/session/)
# src/session/manager.py
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
import uuid
@dataclass
class DiarySession:
"""日记会话"""
id: str
created_at: datetime
messages: list[dict] # [{"role": "user/assistant", "content": "..."}]
summary: Optional[str] = None # 归档总结
status: str = "active" # active / archived
class SessionManager:
"""会话管理器"""
def __init__(self, storage):
self.storage = storage
self.current_session: Optional[DiarySession] = None
def create_session(self) -> DiarySession:
"""创建新会话"""
session = DiarySession(
id=str(uuid.uuid4()),
created_at=datetime.now(),
messages=[]
)
self.storage.save_session(session)
self.current_session = session
return session
def add_message(self, role: str, content: str):
"""添加消息到当前会话"""
self.current_session.messages.append({
"role": role,
"content": content,
"timestamp": datetime.now().isoformat()
})
self.storage.update_session(self.current_session)
def archive_session(self, summary: str):
"""归档会话,生成总结"""
self.current_session.summary = summary
self.current_session.status = "archived"
self.storage.update_session(self.current_session)
self.storage.export_to_txt(self.current_session)
4.3 Prompt引导引擎 (src/prompt/)
# src/prompt/engine.py
from typing import Optional
import json
from pathlib import Path
class PromptEngine:
"""AI引导Prompt引擎"""
def __init__(self, prompts_dir: str = "prompts"):
self.prompts_dir = Path(prompts_dir)
self.prompts = self._load_prompts()
def _load_prompts(self) -> dict:
"""加载prompts配置"""
default_prompt = {
"system": "你是一个温暖的朋友,擅长通过提问帮助用户记录日记。请用友好的语气,主动引导用户分享今天的经历、感受和想法。每次对话后,根据用户的回答提出一个深入的追问。",
"archive": "请为这段日记生成一个简洁的总结,包括:1)今天的主要事件;2)用户的情绪变化;3)重要的收获或感悟。不超过100字。"
}
prompt_file = self.prompts_dir / "prompts.json"
if prompt_file.exists():
with open(prompt_file) as f:
return json.load(f)
return default_prompt
def get_system_prompt(self) -> str:
"""获取系统引导语"""
return self.prompts.get("system", "")
def get_archive_prompt(self) -> str:
"""获取归档总结Prompt"""
return self.prompts.get("archive", "")
def build_messages(self, user_input: str, history: list[dict]) -> list[dict]:
"""构建完整的messages数组"""
messages = [
{"role": "system", "content": self.get_system_prompt()}
]
messages.extend(history)
messages.append({"role": "user", "content": user_input})
return messages
4.4 存储层 (src/storage/)
SQLite存储
# src/storage/sqlite.py
import sqlite3
from pathlib import Path
from datetime import datetime
from typing import Optional
import json
class SQLiteStorage:
"""SQLite存储"""
def __init__(self, db_path: str = "diary.db"):
self.db_path = db_path
self._init_db()
def _init_db(self):
"""初始化数据库表"""
conn = sqlite3.connect(self.db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
messages TEXT NOT NULL,
summary TEXT,
status TEXT DEFAULT 'active'
)
""")
conn.commit()
conn.close()
def save_session(self, session):
conn = sqlite3.connect(self.db_path)
conn.execute("""
INSERT INTO sessions (id, created_at, messages, status)
VALUES (?, ?, ?, ?)
""", (
session.id,
session.created_at.isoformat(),
json.dumps(session.messages),
session.status
))
conn.commit()
conn.close()
def update_session(self, session):
conn = sqlite3.connect(self.db_path)
conn.execute("""
UPDATE sessions
SET messages = ?, summary = ?, status = ?
WHERE id = ?
""", (
json.dumps(session.messages),
session.summary,
session.status,
session.id
))
conn.commit()
conn.close()
def list_sessions(self, limit: int = 10) -> list[dict]:
"""列出最近会话"""
conn = sqlite3.connect(self.db_path)
cursor = conn.execute("""
SELECT id, created_at, summary, status
FROM sessions
ORDER BY created_at DESC
LIMIT ?
""", (limit,))
rows = cursor.fetchall()
conn.close()
return [{"id": r[0], "created_at": r[1], "summary": r[2], "status": r[3]}
for r in rows]
Txt归档
# src/storage/txt.py
from pathlib import Path
from datetime import datetime
class TxtArchiver:
"""txt归档器"""
def __init__(self, diary_dir: str = "diary"):
self.diary_dir = Path(diary_dir)
self.diary_dir.mkdir(exist_ok=True)
def export_session(self, session) -> Path:
"""导出会话到txt文件"""
date_str = session.created_at.strftime("%Y-%m-%d")
file_path = self.diary_dir / f"{date_str}.txt"
content = self._build_content(session)
if file_path.exists():
with open(file_path, "a", encoding="utf-8") as f:
f.write("\n\n" + "="*30 + "\n\n")
f.write(content)
else:
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
return file_path
def _build_content(self, session) -> str:
"""构建txt内容"""
lines = [f"# 日记 - {session.created_at.strftime('%Y年%m月%d日 %H:%M')}\n"]
for msg in session.messages:
role = "我" if msg["role"] == "user" else "AI"
lines.append(f"**{role}**: {msg['content']}")
if session.summary:
lines.append(f"\n---\n**总结**: {session.summary}")
return "\n\n".join(lines)
4.5 TUI界面层 (src/ui/)
# src/ui/app.py
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static, Input, Button
from textual.containers import Container, VerticalScroll
from textual.binding import Binding
class DiaryApp(App):
"""AI日记主应用"""
CSS = """
Screen {
background: $surface;
}
#chat-container {
height: 80%;
}
.message {
padding: 1;
margin: 1 0;
}
.user-message {
color: $text;
background: $accent-darken-1;
}
.ai-message {
color: $text;
background: $primary-darken-1;
}
#input-area {
height: 3;
border-top: solid primary;
}
"""
BINDINGS = [
Binding("ctrl+q", "quit", "退出", priority=True),
Binding("ctrl+n", "new_session", "新建日记"),
Binding("ctrl+s", "archive_session", "归档"),
]
def __init__(self, session_manager, llm_manager):
super().__init__()
self.session_manager = session_manager
self.llm_manager = llm_manager
self.conversation_history = []
def compose(self) -> ComposeResult:
yield Header()
with Container(id="chat-container"):
yield VerticalScroll(Static(id="messages"))
with Container(id="input-area"):
yield Input(placeholder="写下今天的故事...", id="user-input")
yield Button("发送", id="send-btn", variant="primary")
yield Footer()
async def on_mount(self) -> None:
"""应用启动时创建新会话并获取AI引导"""
session = self.session_manager.create_session()
initial_prompt = "你好!今天过得怎么样?有什么想分享的吗?"
await self._add_ai_message(initial_prompt)
async def on_input_submit(self, event: Input.Submit) -> None:
"""处理用户提交"""
user_input = event.value
if not user_input.strip():
return
await self._add_user_message(user_input)
self.session_manager.add_message("user", user_input)
self.query_one("#user-input", Input).value = ""
await self._get_ai_response(user_input)
async def _add_user_message(self, content: str):
"""添加用户消息到界面"""
messages = self.query_one("#messages", Static)
messages.update(messages.renderable + f"\n[我]: {content}")
async def _add_ai_message(self, content: str):
"""添加AI消息到界面"""
messages = self.query_one("#messages", Static)
messages.update(messages.renderable + f"\n[AI]: {content}")
async def _get_ai_response(self, user_input: str):
"""获取AI响应"""
from src.prompt.engine import PromptEngine
prompt_engine = PromptEngine()
messages = prompt_engine.build_messages(user_input, self.conversation_history)
response_text = ""
async for chunk in self.llm_manager.stream_chat(messages):
response_text += chunk
self.session_manager.add_message("assistant", response_text)
self.conversation_history.append({"role": "user", "content": user_input})
self.conversation_history.append({"role": "assistant", "content": response_text})
def action_new_session(self):
"""新建会话"""
self.conversation_history = []
self.session_manager.create_session()
def action_archive_session(self):
"""归档当前会话"""
pass
def action_quit(self):
"""退出应用"""
self.exit()
5. 数据结构
5.1 数据库Schema
-- sessions 表
CREATE TABLE sessions (
id TEXT PRIMARY KEY, -- UUID
created_at TEXT NOT NULL, -- ISO时间
messages TEXT NOT NULL, -- JSON: [{"role":"user","content":"...","timestamp":"..."}]
summary TEXT, -- 归档总结
status TEXT DEFAULT 'active', -- active / archived
tags TEXT -- JSON: ["工作", "情感"] (未来扩展)
);
-- 索引
CREATE INDEX idx_sessions_created ON sessions(created_at);
CREATE INDEX idx_sessions_status ON sessions(status);
5.2 配置文件格式
// config.json
{
"llm": {
"provider": "openai",
"model": "gpt-4o-mini",
"temperature": 0.7,
"max_tokens": 2048
},
"storage": {
"db_path": "diary.db",
"diary_dir": "diary"
},
"prompts": {
"custom": true,
"file": "prompts/prompts.json"
},
"ui": {
"theme": "nord",
"show_timestamp": true
}
}
// prompts/prompts.json
{
"system": "你是一个温暖的朋友,擅长通过提问帮助用户记录日记...",
"archive": "请为这段日记生成一个简洁的总结..."
}
6. 目录结构
ai-diary/
├── main.py # 入口,CLI命令
├── config.json # 主配置
├── requirements.txt # 依赖
├── prompts/
│ └── prompts.json # 可配置的引导prompt
├── src/
│ ├── __init__.py
│ ├── llm/
│ │ ├── __init__.py
│ │ └── manager.py # LLM统一接口
│ ├── session/
│ │ ├── __init__.py
│ │ └── manager.py # 会话管理
│ ├── prompt/
│ │ ├── __init__.py
│ │ └── engine.py # Prompt引导引擎
│ ├── storage/
│ │ ├── __init__.py
│ │ ├── sqlite.py # SQLite操作
│ │ └── txt.py # txt归档
│ └── ui/
│ ├── __init__.py
│ ├── app.py # TUI主应用
│ └── widgets.py # 自定义组件
├── diary/ # 归档txt
│ ├── 2026-05-20.txt
│ └── ...
└── diary.db # SQLite数据库
7. 关键实现要点
| 模块 | 关键点 | 注意事项 |
|---|---|---|
| LLM调用 | 使用 llm 库的 model.chat() 接口 |
流式输出需用 stream=True |
| 异步处理 | Textual基于asyncio,需使用 async/await |
LLM调用必须是异步的 |
| 消息历史 | 每次构建messages时需包含完整history | 考虑token限制,可截断 |
| 归档总结 | 会话结束时调用LLM生成总结 | 需要单独的一次LLM调用 |
| 配置管理 | API Key用 llm keys set 管理 |
不直接存明文 |
8. 相关项目参考
| 项目 | 特点 | 借鉴点 |
|---|---|---|
| simonw/llm | CLI工具,多LLM支持 | LLM抽象层、多Provider |
| local-diary-llm | 本地LLM日记,PyQt6 | Prompt可配置、本地隐私 |
| reflective-journal-cli-app | CLI日记,情绪追踪 | 会话管理 |
9. 待确认问题
- 每日Prompt策略:固定引导语 vs 随日期变化
- 归档触发:手动触发 vs 自动归档
- 多会话支持:是否需要同时多个日记会话