
1. 项目概述为什么我们要从零造轮子最近“AI Agent”这个词火得不行感觉一夜之间从技术论坛到产品发布会不提Agent就落伍了。市面上也涌现了各种框架LangChain、AutoGen、CrewAI……功能强大封装完善。那为什么我们还要费劲去实现一个“Mini Claude Code”呢直接调用API不香吗这正是问题的关键。如果你只停留在调用openai.ChatCompletion.create()然后拼接一下system_prompt和user_input那你对Agent的理解就永远停留在“黑盒调用”的层面。你不知道模型的一次思考Reasoning内部经历了怎样的“头脑风暴”不明白工具Tool调用是如何被触发和执行的更无从下手去设计一个能自主规划、高效执行复杂任务的智能体。这个项目我们称之为“Mini Claude Code”目标不是复现一个生产级的、功能完备的Agent框架而是亲手搭建一个极度简化但核心逻辑完整的Agent骨架。通过这个过程我们将像解剖青蛙一样把Agent最核心的“推理-行动-观察”循环ReAct模式、工具使用、状态管理等概念从抽象的论文描述变成一行行可运行的代码。当你理解了这些底层机制再回头看那些成熟的框架就会有一种“哦原来它是这么干的”的豁然开朗感无论是使用、调试还是定制化开发都将得心应手。2. 核心架构设计拆解AI Agent的“五脏六腑”在动手写代码之前我们必须先想清楚一个最小化的AI Agent应该由哪些核心部件构成。这就像盖房子前先画图纸避免写到一半发现结构混乱。2.1 核心组件定义一个典型的、具备工具使用能力的AI Agent至少需要以下几个部分大脑LLM Core负责所有的推理和决策。它接收当前的“状态”包括对话历史、工具执行结果等思考下一步该做什么是直接回答还是调用某个工具并输出结构化的指令。在我们的Mini版本里我们会用OpenAI的GPT-3.5/4模型来扮演这个大脑因为它能很好地输出我们约定的JSON格式。工具集ToolkitAgent的“手”和“感官”。大脑再聪明没有工具也无法与世界交互。工具可以是一个计算器函数、一个搜索API、一个操作系统的命令行甚至是另一个AI服务。每个工具都需要有明确的名称、描述和参数定义以便大脑理解何时以及如何调用它。记忆与状态管理Memory StateAgent的“短期工作记忆”。它需要记住整个对话的历史HISTORY特别是上一次工具调用的结果OBSERVATION以及整个任务执行至今的上下文。我们将用一个简单的列表来维护这个对话历史。解析与分发器Parser Dispatcher大脑的输出是自然语言或结构化文本我们需要一个模块来解析它判断意图。如果大脑决定调用工具这个模块就要提取出工具名和参数并调用对应的工具函数如果大脑决定直接回答则把内容返回给用户。控制循环Orchestration Loop这是Agent的“心跳”一个while循环不断重复“思考-行动-观察”的过程直到任务完成或达到停止条件比如大脑输出“FINISH”。2.2 技术选型与简化策略为了聚焦核心逻辑我们做出以下合理简化框架不使用任何重型Agent框架如LangChain纯用Python标准库和必要的HTTP请求库requests实现。这能保证代码的透明度和可理解性。LLM接口使用OpenAI的Chat Completion API。选择它的原因一是稳定可靠二是其function calling函数调用能力与我们想要实现的工具调用模式天然契合。虽然我们会模拟类似逻辑但了解其原生机制对后续深入有帮助。工具实现实现2-3个最简单的工具如get_current_time获取当前时间、calculate执行数学计算。它们都是本地同步函数避免引入网络异步调用的复杂性。状态存储使用Python列表在内存中维护对话历史不引入外部数据库。这足以演示核心循环。停止条件设定一个最大循环次数如10次防止Agent陷入死循环。同时当LLM输出特定的结束标记如FINISH时循环终止。这个设计遵循了经典的ReAct (Reasoning Acting)范式也是当前大多数Agent框架如LangChain的AgentExecutor内部运作的基本原理。理解了它你就握住了打开AI Agent大门的钥匙。3. 分步实现从零搭建Mini Claude Code现在我们进入实战环节。请确保你有一个可用的OpenAI API Key。3.1 环境准备与基础配置首先创建一个新的项目目录并安装依赖。我们只需要最基础的库。mkdir mini_claude_code cd mini_claude_code python -m venv venv # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate pip install openai requests python-dotenv创建一个.env文件来安全地存储你的API密钥OPENAI_API_KEY你的api_key_here然后我们创建主文件agent_core.py并开始编写基础结构。# agent_core.py import os import json from typing import Dict, Any, Callable, List from openai import OpenAI from dotenv import load_dotenv load_dotenv() class MiniClaudeCode: def __init__(self, model: str gpt-3.5-turbo): 初始化Mini Agent。 :param model: 使用的OpenAI模型名称。 self.client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) self.model model self.conversation_history: List[Dict[str, str]] [] # 存储对话历史 self.tools: Dict[str, Dict[str, Any]] {} # 工具注册表 self.max_iterations 10 # 防止无限循环 def add_to_history(self, role: str, content: str): 向对话历史中添加一条消息。 self.conversation_history.append({role: role, content: content}) def get_system_prompt(self) - str: 构建系统提示词定义Agent的角色和能力。 # 这里是Agent的“宪法”至关重要。 tool_descriptions \n.join([f- {name}: {info[description]} (参数: {info[parameters]}) for name, info in self.tools.items()]) return f你是一个专业的AI助手可以调用工具来解决问题。你可以使用的工具如下 {tool_descriptions} 请严格按以下格式回应 1. 如果你需要调用工具请输出一个JSON对象格式为{{action: TOOL_CALL, tool_name: 工具名, tool_input: {{参数键: 参数值}}}} 2. 如果你能直接回答用户问题或任务已完成请输出{{action: FINAL_ANSWER, answer: 你的回答内容}} 请一步一步思考每次只做一个决定。关键点解析conversation_history列表模拟了对话的上下文。每次LLM调用我们都会把整个历史喂给它。tools字典将作为工具注册表键是工具名值包含工具函数、描述和参数schema。system_prompt是引导LLM行为的关键。我们明确规定了它的输出必须是两种严格的JSON格式之一这简化了后续的解析逻辑。在复杂Agent中这可能会用更精细的提示工程Prompt Engineering或输出解析器Output Parser来代替。3.2 工具系统的实现与注册工具是Agent能力的延伸。我们来定义两个简单的工具并实现注册机制。# 在 MiniClaudeCode 类中添加方法 def register_tool(self, func: Callable, name: str, description: str, parameters: str): 向Agent注册一个工具。 :param func: 工具函数本身。 :param name: 工具名称LLM将通过这个名称来调用。 :param description: 工具功能的自然语言描述用于提示LLM。 :param parameters: 参数描述例如 expression: 数学表达式字符串。 self.tools[name] { function: func, description: description, parameters: parameters } # 定义几个示例工具函数 def get_current_time(**kwargs) - str: 获取当前日期和时间。无需参数。 from datetime import datetime return f当前时间是{datetime.now().strftime(%Y-%m-%d %H:%M:%S)} def calculate(expression: str) - str: 计算一个数学表达式。警告使用eval仅用于演示生产环境绝对禁用 try: # 严重安全警告在实际项目中永远不要用eval直接执行用户或LLM提供的字符串。 # 这里仅为演示应替换为安全的数学表达式解析库如ast.literal_eval限制操作。 result eval(expression, {__builtins__: None}, {}) return f计算结果{expression} {result} except Exception as e: return f计算错误{e} # 在初始化后注册工具 if __name__ __main__: agent MiniClaudeCode() agent.register_tool(get_current_time, get_time, 获取当前的日期和时间, 无) agent.register_tool(calculate, calculator, 计算一个数学表达式的结果, expression: 一个字符串形式的数学表达式例如 3 5 * 2)实操心得与避坑指南工具描述至关重要description和parameters是LLM理解工具用途的唯一依据。描述必须清晰、无歧义。例如“计算”不如“计算一个数学表达式的结果”明确。参数设计尽量让参数简单、原子化。复杂的嵌套结构会增加LLM理解和使用工具的难度。在我们的简化版中我们用字符串描述参数在生产框架中这里会是一个符合JSON Schema的详细定义。安全性是第一生命线calculate工具中使用的eval()是极度危险的这仅仅是教学演示向你和LLM展示了工具调用的流程。在任何面向用户或生产环境的Agent中都必须彻底避免执行任意代码。应该使用安全的库如numexpr、ast.literal_eval进行严格限制或通过沙盒环境来执行此类操作。3.3 核心推理循环与调度器这是Agent的“发动机”。我们将实现run方法它接收用户查询并驱动整个“思考-行动”循环。# 在 MiniClaudeCode 类中添加 run 方法 def run(self, user_query: str) - str: 运行Agent处理用户查询。 :param user_query: 用户的输入。 :return: Agent的最终回答。 print(f\n[用户] {user_query}) self.add_to_history(user, user_query) for iteration in range(self.max_iterations): print(f\n--- 第 {iteration 1} 轮思考 ---) # 1. 推理调用LLM获取下一步决策 llm_response self._call_llm() print(f[大脑原始输出] {llm_response}) # 2. 解析尝试解析LLM的响应 action_dict self._parse_response(llm_response) if not action_dict: # 如果解析失败将错误信息加入历史让LLM重试 error_msg 我无法理解你的回应格式。请严格按照指定的JSON格式回应。 self.add_to_history(assistant, error_msg) continue action_type action_dict.get(action) # 3. 分发与执行 if action_type TOOL_CALL: tool_name action_dict.get(tool_name) tool_input action_dict.get(tool_input, {}) result self._execute_tool(tool_name, tool_input) # 将工具执行结果作为“观察”加入历史 self.add_to_history(system, f工具 {tool_name} 返回结果{result}) print(f[工具执行] {tool_name}({tool_input}) - {result}) elif action_type FINAL_ANSWER: final_answer action_dict.get(answer, ) print(f[最终答案] {final_answer}) # 清理历史为下一次对话做准备可选 # self.conversation_history.clear() return final_answer else: # 未知的action类型 self.add_to_history(system, f未知的指令类型{action_type}) # 循环结束仍未返回答案 return f达到最大循环次数{self.max_iterations}任务可能未完成。最后的历史记录{self.conversation_history[-5:] if self.conversation_history else 空} def _call_llm(self) - str: 封装对OpenAI API的调用。 messages [{role: system, content: self.get_system_prompt()}] messages.extend(self.conversation_history) # 注入全部历史上下文 try: response self.client.chat.completions.create( modelself.model, messagesmessages, temperature0.1, # 低温度保证输出格式稳定 max_tokens500 ) return response.choices[0].message.content except Exception as e: return f调用LLM API时出错{e} def _parse_response(self, response: str) - Dict[str, Any]: 解析LLM的响应提取出结构化的action字典。 # 这是一个非常简单的解析器实际应用中需要更健壮的错误处理 response response.strip() if response.startswith({) and response.endswith(}): try: return json.loads(response) except json.JSONDecodeError: pass # 如果开头结尾不是大括号尝试提取可能的JSON对象LLM有时会在回答中包裹JSON import re json_match re.search(r\{.*\}, response, re.DOTALL) if json_match: try: return json.loads(json_match.group()) except json.JSONDecodeError: pass return {} # 解析失败返回空字典 def _execute_tool(self, tool_name: str, tool_input: Dict) - str: 查找并执行指定的工具。 if tool_name not in self.tools: return f错误未找到名为 {tool_name} 的工具。 tool_info self.tools[tool_name] tool_func tool_info[function] try: # 根据工具函数签名传递参数 # 这里做了简化假设tool_input的键与函数参数名匹配 result tool_func(**tool_input) return str(result) except TypeError as e: return f工具调用参数错误{e}。所需参数{tool_info[parameters]} except Exception as e: return f工具执行过程中出错{e}核心逻辑拆解_call_llm: 每次循环我们都将系统提示词 完整对话历史发送给LLM。历史中包含了之前的用户问题、Assistant的决策JSON、以及System角色提供的工具执行结果OBSERVATION。这正是ReAct中“观察”环节的体现让LLM能基于最新结果进行下一轮“推理”。_parse_response: 这是一个脆弱的环节。我们依赖LLM严格遵守我们规定的JSON格式。在实际框架中LangChain等库提供了强大的OutputParser甚至可以用LLM本身来解析不规范输出或者直接使用OpenAI的function calling功能让API返回结构化的函数调用参数。_execute_tool: 根据解析出的工具名和参数从注册表中找到对应的Python函数并执行。这里的错误处理非常重要要能清晰反馈给LLM哪里出了问题。循环控制循环的终止条件有两个一是LLM输出FINAL_ANSWER二是达到max_iterations。后者是防止Agent“卡住”或陷入无意义循环的必要安全措施。3.4 完整示例与运行测试让我们创建一个主程序来测试这个Mini Agent。# main.py from agent_core import MiniClaudeCode, get_current_time, calculate def main(): agent MiniClaudeCode(modelgpt-3.5-turbo) # 也可用 gpt-4 # 注册工具 agent.register_tool(get_current_time, get_time, 获取当前的日期和时间, 无) agent.register_tool(calculate, calculator, 计算一个数学表达式的结果, expression: 一个字符串形式的数学表达式) # 测试用例 test_queries [ 现在几点了, 123乘以456等于多少, 先告诉我现在的时间然后计算(15 7) * 3 的值。, 北京和上海的直线距离是多少, # 这是一个我们没有相关工具的问题 ] for query in test_queries: print(\n *50) print(f处理查询: {query}) print(*50) final_answer agent.run(query) print(f\n 最终回复: {final_answer}) # 可选每次测试后清空历史避免交叉影响 agent.conversation_history.clear() if __name__ __main__: main()运行python main.py你会看到类似以下的输出具体内容因模型随机性和时间而异 处理查询: 现在几点了 [用户] 现在几点了 --- 第 1 轮思考 --- [大脑原始输出] {action: TOOL_CALL, tool_name: get_time, tool_input: {}} [工具执行] get_time({}) - 当前时间是2023-10-27 14:30:25 --- 第 2 轮思考 --- [大脑原始输出] {action: FINAL_ANSWER, answer: 当前时间是2023年10月27日 14点30分25秒。} 最终回复: 当前时间是2023年10月27日 14点30分25秒。对于复合任务“先告诉我现在的时间然后计算(15 7) * 3 的值。”Agent会展示出多步推理的能力--- 第 1 轮思考 --- [大脑原始输出] {action: TOOL_CALL, tool_name: get_time, tool_input: {}} [工具执行] get_time({}) - 当前时间是2023-10-27 14:31:10 --- 第 2 轮思考 --- [大脑原始输出] {action: TOOL_CALL, tool_name: calculator, tool_input: {expression: (15 7) * 3}} [工具执行] calculator({expression: (15 7) * 3}) - 计算结果(15 7) * 3 66 --- 第 3 轮思考 --- [大脑原始输出] {action: FINAL_ANSWER, answer: 当前时间是2023年10月27日 14点31分10秒。计算表达式 (15 7) * 3 的结果是66。}而对于没有对应工具的问题“北京和上海的直线距离”LLM在尝试思考后可能会直接给出一个基于其内部知识的回答如果它“知道”的话或者输出一个FINAL_ANSWER表示无法解决。这取决于你的系统提示词如何定义它的行为边界。4. 从Mini到真实问题、优化与扩展方向我们的Mini Claude Code虽然跑起来了但它距离一个健壮、可用的Agent还有十万八千里。下面我们来盘点一下它的问题并探讨如何优化这能让你更深刻地理解成熟框架在解决什么。4.1 当前实现的致命缺陷与解决方案脆弱的输出解析问题我们依赖LLM输出完美的JSON字符串。一旦它“不听话”在JSON外加了说明文字或者格式稍有错误解析就会失败。解决方案使用OpenAI Function Calling这是最推荐的方式。在调用API时直接将工具的函数签名名称、描述、参数schema以tools参数传入。OpenAI模型会返回一个结构化的tool_calls对象其中包含了要调用的函数名和已经解析好的参数字典。这从根本上解决了格式问题。使用输出解析库像LangChain的JsonOutputParser、StructuredOutputParser或者使用Pydantic模型来定义输出结构结合提示词让LLM遵循。让LLM自我修正当解析失败时可以将错误信息反馈给LLM要求它重新生成一个合规的响应。有限且不安全的工具问题工具太少且calculate工具极其危险。解决方案丰富工具生态集成网络搜索如SerpAPI、数据库查询、代码执行在严格沙盒中、文件操作等。工具抽象与发现设计一个BaseTool类所有工具都继承它实现统一的run方法。可以动态加载工具。权限与安全沙盒为工具划分安全等级。像计算、文件读写这类高风险操作必须在隔离的沙盒环境如Docker容器中运行并对输入进行严格的清洗和验证。简单的记忆管理问题我们只用了一个列表存储全部历史。长对话下token数会爆炸且无法区分重要信息。解决方案对话总结当历史超过一定长度调用LLM对之前的对话进行总结浓缩用总结向量代替冗长的历史。向量记忆Vector Memory将历史中的关键信息如事实、用户偏好转换为向量存入向量数据库如Chroma、Weaviate。需要时进行语义检索只召回相关片段。这是实现“长期记忆”的关键。分层记忆分为短期当前会话、长期向量存储、外部记忆知识库等。缺乏规划与反思能力问题我们的Agent是“走一步看一步”单步推理。对于复杂任务它缺乏整体规划也不会在失败后反思调整策略。解决方案任务分解Planning在开始执行前先让LLM根据目标制定一个分步计划Plan。例如“写一份市场报告”可以分解为“1. 搜索行业数据2. 分析竞争对手3. 撰写提纲4. 填充内容”。反思Reflection在执行完一步或遇到错误后让一个“审查者”LLM可以是同一个模型对当前结果和状态进行评估判断是否偏离目标并提出下一步的改进建议。这构成了更高级的ReAct Reflection模式。4.2 引入真实世界组件以搜索工具为例让我们快速实现一个相对安全的网络搜索工具感受一下如何集成外部API。我们将使用一个模拟的搜索API实际可以用SerpAPI、Google Custom Search等。# tools.py import requests def web_search(query: str, max_results: int 3) - str: 执行网络搜索模拟。 在实际应用中这里应替换为真实的搜索API调用如SerpAPI。 :param query: 搜索关键词。 :param max_results: 返回的最大结果数。 :return: 格式化后的搜索结果摘要。 # 这是一个模拟函数。真实调用需要API Key和费用。 print(f[模拟搜索] 正在搜索: {query}) # 假设我们调用了一个真实API并获得了结果列表 mock_results [ f1. 关于{query}的百科介绍...摘要A, f2. 最新关于{query}的新闻报道...摘要B, f3. {query}相关的技术论坛讨论...摘要C, ] return f针对“{query}”的搜索结果共{len(mock_results)}条\n \n.join(mock_results[:max_results]) # 在主程序中注册 # agent.register_tool(web_search, web_search, 在互联网上搜索信息, query: 搜索关键词字符串; max_results: 返回结果数量默认为3)现在当用户问“特斯拉最新的车型是什么”时Agent可以自主决定调用web_search工具获取实时信息后再组织答案。这实现了知识实时性的突破超越了LLM本身的知识截止日期限制。4.3 架构演进理解Harness与Agent Core在阅读行业资料时你可能会遇到“Harness”这个词。正如一些资料所述Harness是一套包裹在AI Agent核心推理逻辑之外的基础设施层。它不负责代替Agent做决策那是LLM Core的工作而是为Agent的稳定、高效、安全运行提供支撑。我们的Mini项目实现了最最核心的“推理-行动”循环。而一个完整的Harness可能包含生命周期管理Agent的启动、暂停、恢复、销毁。资源管理与隔离为每个Agent或任务分配独立的计算资源、内存空间防止相互干扰。持久化与状态恢复将Agent的对话历史、工具调用记录等保存到数据库即使进程重启也能恢复状态。监控与可观测性Observability记录详细的日志、跟踪每个工具调用的耗时和结果、生成执行链Chain的可视化图谱方便调试和优化。流量控制与负载均衡当有大量Agent并发时管理对LLM API、工具API的调用频率防止超限。安全与审计对所有输入输出进行安全检查记录完整的操作日志以备审计。当你理解了Agent Core和Harness的分离就能更好地评估像LangChain这样的框架它既提供了高层次的Chain和Agent构建模块Core也通过CallbackHandlers等机制提供了部分Harness能力如日志记录。而像AutoGen的GroupChatManagerCrewAI的Crew和Process都是在Core之上更复杂的编排与Harness层。5. 避坑指南与进阶学习路线基于这个迷你项目的实践我想分享一些从零到一构建AI Agent时一定会遇到的坑以及如何继续深入学习。5.1 实操中常见的“坑”提示词Prompt不稳定现象Agent有时能正确调用工具有时却直接回答或输出错误格式。对策提示词需要反复迭代测试A/B测试。在系统提示中明确格式要求并给出1-2个清晰的示例Few-shot Learning。使用更强大的模型如GPT-4通常能获得更稳定的格式遵循能力。工具描述不清导致误用现象LLM调用了错误的工具或传递了错误的参数。对策工具描述要像给新手写说明书一样详细。参数名尽量直观描述中可包含示例。例如“query: 搜索关键词例如 ‘python latest version’”。循环失控与成本飙升现象Agent陷入“调用工具-得到结果-再次调用同一工具”的死循环API调用次数激增。对策除了设置最大循环次数还可以在系统提示中强调“避免重复操作”。更高级的做法是引入“反思”步骤让Agent评估当前进展是否朝着目标前进。处理复杂、模糊的用户请求现象用户说“帮我安排一下下周的工作”Agent无从下手。对策引入“任务分解”或“澄清”步骤。让Agent先输出一个计划或者反问用户“您能具体说一下有哪些工作项和优先级吗”。这需要设计更复杂的提示逻辑和多轮交互。5.2 从Mini项目出发的进阶路线如果你已经理解了本文的所有代码和概念那么你可以沿着以下路径继续深入拥抱成熟框架LangChain深入理解其Agent、Tool、Memory、Chain这几个核心概念。尝试用LangChain重写我们这个Mini项目你会发现它帮你处理了输出解析、对话历史管理、流式响应等大量脏活累活。AutoGen研究其多智能体对话模式。如何让一个“程序员”Agent和一个“测试员”Agent协作写代码AutoGen提供了优雅的解决方案。CrewAI专注于角色扮演和任务驱动的多智能体协作。它关于Role、Task、Crew、Process的抽象非常适合模拟一个团队的工作流。深入底层技术学习ReAct、CoT、ToT等范式阅读原始论文理解不同推理框架的优劣。掌握Function Calling深入研究OpenAI、Anthropic Claude、Google Gemini等模型的原生函数调用能力这是构建高效Agent的基石。探索向量数据库学习Chroma、Pinecone、Weaviate理解如何为Agent添加“长期记忆”和“知识库检索”能力。项目实践与迭代构建一个专属Agent比如一个能自动分析GitHub仓库并生成代码摘要的Agent或者一个能根据你的日历和邮件自动安排会议的私人助理。关注开源项目在GitHub上关注langchain-ai、microsoft/autogen、joaomdmoura/crewai等官方仓库学习其源码和示例。参与社区在Hugging Face、Reddit的r/LocalLLaMA、Discord的相关频道中有很多前沿的讨论和项目分享。实现这个Mini Claude Code的最大价值不在于代码本身而在于你亲手触摸了AI Agent的脉搏。下次当你看到一段复杂的Agent编排代码时你看到的将不再是一团迷雾而是一个个熟悉的“推理”、“工具调用”、“状态管理”模块的组合。你知道了问题可能出在提示词、解析器还是工具本身你也知道了该从哪里入手去定制和优化。AI Agent的世界正在快速演进但核心的“感知-思考-行动”循环不会变。掌握了这个内核你就拥有了理解和创造下一代AI应用的基本能力。