从去年开始,我就在折腾一个自用的 AI 编程助手项目,代号叫 free-claude-code。说白了,我想要的并不是再买一个按量计费的商业插件,而是一个自己能掌控的“代码副驾”:能补全代码、能听懂我一句“把这里的解析逻辑改成异步”、能解释一段陌生代码,同时不用把整个仓库丢到第三方服务上。这个项目经过几次推倒重来后,现在已经稳定跑在我每天的开发环境里,帮我处理大量重复重构、日志排查和样板代码生成。这篇文章想把整个链路拆开讲清楚:整体架构、模型选型、工具调用协议、编辑器集成,以及实际踩过的坑。无论你是想自建编程助手,还是对“大模型怎么真正动手改代码”感兴趣,都能从这里拿到一套可落地的参考。
先说清楚一个边界:free-claude-code 是我自己维护的开源工程代号,里面的 claude 只是随手起的内部叫法,不代表任何商业产品。整个项目没有训练基座模型,也没有修改权重,我做的事情是把现有模型能力通过工程化手段封装成一套适合“改代码”的交互系统。这也是我想强调的核心观点:别想着从头训练一个编程模型,普通开发者真正该做的,是在现有模型之上做好提示词、工具协议、上下文管理这三件事。
1. 项目整体设计:不是训练模型,而是“驯服”模型
1.1 三个让我决定自建的痛点
先说动机。商业编程助手我用过一段时间,功能确实强,但有三个问题始终绕不过去。
第一是成本不可控。按量计费的模式下,重度使用一个月下来账单相当肉疼,尤其是我这种习惯频繁让助手改写整个函数甚至模块的人,Token 消耗速度远超预期。第二是代码隐私。工作上经常要处理未公开的业务逻辑,把代码片段甚至整个项目结构发给外部服务,心里那道坎过不去。虽然很多商业方案承诺“数据不用训练”,但“不训练”不等于“绝对不上传”,这个区别在敏感项目里很致命。第三是定制能力太弱。我想要“助手修改代码后自动运行测试并把结果回填给模型再迭代”,这种多步工具调用工作流在商业产品里往往被严格限制,你只能在它给定的交互模式里使用。
所以我的结论很直接:要真正让 AI 编程助手融入自己的开发习惯,必须自己掌握工具链。free-claude-code 的定位也因此确定下来:一个可自托管的、面向代码修改场景的 AI 辅助工具,模型层可替换,工具层自由扩展,交互层自己定义。
1.2 技术选型:本地模型为主,云端 API 兜底
选型的时候,我第一时间排除了“自己训练模型”的方案。训练一个代码模型需要的数据量和算力,不是个人项目能承受的,而且开源社区已经有大量效果不错的基座模型,直接拿来用才是正路。
我的选型思路分两层。本地推理负责常规任务,云端 API 负责兜底。本地跑一个中等参数量的量化模型,日常的代码补全、解释、简单重构都能应付,而且因为推理在本地,隐私和成本都解决了。遇到本地模型实在搞不定的复杂任务,比如跨多个文件的重大重构,再转发到云端通用大模型 API,按量付费但只偶尔用,成本也可以接受。
| 维度 | 本地模型方案 | 云端 API 方案 |
|---|---|---|
| 隐私性 | 完全本地,数据不出机器 | 数据会发送到服务端 |
| 成本 | 一次性硬件投入,电费可忽略 | 按 Token 计费,重度使用较贵 |
| 离线可用 | 可以 | 依赖网络 |
| 模型能力 | 受限于硬件参数 | 通常强于本地小模型 |
| 定制程度 | 完全可控 | 受服务商限制 |
这个表格基本概括了我的取舍逻辑。对于主力开发机,本地模型优先;对于服务器上的批处理任务或临时需要强推理的场景,配置云端 API 作为 fallback。这条策略我一直用到现在,实测下来稳定性很好。
1.3 功能边界:什么能做,什么不做
明确边界很重要,否则项目会无限膨胀。free-claude-code 一开始我就划了几条硬边界:不做训练、不做数据收集、不搞复杂的图形界面。核心只做四件事:代码补全、自然语言指令改代码、代码解释和问答、执行工具调用(读文件、写文件、跑命令)。
边界划定之后,架构就清晰了。四层结构:模型层负责推理和输出;服务层负责封装提示词、维护会话上下文、解析模型输出;工具层把“读文件、写文件、执行命令”等能力抽象成统一接口;交互层提供 CLI 和编辑器插件入口。这样拆分的最大好处是每一层都能独立替换,尤其是模型层,今天用这套权重,明天换另一个厂商的 API,服务层和工具层完全不用动。
2. 模型层:选型、量化与提示词调优
2.1 本地模型怎么选
很多朋友一上来就问“哪个模型最好”,这种问法其实没意义,要看你的硬件条件和任务场景。我把我用过的方式分成三档,供大家参考:
第一档是 7B 级别的小模型,量化到 4bit 之后大概占用 5~6GB 显存,普通消费级显卡就能跑。速度很快,适合做单行补全、简单解释这类轻量任务,但复杂推理和长上下文任务明显吃力。第二档是 13B 级别,同样量化后需要 10GB 左右显存,综合能力比 7B 强一截,代码理解和中等难度重构都能胜任,这是我目前的主力。第三档是 70B 级别,量化后也得 40GB 以上显存,个人工作站体验很好,但如果你的机器跑不动,建议直接走云端 API。
我的建议是:不要追求参数最大,而是选择“在你的硬件上能以每秒 10 个 Token 以上速度生成”的模型。AI 编程助手对交互延迟极其敏感,如果生成速度像老式打字机,人根本等不住。实测下来,13B + 4bit 量化在我的开发机上能做到每秒 15~20 Token,配合流式输出,体感已经接近商业产品。
2.2 系统提示词:把模型“按”在正确的输出格式上
模型选好只是开始,提示词才是决定效果的关键。我见过太多人直接把裸模型接进来,然后抱怨“AI 写代码胡说八道”,实际上很大程度是系统提示词没做好。
我给 free-claude-code 设计了一套强调“输出格式”的系统提示词,核心思想是:不要试图让模型自由发挥,而是用强约束格式把它的输出限定在可解析的范围内。这套提示词经过多次迭代,目前包含五个要素:角色定义、任务模式、输出协议、禁止项、示例片段。
角色定义决定模型的“人设”,我让它充当“一个熟悉多种编程语言的资深开发者”。任务模式告诉它“你通过自定义动作协议修改代码”,而不是直接输出大段代码。输出协议是最重要的,我要求所有需要改动文件的操作必须走结构化动作,禁止直接输出“整个文件”或“大段 diff”。禁止项包括不要解释多余内容、不要生成无关建议、不要编造不存在的文件路径。示例片段则是给出一两个标准动作示例,让模型照猫画虎。
这套提示词落地为实际文本大概有 400 多个字,不算长,但约束效果非常明显。改之前模型经常自作主张输出一整个文件替换,改之后基本都能按照动作协议输出精准的定位修改。这个经验后来我也用到其他项目里,发现“格式约束”永远优先于“语义引导”。
2.3 自研编辑动作协议:让模型学会“手术式改代码”
模型直接生成代码是一回事,让它修改现有代码是另一回事。如果模型输出的是完整新文件,覆盖原文件时很容易把无关内容搞丢;如果输出 diff,解析和落版又是一堆兼容性问题。
我采用的方案是自定义一种极简的动作协议,本质是一个 JSON 对象,包含动作类型、目标路径、定位内容和替换内容。常见的动作有三类:replace 用于替换指定代码块,insert 用于在某个锚点后插入内容,delete 用于删除指定代码块。
{ "action": "replace", "path": "src/parser.py", "search": "def parse_line(line):\n return line.strip()", "replace": "def parse_line(line: str) -> str:\n _value = line.strip()\n return _value" }模型输出这个 JSON 后,服务端做三件事:校验路径合法性、定位 search 内容、执行替换。如果 search 内容在目标文件中找不到,修改会被拒绝并反馈给模型重新生成。这套协议很笨,但极其有效,因为它把“模型应该怎么改”这个模糊问题变成了“定位字符串 + 替换字符串”的精确操作,解析和回滚都变简单了。后来我又加了 rollback 机制:每次修改前自动备份原文件内容,修改失败或不符合预期时一键还原。
3. 服务端实现:FastAPI 接入模型与工具调用
3.1 项目骨架与核心接口
服务端我选的是 Python 生态的 FastAPI。选它的原因很朴素:异步原生,WebSocket 支持好,适合做流式输出;类型提示和自动文档能减少不少调试时间;生态里直接集成了 SSE 等流式传输方式,做 AI 交互很顺手。
项目骨架比较简单,核心是一个接受会话消息的 POST 接口。客户端把用户的自然语言指令、当前文件路径、光标位置附近代码一起传上来,服务端把这些信息组装成上下文,调用模型推理,然后返回模型生成的响应(包括普通文本回复和动作序列)。
from fastapi import FastAPI from pydantic import BaseModel class CodeRequest(BaseModel): session_id: str instruction: str file_path: str | None = None code_context: str | None = None history: list[dict] = [] app = FastAPI() @app.post("/chat") async def chat(req: CodeRequest): messages = build_messages(req) result = await run_model(messages) actions = parse_actions(result) return {"reply": result, "actions": actions}实际生产版本比这个复杂,多了认证、限流、日志追踪和错误处理,但核心骨架就是这样。我在开发过程中一个很深的体会是:接口设计要尽量薄,服务端千万不要自己判断“该不该改代码”,把所有决策都丢给模型,服务端只做解析、校验和执行。这样职责单一,排查问题容易得多。
3.2 工具调用协议与安全边界
模型不能只改文件,很多时候还要读文件、搜索项目结构、甚至执行命令。free-claude-code 的工具层目前挂了四类工具:读文件、写文件、执行命令、搜索关键词。
工具调用本质上也是动作协议,只是把动作对象从“改动文件”扩展成“调用函数”。模型输出一个包含 tool 名称和参数的对象,服务端查表分发到对应函数执行。
{ "tool": "run_command", "arguments": { "command": "pytest tests/test_parser.py", "timeout": 120 } }这里最关键的是安全边界。模型输出的命令不可信,这必须成为一条铁律。我的处理方式有三个层级:第一,所有工具执行都限定在项目根目录内,路径必须经过规范化处理后才能拼接;第二,命令执行统一走白名单机制,只允许预设命令(比如 pytest、node、git status)以及显式授权的命令;第三,任何写操作执行前都自动创建备份。这套规矩救了我很多次,有一次模型不知怎么就输出了一条清理命令,被白名单挡住了,事后想想都后怕。
3.3 沙箱与权限控制
如果你的项目要跑不可信的模型输出,光靠白名单还不够,最好上到系统层面隔离。我的做法是把工具执行放在一个受限目录里,必要时直接容器化,把项目目录只读挂载进去,让工具只能通过特定接口读取和写入。
有人觉得这一步“过度设计”,但我的实测经验恰恰相反。模型输出在语法上完全合理、语义上却是破坏性的情况并不罕见,尤其是当上下文里有诱导性内容时,它可能执行递归删除这种灾难操作。我在沙箱上花的时间,最终换来的是可以放心让模型执行命令而不用人工盯着每一步。给同样想做的朋友一个建议:权限系统从第一天就做,后面补会很痛。
4. CLI 与编辑器集成:从终端命令到鼠标选中
4.1 先做一个能用的 CLI
服务端跑通后,交互层我首先做了一个 CLI,原因很直接:CLI 是测试工具能力成本最低的方式,可以在终端里快速模拟后续编辑器的调用场景。
CLI 的核心循环非常朴素:读取用户输入,携带上下文发到服务端,展示回复,如果回复里有动作序列就直接执行并打印结果。新版加了交互式补全,用户输入@file src/utils.py后面再跟自然语言指令,CLI 会自动把文件内容放入上下文;@selection表示把当前终端选中(或剪贴板)的代码作为处理对象。
$ free-claude > @file src/parser.py 帮我优化这个函数的循环写法,不要改变对外行为CLI 交互模式天然适合脚本化调用,比如配合 git hooks 做提交前自动 review。这些能力后来又复用到了编辑器插件里,等于一份核心逻辑多端使用。
4.2 编辑器侧集成:走 LSP 思路
CLI 能跑通之后,编辑器集成就很顺了。现在的插件体系五花八门,我没有针对单一编辑器写死实现,而是走 LSP 语言服务协议思想。插件本身只负责两件事:把当前文件的上下文交给服务端、把服务端返回的结果展示出来。对选中代码发指令、拿到修改动作后自动落盘,这些逻辑全在服务端和协议层完成,插件只是一个很薄的壳。
这样做的收益很明显:换编辑器几乎不用动核心代码,只要照着协议重新写一个几十行的插件壳子就行。我自己就同时维护了终端 CLI 和一个编辑器插件,两边体验一致。如果你是从零做起,我建议也走这条路,别用某个编辑器私有 API 把项目绑死。
4.3 配置管理与多模型切换
多模型切换是我刻意保留的一个功能。本地模型和云端 API 的使用场景不同,配置管理必须简单。
我用的配置方案是一个 YAML 文件,指定默认模型、备选模型、量化参数、上下文窗口限制等。运行时通过环境变量覆盖关键项,方便在服务器和本机之间切换。
active_profile: local profiles: local: provider: llama_cpp model_path: /models/13b-q4.gguf n_ctx: 8192 temperature: 0.2 cloud: provider: openai_compatible_api endpoint: http://127.0.0.1:8000/v1 api_key_env: API_TOKEN model_name: code-generator-v2多配置这件事看着不起眼,实际用起来才知道多香。白天干活用 local 配置,晚上跑批量任务就直接切到 cloud 配置,不用改代码,一条命令的事。
5. 常见问题与实战排查
5.1 上下文窗口爆掉之后,我是怎么兜底的
自建编程助手最容易踩的坑就是上下文窗口溢出。对话一长,Token 数就高,本地模型直接变慢甚至报错,云端 API 则是账单飙升。
我的兜底方案是三层。第一层是滚动窗口,只保留最近 N 轮对话,更早的塞进文字摘要;第二层是局部注入,改代码时不要给整份文件,而是只给“光标所在函数 + 周围几行”,这个策略效果极好,模型注意力更集中,输出反而更准;第三层是路由分流,如果检测到用户指令涉及多个文件甚至整个仓库的问题,直接把请求转给云端 API,发挥它的长上下文能力。这套组合跑到现在,还没有出现真正“上下文把我卡死”的情况。
5.2 模型“幻觉”路径导致误改文件
另一个高频问题:模型生成文件路径时会产生幻觉,指向一个实际不存在的目录或文件。这个问题的根源在于大模型是在 token 层做概率预测,它并没有“真的看过”你的项目结构。
我的解法是在提示词里强制要求路径引用必须先读目录树,得到准确路径后再生成动作;服务端再做一个纵深防御,所有路径必须经过规范化处理,并且检查前缀是否属于项目根目录。一旦发现路径不存在,修改动作直接返回错误给模型,让它重新读取目录树。经过这几层过滤,误改文件的概率已经降到了我可以接受的水平。另外,自动备份在关键时刻真的救过我,现在已经是我每次提交代码之前都会确认开启的功能。
5.3 输出不稳定:四个立竿见影的调参手段
如果你发现模型输出时好时坏,大概率不是模型本身的问题,而是你还没把任务的“确定性”提上来。我的经验有四条:第一,把 temperature 调到 0.2 以下,代码生成任务要的是确定性,不是创造力;第二,固定 few-shot 示例,在系统提示词里始终放同一个标准动作示例,模型行为一致性会明显提升;第三,禁用采样相关选项(比如 top_p 设置),减少随机性;第四,对模型输出做严格 JSON 校验,格式不对就让模型重试一次,而不是冒险去解析“接近但不完全合法”的输出。
这四招各解决不同环节的波动,叠在一起效果非常明显。我的实测数据是:在没调参之前,模型返回合法动作序列的比率大概 85%,调完基本稳定在 98% 以上,这个差距直接决定了工具敢不敢“自动执行”。
5.4 问题速查表
| 症状 | 可能原因 | 处理办法 |
|---|---|---|
| 生成速度极慢 | 上下文过长 / 量化档位太低 | 裁剪上下文,降低 n_ctx,升级量化 |
| 修改动作找不到 search 文本 | 代码上下文与文件实际内容不一致 | 强制重新读取文件片段,从真实内容里取锚点 |
| 模型返回一堆废话 | 系统提示词里缺输出协议 | 补上强格式约束和禁止项 |
| 路径写错 | 模型没看目录树 | 提示词要求先读目录树再生成动作 |
| 工具执行超时 | 命令本身挂起 | 加 timeout 参数,设置合理上限 |
| 云端 API 调用报鉴权错 | 环境变量没传对 | 检查 .env 和 profile 配置 |
这张表是我实际 Debug 过程中沉淀出来的,基本收录了使用频率最高的排查路径。每次遇到新问题,我都会往里面补一行,现在已经是我项目里最值钱的文档之一。
6. 经验沉淀
项目做到这个阶段,我自己最大的体会是:AI 编程助手的瓶颈往往不在模型,而在工程化。一套设计良好的动作协议、一套严格的权限沙箱、一条稳定可控的上下文管理策略,这三件事比换一个更强的大模型更能提升实际体验。我试过很多种模型配置,最后发现“能稳定输出合法动作序列”的模型,远比“偶尔写出惊艳代码但经常格式出走”的模型更适合做自动化工具。
另外一个很重要的心得:一定要给自己留后门。无论是文件备份、命令白名单、还是动作回滚机制,这些“保守”设计在项目初期看起来拖慢速度,但遇到一次模型输出异常就知道值回票价。这也是为什么我现在能放心让 free-claude-code 在我的开发环境里自动改文件、跑测试,而不需要盯着每一步操作。工具越自动化,安全边界越要前置,这是所有自建 AI 辅助系统的人都该刻在心里的原则。