资讯详情

1.1CS146S:皇帝的新衣,如何用 200 行代码实现 Claude Code。

📅 2026/11/4 2:37:11 | 华诺云谱 👁 阅读
1.1CS146S:皇帝的新衣,如何用 200 行代码实现 Claude Code。
The Emperor Has No Clothes: How to Code Claude Code in 200 Lines of Code从零搭建一个简易编程 Agent看懂模型如何调用工具、读取文件和修改代码。本文根据Mihail Eric的文章整理。作者是工程师、研究者也是斯坦福大学CS146S《现代软件开发者》课程的授课教师。阅读原文 ↗如今AI 编程助手用起来就像魔法。你用英语描述自己的需求哪怕表达有时都不太连贯它们也能读取文件、修改项目并写出可以运行的代码。但关键在于这些工具的核心并不是什么魔法而是大约 200 行简单直白的 Python 代码。接下来我们就从零开始构建一个能实际工作的编程 Agent。理解工作原理在动手写代码之前我们先弄清楚使用编程 Agent 时究竟发生了什么本质上你是在与一个配备了工具箱的强大大语言模型LLM进行对话。你发送一条消息例如“创建一个新文件在里面写一个 Hello World 函数。”LLM 判断需要使用工具于是返回一个结构化的工具调用也可能是多个工具调用。你的程序在本地执行工具调用真正创建文件。执行结果被发送回 LLM。LLM 根据这些上下文继续处理任务或者给出回复。这就是整个循环。LLM 从未直接操作你的文件系统。它只是提出操作请求再由你的代码把这些操作真正执行出来。你需要的三个工具我们的编程 Agent 需要具备三项基本能力读取文件让 LLM 能够看到你的代码。列出文件让它能够浏览你的项目。编辑文件让它能够发出创建和修改代码的指令。就这些。像 Claude Code 这样的生产级 Agent 还提供了更多能力包括grep、bash、websearch等。不过对于我们的目标而言你会发现仅凭这三个工具就足以完成令人惊叹的事情。搭建基础框架我们先导入所需模块并初始化一个 API 客户端。这里使用 OpenAI但这套方法同样适用于其他 LLM 服务商。译注原文此处写的是 OpenAI但下方代码实际使用的是 Anthropic SDK调用 Claude。importinspectimportjsonimportosimportanthropicfromdotenvimportload_dotenvfrompathlibimportPathfromtypingimportAny,Dict,List,Tuple load_dotenv()claude_clientanthropic.Anthropic(api_keyos.environ[ANTHROPIC_API_KEY])接着为终端输出设置一些颜色让内容更易读YOU_COLOR\u001b[94mASSISTANT_COLOR\u001b[93mRESET_COLOR\u001b[0m再定义一个用于解析文件路径的辅助函数将file.py这样的相对路径转换为/Users/you/project/file.py这样的绝对路径defresolve_abs_path(path_str:str)-Path: file.py - /Users/you/project/file.py pathPath(path_str).expanduser()ifnotpath.is_absolute():path(Path.cwd()/path).resolve()returnpath实现工具请注意工具函数的文档字符串应尽量写得详细因为 LLM 会根据这些说明判断在对话中应该调用哪些工具。下文会进一步介绍这一点。工具 1读取文件这是最简单的工具接收一个文件名返回文件内容。defread_file_tool(filename:str)-Dict[str,Any]: Gets the full content of a file provided by the user. :param filename: The name of the file to read. :return: The full content of the file. full_pathresolve_abs_path(filename)print(full_path)withopen(str(full_path),r)asf:contentf.read()return{file_path:str(full_path),content:content}这里返回一个字典是因为 LLM 需要以结构化的形式了解执行结果。工具 2列出文件通过列出目录中的内容浏览目录deflist_files_tool(path:str)-Dict[str,Any]: Lists the files in a directory provided by the user. :param path: The path to a directory to list files from. :return: A list of files in the directory. full_pathresolve_abs_path(path)all_files[]foriteminfull_path.iterdir():all_files.append({filename:item.name,type:fileifitem.is_file()elsedir})return{path:str(full_path),files:all_files}工具 3编辑文件这是三个工具中最复杂的一个但逻辑仍然很直观。它处理两种情况创建新文件当old_str为空时创建文件。替换文本找到old_str将其替换为new_str。defedit_file_tool(path:str,old_str:str,new_str:str)-Dict[str,Any]: Replaces first occurrence of old_str with new_str in file. If old_str is empty, create/overwrite file with new_str. :param path: The path to the file to edit. :param old_str: The string to replace. :param new_str: The string to replace with. :return: A dictionary with the path to the file and the action taken. full_pathresolve_abs_path(path)ifold_str:full_path.write_text(new_str,encodingutf-8)return{path:str(full_path),action:created_file}originalfull_path.read_text(encodingutf-8)iforiginal.find(old_str)-1:return{path:str(full_path),action:old_str not found}editedoriginal.replace(old_str,new_str,1)full_path.write_text(edited,encodingutf-8)return{path:str(full_path),action:edited}这里的约定是old_str为空表示“创建这个文件”否则就查找并替换。实际的 IDE 会在找不到目标字符串时加入更完善的回退处理不过这个实现已经能够完成基本操作。工具注册表我们需要一种通过名称查找工具的方式TOOL_REGISTRY{read_file:read_file_tool,list_files:list_files_tool,edit_file:edit_file_tool}向 LLM 介绍我们的工具LLM 需要知道有哪些工具以及如何调用它们。我们通过函数签名和文档字符串动态生成这些信息defget_tool_str_representation(tool_name:str)-str:toolTOOL_REGISTRY[tool_name]returnf Name:{tool_name}Description:{tool.__doc__}Signature:{inspect.signature(tool)}defget_full_system_prompt():tool_str_reprfortool_nameinTOOL_REGISTRY:tool_str_reprTOOL\nget_tool_str_representation(tool_name)tool_str_reprf\n{*15}\nreturnSYSTEM_PROMPT.format(tool_list_reprtool_str_repr)系统提示词本身如下SYSTEM_PROMPT You are a coding assistant whose goal it is to help us solve coding tasks. You have access to a series of tools you can execute. Here are the tools you can execute: {tool_list_repr} When you want to use a tool, reply with exactly one line in the format: tool: TOOL_NAME({{JSON_ARGS}}) and nothing else. Use compact single-line JSON with double quotes. After receiving a tool_result(...) message, continue the task. If no tool is needed, respond normally. 核心思路就在这里我们只是告诉 LLM“这些是你可以使用的工具这是调用它们的格式。”至于何时使用、如何使用则由 LLM 自行判断。解析工具调用当 LLM 返回回复时我们需要判断它是否在请求执行工具defextract_tool_invocations(text:str)-List[Tuple[str,Dict[str,Any]]]: Return list of (tool_name, args) requested in tool: name({...}) lines. The parser expects single-line, compact JSON in parentheses. invocations[]forraw_lineintext.splitlines():lineraw_line.strip()ifnotline.startswith(tool:):continuetry:afterline[len(tool:):].strip()name,restafter.split((,1)namename.strip()ifnotrest.endswith()):continuejson_strrest[:-1].strip()argsjson.loads(json_str)invocations.append((name,args))exceptException:continuereturninvocations这就是简单的文本解析找到以tool:开头的行提取函数名称和 JSON 格式的参数。调用 LLM对 API 做一层简单封装defexecute_llm_call(conversation:List[Dict[str,str]]):system_contentmessages[]formsginconversation:ifmsg[role]system:system_contentmsg[content]else:messages.append(msg)responseclaude_client.messages.create(modelclaude-sonnet-4-20250514,max_tokens2000,systemsystem_content,messagesmessages)returnresponse.content[0].textAgent 循环现在我们把前面的部分串起来。“魔法”就在这里发生defrun_coding_agent_loop():print(get_full_system_prompt())conversation[{role:system,content:get_full_system_prompt()}]whileTrue:try:user_inputinput(f{YOU_COLOR}You:{RESET_COLOR}:)except(KeyboardInterrupt,EOFError):breakconversation.append({role:user,content:user_input.strip()})whileTrue:assistant_responseexecute_llm_call(conversation)tool_invocationsextract_tool_invocations(assistant_response)ifnottool_invocations:print(f{ASSISTANT_COLOR}Assistant:{RESET_COLOR}:{assistant_response})conversation.append({role:assistant,content:assistant_response})breakforname,argsintool_invocations:toolTOOL_REGISTRY[name]respprint(name,args)ifnameread_file:resptool(args.get(filename,.))elifnamelist_files:resptool(args.get(path,.))elifnameedit_file:resptool(args.get(path,.),args.get(old_str,),args.get(new_str,))conversation.append({role:user,content:ftool_result({json.dumps(resp)})})整体结构如下外层循环获取用户输入将其添加到对话历史中。内层循环调用 LLM检查是否有工具调用请求。如果不需要使用工具就打印回复并退出内层循环如果需要使用工具就执行工具将结果添加到对话历史中然后继续循环。内层循环会持续运行直到 LLM 的回复不再请求使用任何工具。这样Agent 就能够连续完成多次工具调用例如先读取文件再编辑文件最后确认修改。运行起来if__name____main__:run_coding_agent_loop()现在你就可以进行这样的对话你帮我创建一个名为hello.py的新文件并在里面实现 Hello World。Agent 调用edit_file传入pathhello.py、old_str、new_strprint(Hello World)。助手完成已创建hello.py并写入 Hello World 代码。也可以进行多步骤交互你编辑hello.py添加一个计算两个数乘积的函数。Agent 先调用read_file查看当前内容再调用edit_file添加函数。助手已在hello.py中添加乘法函数。我们的实现与生产级工具的区别这个实现大约有 200 行代码。像 Claude Code 这样的生产级工具还会加入更完善的错误处理和回退机制。流式响应改善用户体验。更智能的上下文管理例如对长文件进行摘要。更多工具例如运行命令、搜索代码库等。针对破坏性操作的审批流程。但核心循环与我们在这里构建的完全一致LLM 决定做什么你的代码执行操作再将结果返回。这就是整个架构。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑