news 2026/9/3 5:29:22

本地Markdown笔记+本地AI:用Python和FastAPI构建隐私优先的AI笔记本

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
本地Markdown笔记+本地AI:用Python和FastAPI构建隐私优先的AI笔记本

1. 这篇文章真正要解决的问题

如果你平时用 Markdown 记录技术笔记、工作日志或产品想法,最近一定被各种「AI 笔记工具」的宣传刷过屏。它们普遍能做两件事:第一,把零散的文字整理成结构化内容;第二,帮你润色、补全、摘要。但很多工具有一个共同的前提——你把笔记内容上传到了云端。对于技术文档、私有化项目记录、甚至只是个人日记来说,这个前提本身就是一种顾虑。

VelocityNote 这个项目,无论最终实现成什么样,它把两个关键词放在了一起:tiny Markdown notebooklocal AI。与其说它是一款产品,不如说它代表了一类更值得关注的开发方向:笔记正文仍然用最朴素、最通用的 Markdown 文件保存,所有 AI 能力都在本机完成。它不依赖云服务、不锁定格式、不强制你改变工作方式。这个方向解决了三类问题:

  1. 隐私与可控:笔记是纯文本,保存在你自己的目录里。AI 请求也发往本机模型服务,内容不出机器。
  2. 格式自由:Markdown 是纯文本,可以使用任何编辑器打开。将来想换工具,复制文件即可,不需要导出。
  3. 成本与离线可用:本地模型只需要一次性的硬件投入,没有人按 token 收费,断网环境也能使用。

这篇文章不是对 VelocityNote 官方文档的翻译,而是要做两件事:先帮你拆解这类「本地 Markdown 笔记 + 本地 AI」工具的核心架构;再带着你用 Python + FastAPI + 本地模型,从零搭建一个最小可用的版本。读完以后,你能理解本地 AI 到底怎么嵌入笔记流程,也能动手做出一个属于自己的「tiny Markdown notebook」。

如果只对现成工具感兴趣,这篇教程也能帮你理解底层原理。以后遇到任何带 local AI 的笔记工具,你会更容易判断它是真本地,还是只做了一个「本地接口壳子」。

2. 基础概念:Markdown 与本地 AI 的协作模式

2.1 Markdown 为什么适合做笔记格式

Markdown 之所以能长期存在于开发者圈子,核心原因是它在「人类可读」和「机器可处理」之间取得了平衡。它不像 Word 那样把格式信息塞进二进制文件,也不像 HTML 那样充满标签噪音。一个.md文件,用记事本打开是干净的纯文本,用渲染器打开是带标题、列表、代码块的排版文档。

正是这个特点,让 Markdown 天然适合与 AI 协作。本地模型读入的是纯文本,不会因为文件格式复杂而丢失上下文;AI 生成的内容也可以直接以 Markdown 语法写回文件,以后人工修改仍然很方便。很多团队把 Markdown 当成「对话中间格式」,因为 LLM 对 Markdown 的理解能力很强,生成的代码块、表格、列表结构都比较稳定。

2.2 本地 AI 的本质

本地 AI 指的是 LLM(大语言模型)运行在你自己电脑或局域网服务器上,而不是调用云端 API。常见的本地推理工具有 Ollama、llama.cpp、LM Studio 等,它们的核心区别在部署方式,但对外提供的服务模式大同小异:启动一个本地 HTTP 服务,接收 prompt,返回补全或生成结果。

本地 AI 和云端 API 的对比可以这样看:

对比维度本地 AI云端 API
数据出口不出本机需要上传到第三方服务器
网络依赖高度离线可用必须联网
算力成本一次性硬件投入按调用量计费
模型规格受本机显存/内存限制可以使用超大模型
部署维护需要自己管理模型和推理服务无需关注基础服务

这里需要澄清一个容易混淆的点:本地 AI 并不等于「高质量 AI」。在同等参数规模下,本地模型通常不如云端大模型能打。但笔记场景里,很多任务并不需要多高深的推理能力,比如「生成 5 条标签」「把一段口语整理成标题列表」「找出一段文字里的待办事项」。这些任务用 7B 乃至 3B 的本地模型就能完成,而且响应速度更快、隐私性更好。

2.3 笔记流程中的 AI 协作模式

本地 AI 在 Markdown 笔记本里可以承担的角色很多,常见有这几种:

  • 内容摘要:把一篇长笔记压缩成几句话,方便以后快速回顾。
  • 格式整理:把杂乱的记录改成规范的 Markdown 结构,比如把列表分层、生成二级标题。
  • 标签推荐:阅读笔记内容后,自动生成几个候选标签,帮你做分类。
  • 问答回顾:针对当前笔记提问,让模型基于笔记内容回答,而不是基于记忆回答。
  • 待办提取:从会议记录、灵感碎片中挑出行动项,生成 checklist。

而 VelocityNote 这类「tiny」工具的定位,通常不会把上面所有能力一次全做出来,而是先跑通「Markdown 文件读写」和「本地 AI 调用」这两个基础能力,再逐步叠加。这也是这篇文章的实践思路:先把最小闭环做出来。

3. 最小可行性架构设计

在你开始写代码之前,需要先理解一个本地 Markdown 笔记应用该分成哪几层。这不是为了过度设计,而是为了让你知道:本地 AI 不是替换掉整个应用,而是作为其中一个服务模块存在

从 VelocityNote 能预判的方向来看,一个最小实现通常包含四层:

  1. 文件存储层:笔记以.md文件形式保存,按文件名或目录组织。这一层最简单,但也是最核心的,因为它决定了数据的长期可迁移性。
  2. 本地 AI 服务层:通过 Ollama 或同类工具跑一个本地模型,暴露 HTTP 接口,负责接收 prompt、生成文本。
  3. 后端 API 层:负责把文件层和 AI 层粘起来。它提供笔记的增删改查接口,也提供summarizetag这类 AI 动作接口。
  4. 前端界面层:不需要复杂渲染引擎,一个能编辑 Markdown、并展示 HTML 预览的网页就够了。

架构图用文字描述就是:浏览器 → 后端 API → 本地文件系统 / 本地模型服务。整个过程没有公网依赖。

技术选型上,我会用 Python + FastAPI 做后端,用系统原生requests调用 Ollama 的 HTTP API,前端用最简单的marked.js在浏览器里把 Markdown 渲染成 HTML。这套方案的好处是依赖少、代码直观、容易看懂。至于生产环境里要不要用 Node.js、Electron 或 Obsidian 插件,那是后话,核心原理一致。

把目录结构固定下来,建议如下:

velocitynote/ ├── main.py ├── requirements.txt ├── static/ │ └── index.html ├── notes/ │ └── welcome.md └── summarize.py

我会在下一节先讲环境准备,再逐个文件展开。

4. 环境准备与前置条件

开始写代码前,需要先准备好三样东西:Python 运行环境、FastAPI 依赖、本地模型服务。版本细节不要照抄任何博客,以你当前安装的为准;重要的是理解每一步在做什么。

4.1 安装 Python 与后端依赖

我建议使用 Python 3.9 及以上版本,因为代码里用了Path类型标注和dict的一些快捷语法。如果你不想在系统环境里装太多东西,可以用虚拟环境:

python3 -m venv venv source venv/bin/activate

然后安装依赖。为了让教程更通用,这里不锁定具体版本,只列出三个必须的包:

# requirements.txt fastapi uvicorn requests

安装命令:

pip install -r requirements.txt

这里fastapi用来提供 HTTP 接口,uvicorn是 FastAPI 的开发服务器,requests用来调用本地 Ollama 服务。

4.2 安装并启动本地模型服务

本地 AI 模型种类很多,Ollama 是其中对开发者友好的选择。安装方式请以 Ollama 官网为准,通常支持 macOS、Linux 和 Windows 的安装包。安装完成后核心命令有两个:

ollama pull qwen2.5:7b
ollama serve

如果你显存不大,建议把qwen2.5:7b换成一个更小的模型,比如qwen2.5:3bllama3.2:3b。模型的选择会影响后续 AI 响应的速度和质量,建议先用小模型跑通流程,再根据需求换大模型。

ollama serve启动后,默认会在本地11434端口提供一个 HTTP 服务。验证它是否正常工作,可以在终端执行:

curl http://localhost:11434/api/generate \ -d '{"model": "qwen2.5:7b", "prompt": "你好", "stream": false}'

如果返回一段 JSON 且包含"response"字段,说明本地模型服务已经就绪。这一步如果失败,后面所有 AI 接口都不会通。

4.3 准备笔记目录

创建一个空目录来存放所有 Markdown 笔记:

mkdir notes echo "# 欢迎使用 VelocityNote" > notes/welcome.md

这个目录就是整个应用的数据层。你可以把它放进 Git 仓库,也可以用坚果云之类的工具做同步。重点是:它是普通文件夹,不是数据库。

5. 核心流程拆解

5.1 步骤一:规划后端接口

一个最小可用的笔记应用,至少需要这几类接口:

  • GET /notes:列出所有笔记。
  • GET /notes/{note_id}:读取一篇笔记的内容。
  • PUT /notes/{note_id}:保存或更新一篇笔记。
  • POST /notes/{note_id}/summarize:调用本地 AI 生成摘要。

这里的note_id我直接设计成文件名(不含.md后缀)。因为家庭级别的笔记应用不需要 UUID,文件名就能很好地标识一篇文章。同时要特别小心:用户传入的文件名不能直接拼到路径里,否则会存在路径穿越风险。最稳妥的做法是用Path(note_id).name取出安全文件名。

5.2 步骤二:实现文件读写

文件读写本身不复杂,但要注意编码统一使用 UTF-8。Markdown 文件如果是中文笔记,必须用 UTF-8 读写,否则会出现乱码。

在 FastAPI 中,我会写两个内部函数read_notewrite_note。它们不是接口,只是文件层的能力,接口层调用它们。

5.3 步骤三:实现本地 AI 调用

调用本地 AI 的关键是了解 Ollama 的 HTTP API。基本请求格式是:

{ "model": "qwen2.5:7b", "prompt": "请总结下面的内容", "stream": false }

其中stream设置为false,表示等模型生成完整结果后再一次性返回。这样做简单,但响应时间会稍长。后续如果你想做流式输出,可以把stream改为true,然后在后端用 SSE 或 WebSocket 推送。

AI 调用模块要设置超时时间。本地模型如果参数偏大或没有 GPU 加速,一次生成可能需要几十秒。如果直接使用默认超时,请求很容易中断。我会把超时设置为 300 秒,并捕获网络异常,返回 502 状态码,这样前端能给出明确提示。

5.4 步骤四:制作前端界面

前端界面不求美观,只求能用。需要具备两个能力:

  1. 左侧 Markdown 编辑框,用于修改笔记内容。
  2. 右侧渲染区,把 Markdown 实时渲染成 HTML。

我会使用marked.js这个开源库来做渲染。它可以通过 CDN 加载,也可以下载到本地static目录,保证离线可用。这里的重点是:Markdown 编辑框的「实时预览」不是 AI 功能,而是通用能力,但配合 AI 摘要后,使用体验会完全不同。

6. 完整示例与代码实现

现在开始写代码。这套代码不能直接覆盖 VelocityNote 的全部能力,但它完整跑通了「Markdown 笔记 + 本地 AI」的最小闭环。

6.1 后端服务 main.py

# 文件路径:velocitynote/main.py from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from pydantic import BaseModel import requests NOTES_DIR = Path("./notes") NOTES_DIR.mkdir(exist_ok=True) app = FastAPI(title="VelocityNote Local API") # 允许本地前端跨域访问 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 静态页面目录 app.mount("/static", StaticFiles(directory="static"), name="static") class Note(BaseModel): content: str class SummaryRequest(BaseModel): model: str = "qwen2.5:7b" prompt: str = "请用中文总结这篇 Markdown 笔记,输出 3-5 条要点。" def _safe_note_path(note_id: str) -> Path: """防止路径穿越:只使用文件名的最后一部分。""" safe_name = Path(note_id).name if not safe_name: raise HTTPException(status_code=400, detail="note_id is empty") return NOTES_DIR / f"{safe_name}.md" def read_note(note_id: str) -> str: path = _safe_note_path(note_id) if not path.exists(): raise HTTPException(status_code=404, detail="note not found") return path.read_text(encoding="utf-8") def write_note(note_id: str, content: str) -> None: path = _safe_note_path(note_id) path.write_text(content, encoding="utf-8") def call_local_llm(prompt: str, model: str) -> str: """调用本地 Ollama 服务,不请求任何公网接口。""" url = "http://localhost:11434/api/generate" payload = { "model": model, "prompt": prompt, "stream": False, } try: resp = requests.post(url, json=payload, timeout=300) resp.raise_for_status() data = resp.json() return data.get("response", "").strip() except requests.exceptions.RequestException as exc: raise HTTPException(status_code=502, detail=f"local AI call failed: {exc}") @app.get("/") def index(): return {"message": "VelocityNote API is running"} @app.get("/notes") def list_notes(): return [p.stem for p in NOTES_DIR.glob("*.md")] @app.get("/notes/{note_id}") def get_note(note_id: str): content = read_note(note_id) return {"id": note_id, "content": content} @app.put("/notes/{note_id}") def save_note(note_id: str, note: Note): write_note(note_id, note.content) return {"id": note_id, "updated": True} @app.post("/notes/{note_id}/summarize") def summarize_note(note_id: str, req: SummaryRequest): content = read_note(note_id) # 限制输入长度,避免本地模型 context 过长 truncated = content[:2000] prompt = f"{req.prompt}\n\n{truncated}" summary = call_local_llm(prompt, req.model) return {"id": note_id, "summary": summary}

这段代码有几点值得说明。_safe_note_path使用Path(note_id).name,可以避免../之类的路径穿越。call_local_llm把 AI 调用封装成一个独立函数,以后想要接入 OpenAI 或者其他服务,只需要改这一个函数。SummaryRequest允许前端指定模型和 prompt,让接口更灵活。

6.2 前端页面 static/index.html

由于是本地工具,前端不追求复杂框架,只用一个 HTML 文件实现编辑和预览。

<!-- 文件路径:velocitynote/static/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>VelocityNote Mini</title> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <style> body { font-family: "Segoe UI", "PingFang SC", sans-serif; margin: 0; display: flex; flex-direction: column; height: 100vh; background: #f5f6fa; color: #333; } header { padding: 16px 24px; background: #ffffff; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 12px; } header input { flex: 1; padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } button { padding: 8px 16px; border: none; border-radius: 6px; background: #4a6cf7; color: #fff; cursor: pointer; font-size: 14px; } button:hover { background: #3a5cd9; } .container { display: flex; flex: 1; overflow: hidden; } .editor, .preview { flex: 1; padding: 24px; overflow-y: auto; } .editor textarea { width: 100%; height: 100%; border: none; outline: none; resize: none; font-family: "JetBrains Mono", Consolas, monospace; font-size: 14px; line-height: 1.8; background: transparent; } .preview { background: #fff; border-left: 1px solid #e0e0e0; } .preview pre { background: #f6f8fa; padding: 12px; border-radius: 6px; overflow-x: auto; } .summary-box { margin-top: 16px; padding: 12px; background: #eef4ff; border-radius: 8px; white-space: pre-wrap; } </style> </head> <body> <header> <strong>VelocityNote</strong> <input id="noteId" placeholder="笔记名称,例如 welcome" value="welcome" /> <button id="saveBtn">保存</button> <button id="loadBtn">加载</button> <button id="summarizeBtn">AI 摘要</button> </header> <div class="container"> <div class="editor"> <textarea id="editor" placeholder="在这里编写 Markdown"></textarea> </div> <div class="preview"> <div id="preview"></div> <div id="summary" class="summary-box" style="display: none;"></div> </div> </div> <script> const editor = document.getElementById('editor'); const preview = document.getElementById('preview'); const summaryBox = document.getElementById('summary'); const noteIdInput = document.getElementById('noteId'); function renderPreview() { preview.innerHTML = marked.parse(editor.value); } editor.addEventListener('input', renderPreview); async function loadNote() { const id = noteIdInput.value.trim(); if (!id) return; const res = await fetch(`/notes/${encodeURIComponent(id)}`); if (!res.ok) { alert('加载失败,请检查笔记是否存在'); return; } const data = await res.json(); editor.value = data.content; summaryBox.style.display = 'none'; renderPreview(); } async function saveNote() { const id = noteIdInput.value.trim(); if (!id) return; const res = await fetch(`/notes/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: editor.value }) }); if (res.ok) { alert('保存成功'); } else { alert('保存失败'); } } async function summarizeNote() { const id = noteIdInput.value.trim(); if (!id) return; summaryBox.style.display = 'block'; summaryBox.textContent = '正在调用本地 AI,请稍候...'; const res = await fetch(`/notes/${encodeURIComponent(id)}/summarize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); if (!res.ok) { summaryBox.textContent = 'AI 摘要失败:' + res.status; return; } const data = await res.json(); summaryBox.textContent = data.summary; } document.getElementById('loadBtn').addEventListener('click', loadNote); document.getElementById('saveBtn').addEventListener('click', saveNote); document.getElementById('summarizeBtn').addEventListener('click', summarizeNote); loadNote(); </script> </body> </html>

这里需要注意一个问题:marked的 CDN 路径会受网络环境影响。如果是在完全离线或内网环境,建议把marked.min.js下载到static目录,再通过<script src="/static/marked.min.js"></script>引入。本地工具的核心价值本来就应该包括离线可用。

6.3 命令行辅助脚本 summarize.py

有时你不想打开浏览器,只想在终端里快速让 AI 总结一篇笔记。可以单独写一个命令行脚本:

# 文件路径:velocitynote/summarize.py import sys import requests OLLAMA_URL = "http://localhost:11434/api/generate" MODEL = "qwen2.5:7b" def summarize_file(file_path: str): with open(file_path, "r", encoding="utf-8") as f: content = f.read()[:2000] prompt = f"请用中文总结以下笔记的要点:\n\n{content}" payload = { "model": MODEL, "prompt": prompt, "stream": False, } resp = requests.post(OLLAMA_URL, json=payload, timeout=300) resp.raise_for_status() data = resp.json() print(data.get("response", "")) if __name__ == "__main__": if len(sys.argv) < 2: print("用法: python summarize.py <markdown文件路径>") sys.exit(1) summarize_file(sys.argv[1])

这个脚本的价值在于:它不依赖后端 API,是一个独立的 AI 工具。你可以把它接到自己常用的编辑器快捷键里,也可以放进 Git 钩子里。这也体现了「本地 Markdown 笔记本」的灵活性——AI 能力不是一个锁在应用里的黑盒,而是一个可以随时调用的服务。

6.4 启动与运行

现在整个项目应该有三个代码文件:requirements.txtmain.pystatic/index.html,还有一个可选脚本summarize.py

在项目根目录先启动本地模型服务。如果 Ollama 还没有在后台运行,执行:

ollama serve

然后启动 FastAPI 后端:

uvicorn main:app --reload --port 8000

启动后,浏览器访问http://localhost:8000/static/index.html。如果看到左侧 Markdown 编辑框、右侧渲染区,说明环境已经搭好。

7. 运行结果与效果验证

7.1 验证保存与加载

先在编辑框里写一行 Markdown:

# 会议记录 - 讨论本地 AI 接入方案 - 确定使用 Ollama - 下一步:完成笔记工具的 MVP

点击「保存」,再清空编辑框,点击「加载」,如果内容恢复,说明文件读写接口正常。

这一步成功意味着你已经拥有一个纯本地、纯 Markdown 的笔记存储系统。哪怕没有 AI,这套体系已经比很多云笔记工具更可控。

7.2 验证 AI 摘要

点击「AI 摘要」,如果一切正常,几分钟内会在预览区出现一条摘要。具体输出取决于模型和 prompt,例如:

1. 这是一个关于本地 AI 接入方案的会议记录。 2. 团队决定使用 Ollama 作为推理服务。 3. 下一步任务是完成笔记工具 MVP。

如果这一步成功,说明本地模型已经能读取你的 Markdown 内容,并且返回结构化文本。你可以通过修改SummaryRequest里的prompt来调整输出风格,比如让它输出 JSON、生成标签、提取待办。

7.3 如果失败,怎么判断问题出在哪一层

这个问题很重要。AI 报错不一定是你代码的问题,很多坑在模型层。建议按下面顺序排查:

  1. 后端是否启动:访问http://localhost:8000,看是否有 JSON 返回。
  2. 静态页面是否加载:访问http://localhost:8000/static/index.html,确认页面能打开。
  3. 本地 AI 服务是否在监听:执行curl http://localhost:11434/api/tags,应该能看到模型列表。
  4. 模型是否已下载:执行ollama list,确认模型名称和调用代码里一致。
  5. 显存是否足够:如果模型过大,ollama serve的控制台会出现显存不足或内存不足的报错。

8. 常见问题与排查思路

问题现象可能原因排查方式解决方案
前端页面访问不了FastAPI 静态目录配置错误检查static目录和main.py是否在同一级确认app.mount("/static", StaticFiles(directory="static"), name="static")中目录存在
保存笔记后内容变乱码文件编码不是 UTF-8用编辑器或file命令查看文件编码统一使用encoding="utf-8"读写文件
AI 请求返回 502本地模型服务未启动或模型名错误curl http://localhost:11434/api/tags查看模型列表启动 Ollama 服务,并修正model字段
AI 响应太慢模型参数过大或没有 GPU 加速查看 CPU/GPU 占用换更小模型,或调低 context 长度
页面能打开但没有 Markdown 渲染marked.js加载失败打开浏览器开发者工具查看网络请求marked.min.js下载到本地并替换 CDN 地址
接口返回 404note_id文件名不存在查看notes目录下有哪些文件先创建笔记文件,或调用PUT /notes/{note_id}保存新笔记
调用 URL 出现路径穿越风险文件名包含../检查终端请求参数_safe_note_path使用Path(note_id).name已验证

这里特别想提醒一点:本地 AI 不等于零成本。第 3 行的「模型名错误」是新手最容易踩的坑。很多人从网上抄了一段代码,里面的模型名是llama2,但自己本地只下载了qwen2.5,结果请求一直失败。排查时要记得先敲一遍ollama list

9. 最佳实践与工程建议

9.1 安全边界:不要把本地模型服务暴露到公网

这个项目里 Ollama 默认监听localhost:11434,只允许本机访问,这是安全默认值。如果你出于远程访问需求,把服务绑定到0.0.0.0,一定要确认局域网可信,并且加上认证层,否则任何局域网设备都可以无限制消耗你的算力,甚至读取你传过去的内容。更稳的做法是永远不加--host 0.0.0.0,需要通过 Web 远程访问时,由后端 API 做转发和鉴权。

9.2 文件命名与目录规划

笔记工具最怕的是文件多了以后难以管理。建议在notes目录下按「主题/日期」建立子目录,例如:

notes/ ├── projects/ │ └── velocitynote.md ├── daily/ │ └── 2025-01-01.md └── inbox/ └── idea.md

后端接口如果要支持子目录,需要把_safe_note_path里的Path(note_id).name改成「只允许相对路径 + 白名单校验」。一个简单方案是:把note_id转换成Path后,使用resolve()判断是否仍然位于notes根目录下,否则拒绝。这个改进在文件数量增多后非常有必要。

9.3 模型选择与 prompt 设计

本地模型的选择直接决定体验。如果你 90% 的笔记都是中文,建议使用在中英文混合场景表现更好的模型,比如 Qwen 系列。如果英文为主,Llama 3.2 系列也能胜任。参数规模上,16GB 内存的笔记本跑 7B 模型勉强可以,如果同时开浏览器和其他应用,建议降到 3B。

prompt 设计同样重要。不要只写「总结这段文字」,而是给出格式约束,比如:

请把下面的笔记整理成 3 个要点,每个要点不超过 20 个字,使用 Markdown 无序列表输出。

这样模型的输出更容易被程序直接解析。将来如果你想在笔记里通过特殊标记嵌入 AI 命令,比如<!-- ai: summarize -->,也可以在这里做统一的 prompt 映射。

9.4 数据备份与同步

Markdown 文件放到notes目录后,最舒服的备份方式是 Git。每改一次笔记就是一个 commit,既可以回溯历史版本,也能推送到私有 Git 仓库同步到其他设备。要注意的是:不要把这个目录直接塞进一个大的 Git 仓库,因为笔记文件会快速增长。建议单独初始化一个仓库,或者用git submodule管理。

9.5 从「能跑」到「好用」的三个扩展方向

如果你准备把这个小工具做成自己每天会用的版本,有三个方向值得优先考虑:

  1. 全文检索:笔记多起来后,GET /notes变成列表已经没有意义。可以用 SQLite FTS5 建一个全文索引,每次写入笔记时更新。
  2. 标签系统:在 Markdown 文件的 front-matter 里加 tags 字段,然后让 AI 在摘要接口里同时生成标签,这样能组合出轻量知识库。
  3. 流式输出:当前 AI 摘要是一次性返回,等待时间长。可以把 Ollama 的stream设置为true,后端用 SSE 逐字推给前端,体验会好很多。

这三个方向里,流式输出对前端改动最小,也最直观,建议下一个版本先做它。

10. 总结与后续学习方向

到这里,你已经有了一个可以运行的「纯本地 Markdown 笔记 + 本地 AI 摘要」工具。虽然它不叫 VelocityNote,但核心思路是同一类:笔记内容永远是普通 Markdown 文件,AI 能力只是挂载在本地的一个服务。你不用担心数据被上传,不用担心格式锁定,也不用因为这个 AI 功能而被迫订阅某个云服务。

回顾这篇文章,真正讲清楚的东西有三点:

  1. Markdown 作为笔记存储格式为什么适合做本地优先工具——它是纯文本,可以被任何编辑器读取,也能被本地模型直接处理。
  2. 本地 AI 不是一个完整的笔记应用,而是一个 HTTP 服务模块。你只需要通过 API 调用它,就能给笔记加上摘要、标签、问答等能力。
  3. 一个最小闭环应该包含文件存储层、AI 服务层、后端 API 层和前端界面层。这个结构不复杂,但已经能支撑日常个人使用。

下一步的实践建议是:先别急着加功能,把上文代码跑通,然后真实使用一周。每天用这个网页记录技术、日记或会议内容,你会很快发现它缺什么。也许缺搜索,也许缺编辑器体验,也许缺更好的 AI prompt。那时候再动手改,方向会清晰很多。

如果你想深入,可以继续研究这几个方向:Ollama 的流式输出与 SSE 实现、嵌入式向量模型做语义搜索、用 front-matter 给 Markdown 文件增加元信息、把 FastAPI 打包成桌面应用。等到你把这些问题一个个解决,你就拥有了一个真正符合自己习惯的 VelocityNote。

建议收藏这篇文章,等你开始动手搭建时,可以直接对照文中的代码和排查表来操作。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 5:28:55

国内四向车厂家供应现状:产能、交付与选型参考

四向穿梭车从概念产品到规模交付&#xff0c;中间隔着一条完整的供应链。不少采购方在项目启动时关注的第一个问题就是&#xff1a;国内四向车厂家供应能力怎么样&#xff1f;交付周期多长&#xff1f;核心组件是自产还是外购&#xff1f;这些问题的答案直接影响项目时间表和后…

作者头像 李华
网站建设 2026/9/3 5:28:52

国内四向车品牌评测:五家本土厂商综合实力排名

智能仓储升级热潮之下&#xff0c;国内四向穿梭车品牌已经形成相当成熟的竞争格局。从专注型制造商到机器人平台厂商&#xff0c;不同类型的品牌各有侧重。对于采购方来说&#xff0c;面对"国内四向车品牌选哪个"这个问题&#xff0c;最需要的不是厂商宣传册上的参数…

作者头像 李华
网站建设 2026/9/3 5:27:42

干预感知临床世界模型:心脏术后结局预测的工程化落地

心脏外科或介入术后&#xff0c;患者接下来几天会不会出问题——比如术后心梗、恶性心律失常、急性肾损伤、计划外再入院——本质上是一个多步动态预测问题&#xff0c;而不是简单的“查表打分”。这几年 World Model&#xff08;世界模型&#xff09;在自动驾驶、游戏智能体里…

作者头像 李华
网站建设 2026/9/3 5:26:22

MatCont实战:非线性动力系统分岔分析从入门到精通

简介&#xff1a;本资源是面向数学建模、非线性动力系统研究及工程仿真领域的科研人员与高年级研究生的MATLAB专业工具箱——MatCont7p1&#xff0c;专用于常微分方程&#xff08;ODE&#xff09;与离散映射的分岔分析。它解决动态系统中参数变化引发的稳定性突变、周期解分支、…

作者头像 李华
网站建设 2026/9/3 5:26:20

Lovart国内直通上线!可以来体验这款甜品“设计秘书”

Lovart 国内版上线&#xff01;Skills、MCP 连接器全来了&#xff0c;这一次 AI 设计门槛真的被踏平了 最近 AI 设计圈炸了。 不是跑图模型又卷出了新版本&#xff0c;而是一个「会自己设计」的 Agent —— Lovart&#xff0c;正式在国内上线了。 之前不少同学只能在海外站绕来…

作者头像 李华
网站建设 2026/9/3 5:26:07

Aspen Plus流程模拟在二甲醚羰基化合成乙酸甲酯中的应用与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华