今天,我只是觉得 Claude 和 Codex 的使用成本有点高,想在一台 16GB 的 M4 Mac mini 上把 Gemma4 和 Qwen 这些开源大模型跑起来,看看本地写项目代码到底能不能顶上一部分。大模型真跑起来以后,我又顺着这个想法往前多走了一步:既然单独
ollama run已经能用,那能不能再给自己包一层更像 Codex 的本地终端工作流?于是就有了 Marcodex,我把 Ollama、项目索引、模型分工、patch 草稿、安全拦截和项目记忆串到了一起。
01
这件事最开始,其实没那么“产品化”。
我今天会突然去折腾本地开源大模型,一个很现实的原因是:Claude 和 Codex 虽然很好用,但费用确实不便宜。
如果只是偶尔问几句,这个成本可能还没那么明显,但如果真的把它们当成日常项目开发里的高频助手,一轮一轮地分析代码、改方案、看文档、修 bug,这个成本就会开始显著上升。
所以当我今天看到 Google 开源大模型 gemma4 发布的时候,第一反应不是“我要追个热点”,而是有点想认真试一下:现在能不能直接在我自己的 Mac 上跑开源大模型,看看它到底能不能替我接住一部分项目开发工作?
我公司的苹果电脑也不是什么顶配,就是一台 2024 款 Mac mini,M4 芯片,16GB 统一内存。
所以第一个问题很朴素:这台机器到底能不能在本地跑一个足够好用的大模型,而且不是跑个 demo 就结束,而是真的能在项目开发里帮我分担一部分 Claude / Codex 原来在做的事?
后来真的装起来之后,我发现答案是“能”,但事情马上就拐到了另一个方向。
因为手动 ollama run xxx 虽然能对话,但由于我习惯了codex cli、claude code,所以我就想试试看能不能做到类似的工作方式。
02
我先把本机模型这件事跑了一遍。
最后这台 Mac 上真正留住的,是这三个模型:
gemma4:e4b
modelscope.cn/Qwen/Qwen2.5-Coder-14B-Instruct-GGUF:latest
gemma3:12b-it-qat
它们的分工也很直接:
-
Qwen2.5-Coder-14B主要负责代码解释、局部修改方案、生成小范围 diff。 -
Gemma3 12B主要负责一般文档问答、图片理解、录屏抽帧后的辅助分析。 -
Gemma4 E4B现在主要被我放在fast-doc这个位置,拿来做更快的文档扫读和短总结。
这里面还有一个小插曲。
我一开始也装过 Qwen3-VL-8B,本来是想让它专门看截图和录屏帧图,模型本身确实能装上,ollama show 也能看到 vision 能力,但只要我通过 Ollama HTTP API 真往里传图片,它就会直接报 model runner has unexpectedly stopped。
这类问题很适合拿出来说一句实话:本地开源模型这套东西,很多时候不是“能不能下下来”这么简单,而是“下载下来了以后,真调起来到底稳不稳”。
所以后来我干脆把 Qwen3-VL 从本机删掉了,图片和视频分析先统一回到 Gemma3。这个决定不酷,但它更像一个真的要把工具拿去干活的人会做的选择。
03
模型能跑起来以后,我很快就发现,直接在终端里和模型聊天,还是不够。
比如我在一个老 iOS 项目里改代码,真正麻烦的从来不是“让大模型回答一个问题”,而是这些事:
-
它不知道这个项目的目录结构和关键模块在哪里。
-
它不知道哪些核心流程已经跑通,不能让它顺手重写。
-
它不知道我这轮只想改哪个文件,不想让它把一堆旁边文件也“顺手优化”了。
-
它不知道我默认不让它替我
git commit、git push。 -
它也记不住我这个项目里那些长期协作规则,每一轮都要重新交代。
这时候我脑子里冒出来的就不是“再找一个更大的模型”,而是另一个念头:
能不能在 Ollama 上面自己包一层终端工具,让本地模型按我的项目上下文、我的修改边界、我的使用习惯来工作?
这就是 Marcodex 这个想法的起点。
说得再直白一点,我不是突然想“造一个新玩具”,而是想看看:有没有可能在成本更可控的前提下,把一部分原来交给 Claude / Codex 的项目开发流程,搬到本地开源模型上来。
我当时给它定的目标也没那么大,不是“复刻一个完整 Codex”,毕竟受到电脑性能的限制,我下载下来的开源大模型的能力,肯定是比不过gpt5.4、claude opus4.6这些顶级大模型,
所以,我先做了一个对我自己真的有用的本地版本:
Marcodex = 全局 CLI + 项目索引 + 上下文装配 + 模型路由 + patch 草稿 + 安全门控 + 项目记忆
而且有几个边界,我一开始就写死了。
第一,它不会在我一打开终端时自动接管 shell,只有我自己输入 marcodex ... 的时候才启动。
第二,它不负责代码提交流程,只做本地开发和问题修复辅助,不碰 git commit/push。
第三,第一版不追求“模型自己全自动改完整个项目”,而是坚持“先分析、再出 patch、我确认后再落文件”。
这几个边界,后来反而成了这套东西最重要的部分。
04
架构设计这一步,我没有一上来就堆很多“智能体”概念,而是先把这件事拆成几个很具体的模块。
最后 Marcodex 大概长这样:
marcodex CLI
-> WorkspaceState / MemoryStore / SessionStore
-> ProjectIndexer
-> ContextBuilder
-> ModelRouter
-> OllamaClient
-> PatchManager / SafetyGuard
-> VideoFrameExtractor
这几个模块各自干的事,其实都很“土”,但拼起来就够用了。
ProjectIndexer 负责 /init。
它不会让模型自己去乱翻整个仓库,而是先由脚本做确定性的扫描:顶层目录有哪些、AGENTS.md 和 docs/memory/ 这些规则文档在不在、主要语言是什么、当前 git 分支和脏文件有哪些。然后再把这份结构化结果交给文档模型,让它生成一份人能看的 project-index.md。
这个顺序很重要。
因为我不想把“代码仓库遍历”这种事交给模型猜,我只想让模型做归纳。
ContextBuilder 负责 /ask 和 /patch 之前的上下文拼装。
我最后给它定的顺序是:先放项目长期规则,再放项目结构摘要,再放这次我显式点名的文件内容,再补当前工作区只读状态,最后才放我的问题。
代码里这个思路大概是这样:
sections = [
prompt_template.strip(),
"",
"## 项目硬规则 / 用户偏好",
self._read_optional_text(self.memory_path, fallback="(当前项目尚未初始化 memory)"),
]
if include_project_index:
sections.extend([
"",
"## 项目结构摘要",
self._read_optional_text(self.project_index_path, fallback="(当前项目尚未生成 project-index.md)"),
])
这一步做完之后,我对一件事的感受特别明显:本地模型能不能帮上忙,很多时候不只看模型参数量,还看你喂给它的上下文是不是按正确顺序摆好了。
ModelRouter 负责决定这一轮到底叫哪个模型。
我没有再请一个模型来“判断该找谁”,第一版直接写规则路由:有图片或视频就走视觉模型,/patch 就走代码模型,/init 和文档类任务走文档模型,fast-doc 优先用 Gemma4,不可用就回到 Gemma3。
if role == "vision" or has_image:
return RouteResult(model=self._config_text("vision_model"))
if command == "/patch":
return RouteResult(model=self._config_text("code_model"))
if role == "fast-doc":
fast_doc_model = self._config_text("fast_doc_model")
doc_model = self._config_text("doc_model")
if fast_doc_model not in self.available_models and doc_model:
return RouteResult(model=doc_model, fallback_message=...)
return RouteResult(model=fast_doc_model)
这套分工规则没有什么神秘的地方,但对一台 16GB 的机器来说,它比“多个大模型一起常驻、互相商量”现实得多。
05
真正让我觉得这工具开始“像个能干活的东西”的,其实是 /patch 和 /apply 这条线。
因为只做 /ask,本质上还是一个带项目上下文的聊天壳。
但一旦模型要开始改代码,问题就变了:我怎么保证它只改我允许改的文件,怎么保证它别碰 .git/、Pods/ 这些地方,怎么保证它吐出来的 diff 真能安全落下去?
所以我没有让模型直接写文件,而是做成了这个流程:
marcodex /patch --files path/to/file "只改这个文件,修复这个问题"
marcodex /apply <patch-id>
中间那层 PatchManager + SafetyGuard,我刻意做得比“能跑就行”更重一点。
/patch 必须显式传 --files,不然直接拒绝,patch 里如果出现不在白名单里的文件,拒绝。
如果路径指向 .git、Pods、.marcodex、绝对路径、.. 路径逃逸,拒绝。
如果单次 diff 超过 5 个文件或者 800 行,也拒绝,让我先把范围收小。
代码里最核心的判断,大概长这样:
if safe_relative_path not in allowed_files:
raise PatchSafetyError(f"patch 越界修改未授权文件:{safe_relative_path}")
if normalized_path.parts and normalized_path.parts[0] in BLOCKED_TOP_LEVEL_DIRS:
raise PatchSafetyError(f"禁止修改高风险目录:{relative_path}")
还有一个我后来补上的细节,我觉得很值得讲。
模型生成 patch 的那一刻,我会把目标文件当时的 sha256 摘要记下来,真正 /apply 之前,再把当前文件摘要算一遍,如果这个文件在 patch 生成之后已经被我手动改过,就直接拒绝应用。
这个机制不复杂,但它解决的是一个很真实的问题:风险不只是“模型会不会改错”,还有一种更烦人的情况是,模型给的是旧版本 diff,但我这边文件已经先改过了。
另外,本地模型输出 unified diff 的格式有时也没那么稳,它可能把 hunk 行数写错,或者在 diff 里混几行没前缀的裸文本。
所以我又补了一层格式收口:先从模型输出里提取代码块,再检查 hunk 行前缀,再按实际 body 重新计算 @@ -x,y +u,v @@ 里的行数,尽量减少 git apply --check 因为计数不准直接失败。
这个地方做完以后,Marcodex 才从“会给建议”往前走了一步,变成“能在一个很小的边界里,给我一份可以审、可以落、可以拒绝的修改草稿”。
06
视频这条线,我一开始也没有直接让模型“看整段视频然后理解交互”。
因为在本地机器上,这么干大概率又重又不稳,而且对 UI 录屏来说,真正有价值的往往不是每一帧都看,而是先把关键画面按时间顺序抽出来。
所以 /ask --video 最后做成了“先 ffmpeg/ffprobe 抽帧,再把帧图和时间戳摘要交给模型”。
现在支持三种模式:
-
interval:按固定时间间隔抽帧,最适合 UI 流程先保底不漏。 -
scene:按画面变化程度抽帧,适合抓页面跳转、弹窗出现这种明显变化。 -
iframe:只抽编码 I-frame,作为补充,不作为默认主方案。
我自己现在最常用的还是这个:
marcodex /ask --video ./demo.mp4 \
--frame-mode interval \
--fps 1 \
--max-video-frames 8 \
"按时间顺序说明这段录屏里页面状态怎么变化、问题出在哪"
这个设计背后的判断其实也很简单:能先交给脚本整理的东西,就别直接扔给模型硬猜;模型更适合看整理后的材料,而不是一口吞一整坨原始输入。
07
最后真正落地的时候,我是按四个阶段把 Marcodex 做出来的。
第一阶段,先把全局命令入口和项目状态跑通。
也就是 marcodex、/model、/status、/memory show/add,再加上 ~/.marcodex/global-config.json 和每个项目自己的 .marcodex/。
第二阶段,再补 /init 和 /ask。
这一步做完以后,它就不再只是“一个 Ollama 启动器”,而是开始能带着项目索引、项目规则和指定文件内容来回答问题。
第三阶段,做 /patch 和 /apply。
这一步的重点不是“让模型更自由”,而是把“只改哪些文件、怎么保存 diff、怎么安全应用、怎么留下本轮记录”这件事先工程化。
第四阶段,再补 /ask --video 和 fast-doc。
Gemma4 装好之前,fast-doc 先回退到 Gemma3;Gemma4 真下完以后,我直接把这个角色切过去,不用改主程序代码。
现在这套东西已经可以这么用了:
marcodex /init --refresh
marcodex /ask "先结合当前项目索引,帮我说明这个需求大概应该从哪些模块入手"
marcodex /ask --files path/to/A.swift,path/to/B.swift "先解释这几个文件现在的职责和调用关系"
marcodex /patch --files path/to/A.swift "只改这个文件,按最小风险方式修复这个问题"
marcodex /status
marcodex /apply <patch-id>
如果是长文档快速扫读,我就直接用:
marcodex /ask --role fast-doc "先快速总结当前项目文档里和这个需求最相关的约束"
如果这个项目里新形成了一条长期规则,我不会只把它留在聊天窗口里,而是直接写进项目记忆:
marcodex /memory add "已跑通的核心业务流程默认不要大范围重写,优先做低侵入修复"
这样下一轮再问的时候,这条规则会自动进上下文,而不是每次都靠我重新提醒。
08
当然,这套东西现在还不是一个“本地版 Codex 完全替代品”。
我现在对它的定位反而更收敛了:它不是来证明本地小模型已经无所不能的,而是把本地开源模型接进一个更像真实工程工作流的壳里。
所以它现在最适合做的,是这些事:
-
先读项目结构和规则,再帮我分析一个需求该从哪里下手。
-
针对我点名的几个文件,解释当前代码在干什么。
-
在很小的文件范围里,给我一份可以人工 review 的 patch 草稿。
-
对截图或录屏抽帧做一层辅助分析。
-
把项目级长期规则和单轮会话记录留在本地。
它不适合做的,我现在也不会硬吹:
-
让它自己接手一个老项目大模块,然后跨几十个文件稳定重构。
-
让多个本地大模型并行常驻,像云端 agent 一样自己长时间连续规划。
-
完全不看 diff、不做人工确认,就让它直接改项目核心流程。
这其实也是这轮做下来,我自己最明确的一个感受。
在本地搭开源大模型,真正有意思的地方,不只是“模型能不能跑起来”,而是你能不能把它放进一个边界足够清楚、上下文足够稳定、出了问题还能收得住的工作流里。
Marcodex 对我来说,就是沿着这个方向先做出来的一个版本。
它不神奇,但它已经开始真的能干活了。
如果后面我继续往下做,下一步大概率会先补三件事:
第一,把 /memory summarize 从“只备份”补成真正的模型压缩;
第二,给 .marcodex/cache/video-frames/ 补一个更明确的缓存清理策略;
第三,给 SafetyGuard、PatchManager、ContextBuilder、ModelRouter 这些模块补一组最小测试。
因为工具一旦真的开始被自己天天用,下一步最该补的,往往就不是新功能了,而是那些能让它更稳、更不容易在关键时候添乱的东西。