三个月前,我做的本地 AI 学习软件终于能稳定跑起来了。这个小工具说来也不复杂:基于 Python 和 Flask 的一套本地网页应用,底层调用 Ollama 加载开源大模型,所有推理都在自己的电脑上完成,不需要联网调任何云端接口。说白了,它就是一个“完全属于自己的 AI 助教”,资料放进来,模型在自己机器上算,聊天记录、训练数据全都留在本地。项目本身是免费开源的,代码量不大,每个文件都能用“一张纸讲清楚”,所以也很适合当作本地 AI 应用的基础模板来研究。
我平时是这样用它的:备课时丢两页 PDF 进去,让模型先列出核心术语;遇到教材里说不清楚的概念,追问时要求模型先回到原文再回答,不许张口就来;期末复习阶段,把错题导进去,让它按知识点生成同类练习题。用过云端 AI 的人可能觉得这也没什么稀奇,但关键在于:我不用把课堂名单、学生作业、内部讲义传到任何人的服务器上,模型也不知道我是谁。冲着这一点,它就值得单独写一篇完整的实操分享。
这篇文章既写给想直接“抄作业”的人,也写给准备自己做本地 AI 工具的同学。我会把设计思路、技术选型、完整搭建过程、调优踩坑全部摊开讲,你可以照着敲,也可以只挑自己需要的章节看。
1. 为什么我要做“本地运行”的 AI 学习软件
先聊点动机层面的东西。我见过不少想做 AI 学习工具的人,第一反应都是“套一个 OpenAI 的接口就完事了”。这个方案确实快,但是用在教育场景里,我心里始终有几个过不去的坎。
1.1 云端学习工具的三个老问题
第一个是隐私。学生笔记、教案、论文初稿这些东西,本质上都属于“半公开但不希望被收集”的资料。我之前用某款云端笔记 AI,上传一段备课内容后,过两天发现推荐流里出现了风格极其相似的内容,虽然不能确定是巧合还是训练数据串了,但那种“资料被拿去喂模型”的感觉非常不舒服。学习场景对隐私的敏感度,比很多人想象得高。
第二个是稳定性和成本。云端接口按 token 计费,聊一次长文档可能几毛钱,看起来不贵,但真到了学期末集中复习,每天几十次提问,账单很快就不耐看了。而且一旦服务商调整接口、限流、停服,整个工具就瘫了。我见过有人上午还在给学生演示 AI 辅助学习,下午接口报错,课都没法上。
第三个是定制空间太窄。云端模型你只能用别人封装好的功能,想做“从原文出发”“不猜测教材内容”“按教学大纲重新组织答案”这类定制,几乎不可能。聪明一点的用户会写长 prompt,但 system 层的东西依然捏在平台手里。
1.2 本地运行反而更实在
我一开始也担心本地运行会有性能瓶颈,但这两年开源模型的进展比我预期快得多。现在一台普通的 16GB 内存笔记本,跑一个 7B 参数的量化模型,已经可以做到每秒输出六到十个字,应付日常问答和资料总结足够。更关键的是,本地运行带来了几个云服务给不了的特性:
- 断网可用。宿舍没网、高铁上、图书馆弱网环境,我一样能查资料、问问题,这对学生党是真香。
- 零增量成本。模型参数已经下到本地,问多少次都只花电费,没有按 token 计费的心理负担。
- 完全可定制。prompt、上下文逻辑、界面、导出格式,全部自己说了算,想怎么改都行。
- 数据不出设备。我自己的资料、学生的错题集只存在于这台电脑的硬盘里,没有第三方能看到。
有人会问,本地模型能力是不是比云端弱很多?这个确实要看任务。复杂创意写作、多语言长文翻译,本地小模型拼不过云端大模型;但学习场景里大量需求是“基于给定资料解释概念、提炼要点、生成练习”,这些恰恰是 7B 级别模型的强项。模型小不代表没用,关键是把任务范围设计好。
1.3 技术栈为什么长这样:Python + Flask + SQLite + Ollama
先说背景。在这套学习软件之前,我先用 Flask 做过一个轻量级网页端练手项目,功能是校园失物招领的发布和匹配,核心是用关键词相似度算法把“遗失物”和“招领物”自动对上,数据存进轻量化数据库。那个项目让我对 Flask 的轻量模型很有好感,一个小团队或个人完全能驾驭,不复杂、不拖沓。
所以这次做本地 AI 学习软件,我几乎原封不动把网页框架的经验迁移了过来,把“关键词匹配”升级成“语义理解和生成”。技术栈就四样:
- Python:生态最成熟,AI 相关工具链几乎没有 Python 搞不定的,写起来快。
- Flask:极简 Web 框架,没有 Django 那么重的规范,个人项目拿着不累。
- SQLite:轻量数据库,比 MySQL 省事,本地单机场景足够用,零运维。
- Ollama:本地模型运行和调度工具,负责加载开源模型、提供 HTTP API,帮我把“跑模型”这件麻烦事封装好了。
选 Ollama 而不是直接用 llama.cpp 或 transformers,是因为 Ollama 把量化、内存管理、模型常驻这些底层细节都处理好了,我可以用十分钟内上手。对普通用户来说,不需要理解 CUDA 内核也能跑起来,这一点很重要。
2. 功能设计与实现思路拆解
明确了技术栈之后,要回答的不是“能做什么”,而是“该做什么”。本地 AI 学习软件如果只是做一个聊天框,那和直接命令行里跑模型没区别,所以我在功能上做了一轮取舍。
2.1 核心功能与背后的提示词策略
我保留了六个实用功能,每一个都对应一个明确的学习场景:
| 功能 | 典型场景 | 实现策略 |
|---|---|---|
| 知识问答 | 学生问教材概念 | 低温度,先看资料原文再作答 |
| 长文摘要 | 浓缩复习资料 | 分块总结后合并,避免上下文溢出 |
| 术语解释 | 名词看不懂 | 先下定义,再用一句话举例 |
| 翻译与润色 | 外文文献阅读 | 术语对照表限定,防止乱翻 |
| 错题变式题生成 | 期末刷题 | 分析错因后生成同类变体 |
| 学习计划拆解 | 考前突击 | 按剩余天数倒排任务目标 |
功能少一点没关系,关键是每个都能做扎实。比如“术语解释”这个功能,我专门设计了提示词:要求模型先下定义,再用学习场景中的例子解释,最后给一个记忆口诀。这一套结构下来,模型输出的内容比直接问“解释一下贝叶斯定理”要规整得多,学生拿过去就能背。
2.2 上下文池和会话管理:不能让 AI 失忆
学习场景里有个特殊需求——学生提问往往是有连续性的。上一句问“什么是梯度下降”,下一句问“那它和最小二乘法有什么区别”,如果模型只看到第二句,很容易答偏。所以我在后端维护了一个会话池。
具体逻辑是这样:前端把问题发给 Flask,Flask 从 SQLite 里取出这个 session 最近的六轮对话,组成 messages 数组,再交给 Ollama。只取最近六轮是有原因的——本地模型上下文窗口有限,塞太多历史既拖慢速度,又可能让注意力涣散。六轮刚好覆盖一个持续讨论的链条,又不会让内存吃不消。
会话数据存在 SQLite 里,表结构非常简单,只有 session_id、role、content、created_at 这几个字段。用户在新一轮学习后可以清理会话记录,把数据库备份带走,这个我们后面实操部分再细说。
2.3 免费开源的边界:什么能开源,什么不能
既然打了免费开源的旗号,就必须把边界说清楚。项目本体代码用的是 MIT 许可证,意味着你可以随便改、随便用、甚至商用。但要注意,项目依赖的模型权重不在开源代码范围内——Ollama 仓库里的模型各有各的开源协议,比如 Qwen 系列和 Llama 系列都允许商用,但如果你想换个更严格的模型,就要自己去看对应协议。
所以我在项目里做了一个很清晰的区隔:仓库里只放源码、prompt 模板、文档和示例数据,不托管任何模型文件。用户部署时自己去拉模型,这样既避免了仓库体积爆炸,也规避了许可证混淆。贡献者也约定了一条规则:凡是涉及模型本身的问题,一律去模型仓库提 issue,不要污染本项目的主线。
这个边界一开始就要讲清楚,否则开源项目维护起来会非常累。
3. 完整实操:从零搭一套本地 AI 学习助手
这部分我按顺序走一遍,从装机环境一直到页面聊天。建议你跟着操作,大概三十分钟可以跑通。
3.1 环境准备:Python、Ollama、模型下载
基础环境是 Python 3.10 以上,系统可以是 Windows、macOS 或 Linux。先建一个干净的虚拟环境,避免依赖打架。
mkdir ai-tutor cd ai-tutor python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install flask requests接着安装 Ollama。你可以去官网下载对应系统的安装包,装完终端里执行ollama --version确认。然后拉一个适合学习场景的中文模型,我推荐 qwen2.5:7b-instruct 的 4-bit 量化版,质量和资源占用平衡得不错。
ollama pull qwen2.5:7b-instruct-q4_K_M拉完确认服务已经在后台运行,默认端口是 11434。执行curl http://127.0.0.1:11434/api/tags,能看到模型列表,说明 Ollama 的 API 已经就绪了。
3.2 后端核心代码:用 Flask 封装本地模型调用
接下来是最核心的部分:一个 /api/chat 接口,接收用户消息,拼接历史,转给 Ollama,再把结果返回前端。
import requests from flask import Flask, request, jsonify OLLAMA_ENDPOINT = "http://127.0.0.1:11434/api/chat" app = Flask(__name__) SYSTEM_PROMPT = """你是一名严谨的本地学习助手。请始终使用中文回答。 要求: 1. 如果用户给过资料,先从资料中找依据,再回答; 2. 如果不确定,明确说“根据现有资料无法确认”; 3. 回答控制在 400 字以内,优先用分点和短段落; 4. 禁止胡编教材里不存在的结论。""" @app.route("/api/chat", methods=["POST"]) def chat(): data = request.get_json(force=True) user_msg = data.get("message", "") history = data.get("history", [])[-6:] messages = [{"role": "system", "content": SYSTEM_PROMPT}] messages.extend(history) messages.append({"role": "user", "content": user_msg}) payload = { "model": "qwen2.5:7b-instruct-q4_K_M", "messages": messages, "stream": False, "options": { "temperature": 0.3, "num_predict": 1024, "num_ctx": 4096 }, } try: r = requests.post(OLLAMA_ENDPOINT, json=payload, timeout=300) r.raise_for_status() reply = r.json().get("message", {}).get("content", "") return jsonify({"reply": reply}) except Exception as e: return jsonify({"error": f"本地模型调用失败: {e}"}), 502 if __name__ == "__main__": app.run(host="127.0.0.1", port=5000, debug=True)这段代码里有几个关键参数要解释一下。temperature 设成 0.3,是为了让回答更稳定、更接近事实,学习场景不需要太多发散;num_ctx 是模型上下文窗口,设 4096 意味着模型一次最多能“看到”约四千个 token,再长就只能截断;num_predict 控制单次回答的最大长度,1024 个 token 对学习问题来说已经够用。
注意我只监听 127.0.0.1,不用 0.0.0.0。这是故意的——本地学习助手只服务本机用户,没必要暴露到局域网。暴露出去反而引入安全风险,比如局域网里的其他人可以借用你的模型算力,甚至通过对话接口消耗你的内存。
3.3 前端和数据库:把页面搭得像聊天软件
后端接口有了,前端我用了最简单的方式:一个静态 HTML 页面,配上一点 CSS 和 JavaScript,通过 fetch 调用 /api/chat。没有引入 Vue、React 这类框架,因为功能简单,引入框架反而增加学习和维护成本。
前端核心逻辑只有一小段:
async function sendMessage() { const msg = document.getElementById("input").value; const history = chatHistory; // 保存最近的会话历史 const resp = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: msg, history: history }) }); const data = await resp.json(); appendMessage("assistant", data.reply); }聊天历史我用 JavaScript 数组存,同时同步写一份到 SQLite。数据库表结构这样建:
CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );session_id 用来区分不同学习主题,比如“高数期末”和“英语四级”各开一个会话,互不干扰。这样能让模型在特定主题下上下文更集中,不会把两个话题混在一起。
3.4 把完整流程跑起来,第一次提问
回到终端,分两步启动。先确认 Ollama 在跑,再启动 Flask 应用。
ollama serve # 另开一个终端,进入虚拟环境 flask --app app run --port 5000浏览器打开http://127.0.0.1:5000,输入“用一句话解释什么是马尔可夫链”,正常情况下几秒内就能看到回答。如果页面转了半天没回应,多半是模型第一次加载比较慢,或者内存不够导致 swap 严重。此时可以打开终端的 Flask 日志,能看到每次请求的耗时。
第一次完整跑通后,你会发现自己拥有了一个从“模型加载”到“对话界面”全链路可控的本地 AI 工具。那种感觉,和调用别人封装好的黑盒 API 完全不同。
4. 实测过程中踩到的坑与调优记录
真正的干货都在踩坑里。这部分是我连着用了一个学期,修修补补总结出来的,每条都能省你半天时间。
4.1 没有独显的设备能不能跑
很多学生问的第一句话是:“我笔记本没有独立显卡,能跑吗?”答案是能,但要有合理的预期。我整理了一张常见硬件的实测参考:
| 硬件环境 | 推荐模型 | 量化方案 | 预计速度 | 内存占用 |
|---|---|---|---|---|
| 8GB 内存纯 CPU | qwen2.5:3b | Q4_K_M | 8-12 token/s | 3-4 GB |
| 16GB 内存纯 CPU | qwen2.5:7b | Q4_K_M | 6-10 token/s | 5-7 GB |
| M1/M2 Mac | qwen2.5:7b | Q4_K_M | 18-25 token/s | 5 GB 左右 |
| 8GB 显存 GPU | qwen2.5:7b | Q4_K_M | 40+ token/s | 显存占用约 6 GB |
注意这些数字是近似值,浮动范围很大。纯 CPU 跑 7B 模型,回答 400 字大约需要 40 到 60 秒,属于“能忍但不流畅”的级别。如果你的设备只有 8GB 内存,建议老老实实换 3B 模型,流畅度比参数大小更影响实际使用欲望。
还有一个建议:别追求宝塔式参数堆叠。本地学习助手要的是“够用”,不是“跑分”。7B 模型在一个明确的 prompt 框架下,对教材级内容的理解能力已经足够好,强行上 14B 只会让你的电脑风扇起飞。
4.2 中文回答质量差,问题到底出在哪
刚开始跑的时候,中文回答经常“说不利索”,要么夹英文,要么用词别扭。我排查了一圈,发现原因不在模型本身,而在三个细节上。
第一是系统提示词没有写清楚。默认不写的话,模型可能根据自己的偏好输出。我后来在 SYSTEM_PROMPT 里明确写了“请始终使用中文回答”,效果立竿见影。第二是温度太高。默认的 0.7 输出很“飘”,学习场景下把 temperature 调到 0.2 到 0.4 之间,回答会老实很多。第三是模型本身在两门语言之间切换导致混用,稍微多一点约束就能把它按在中文轨道上。
如果你用的模型对中文支持本身一般,还有一个土办法:在提示词末尾加一句“如果某个术语没有标准中文译名,请保留英文并在括号里注明中文含义”。这样既不影响术语准确性,又能降低中英混杂的频率。
4.3 输入资料太长,内存直接爆掉怎么办
教材章节动辄几万字,直接丢进上下文会触发两个问题:Ollama 截断上下文,或者内存占用飙升。我最初把一整章 PDF 灌进去,结果程序直接卡死,Task Manager 里看到内存占用 8GB 以上。
后来我改成“分块摘要 + 合并总结”的做法,原理很像论文里的 map-reduce:先把长文本切成小块,逐块让模型生成要点,再把所有要点拼起来做一轮总结。分块代码很简单:
def split_text(text, chunk_size=800, overlap=100): texts = [] start = 0 while start < len(text): end = min(start + chunk_size, len(text)) texts.append(text[start:end]) if end == len(text): break start = end - overlap return texts注意 overlap 参数。相邻块之间留一百个字符的重叠,是为了避免重要内容恰好被切断。比如一句话前半段在前一块末尾、后半段在后一块开头,有了重叠,模型两轮滑动窗口都能看到完整语义。
实践下来,先让每个分块输出 3 到 5 条要点,再让模型汇总,效果比直接塞整篇好太多。虽然代码多绕了一步,但换来的是学习资料的完整性。
4.4 端口占用、模型加载慢这类“小事”该怎么处理
最后说几个看起来很不起眼、但经常耽误事的杂项问题。
端口冲突。Ollama 默认占用 11434,如果别的程序抢了这个端口,API 就调不通。排查命令是lsof -i :11434(Windows 用netstat -ano | findstr 11434),找到占用进程后换个端口,或者改 Ollama 的配置。Flask 默认 5000 端口同理。
模型加载慢。每次对话如果都要等几十秒,大概率是模型没有常驻内存,每个请求都在重新加载。Ollama 有一个 keep_alive 参数,默认会把模型留在内存里一段时间,但如果它被系统回收了,就会重新加载。在项目里不要频繁用ollama run手动占着,它会打乱 keep_alive 的节奏。把启动和调用分离,Flask 启动后先发一个预热请求,让模型常驻,后续响应就快了。
请求超时。我最初把 timeout 写的是 120 秒,结果 CPU 机器上跑长文档偶尔会超时。后来统一改成 300 秒,不会再因为慢一点就被误杀。
5. 用了一学期后的真实体会
工具做到能稳定用之后,我开始观察学生们对它的实际使用方式,有几个现象很有意思。
第一个发现是,学生更愿意在本地工具里问“傻问题”。用云端工具时,他们可能是担心问题太简单被记录,也可能是怕被平台的其他用户看到,总之提问会比较收敛。本地工具没有这层心理负担,问的问题很底层、很基础,这恰恰是学习里最需要的部分。没有后顾之忧的提问,才能真正暴露理解上的空洞。
第二个发现是,回答的结构比回答的内容更重要。那些把知识点拆成“定义 + 例子 + 注意事项”的回答,学生留存率明显更高。这也印证了一件事:本地模型能力够不够,很大程度上取决于你 prompt 结构好不好。同一台电脑,同一个模型,换一套组织方式,效果可能差一个档次。
第三个发现,也是最意外的一个,本地工具让人更珍惜自己的资料。因为所有数据都在本地,学生在导入笔记前会整理格式、清理无效信息,这个动作本身就是学习。当一个工具能让你的数据“留在身边”,你对待学习资料的态度会变得更认真。
现在这个项目已经开源,也有不少朋友在上面提交过代码,改了 prompt,做了更细的语言包。我个人最大的体会是:本地 AI 学习软件不是一个惊天动地的技术发明,它只是把“人工智能辅助学习”这件事重新放回个人手里。如果你也想做一个类似的工具,我的建议是从最小的场景切入,先跑通,再慢慢优化。学习的壁垒不在模型大小,而在你怎么设计那个“从问题到答案”的过程。