简介:这份资源面向AI工具研究者、前端架构学习者与逆向工程爱好者,系统整理了Claude Code的三套源码方案,覆盖从破解还原研究到本地快速运行的不同需求。包内共2000个文件,以1324个ts与541个tsx源码为主体,辅以js脚本、md文档、json配置及少量html、yaml等,压缩包约94.78MB,目录按方案分层组织,便于按需检索。第一套保留npm安装包、自动化还原脚本与source map反编译源码,附构建流程文档,适合深入探究破解还原过程;第二套提供纯净src快照与架构分析文档,梳理模块设计、通信机制与核心逻辑,可作为大型AI应用架构的学习材料;第三套已配置shim补丁、依赖与tsconfig,下载后执行bun install与bun run dev即可启动,适合快速体验。目前已有968人学习,适合希望理解Claude Code实现原理或搭建本地测试环境的开发者参考。
1. 从「Claude Code 源码」这个搜索词说起:你到底在找什么
很多人搜「Claude Code 源码」,其实心里想的是三件不同的事:一是想看看这个终端里的 AI 编程助手到底怎么把自然语言变成文件改动,二是想找一个能直接跑起来的本地版本,三是想拿它当研究对象,改改提示词、换换模型、接自己的工具链。这三件事对应三种完全不同的路径,混在一起搜,结果就是下载一堆来路不明的压缩包,解压完发现要么跑不起来,要么根本看不懂。
先把结论放前面:真正有价值的不是某个「破解版」压缩包,而是理解 Claude Code 这类工具的架构分层——CLI 入口层、会话与上下文管理层、工具调用层、模型适配层。这四层拆开之后,你会发现每一层都可以用开源组件复现,而且复现出来的东西比任何来路不明的「直接运行版」都更可控。这篇文章就按这个思路走:先讲清楚这类工具的结构,再带你用常见做法搭一个能跑的最小版本,然后说清楚参数怎么调、坑在哪、什么情况下不值得自己造。
适合读这篇的人:写过 Node.js 或 Python、用过至少一个 AI 编程助手、想搞清楚它内部怎么工作、或者想做一个内部定制版的工程师。如果你只是想找个现成工具用,那直接用官方版本就行,没必要折腾源码。
2. 拆开看:一个终端 AI 编程助手的四层结构
2.1 为什么是这四层,而不是「一个模型加一个循环」
很多人第一次尝试复现这类工具,写出来的东西大概是这样:读用户输入,拼一个 prompt,调模型 API,把返回的文本打印出来。跑起来能用,但用不了十分钟就会发现问题——模型说「我已经修改了文件」,实际上什么都没改;模型想读一个文件,但你根本没给它读文件的能力;对话长了之后上下文爆掉,模型开始胡言乱语。
问题出在把「模型」当成了整个系统。实际上模型只是其中一层,它负责决策,但不负责执行。一个能用的终端编程助手,至少要有四层:
| 层级 | 职责 | 常见实现方式 |
|---|---|---|
| CLI 入口层 | 解析命令、管理交互循环、处理中断 | Node.js 的 readline、Python 的 prompt_toolkit |
| 会话与上下文层 | 维护对话历史、压缩上下文、管理 token 预算 | 滑动窗口 + 摘要压缩 |
| 工具调用层 | 定义可用工具、解析模型返回的工具调用、执行并回传结果 | JSON Schema 定义 + 本地执行器 |
| 模型适配层 | 统一不同模型的接口差异、处理流式返回、重试 | 适配器模式 |
这四层里,模型适配层是最薄的,工具调用层是最容易出 bug 的,会话与上下文层是最影响体验的。很多人把精力花在换模型上,实际上换模型带来的体验差异,远不如把工具调用做稳。
2.2 工具调用层:整个系统里最容易翻车的地方
工具调用层的核心逻辑是:把本地能力(读文件、写文件、执行命令、搜索)用 JSON Schema 描述出来,塞进模型的系统提示里,模型返回一个结构化的调用请求,本地执行后把结果回传。
常见做法是用这样的结构定义工具:
# 工具定义:每个工具包含名称、描述、参数 schema TOOLS = [ { "name": "read_file", "description": "读取指定路径的文件内容,返回文本。路径必须是相对路径。", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "相对于项目根目录的路径"} }, "required": ["path"] } }, { "name": "write_file", "description": "写入文件。如果文件已存在则覆盖。", "parameters": { "type": "object", "properties": { "path": {"type": "string"}, "content": {"type": "string"} }, "required": ["path", "content"] } } ]这段定义看起来简单,但有几个参数必须注意。description不是写给人看的,是写给模型看的,它直接决定模型会不会在正确的时机调用这个工具。我见过太多人把 description 写成「读取文件」四个字,结果模型在该读文件的时候选择了瞎猜。正确的写法是把使用场景、边界条件、返回值格式都写进去。
required字段也很关键。如果你把path写成可选,模型有一定概率不传路径就调用,然后你的执行器就会拿到一个空字符串。这不是模型的错,是 schema 没约束好。
执行器的部分,核心是一个分发函数:
import os, subprocess def execute_tool(name, args, workdir): # 所有路径操作都必须限制在 workdir 内,防止越权访问 if name == "read_file": target = os.path.realpath(os.path.join(workdir, args["path"])) if not target.startswith(os.path.realpath(workdir)): return {"error": "路径越界"} with open(target, "r", encoding="utf-8") as f: return {"content": f.read()} elif name == "write_file": target = os.path.realpath(os.path.join(workdir, args["path"])) if not target.startswith(os.path.realpath(workdir)): return {"error": "路径越界"} os.makedirs(os.path.dirname(target), exist_ok=True) with open(target, "w", encoding="utf-8") as f: f.write(args["content"]) return {"status": "ok"} else: return {"error": f"未知工具: {name}"}这里的路径越界检查是必须的。模型有时候会生成../../etc/passwd这样的路径,如果你不做检查,它真的会去读。这不是危言耸听,是实际跑的时候一定会遇到的情况。os.path.realpath会把符号链接也解析掉,比单纯拼字符串靠谱。
2.3 会话与上下文层:token 预算怎么管
上下文管理的核心问题是:对话轮次多了之后,历史消息会撑爆模型的上下文窗口。常见做法是滑动窗口加摘要压缩——保留最近 N 轮完整对话,更早的内容压缩成一段摘要。
MAX_RECENT_TURNS = 10 MAX_TOKENS = 8000 def build_context(history, system_prompt): # 保留最近 N 轮,更早的压缩成摘要 recent = history[-MAX_RECENT_TURNS:] older = history[:-MAX_RECENT_TURNS] messages = [{"role": "system", "content": system_prompt}] if older: summary = summarize(older) # 调用模型生成摘要 messages.append({"role": "system", "content": f"之前的对话摘要:{summary}"}) messages.extend(recent) return messagesMAX_RECENT_TURNS这个参数没有标准答案。设太小,模型会忘记你刚才让它改的文件;设太大,token 消耗快。我一般会按任务类型调:纯问答场景设 5 到 8 轮就够,涉及多文件修改的场景设 15 到 20 轮。摘要压缩本身也要消耗一次模型调用,所以不要每轮都压缩,攒到快超限了再压。
还有一个容易被忽略的点:工具调用的结果也要算进上下文。读一个 500 行的文件,返回的内容可能就占掉两千 token。所以读文件工具最好支持行号范围参数,让模型按需读取,而不是一次性把整个文件塞进去。
3. 动手搭一个能跑的最小版本:从入口到第一次工具调用
3.1 环境准备与依赖选择
先明确技术栈。CLI 入口用 Node.js 还是 Python 都行,我选 Python,因为工具调用层的子进程管理和文件操作写起来更直接。依赖只需要两个:一个 HTTP 客户端用来调模型 API,一个终端交互库用来处理输入。
# 创建项目目录 mkdir mini-code-agent && cd mini-code-agent python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖 pip install httpx prompt_toolkithttpx比requests更适合,因为它原生支持异步和流式响应,后面做流式输出的时候不用换库。prompt_toolkit负责终端输入,支持历史记录和快捷键,比裸input()好用得多。
模型 API 的接入方式,常见做法是走 OpenAI 兼容接口,因为大部分模型服务都支持这个格式。你需要准备一个 API key 和一个 base_url,这两个值从环境变量读,不要硬编码在代码里。
import os API_KEY = os.environ.get("MODEL_API_KEY") BASE_URL = os.environ.get("MODEL_BASE_URL", "https://api.example.com/v1") MODEL_NAME = os.environ.get("MODEL_NAME", "default-model")把这三个值做成环境变量,切换模型的时候不用改代码。MODEL_NAME给一个默认值是为了让代码在没有配置的情况下也能跑起来,虽然会报错,但报错信息比KeyError清楚。
3.2 主循环:一次完整的「输入 → 决策 → 执行 → 回传」
主循环的逻辑是:读用户输入,拼上下文,调模型,如果模型返回工具调用就执行,把结果追加到上下文,再调一次模型,直到模型返回纯文本回复。
import json, httpx def call_model(messages, tools): # 把工具定义转成模型能识别的格式 payload = { "model": MODEL_NAME, "messages": messages, "tools": [{"type": "function", "function": t} for t in tools], "stream": False } resp = httpx.post( f"{BASE_URL}/chat/completions", headers={"Authorization": f"Bearer {API_KEY}"}, json=payload, timeout=60 ) resp.raise_for_status() return resp.json()["choices"][0]["message"] def agent_loop(user_input, history, workdir): history.append({"role": "user", "content": user_input}) for _ in range(10): # 最多 10 轮工具调用,防止死循环 messages = build_context(history, SYSTEM_PROMPT) reply = call_model(messages, TOOLS) history.append(reply) if not reply.get("tool_calls"): return reply["content"] for call in reply["tool_calls"]: fn = call["function"]["name"] args = json.loads(call["function"]["arguments"]) result = execute_tool(fn, args, workdir) history.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(result, ensure_ascii=False) }) return "达到最大工具调用轮次,已停止。"这段代码里有三个关键参数。timeout=60是 HTTP 超时,模型响应慢的时候会触发,设太短会频繁超时,设太长会卡住终端。range(10)是工具调用轮次上限,防止模型陷入「读文件 → 改文件 → 再读 → 再改」的死循环。ensure_ascii=False保证中文内容不会被转义成\uXXXX,不然模型读到的工具结果会是一堆乱码。
工具结果的回传格式也要注意。role必须是tool,tool_call_id必须和模型返回的id对上,否则模型会认为工具没被调用。这两个字段对不上是新手最常犯的错误,表现是模型反复调用同一个工具,因为它觉得上次调用没成功。
3.3 系统提示:决定模型行为的关键文件
系统提示是整个系统里最需要反复调的部分。它要告诉模型:你是谁、你能做什么、你不能做什么、输出格式是什么。
SYSTEM_PROMPT = """你是一个终端编程助手。你可以读取文件、写入文件、执行命令。 规则: 1. 修改文件前必须先读取该文件,确认当前内容。 2. 不要猜测文件内容,不确定就调用 read_file。 3. 每次只做一件事,做完等结果再决定下一步。 4. 不要执行删除操作,不要访问项目目录之外的路径。 5. 回复用中文,代码和路径保持原样。"""这五条规则每一条都是踩坑之后加的。第一条防止模型凭记忆改文件,第二条防止它瞎编内容,第三条防止它一次性发起多个互相依赖的工具调用,第四条是安全底线,第五条是输出规范。
系统提示不要写太长。超过 500 字之后,模型对后面内容的注意力会下降。把最重要的规则放在前面,次要的放后面。如果规则太多,考虑合并或者删掉一些——每一条规则都应该对应一个实际发生过的问题,没发生过的问题不要提前加。
4. 避坑与排查:那些跑起来才会暴露的问题
4.1 模型返回的工具参数是坏 JSON
现象:执行器报json.loads失败,或者参数解析出来是空字典。
原因:模型生成工具调用参数时,偶尔会输出不完整的 JSON,比如少一个右括号,或者字符串里带了未转义的引号。这在长参数(比如写一个大文件)的时候特别常见。
解决:在解析外面包一层容错,解析失败时把原始字符串回传给模型,让它重新生成。
def safe_parse_args(raw): try: return json.loads(raw), None except json.JSONDecodeError as e: return None, f"参数解析失败:{e}。原始内容:{raw[:200]}"把错误信息回传给模型,比直接崩溃好。模型看到错误之后,大部分情况下会重新生成一个合法的调用。如果连续三次都失败,就中断这一轮,让用户介入。
4.2 上下文突然爆掉,模型开始重复之前的内容
现象:对话进行到二三十轮之后,模型开始重复之前说过的话,或者忘记当前任务。
原因:历史消息总长度超过了模型的上下文窗口,服务端做了截断,但截断的位置在中间,导致模型看到的是不连贯的对话。
解决:在本地做 token 估算,超限之前主动压缩。不要依赖服务端的截断,因为你不知道它从哪里截。
def estimate_tokens(text): # 粗略估算:中文约 1.5 字符/token,英文约 4 字符/token chinese = sum(1 for c in text if '\u4e00' <= c <= '\u9fff') other = len(text) - chinese return int(chinese / 1.5 + other / 4)这个估算不精确,但足够用来做预警。当总 token 超过模型上限的 80% 时,触发压缩。压缩的时候优先压缩工具返回的大块内容,比如文件读取结果,保留用户的原始指令和模型的决策记录。
4.3 工具执行卡住,整个终端没响应
现象:执行某个命令之后,终端一直转圈,按 Ctrl+C 也没用。
原因:工具执行器调用了子进程,子进程没有超时机制,比如执行了一个等待输入的交互式命令,或者一个死循环脚本。
解决:所有子进程调用都必须加超时,并且把标准输入关掉。
def run_command(cmd, workdir, timeout=30): try: result = subprocess.run( cmd, shell=True, cwd=workdir, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL ) return {"stdout": result.stdout, "stderr": result.stderr, "code": result.returncode} except subprocess.TimeoutExpired: return {"error": f"命令超时({timeout}秒)"}stdin=subprocess.DEVNULL这一行很关键。不加的话,子进程会继承终端的标准输入,遇到需要输入的命令就会挂起。timeout设 30 秒是折中值,编译类命令可能需要更长,但大部分文件操作和查询命令 30 秒足够。
4.4 模型坚持要执行危险操作
现象:模型反复尝试执行rm -rf、git reset --hard或者修改项目目录之外的文件。
原因:系统提示里的约束不够强,或者用户输入里包含了诱导性的指令。
解决:在执行器层面做硬拦截,不依赖模型的自觉。
BLOCKED_PATTERNS = ["rm -rf", "git reset --hard", "> /dev/", "chmod 777"] def is_dangerous(cmd): return any(p in cmd for p in BLOCKED_PATTERNS)拦截之后要把拒绝原因回传给模型,让它知道这条路走不通。只拦截不告知,模型会反复尝试。告知之后,大部分模型会换一种方式。
4.5 换模型之后工具调用格式不兼容
现象:换了一个模型服务,之前能跑的工具调用全部失效,模型返回的是纯文本而不是结构化的工具调用。
原因:不同模型服务对工具调用的支持程度不一样。有的只支持 OpenAI 格式,有的用自己的格式,有的干脆不支持工具调用,只能靠提示词让模型输出特定格式的文本。
解决:在模型适配层做格式转换。如果目标模型不支持原生工具调用,就退化成「让模型输出 JSON 块,本地解析」的模式。
def adapt_tools_for_model(tools, model_supports_native): if model_supports_native: return [{"type": "function", "function": t} for t in tools] else: # 退化成提示词模式,把工具定义塞进系统提示 tool_desc = "\n".join(f"- {t['name']}: {t['description']}" for t in tools) return None, f"可用工具:\n{tool_desc}\n调用格式:<tool>名称</tool><args>JSON</args>"这个降级方案不如原生工具调用稳定,但至少能跑。判断模型是否支持原生工具调用,最可靠的方式是发一个测试请求,看返回里有没有tool_calls字段。
5. 进阶:把「能跑」变成「好用」的几个具体技巧
5.1 用文件快照做回滚,给自己留后悔药
模型改文件的时候,你永远不知道它会不会把好的代码改坏。最实用的保险是每次写文件之前先存一份快照。
import shutil, time def snapshot(path): if os.path.exists(path): backup = f"{path}.bak.{int(time.time())}" shutil.copy2(path, backup) return backup return None快照文件不要放在项目目录里,放到一个统一的临时目录,否则模型会读到一堆.bak文件然后困惑。回滚的时候按时间戳找最近的快照就行。这个机制看起来笨,但在模型连续改错三次的时候,能省掉大量手动恢复的时间。
5.2 给工具调用加日志,出问题的时候有据可查
终端里滚过的内容很容易丢,把每次工具调用和结果写到日志文件里,排查的时候直接翻日志。
import logging logging.basicConfig( filename="agent.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" ) def log_tool_call(name, args, result): logging.info(f"tool={name} args={json.dumps(args, ensure_ascii=False)[:500]}") logging.info(f"result={json.dumps(result, ensure_ascii=False)[:500]}")日志里对内容做截断,不然一个大文件的读取结果就能把日志撑到几百兆。截断长度 500 字符是个经验值,足够看出调用了什么、返回了什么类型的结果。
5.3 验证一个终端编程助手是否真的可用
搭完之后怎么判断它能不能用?我一般跑三个测试用例。
第一个是单文件修改:让它读一个文件,把里面的某个函数改名,然后确认文件真的被改了,而且其他部分没动。这个测试验证工具调用链路是否通畅。
第二个是多文件依赖:给它两个有引用关系的文件,让它修改被引用的那个,然后确认它有没有去读引用方。这个测试验证模型会不会主动收集上下文。
第三个是错误恢复:故意给它一个不存在的文件路径,看它是报错还是瞎编内容。这个测试验证系统提示里的约束有没有生效。
三个都过了,基本就能日常用了。过不了的话,回去看日志,问题一般出在工具 schema 的 description 或者系统提示的规则上。
5.4 什么情况下不值得自己造
最后说一个判断标准。如果你只是想要一个能用的 AI 编程助手,直接用现成的就行,自己造的成本远高于收益。自己造的价值在于三种情况:一是你需要接入内部私有模型,现成工具不支持;二是你需要定制工具集,比如接入公司内部的代码检索服务;三是你需要把助手嵌入到已有的工作流里,而不是作为一个独立终端。
这三种情况之外,把时间花在调提示词和熟悉现有工具上,回报率更高。我见过不少人花两周搭了一个能跑的最小版本,然后发现功能还不如现成工具的一半,最后又回去用现成的。搭之前先想清楚你要解决的具体问题是什么,如果那个问题用配置就能解决,就不要写代码。
希望帮到你。
本文还有配套的精品资源,点击获取