news 2026/9/9 13:34:17

Local ChatGPT with Gemma 3 源码实战:用 Ollama 与 Chainlit 搭建 100% 本地运行的多模态聊天助手

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Local ChatGPT with Gemma 3 源码实战:用 Ollama 与 Chainlit 搭建 100% 本地运行的多模态聊天助手

Local ChatGPT with Gemma 3 源码实战:用 Ollama 与 Chainlit 搭建 100% 本地运行的多模态聊天助手

【免费下载链接】ai-engineering-hubIn-depth tutorials on LLMs, RAGs and real-world AI agent applications.项目地址: https://gitcode.com/GitHub_Trending/ai/ai-engineering-hub

本指南以仓库local-chatgpt with Gemma 3项目为核心,完整拆解如何基于 Google DeepMind 的 Gemma 3 开源模型与 Chainlit 框架,在本地构建一个类 ChatGPT 的多轮对话应用:模型权重完全运行在本机、无需调用任何云端 API,同时天然支持图像输入与“打字机式”消息渲染。读完你将掌握 Chainlit 的事件回调编程模型、Ollama 本地推理的接入方式、会话级上下文管理与多模态消息构造,并能在此模板上自由切换模型或扩展功能。

项目概览:一台机器上的“迷你 ChatGPT”

该项目定位非常清晰:利用 Google DeepMind 的 Gemma 3 与 Chainlit,创建一个 100% 在本机运行的 mini-ChatGPT(见 README 开篇描述)。整个技术栈只有三个核心组件:

组件作用在本项目中的角色
Ollama本地模型运行时与推理引擎负责下载权重、启动本地服务并执行gemma3:4b推理
Gemma 3(4B)Google DeepMind 的开源大语言模型对话与多模态理解的“大脑”,模型参数gemma3:4b
ChainlitPython 聊天应用 UI 框架提供聊天界面、事件回调、会话状态与流式消息渲染
app.py应用入口逻辑串联 UI 事件与本地模型调用

这种“Ollama 出算力、Chainlit 出界面”的组合带来的直接收益是:对话数据不离开本机、不需要 OpenAI/Anthropic 等云端 API Key,联网需求几乎为零。从 app.py 源码可以看出,整个推理链路只有一个对ollama.chat(...)的本地调用,没有发起任何 HTTP 外部请求,是名副其实的本地部署方案。

环境准备:安装 Ollama 并拉取 Gemma 3 模型

README 的安装步骤面向 Linux 环境给出了一条可完整照做的命令链。在终端依次执行:

# 在 Linux 上安装 Ollama curl -fsSL https://ollama.com/install.sh | sh # 拉取 Gemma 3 4B 模型权重 ollama pull gemma3:4b

ollama pull gemma3:4b会从 Ollama 模型仓库下载约 4B 参数的 Gemma 3 权重并缓存在本地,后续推理完全离线进行。若你的机器显存充足,也可以把4b换成更大规格的标签(例如gemma3:12b),这只需修改一处模型名,详见后文“横向对照”一节。

注意一个 README 中的笔误:原文档中“pull the DeepSeek-R1 model”的注释是从姊妹项目复制遗留的描述,实际应拉取的是gemma3:4b,请以命令本身为准。这一笔误恰好说明本仓库内多个“Local ChatGPT”变体共用同一套模板(后文会对照验证)。

如果你在 macOS 或 Windows 上使用,README 中的脚本式安装不适用,需要改用 Ollama 对应平台的桌面安装方式;安装后本机同样会常驻一个 Ollama 本地服务(默认监听 11434 端口),后续 Python 客户端默认即连接该地址。

安装 Python 依赖

项目要求Python 3.11 或更高版本,依赖仅三个:

pip install pydantic==2.10.1 chainlit ollama

各依赖角色如下:

  • chainlit:聊天 UI 框架,提供@cl.on_chat_start@cl.on_message等事件装饰器;
  • ollama:Ollama 官方 Python 客户端,用于在 Python 侧发起本地模型推理;
  • pydantic==2.10.1:被 Chainlit 依赖的数据校验库。这里锁定了精确版本(而非pydantic>=2.x),从写法上可推断是为了与所用 Chainlit 版本保持严格兼容,避免大版本升级带来的接口不兼容问题——本地复现时建议保留该固定版本。

启动应用并体验对话

依赖装好后,在项目目录下运行:

chainlit run app.py -w
  • app.py是要启动的应用文件;
  • -w(watch)开启热重载,修改源码后无需手动重启,服务会自动刷新。

命令执行成功后,Chainlit 会在本地启动 Web 界面(默认监听localhost:8000),浏览器打开即可看到聊天页面。首次运行时,它会自动生成 Chainlit 所需的配置文件目录;正常启动的前提是本机 Ollama 服务处于运行状态、且已pullgemma3:4b

仓库还提供了实际运行效果演示:点击观看 AI 助手的 Demo 视频,可在动手前直观了解最终界面与交互形态。

app.py 逐段拆解:从聊天事件到本地推理

本项目的核心逻辑全部集中在一个仅 65 行的 app.py 文件中,堪称“小文件大模式”。它定义了三个 Chainlit 回调:

装饰器/函数行号触发时机职责
@cl.on_chat_startstart_chatapp.py#L5-L25每个会话开始初始化 system 上下文与欢迎语
@cl.step(type="tool")toolapp.py#L27-L46每次收到用户输入后由main调用拼装消息、调用 Gemma 3、写入上下文
@cl.on_messagemainapp.py#L49-L65收到用户消息/图片解析附件并驱动推理与输出

下面逐段说明其工作机制。

1.start_chat:初始化会话级上下文与打字机欢迎语

@cl.on_chat_start async def start_chat(): cl.user_session.set( "interaction", [{"role": "system", "content": "You are a helpful assistant."}], ) msg = cl.Message(content="") start_message = "Hello, I'm your 100% local ChatGPT powered by Google Deepmind's Gemma 3. How can I help you today?" for token in start_message: await msg.stream_token(token) time.sleep(0.005) await msg.send()

关键点:

  • 会话级状态存储cl.user_session.set("interaction", [...])把消息列表写入 Chainlit 的用户会话存储interaction是整个多轮对话的“记忆载体”,初始只包含一条 system 指令"You are a helpful assistant.",后续每一轮问答都会向这个列表追加内容,从而让模型具备上下文连续性(详见下文“会话级上下文”一节)。
  • 打字机式欢迎语:代码先把欢迎文本逐字符通过msg.stream_token(token)送入消息对象,每字符间隔time.sleep(0.005)(5 毫秒),最后msg.send()一次性发出,模拟出“逐字浮现”的 AI 启动效果。

2.tool:真正的本地推理函数

@cl.step(type="tool") async def tool(input_message, image=None): interaction = cl.user_session.get("interaction") if image: interaction.append({"role": "user", "content": input_message, "images": image}) else: interaction.append({"role": "user", "content": input_message}) response = ollama.chat(model="gemma3:4b", messages=interaction) interaction.append({"role": "assistant", "content": response.message.content}) return response

这段是整个应用的核心:

  • @cl.step(type="tool")装饰器把该函数注册为一个 Chainlit “工具步骤”。调用它时,界面上会显示一个可展开的中间步骤节点,方便观察推理过程。
  • 多模态消息构造:如果本次输入携带图片,用户消息会被构造成带images字段的字典({"role": "user", "content": ..., "images": image})。image实际是图片文件的本地路径列表,Ollama 客户端会读取这些路径对应的图片并随请求发送给模型——这正是 Gemma 3 视觉能力的接入点。
  • 本地推理调用ollama.chat(model="gemma3:4b", messages=interaction)把“完整的 interaction 历史”一次性交给本地 Gemma 3,返回的response.message.content即模型生成的回答文本。
  • 上下文的双向写入:调用前后分别把user消息与assistant回复追加进interaction,保证每一轮对话都“记得”之前的全部内容。

3.main:消息入口与图片附件识别

@cl.on_message async def main(message: cl.Message): images = [file for file in message.elements if "image" in file.mime] if images: tool_res = await tool(message.content, [i.path for i in images]) else: tool_res = await tool(message.content) msg = cl.Message(content="") for token in tool_res.message.content: await msg.stream_token(token) await msg.send()
  • 附件识别message.elements是用户上传的文件列表;file.mime是文件 MIME 类型,代码通过"image" in file.mime判断是否为图片(png、jpeg、gif 等均命中)。存在图片时,提取所有i.path(本地文件路径)作为多模态输入传给tool
  • 输出渲染:模型完整生成后,main同样采用stream_token逐字符渲染回答。注意这里与欢迎语的渲染方式一致——即先整体拿到回复、再逐字播报的“打字机效果”(下文会解释它与真正 token 流式推理的区别)。

4. 一次完整对话的数据流

整个请求链路可以用下图概括(依据 app.py 各回调的调用关系):

用户在 Web 界面输入文字 / 上传图片 │ ▼ @cl.on_message (main) │ 遍历 message.elements 过滤 "image" 类型,得到图片路径列表 ▼ @cl.step(type="tool") (tool) │ 将 user 消息(含可选 images 字段)追加到 interaction ▼ ollama.chat(model="gemma3:4b", messages=interaction) │ 本机 Ollama 推理,返回 response ▼ 将 assistant 回复追加到 interaction(记忆沉淀) │ ▼ main 用 stream_token 逐字渲染回复 → 展示在聊天界面

三个值得深挖的实现机制

会话级上下文:记忆是如何“沉淀”的

interaction列表始终存放在cl.user_session中,这是 Chainlit 提供的按用户会话隔离的存储,同一会话内所有回调都能读取同一份数据。整个对话期间该列表的生长轨迹如下:

  1. 会话开始:[{"role": "system", "content": "You are a helpful assistant."}]
  2. 第一轮提问后:追加一条user消息
  3. 模型回复后:追加一条assistant消息
  4. 第二轮提问、回复……列表持续增长

因此模型的每一轮回答都建立在完整的“历史消息序列”之上——这与 OpenAI 等云端 API 的messages协议在结构上完全同构,也意味着你随时可以把ollama.chat替换为任何遵循相同消息协议的本地或远程推理接口。

“打字机效果”与真正的 token 流式推理

源码中所有输出都走for token in content: await msg.stream_token(token)。需要厘清的是:这里的content来自response.message.content,即模型已经完整生成的一整段文本;逐字符渲染只是在 UI 层制造打字机效果,中间time.sleep(0.005)制造了节奏感,并非逐 token 推理。

如果希望实现“边生成边输出”的真正流式体验,可以将 app.py#L40-L41 处的调用改为ollama.chat(..., stream=True),再对迭代返回的块做增量渲染——当前仓库提交的是一个更简单直接的版本,这一点从源码结构可以明确推断。

多模态链路:从拖拽图片到模型“看懂”

Gemma 3 属于支持图像输入的开放模型,而本项目让这一能力变得几乎零成本:Chainlit 聊天框天然支持文件上传 →message.elements暴露上传文件 → 按 MIME 过滤图片 → 把本地路径列表放入消息的images字段 → Ollama 读取图片并送入 Gemma 3。整个图片处理无需任何额外的视觉预处理管线,这是该实现最有价值的开箱即用特性之一。

配套 notebook:最小可复现的推理验证

仓库中的 notebook.ipynb 记录了开发过程中对ollama.chat的最小调用验证。值得注意的是,其中测试使用的是deepseek-r1模型(该 notebook 大概率由 DeepSeek 变体项目复用而来),但它验证的能力对 Gemma 3 同样成立:

import ollama response = ollama.chat(model="deepseek-r1", messages=[{"role": "user", "content": "Hello, how are you?"}]) print(response)

从 notebook 输出的响应对象可以看到ollama.chat返回体的关键字段:response.message(含rolecontent)、created_atdone_reason、以及eval_count/eval_duration等耗时统计。app.py 正是通过response.message.content取出回答文本——理解了这一点,就抓住了整个项目“UI 与推理衔接”的接口契约。如果你想绕过 UI 单独验证模型是否就绪,这段 notebook 代码是最快的冒烟测试。

横向对照:仓库内同一模板的多种模型变体

本项目并非孤立存在。在当前仓库中,local-chatgptlocal-chatgpt with DeepSeeklocal-chatgpt with Gemma 3共用几乎相同的工程模板与安装说明,差异仅在模型一行:

仓库目录驱动模型模型拉取命令
local-chatgpt with Gemma 3gemma3:4b(本主题)ollama pull gemma3:4b
local-chatgpt with DeepSeekdeepseek-r1ollama pull deepseek-r1
local-chatgptllama3.2-visionollama pull llama3.2-vision

例如在 local-chatgpt/app.py#L38 中,模型名换成了llama3.2-vision,其余回调结构与 Gemma 3 变体完全一致。这揭示了一个可迁移结论:这套 Chainlit 回调骨架对模型完全无感。想换模型时,只需执行ollama pull <新模型>,并把 app.py#L40 的model="gemma3:4b"改为新模型标签即可(若模型不支持视觉,则需同步去掉images字段的传参逻辑)。

常见问题与排查思路

基于对安装与源码的分析,本地复现时最常见的几类问题及对应处理建议如下:

  • 模型未找到(model not found:确认已执行ollama pull gemma3:4b,且模型名与代码中model="gemma3:4b"完全一致;
  • 推理无响应/连接失败:检查本机 Ollama 服务是否在运行(默认端口 11434);Linux 安装脚本执行后可通过ollama list验证服务与模型状态;
  • 界面卡在欢迎语:确认start_chat中的time.sleep与渲染逻辑没有因终端中断而异常,必要时在启动命令中保留-w以便观察热重载日志;
  • 上传图片无效main只接收 MIME 类型含"image"的文件,若上传的是非图片文件会被静默忽略。

小结与可行的扩展方向

local-chatgpt with Gemma 3用极少的代码量演示了一套完整、可离线运行的多模态对话范式:Chainlit 负责交互与状态,Ollama 负责本地推理,Gemma 3 提供文本与视觉理解能力,三者以标准messages协议串联。它既是零成本上手本地 LLM 应用开发的样板,也是理解会话记忆、多模态消息与 UI 流式渲染的绝佳教材。

如果你希望在此基础上继续实验,结合源码留出的扩展点,以下方向都只需在 app.py 上做局部改动即可实现:

  1. 接入真流式推理:把ollama.chat改为stream=True并增量渲染,让回答“边算边出”;
  2. 切换/对比更多模型:借助ollama pull拉取gemma3:12bdeepseek-r1等并替换模型名;
  3. 增强 system 角色设定:修改start_chat中初始化的 system 内容,即可定制助手人设与行为边界;
  4. 为长对话做截断/摘要interaction无限增长会拖慢推理,可加入消息窗口裁剪或历史摘要逻辑。

上述改动均属于读者在自己本地副本上的实践范畴,仓库本身保持只读即可运行验证。

【免费下载链接】ai-engineering-hubIn-depth tutorials on LLMs, RAGs and real-world AI agent applications.项目地址: https://gitcode.com/GitHub_Trending/ai/ai-engineering-hub

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

Java Object类11个方法详解:从源码原理到实战应用

做Java开发这些年&#xff0c;我面试过不少候选人&#xff0c;也被人问过很多次“Object类有哪些方法”。这个问题看似基础&#xff0c;但它就像一面镜子&#xff0c;能照出一个人对Java语言底层设计到底理解到什么程度。毕竟Object是所有类的父类&#xff0c;Java里一切对象行…

作者头像 李华
网站建设 2026/9/9 13:32:50

深入解析MyBatis分页插件原理:PageHelper与MyBatis Plus实战

1. 从手写分页到插件接管&#xff0c;先聊清楚分页这件事 做Java后端的人&#xff0c;只要接触过数据库&#xff0c;基本都逃不过分页查询。早期用JDBC的时候&#xff0c;分页是纯手工活&#xff0c;MySQL写 LIMIT offset, size &#xff0c;Oracle玩 ROWNUM &#xff0c;S…

作者头像 李华
网站建设 2026/9/9 13:32:16

级联H桥SVG/STATCOM三相不平衡补偿的三层控制策略与仿真实践

1. 项目概述与整体设计思路 搞电力电子的朋友应该都有体会&#xff0c;SVG&#xff08;静止无功发生器&#xff09;和STATCOM&#xff08;静止同步补偿器&#xff09;在行业内基本被当成同一个东西用&#xff0c;只是叫法不同&#xff0c;一个侧重低压配电&#xff0c;一个侧重…

作者头像 李华
网站建设 2026/9/9 13:32:12

Seelen-UI 插件系统实战:4 类内置模块,从 0 到能用的桌面定制

Seelen-UI 插件系统实战&#xff1a;4 类内置模块&#xff0c;从 0 到能用的桌面定制 【免费下载链接】Seelen-UI The Fully Customizable Desktop Environment for Windows 10/11. 项目地址: https://gitcode.com/GitHub_Trending/se/Seelen-UI 一句话讲清楚它是什么 …

作者头像 李华
网站建设 2026/9/9 13:31:36

从被AI气晕到理性共处:大模型能力边界、偏见与落地实践

1. 从"被AI气晕"到"重新认识AI"&#xff1a;我的认知转变1.1 早期我对AI的理解&#xff1a;死板的规则引擎大概十年前&#xff0c;我对人工智能的看法还停留在一个非常朴素的层面&#xff1a;某个程序能不能"听懂人话"&#xff0c;本质上就是一堆…

作者头像 李华
网站建设 2026/9/9 13:29:26

Ant Design表单焦点错乱:同名Field注册冲突的成因与解法

把时间拨回两周前&#xff0c;我正对着一个Bug工单发愁。同事的描述很简短&#xff1a;“弹窗里的输入框&#xff0c;鼠标点一下&#xff0c;光标却跑到页面顶部的搜索框里去了&#xff1b;弹出的字符没有进弹窗&#xff0c;反而把搜索框填满了。” 我第一反应是“怎么可能”&a…

作者头像 李华