🚀 模块3:首个极简 AI Agent 实战

3.1 我们要做什么?

开发一个「自主问答 + 简单任务执行」极简 Agent,命名为 MiniAgent

MiniAgent 的能力清单

┌──────────────────────────────────────────────────┐ │ MiniAgent 架构(极简版) │ │ │ │ 用户输入 │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ 任务分类器 │ ← 用 Claude 判断意图 │ │ └──────┬───────────┘ │ │ │ │ │ ┌────┴────┐ │ │ ▼ ▼ │ │ 简单问答 需要工具 │ │ │ │ │ │ ▼ ▼ │ │ 直接回复 选择工具 ─► 执行 ─► 获取结果 │ │ (计算器/时间/读文件) │ │ │ │ │ │ └────┬────┘ │ │ ▼ │ │ 格式化输出给用户 │ └──────────────────────────────────────────────────┘

3.2 完整可运行代码

在项目目录 ~/ai-agent-study/ 下创建 mini_agent.py

#!/usr/bin/env python3
"""
MiniAgent — 你的第一个 AI Agent
=================================
功能:
  1. 接收用户输入
  2. 自主判断:简单问答 vs 需要执行任务
  3. 问答直接回复;任务调用工具执行
  4. 返回执行结果

依赖:anthropic, python-dotenv, requests
运行:python3 mini_agent.py
"""

# ============================================================
# 第1部分:导入依赖与配置
# ============================================================
import os
import sys
import json
from datetime import datetime
from pathlib import Path

from dotenv import load_dotenv
from anthropic import Anthropic

# 加载配置
load_dotenv()

# ============================================================
# 第2部分:Agent 类定义
# ============================================================
class MiniAgent:
    """
    极简 AI Agent:
    - brain: Claude 大模型(负责"思考")
    - tools: 工具字典(负责"执行")
    - memory: 对话历史列表(负责"记忆")
    """

    def __init__(self, name: str = "MiniAgent"):
        """初始化 Agent:设置大脑、记忆、工具"""
        self.name = name

        # 🧠 大脑:连接 Claude API
        self.client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
        self.model = "claude-sonnet-4-6"  # 性价比最优

        # 📝 记忆:存储对话历史(短期记忆)
        self.memory = []

        # 🔧 工具注册:Agent 可以使用的工具
        self.tools = {
            "calculator": self._tool_calculator,
            "get_time": self._tool_get_time,
            "read_file": self._tool_read_file,
        }

        print(f"🤖 {self.name} 初始化完成!")

    # ============================================================
    # 第3部分:工具定义(Agent 的"双手")
    # ============================================================

    def _tool_calculator(self, expression: str) -> str:
        """
        🧮 计算器工具:安全地计算数学表达式
        输入:数学表达式字符串,如 "2 + 3 * 4"
        输出:计算结果
        """
        try:
            # ⚠️ 安全警告:eval 只能用于学习,生产环境必须用更安全的方式
            # 这里限制只允许数字和基本运算符
            allowed = set("0123456789+-*/().% ")
            if not all(c in allowed for c in expression):
                return "❌ 表达式包含不允许的字符,仅支持基本数学运算"
            result = eval(expression)
            return f"✅ 计算结果: {expression} = {result}"
        except Exception as e:
            return f"❌ 计算出错: {str(e)}"

    def _tool_get_time(self, _: str = "") -> str:
        """
        🕐 时间查询工具:获取当前日期时间
        输入:忽略(不需要参数)
        输出:当前时间的格式化字符串
        """
        now = datetime.now()
        weekday_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
        wd = weekday_names[now.weekday()]
        return f"🕐 当前时间: {now.strftime('%Y年%m月%d日')} {wd} {now.strftime('%H:%M:%S')}"

    def _tool_read_file(self, filepath: str) -> str:
        """
        📄 文件读取工具:读取指定文件内容
        输入:文件路径
        输出:文件内容(最多2000字符)
        """
        path = Path(filepath)
        if not path.exists():
            return f"❌ 文件不存在: {filepath}"
        try:
            content = path.read_text(encoding="utf-8")
            if len(content) > 2000:
                content = content[:2000] + "\n... (内容过长,已截断)"
            return f"📄 文件内容 ({filepath}):\n{content}"
        except Exception as e:
            return f"❌ 读取文件出错: {str(e)}"

    # ============================================================
    # 第4部分:工具选择逻辑
    # ============================================================

    def _get_tools_description(self) -> str:
        """生成工具描述,供 Claude 理解可用工具有哪些"""
        return """
可用工具列表:
1. calculator — 计算数学表达式,参数: expression (如 "2+3*4")
2. get_time — 获取当前日期和时间,无需参数
3. read_file — 读取文件内容,参数: filepath (文件路径)

如果用户的问题需要计算、查时间、读文件,你必须输出如下格式的JSON来调用工具:
{"tool": "工具名", "args": {"参数名": "参数值"}}

如果用户的问题不需要工具(普通问答),直接回复即可,不要输出JSON。
"""

    # ============================================================
    # 第5部分:核心运行循环
    # ============================================================

    def run(self, user_input: str) -> str:
        """
        Agent 的核心运行方法:
        1. 让 Claude 分析用户输入
        2. 判断需要直接回复还是调用工具
        3. 如需工具,则调用工具并返回结果
        """
        print(f"\n{'='*50}")
        print(f"👤 用户: {user_input}")
        print(f"{'='*50}")

        # --- 步骤1:构建给 Claude 的消息 ---
        system_prompt = f"""你是 {self.name},一个智能助手。你的职责是帮助用户解决问题。

{self._get_tools_description()}

重要规则:
- 如果用户问当前时间、需要计算、或要求读取文件,你必须调用工具
- 调用工具时,只输出JSON,不要输出其他内容
- 如果只是普通问答,直接友好地回复
- 回复使用中文"""

        messages = [{"role": "user", "content": user_input}]

        # 如果有历史对话,也加入(让 Agent 有"记忆")
        if self.memory:
            # 只保留最近5轮对话,防止上下文过长
            recent = self.memory[-5:]
            full_messages = []
            for turn in recent:
                full_messages.append({"role": "user", "content": turn["user"]})
                full_messages.append({"role": "assistant", "content": turn["assistant"]})
            full_messages.append({"role": "user", "content": user_input})
            messages = full_messages

        # --- 步骤2:调用 Claude 获取响应 ---
        try:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=1024,
                system=system_prompt,
                messages=messages,
                temperature=0.3,  # 低温度让输出更稳定
            )
            reply = response.content[0].text.strip()
        except Exception as e:
            return f"❌ Agent 调用失败: {str(e)}"

        # --- 步骤3:判断响应是工具调用还是直接回复 ---
        result = self._process_response(reply, user_input)

        # --- 步骤4:保存到记忆 ---
        self.memory.append({
            "user": user_input,
            "assistant": result,
            "timestamp": datetime.now().isoformat()
        })

        # 控制记忆长度(最多保留20轮)
        if len(self.memory) > 20:
            self.memory = self.memory[-20:]

        print(f"🤖 {self.name}: {result}")
        return result

    def _process_response(self, reply: str, user_input: str) -> str:
        """
        处理 Claude 的响应:
        - 如果是 JSON 工具调用 → 执行工具 → 返回结果
        - 如果是普通文本 → 直接返回
        """
        # 尝试解析 JSON(工具调用)
        try:
            # 提取 JSON 部分(可能被包裹在代码块中)
            json_str = reply
            if "```json" in reply:
                json_str = reply.split("```json")[1].split("```")[0]
            elif "```" in reply:
                json_str = reply.split("```")[1].split("```")[0]

            tool_call = json.loads(json_str.strip())

            if "tool" in tool_call:
                tool_name = tool_call["tool"]
                args = tool_call.get("args", {})

                print(f"🔧 调用工具: {tool_name}")
                print(f"📥 参数: {args}")

                # 查找并执行工具
                if tool_name in self.tools:
                    tool_func = self.tools[tool_name]
                    # 工具函数的第一个参数是主要参数
                    first_arg = list(args.values())[0] if args else ""
                    tool_result = tool_func(first_arg)
                    return tool_result
                else:
                    return f"❌ 未知工具: {tool_name},可用工具: {list(self.tools.keys())}"
        except (json.JSONDecodeError, KeyError, IndexError):
            pass  # 不是工具调用,按普通回复处理

        # 普通回复,直接返回
        return reply


# ============================================================
# 第6部分:交互式运行入口
# ============================================================
def main():
    """Agent 交互式主循环"""
    print("=" * 50)
    print("🤖 MiniAgent — 你的第一个 AI Agent")
    print("=" * 50)
    print("支持功能:")
    print("  • 普通问答(任何问题)")
    print("  • 数学计算(如 '帮我算 156 * 23 + 89')")
    print("  • 时间查询(如 '现在几点了?')")
    print("  • 文件读取(如 '读取 test.txt 的内容')")
    print("  • 输入 'quit' 或 'exit' 退出")
    print("=" * 50)

    # 检查 API 密钥
    if not os.getenv("ANTHROPIC_API_KEY"):
        print("❌ 错误:未找到 ANTHROPIC_API_KEY!")
        print("   请在 .env 文件中配置你的 API 密钥")
        sys.exit(1)

    # 创建 Agent 实例
    agent = MiniAgent()

    # 创建测试文件(用于演示文件读取功能)
    test_file = Path("test.txt")
    if not test_file.exists():
        test_file.write_text(
            "你好!这是一个测试文件。\n"
            "AI Agent 可以读取这个文件的内容。\n"
            "试试输入:读取 test.txt 的内容\n",
            encoding="utf-8"
        )
        print(f"📝 已创建测试文件: {test_file.absolute()}")

    print("\n开始对话吧!\n")

    # 主循环
    while True:
        try:
            user_input = input("👤 你: ").strip()
            if not user_input:
                continue
            if user_input.lower() in ("quit", "exit", "q"):
                print(f"\n👋 {agent.name} 已退出。再见!")
                break

            agent.run(user_input)

        except KeyboardInterrupt:
            print(f"\n\n👋 {agent.name} 已中断。再见!")
            break
        except Exception as e:
            print(f"❌ 运行出错: {e}")

    # 显示会话统计
    print(f"\n📊 本次会话统计:共 {len(agent.memory)} 轮对话")


if __name__ == "__main__":
    main()

3.3 逐行代码讲解

第1部分:导入与配置(第1-22行)

导入所需的 Python 库。核心是 anthropic(连接 Claude API)和 python-dotenv(从 .env 文件加载密钥)。from anthropic import Anthropic 创建 API 客户端。

第2部分:MiniAgent 类(第25-55行)

__init__ 是 Agent 的"出生证明":创建时完成3件事:

第3部分:工具定义(第58-100行)

三个工具函数就是普通的 Python 函数:

⚠️ eval() 安全注意

生产环境中绝对不能用 eval() 直接执行用户输入,这里仅为了简化示例加了字符白名单过滤。后续阶段我们会用更安全的方式。

第4部分:工具选择逻辑(第103-115行)

_get_tools_description() 生成一段"工具说明书",告诉 Claude 有哪些工具可用、每个工具的参数格式。这是 Agent 能"自主选择工具"的关键——Claude 读了这个说明书就知道什么时候该用什么工具。

第5部分:核心运行循环(第118-178行)

run() 是 Agent 的"心跳"——每次用户输入都走这个流程:

  1. 构建消息:把系统提示词 + 对话历史 + 用户新输入打包发给 Claude
  2. 调用 API:self.client.messages.create(...) 把消息发给 Claude,拿到回复
  3. 处理响应:调用 _process_response() 判断是工具调用还是直接回复
  4. 保存记忆:把本轮对话存入 self.memory

_process_response() 的核心逻辑:尝试把 Claude 的回复解析成 JSON → 如果成功且包含 "tool" 字段,就调用对应工具 → 否则当普通回复处理。

第6部分:交互入口(第181-215行)

main() 函数创建一个 Agent 实例,然后进入 while True 无限循环,不断等待用户输入 → 调用 agent.run() → 输出结果。

3.4 Claude Code 运行与调试完整流程

在 Claude Code 终端中运行

# 确保在项目目录且虚拟环境已激活
cd ~/ai-agent-study
source venv/bin/activate

# 运行 MiniAgent
python3 mini_agent.py

# 期望看到:
# ==================================================
# 🤖 MiniAgent — 你的第一个 AI Agent
# ==================================================
# 支持功能:...
# 📝 已创建测试文件: /home/user/ai-agent-study/test.txt
# 开始对话吧!
# 👤 你:

测试用例(逐个输入测试)

# 测试1:普通问答
👤 你: 你好,请用一句话介绍你自己

# 测试2:数学计算
👤 你: 帮我计算 156 * 23 + 89

# 测试3:时间查询
👤 你: 现在几点了?

# 测试4:文件读取
👤 你: 读取 test.txt 的内容

# 测试5:带记忆的对话
👤 你: 我刚才问了你什么问题?
# Agent 应该能回忆之前的对话(因为 memory 里有记录)

查看调试日志(增强版)

# 创建带详细日志的调试版本
cat > mini_agent_debug.py << 'PYEOF'
# 在原有代码基础上,在 run() 方法中添加日志
# (这里展示关键的调试代码片段)

import logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler('agent_debug.log'),
        logging.StreamHandler()
    ]
)

# 在 run() 方法的各个关键步骤添加日志:
# logging.debug(f"发送给API的消息: {messages}")
# logging.debug(f"API原始响应: {reply}")
# logging.debug(f"当前记忆长度: {len(self.memory)}")
PYEOF

echo "调试版脚本已创建,需要时可自行整合完整代码。"

3.5 运行效果解读

一次完整的工具调用流程(计算器示例)

👤 你: 帮我计算 35 * 12 + 100

=== 后台发生了什么? ===

1️⃣ Agent 构建消息发给 Claude:
   "你是 MiniAgent...可用工具:calculator、get_time、read_file..."
   "用户说:帮我计算 35 * 12 + 100"

2️⃣ Claude 分析:用户要计算 → 需要调用 calculator 工具
   Claude 返回JSON: {"tool": "calculator", "args": {"expression": "35 * 12 + 100"}}

3️⃣ Agent 解析 JSON → 找到 tool="calculator"
   → 调用 self.tools["calculator"]("35 * 12 + 100")
   → Python 计算:35*12=420, 420+100=520

4️⃣ Agent 返回结果给用户:
   🤖 MiniAgent: ✅ 计算结果: 35 * 12 + 100 = 520

5️⃣ 保存到 memory(用户问了什么 + Agent 回了什么)

3.6 高频问题答疑

Q: Agent 没有调用工具,而是直接回复了文字,怎么办?
这是最常见的问题。原因通常是 Claude 没有正确理解工具调用格式。解决方案:降低 temperature 到 0.1,并在系统提示词中更强调"必须输出 JSON 格式"。也可以给一个工具调用的示例。
Q: JSON 解析失败(JSONDecodeError),怎么处理?
Claude 有时会在 JSON 前后加文字,或者 JSON 格式不对。解决方案:在代码中增加 JSON 提取逻辑(我们的代码已经处理了 markdown 代码块的情况),以及增加重试逻辑——如果解析失败,让 Claude 再试一次。
Q: 对话历史太长,API 报错 context length exceeded?
我们的代码已经在 run() 中限制了只保留最近5轮对话(self.memory[-5:]),并且总记忆不超过20轮。如果仍然超限,可以减少到3轮或缩短每轮的文本长度。
Q: 如何在 Claude Code 中调试 Agent 的响应?
最简单的方法:在 run() 方法的 API 调用后加一行 print(f"DEBUG 原始响应: {reply}")。更规范的方式是用 Python 的 logging 模块(参考3.4节的调试版脚本)。

📝 知识点总结

知识点在本项目中的体现
Agent 大脑Anthropic() 客户端连接 Claude API
Agent 记忆self.memory 列表存储对话历史
Agent 工具self.tools 字典,函数名 → 实际函数
工具选择Claude 分析用户输入 → 输出 JSON → Python 解析 → 执行
自主闭环用户输入 → Claude思考 → 选择工具(或直接回复) → 执行 → 返回结果 → 记忆留存

✏️ 课后练习

  1. 运行调试:运行 MiniAgent,完成上述5个测试用例,确认每个都能正常工作。
  2. 添加工具:给 MiniAgent 添加一个新工具 word_count——输入一段文本,返回字数统计。提示:在 self.tools 字典中添加新条目,实现对应的函数。
  3. 功能增强:修改 _process_response,增加 JSON 解析失败时的重试逻辑(让 Agent 重新生成)。
  4. 思考题:如果用户说"帮我计算 100除以3,再帮我看看现在几点",Agent 目前能处理吗?如果不能,需要怎样改进?(提示:多工具调用、多步规划——第二阶段会学)
  5. 创意扩展:修改 Agent 的名字和系统提示词,把它变成"专属英语老师"——只回答英语学习相关问题,其他问题礼貌拒绝。
← 模块2:环境搭建 进入第二阶段 →