资讯动态

free-claude-code:自托管终端编码助手架构与实战

发布时间:2026/10/17 13:44:42 来源:尧图企业网站定制
你有没有遇到过这种情况改一个 bug 要翻遍几个文件写完脚本还要手动查文档终端里同时开三个 tab一个跑测试、一个查日志、一个翻文档。我自己折腾了一个叫free-claude-code的命令行工具它把这些事全部收敛到同一个对话里让它读代码、改文件、跑命令最后汇总结果给你。这篇文章就把整个从零开发到日常使用的东西都交出来包括架构设计、核心代码、踩坑记录。free-claude-code是一个开源的、可自托管的终端编码助手核心思路是“一个贴近自然语言的工具调度器”。它不绑定任何特定厂商只要你有一个兼容标准接口的大模型服务就能把“读文件、改文件、执行命令、搜索代码”这些高频操作全部交给它。无论你是厌倦了反复切换工具的独立开发者还是想在团队里做一套统一编码助手的开发者这篇文章都值得花十分钟看完。1. 整体设计思路与项目规划1.1 为什么我选择自建而不是直接使用现成的商业服务最开始我也直接用过几家商业编码助手体验确实好但它们有几个让我不舒服的点第一费用不可控按量计费在重度使用下很容易超预算第二数据要经过第三方服务公司内部项目不方便直接扔上去第三工具行为是一个“黑盒”我想让它支持内部脚本和私有工具扩展起来很麻烦。于是决定自建一个脚手架目标不是复刻商业产品而是做一个“够用、可扩展、本地优先”的终端代理。设计之初我给自己定了三条硬性指标命令行启动后能连续对话模型可以调用我本地的工具读写文件、执行命令所有配置都能通过一个配置文件修改不写死在代码里。free-claude-code这个命名也反映了我对它的定位free 指的是开源自由不是指一定要零成本运行claude-code 这部分则是对“通用编码智能体”的指代。我更看重的是把“让模型操作终端”这个模式抽离出来让大家能基于自己的模型和工具组合去复用。1.2 技术选型为什么用 Python 而不是 Node.js 或 Go做 CLI 工具Node.js 和 Go 都是热门选择但我最后还是选了 Python 3.10。原因很简单团队和社区里的 AI 相关库大多数都是 Python 优先后续接向量检索、接本地推理框架Python 生态最顺另一个原因是写原型快一个周末就能跑通核心循环。交互层上我用了prompt_toolkit它能很好地支持多行输入、历史记录、自动补全。比起readline它在 Windows 和 Linux 下的体验更一致也不会出现输入中文时光标错位的问题。命令解析和参数校验用了 Typer实际上就是一个基于类型注解的 CLI 框架写起来比 argparse 舒服太多。模型接入层我选择实现一套“兼容标准对话接口”的客户端而不是绑定某个特定模型 SDK。这样无论是本地部署的开源模型还是各种云端模型服务只要它们提供chat/completions风格的接口就能直接对接。这一点是后期最省心的地方我测试模型时只需要改base_url和model_name代码完全不用动。1.3 整体架构从用户输入到工具执行的全链路整个系统分成四层交互层、代理循环层、工具层、模型层。交互层负责读取用户输入展示流式输出管理会话历史。代理循环层是核心它维护一个messages数组不断把用户意图、模型回应、工具执行结果填进去再发给模型。工具层就是一组注册好的函数模型一旦返回“调用某个工具”的指令代理循环就去执行对应的函数。模型层负责把请求发出去并把模型返回的工具调用指令解析出来。用户输入 - 交互层 - 代理循环 - 模型层 - 返回响应 ↑ ↓工具调用指令 工具执行结果 - 工具层这四层之间用 JSON 消息串起来所以调试的时候可以直接打印messages数组看看模型到底收到了什么。后面排查问题依赖这个能力比想象中更重要。2. 核心细节解析与实操要点2.1 会话上下文管理的核心逻辑编码助手和普通聊天机器人最大的区别在于上下文结构。每次对话不是简单的“你一句我一句”而是要把“用户需求、代码文件内容、命令输出、模型分析”混在一起。我一开始只把用户输入和模型输出塞进messages后来发现模型常常在第二轮就忘了前面让它改的是哪个文件。所以free-claude-code把上下文拆成了两个部分基础指令System Prompt和动态消息。基础指令里固定写清楚工具的用法、输出格式要求、安全约束动态消息就是不断增长的用户请求、工具调用记录和执行结果。消息管理这里有个容易忽视的细节不要把所有工具结果原封不动塞回去。比如run_command执行一个make test可能输出几千行日志如果不加截断下一轮模型根本看不完全而且 token 消耗会爆炸。我的做法是默认只保留每条命令输出的前 2000 字符超出部分提示模型“输出已截断请用更精确的命令重试”。2.2 工具注册机制让模型能“看见”能操作什么模型本身不知道你能提供什么工具它只知道你在 System Prompt 里写了什么。所以我设计了一个装饰器注册表用函数签名自动生成工具描述。import json import inspect TOOL_REGISTRY {} def tool(name, description): def decorator(func): signature inspect.signature(func) parameters { type: object, properties: {}, required: [] } for param_name, param in signature.parameters.items(): param_type string default param.default if param.default is not inspect.Parameter.empty: if isinstance(default, bool): param_type boolean elif isinstance(default, int): param_type integer parameters[properties][param_name] {type: param_type} if param.default is inspect.Parameter.empty: parameters[required].append(param_name) TOOL_REGISTRY[name] { description: description, parameters: parameters, function: func } return func return decorator这段代码的作用是把read_file(path)、run_command(command)这类 Python 函数的签名转换成模型需要的 JSON Schema。模型响应里的工具调用请求会带上参数名和值我只要在代理循环里匹配注册表里的名称再把参数**kwargs传给函数即可。这里实践上的一个坑是工具的description写得越具体模型的调用准确率越高。比如模糊地写“执行命令”模型会在不确定时反复问你而非调用工具但如果写清楚“执行 shell 命令输出会被截断超时默认为 30 秒”模型的判断就果断很多。2.3 工具调用循环别让模型永远在“计划”而不动手代理循环是整个项目里最容易写出 bug 的地方。最初的实现是一个while True每次循环都调一次模型直到模型返回普通文本。问题在于某些参数化模型在没有明确“停止调用工具”限制时会陷入“计划 - 调用 - 再计划 - 再调用”的无限循环里甚至明明已经完成任务还继续调用工具。我的解决方案是引入“最大工具调用次数”和“观察结果注入”。每一轮工具调用结果会以role: tool的消息加入上下文同时累计调用次数。当次数超过max_tool_calls默认 8 次时强制让模型停止调用工具直接输出当前状态摘要。这避免了预算失控也反过来倒逼模型在有限的步骤里尽量高效。另一个重要细节是模型返回的普通文本也要在用户界面上流式显示。我用生成器逐个词输出而不是等完整响应后再打印。这样用户体验会好很多尤其是在长回答时能看到实时输出而不是转圈等待。2.4 模型接入兼容标准chat/completions格式的客户端我之前提到过不绑定 SDK实际客户端代码非常简单大概 40 行。它接收model、messages、tools、tool_choice这些参数然后发到配置的 endpoint。这里的关键是接口返回的finish_reason如果是tool_calls就要把响应里的工具调用请求解析出来而不是当成普通文本直接输出。import httpx class ChatClient: def __init__(self, base_url, api_key, model): self.base_url base_url.rstrip(/) self.api_key api_key self.model model def chat(self, messages, toolsNone, tool_choiceNone): url f{self.base_url}/chat/completions headers {Authorization: fBearer {self.api_key}} payload {model: self.model, messages: messages} if tools: payload[tools] tools if tool_choice: payload[tool_choice] tool_choice with httpx.Client(timeout60) as client: resp client.post(url, headersheaders, jsonpayload) resp.raise_for_status() return resp.json()这个模块设计成只依赖httpx方便测试时 mock。我在项目里加了一个mock_model.py专门模拟返回虚假的工具调用命令用来测试代理循环是否正确执行。没有这一步调试真实模型时的成本会高到让人崩溃。3. 完整实操过程与核心环节实现3.1 环境初始化与目录结构项目从空目录开始我建议你保持以下结构free-claude-code/ ├── config.yaml ├── main.py ├── agent/ │ ├── __init__.py │ ├── core.py │ ├── tools.py │ └── client.py ├── tools/ │ ├── __init__.py │ ├── file_ops.py │ └── shell_ops.py └── requirements.txtconfig.yaml是全局配置的中心它长这样model: base_url: http://127.0.0.1:8000/v1 api_key: local-key model_name: local-model temperature: 0.1 agent: max_tool_calls: 8 max_output_chars: 2000 default_timeout: 30 confirm_before_exec: trueconfirm_before_exec建议保留为true也就是每次模型要执行命令前都让用户按一个y确认。虽然多了一步但能挡住不少模型自说自话乱删文件的情况。我在真实使用中只关过一次后来又赶紧打开了。3.2 代理循环核心代码实现代理循环是整个项目的大脑。下面这段是简化版但保留了两个关键机制工具调用轮转和超限停止。def run_agent(agent, user_input): agent.messages.append({role: user, content: user_input}) tool_count 0 while tool_count agent.max_tool_calls: response agent.chat() if response[finish_reason] ! tool_calls: agent.messages.append({ role: assistant, content: response[message][content] }) break tool_call response[message][tool_calls][0] tool_name tool_call[function][name] tool_args json.loads(tool_call[function][arguments]) if agent.confirm_before_exec and tool_name run_command: confirm input(f[执行] {tool_args}确认? [y/N] ) if confirm.lower() ! y: result 用户取消了命令执行 else: result execute_tool(tool_name, tool_args) else: result execute_tool(tool_name, tool_args) agent.messages.append({ role: tool, tool_call_id: tool_call[id], content: truncate(result, agent.max_output_chars) }) tool_count 1 else: agent.messages.append({ role: user, content: [系统] 工具调用次数已用完请直接给出阶段性总结。 }) agent.chat()这段代码里有个容易被忽略的地方tool_call[id]很重要模型返回的工具调用消息会带一个唯一 ID后续工具结果必须通过tool_call_id关联回去。如果不带这个 id很多模型会报错或直接忽略结果。3.3 工具函数实现读文件、写文件、执行命令工具层我实现了五个常用操作读文件、写文件、列出目录、执行命令、文本搜索。以读文件为例关键点是支持offset和limit避免一次性读整个大文件让模型学会“分段读取”。tool(read_file, 读取文本文件按行范围返回内容。) def read_file(path: str, offset: int 0, limit: int 100): with open(path, r, encodingutf-8) as f: lines f.readlines() return .join(lines[offset:offset limit])这里的一个技巧是默认offset是 0模型如果在代码里发现要修改的位置在第 200 行第二次调用会带上{offset: 200, limit: 100}。这样每次读文件的 token 开销都很小模型也能拿到准确的目标代码段。执行命令的时候我用了subprocess并且强制加上了超时。因为一个模型热启动时经常会产生“先休眠一下”的奇怪念头如果命令是sleep 1000没有超时整个代理就会卡死。超时默认 30 秒命令结束时如果返回非零退出码我会把stderr拼到结果里模型才能知道报错信息。3.4 参数调优与几个真实任务的验证核心循环跑通后我用三个任务做了一次冒烟测试第一个任务是“修改某个 Python 文件里的函数名并同步修改所有调用点”。模型会先搜索哪些文件引用了旧函数名再用sed或者write_file替换。实测在代码量 2000 行以内时工具调用次数 3-5 次就能完成。第二个任务是“跑测试并修复失败用例”。模型需要执行pytest然后根据报错信息定位文件、修改代码、再跑一遍。这一步经常遇到的是模型在读取错误信息后把错误原因理解错了。我通过调高temperature到 0.1明显减少了这种“自信拍脑袋”的行为。第三个任务是“把某个目录下的所有 markdown 文件里标题层级统一”。模型会通过list_dir找到.md文件再逐个read_file检查标题最后write_file修改。这个任务暴露了上下文问题当文件很多时模型容易漏掉一部分。解决方法是在 User Prompt 里加上一句“请先列出所有目标文件再逐一处理”。这三个任务验证下来系统算是达到可日常使用的水平。当然它离商业产品还有距离但作为一个自托管工具已经能解决我八成以上的终端编码场景。4. 常见问题排查与实操避坑4.1 模型陷入工具调用死循环这是最经典的问题表现是模型一直在调用工具每次调用的参数都差不多不输出普通文本。排查方式有两个先用max_tool_calls限制兜底再定位是不是工具描述写得不清楚导致模型无法判断完成条件。我遇到过一个具体场景让模型“统计项目代码行数”它反复调用run_command执行find . | xargs wc -l但每次拿到的输出都是“输出太长已截断”所以它一直重试。后面我改成让模型只执行一次如果截断就换个更精确的命令。同时我加了一条 System Prompt“如果一个命令输出过长请换更精确的命令不要重复相同命令。”4.2 模型上下文膨胀导致指令丢失当对话进行到二三十轮后模型开始忽略早先的工具结果甚至会出现“重新清空文件”这种危险操作。我这边收集到的典型例子是模型在第五轮修改了一个文件到第十二轮又忘掉了直接根据新的输入覆盖了文件内容。效率最高的办法不是无限扩大上下文而是做一个“关键事实压缩层”。每完成一个工具任务我就提取一句摘要放进一个独立的summary字段下次发请求时挂到 System Prompt 后面。比如“已完成的修改将parse_config函数重命名为load_config并更新所有引用”。这个摘要通常不超过 200 字符但比保留一堆历史记录有用得多。4.3 工具权限过宽导致误操作如果让模型自由执行rm、mv等命令风险很大。有一次模型为了清理临时文件执行了rm -rf build/我本来没打算现在就清理结果整个构建目录没了。好在源码目录还在但足以让我下定决心加权限控制。我的方案是命令前缀白名单默认只放行git、python、pytest、ls、cat、grep等安全命令遇到rm、mv、dd等需要交互确认。如果你用的模型能力足够强还可以在工具描述里写上“删除文件前必须调用list_dir确认路径”。不过最稳妥的还是“确认模式常开不然后悔莫及”。4.4 输出截断导致的隐性失败工具执行结果截断后模型仍然返回“所有操作完成”但实际上它并没有看到完整输出可能漏掉错误。比如pytest输出 200 行截断后只保留了前 50 行尾部往往才是关键的错误栈。模型却会基于不完整信息下结论。解决方案是我在截断文本前面加了一个醒目标记[TRUNCATED] 命令输出超过 2000 字符已截断。若需查看完整输出请使用 tail/head 或重定向到文件再读取。同时我还加了一个read_large_output工具让模型可以主动读取已经被写进临时文件里的完整输出。这个组合拳解决了很多看似“模型智力不足”的问题。4.5 连接本地模型时的性能与稳定性问题如果你像我喜欢折腾本地模型大概率会遇到生成速度慢、偶发超时。特别是用 CPU 推理时一段代码修改请求可能要等一两分钟模型还会因为超时中断而“断片”。我这里的建议是连接本地模型时把 HTTP 客户端超时调到 120 秒并且开启流式接收至少让界面持续有内容输出。另外别在 System Prompt 里塞太多历史摘要本地模型对长上下文的处理能力弱能精简就精简。常见问题表现解决方案工具调用死循环模型反复执行相同工具设置最大工具调用次数优化工具描述上下文膨胀模型遗忘之前修改手动维护关键事实摘要权限误操作模型执行危险命令开启确认模式命令白名单输出截断模型基于不完整信息判断明确截断标记提供读完整输出工具本地模型超时长时间无响应调大超时开流式输出5. 一些值得记住的实战经验与扩展方向项目做到现在最让我意外的是“工具设计”比“模型能力”更影响最终效果。同一个模型在工具描述模糊时的判断力和描述清晰时完全是两个档次。我后来把每个工具说明都按照“功能用途、适用场景、参数含义、常见错误”四个维度重写了一遍模型的工具调用准确率提升非常明显。如果你也想在自己的项目里复刻这套思路可以先从最小版本开始只实现读文件、写文件、执行命令三个工具配合一个本地模型跑通循环。不要一上来就加代码搜索、远程调用这些高级功能否则你会被一堆边缘情况淹没。后续我计划扩展的方向有三个一是支持多轮并行工具调用模型可以一次返回多个工具指令同时处理多个文件二是增加项目级索引让模型可以直接搜索语义而不是只能 grep三是把确认模式做成更细粒度的规则比如某些目录只读、某些命令黑名单。这些做出来之后free-claude-code才更像一个成熟的编码代理。最后分享一个小技巧如果你和我一样经常在多个终端切换建议把配置文件的路径写成环境变量比如FCC_CONFIG/path/to/config.yaml。这样每个项目都能有自己的模型配置和工具策略同一个free-claude-code二进制换环境变量就是换一套专属助手。这个习惯让我的项目适配成本低了很多也算是我折腾这么久最值得的收获。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑