{ "cells": [ { "cell_type": "markdown", "id": "2bf5f533", "metadata": {}, "source": [ "# 🧠 最小 AI 编程智能体 — 从零理解 Agent Loop\n", "\n", "> **目标**:用约 150 行 Python 代码,从零搭建一个 AI 编程助手,理解大模型「智能体」的工作原理。\n", "\n", "---\n", "\n", "## 你将学到什么?\n", "\n", "- 大模型(LLM)如何通过 **函数调用(Function Calling)** 操作你的文件\n", "- **Agent Loop** 是什么——整个 AI 编程助手的核心架构\n", "- 亲手实现一个能读文件、写文件、列出目录的 AI 助手\n", "\n", "## 你需要准备什么?\n", "\n", "| 你需要 | 说明 |\n", "|--------|------|\n", "| LLM API Key | 任一支持 OpenAI 兼容接口的模型均可(智谱 / DeepSeek / Qwen / OpenAI 等)|\n", "| Python 基础 | 了解变量、函数、`print()` 即可 |\n", "\n", "> 💡 免费额度推荐:[智谱 GLM-4](https://open.bigmodel.cn/) / [DeepSeek](https://platform.deepseek.com/)\n", "\n", "## 学习路径\n", "\n", "1. **Part 1** ← 你在这里:安装依赖,配置模型,验证连接\n", "2. **Part 2**:直接体验 Agent 的效果(先感受,不用管代码)\n", "3. **Part 3**:逐段拆解代码,理解每一个部分\n", "4. **Part 4**:🎉 自由探索你的 Agent\n", "\n", "> ⚠️ **请按顺序执行每个 Cell**,不要跳着运行。\n" ] }, { "cell_type": "markdown", "id": "5d65c7e1", "metadata": {}, "source": [ "## Part 1:环境准备\n", "\n", "### 1.1 安装依赖" ] }, { "cell_type": "code", "execution_count": null, "id": "07d09095", "metadata": {}, "outputs": [], "source": [ "!pip install openai -q" ] }, { "cell_type": "markdown", "id": "76b9eeb1", "metadata": {}, "source": [ "### 1.2 配置你的模型\n", "\n", "> ⚠️ **重要提醒**:API Key 相当于你账户的密码。**不要**把含 Key 的 Notebook 分享给他人!\n", "> 每次重新打开 Notebook 需要重新填入 Key。\n", "\n", "选择任一支持 OpenAI 兼容接口的模型,修改下方配置后运行。\n", "\n", "| Provider | base_url | 免费额度 |\n", "|----------|----------|---------|\n", "| 智谱 | `https://open.bigmodel.cn/api/paas/v4` | ✅ 有 |\n", "| DeepSeek | `https://api.deepseek.com` | ✅ 有 |\n", "| Qwen(阿里) | `https://dashscope.aliyuncs.com/compatible-mode/v1` | ✅ 有 |\n", "| OpenAI | `https://api.openai.com/v1` | ❌ 付费 |\n", "| 其他兼容接口 | 问你用的提供商 | - |" ] }, { "cell_type": "code", "execution_count": null, "id": "ab20e527", "metadata": {}, "outputs": [], "source": "# 👇 改下面三行为你自己的配置\nAPI_KEY = \"********************************\"\nBASE_URL = \"https://open.bigmodel.cn/api/paas/v4\" # 改成你用的服务商地址\nMODEL = \"glm-4.5-air\" # 改成你用的模型名\n\nprint(\"✅ 配置已保存(请运行下一个 Cell 验证连接)\")" }, { "cell_type": "markdown", "id": "c4fe72d0", "metadata": {}, "source": [ "### 1.3 验证模型连接\n", "\n", "发送一条测试消息,确认 API 可用。" ] }, { "cell_type": "code", "execution_count": 2, "id": "730a4b25", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "⚠️ 模型返回 content 为空(),但 API 调用本身成功\n", " 模型信息: glm-4.5-air\n", " 这通常不影响后续 function calling 功能\n" ] } ], "source": [ "from openai import OpenAI\n", "\n", "client = OpenAI(api_key=API_KEY, base_url=BASE_URL)\n", "\n", "try:\n", " response = client.chat.completions.create(\n", " model=MODEL,\n", " messages=[{\"role\": \"user\", \"content\": \"你好,请回复'连接成功'\"}],\n", " max_tokens=50\n", " )\n", " msg = response.choices[0].message\n", " # 检查 content 是否为 None(某些 provider 在 function calling 模式下不返回 content)\n", " reply = msg.content\n", " if reply is None or reply.strip() == \"\":\n", " print(f\"⚠️ 模型返回 content 为空({type(reply)}),但 API 调用本身成功\")\n", " print(f\" 模型信息: {response.model}\")\n", " print(f\" 这通常不影响后续 function calling 功能\")\n", " else:\n", " print(f\"✅ 连接成功!模型回复:{reply}\")\n", " print(f\" 使用模型:{response.model}\")\n", "except Exception as e:\n", " print(f\"❌ 连接失败:{e}\")\n", " print(f\" 请检查:\")\n", " print(f\" 1) API Key 是否正确\")\n", " print(f\" 2) BASE_URL 是否正确(注意末尾不要带 /chat/completions)\")\n", " print(f\" 3) MODEL 名称是否正确\")" ] }, { "cell_type": "markdown", "id": "6460c24a", "metadata": {}, "source": [ "---\n", "\n", "## Part 2:先感受!Agent 能做什么\n", "\n", "> 下面这个 Cell 定义了完整的智能体。**先点运行,不用管里面是什么代码**——我们会在 Part 3 逐行拆解。\n", "> \n", "> 运行成功后,你会看到 Agent 已经准备好,等待你的指令。" ] }, { "cell_type": "code", "execution_count": 3, "id": "4a85a9a4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Agent 初始化完成!试试下面的示例吧 👇\n" ] } ], "source": [ "import json\n", "import os\n", "from pathlib import Path\n", "\n", "# ═══════════════════════════════════════════\n", "# Agent 完整定义(点击左侧三角展开查看)\n", "# ═══════════════════════════════════════════\n", "\n", "# 路径工具\n", "def resolve_path(path_str: str) -> Path:\n", " \"\"\"相对路径转绝对路径\"\"\"\n", " p = Path(path_str).expanduser()\n", " return p.resolve() if not p.is_absolute() else p\n", "\n", "# 工具 1:读取文件\n", "def read_file(path: str) -> dict:\n", " \"\"\"读取文件的完整内容\"\"\"\n", " full_path = resolve_path(path)\n", " content = full_path.read_text(encoding=\"utf-8\")\n", " return {\"file_path\": str(full_path), \"content\": content}\n", "\n", "# 工具 2:列出目录\n", "def list_files(path: str) -> dict:\n", " \"\"\"列出目录中的文件和子目录\"\"\"\n", " full_path = resolve_path(path)\n", " items = []\n", " for item in sorted(full_path.iterdir()):\n", " items.append({\"filename\": item.name, \"type\": \"file\" if item.is_file() else \"dir\"})\n", " return {\"path\": str(full_path), \"files\": items}\n", "\n", "# 工具 3:编辑文件\n", "def edit_file(path: str, old_str: str, new_str: str) -> dict:\n", " \"\"\"编辑文件:old_str为空则创建,非空则替换首次出现\"\"\"\n", " full_path = resolve_path(path)\n", " if old_str == \"\":\n", " full_path.write_text(new_str, encoding=\"utf-8\")\n", " return {\"path\": str(full_path), \"action\": \"created_file\"}\n", " original = full_path.read_text(encoding=\"utf-8\")\n", " if old_str not in original:\n", " return {\"path\": str(full_path), \"action\": \"old_str not found\"}\n", " edited = original.replace(old_str, new_str, 1)\n", " full_path.write_text(edited, encoding=\"utf-8\")\n", " return {\"path\": str(full_path), \"action\": \"edited\"}\n", "\n", "# 工具注册表\n", "TOOL_REGISTRY = {\n", " \"read_file\": read_file,\n", " \"list_files\": list_files,\n", " \"edit_file\": edit_file,\n", "}\n", "\n", "# 工具 JSON Schema(LLM 的「菜单」)\n", "TOOLS = [\n", " {\n", " \"type\": \"function\",\n", " \"function\": {\n", " \"name\": \"read_file\",\n", " \"description\": \"读取文件的完整内容\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\"path\": {\"type\": \"string\", \"description\": \"文件路径\"}},\n", " \"required\": [\"path\"]\n", " }\n", " }\n", " },\n", " {\n", " \"type\": \"function\",\n", " \"function\": {\n", " \"name\": \"list_files\",\n", " \"description\": \"列出目录中的文件和子目录\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\"path\": {\"type\": \"string\", \"description\": \"目录路径\"}},\n", " \"required\": [\"path\"]\n", " }\n", " }\n", " },\n", " {\n", " \"type\": \"function\",\n", " \"function\": {\n", " \"name\": \"edit_file\",\n", " \"description\": \"编辑文件:old_str为空则创建文件;否则替换首次出现的old_str\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\n", " \"path\": {\"type\": \"string\", \"description\": \"文件路径\"},\n", " \"old_str\": {\"type\": \"string\", \"description\": \"要替换的文本,空字符串=创建新文件\"},\n", " \"new_str\": {\"type\": \"string\", \"description\": \"替换后的文本\"}\n", " },\n", " \"required\": [\"path\", \"old_str\", \"new_str\"]\n", " }\n", " }\n", " }\n", "]\n", "\n", "# 系统提示词\n", "SYSTEM_PROMPT = \"\"\"你是一个编程助手智能体。你可以使用以下工具:\n", "\n", " • read_file:读取文件的完整内容\n", " • list_files:列出目录中的文件和子目录\n", " • edit_file:编辑文件(old_str为空则创建,非空则替换)\n", "\n", "使用规则:\n", "1. 先判断是否需要工具\n", "2. 需要时直接发起工具调用\n", "3. 收到结果后继续推理或回复\n", "4. 创建文件时 old_str 传空字符串\"\"\n", "5. 修改文件前,先 read_file 了解当前内容\n", "\"\"\"\n", "\n", "# ── SimpleAgent 类 ────────────────────────────\n", "class SimpleAgent:\n", " \"\"\"最小 AI 编程智能体\"\"\"\n", " \n", " def __init__(self, api_key, base_url, model):\n", " self.client = OpenAI(api_key=api_key, base_url=base_url)\n", " self.model = model\n", " self.conversation = [{\"role\": \"system\", \"content\": SYSTEM_PROMPT}]\n", " \n", " def chat(self, user_message):\n", " \"\"\"处理一条用户消息,运行完整的 Agent Loop\"\"\"\n", " print(f\"📝 用户: {user_message}\")\n", " self.conversation.append({\"role\": \"user\", \"content\": user_message})\n", " \n", " while True: # ← 内层循环\n", " response = self.client.chat.completions.create(\n", " model=self.model,\n", " messages=self.conversation,\n", " tools=TOOLS,\n", " tool_choice=\"auto\",\n", " )\n", " msg = response.choices[0].message\n", " \n", " # 没有 tool_calls → LLM 直接回复,结束内层循环\n", " if not msg.tool_calls:\n", " self.conversation.append({\"role\": \"assistant\", \"content\": msg.content})\n", " print(f\"🤖 Agent: {msg.content or '(空消息)'}\\n\")\n", " return msg.content\n", " \n", " # 有 tool_calls → 执行工具\n", " self.conversation.append({\n", " \"role\": \"assistant\",\n", " \"content\": msg.content,\n", " \"tool_calls\": [{\n", " \"id\": tc.id,\n", " \"type\": \"function\",\n", " \"function\": {\"name\": tc.function.name, \"arguments\": tc.function.arguments}\n", " } for tc in msg.tool_calls]\n", " })\n", " \n", " # 逐个执行工具调用\n", " for tc in msg.tool_calls:\n", " tool_name = tc.function.name\n", " tool_args = json.loads(tc.function.arguments)\n", " tool_fn = TOOL_REGISTRY.get(tool_name)\n", " \n", " print(f\" 🔧 调用工具: {tool_name}({json.dumps(tool_args, ensure_ascii=False)})\")\n", " \n", " if tool_fn is None:\n", " result = {\"error\": f\"未知工具: {tool_name}\"}\n", " else:\n", " try:\n", " result = tool_fn(**tool_args)\n", " except Exception as e:\n", " result = {\"error\": str(e)}\n", " \n", " preview = json.dumps(result, ensure_ascii=False)\n", " print(f\" 📋 工具结果: {preview[:120]}{'...' if len(preview) > 120 else ''}\")\n", " \n", " self.conversation.append({\n", " \"role\": \"tool\",\n", " \"tool_call_id\": tc.id,\n", " \"content\": json.dumps(result, ensure_ascii=False)\n", " })\n", " # 继续内层循环,让 LLM 处理工具结果\n", "\n", "# 初始化 Agent\n", "agent = SimpleAgent(API_KEY, BASE_URL, MODEL)\n", "print(\"✅ Agent 初始化完成!试试下面的示例吧 👇\")" ] }, { "cell_type": "markdown", "id": "e5eac904", "metadata": {}, "source": [ "### 示例 1:创建文件\n", "\n", "让 Agent 创建一个 `hello.py`。注意观察 `edit_file` 工具被调用时 `old_str` 为空。" ] }, { "cell_type": "code", "execution_count": 4, "id": "f7da918a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📝 用户: 创建一个文件 hello.py,内容为打印 'hello ,小帅' 的 Python 代码\n", " 🔧 调用工具: edit_file({\"path\": \"hello.py\", \"old_str\": \"\", \"new_str\": \"print('hello ,小帅')\"})\n", " 📋 工具结果: {\"path\": \"D:\\\\00_mywork\\\\15_agent_loop_cc\\\\hello.py\", \"action\": \"created_file\"}\n", "🤖 Agent: \n", "已成功创建 hello.py 文件,文件内容为:\n", "```python\n", "print('hello ,小帅')\n", "```\n", "\n", "这个文件包含了您要求的 Python 代码,运行后会打印出 \"hello ,小帅\"。\n", "\n" ] }, { "data": { "text/plain": [ "'\\n已成功创建 hello.py 文件,文件内容为:\\n```python\\nprint(\\'hello ,小帅\\')\\n```\\n\\n这个文件包含了您要求的 Python 代码,运行后会打印出 \"hello ,小帅\"。'" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "agent.chat(\"创建一个文件 hello.py,内容为打印 'hello ,小帅' 的 Python 代码\")" ] }, { "cell_type": "markdown", "id": "cecb7420", "metadata": {}, "source": [ "### 示例 2:修改文件(重点观察!)\n", "\n", "让 Agent 给 `hello.py` 添加一个函数。**仔细观察输出**:Agent 会**自动先读文件,再修改**——这个「先读后改」的决策完全是 LLM 自己做出的!" ] }, { "cell_type": "code", "execution_count": 7, "id": "e8613167", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📝 用户: 给 hello.py 添加一个加法函数 add(a, b),返回两数之和\n", " 🔧 调用工具: read_file({\"path\": \"hello.py\"})\n", " 📋 工具结果: {\"file_path\": \"D:\\\\00_mywork\\\\15_agent_loop_cc\\\\hello.py\", \"content\": \"print('hello ,小帅')\"}\n", " 🔧 调用工具: edit_file({\"path\": \"hello.py\", \"old_str\": \"print('hello ,小帅')\", \"new_str\": \"def add(a, b):\\n return a + b\\n\\nprint('hello ,小帅')\"})\n", " 📋 工具结果: {\"path\": \"D:\\\\00_mywork\\\\15_agent_loop_cc\\\\hello.py\", \"action\": \"edited\"}\n", " 🔧 调用工具: read_file({\"path\": \"hello.py\"})\n", " 📋 工具结果: {\"file_path\": \"D:\\\\00_mywork\\\\15_agent_loop_cc\\\\hello.py\", \"content\": \"def add(a, b):\\n return a + b\\n\\nprint('hello ...\n", "🤖 Agent: \n", "已成功为 hello.py 添加了 add(a, b) 函数。现在文件内容为:\n", "\n", "```python\n", "def add(a, b):\n", " return a + b\n", "\n", "print('hello ,小帅')\n", "```\n", "\n", "这个 add 函数可以接收两个参数 a 和 b,并返回它们的和。\n", "\n" ] }, { "data": { "text/plain": [ "\"\\n已成功为 hello.py 添加了 add(a, b) 函数。现在文件内容为:\\n\\n```python\\ndef add(a, b):\\n return a + b\\n\\nprint('hello ,小帅')\\n```\\n\\n这个 add 函数可以接收两个参数 a 和 b,并返回它们的和。\"" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "agent.chat(\"给 hello.py 添加一个加法函数 add(a, b),返回两数之和\")" ] }, { "cell_type": "markdown", "id": "357caf33", "metadata": {}, "source": [ "👀 **查看左侧文件树**,双击 `hello.py` 看看内容是否已经更新!" ] }, { "cell_type": "markdown", "id": "878e06ff", "metadata": {}, "source": [ "### 示例 3:列出目录\n", "\n", "让 Agent 看看当前目录有哪些文件。" ] }, { "cell_type": "code", "execution_count": 5, "id": "4440668e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📝 用户: 列出当前目录的所有文件\n", " 🔧 调用工具: list_files({\"path\": \".\"})\n", " 📋 工具结果: {\"path\": \"D:\\\\00_mywork\\\\15_agent_loop_cc\", \"files\": [{\"filename\": \".env\", \"type\": \"file\"}, {\"filename\": \".env.example\",...\n", " 🔧 调用工具: read_file({\"path\": \"hello.py\"})\n", " 📋 工具结果: {\"error\": \"[Errno 2] No such file or directory: 'D:\\\\\\\\00_mywork\\\\\\\\15_agent_loop_cc\\\\\\\\hello.py'\"}\n", " 🔧 调用工具: edit_file({\"path\": \"hello.py\", \"old_str\": \"\", \"new_str\": \"print('hello ,小帅')\"})\n", " 📋 工具结果: {\"path\": \"D:\\\\00_mywork\\\\15_agent_loop_cc\\\\hello.py\", \"action\": \"created_file\"}\n", " 🔧 调用工具: list_files({\"path\": \".\"})\n", " 📋 工具结果: {\"path\": \"D:\\\\00_mywork\\\\15_agent_loop_cc\", \"files\": [{\"filename\": \".env\", \"type\": \"file\"}, {\"filename\": \".env.example\",...\n", "🤖 Agent: \n", "现在可以看到当前目录包含以下文件和目录:\n", "\n", "**文件:**\n", "- .env\n", "- .env.example\n", "- agent.ipynb\n", "- CLAUDE.md\n", "- hello.py (刚刚创建的)\n", "\n", "**目录:**\n", "- .ipynb_checkpoints\n", "- .pi\n", "- openspec\n", "- test\n", "- work\n", "\n", "hello.py 文件已经成功创建并出现在目录列表中。\n", "\n" ] }, { "data": { "text/plain": [ "'\\n现在可以看到当前目录包含以下文件和目录:\\n\\n**文件:**\\n- .env\\n- .env.example\\n- agent.ipynb\\n- CLAUDE.md\\n- hello.py (刚刚创建的)\\n\\n**目录:**\\n- .ipynb_checkpoints\\n- .pi\\n- openspec\\n- test\\n- work\\n\\nhello.py 文件已经成功创建并出现在目录列表中。'" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "agent.chat(\"列出当前目录的所有文件\")" ] }, { "cell_type": "markdown", "id": "bb18976d", "metadata": {}, "source": [ "### 示例 4:异常处理\n", "\n", "故意让 Agent 读取一个不存在的文件,观察它如何处理错误。" ] }, { "cell_type": "code", "execution_count": 9, "id": "d3133331", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📝 用户: 读取文件 not_exist_xyz.py 的内容\n", " 🔧 调用工具: read_file({\"path\": \"not_exist_xyz.py\"})\n", " 📋 工具结果: {\"error\": \"[Errno 2] No such file or directory: 'not_exist_xyz.py'\"}\n", "🤖 Agent: \n", "文件 `not_exist_xyz.py` 不存在,系统返回了错误信息:`[Errno 2] No such file or directory: 'not_exist_xyz.py'`\n", "\n", "这个文件在当前目录中不存在。\n", "\n" ] }, { "data": { "text/plain": [ "\"\\n文件 `not_exist_xyz.py` 不存在,系统返回了错误信息:`[Errno 2] No such file or directory: 'not_exist_xyz.py'`\\n\\n这个文件在当前目录中不存在。\"" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "agent.chat(\"读取文件 not_exist_xyz.py 的内容\")" ] }, { "cell_type": "markdown", "id": "02851e31", "metadata": {}, "source": [ "---\n", "\n", "## 你已经体验了 Agent 的核心能力!\n", "\n", "总结一下刚才发生了什么:\n", "\n", "| 示例 | Agent 做了什么 | 关键观察 |\n", "|------|---------------|---------|\n", "| 创建文件 | 调用 `edit_file`,`old_str` 为空 | 单次工具调用完成 |\n", "| 修改文件 | **先** `read_file`,**再** `edit_file` | LLM 自主决定了两步! |\n", "| 列出目录 | 调用 `list_files` | 返回结构化文件列表 |\n", "| 异常处理 | `read_file` 失败 → 错误传回 LLM → 友好回复 | Agent 能优雅处理错误 |\n", "\n", "> 接下来,我们逐段拆解代码,看看这个「魔法」到底是怎么实现的。" ] }, { "cell_type": "markdown", "id": "7564f300", "metadata": {}, "source": [ "## Part 3:逐段拆解 — 魔法是怎么发生的\n", "\n", "刚才你看到 Agent 能自主创建文件、修改代码、处理错误。现在我们把代码**拆成一块一块**,理解每一部分在做什么。\n", "\n", "---\n", "\n", "### 3.1 配置模型\n", "\n", "这是整个 Agent 的「发动机」。任何支持 OpenAI 兼容接口的大模型都可以用。\n", "\n", "**用生活中的概念来理解:**\n", "\n", "| 概念 | 生活类比 | 代码中的体现 |\n", "|------|---------|-------------|\n", "| `api_key` | 门禁卡,证明你有权限使用服务 | `API_KEY` |\n", "| `base_url` | 服务器地址,告诉程序去哪找模型 | `BASE_URL` |\n", "| `model` | 具体用哪个模型 | `MODEL` |" ] }, { "cell_type": "code", "execution_count": null, "id": "6adc1e7e", "metadata": {}, "outputs": [], "source": [ "# 创建 OpenAI 客户端(连接到你自己的服务商)\n", "from openai import OpenAI\n", "\n", "client = OpenAI(\n", " api_key=API_KEY, # 你的「门禁卡」\n", " base_url=BASE_URL # 你的服务商地址\n", ")\n", "\n", "# 验证一下\n", "r = client.chat.completions.create(\n", " model=MODEL,\n", " messages=[{\"role\": \"user\", \"content\": \"回复 OK\"}],\n", " max_tokens=10\n", ")\n", "print(f\"✅ 模型 {r.model} 连接正常\")\n", "print(f\" 回复:{r.choices[0].message.content}\")" ] }, { "cell_type": "markdown", "id": "4d75a31e", "metadata": {}, "source": [ "### 3.2 定义工具 — Agent 的「手和眼睛」\n", "\n", "LLM 本身只是一个文本生成器,它**不能操作文件**。所谓「AI 编程助手」,其实是:LLM 告诉你它想做什么 → 你的代码帮它做 → 把结果告诉 LLM。\n", "\n", "这三个工具就是 Agent 的「手和眼睛」。\n", "\n", "#### 工具 1:`read_file` — 眼睛\n", "\n", "读取文件内容,让 LLM「看到」代码。" ] }, { "cell_type": "code", "execution_count": 10, "id": "df432f63", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "文件路径:D:\\00_mywork\\15_agent_loop_cc\\hello.py\n", "文件内容:\n", "def add(a, b):\n", " return a + b\n", "\n", "print('hello ,小帅')\n" ] } ], "source": [ "def read_file(path: str) -> dict:\n", " \"\"\"读取文件的完整内容\"\"\"\n", " from pathlib import Path\n", " full_path = Path(path).expanduser().resolve()\n", " content = full_path.read_text(encoding=\"utf-8\")\n", " return {\"file_path\": str(full_path), \"content\": content}\n", "\n", "# 试一试\n", "result = read_file(\"hello.py\")\n", "print(f\"文件路径:{result['file_path']}\")\n", "print(f\"文件内容:\\n{result['content']}\")" ] }, { "cell_type": "markdown", "id": "e7c02de5", "metadata": {}, "source": [ "#### 工具 2:`list_files` — 导航\n", "\n", "列出目录内容,让 LLM「浏览」项目结构。" ] }, { "cell_type": "code", "execution_count": 11, "id": "f2c4ac85", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 📄 .env\n", " 📄 .env.example\n", " 📁 .ipynb_checkpoints\n", " 📁 .pi\n", " 📄 agent.ipynb\n", " 📄 agent.py\n", " 📄 build_notebook.py\n", " 📄 cells_data.json\n", " 📄 CLAUDE.md\n", " 📄 hello.py\n", " 📁 nul\n", " 📁 openspec\n", " 📄 prompt_his.md\n", " 📄 README.md\n", " 📄 requirements.txt\n", " 📁 test\n", " 📄 test_agent.py\n" ] } ], "source": [ "def list_files(path: str) -> dict:\n", " \"\"\"列出目录中的文件和子目录\"\"\"\n", " from pathlib import Path\n", " full_path = Path(path).expanduser().resolve()\n", " items = []\n", " for item in sorted(full_path.iterdir()):\n", " items.append({\n", " \"filename\": item.name,\n", " \"type\": \"file\" if item.is_file() else \"dir\"\n", " })\n", " return {\"path\": str(full_path), \"files\": items}\n", "\n", "# 试一试\n", "result = list_files(\".\")\n", "for f in result[\"files\"]:\n", " print(f\" {'📄' if f['type']=='file' else '📁'} {f['filename']}\")" ] }, { "cell_type": "markdown", "id": "39cb0b3d", "metadata": {}, "source": [ "#### 工具 3:`edit_file` — 手\n", "\n", "最复杂的工具,有**两种模式**:\n", "\n", "| 条件 | 行为 | 使用场景 |\n", "|------|------|---------|\n", "| `old_str` 为空 `\"\"` | 创建/覆盖文件 | 新建文件 |\n", "| `old_str` 非空 | 替换文件中首次出现的匹配文本 | 修改现有文件 |" ] }, { "cell_type": "code", "execution_count": null, "id": "70b38b75", "metadata": {}, "outputs": [], "source": [ "def edit_file(path: str, old_str: str, new_str: str) -> dict:\n", " \"\"\"编辑文件:old_str为空则创建,非空则替换\"\"\"\n", " from pathlib import Path\n", " full_path = Path(path).expanduser().resolve()\n", "\n", " if old_str == \"\":\n", " # 模式 1:创建新文件\n", " full_path.write_text(new_str, encoding=\"utf-8\")\n", " return {\"path\": str(full_path), \"action\": \"created_file\"}\n", "\n", " # 模式 2:替换文本\n", " original = full_path.read_text(encoding=\"utf-8\")\n", " if old_str not in original:\n", " return {\"path\": str(full_path), \"action\": \"old_str not found\"}\n", "\n", " edited = original.replace(old_str, new_str, 1)\n", " full_path.write_text(edited, encoding=\"utf-8\")\n", " return {\"path\": str(full_path), \"action\": \"edited\"}\n", "\n", "# 试一试:模式 1 创建文件\n", "result = edit_file(\"test_temp.txt\", \"\", \"这是测试内容\")\n", "print(f\"创建文件:{result}\")\n", "\n", "# 试一试:模式 2 替换文本\n", "result = edit_file(\"test_temp.txt\", \"测试\", \"演示\")\n", "print(f\"替换文本:{result}\")" ] }, { "cell_type": "markdown", "id": "97e332eb", "metadata": {}, "source": [ "### 3.3 工具注册 — 告诉 LLM「你能用什么」\n", "\n", "现在有了三个工具函数,还需要**两样东西**把工具「介绍」给 LLM:\n", "\n", "1. **`TOOL_REGISTRY`**:Python 字典,名称 → 函数映射(供代码查找和执行)\n", "2. **`TOOLS`**:JSON 数组,描述每个工具的名称、用途、参数(供 LLM 理解)\n", "\n", "`TOOLS` 是 LLM 的「菜单」,LLM 看了就知道有哪些工具可用、每个工具需要什么参数。" ] }, { "cell_type": "code", "execution_count": null, "id": "ddad90e6", "metadata": {}, "outputs": [], "source": [ "# 工具注册表:名称 → 函数\n", "TOOL_REGISTRY = {\n", " \"read_file\": read_file,\n", " \"list_files\": list_files,\n", " \"edit_file\": edit_file,\n", "}\n", "\n", "# 工具的 JSON Schema(LLM 的「菜单」)\n", "TOOLS = [\n", " {\n", " \"type\": \"function\",\n", " \"function\": {\n", " \"name\": \"read_file\",\n", " \"description\": \"读取文件的完整内容\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\"path\": {\"type\": \"string\"}},\n", " \"required\": [\"path\"]\n", " }\n", " }\n", " },\n", " {\n", " \"type\": \"function\",\n", " \"function\": {\n", " \"name\": \"list_files\",\n", " \"description\": \"列出目录中的文件和子目录\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\"path\": {\"type\": \"string\"}},\n", " \"required\": [\"path\"]\n", " }\n", " }\n", " },\n", " {\n", " \"type\": \"function\",\n", " \"function\": {\n", " \"name\": \"edit_file\",\n", " \"description\": \"编辑文件:old_str为空则创建,非空则替换\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\n", " \"path\": {\"type\": \"string\"},\n", " \"old_str\": {\"type\": \"string\"},\n", " \"new_str\": {\"type\": \"string\"}\n", " },\n", " \"required\": [\"path\", \"old_str\", \"new_str\"]\n", " }\n", " }\n", " }\n", "]\n", "\n", "print(\"✅ 工具注册完成!\")\n", "print(f\" 已注册 {len(TOOLS)} 个工具:\", [t['function']['name'] for t in TOOLS])" ] }, { "cell_type": "markdown", "id": "224b8051", "metadata": {}, "source": [ "### 3.4 系统提示词 — 给 LLM 的「说明书」\n", "\n", "系统提示词是给 LLM 的角色设定和操作规则。它告诉 LLM:\n", "\n", "- **你是谁**:编程助手\n", "- **你能用什么**:三个工具\n", "- **怎么用**:先判断、再调用、有结果后继续\n", "\n", "系统提示词附加在对话历史的**最前面**,LLM 在整个对话中都会遵守这些规则。" ] }, { "cell_type": "code", "execution_count": null, "id": "7146e6ff", "metadata": {}, "outputs": [], "source": [ "SYSTEM_PROMPT = \"\"\"你是一个编程助手智能体。你可以使用以下工具:\n", "\n", " • read_file:读取文件的完整内容\n", " • list_files:列出目录中的文件和子目录\n", " • edit_file:编辑文件(old_str为空则创建,非空则替换)\n", "\n", "使用规则:\n", "1. 先判断是否需要工具\n", "2. 需要时直接发起工具调用\n", "3. 收到结果后继续推理或回复\n", "4. 创建文件时 old_str 传空字符串\"\"\n", "5. 修改文件前,先 read_file 了解当前内容\n", "\"\"\"\n", "\n", "print(\"系统提示词:\")\n", "print(SYSTEM_PROMPT)" ] }, { "cell_type": "markdown", "id": "fdef6df9", "metadata": {}, "source": [ "### 3.5 Agent Loop — 核心中的核心 ⭐\n", "\n", "这是整个智能体的「心脏」。理解了这个循环,就理解了所有 AI 编程助手的工作原理。\n", "\n", "```\n", "用户输入:\"添加一个乘法函数\"\n", " │\n", " ▼\n", "┌──────────────┐ ┌────────────────┐\n", "│ 第 1 轮 │────▶│ read_file() │\n", "│ LLM 推理 │ │ 返回文件内容 │\n", "└──────┬───────┘ └───────┬────────┘\n", " │ │\n", " │ 结果送回 LLM │\n", " ▼ │\n", "┌──────────────┐ ┌──────▼─────────┐\n", "│ 第 2 轮 │────▶│ edit_file() │\n", "│ LLM 推理 │ │ 文件修改成功 │\n", "└──────┬───────┘ └───────┬────────┘\n", " │ │\n", " │ 结果送回 LLM │\n", " ▼ │\n", "┌──────────────┐ │\n", "│ 第 3 轮 │ 无 tool_calls\n", "│ LLM 推理 │──────────────┘\n", "└──────┬───────┘\n", " │\n", " ▼\n", "\"乘法函数已添加完成!\" ← 内层循环结束\n", "\n", "回到外层循环,等待下一个用户输入\n", "```\n", "\n", "**关键理解**:\n", "- 每一轮 LLM 调用后,检查「是否还需要工具?」\n", "- 如果需要 → 执行工具 → 结果送回 → 继续内层循环\n", "- 如果不需要 → 输出回复 → 内层循环结束 → 等待用户下一条消息" ] }, { "cell_type": "code", "execution_count": null, "id": "d47b8488", "metadata": {}, "outputs": [], "source": [ "class SimpleAgent:\n", " \"\"\"最小 AI 编程智能体\"\"\"\n", " \n", " def __init__(self, api_key, base_url, model):\n", " self.client = OpenAI(api_key=api_key, base_url=base_url)\n", " self.model = model\n", " # 对话历史:以系统提示词开头\n", " self.conversation = [{\"role\": \"system\", \"content\": SYSTEM_PROMPT}]\n", " \n", " def chat(self, user_message):\n", " \"\"\"处理一条用户消息,运行完整的 Agent Loop\"\"\"\n", " self.conversation.append({\"role\": \"user\", \"content\": user_message})\n", " \n", " while True: # ← 内层循环:LLM 推理 → 工具执行 → 结果回传\n", " response = self.client.chat.completions.create(\n", " model=self.model,\n", " messages=self.conversation,\n", " tools=TOOLS,\n", " tool_choice=\"auto\",\n", " )\n", " msg = response.choices[0].message\n", " \n", " # ── 关键判断:LLM 要调用工具吗? ──\n", " if not msg.tool_calls:\n", " # 不需要工具 → 直接回复用户 → 结束内层循环\n", " self.conversation.append({\"role\": \"assistant\", \"content\": msg.content})\n", " return msg.content\n", " \n", " # 需要工具 → 追加 assistant 消息(含 tool_calls)\n", " self.conversation.append({\n", " \"role\": \"assistant\",\n", " \"content\": msg.content,\n", " \"tool_calls\": [{\n", " \"id\": tc.id, \"type\": \"function\",\n", " \"function\": {\"name\": tc.function.name, \"arguments\": tc.function.arguments}\n", " } for tc in msg.tool_calls]\n", " })\n", " \n", " # 执行每个工具\n", " for tc in msg.tool_calls:\n", " tool_name = tc.function.name\n", " tool_args = json.loads(tc.function.arguments)\n", " tool_fn = TOOL_REGISTRY.get(tool_name)\n", " \n", " try:\n", " result = tool_fn(**tool_args)\n", " except Exception as e:\n", " result = {\"error\": str(e)}\n", " \n", " # 结果追加为 tool 角色消息(含 tool_call_id 匹配)\n", " self.conversation.append({\n", " \"role\": \"tool\",\n", " \"tool_call_id\": tc.id,\n", " \"content\": json.dumps(result, ensure_ascii=False)\n", " })\n", " # 循环继续,让 LLM 处理工具结果\n", "\n", "print(\"✅ SimpleAgent 类已定义\")\n", "print(\" 核心就一个 chat() 方法 + 一个 while True 内层循环\")" ] }, { "cell_type": "markdown", "id": "9d7fef7e", "metadata": {}, "source": [ "### Part 3 小结\n", "\n", "恭喜!你已经理解了 Agent Loop 的全部组成部分:\n", "\n", "| 组件 | 作用 | 代码量 |\n", "|------|------|--------|\n", "| 模型配置 | 连接 LLM(发动机) | ~5 行 |\n", "| 三个工具 | Agent 的「手和眼睛」 | ~30 行 |\n", "| 工具注册 | 告诉 LLM 能做什么(菜单) | ~40 行 |\n", "| 系统提示词 | LLM 的角色说明书 | ~10 行 |\n", "| **Agent Loop** | **核心循环:推理→执行→回传** | **~30 行** |\n", "\n", "**核心公式:**\n", "> LLM 决定要什么工具 → 代码执行 → 结果送回 → 继续循环 → 直到 LLM 不再要工具" ] }, { "cell_type": "markdown", "id": "6917be86", "metadata": {}, "source": [ "---\n", "\n", "## Part 4:🎉 自由探索\n", "\n", "现在你已经理解了原理——试试你自己的任务吧!\n", "\n", "Agent 保留了之前的对话上下文(还记得 hello.py),你可以继续操作,也可以做全新的任务。" ] }, { "cell_type": "code", "execution_count": null, "id": "78f18286", "metadata": {}, "outputs": [], "source": [ "# 🎉 自由探索模式\n", "# 输入任何编程任务,观察 Agent 的工作过程\n", "# 输入 'exit' 退出\n", "\n", "while True:\n", " task = input(\"\\n👉 你的任务(输入 exit 退出): \").strip()\n", " if task.lower() == \"exit\" or task == \"\":\n", " break\n", " agent.chat(task)\n", "\n", "print(\"👋 探索结束!翻到下一个 Cell 查看总结。\")" ] }, { "cell_type": "markdown", "id": "de775269", "metadata": {}, "source": [ "## 📝 总结:你学到了什么\n", "\n", "### Agent Loop = 一个循环\n", "\n", "```python\n", "while True: # 对每条用户消息\n", " response = LLM(messages, tools) # 调用 LLM\n", " if 没有 tool_calls: # 不需要工具?\n", " print(response) # 直接回复\n", " break # 等下一句\n", " 执行所有工具调用() # 需要工具 → 执行\n", " 结果追加到 messages # 结果送回去\n", " # 再问一次 LLM\n", "```\n", "\n", "### 为什么这个模式如此强大?\n", "\n", "1. **LLM 自主决策**:代码没有写死「修改前先读文件」,LLM 自己决定的\n", "2. **工具可扩展**:加新工具只需定义函数 + 注册,LLM 自动学会使用\n", "3. **错误自恢复**:工具执行失败 → 错误回传 → LLM 调整策略\n", "4. **上下文连续**:对话历史累积,LLM 记住之前的所有操作\n", "\n", "### 这 150 行代码 = Claude / Cursor / Copilot 的核心\n", "\n", "生产级 AI 编程助手多出来的:\n", "- 更多工具(bash、grep、web search...)\n", "- 更好的错误处理(模糊匹配、自动重试...)\n", "- 流式输出、UI 界面...\n", "- 上下文管理(长对话的裁剪和摘要...)\n", "\n", "但 **核心循环一模一样**。\n", "\n", "### 延伸学习\n", "\n", "- 📄 [原文: The Emperor Has No Clothes](https://www.mihaileric.com/The-Emperor-Has-No-Clothes/)\n", "- 🧪 [GitHub: 完整源码(~200行)](https://shorturl.at/HmMeI)\n", "- 🔧 练习:试试给 Agent 添加一个新工具(比如 `grep` 搜索代码)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.7" } }, "nbformat": 4, "nbformat_minor": 5 }