本地搭建大语言模型:从"手动三步"到 Ollama 接入
模型不是双击就能运行的软件。本文先拆解在不依赖第三方工具的前提下,本地跑通一个大模型的完整链路;再介绍用 Ollama 将这套流程"一键化"的本地接入方案,并与远程接入方式做对比。
一、不用第三方工具:本地跑通大模型要过三关
第 1 关:下载模型
去哪里下? 主流渠道如下:
| 下载渠道 | 地址 |
|---|---|
| Hugging Face Hub | https://huggingface.co/ |
| ModelScope(魔搭) | ModelScope - 模型列表页 |
| 模型官方仓库 | 以模型官方发布页为准 |
怎么选型? 结合自己的任务需求 与硬件环境(内存 / 显存)挑选合适的模型与规格。
一个关键认知: 模型下载到本地后并不能直接运行 。模型不是可执行程序,下载模型实际上是下载了一堆文件------权重分片、配置与校验信息等,如图 1。
📌 插图 1:模型目录下的文件列表(sha256 命名的权重分片等)|此处替换为你的截图
第 2 关:准备推理引擎
推理引擎本质是一个专门的模型执行器,负责:
- 加载模型权重
- 处理输入文本
- 执行数学计算
- 生成输出文本
常见的开源推理引擎:llama.cpp 、Transformer。
第 3 关:编写加载 / 推理代码
最后还需要自己实现一个简单的 Python / C++ 程序,完成模型的加载与交互调用。
小结: 下载模型 → 推理引擎 → 交互代码,三环相扣、缺一不可。流程不复杂但足够琐碎------于是有了更省心的方案:Ollama。
二、Ollama:把三步封装成一条命令
- 下载并安装 Ollama 工具;
- 拉取对应模型,例如:
ollama pull deepseek-r1:1.5b; - 在代码中实现接入程序 LLMManager,由它负责与本地模型交互。
三、两种接入方式:远程 vs 本地
方式 A:远程接入 DeepSeek
本机只负责"发请求",模型运行在官方服务器上:
- 本机 LLMManager(发送请求的代码)通过网络向 DeepSeek 官方服务器发起请求;
- 服务器上的 deepseekServer 运行对应模型,并将结果返回本机。
📌 插图 2:远程接入 DeepSeek 架构示意图|此处替换为你的截图
方式 B:Ollama 本地接入
接入程序、OllamaServer、模型文件全部跑在自己的机器上:
- LLMManager 调用
sendMessage()向本机 OllamaServer 发起请求; - OllamaServer 解析出模型名称(如 deepseek-r1:1.5b);若模型尚未运行,则从 Ollama 官方模型仓库 pull 模型文件并加载进内存运行;
- OllamaServer 将用户传入的模型参数转发给模型,模型返回生成结果。
📌 插图 3:Ollama 本地接入架构示意图|此处替换为你的截图
Ollama 的请求处理机制:
| 请求阶段 | OllamaServer 的行为 |
|---|---|
| 第一次请求 | 解析模型名 → 发现模型未运行 → 将模型加载进内存运行 → 把用户的模型参数转给模型 |
| 第二次及以后 | 模型已在内存中,直接将请求转发给大模型 |
一张表看懂两种接入
| 对比项 | 远程接入 DeepSeek | Ollama 本地接入 |
|---|---|---|
| 模型运行位置 | DeepSeek 官方服务器 | 自己的机器 |
| 模型获取方式 | 无需下载,直接调用服务 | 从 Ollama 官方仓库 pull 到本地 |
| 接入程序 | LLMManager(发送请求的代码) | LLMManager → 本机 OllamaServer |
| 网络依赖 | 每次请求都走网络 | 仅首次拉取模型时需要 |
| 本地硬件要求 | 低 | 需足以承载模型运行 |
四、写在最后
手动搭建的三步,帮你真正理解"模型是如何被跑起来的";Ollama 则把这三步工程化、命令化。先懂原理,再用工具,本地大模型并没有想象中遥远。
Ollama /api/chat 接口接入实战:参数详解 · 代理排坑 · 流式响应解析
本文整理 Ollama 对话接口的核心参数、请求失败时的代理排查思路,以及非流式/流式两种响应的解析要点。
一、接口概览
POST /api/chat
该接口基于给定模型生成下一条对话消息。它是一个流式端点(streaming endpoint) ,默认会返回一系列增量响应;若想一次性拿到完整结果,可在请求中设置 "stream": false,此时最终响应对象会附带统计信息和附加数据。
二、请求参数详解
① 核心参数
| 参数 | 必填 | 说明 |
|---|---|---|
model |
✅ | 模型名称 |
messages |
对话消息列表,可用于维持聊天记忆 | |
tools |
模型可用的工具列表(JSON 格式,需模型支持) |
② message 对象字段
| 字段 | 说明 |
|---|---|
role |
消息角色:system / user / assistant / tool |
content |
消息内容 |
images(可选) |
附带的图片列表,用于多模态模型(如 llava) |
tool_calls(可选) |
模型想调用的工具列表(JSON 格式) |
③ 高级参数(可选)
| 参数 | 说明 |
|---|---|
format |
返回格式,可为 json 或 JSON Schema |
options |
额外模型参数,见 Modelfile 文档,如 temperature |
stream |
设为 false 时返回单个完整响应对象,而非流式对象序列 |
keep_alive |
控制模型加载后驻留内存的时长(默认 5m) |
三、请求失败的排查:代理问题
给 Ollama 服务发请求失败时,优先排查本机代理,按以下顺序处理:
① 关闭系统代理
关闭 Clash 等代理工具,并屏蔽 ~/.bashrc 中代理相关的环境变量。
⚠️ 注意:bashrc 修改后必须重新加载才能生效:
source ~/.bashrc
② 检查 curl 是否仍在走代理
即使环境变量已清理,~/.curlrc 中可能仍残留代理配置,注释掉即可:
$ cat .curlrc
#proxy = http://127.0.0.1:7890 # 行首加 # 屏蔽代理
(📷 此处可插入 .curlrc 内容截图)
四、非流式响应(stream: false)与解析要点
设置 "stream": false 后,一次请求即可拿到完整响应:
{
"model": "deepseek-r1:1.5b",
"created_at": "2025-10-10T07:20:42.745656618Z",
"message": {
"role": "assistant",
"content": "<think>\n\n</think>\n\n您好!我是由中国的深度求索(DeepSeek)公司开发的智能助手DeepSeek-R1,如您有任何问题,我会尽我所能为您提供帮助。"
},
"done_reason": "stop",
"done": true,
"total_duration": 6983002409,
"load_duration": 1525778783,
"prompt_eval_count": 6,
"prompt_eval_duration": 523945627,
"eval_count": 40,
"eval_duration": 4932264848
}
💡 原始 JSON 中的
\u003c/\u003e即</>的 Unicode 转义,解析后自动还原。
解析要点(层层校验,防止空指针):
反序列化之后,按如下顺序逐层校验:
- JSON 中是否包含
message字段; message字段是否为 JSON 对象;- 该对象中是否包含
content字段。
任一环节缺失都可能引发解析异常,取值前逐层判断更稳妥。
五、流式响应:done 字段是关键
默认情况下接口为流式返回,响应是一连串增量 JSON 对象。边收边拼接 content,盯住 done 判断是否结束即可还原完整回答:
{'model': 'deepseek-r1:1.5b', 'created_at': '2025-10-10T08:35:15.223353663Z', 'message': {'role': 'assistant', 'content': '<think>'}, 'done': False}
{'model': 'deepseek-r1:1.5b', ..., 'content': '\n\n'}, 'done': False}
{'model': 'deepseek-r1:1.5b', ..., 'content': '</think>'}, 'done': False}
{'model': 'deepseek-r1:1.5b', ..., 'content': '您好'}, 'done': False}
{'model': 'deepseek-r1:1.5b', ..., 'content': '!'}, 'done': False}
... # 我是 → 由 → 中国的 → 深度 → 求 → 索 → Deep → Seek → 公司 ...
{'model': 'deepseek-r1:1.5b', ..., 'content': '为您提供'}, 'done': False}
{'model': 'deepseek-r1:1.5b', ..., 'content': '帮助'}, 'done': False}
{'model': 'deepseek-r1:1.5b', ..., 'content': '。'}, 'done': False}
# 最后一个数据包:content 为空,携带统计信息,done = true
{'model': 'deepseek-r1:1.5b', 'created_at': '2025-10-10T08:35:20.331607895Z',
'message': {'role': 'assistant', 'content': ''},
'done_reason': 'stop', 'done': True,
'total_duration': 5327945887, 'load_duration': 91426389,
'prompt_eval_count': 6, 'prompt_eval_duration': 124785703,
'eval_count': 40, 'eval_duration': 5110917433}
规律总结:
done 取值 |
含义 | 客户端动作 |
|---|---|---|
false |
增量数据尚未结束 | 把 message.content 追加到结果缓冲区 |
true |
增量数据已结束 | 停止接收,读取统计信息(token 数、耗时等) |
*(📷 此处可插入完整流式响应日志截图)*
六、小结
/api/chat默认流式返回,"stream": false可切换为一次性完整响应;- 请求失败先排查代理:系统代理 →
~/.bashrc→~/.curlrc三处逐一确认; - 响应解析按
message→content的层级逐层校验; - 流式场景抓住
done字段:false拼接、true收尾。
*标签建议:Ollama | DeepSeek-R1 | API 调用 | 流式响应*
以上内容直接全选复制即可粘贴进 CSDN 编辑器(表格、代码块格式都会保留)。文中我标注了两处「📷 配图建议」,方便你把原截图插到对应位置,不需要的话删掉即可。
智能聊天助手会话管理设计:从问题到解决方案
在开发基于 LLMManager 封装的聊天助手时,我们遇到了几个典型痛点。这些痛点看似独立,实则都指向同一个核心缺失------会话(Session)机制。本文将从实际问题出发,推导出合理的数据结构与接口设计,并解释为何返回会话 ID 比返回对象更优雅。
一、四个"连环"问题
| 问题编号 | 现象描述 | 本质原因 |
|---|---|---|
| 1 | 多轮消息在程序中如何存储? | 缺乏统一的消息容器 |
| 2 | 模型无记忆,每次请求需携带历史消息 | 历史消息未被持久化或组织 |
| 3 | 多轮聊天中的消息归属混乱 | 缺少"轮次"标识 |
| 4 | 如何一次性获取某次聊天的全部历史? | 消息与聊天未绑定 |
推导结论:需要引入会话概念,一次会话即一次完整的多轮对话过程。
二、会话数据结构设计
一个会话需要承载的信息远不止消息列表,还应包括模型名称、时间戳等元数据。我们设计的 Session 结构如下:
cpp
struct Session {
std::string _sessionId; // 唯一标识
std::string _modelName; // 本次会话使用的模型
std::vector<Message> _messages; // 消息列表(按时间顺序)
std::time_t _createdAt; // 创建时间
std::time_t _updatedAt; // 最后更新时间
Session(const std::string& modelName = "") : _modelName(modelName) {}
};
设计要点:
-
_messages存储该会话下所有消息,解决归属问题(问题3)。 -
携带
_modelName便于切换模型时追溯。 -
双时间戳支持排序与清理策略。
三、会话管理器的接口设计
3.1 获取所有会话列表
我们提供 getSessionLists() 方法,返回 会话 ID 的集合 ,而不是直接返回 Session 对象。
cpp
std::vector<std::string> SessionManager::getSessionLists() const {
std::lock_guard<std::mutex> lock(_mutex);
std::vector<std::string> sessionIds;
for (const auto& pair : _sessions) {
sessionIds.push_back(pair.first);
}
return sessionIds;
}
3.2 为什么只返回 ID,而不是对象?
| 考虑维度 | 返回对象 | 返回 ID |
|---|---|---|
| 内存开销 | 拷贝整个对象,包含大量消息,影响性能 | 仅拷贝轻量级字符串 |
| 模块耦合 | 外部模块直接操作 Session 内部字段,管理逻辑分散 | 所有操作通过 ID 委托给 Manager,单一职责 |
| 数据源适配 | 若后续改用 SQLite,对象反序列化成本高 | ID 是持久化主键,天然支持 CURD |
| 维护性 | 修改 Session 字段需同步所有调用方 | 只需修改 Manager 内部实现 |
结论:返回 ID 更轻量、解耦,且更贴合持久化存储(如 SQLite)的操作方式。
四、额外优化:按更新时间降序排列
在返回会话列表时,我们强制按 _updatedAt 降序排列(最近更新的在前)。这符合用户习惯,也方便前端展示"最新会话"列表。
实现伪代码:
cpp
std::sort(sessionIds.begin(), sessionIds.end(),
[&](const std::string& a, const std::string& b) {
return _sessions[a]._updatedAt > _sessions[b]._updatedAt;
});
五、总结
| 核心设计 | 解决的问题 |
|---|---|
Session 结构包含会话ID、模型名、消息列表、时间戳 |
消息存储、归属、历史获取 |
| 管理器返回会话 ID 而非对象 | 降低内存开销、解耦、适配持久化 |
| 按更新时间降序排列 | 提升用户体验,快速定位活跃会话 |
这一套会话机制不仅让代码更清晰,也为后续扩展(如会话重命名、导出、删除)提供了统一的操作入口。实现时建议结合 SQLite 存储,让 _sessionId 作为主键,真正实现数据与逻辑分离。
延伸思考:若会话消息量极大,可考虑分页加载,但核心结构不变。会话机制是智能聊天应用的基础设施,值得从一开始就设计周全。