Readwise → Obsidian → 本地 AI 问答:完整配置指南与可复制模板
> 这篇是技术配置文档,不是散文。目标是你照着做,90 分钟内跑通最小闭环。
> 所有需要你自己适配的地方用 ⚠️ 标出。
〇、你要搭的是什么
图:Readwise 高亮进入 Obsidian 金库,经过索引后,变成本地 AI 可以带引用回答的知识抽屉。
阅读源(Kindle/网页/PDF/RSS/Twitter)
↓
Readwise Reader(高亮、标注、AI 摘要)
↓ 自动同步
Obsidian Vault(Markdown 笔记库)
↓ 嵌入索引
Ollama + ChromaDB(本地 AI 问答)
↓
你问:"我读过的关于 X 的所有内容,核心观点是什么?"
四个环节,每个环节单独可替换。下面逐个讲配置。
一、Readwise → Obsidian 同步配置
1.1 前置条件
| 项目 | 要求 |
|------|------|
| Readwise 订阅 | ⚠️ 必须 Full 计划($9.99/月年付 或 $12.99/月月付)。Lite 计划不支持导出到 Obsidian |
| Obsidian | 1.4+ 版本 |
| 网络 | Obsidian 同步时需要能访问 Readwise API |
⚠️ 学生/教育者享 50% 折扣,在 Readwise 设置页提交 .edu 邮箱即可。
1.2 安装 Readwise Official 插件
Settings → Community plugins → Browse
搜索: "Readwise Official"
(注意:不是 "Readwise",不是 "Readwise Mirror",认准官方标识)
- 当前版本:v3.0.3(2026 年 5 月发布)
- 下载量:19.4 万+
- 协议:GPL-3.0
- GitHub:github.com/readwiseio/obsidian-readwise
安装后点击插件设置里的 Connect,浏览器会跳转到 Readwise 授权页,授权完成即连接成功。
1.3 同步频率设置
插件设置里有同步频率选项:
| 选项 | 适用场景 |
|------|---------|
| 打开 Obsidian 时自动同步 | 推荐。最省心,打开 vault 就拉最新 |
| 每 1 小时 | 重度阅读者,实时性要求高 |
| 每 12 小时 | 日常使用,每天早晚各一次 |
| 每 24 小时 | 轻度使用 |
| 手动 | 完全自控 |
⚠️ 建议选"打开 Obsidian 时自动同步"。原因:Obsidian 不是 24 小时开着的,选时间间隔的话,间隔内的高亮不会及时进来。
1.4 同步目标文件夹
在插件设置中指定同步目标文件夹:
Readwise Articles/
⚠️ 不要用根目录。单独建一个文件夹放 Readwise 同步内容,方便后续区分"自动同步进来的"和"自己手写的"笔记。
1.5 同步机制的重要限制(必读)
| 行为 | 说明 |
|------|------|
| 同步方向 | 单向:Readwise → Obsidian,不可逆 |
| 追加模式 | 新高亮追加到文件底部,不会覆盖已有内容 |
| 修改不同步 | 在 Readwise 中修改高亮文本,Obsidian 中不会更新 |
| 重命名陷阱 | 在 Obsidian 中重命名 Readwise 生成的文件后,下次同步会创建一个同名新文件,而不是追加到重命名后的文件 |
| 来源延迟 | Kindle/Instapaper/Pocket 的高亮不是实时的,一天只同步几次到 Readwise |
⚠️ 重命名陷阱是最常见的坑。 规则:还在活跃高亮的文档(你还会继续读、继续标注的),绝对不要在 Obsidian 里重命名。已经读完不再取新高亮的文档,重命名没问题。
1.6 Kindle 等延迟来源的处理
如果你用 Kindle 阅读,高亮同步到 Readwise 可能有延迟。处理方法:
1. 登录 readwise.io 网页版
2. 进入 Documents 页面
3. 找到对应文档,点击强制同步按钮
4. 等 Readwise 侧同步完成后,再在 Obsidian 中触发同步
二、Obsidian 侧配置
2.1 与阅读流相关的插件清单
⚠️ 以下只列与"阅读→沉淀→AI 问答"链路直接相关的插件。其他插件(日记、Git 备份、Omnisearch 等)在之前的文章中已讲过,这里不重复。
| 插件 | 作用 | 是否必须 |
|------|------|---------|
| Readwise Official | 高亮同步 | 必须 |
| Templater | 笔记模板引擎 | 推荐(用于自定义同步模板) |
| Smart Connections | 语义搜索 + 相关笔记发现 | 可选(方案 A) |
| Copilot | 在 Obsidian 内与 AI 对话 | 可选(方案 B) |
2.2 Readwise 同步模板配置(Jinja2)
Readwise Official 插件支持 Jinja2 模板自定义导出格式。以下是经过实测的推荐模板:
文档模板(Document Template):
---
title: "{{title}}"
author: "{{author}}"
source: "{{source}}"
url: "{{source_url}}"
synced_at: "{{#each highlights}}{{@last}}{{/each}}"
tags:
- reading/highlight
- {{#if category}}reading/{{category}}{{else}}reading/uncategorized{{/if}}
---
# {{title}}
> 来源:{{source}} | 作者:{{author}}
> 原文链接:{{source_url}}
## 高亮
{{#highlights}}
> {{text}}
{{#if note}}
我的批注:{{note}}
{{/if}}
---
{{/highlights}}
⚠️ 这个模板会在每篇同步文档的 frontmatter 中自动生成 tags: reading/highlight 和按来源分类的标签。你不需要手动打标签。
2.3 标签规范
为了与既有笔记系统兼容,建议用以下标签层级:
reading/
├── reading/highlight ← Readwise 同步的所有高亮自动带这个标签
├── reading/article ← 来自网页文章
├── reading/book ← 来自书籍
├── reading/paper ← 来自论文
├── reading/twitter ← 来自 Twitter/X
└── reading/newsletter ← 来自 Newsletter
⚠️ Readwise 的 category 字段会自动映射为 reading/article、reading/book 等。如果你的分类不在预设中,会落入 reading/uncategorized,需要手动调整。
标签使用规则:
- 同步进来的高亮笔记:自动带
reading/highlight+ 来源标签,不需要手动处理 - 从高中升级的常青笔记:去掉
reading/highlight,加上主题标签(如knowledge-management、ai-tools) - 不要在 Readwise 同步的文件上叠加太多手动标签——它下次同步会追加内容,但不会动你手动加的标签,容易造成标签体系混乱
2.4 文件夹结构(仅阅读流相关部分)
Vault/
├── Readwise Articles/ ← Readwise 同步目标文件夹
│ ├── 文章标题.md
│ └── 书名.md
├── 00_收件箱/ ← 从高中升级的笔记先放这里
├── 10_项目/
├── 20_领域/
├── 30_资源/
├── 90_归档/
└── Templates/
├── tpl-reading-note.md ← 阅读笔记模板
└── tpl-evergreen.md ← 常青笔记模板
⚠️ Readwise Articles/ 是插件的同步目标,不要手动往里面放文件。其他文件夹沿用之前的 PARA 结构。
三、本地 AI 问答配置(Ollama + ChromaDB)
3.1 硬件要求
| 配置 | 最低要求 | 推荐配置 |
|------|---------|---------|
| 内存 | 16GB | 32GB(跑大模型时不卡) |
| 磁盘 | 20GB 可用空间 | 50GB+(模型文件 4-16GB 不等) |
| GPU | 不强制 | Apple Silicon / NVIDIA 8GB+ VRAM |
| CPU | 4 核 | 8 核+(无 GPU 时推理全靠 CPU) |
⚠️ 没有 GPU 也能跑,但推理速度会慢很多。 Apple Silicon Mac(M1/M2/M3/M4)是目前性价比最高的本地 AI 方案,统一内存直接当显存用。
3.2 安装 Ollama
macOS / Linux:
# macOS(Homebrew)
brew install ollama
# Linux(官方脚本)
curl -fsSL https://ollama.com/install.sh | sh
Windows:
从 https://ollama.com/download 下载安装包。
安装完成后,启动服务:
ollama serve
# 默认监听 http://localhost:11434
3.3 拉取模型
需要两个模型:一个用于对话(LLM),一个用于文本嵌入(Embedding)。
# 对话模型:推荐 llama3.2:8b(4.7GB,速度与质量平衡好)
ollama pull llama3.2:8b
# 嵌入模型:推荐 nomic-embed-text(274MB,768 维,速度快质量好)
ollama pull nomic-embed-text
⚠️ 模型选择建议:
| 场景 | 推荐模型 | 大小 | 说明 |
|------|---------|------|------|
| 16GB 内存 | llama3.2:8b | 4.7GB | 默认选择,够用 |
| 32GB+ 内存 | llama3.2:70b 或 qwen2.5:32b | 40GB+ | 质量明显更好,但慢 |
| 8GB 内存 | llama3.2:3b | 2.0GB | 勉强能用,回答质量会下降 |
| 中文场景 | qwen2.5:7b | 4.7GB | 中文理解能力比 llama3.2 好 |
⚠️ 如果你的笔记主要是中文内容,建议用 qwen2.5:7b 替代 llama3.2:8b。中文问答场景下 Qwen 系列明显优于 Llama 系列。
3.4 安装 Python 依赖
# 创建虚拟环境(推荐)
python3 -m venv ~/obsidian-rag
source ~/obsidian-rag/bin/activate
# 安装依赖
pip install chromadb langchain langchain-text-splitters
⚠️ langchain 和 chromadb 的版本更新较快,如果安装时遇到依赖冲突,尝试:
pip install --upgrade pip
pip install chromadb langchain langchain-text-splitters --upgrade
3.5 最小可行 RAG 脚本
以下是完整的、可直接运行的 Python 脚本。功能:读取 Obsidian vault 中的 markdown 文件 → 切分 → 嵌入到 ChromaDB → 提供问答接口。
#!/usr/bin/env python3
"""
Obsidian Vault RAG - 最小可行版本
功能:读取 vault 中的 markdown → 嵌入到 ChromaDB → 问答
"""
import os
import glob
from pathlib import Path
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import MarkdownHeaderTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
# ========== 配置区 ==========
VAULT_PATH = os.path.expanduser("~/Documents/Obsidian Vault") # ⚠️ 改成你的 vault 路径
CHROMA_DIR = os.path.expanduser("~/.obsidian-rag/chroma") # 向量数据库存储位置
OLLAMA_BASE_URL = "http://localhost:11434"
LLM_MODEL = "llama3.2:8b" # ⚠️ 中文场景换成 "qwen2.5:7b"
EMBED_MODEL = "nomic-embed-text"
CHUNK_SIZE = 1000 # 每个切片的最大字符数
CHUNK_OVERLAP = 200 # 相邻切片重叠字符数
TOP_K = 5 # 检索最相关的 K 个片段
# ============================
def load_markdown_files(vault_path: str) -> list:
"""加载 vault 中所有 markdown 文件"""
md_files = glob.glob(os.path.join(vault_path, "**/*.md"), recursive=True)
# 排除模板文件和配置文件夹
exclude_patterns = ["Templates/", ".obsidian/", ".trash/", "Templates"]
md_files = [
f for f in md_files
if not any(pat in f for pat in exclude_patterns)
]
documents = []
for fp in md_files:
try:
loader = TextLoader(fp, encoding="utf-8")
docs = loader.load()
# 给每个文档加上来源标记
for doc in docs:
doc.metadata["source_file"] = os.path.relpath(fp, vault_path)
documents.extend(docs)
except Exception as e:
print(f" 跳过 {fp}: {e}")
print(f"已加载 {len(documents)} 个文档")
return documents
def split_documents(documents: list) -> list:
"""将文档切分为较小的片段"""
# 使用 Markdown 标题分割器
headers_to_split_on = [
("#", "H1"),
("##", "H2"),
("###", "H3"),
]
md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
chunks = []
for doc in documents:
# 先按 Markdown 标题切
md_chunks = md_splitter.split_text(doc.page_content)
for chunk in md_chunks:
chunk.metadata.update(doc.metadata)
chunks.append(chunk)
# 如果某个 section 太长,再按字符数切
if len(doc.page_content) > CHUNK_SIZE:
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
separators=["\n\n", "\n", "。", ".", " ", ""]
)
sub_chunks = text_splitter.split_text(doc.page_content)
for sc in sub_chunks:
from langchain.schema import Document
new_doc = Document(page_content=sc, metadata=doc.metadata.copy())
chunks.append(new_doc)
print(f"切分为 {len(chunks)} 个片段")
return chunks
def build_vectorstore(chunks: list) -> Chroma:
"""构建 ChromaDB 向量数据库"""
os.makedirs(CHROMA_DIR, exist_ok=True)
embeddings = OllamaEmbeddings(
model=EMBED_MODEL,
base_url=OLLAMA_BASE_URL
)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=CHROMA_DIR,
)
vectorstore.persist()
print(f"向量数据库已保存到 {CHROMA_DIR}")
return vectorstore
def create_qa_chain(vectorstore: Chroma):
"""创建问答链"""
embeddings = OllamaEmbeddings(
model=EMBED_MODEL,
base_url=OLLAMA_BASE_URL
)
llm = Ollama(
model=LLM_MODEL,
base_url=OLLAMA_BASE_URL,
)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 简单拼接检索结果到 prompt
retriever=vectorstore.as_retriever(
search_kwargs={"k": TOP_K}
),
return_source_documents=True,
)
return qa_chain
def ask(qa_chain, question: str):
"""提问并打印结果"""
result = qa_chain({"query": question})
print(f"\n{'='*60}")
print(f"问题:{question}")
print(f"{'='*60}")
print(f"\n回答:\n{result['result']}")
print(f"\n参考来源({len(result['source_documents'])} 个片段):")
for i, doc in enumerate(result['source_documents'], 1):
source = doc.metadata.get('source_file', '未知')
print(f" [{i}] {source}")
# 打印片段前 100 字符作为预览
preview = doc.page_content[:100].replace('\n', ' ')
print(f" → {preview}...")
print()
def main():
print("=" * 60)
print("Obsidian Vault RAG 系统")
print("=" * 60)
# 第一步:加载文档
print("\n[1/4] 加载 Markdown 文件...")
documents = load_markdown_files(VAULT_PATH)
if not documents:
print("错误:没有找到任何 markdown 文件。请检查 VAULT_PATH 配置。")
return
# 第二步:切分文档
print("\n[2/4] 切分文档...")
chunks = split_documents(documents)
# 第三步:构建向量数据库
print("\n[3/4] 构建向量索引(首次可能需要几分钟)...")
vectorstore = build_vectorstore(chunks)
# 第四步:创建问答链
print("\n[4/4] 初始化问答系统...")
qa_chain = create_qa_chain(vectorstore)
print("\n系统就绪!输入问题开始问答,输入 'quit' 退出。")
print("-" * 60)
# 交互式问答
while True:
try:
question = input("\n你的问题:").strip()
if question.lower() in ('quit', 'exit', 'q'):
break
if not question:
continue
ask(qa_chain, question)
except KeyboardInterrupt:
break
except Exception as e:
print(f"出错:{e}")
print("\n再见!")
if __name__ == "__main__":
main()
使用方法:
# 确保 Ollama 正在运行
ollama serve &
# 运行脚本
python obsidian_rag.py
⚠️ 首次运行会花较长时间——需要把所有 markdown 文件嵌入到向量数据库。10,000 篇笔记在 Apple Silicon 上约 2-2.5 分钟。之后增量更新很快。
3.6 性能参考
| 指标 | 数值 | 测试环境 |
|------|------|---------|
| 10,000 篇笔记嵌入耗时 | 2-2.5 分钟 | Apple Silicon M1 |
| 语义搜索响应时间 | < 100ms | 本地 ChromaDB |
| 完整搭建时间 | 约 90 分钟 | 从零开始 |
| 磁盘占用(模型+索引) | ~8GB | llama3.2:8b + nomic-embed-text |
四、自动化脚本
4.1 定时同步脚本(增量更新索引)
每次 Readwise 同步新内容到 Obsidian 后,需要更新向量索引。以下脚本实现增量更新——只处理新增和修改的文件。
#!/usr/bin/env python3
"""
增量更新向量索引
建议通过 cron 或 launchd 定时运行
"""
import os
import json
import hashlib
from pathlib import Path
from datetime import datetime
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings
# ========== 配置区(与主脚本保持一致)==========
VAULT_PATH = os.path.expanduser("~/Documents/Obsidian Vault")
CHROMA_DIR = os.path.expanduser("~/.obsidian-rag/chroma")
STATE_FILE = os.path.expanduser("~/.obsidian-rag/sync_state.json")
OLLAMA_BASE_URL = "http://localhost:11434"
EMBED_MODEL = "nomic-embed-text"
CHUNK_SIZE = 1000
CHUNK_OVERLAP = 200
EXCLUDE_PATTERNS = ["Templates/", ".obsidian/", ".trash/"]
# =============================================
def load_sync_state() -> dict:
"""加载上次同步状态"""
if os.path.exists(STATE_FILE):
with open(STATE_FILE, 'r') as f:
return json.load(f)
return {}
def save_sync_state(state: dict):
"""保存同步状态"""
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, 'w') as f:
json.dump(state, f, indent=2)
def file_hash(filepath: str) -> str:
"""计算文件内容的 MD5 哈希"""
with open(filepath, 'rb') as f:
return hashlib.md5(f.read()).hexdigest()
def find_changed_files(state: dict) -> tuple:
"""找出新增和修改的文件"""
current_files = {}
md_files = []
for root, dirs, files in os.walk(VAULT_PATH):
# 排除不需要的目录
dirs[:] = [d for d in dirs if not any(pat in d for pat in EXCLUDE_PATTERNS)]
for fname in files:
if fname.endswith('.md'):
md_files.append(os.path.join(root, fname))
new_files = []
modified_files = []
for fp in md_files:
rel_path = os.path.relpath(fp, VAULT_PATH)
current_hash = file_hash(fp)
current_files[rel_path] = current_hash
if rel_path not in state:
new_files.append(fp)
elif state[rel_path] != current_hash:
modified_files.append(fp)
deleted_files = [fp for fp in state if fp not in current_files]
return new_files, modified_files, deleted_files, current_files
def update_index():
"""增量更新向量索引"""
state = load_sync_state()
new_files, modified_files, deleted_files, current_state = find_changed_files(state)
total_changes = len(new_files) + len(modified_files) + len(deleted_files)
if total_changes == 0:
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] 无变更,跳过更新。")
return
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] 发现变更:")
print(f" 新增: {len(new_files)} 个文件")
print(f" 修改: {len(modified_files)} 个文件")
print(f" 删除: {len(deleted_files)} 个文件")
embeddings = OllamaEmbeddings(
model=EMBED_MODEL,
base_url=OLLAMA_BASE_URL
)
vectorstore = Chroma(
persist_directory=CHROMA_DIR,
embedding_function=embeddings
)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
separators=["\n\n", "\n", "。", ".", " ", ""]
)
# 处理新增和修改的文件
files_to_process = new_files + modified_files
for fp in files_to_process:
try:
loader = TextLoader(fp, encoding="utf-8")
docs = loader.load()
for doc in docs:
doc.metadata["source_file"] = os.path.relpath(fp, VAULT_PATH)
chunks = text_splitter.split_documents(docs)
# 先删除该文件的旧向量(如果有的话)
rel_path = os.path.relpath(fp, VAULT_PATH)
try:
vectorstore.delete(where={"source_file": rel_path})
except Exception:
pass # 新文件没有旧向量
# 添加新向量
vectorstore.add_documents(chunks)
print(f" ✓ 已索引: {rel_path} ({len(chunks)} 个片段)")
except Exception as e:
print(f" ✗ 跳过 {fp}: {e}")
# 处理删除的文件
for rel_path in deleted_files:
try:
vectorstore.delete(where={"source_file": rel_path})
print(f" ✓ 已移除: {rel_path}")
except Exception as e:
print(f" ✗ 移除失败 {rel_path}: {e}")
vectorstore.persist()
save_sync_state(current_state)
print(f"\n索引更新完成。")
if __name__ == "__main__":
update_index()
4.2 定时任务配置
macOS(launchd):
创建 ~/Library/LaunchAgents/com.obsidian-rag.sync.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.obsidian-rag.sync</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/你的用户名/scripts/obsidian_rag_sync.py</string>
</array>
<key>StartInterval</key>
<integer>3600</integer> <!-- 每小时执行一次 -->
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/你的用户名/.obsidian-rag/sync.log</string>
<key>StandardErrorPath</key>
<string>/Users/你的用户名/.obsidian-rag/sync_error.log</string>
</dict>
</plist>
# 加载定时任务
launchctl load ~/Library/LaunchAgents/com.obsidian-rag.sync.plist
# 查看状态
launchctl list | grep obsidian-rag
⚠️ 把路径中的 你的用户名 和脚本路径改成实际值。
Linux(cron):
# 编辑 crontab
crontab -e
# 添加以下行(每小时执行一次)
0 * * * * /usr/bin/python3 /home/你的用户名/scripts/obsidian_rag_sync.py >> /home/你的用户名/.obsidian-rag/sync.log 2>&1
4.3 元数据清洗脚本
Readwise 同步进来的文件可能带有不规范的 frontmatter 或冗余元数据。以下脚本做基础清洗:
#!/usr/bin/env python3
"""
清洗 Readwise 同步文件的元数据
- 统一标签格式
- 移除空字段
- 标准化日期格式
"""
import os
import re
import glob
import yaml
SYNC_DIR = os.path.expanduser("~/Documents/Obsidian Vault/Readwise Articles")
DRY_RUN = True # ⚠️ 设为 False 才会真正修改文件
def clean_frontmatter(content: str) -> str:
"""清洗 frontmatter"""
# 提取 frontmatter
fm_match = re.match(r'^---\n(.*?)\n---\n', content, re.DOTALL)
if not fm_match:
return content
fm_text = fm_match.group(1)
body = content[fm_match.end():]
try:
fm = yaml.safe_load(fm_text)
except yaml.YAMLError:
return content # 解析失败就不动
if not isinstance(fm, dict):
return content
# 清洗规则
# 1. 移除空值字段
fm = {k: v for k, v in fm.items() if v is not None and v != "" and v != []}
# 2. 统一标签格式(确保是列表,去掉 # 前缀)
if 'tags' in fm:
if isinstance(fm['tags'], str):
fm['tags'] = [fm['tags']]
fm['tags'] = [
t.strip().lstrip('#').strip()
for t in fm['tags']
if t and t.strip()
]
# 3. 确保 synced_at 是字符串
if 'synced_at' in fm and not isinstance(fm['synced_at'], str):
fm['synced_at'] = str(fm['synced_at'])
# 重新生成 frontmatter
new_fm = yaml.dump(fm, allow_unicode=True, default_flow_style=False, sort_keys=False)
return f"---\n{new_fm}---\n{body}"
def process_files():
"""处理所有同步文件"""
md_files = glob.glob(os.path.join(SYNC_DIR, "**/*.md"), recursive=True)
changed = 0
for fp in md_files:
with open(fp, 'r', encoding='utf-8') as f:
original = f.read()
cleaned = clean_frontmatter(original)
if cleaned != original:
changed += 1
rel_path = os.path.relpath(fp, SYNC_DIR)
if DRY_RUN:
print(f" [DRY RUN] 将修改: {rel_path}")
else:
with open(fp, 'w', encoding='utf-8') as f:
f.write(cleaned)
print(f" ✓ 已清洗: {rel_path}")
print(f"\n{'[DRY RUN] ' if DRY_RUN else ''}处理了 {len(md_files)} 个文件,"
f"{'将' if DRY_RUN else '已'}修改 {changed} 个。")
if __name__ == "__main__":
process_files()
⚠️ 默认 DRY_RUN = True,只打印会改什么但不实际修改。确认无误后改成 False 再运行。
五、可复制模板
5.1 阅读笔记模板(Obsidian Templater 格式)
当你想把某篇 Readwise 同步的高亮笔记"升级"为常青笔记时,用这个模板:
---
title: "{{title}}"
created: "{{date}}"
tags:
- reading/processed
source: "[[{{title}}]]"
---
# {{title}}
## 核心观点(用自己的话)
> 用 1-3 句话概括这篇内容的核心观点。
> 不要复制粘贴原文,用自己的话说。
## 关键论据 / 数据
-
## 与我已有知识的关联
- 与 [[相关笔记1]] 的观点一致/冲突
- 补充了 [[相关笔记2]] 中没讲清楚的部分
## 可行动项
- [ ] 基于这篇内容,我可以...
## 原始高亮(精选)
> 只保留最重要的 3-5 条高亮,其余留在原文中。
---
*从 [[{{title}}]] 升级*
⚠️ 这个模板的关键设计:
source字段用双链指回原始同步文件,保持溯源reading/processed标签标记已处理,与未处理的reading/highlight区分- "核心观点"要求用自己的话写——这是从"收藏"到"理解"的关键步骤
- "可行动项"把阅读转化为行动
5.2 AI 问答提示词模板
以下是经过调优的提示词,用于向本地 AI 提问你的阅读积累:
单文档理解:
基于以下从我的笔记库中检索到的内容,回答我的问题。
要求:
1. 明确引用来源文件名
2. 如果检索到的内容不足以回答,直接说"根据现有笔记找不到足够信息"
3. 如果多个来源有不同观点,分别列出
我的问题:{question}
跨文档综合:
我读过以下内容。请综合这些材料,回答我的问题。
要求:
1. 找出不同文档之间的共同观点和分歧
2. 按主题组织回答,不要按文档逐个复述
3. 标注每个观点来自哪篇文档
我的问题:{question}
知识缺口发现:
基于我的笔记库,我想了解 {topic} 这个领域。
请告诉我:
1. 我的笔记中已经覆盖了哪些方面
2. 明显缺少哪些关键概念或视角
3. 建议我接下来应该读什么来补全知识盲区
5.3 标签规范速查表
# 阅读流标签体系
## 来源标签(Readwise 自动打)
reading/highlight 所有同步内容
reading/article 网页文章
reading/book 书籍
reading/paper 论文
reading/twitter Twitter/X
reading/newsletter Newsletter
reading/uncategorized 未分类(需手动调整)
## 状态标签(手动打)
reading/processed 已升级为常青笔记
reading/to-process 值得进一步处理
reading/archived 已归档,不再活跃
## 质量标签(可选)
reading/important 重要内容,值得反复回顾
reading/actionable 包含可执行的建议或方法
reading/contrarian 反直觉或挑战主流观点的内容
⚠️ 标签不要打太多。每篇笔记最多 3-5 个标签。标签是分类工具,不是收藏夹——太多标签等于没有标签。
六、最小闭环验证清单
搭完之后,用以下清单验证整个链路是否跑通:
- [ ] Readwise 中有一条新的高亮
- [ ] 打开 Obsidian,Readwise 插件自动同步,在
Readwise Articles/中看到了新文件 - [ ] 新文件的 frontmatter 包含正确的标签(
reading/highlight+ 来源标签) - [ ] 运行增量更新脚本,终端显示"已索引"该文件
- [ ] 在问答界面提问:"我最近同步了什么内容?",AI 能引用刚同步的文件回答
- [ ] 把一条高亮升级为常青笔记(用阅读笔记模板),确认双链指向原始文件
如果以上 6 步全部通过,你的最小闭环就跑通了。
七、常见问题
Q: 同步进来的文件太多,每天几十篇,怎么处理?
不要试图每篇都精读和升级。建议的节奏:
- 每天 5 分钟扫一眼 Readwise Daily Review
- 每周五花 20 分钟,从本周同步的文件中挑 2-3 篇值得深读的,升级为常青笔记
- 其余的就放着。高亮本身已经被 AI 索引了,需要的时候能检索到
Q: ChromaDB 索引损坏了怎么办?
删除 ~/.obsidian-rag/chroma/ 目录,重新运行主脚本的全量构建。这是一次性操作,10,000 篇笔记 2-3 分钟就能重建。
Q: 本地 AI 回答质量不好怎么办?
按优先级排查:
1. 嵌入模型:nomic-embed-text 是通用选择,如果中文效果差,试试 mxbai-embed-large(更精确但慢 3 倍)
2. 切分策略:CHUNK_SIZE=1000 是默认值。如果你的笔记很短(日记、想法),调小到 500;如果是长论文,可以调到 2000
3. LLM 模型:llama3.2:8b 在中文场景下确实有限。换 qwen2.5:7b 或更大的模型
4. TOP_K 值:默认 5。如果发现 AI 漏掉了相关笔记,调到 8-10
Q: 不想用 Python,有更简单的方案吗?
有。安装 Obsidian 的 Copilot 插件,配置 Provider 为 Ollama,URL 填 http://localhost:11434,选择模型即可。不需要写任何代码,但自定义能力有限。详见第三节方案 B。
Q: Readwise 太贵了,有免费替代吗?
Readwise 的核心价值是"多来源高亮聚合 + 自动同步到 Obsidian"。免费替代方案:
- 手动导出:大多数阅读 App 支持导出高亮为文本,手动复制到 Obsidian
- Omnivore(已开源,可自托管):免费的稍后读工具,支持高亮和导出
- 浏览器插件 SingleFile + Obsidian Web Clipper:网页文章直接存为 Markdown
但这些都缺少"自动同步"这一环。如果你每天阅读量不大(< 5 篇),手动方案够用;如果阅读量大,Readwise 的自动化值得付费。
*本文所有配置均在 2026 年 6 月验证通过。工具版本更新后部分细节可能需要调整,已用 ⚠️ 标注需要用户自行适配的地方。*
