
1. 这篇文章真正要解决的问题如果你平时用 Markdown 记录技术笔记、工作日志或产品想法最近一定被各种「AI 笔记工具」的宣传刷过屏。它们普遍能做两件事第一把零散的文字整理成结构化内容第二帮你润色、补全、摘要。但很多工具有一个共同的前提——你把笔记内容上传到了云端。对于技术文档、私有化项目记录、甚至只是个人日记来说这个前提本身就是一种顾虑。VelocityNote 这个项目无论最终实现成什么样它把两个关键词放在了一起tiny Markdown notebook和local AI。与其说它是一款产品不如说它代表了一类更值得关注的开发方向笔记正文仍然用最朴素、最通用的 Markdown 文件保存所有 AI 能力都在本机完成。它不依赖云服务、不锁定格式、不强制你改变工作方式。这个方向解决了三类问题隐私与可控笔记是纯文本保存在你自己的目录里。AI 请求也发往本机模型服务内容不出机器。格式自由Markdown 是纯文本可以使用任何编辑器打开。将来想换工具复制文件即可不需要导出。成本与离线可用本地模型只需要一次性的硬件投入没有人按 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 能预判的方向来看一个最小实现通常包含四层文件存储层笔记以.md文件形式保存按文件名或目录组织。这一层最简单但也是最核心的因为它决定了数据的长期可迁移性。本地 AI 服务层通过 Ollama 或同类工具跑一个本地模型暴露 HTTP 接口负责接收 prompt、生成文本。后端 API 层负责把文件层和 AI 层粘起来。它提供笔记的增删改查接口也提供summarize、tag这类 AI 动作接口。前端界面层不需要复杂渲染引擎一个能编辑 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:7bollama serve如果你显存不大建议把qwen2.5:7b换成一个更小的模型比如qwen2.5:3b或llama3.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_note和write_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 步骤四制作前端界面前端界面不求美观只求能用。需要具备两个能力左侧 Markdown 编辑框用于修改笔记内容。右侧渲染区把 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_okTrue) app FastAPI(titleVelocityNote Local API) # 允许本地前端跨域访问 app.add_middleware( CORSMiddleware, allow_origins[*], allow_credentialsTrue, allow_methods[*], allow_headers[*], ) # 静态页面目录 app.mount(/static, StaticFiles(directorystatic), namestatic) 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_code400, detailnote_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_code404, detailnote not found) return path.read_text(encodingutf-8) def write_note(note_id: str, content: str) - None: path _safe_note_path(note_id) path.write_text(content, encodingutf-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, jsonpayload, timeout300) resp.raise_for_status() data resp.json() return data.get(response, ).strip() except requests.exceptions.RequestException as exc: raise HTTPException(status_code502, detailflocal 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 langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleVelocityNote Mini/title script srchttps://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 strongVelocityNote/strong input idnoteId placeholder笔记名称例如 welcome valuewelcome / button idsaveBtn保存/button button idloadBtn加载/button button idsummarizeBtnAI 摘要/button /header div classcontainer div classeditor textarea ideditor placeholder在这里编写 Markdown/textarea /div div classpreview div idpreview/div div idsummary classsummary-box styledisplay: 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, encodingutf-8) as f: content f.read()[:2000] prompt f请用中文总结以下笔记的要点\n\n{content} payload { model: MODEL, prompt: prompt, stream: False, } resp requests.post(OLLAMA_URL, jsonpayload, timeout300) 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.txt、main.py、static/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 报错不一定是你代码的问题很多坑在模型层。建议按下面顺序排查后端是否启动访问http://localhost:8000看是否有 JSON 返回。静态页面是否加载访问http://localhost:8000/static/index.html确认页面能打开。本地 AI 服务是否在监听执行curl http://localhost:11434/api/tags应该能看到模型列表。模型是否已下载执行ollama list确认模型名称和调用代码里一致。显存是否足够如果模型过大ollama serve的控制台会出现显存不足或内存不足的报错。8. 常见问题与排查思路问题现象可能原因排查方式解决方案前端页面访问不了FastAPI 静态目录配置错误检查static目录和main.py是否在同一级确认app.mount(/static, StaticFiles(directorystatic), namestatic)中目录存在保存笔记后内容变乱码文件编码不是 UTF-8用编辑器或file命令查看文件编码统一使用encodingutf-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 从「能跑」到「好用」的三个扩展方向如果你准备把这个小工具做成自己每天会用的版本有三个方向值得优先考虑全文检索笔记多起来后GET /notes变成列表已经没有意义。可以用 SQLite FTS5 建一个全文索引每次写入笔记时更新。标签系统在 Markdown 文件的 front-matter 里加 tags 字段然后让 AI 在摘要接口里同时生成标签这样能组合出轻量知识库。流式输出当前 AI 摘要是一次性返回等待时间长。可以把 Ollama 的stream设置为true后端用 SSE 逐字推给前端体验会好很多。这三个方向里流式输出对前端改动最小也最直观建议下一个版本先做它。10. 总结与后续学习方向到这里你已经有了一个可以运行的「纯本地 Markdown 笔记 本地 AI 摘要」工具。虽然它不叫 VelocityNote但核心思路是同一类笔记内容永远是普通 Markdown 文件AI 能力只是挂载在本地的一个服务。你不用担心数据被上传不用担心格式锁定也不用因为这个 AI 功能而被迫订阅某个云服务。回顾这篇文章真正讲清楚的东西有三点Markdown 作为笔记存储格式为什么适合做本地优先工具——它是纯文本可以被任何编辑器读取也能被本地模型直接处理。本地 AI 不是一个完整的笔记应用而是一个 HTTP 服务模块。你只需要通过 API 调用它就能给笔记加上摘要、标签、问答等能力。一个最小闭环应该包含文件存储层、AI 服务层、后端 API 层和前端界面层。这个结构不复杂但已经能支撑日常个人使用。下一步的实践建议是先别急着加功能把上文代码跑通然后真实使用一周。每天用这个网页记录技术、日记或会议内容你会很快发现它缺什么。也许缺搜索也许缺编辑器体验也许缺更好的 AI prompt。那时候再动手改方向会清晰很多。如果你想深入可以继续研究这几个方向Ollama 的流式输出与 SSE 实现、嵌入式向量模型做语义搜索、用 front-matter 给 Markdown 文件增加元信息、把 FastAPI 打包成桌面应用。等到你把这些问题一个个解决你就拥有了一个真正符合自己习惯的 VelocityNote。建议收藏这篇文章等你开始动手搭建时可以直接对照文中的代码和排查表来操作。