最近一个月我把主力工作流里的 AI 编程助手换成了一个完全自托管的方案,项目代号就叫 free-claude-code。先说结论:它没有办法一比一复刻商业版那种“像有一个资深同事坐在旁边盯屏”的体验,但在脚本生成、批量文件处理、代码审查辅助这类高频场景里,能覆盖我日常至少七成的需求。更关键的是,没有按量计费、没有用量配额中断、不会把公司内部代码片段发到第三方服务,这在某些场景下比降低一点智能度重要得多。
这篇东西不是项目文档搬运,是我从零部署、配置、跑通任务、踩坑排查的完整记录。你会看到一个工具型项目被拆开后长什么样,哪些模块是必须的,哪些问题是肉眼看不见但迟早会撞上的。如果你是第一次接触这类自托管编程助手,照着我的目录走一遍,半天之内应该能跑出自己的版本。
1. 为什么我要折腾一个“免费版”的 AI 编程助手
1.1 商业助手好用,但额度用完之后只剩尴尬
先说痛点。我在公司和家里都有密集编码的时段,尤其改一堆历史遗留脚本的时候,对话式编程助手能省下大量查文档的时间。问题在于它的限制很松散:一天的额度用完,模型会继续响应但质量下降,或者干脆停在“请求过多”的状态。在最需要它帮我处理机械重复工作的时候,它沉默了。
有人会说,那就买更高额度。但在团队里不一定每个人都能获批费用,而且不同项目的保密要求也不一样,某些客户代码不能外传,这直接否决了云端方案。所以我开始找可以自己在服务器上部署的开源替代品,要求很朴素:支持命令行对话、能读写项目文件、能执行有限度的系统命令、模型可以用本地推理服务。
1.2 免费不是目的,可控和可复现才是
免费在项目名里,但接触下来我发现真正的价值是“可控”。我第一次部署完成后,把整个对话记录、模型输出、工具调用日志全部落到本地磁盘,这解决了一个很实际的问题:复盘。商业助手只给你最终答案,你很难知道它为什么删了一个文件、为什么改错了某一行。而自托管方案,每一步调用都有日志,我可以精确回放。
另一个点是可复现。同一个任务,换一个模型参数,结果可能完全不同。自托管以后,配置文件里写死 temperature、top_p、上下文长度,团队其他人拉下来同一份配置,行为基本一致。这种确定性在协作场景里非常有用。
2. 拆开看 free-claude-code 的四个核心模块
2.1 终端交互层:把输入转成结构化请求
整个项目的入口是一个命令行程序,它在终端里等待你输入自然语言,同时把当前工作目录的路径、最近改动的文件列表、git 状态等上下文一并组装成请求。这一层要解决的问题不是“读懂你的话”,而是“让模型知道它现在在哪、可以动什么”。
我在早期版本里犯过一个设计错误:把用户输入直接塞给模型,结果模型经常靠猜测来回答。比如用户问“把这几个文件里重复的行去掉”,模型会返回一段通用脚本,而不是基于当前目录真实文件去写。后来我在交互层加了上下文采集:先把目录树压缩成一份摘要,再把用户输入追加在后面,模型才知道该对什么下手。简单说,交互层做得越厚,模型后续的幻觉越少。
2.2 模型代理层:屏蔽不同模型服务的差异
模型代理层是项目的通讯中枢,它把所有模型服务统一成一套接口。本地推理服务走 HTTP 接口,远端自建模型服务也走同样的协议。好处是换模型不用改业务代码,只需要改配置。我在这个层遇到最多的坑反而不是协议本身,而是模型格式的兼容性——不同模型返回的字段名、结束标记、工具调用格式都不一样,如果没有一层适配器,接一个模型就要改一遍主程序。
代理层还负责把模型返回的文本切割成“事件”,比如“它想要读取文件”“它想要执行命令”。模型原生的输出往往是混合的,一段话里夹着代码、命令、注释,如果不解析直接丢给执行层,很容易出事。我在代理层里加了一个严格的解析器:只认固定格式的 JSON 块,其他一律视为普通文本输出,打印给用户看但不执行。
2.3 工具调用层:让模型能真正操作文件和命令
工具调用层是价值核心,也是最容易出安全问题的部分。我预置了四个基础工具:
- list_files:列出目录内容,支持通配符和递归深度限制
- read_file:读取文件内容,默认限制单次读取不超过 20000 字符
- write_file:写入或覆盖文件,必须给出完整路径和内容
- run_command:在项目目录内执行 shell 命令,默认超时 15 秒
每个工具都用 JSON Schema 定义参数。模型要读取文件,必须返回一个标准动作对象,类似这样:
{ "tool": "read_file", "params": { "path": "src/utils/parser.js", "offset": 0, "limit": 20000 } }这一层最重要的特性是“确认机制”。默认情况下,read_file 和 list_files 是安全操作,直接执行;write_file 会做一次覆盖提醒;run_command 里凡是命中危险命令前缀,必须由用户输入 y/N 确认才会真正执行,这条规则我后面还会细讲。
2.4 规则引擎:用一组本地配置控制模型行为
规则引擎听起来玄乎,其实就是一个配置文件加一份系统提示词,两者配合控制模型的边界。配置文件定义硬限制,比如“工作目录必须是 /home/user/projects 下的子目录”“任何命令都不能以 root 权限运行”“禁止访问 .env 文件”。系统提示词则定义软行为,比如“修改代码前先列计划”“替换内容时保留原作者的注释风格”。
我强烈建议把规则分成硬规则和软规则两层。硬规则由代码强制校验,不依赖模型自觉;软规则只是提示词,模型有概率忽略。我踩过一次坑就是只写了提示词,没写硬校验,结果模型在上下文被截断后完全忘了什么不能做,这是后话,后面第 5 章展开讲。
3. 从零部署:环境准备与配置文件详解
3.1 运行环境与依赖清单
部署环境比想象中简单,一个能跑 Node 运行时和 Python 3 的 Linux 服务器就够了。项目本体是 Node 写的命令行程序,Python 是用来跑演示脚本和一部分模型推理辅助工具的。需要注意,模型推理不一定在本机跑,如果你有一台单独的 GPU 机器或者一个内网模型服务,free-claude-code 只需要拿到服务地址,本地机器零压力。
依赖安装顺序建议这样走:
- 准备 Node 运行时,建议版本不低于项目 README 里的最低要求,太老的版本在处理异步流式请求时容易内存泄漏。
- 安装 Python 3,并把 pip 的源指向内网镜像,避免联网失败。
- 拉取项目代码到工作目录,然后运行一次依赖安装脚本。
- 启动本地模型推理服务(如果有),确认接口能返回正常响应。
- 复制一份 config.example.json 为 config.json,开始逐项配置。
3.2 config 里的关键参数说明,别急着全部改
配置文件初看字段很多,但真正决定行为稳定性的只有几个。我把它分成三档,按重要程度排序。
第一档是模型连接参数:
{ "model": { "provider": "local", "modelId": "code-model-7b", "endpoint": "http://127.0.0.1:8080/v1", "apiKey": "local-test-key", "contextWindow": 8192, "temperature": 0.2, "topP": 0.9 } }provider 指模型服务的类型,local 代表本机或内网私有化服务。endpoint 是模型服务的接口地址,如果你用的是常见推理框架,路径通常带 /v1 后缀。contextWindow 一定要按模型实际支持的长度写,不要贪大,写大了模型服务会直接报错或者静默截断,表现成越聊越傻。temperature 建议固定得低一点,0.2 左右,代码生成任务需要确定性,太高容易发散去编 API。
第二档是工作目录和安全边界:
{ "workspace": { "rootDir": "/home/dev/projects", "allowSubdirs": true }, "safety": { "dangerousCommandPrefixes": [ "rm -rf", "mkfs", "dd", "curl -L | sh" ], "confirmUnknownCommands": true, "timeoutSeconds": 15 } }rootDir 是整个助手能接触到的最高目录,我建议你把它设成一个专门的 projects 根目录,而不是直接用家目录。dangerousCommandPrefixes 是硬校验黑名单,命中后无论模型说什么都必须人工确认。我自己的列表里甚至加了 rm -rf,没有前缀匹配的完全豁免。confirmUnknownCommands 打开以后,凡是工具调用层无法识别归属的命令,一律先暂停询问。
第三档是日志与审计,这个容易被忽略:
{ "logging": { "dir": "/var/log/fcc", "saveTranscript": true, "saveToolCalls": true, "maxLogDays": 30 } }建议从一开始就开全量日志。项目本身还处在快速迭代阶段,很多奇怪行为如果不留痕,事后根本没法查。我的习惯是每轮对话都会生成一个以时间戳命名的 JSON 文件,里面同时记录模型原始输出、解析后的工具调用、实际执行结果三部信息。排查问题的时候,直接对比这三个时间线就够了。
4. 用一次真实任务演示完整工作流
4.1 任务需求:批量整理日志文件
纸上谈兵没意思,我用一个真实到不能再真实的场景来演示:服务器上一夜之间产生了上千个日志文件,散落在按小时命名的一堆目录里,我需要按日期归档,同时统计每天有多少条 ERROR 级别记录。这种任务手写脚本也就二十分钟,但我想看看 free-claude-code 能不能自己完成大部分工作。
启动 CLI 后,我输入的任务描述是:
用户:统计 /logs/raw 下面所有 .log 文件中 ERROR 行数量,并按日期归档到 /logs/archive/{yyyymmdd}/,最后生成一份 summary.md 写入 /logs/archive/。
这里注意一个细节:任务里的目录 /logs/raw 是项目根目录 /logs 下的子目录,在 workspace 允许范围内。如果路径写在外面,规则引擎会直接拒绝。
4.2 从生成脚本到执行的完整链路
第一轮交互,free-claude-code 没有直接动手,而是先调用了 list_files 查看目录结构。模型输出了一段普通文本:“我先确认日志目录结构,再决定用什么方式遍历。”然后工具调用层实际执行了 list_files /logs/raw,返回了目录树摘要。
拿到真实目录结构后,模型开始写执行计划。它没有立刻生成脚本,而是先输出计划,请求确认。计划内容大致是:先递归找出所有.log 文件,再按文件修改日期提取 yyyymmdd,复制到对应归档目录,同时用正则统计 ERROR 行数写入统计表。我确认后,它才开始生成正式脚本。
这里我特意留意了一个细节:模型生成的脚本路径不是硬编码的固定值,而是通过读取当前目录树动态构造的 file_list。代码如下:
import os import re import shutil from pathlib import Path from collections import defaultdict raw_root = Path("/logs/raw") archive_root = Path("/logs/archive") date_to_files = defaultdict(list) error_counter = defaultdict(int) for root, dirs, files in os.walk(raw_root): for name in files: if not name.endswith(".log"): continue p = Path(root) / name mtime = p.stat().st_mtime date_str = time.strftime("%Y%m%d", time.localtime(mtime)) date_to_files[date_str].append(p) with open(p, "r", encoding="utf-8", errors="ignore") as f: for line in f: if re.search(r"\bERROR\b", line): error_counter[date_str] += 1 for date_str, files in date_to_files.items(): dest = archive_root / date_str dest.mkdir(parents=True, exist_ok=True) for f in files: shutil.copy2(f, dest / f.name)脚本生成之后并没有立刻执行。工具调用层把 write_file 的参数展示给我看,我确认了目标路径和内容,然后运行了第二遍解析,才真正写入文件并执行 python 命令。这个“二次确认”看起来繁琐,但对于一个会异想天开生成 rm 命令的模型来说,是必要的安全网。
4.3 中途检查与结果修正
脚本第一次执行抛了一个异常——模型忘了 import time,而我在演示前特意没有提示它。free-claude-code 抓到了 stderr 报错信息,自动把报错喂回给模型,模型很快定位到缺失的 time 模块,重新生成修正版脚本。这一轮“自我修复”没有人工参与,全部发生在转录链路内部。
第二版脚本跑完后,summary.md 生成了,里面按日期列出了归档文件数量和 ERROR 统计数。我没有直接用它的结果,而是手动抽查了两个目录,确认归档内容一致、统计行数和我用 grep 手工数的吻合。结果没问题。整轮交互从输入任务到最终交付,一共用了 8 次工具调用,耗时约 2 分钟,其中大部分时间花费在模型推理上。
这个演示给我最大的感触是:它不是 AI 自动编程,而是“人在回路里的 AI 编程助手”。每一步关键动作都有确认点,出错之后能自我修正,这才是自托管方案相对云端方案最实在的优势。
5. 我踩过的几个坑及排查思路
5.1 模型输出带上了一整块 Markdown 代码围栏,执行层直接识别失败
第一次让 free-claude-code 生成脚本并执行时,工具调用层一直报解析错误。打开日志看模型原始输出,发现它在返回工具调用对象之前,先输出了一大段说明文字,里面还夹着 Markdown 的代码围栏。解析器只允许纯 JSON 块,于是整个工具调用被判定为非法,流程卡住。
这是很多本地模型的通病:它太想“展示代码”,反而忘了协议要求。我的修复分两层。第一层是防御和解析:解析器先剔除正则匹配到的python 和包围的代码块,再找 JSON 起始括号。第二层是治本:在系统提示词的底部明确加了一句“任何情况下,工具调用必须以 JSON 格式单独输出,禁止放在 Markdown 围栏内,禁止附带解释文字”。改完之后,同一个模型连续十次生成都没有再出现问题。
5.2 长对话进行到一半,规则文件悄悄失效了
用了两天,我发现一个诡异现象:对话刚开始时,模型严格遵守“危险命令需确认”的规则,但连续聊了二十多轮以后,它开始直接返回命令执行结果,跳过了确认环节。一开始我以为是模型变聪明了,懂得省略确认,后来看日志才发现不是。
根本原因是上下文压缩。本地模型的 contextWindow 只有 8192,多轮交互后,早期写入的系统提示词被挤到了截断区间之外,相当于模型已经忘记自己还有一套规则。这个问题的经典程度堪比程序员的“缓存失效”:规则只在模型记得它的时候生效。
我的应对方案是组合拳。第一,把硬规则从提示词里剥出来,同样的事情在代码层做二次校验。第二,在每次工具调用前,把最核心的三条安全规则重新注入到消息序列的末尾,而不是只放在开头。这样即使模型没有长记忆,最近一轮输入里也一定有规则的锚点。第三,contextWindow 调小到 7000 左右,给系统提示词和最近几轮对话留出足够空间,宁可丢更早的历史,也不能丢系统指令。
5.3 模型差一点跑了一条“空变量 rm -rf”,被确认机制拦了下来
这是最惊险的一次。我让它清理一个临时目录里的旧缓存文件,模型生成的 shell 命令大致长这样:
rm -rf /logs/cache/$BUILD_ID/正常情况下没问题,但在当时的环境里 BUILD_ID 环境变量是空的,命令解析出来变成了:
rm -rf /logs/cache/如果不是代码层在 rm -rf 前缀上强制挂了人工确认,这一下就会把整个缓存目录连同上级目录里其他东西一起删掉。事后排查日志,模型没有任何异常,它自己写的变量引用完全合理,只是没有验证执行环境里变量是否存在。
这个坑让我重新认识了规则引擎的必要性。模型对执行环境的感知是有限的,它看不到环境变量是否为空、当前用户是什么权限、某个路径是否软链接。所有依赖外部状态的操作,都必须经过一次人工确认。后来我在规则配置文件里额外加了一条:run_command 在执行前会把命令渲染成最终形态展示给用户,并且检查环境变量占位符是否为空。
5.4 路径分隔符与相对路径:跨平台接项目时的隐藏地雷
在 Linux 上跑通所有流程后,我想在 Windows 开发机上跑同一套环境,结果 list_files 返回的路径用的是反斜杠,read_file 工具拿这个路径去读文件时直接失败。问题是模型生成路径时自带平台偏好,而工具调用层没有做归一化。
修复方式是统一让所有文件工具接收相对路径,路径分隔符统一转成 /,到执行层再根据平台转换成系统原生的分隔符。这样模型不用感知运行平台,工具层负责适配。我建议任何自托管助手项目都尽早做这一层抽象,不要等到双平台跑起来才回头补。
6. 进阶用法:把 free-claude-code 接进日常开发流
6.1 不止对话:几种实用的调用姿势
命令行对话只是最基础的一层。我实际用得最多的是一个脚本化模式:直接传一段任务文本和项目路径,让 free-claude-code 非交互地执行一轮任务并退出,适合处理重复性工作。比如每次发布前,我让它扫描代码库里的 TODO 标记和调试输出,生成一份清单,整个过程不需要我盯终端。
另一个姿势是配合编辑器使用。在编辑器里选中一段代码,通过自定义快捷键把代码作为上下文发送给助手,让它输出修改建议。这一层操作起来非常轻,实际上就是调用了 CLI 的某个子命令,传入参数是临时文件路径。
还有一个很实用的场景是提交前审查。我习惯把 git diff 导出成临时文件,然后让 free-claude-code 只看这份 diff,重点挑出可能导致编译失败的引用、遗漏的错误处理、魔法数字改动。因为输入局限在 diff 范围内,模型不容易跑偏。
6.2 离线环境下的建议
如果你需要在完全离线的环境里使用,需要注意模型推理服务的部署方式。量化版本的开源模型在中等配置的单机上就能跑,但上下文窗口和并发能力都会受限,建议把任务拆小。离线环境下更要依赖规则文件,因为模型没有额外的互联网知识更新,它的行为完全由系统提示词和参数决定,预设的规则就是最后一道防线。
关于模型选择,我个人的标准是先看代码生成评测结果,而不是看模型体积。7B 量级的开源模型配合低 temperature,在代码生成场景下表现非常稳定,偶尔还会写出让我意外的简洁实现;14B 或更大参数的模型聪明一些,但在长上下文和工具调用格式上更不稳定,需要更多调教。选型没有绝对答案,先小规模试跑一周,再决定是否切换。
最后说一条贯穿始终的安全习惯:一旦你决定用自托管 AI 编程助手处理代码文件,先在工作目录里初始化版本控制。任何自动生成或修改的内容,都能通过 diff 和还原命令回到之前的状态。这个习惯救过我很多次,比模型的任何自我修复机制都可靠得多。